/* Base, layout chrome and shared components: reset, fixed 3D canvas, text reveal, header,
   menu drawer and contact form, buttons, footer layer, cursor, hero, scroll progress.
   Section layouts live in sections.css; tokens in tokens.css; type scale in typography.css. */

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

html {
  scroll-behavior: smooth;
  /* Hide the native scrollbar (scrolling still works) — the site has its own
     .scroll-progress indicator riding the right edge */
  scrollbar-width: none; /* Firefox */
}

/* Chromium / WebKit */
html::-webkit-scrollbar {
  display: none;
}

::selection {
  background: rgba(255, 255, 255, 0);
  color: var(--ink);
}

body {
  background: var(--scene-bg); /* the scene's colour, so the load shows no white flash before the canvas paints */
}

img,
svg {
  display: block;
  max-width: 100%;
}

/* Kept for readers and search, removed from the layout */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  border: 0;
  margin: -1px; /* so a 1px box never opens a gap in a flex/grid parent */
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%); /* clip: is deprecated; keep both while it is still honoured */
  white-space: nowrap;
}

/* WCAG 2.4.7 / 2.4.11: one visible focus ring for the whole page, 2px of --ink (18.4:1 on
   the white sheet, 12.7:1 on the scene ground, past the 3:1 minimum). Components needing a
   different offset (.player, .project) restate it; nothing removes it. :focus-visible only,
   so a pointer press never leaves a ring behind. */
:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}

/* 2.4.1 Bypass Blocks: off-screen until it takes focus, then a plain white chip in the
   top-left corner, over the fixed header (--z-header 100). */
.skip-link {
  position: fixed;
  top: 8px;
  left: 8px;
  z-index: calc(var(--z-menu) + 1);
  padding: 10px 18px;
  border-radius: var(--r-pill);
  background: var(--white);
  box-shadow: var(--shadow-btn-hover);
  color: var(--ink);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  text-decoration: none;
  transform: translateY(-160%);
  transition: transform var(--dur-fast) var(--ease-out);
}

/* :focus as well as :focus-visible -- a screen reader moving its own caret here can focus
   the link without the browser calling it a "visible" focus, and then the chip would stay
   off-screen while it is being read out. */
.skip-link:focus,
.skip-link:focus-visible { transform: translateY(0); }

@media (prefers-reduced-motion: reduce) {
  .skip-link { transition: none; }
}

/* <main> only carries tabindex so the skip link can land the caret on it; it is not a
   control and must not draw a ring of its own. */
.page-content:focus,
.page-content:focus-visible { outline: none; }

button {
  font: inherit;
  color: inherit;
}

#canvas-container {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  z-index: var(--z-canvas);
}

#canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* Header is static on load (no entrance); hero text reveals line by line (.rv) and the
   overlay card rises as the last item of that sequence */

/* Text queued for the line reveal stays invisible until landing-reveal.js has split and armed it.
   The html.js rule catches the window before the module runs (first paint → ~300ms),
   .rv-pending covers the rest; keep the list in sync with SELECTOR in landing-reveal.js. */
html.js :is(
  .page-content h1, .page-content h2, .page-content h3, .page-content p,
  .page-content .eyebrow, .page-content .h4, .page-content .label,
  .page-content .caption, .page-content .caption-xl, .page-content .small,
  .page-content .chip,
  .page-content .partner-logos,
  .page-content .hero-actions, .page-content .player, .page-content .cta-act,
  .footer-layer p, .footer-layer .label, .footer-layer .small,
  .footer-layer .footer-col a, .footer-layer .footer-bottom > span
):not(.rv):not(.quote-typewriter):not(.why-lines *):not(.what-card *):not(.visually-hidden),
.rv-pending:not(.visually-hidden) {
  visibility: hidden;
}
/* (:not(.visually-hidden): the off-screen section headings are already out of the layout
   and must stay in the a11y tree -- visibility:hidden would take them out of it, and they
   have no rendered line boxes for landing-reveal.js to split into masks either. SKIP there keeps
   them out of the reveal for the same reason.) */

/* Accent words in headings (<em> in the markup; the splitters tag the words instead) */
.rw-em, .w-em, .display em { font-style: italic; } /* same weight as the text around it */
/* A split word stays whole: one broken at its hyphen ("back-|end") was counted on its first
   line, and its line then ran too long and wrapped inside its mask */
.rw { white-space: nowrap; }

/* Line-by-line text reveal (ui.js splits real rendered lines into .rl > .rli). Each line
   is masked by its wrapper and slides up into place with a per-line delay; .rv-block is
   the fallback for elements with nested markup — the whole block rises once. */
.rv .rl {
  display: block;
  overflow: hidden;
  /* Keep descenders inside the mask without adding visible space */
  padding-bottom: 0.12em;
  margin-bottom: -0.12em;
}

.rv .rli {
  display: block;
  transform: translateY(var(--rise-mask));
  /* Leaving the viewport: the rise played backwards, bottom line first (--d-out) */
  transition: transform var(--dur-drop) var(--ease-in);
  transition-delay: var(--d-out, 0s);
  /* (no will-change: this matches every split line on the page -- a hundred-odd elements --
     and each one animates once on entry and then sits still; promoting them all for the
     life of the page costs GPU memory for nothing) */
}

.rv.is-in .rli {
  transform: translateY(0);
  transition: transform var(--dur-reveal) var(--ease-out);
  transition-delay: var(--d-in, 0s);
}

/* Scroll-out (hero, ui.js): the text leaves UP through the mask, top line first, and comes
   back down from above when the page returns to the top */
.rv.is-in.is-out .rli {
  transform: translateY(var(--rise-mask-out));
  transition: transform var(--dur-exit) var(--ease-in);
  transition-delay: calc(var(--d-in, 0s) * var(--exit-stagger));
}

/* The first scroll releases the hand immediately; let the hero copy leave more gradually
   so the transition feels intentional, not snapped away. */
.hero .rv.is-in.is-out .rli {
  transition-duration: var(--dur-reveal);
  transition-timing-function: var(--ease-out);
}

.hero .rv.rv-block.is-in.is-out {
  transition: opacity var(--dur-reveal) ease, transform var(--dur-reveal) var(--ease-out);
}

/* Gradient display text split into lines: background-clip:text on the PARENT would paint
   the gradient through the descendants' glyphs at their layout position — ignoring their
   transform — so split text would show in place while the real lines move under the mask.
   Instead, once split, the parent gives up its fill and every line clips its own copy. */
.display.rv:not(.rv-block) {
  background-image: none;
}

/* Display sizes: the font's content box (~1.38em) overhangs the 1.2 line box, so the
   descenders need a deeper mask than body text (0.12em left ~4px on "Signature works") */
.display.rv:not(.rv-block) .rl {
  padding-bottom: 0.24em;
  margin-bottom: -0.24em;
}

.display.rv:not(.rv-block) .rli {
  background-image: var(--text-gradient);
  -webkit-background-clip: text;
  background-clip: text;
  /* background-clip: text only paints inside the line's box, and Archivo descenders reach
     its very bottom edge: extend the painted box below the baseline (no layout change) */
  padding-bottom: 0.2em;
  margin-bottom: -0.2em;
}

.rv.rv-block {
  opacity: 0;
  transform: translateY(var(--rise-block));
  transition: opacity var(--dur-slow) ease, transform var(--dur-slow) ease;
}

.rv.rv-block.is-in {
  opacity: 1;
  transform: none;
}

/* The first frame as a block takes the hidden pose at once (text-reveal.js prepareWords) */
.rv.rv-block.rv-arming { transition: none !important; }

/* A call's button (.cta-act: the landing's close, a case's closing call) is set off with its
   title and follows it by --cta-follow: the text first, then what to press (MOTION.md) */
.cta-act.rv-block.is-in:not(.is-out) { transition-delay: var(--cta-follow); }

.rv.rv-block.is-in.is-out {
  opacity: 0;
  transform: translateY(var(--rise-block-out));
}

/* The way back up (html.scroll-up, scroll-bus.js): text coming into view as the page goes
   up -- back down from above, or rising again -- comes in at once, its lines and blocks
   together (their delays x --back-stagger, 0); going down it rises in turn, as above. A rule
   of the system (19 Oct): the hero's copy, the landing's text, the presence stats, a row of
   "what we do", a service's "what's next". Leaving and dropping keep their own order: they
   do not match. (Why us's phrases have their own way back, sections.css .s-why.is-back.) */
html.scroll-up .rv.is-in:not(.is-out) .rli,
html.scroll-up .rv.rv-block.is-in:not(.is-out) {
  transition-delay: calc(var(--d-in, 0s) * var(--back-stagger));
}

/* ---- Header (Figma 1216:4695) — 72px, blurred, logo / menu / contact ---------------- */
/* Sized in --ui-head: the mock's px up to 1512, then growing a step behind the rest of the
   chrome (x1.2 at 2560; the menu drawer's --ui is x1.35), so on a big screen it keeps up
   without swelling like the headlines. The burger is an SVG on a viewBox: its bars and their moves scale with it. */

/* Header stays fixed at the top instead of travelling with the hero, so the logo and
   burger stay on hand the whole way down; it sits at body level, outside .page-content,
   so nothing further down the page can paint over it. */
header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: var(--header-h);
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: calc(14 * var(--ui-head)) calc(20 * var(--ui-head));
  z-index: var(--z-header);
}

/* Entrance: the logo, then the menu icon, slide down from above the screen on load (a
   header's height up, so each starts fully off it) -- CSS only, so they start with the
   first paint and wait for no script; the hero copy follows on its own (ui.js playHero).
   (backwards, not both: no transform is left on them afterwards; the burger's own
   transforms are on its bars.) */
header > :is(.logo, .menu-btn) {
  animation: header-in var(--dur-reveal) var(--ease-out) backwards;
}
header > .logo { animation-delay: 0.1s; }
header > .menu-btn { animation-delay: 0.25s; } /* = the scroll cue's (sp-in) */

@keyframes header-in {
  /* (+ the safe area: on a notched phone the header's items sit that much lower, and
     without it they would start half in view under the status bar; 0 elsewhere) */
  from { transform: translateY(calc(-1 * (var(--header-h) + env(safe-area-inset-top, 0px)))); }
}

@media (prefers-reduced-motion: reduce) {
  header > :is(.logo, .menu-btn) { animation: none; }
}

.logo {
  display: flex;
  align-items: center;
  gap: calc(9 * var(--ui-head));
  font-size: calc(20 * var(--ui-head));
  font-weight: 660; /* Archivo is variable (weight 200..700), so 660 is a valid custom weight */
  line-height: 0.9;
  letter-spacing: var(--track-chrome);
  color: var(--ink);
  text-decoration: none;
}

.logo img {
  width: calc(42 * var(--ui-head));
  height: calc(42 * var(--ui-head));
}

.menu-btn {
  width: calc(64 * var(--ui-head)); /* wide enough for longer bars while the right inset stays put */
  height: calc(40 * var(--ui-head));
  padding: 0;
  background: none;
  border: none;
  cursor: pointer;
  transition: opacity 0.25s ease; /* it steps out while the drawer is open (html.menu-open) */
}

.menu-btn .menu-icon {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

/* Two 51.5px bars centred at x:32/y:20; open turns them into a 25x25 cross centred in the
   box -- each bar slides to the centre line, turns 45deg and shortens to the cross's
   35.5px diagonal. */
.menu-icon-bar {
  stroke: var(--ink);
  stroke-width: 2; /* matches the hairline set; the icon's box is four times the icon size */
  stroke-linecap: butt; /* square-ended, as every line on the site */
  transform-box: view-box;
  transition: transform 0.45s var(--ease-out);
}

/* Each bar turns about its own centre, then slides to the middle line: a single shared
   origin would swing the bars' midpoints apart and cross the X off-centre */
.menu-icon-bar-1 { transform-origin: 32px 15.25px; }
.menu-icon-bar-2 { transform-origin: 32px 25.25px; }

.menu-btn:hover .menu-icon-bar-1 { transform: translateY(-1.5px); }
.menu-btn:hover .menu-icon-bar-2 { transform: translateY(1.5px); }

.menu-btn.is-open .menu-icon-bar-1 { transform: translateY(5px) rotate(45deg) scaleX(0.69); }
.menu-btn.is-open .menu-icon-bar-2 { transform: translateY(-5px) rotate(-45deg) scaleX(0.69); }

/* ---- Menu: the side drawer (design 4h) ---------------------------------------------
   A 520px panel down the right edge: a dotted globe behind its lower corner, the address
   set as two big lines, the social links, the offices' times, and at the foot who you
   would be writing to. The contact form takes the middle block's place (.menu-slot). */

.menu-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-menu);
  display: flex;
  align-items: stretch;
  justify-content: flex-end;
  /* The page behind softens while the drawer is out: the blur lives on this layer, which
     never fades (an ancestor at opacity < 1 would have nothing of the page left to blur).
     The layer takes no clicks either -- only the drawer does; a click anywhere else closes
     it (ui.js). main.js holds the 3D frame while it is open, so the blur re-samples a
     still picture instead of a running one. */
  background: transparent;
  pointer-events: none;
  visibility: hidden;
  backdrop-filter: blur(0px);
  -webkit-backdrop-filter: blur(0px);
  transition: visibility 0s linear 0.42s, backdrop-filter 0.4s ease, -webkit-backdrop-filter 0.4s ease;
}

.menu-overlay.is-open {
  visibility: visible;
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  transition: visibility 0s, backdrop-filter var(--dur-slow) ease, -webkit-backdrop-filter var(--dur-slow) ease;
}

.menu-card {
  pointer-events: auto;
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  /* Everything in the drawer is written in --ui (1px up to 1512, 1.35px at 2560): it grows
     as one piece on a big screen instead of standing at its laptop size */
  width: min(calc(480 * var(--ui)), 100%);
  height: 100%;
  padding: calc(32 * var(--ui)) calc(40 * var(--ui)) calc(22 * var(--ui));
  overflow: hidden;
  /* A very wide, faint shadow along its edge: over the white sections the blur behind the
     drawer has nothing to show and the white card would merge into the white page. Spread
     wide and pulled in (the negative spread) so it reads as the card lifting, not as a dark
     strip laid on the page. */
  box-shadow: -32px 0 120px -24px rgba(20, 20, 20, 0.14);
  background: var(--white);
  /* Its own stacking context, so the sparks' negative z-index stays inside it (below) */
  isolation: isolate;
  /* Closed: off the right edge */
  translate: 100% 0;
  transition: translate 0.42s var(--ease-in);
}

.menu-overlay.is-open .menu-card {
  translate: 0 0;
  transition: translate 0.62s var(--ease-out);
}

/* The drawer's specks (plate-sparks.js): over the card's white, under its text -- the
   negative z-index needs the card to be its own stacking context (.menu-card isolation). */
.menu-sparks {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
  pointer-events: none;
}
@media (hover: none), (prefers-reduced-motion: reduce) {
  .menu-sparks { display: none; }
}

/* The globe behind the lower corner. menu-globe.js paints it on this canvas in the site's
   own dotted language -- dots strung between points of a sphere, the offices wearing the
   callouts' ring and dot -- turning about the same tilted axis the presence globe stands on. */
.menu-globe {
  position: absolute;
  right: calc(-62 * var(--ui));
  bottom: calc(-54 * var(--ui));
  width: calc(280 * var(--ui));
  height: calc(280 * var(--ui));
  opacity: 1;
  pointer-events: none;
}

/* ---- Head: the label and the close ---- */
.menu-head {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: calc(40 * var(--ui));
  flex: none;
}

.menu-close {
  display: grid;
  place-items: center;
  /* Even whole pixels, box and icon alike: at a fractional size (15px of a 12-unit
     drawing, then x --ui) the cross's centre falls between pixels and its two strokes
     antialias differently, one arm reading heavier than the other */
  width: round(calc(32 * var(--ui)), 2px);
  height: round(calc(32 * var(--ui)), 2px);
  margin-right: calc(-8 * var(--ui));
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink);
  cursor: pointer;
  transition: transform 0.35s var(--ease-out);
}

.menu-close:hover { transform: rotate(90deg); }
.menu-close svg { display: block; width: round(calc(16 * var(--ui)), 2px); height: round(calc(16 * var(--ui)), 2px); }

/* ---- The middle: the links, or the contact form in their place ---- */
.menu-slot {
  position: relative;
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: 0;
  container-type: inline-size; /* the address sizes itself to this column (cqw) */
}

.menu-links {
  display: flex;
  flex-direction: column;
  justify-content: center;
  height: 100%;
  padding-bottom: calc(48 * var(--ui)); /* room for the legal row on the floor */
  gap: clamp(calc(20 * var(--ui)), 3.4vh, calc(36 * var(--ui)));
  transition: opacity 0.18s ease;
}

.menu-form {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-fast) ease;
}

.menu-card.is-form .menu-links { opacity: 0; pointer-events: none; }
.menu-card.is-form .menu-form { opacity: 1; pointer-events: auto; }

/* The address, set as two lines: the name in ink, the domain a shade back */
.menu-email {
  display: flex;
  flex-direction: column;
  gap: 0.1em; /* air between "hello@" and the domain: line-height 1 left them touching */
  /* It rides up close to the links: at this size the line box carries a lot of air under
     the descenders, so some of the gap is taken out of it rather than added to */
  margin-bottom: -0.06em;
  /* Set to the drawer's own width: "hengebytes.com" is 13 characters, and at this size
     that line fills the column edge to edge */
  font-size: clamp(40px, calc(14.2cqw + 2px), calc(94 * var(--ui) + 2px));
  font-weight: 300;
  line-height: 1;
  letter-spacing: -0.06em; /* tighter, so the domain still fits the column at the bigger size */
  color: var(--ink);
  text-decoration: none;
}

.menu-email .menu-rv { display: block; }
/* The top line, "hello@", in the headings' italic accent (weight 200, like the other accents) */
.menu-email .menu-rv:first-child .rli { font-style: italic; }

/* Copying the address turns this row over: the links leave up under their own masks and
   the word rises into the line they were on. They share the box, so nothing in the drawer
   moves, and the confirmation lands where the eye already is -- right under the address. */
.menu-social-row { position: relative; }

.menu-social-note {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  /* It lies over the links: without this the pointer lands on it and they stop being
     hoverable at all (the cursor's own hover test walks up from e.target) */
  pointer-events: none;
  font-size: calc(20 * var(--ui));
  font-weight: 300;
  line-height: 1.3;
  letter-spacing: var(--track-chrome);
  color: var(--ink);
}

/* At rest the word waits under its mask, and the links stand */
.menu-overlay.is-open .menu-social-row:not(.is-copied) .menu-social-note .rli {
  transform: translateY(var(--rise-mask));
}
.menu-overlay:not(.is-open) .menu-social-note .rli { transform: translateY(var(--rise-mask)); }

/* Never both on the line at once: whichever is leaving goes quickly and alone, and the
   other only starts once the line is clear. */
.menu-overlay.is-open .menu-social-row.is-copied .menu-social a .rli {
  transform: translateY(var(--rise-mask-out));
  transition: transform var(--dur-swap-out) var(--ease-in);
}

.menu-overlay.is-open .menu-social-row.is-copied .menu-social-note .rli {
  transform: translateY(0);
  transition: transform var(--dur-swap-in) var(--ease-out) var(--dur-swap-gap);
}

/* Coming back, the same in reverse. Scoped to .has-copied, which is only on the row while
   it is returning -- otherwise this would delay the drawer's own entrance reveal. */
.menu-overlay.is-open .menu-social-row.has-copied:not(.is-copied) .menu-social-note .rli {
  transition: transform var(--dur-swap-out) var(--ease-in);
}

.menu-overlay.is-open .menu-social-row.has-copied:not(.is-copied) .menu-social a .rli {
  transition: transform var(--dur-swap-in) var(--ease-out) var(--dur-swap-gap);
}
.menu-email .rl { padding-bottom: 0.2em; margin-bottom: -0.2em; }
.menu-email-dim { color: var(--muted); transition: color var(--dur-base) ease; }
.menu-email:hover .menu-email-dim { color: var(--ink); }

.menu-social {
  list-style: none;
  display: flex;
  align-items: baseline;
  gap: calc(28 * var(--ui));
}

.menu-social a {
  display: inline-block;
  font-size: calc(20 * var(--ui));
  font-weight: 300;
  line-height: 1.3;
  letter-spacing: var(--track-chrome);
  color: var(--ink);
  text-decoration: none;
  transition: opacity 0.25s ease;
}

.menu-social a:hover { opacity: 0.55; }

/* The mark of a link that leaves the site (Linkedin in the drawer): a chevron pointing up and to the
   right at 45deg, set like an exponent -- small, in the top right corner of the last letter.
   Inside the word's own mask, so it rises and leaves with it. */
.ext-arrow {
  display: inline-block;
  width: 0.34em;
  height: 0.34em;
  margin-left: 0.08em;
  vertical-align: 0.46em; /* its foot at the x-height's top, its head at the cap line */
  overflow: visible;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.2;
  stroke-linecap: butt; /* square-ended, as every line on the site */
  stroke-linejoin: miter;
}

/* The offices: two columns, the city against its time */
.menu-clocks {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(8px, 1.4vh, calc(14 * var(--ui))) calc(24 * var(--ui));
  padding-top: clamp(16px, 3vh, calc(28 * var(--ui)));
  border-top: 1px solid var(--hairline-soft);
  font-size: var(--fs-copy);
  letter-spacing: var(--track-body);
}

.menu-clocks li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: calc(12 * var(--ui));
  white-space: nowrap;
}

.menu-clocks .menu-city { font-weight: 500; color: var(--ink); }
.menu-clocks time { font-weight: 300; color: var(--muted); font-variant-numeric: tabular-nums; }

/* His name sits in the foot, beside the pill wearing his photo: the person you'd be
   writing to. Privacy and Cookies links are omitted until the site needs them. */
.menu-who {
  display: flex;
  flex-direction: column;
  gap: calc(9 * var(--ui)); /* keeps the name and role from sitting too close at 1512 */
  transition: opacity 0.22s ease, translate var(--dur-base) var(--ease-out);
}

/* With the form up he steps aside for the way back */
.menu-card.is-form .menu-who {
  position: absolute;
  opacity: 0;
  translate: -8px 0;
  pointer-events: none;
}

/* ---- Foot: who you are writing to, and the pill that opens the form ---- */
.menu-foot {
  --foot-pad: calc(24 * var(--ui)); /* its top padding, which the absolute back button has to clear */
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: calc(20 * var(--ui));
  padding-top: var(--foot-pad);
  border-top: 1px solid var(--hairline-soft);
  flex: none;
}

/* Out of the flow, at the row's left: in flow, its hidden box would be a third flex item
   and the space-between row would overflow, pushing the pill past the card's margin. It
   spans the row's content box (the padding box is what an absolute box is measured
   against, so it clears the top padding itself) and centres its label in it, level with
   the pill. */
.menu-back {
  position: absolute;
  left: 0;
  top: var(--foot-pad);
  bottom: 0;
  display: inline-flex;
  align-items: center;
  gap: 10px; /* the chevron to the word */
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink);
  font-family: inherit;
  font-size: var(--fs-btn);
  font-weight: 600;
  line-height: 1;
  letter-spacing: var(--track-caps); /* = the pill's, so the two read as one row */
  text-transform: uppercase;
  cursor: pointer;
  opacity: 0;
  translate: 8px 0;
  pointer-events: none;
  transition: opacity 0.25s ease 0.08s, translate var(--dur-base) var(--ease-out) 0.08s, color var(--dur-fast) ease;
}

.menu-back:hover { color: var(--muted); }

.menu-card.is-form .menu-back {
  opacity: 1;
  translate: 0 0;
  pointer-events: auto;
}

/* Who you are writing to, in the type scale's own steps (typography.css): the name is a
   .label (12, caps, 0.2em), the role a .caption (14). Only the unit differs: --ui, the
   drawer's chrome ramp, instead of --ui-label. */
.menu-foot .label { font-size: calc(12 * var(--ui)); }
.menu-foot .caption { font-size: var(--fs-copy); white-space: nowrap; }

/* "let's talk": a white pill wearing his photo in its left cap, like the why-us player's.
   It swaps the middle block for the contact form. */
.menu-talk {
  display: inline-flex;
  align-items: center;
  gap: 10px; /* dot to label */
  height: 64px;
  margin-left: auto; /* always at the row's right end: with the form up it is the only item left in the flow, and space-between would drop it at the start */
  padding: 0 30px 0 6px; /* the photo fills the left cap */
  border: 0;
  border-radius: var(--r-pill);
  background: var(--white);
  box-shadow: var(--lift-rest);
  color: var(--ink);
  font-family: inherit;
  font-size: var(--fs-btn);
  font-weight: 600;
  line-height: 1;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  /* One `transition` declaration only: a second one at the end of this rule would override
     the first, snapping the pill's shape instead of easing it */
  transition: translate var(--dur-fast) ease, box-shadow var(--dur-fast) ease,
    padding 0.34s var(--ease-out),
    gap 0.34s var(--ease-out);
}

/* The photo the way the why-us player wears it (sections.css .player-ava): no ring, no
   ground of its own -- the image clips itself round */
/* The pill carries the presence: the dot sits between his photo and the word, and folds
   away with the photo when the form comes up */
.menu-talk .online-dot {
  flex: 0 0 8px;
  width: 8px;
  height: 8px;
  transition: flex-basis var(--dur-base) var(--ease-out), width var(--dur-base) var(--ease-out), opacity var(--dur-fast) ease 0.1s;
}

.menu-card.is-form .menu-talk .online-dot {
  flex-basis: 0;
  width: 0;
  opacity: 0;
  transition: flex-basis var(--dur-base) var(--ease-out), width var(--dur-base) var(--ease-out), opacity var(--dur-fast) ease;
}

.menu-talk-ava {
  flex: 0 0 52px;
  width: 52px;
  height: 52px;
  margin-right: 4px; /* photo to the label */
  /* The portrait is a cut-out; it needs a ground against the white pill */
  border-radius: var(--r-round);
  background: var(--ava-ground);
  transition: flex-basis 0.34s var(--ease-out), width 0.34s var(--ease-out), margin 0.34s var(--ease-out), opacity 0.22s ease;
}

.menu-talk-ava img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 18%;
  border-radius: var(--r-round);
}

/* With the form up the photo folds away: only the word is left */
.menu-card.is-form .menu-talk-ava {
  flex-basis: 0;
  width: 0;
  margin-right: 0;
  opacity: 0;
}

/* With the form up the pill is the way back: the two labels ride one mask, the first
   leaving upward as the second comes from below, and the green dot steps out */
.menu-swap .rl {
  position: relative;
  width: var(--w-talk, auto);
  transition: width 0.34s var(--ease-out);
}

.menu-card.is-form .menu-swap .rl { width: var(--w-back, auto); }

.menu-swap .rli {
  text-align: center; /* the shorter label sits in the middle of the mask */
  white-space: nowrap;
}

.menu-swap .rli-back {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  /* "send" is set like "contact" and every other button label on the page: one size (13)
     for all of them. The mask's width is measured from this element in ui.js, so it
     follows on its own. */
  font-size: var(--fs-btn);
}

/* Needed to out-specificity the entrance rule below (four classes deep), which would
   otherwise override the rest state and show both labels in the pill at once */
.menu-overlay.is-open .menu-card:not(.is-form) .menu-swap .rli-back { transform: translateY(var(--rise-mask)); }
.menu-overlay:not(.is-open) .menu-swap .rli-back { transform: translateY(var(--rise-mask)); }

.menu-overlay.is-open .menu-card.is-form .menu-swap .rli:first-child { transform: translateY(var(--rise-mask-out)); transition-delay: 0s; }
.menu-overlay.is-open .menu-card.is-form .menu-swap .rli-back { transform: translateY(0); transition-delay: 0.06s; }

.menu-card.is-form .menu-talk {
  /* Wider than the other button labels: send is the form's one action */
  padding: 0 40px;
  gap: 0; /* the folded photo and dot are still flex items: their gaps would stand */
}

/* ---- The entrance: the drawer slides in, its lines rise from under their masks ---- */
.menu-rv .rl {
  display: block;
  overflow: hidden;
  padding-bottom: 0.12em;
  margin-bottom: -0.12em;
}

.menu-rv .rli {
  display: block;
  transform: translateY(var(--rise-mask));
  transition: transform var(--dur-drop) var(--ease-in);
  will-change: transform;
}

.menu-overlay.is-open .menu-rv .rli {
  transform: translateY(0);
  transition: transform var(--dur-reveal) var(--ease-out);
}

/* Closed: the lines drop back at once (the drawer is off the edge by then). Otherwise a
   reopen mid-transition would catch them half-way and they'd twitch into place. */
.menu-overlay:not(.is-open) .menu-rv .rli { transition: none; }
.menu-overlay:not(.is-open) .menu-links,
.menu-overlay:not(.is-open) .menu-form { transition: none; }

/* Top to bottom, behind the panel's own slide */
.menu-overlay.is-open .menu-email .menu-rv:nth-child(1) .rli { transition-delay: 0.16s; }
.menu-overlay.is-open .menu-email .menu-rv:nth-child(2) .rli { transition-delay: 0.21s; }
.menu-overlay.is-open .menu-social li:nth-child(1) .rli { transition-delay: 0.28s; }
.menu-overlay.is-open .menu-clocks li:nth-child(1) .rli { transition-delay: 0.36s; }
.menu-overlay.is-open .menu-clocks li:nth-child(2) .rli { transition-delay: 0.4s; }
.menu-overlay.is-open .menu-clocks li:nth-child(3) .rli { transition-delay: 0.44s; }
.menu-overlay.is-open .menu-clocks li:nth-child(4) .rli { transition-delay: 0.48s; }
.menu-overlay.is-open .menu-who .menu-rv:nth-child(1) .rli { transition-delay: 0.5s; }
.menu-overlay.is-open .menu-who .menu-rv:nth-child(2) .rli { transition-delay: 0.54s; }
.menu-overlay.is-open .menu-foot .rli { transition-delay: 0.56s; }
/* ...but not on the pill's own labels: that delay is the drawer's entrance, and would make
   the swap back to "let's talk" wait half a second after the button is pressed */
.menu-overlay.is-open .menu-card:not(.is-form) .menu-swap .rli { transition-delay: 0s; }

/* The head and the foot ride in a touch after the panel */
.menu-head,
.menu-foot { opacity: 0; }

.menu-overlay.is-open .menu-head,
.menu-overlay.is-open .menu-foot {
  opacity: 1;
  animation: menu-element-rise 0.62s var(--ease-out) backwards;
}

.menu-overlay.is-open .menu-head { animation-delay: 0.12s; }
.menu-overlay.is-open .menu-foot { animation-delay: 0.3s; }

@keyframes menu-element-rise {
  from { opacity: 0; transform: translateY(18px); }
  to { opacity: 1; transform: none; }
}

/* ---- The contact form in the drawer ---- */
/* His photo beside the form's title, the way the why-us player wears it: the image clips
   itself round, no ring and no ground of its own */
.contact-title-row {
  display: flex;
  align-items: center;
  gap: calc(22 * var(--ui)); /* the portrait has a ground of its own and needs the air around it */
}

/* The form is written in --ui like the rest of the drawer it opens in (1px up to 1512,
   1.35px at 2560); the drawer's pill, which sends it, stays UI and does not grow */
.contact-ava {
  flex: none;
  width: calc(52 * var(--ui));
  height: calc(52 * var(--ui));
  /* The portrait is a cut-out; it needs a ground against the white card */
  border-radius: var(--r-round);
  background: var(--ava-ground);
}

.contact-ava img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 18%;
  border-radius: var(--r-round);
}

/* It comes in the page's own way: the rise the fields and the menu's rows use */
.is-form .contact-ava {
  animation: menu-element-rise 0.55s var(--ease-out) 0.06s backwards;
}
.contact-form--menu { width: 100%; }
/* The drawer's pill in the foot is the send: the form's own button stays only so that
   Enter in a field still submits (a form with no submit button has no implicit submission) */
.contact-form--menu .contact-actions { display: block; height: 0; margin: 0; overflow: hidden; }
.contact-form--menu .contact-submit {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  opacity: 0;
  pointer-events: none;
}

/* The form's entrance, the site's own: its lines rise from under their masks, then the
   fields come up one after another (.form-rv, keyed to the drawer's .is-form) */
.form-rv .rl {
  display: block;
  overflow: hidden;
  padding-bottom: 0.12em;
  margin-bottom: -0.12em;
}

.form-rv .rli {
  display: block;
  transform: translateY(var(--rise-mask));
  transition: none;
}

.is-form .form-rv .rli {
  transform: translateY(0);
  transition: transform var(--dur-reveal) var(--ease-out);
}

.is-form .contact-title .rli { transition-delay: 0.04s; }
.is-form .contact-lead .rli { transition-delay: 0.12s; }

.is-form .contact-form .field,
.is-form .contact-form .contact-actions {
  animation: menu-element-rise 0.55s var(--ease-out) backwards;
}

.is-form .contact-form .field:nth-child(1) { animation-delay: 0.2s; }
.is-form .contact-form .field:nth-child(2) { animation-delay: 0.27s; }
.is-form .contact-form .contact-actions { animation-delay: 0.34s; }

/* ---- Contact form (contact.js) --------------------------------------------------------- */
.contact-form {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: clamp(22px, 4vh, calc(40 * var(--ui)));
  width: 100%;
}

.contact-head {
  display: flex;
  flex-direction: column;
  gap: calc(14 * var(--ui));
}

.contact-title {
  font-size: clamp(38px, 11cqw, calc(62 * var(--ui)));
  font-weight: 300;
  line-height: 1;
  letter-spacing: var(--track-display);
  color: var(--ink);
}

.contact-lead {
  font-size: var(--fs-copy);
  font-weight: 300;
  line-height: 1.4;
  color: var(--text);
  white-space: nowrap;
}

.contact-fields {
  display: flex;
  flex-direction: column;
  gap: calc(18 * var(--ui)); /* explicit gap; the fields have no box edges of their own to supply it */
}

/* A field with its label inside: the hint sits where the value will be, and lifts into a
   small label above it on focus, or once there is a value (placeholder=" " makes
   :placeholder-shown the empty test) */
.field {
  position: relative;
  display: block;
}

.field-input {
  display: block;
  width: 100%;
  height: calc(52 * var(--ui));
  padding: calc(22 * var(--ui)) 0 calc(6 * var(--ui));
  border: 0;
  border-radius: 0;
  background: none;
  /* A line, not a box: the rule under the field is the field (the site's own hairline) */
  box-shadow: inset 0 -1px 0 var(--field-rule);
  font: 400 calc(14 * var(--ui))/1.3 'Archivo', sans-serif;
  color: var(--ink);
  outline: none;
  transition: box-shadow 0.25s var(--ease-out);
}

.field-input:hover { box-shadow: inset 0 -1px 0 var(--field-rule-hover); }
/* Focus draws the line in: it thickens to the ink, from nothing to the full width */
.field-input:focus { box-shadow: inset 0 -1.5px 0 var(--ink); }

.field-label {
  position: absolute;
  left: 0;
  top: 50%;
  translate: 0 -50%;
  font-size: var(--fs-copy);
  font-weight: 300;
  line-height: 1;
  color: var(--muted);
  white-space: nowrap;
  pointer-events: none;
  transform-origin: 0 50%;
  transition: top 0.22s var(--ease-out), translate 0.22s var(--ease-out), font-size 0.22s ease, letter-spacing 0.22s ease, color var(--dur-fast) ease;
}

.field-opt { font-weight: 300; opacity: 0.7; }

.field-input:focus ~ .field-label,
.field-input:not(:placeholder-shown) ~ .field-label {
  top: calc(4 * var(--ui));
  translate: 0 0;
  font-size: calc(11 * var(--ui));
  font-weight: 500;
  letter-spacing: var(--track-caps-tight);
  text-transform: uppercase;
  color: var(--ink);
}

.field-error {
  position: absolute;
  right: 0;
  top: calc(4 * var(--ui));
  font-size: calc(11 * var(--ui));
  font-weight: 500;
  letter-spacing: var(--track-caps-tight);
  text-transform: uppercase;
  color: var(--error);
  opacity: 0;
  translate: 0 4px;
  transition: opacity var(--dur-fast) ease, translate var(--dur-fast) ease;
  pointer-events: none;
}

.field.is-invalid .field-input { box-shadow: inset 0 -1.5px 0 var(--error); }
.field.is-invalid .field-label { color: var(--error); }
.field.is-invalid .field-error { opacity: 1; translate: 0 0; }

/* Anti-bot bits (contact.js): the honeypot is off-screen for people, the Turnstile holder
   only takes room when Cloudflare needs an interaction; a failed send leaves a line */
.contact-hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
.contact-turnstile:empty { display: none; }
.contact-error { font-size: 13px; font-weight: 300; line-height: 1.4; color: var(--error); }
.contact-error:empty { display: none; }
.contact-error a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }

/* Sending: the fields dim; sent: they give way to the thanks */
.contact-form.is-busy .contact-fields { opacity: 0.55; pointer-events: none; transition: opacity 0.25s ease; }

.form-done {
  position: absolute; /* over the fields: it takes no room of its own */
  left: 0;
  right: 0;
  top: 50%;
  display: flex;
  align-items: center;
  gap: calc(14 * var(--ui));
  font-size: var(--fs-copy);
  font-weight: 300;
  line-height: 1.4;
  color: var(--ink);
  opacity: 0;
  translate: 0 calc(-50% + 8px);
  pointer-events: none;
  transition: opacity 0.35s ease 0.15s, translate 0.45s var(--ease-out) 0.15s;
}

.contact-form.is-done .contact-fields { opacity: 0; translate: 0 -8px; pointer-events: none; transition: opacity 0.25s ease, translate var(--dur-base) ease; }
.contact-form.is-done .form-done { opacity: 1; translate: 0 -50%; }

/* The pins' dot in its ring, as the tick */
.form-done-ring {
  position: relative;
  flex: none;
  width: calc(22 * var(--ui));
  height: calc(22 * var(--ui));
  border-radius: var(--r-round);
  box-shadow: inset 0 0 0 1px var(--ink);
}
.form-done-dot {
  position: absolute;
  inset: calc(7 * var(--ui));
  border-radius: var(--r-round);
  background: var(--ink);
}
.contact-form.is-done .form-done-ring { animation: form-done-ring 0.6s var(--ease-out) 0.15s backwards; }
@keyframes form-done-ring {
  from { transform: scale(0.4); }
  to { transform: scale(1); }
}

html.menu-open,
html.menu-open body {
  /* `hidden` makes body a scroll container and breaks the sticky Why/EQ viewport.
     `clip` blocks page overflow without becoming sticky's containing scroller. */
  overflow: clip;
}

/* The headers stay while the drawer is open -- the logo keeps its place. Only the burger
   goes: it sits at the right edge, under the drawer, and the drawer's own cross closes it */
html.menu-open .menu-btn {
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-fast) ease;
}

@media (max-width: 900px) {
  .menu-card { width: 100%; padding: 24px 24px 32px; }
  .menu-email { font-size: clamp(26px, 8vw, 40px); }
  .menu-clocks { grid-template-columns: 1fr; }
}

/* ---- Buttons — pills (Figma "btn") ------------------------------------------------ */

.btn {
  position: relative;
  isolation: isolate;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* A step bigger on a big screen, with the header (--ui-head: x1.08 at 1920, x1.2 at 2560);
     the label keeps the small print's own gentler ramp (--fs-btn) */
  gap: calc(16 * var(--ui-head));
  height: calc(64 * var(--ui-head));
  padding: 2px calc(40 * var(--ui-head)) 0; /* the caps sit optically high in the pill: nudge them down */
  border: 0;
  border-radius: var(--r-pill);
  background: var(--white);
  color: var(--ink);
  box-shadow: var(--lift-rest);
  font-size: var(--fs-btn);
  font-weight: 600;
  line-height: 1;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: translate var(--dur-fast) ease, box-shadow var(--dur-fast) ease;
}

/* THE hover, for every button on the page. It lifts and its shadow deepens by one step
   from wherever it rests (--lift-*, tokens.css), so the same rule reads the same on the
   page's own ground, on the white plate and in the drawer. */
/* (hover: hover): a tap on a phone would otherwise leave the button lifted until the next tap */
@media (hover: hover) {
  :is(.btn, .menu-talk):hover {
    /* `translate`, not `transform`: it composes rather than replaces, and these elements have
       transforms of their own (the reveal's rise, the CTA's tilt) that a shared rule must not
       write over */
    translate: 0 var(--lift-up);
    box-shadow: var(--lift-hover);
  }
}

:is(.btn, .menu-talk):active {
  translate: 0 var(--lift-down);
  box-shadow: var(--lift-active);
}

/* One drawing for every icon on the page: a 12x12 box, 1.4 strokes, round caps and joins */
.ico {
  display: block;
  overflow: visible;
}

/* "discover more" — the chevron points down */
.btn-secondary {
  /* Icon fills its box with no phantom slack, so gap and padding don't need extra room to
     compensate for it */
  gap: calc(14 * var(--ui-head));
  height: calc(56 * var(--ui-head)); /* a shade shorter than the solid pills */
  /* Even top and bottom: the solid pills' 2px top nudge sat the label low in this shorter one */
  padding: 0 calc(33 * var(--ui-head)) 0 calc(36 * var(--ui-head)); /* right padding a touch tighter than the left */
}

/* Not an arrow: "discover more" is not a place, it is the fact that there is more of the
   page. This is the scroll indicator's own line, shortened -- a hairline track that inks in
   from the left on hover. */
.btn-meter {
  position: relative;
  display: inline-block;
  width: calc(17 * var(--ui-head)); /* (with its pill) */
  height: 2px; /* square-ended, as every line on the site */
  background: var(--bar-light);
  overflow: hidden;
}

.btn-meter::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 0;
  border-radius: inherit;
  background: var(--ink);
  transition: width 0.42s var(--ease-out);
}

@media (hover: hover) {
  :is(.btn, .menu-talk):hover .btn-meter::after { width: 100%; }
}
/* Touch has no hover: the press itself inks the line, fast enough to be seen in a tap,
   and it drains at the usual pace on release */
@media (hover: none) {
  :is(.btn, .menu-talk):active .btn-meter::after {
    width: 100%;
    transition-duration: 0.12s;
  }
}

/* The pill only carries one while it is the form's send: with the portrait folded away
   there is room for it, and it is the same promise "discover more" makes. */
.menu-talk-meter {
  flex: none;
  width: 0;
  margin-left: 0;
  opacity: 0;
  transition: width 0.34s var(--ease-out),
    margin-left 0.34s var(--ease-out), opacity var(--dur-fast) ease;
}

.menu-card.is-form .menu-talk-meter {
  width: 17px;
  margin-left: 14px;
  opacity: 1;
  transition-delay: 0.1s;
}

/* Compact pill — header "contact", overlay CTA */
.btn-sm {
  height: 40px;
  padding: 0 16px;
  font-size: var(--fs-btn);
}

/* ---- Case cover: a works tile's bare grey plate, opened out to the whole screen on the
   way into its case page (js/ui/case-leave.js scales it down onto the tile, then opens
   it). Lit like every plate, so it is the case hero's own plate (css/case.css) that the
   next page opens on. Over the header and the menu, under the cursor. ---------------- */
.case-cover {
  position: fixed;
  inset: 0;
  z-index: calc(var(--z-menu) + 2);
  background: var(--plate-grey) var(--plate-lit);
  transform-origin: 0 0;
  pointer-events: none;
}

/* ---- The logo's curtain (js/ui/reload-curtain.js): the same grey came up over the page and
   the page loaded again under it; the new one opens under it from the first paint
   (index.html: html.curtain) and lifts it off, foot to top, once its scene is in
   (.curtain-out). The header waits under it on every screen, as phones' does for the scene,
   and comes down as it lifts. ---------------------------------------------------------- */
html.curtain::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: calc(var(--z-menu) + 2); /* = .case-cover */
  background: var(--plate-grey) var(--plate-lit);
  clip-path: inset(0);
  transition: clip-path var(--dur-curtain) var(--ease-io);
  pointer-events: none;
}
html.curtain.curtain-out::after { clip-path: inset(0 0 100% 0); }
html.curtain.scene-wait header > :is(.logo, .menu-btn) { animation-play-state: paused; }

/* ---- Footer layer ------------------------------------------------------------------
   The footer is a sibling of .page-content (still in the normal flow), under the page
   sheet (--z-footer < --z-page): the site reads as riding up off it.
   The layer itself is only a spacer holding the footer's height in the flow (ui.js
   measures it). The footer is PINNED to the bottom of the screen and never moves -- its
   links stand still while the page above rides up off it, which is the whole effect. What
   moves is the window the footer is seen through: --clip is how much of it the page still
   covers, in px from its top edge. */
.footer-layer {
  position: relative;
  z-index: var(--z-footer);
}

.footer-layer > .s-footer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  /* 1px above the seam on purpose: the clip edge and the corner wedges are rounded to
     device pixels independently by the browser, and on a fractional DPR (Windows scaling)
     the two could leave a one-pixel row of light page between them. Dark overlapping dark
     is invisible; a light gap is not. */
  clip-path: inset(calc(var(--clip, 100%) - 1px) 0 0 0);
  will-change: clip-path;
}

/* ---- Cursor — a hairline ring that inverts whatever it crosses. The ring is painted
   WHITE and blended with `difference`: on the light page that resolves to near-black, over
   the dark case photos it flips to light on its own, so one element works everywhere. Position is driven from ui.js via `translate`.
   The native cursor stays on. The ring eases toward the pointer rather than tracking it
   exactly, and with nothing else on screen that lag is the only thing to read -- it looks
   like the page is slow. Left visible, the arrow answers instantly and the ring is plainly
   a thing trailing it.
   position: fixed is not cosmetic -- as a static div at the end of <body> its transform
   counted toward the document's scrollable overflow and left a screenful of empty scroll
   under the footer. ------------------------------------------------------------------ */

/* The plate's sparks (plate-sparks.js): one fixed canvas over the white plate, under the
   header and the cursor -- a few blue shards that part around the cursor the way the
   Hero's grains do. */
.plate-sparks {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: calc(var(--z-page) + 2); /* over the page's content, under the haze strips and the header */
  pointer-events: none;
}

@media (hover: none), (prefers-reduced-motion: reduce) {
  .plate-sparks { display: none; }
}

.cursor {
  position: fixed;
  top: 0;
  left: 0;
  width: 20px;
  height: 20px;
  margin: -10px 0 0 -10px;
  border-radius: var(--r-round);
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--white); /* the ring; difference turns it dark on light ground */
  mix-blend-mode: difference;
  z-index: calc(var(--z-top) + 1);
  pointer-events: none;
  opacity: 0;
  /* One variable drives every state, so hover and press compose instead of fighting over
     specificity */
  --s: 0.6;
  scale: var(--s);
  transition: opacity 0.25s ease, scale 0.35s cubic-bezier(0.34, 1.4, 0.64, 1),
    width var(--dur-disc) ease, height var(--dur-disc) ease, margin var(--dur-disc) ease, border-radius var(--dur-disc) ease;
  will-change: translate;
}

/* Back from the Why us disc (why-cursor.js holds .is-why-out for the shrink): the ring is
   drawn in ink under a normal blend while the disc shrinks, and only once it is small
   does it hand over to the white-under-difference ring, which looks the same. Flipping
   the blend at once would invert the white disc; flipping it late would show a white ring. */
.cursor.is-why-out {
  mix-blend-mode: normal;
  box-shadow: inset 0 0 0 1px var(--ink);
}

.cursor.is-on {
  opacity: 1;
  --s: 1;
}

/* Over links / buttons / tabs / chips the ring opens up a little */
.cursor.is-on.is-hover {
  --s: 1.7;
}

/* Pressed: it pinches in, and springs back on release (the transition's overshoot) */
.cursor.is-on.is-down {
  --s: 0.72;
}

.cursor.is-on.is-hover.is-down {
  --s: 1.25;
}

.cursor-visual, .cursor-talk {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  opacity: 0;
  transition: opacity var(--dur-fast) ease;
}
.cursor-visual { background: var(--white); perspective: 650px; }
.cursor-talk {
  display: flex;
  align-items: center;
  justify-content: center;
}
.cursor.is-on.is-why {
  width: 112px;
  height: 112px;
  margin: -56px 0 0 -56px;
  mix-blend-mode: normal;
  box-shadow: var(--shadow-float);
  --s: 1;
}
.cursor.is-why .cursor-visual { opacity: 1; }
.cursor.is-on.is-why.is-talk {
  width: 224px;
  height: 64px;
  margin: -32px 0 0 -112px;
  border-radius: 40px;
}
.cursor.is-talk .cursor-visual { opacity: 0; }
.cursor.is-talk .cursor-talk { opacity: 1; }
.cursor.is-on.is-why.is-down { --s: .94; }
@media (prefers-reduced-motion: reduce) {
  .cursor, .cursor-visual, .cursor-talk { transition: none; }
}

@media (hover: none), (pointer: coarse) {
  .cursor { display: none; }
}

/* ---- Page content (scrollable, above the fixed canvas) ---------------------------- */

/* The white background takes over from the entrance curtain, then scrolls away naturally
   after the reviews so the following sections can show the shared canvas. */
.plate {
  position: relative;
  z-index: 2;
  background: var(--white);
}

/* The handover into "what we do" is not a colour fading in over the page: the screen the
   reader is on closes. This element IS the window that is left -- it draws nothing itself,
   its shadow paints everything OUTSIDE it white. As --shrink goes 0..1 the window pulls in
   from the sides and its floor rises, so the section above narrows to a slot and goes, and
   the page underneath (--z-page 10, over this) rides up into the white it leaves.
   At --shrink 0 the window is the whole viewport and the shadow falls off-screen, so there
   is nothing to see and nothing to pay for. ui.js drives it. */
.page-tint {
  position: fixed;
  top: 0;
  /* The sides come in to the page's own gutter and stop there: past that it is no longer a
     panel closing, it is a strip, and the site has a margin of its own to answer to */
  left: calc(var(--shrink, 0) * var(--page-x));
  right: calc(var(--shrink, 0) * var(--page-x));
  bottom: calc(var(--shrink, 0) * 100%);
  z-index: var(--z-tint);
  box-shadow: 0 0 0 100vmax var(--white);
  pointer-events: none;
}

/* Only the entry uses the curtain; scrolling back above the plate restores it. */
.page-tint.is-finished { visibility: hidden; }

/* White pills and buttons on the sheet: a stronger shadow than the page default, else
   white-on-white reads flat. Buttons here rest deeper, and the shared hover steps up from
   that. */
.plate,
.menu-card {
  --lift-rest: var(--shadow-plate);
  --lift-hover: var(--shadow-plate-hover);
  --lift-active: var(--shadow-plate-active);
}

.page-content {
  position: relative;
  z-index: var(--z-page);
  /* The content is one sheet resting on the footer; the radius shapes its bottom corners
     and the footer's wedges draw them (see .footer-layer). No drop shadow: it only banded
     the seam and read as a stray divider. */
  border-radius: 0 0 var(--sheet-r) var(--sheet-r);
  /* The wrapper itself must not catch clicks: over the hero the canvas (hand drag/click)
     is underneath. Every section re-enables them; the hero stage stays transparent and
     the hero's own content opts back in. */
  pointer-events: none;
}

.page-content > :not(.hero-stage) {
  pointer-events: auto;
}

/* ---- Hero (Figma 882:252) ---------------------------------------------------------- */

/* The stage is taller than the viewport by --hero-pin; the hero sticks to the top for
   that extra scroll while the story plays (dune-story.css sets the pin), then scrolls away */
.hero-stage {
  position: relative;
  height: calc(100vh + var(--hero-pin));
  pointer-events: none; /* clicks/drags over the hand must reach the canvas (the hero re-enables its own content) */
}

.hero {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  align-items: center;
  /* vw-based (not a fixed px) so the text column tracks the hand, which is positioned
     in Three.js world space as a fraction of viewport. 80px at the 1512 mock. */
  padding: 0 var(--page-x);
  pointer-events: none;
}

.hero-content {
  display: flex;
  flex-direction: column;
  gap: 40px;
  max-width: 78vw; /* fits "Our native language." on one line: ~73vw at 120px */
  margin-top: -56px; /* pulls the heading + CTA block a little higher */
  pointer-events: auto;
}

.hero-head {
  gap: var(--gap-eyebrow); /* eyebrow to the h1 */
}

.hero-actions {
  display: flex;
  gap: 16px;
  /* The pill's box lines up with the heading, but its 40px left padding pushes the label
     in, reading as indented; -2px pulls it back without overhanging the gutter */
  margin-left: -2px;
}

/* "Our partners" — bottom-left of the hero */
.hero-partners {
  position: absolute;
  left: var(--page-x);
  bottom: 54px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  pointer-events: auto;
}

.hero-partners > .small {
  font-size: var(--fs-small);
  font-weight: 400;
  line-height: 1.3;
}

.partner-logos {
  display: flex;
  align-items: center;
  gap: 24px;
}

.partner-logos .logo-ibexa {
  position: relative;
  width: 55px;
  height: 24px;
}

.partner-logos .logo-ibexa img:first-child {
  width: 54.7px;
  height: 19.1px;
}

.partner-logos .logo-ibexa img:last-child {
  position: absolute;
  top: 21px;
  left: 21.6px;
  width: 33.2px;
  height: 3px;
}

.partner-logos .logo-gha {
  width: 110px;
  height: 16px;
}

/* ---- Scroll progress (Figma 1198:1440) — fixed vertical hairline at the right edge;
   the thumb is translated along it from main.js as the page scrolls. ------------------ */

.scroll-progress {
  position: fixed;
  top: 50%;
  /* --sp-shift (ui.js): on the footer the bar slides to the footer's own centre line */
  translate: 0 var(--sp-shift, 0px);
  /* Its length, insets and labels are in --ui (1px up to 1512, 1.35px at 2560): on a big
     screen a flat bar would be lost. The rule itself stays a 1px hairline at every
     width. --sp-h: its length (only the drawing: the sections share it as before). */
  --sp-h: calc(420 * var(--ui));
  right: calc(27 * var(--ui));
  display: flex;
  flex-direction: column;
  gap: calc(4 * var(--ui));
  height: var(--sp-h);
  width: 1px;
  transform: translateY(-50%);
  z-index: var(--z-header);
  pointer-events: none;
  /* Inverts over whatever is behind it (dark case cards, photos), stays ink on light */
  mix-blend-mode: difference;
  /* Cue <-> bar is a crossfade (ui.js .is-swap dips the opacity while the geometry snaps) */
  transition: translate 0.45s var(--ease-out), opacity 0.45s ease;
  /* Enters with the page's frame, at the menu icon's moment (header > .menu-btn, 0.25s) --
     CSS only, from the first paint -- and comes in like the hero's CTA (.rv-block): up 16px
     and in from 0, --dur-slow ease. Only the container's own `translate` moves (--sp-shift's
     property; its transform keeps the centring), and nothing else animates it, so class
     changes later never replay this. */
  animation: sp-in var(--dur-slow) ease 0.25s backwards;
}

@keyframes sp-in {
  from { opacity: 0; translate: 0 calc(var(--sp-shift, 0px) + 16px); }
}

@media (prefers-reduced-motion: reduce) {
  .scroll-progress { animation: none; }
}

.scroll-progress.is-swap {
  opacity: 0;
  transition: opacity 0.18s ease;
}

/* Hero cue state: a short continuous line bottom-right that fills with the pin progress,
   and the "scroll" label; the segments' fill is idle here */
.scroll-progress.is-cue {
  top: calc(100vh - 160 * var(--ui)); /* 64 from the bottom + its own 96 */
  right: calc(64 * var(--ui));
  height: calc(96 * var(--ui));
  gap: 0;
  transform: none;
}

/* Idle at the top: the line fills itself linearly, top -> down, as the hint; the moment
   the page scrolls the fill follows the scroll instead (--cue) */
@keyframes cueFill {
  0% { transform: scaleY(0); opacity: 1; }
  82% { transform: scaleY(1); opacity: 1; }
  92% { transform: scaleY(1); opacity: 0; }
  100% { transform: scaleY(0); opacity: 0; }
}

.scroll-progress.is-cue.is-idle .sp-cuefill {
  animation: cueFill 4s linear infinite;
}

/* Cue fill: how far through the hand's pin the page is (ui.js --cue 0..1), top -> down */
.sp-cuefill {
  position: absolute;
  left: 0;
  top: 0;
  width: 1px;
  height: 100%;
  background: var(--white); /* difference blend -> ink */
  transform-origin: top;
  transform: scaleY(var(--cue, 0));
  opacity: 0;
  transition: opacity var(--dur-base) ease, transform 0.15s linear;
  z-index: 1;
}

.scroll-progress.is-cue .sp-cuefill {
  opacity: 1;
}

.sp-label {
  position: absolute;
  right: calc(12 * var(--ui));
  bottom: 0;
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  font-size: var(--fs-micro);
  font-weight: 500;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  line-height: 1;
  /* Pre-inverted for the difference blend: on the #dde0f2 page this reads as ~#83848c grey */
  color: var(--map-label);
  opacity: 0;
  transition: opacity var(--dur-base) ease;
}

.scroll-progress.is-cue .sp-label {
  opacity: 1;
}

/* After the first gesture the cue says "keep scrolling" and breathes: label and line pulse */
@keyframes cuePulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.4; }
}

.scroll-progress.is-cue.is-going .sp-label {
  animation: cuePulse 1.6s ease-in-out infinite;
}

/* The line only breathes -- a shivering 1px rule reads as a rendering fault, so the
   tremor is the label's alone */
.scroll-progress.is-cue.is-going .sp-cuefill {
  animation: cuePulse 1.6s ease-in-out infinite;
}

/* The three looping animations on the page that nothing else switches off: the scroll
   cue's self-filling line, its "keep scrolling" pulse, and the green presence dot's ping
   (every copy of it: the why-us pill's, the cursor's and the menu pill's). An animation that never ends is exactly what
   prefers-reduced-motion is for; the elements keep their resting look. */
@media (prefers-reduced-motion: reduce) {
  .scroll-progress.is-cue.is-idle .sp-cuefill,
  .scroll-progress.is-cue.is-going .sp-label,
  .scroll-progress.is-cue.is-going .sp-cuefill,
  .online-dot::before {
    animation: none;
  }
  .scroll-progress.is-cue.is-idle .sp-cuefill { transform: scaleY(1); opacity: 1; }
}

/* Reduced motion (M11, MOTION.md): the line masks, the drawer and the buttons move no more.
   A text that rises is simply there, the drawer opens and shuts in place, a button does not
   lift. Every state is as it was -- what is hidden stays hidden, the masks keep their
   places -- only nothing travels between them. (No script waits on these transitions.) */
@media (prefers-reduced-motion: reduce) {
  .rv .rli, .rv.rv-block, .menu-rv .rli, .form-rv .rli {
    transition-duration: 0s !important;
    transition-delay: 0s !important;
  }
  .menu-overlay, .menu-card { transition: none !important; }
  :root { --lift-up: 0px; --lift-down: 0px; }
}

/* One segment per section (built in ui.js), height ∝ section height via flex-grow;
   --p 0..1 = how far the page cursor is through that section */
/* Colours are pre-inverted for the difference blend: white → reads as ink on the light
   page; the track is near-black so it reads as a light grey (~#c4c5c9) */
.sp-seg {
  position: relative;
  display: block;
  flex: 1 1 0;
  /* Never shorter than a pointer's target (24px, WCAG 2.5.8), whatever its share of the page:
     presence, sharing its pinned screen with the close, had ~50svh of the page and drew a few
     px, too thin to hit with a mouse (19 Oct; 4px until then). The rest share what is left. */
  min-height: calc(24 * var(--ui));
  /* A 121px-wide hit area around the 1px track (the track only paints the content box) */
  margin: 0 -60px;
  padding: 0 60px;
  background: var(--map-track);
  background-clip: content-box;
  pointer-events: auto;
  cursor: pointer;
  transform-origin: center;
  transition: transform 0.25s var(--ease-out);
}

.sp-seg::before {
  content: "";
  position: absolute;
  inset: 0 60px;
  background: var(--white);
  transform-origin: top;
  transform: scaleY(var(--p, 0));
  transition: transform 0.12s linear;
}

/* Hover: the segment thickens; the current section's segment wears the same look all the
   time (ui.js marks it .is-here). In the hero cue state the bar is not a map yet */
.sp-seg:hover,
.scroll-progress:not(.is-cue) .sp-seg.is-here {
  transform: scaleX(2);
}

.scroll-progress.is-cue .sp-seg {
  pointer-events: none;
  /* (the cue is one short line, 96px: the map's 24px floor stretched its nine segments to
     216px -- here they keep the 4px they always had) */
  min-height: 4px;
}

/* Where the page is: beside the current segment, where the hover tip would sit -- the
   section's name, and under it the next section's, smaller and muted (ui.js positions
   the pair and swaps the text). Off in the hero cue state (the bar is not a map yet) and
   while a hover tip is naming a segment. */
.sp-now {
  position: absolute;
  right: calc(16 * var(--ui));
  top: 0;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: calc(5 * var(--ui));
  pointer-events: none;
  /* ui.js writes the segment's centre into the transform (translateY(calc(Ypx - 50%))),
     not into top: the slide between segments stays on the compositor */
  transition: opacity var(--dur-fast) ease, transform 0.25s var(--ease-out); /* slides between segments (hover and scroll alike) */
}

.sp-now-cur,
.sp-now-next {
  line-height: 1;
  white-space: nowrap;
  color: var(--white); /* difference blend -> ink */
}

/* The section the page is in: a spaced capital label */
.sp-now-cur {
  font-size: var(--fs-micro);
  font-weight: 500;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
}

/* The one after: plain body type, caption-small and muted */
.sp-now-next {
  font-size: var(--fs-micro);
  font-weight: 300;
  letter-spacing: var(--track-body);
  opacity: 0.42;
}

/* Hover names a segment with the SAME pair (ui.js slides it there): past the last section
   the pair is off, unless a segment is being hovered */
.scroll-progress.is-cue .sp-now,
.scroll-progress.is-past:not(.is-hover) .sp-now,
.sp-now.is-swap {
  opacity: 0;
}

/* ---- Responsive -------------------------------------------------------------------- */

@media (max-width: 768px) {
  header {
    padding: 14px 16px;
  }


  .hero {
    align-items: flex-end;
    padding-bottom: 160px;
  }

  .hero-content {
    max-width: 100%;
    gap: 28px;
  }

  .hero-actions {
    flex-wrap: wrap;
  }

  .btn {
    height: 56px;
    padding: 0 28px;
  }

  .hero-partners {
    bottom: 32px;
  }

  .scroll-progress {
    display: none;
  }
}
