/* Base, layout chrome and shared components: reset, fixed 3D canvas, glass panels,
   header, hero, buttons, scroll progress, dev tools. 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;
}

/* Until the hand has assembled: no wheel, keys, touch or anchor scrolling */
html.scroll-lock,
html.scroll-lock body {
  overflow: hidden;
  height: 100%;
}

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

::-moz-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, which is
   18.4:1 on the white sheet and 12.7:1 on the scene ground -- far past the 3:1 the
   criterion asks of a focus indicator. Components that need 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: 0.12em;
  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;
}

/* Ambient radial spot — pure CSS pseudo-random drift.
   X and Y run on separate elements with non-commensurate periods (23s/17s/31s),
   so the combined path (Lissajous) never visibly repeats. */
.bg-spot {
  display: none; /* legacy: the opaque canvas covers it; it only showed as blotches before the first frame */
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  animation: bgSpotX 23s ease-in-out infinite alternate;
  will-change: translate;
}

.bg-spot-inner {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 60vmax;
  height: 60vmax;
  margin: -30vmax 0 0 -30vmax;
  background: radial-gradient(
    circle,
    rgba(255, 23, 68, 0.05) 0%,
    transparent 60%
  );
  animation: bgSpotY 17s ease-in-out infinite alternate,
    bgSpotScale 31s ease-in-out infinite alternate;
  will-change: translate, scale;
}

@keyframes bgSpotX {
  from { translate: -18vw 0; }
  to { translate: 18vw 0; }
}

@keyframes bgSpotY {
  from { translate: 0 -14vh; }
  to { translate: 0 14vh; }
}

@keyframes bgSpotScale {
  from { scale: 0.85; }
  to { scale: 1.15; }
}

/* Fixed 3D Canvas Container */
#canvas-container {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  z-index: var(--z-canvas);
}

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

/* Intro entry animations — play on page load */
@keyframes fadeSlideUp {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* 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 ui.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 ui.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 .tab, .page-content .chip,
  .page-content .partner-logos,
  .page-content .hero-actions, .page-content .player,
  .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 ui.js to split into masks either. SKIP in ui.js 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 */

/* 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(110%);
  /* Leaving the viewport: the rise played backwards, bottom line first (--d-out) */
  transition: transform 0.55s 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-soft);
  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(-110%);
  transition: transform 0.4s var(--ease-in);
  transition-delay: calc(var(--d-in, 0s) * 0.6);
}

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

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

/* Gradient display text split into lines: background-clip:text on the PARENT paints the
   gradient through the descendants' glyphs at their layout position — ignoring their
   transform — so the text showed in place while the real lines moved under the mask.
   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 Poppins 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(16px);
  transition: opacity var(--dur-slow) ease, transform var(--dur-slow) ease;
}

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

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

/* Scroll-triggered version of the same fade-slide-up, for text below the fold — toggled
   by main.js's IntersectionObserver (adds .is-visible once, then stops watching). Transition
   (not the fadeSlideUp keyframe) since it needs to sit in a stable "waiting" state until
   scrolled into view, not just play once on load. */
.reveal {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity var(--dur-reveal) ease, transform var(--dur-reveal) ease;
}

.reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
}

/* ---- Header (Figma 1216:4695) — 72px, blurred, logo / menu / contact ---------------- */

/* The header stays at the top of the site: it does not travel with the hero any more, so
   the logo and the burger are 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: 14px 20px;
  z-index: var(--z-header);
}

.logo {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 20px;
  font-weight: 660; /* (600 -> 650 -> 660: Archivo is variable, the link serves 200..700) */
  line-height: 0.9;
  letter-spacing: -0.01em;
  color: var(--ink);
  text-decoration: none;
}

.logo img {
  width: 42px; /* (40 -> 42) */
  height: 42px;
}

/* (was centred in the row, between the logo and a contact button; that button is gone and
   the burger stands in its place, at the right end) */
.menu-btn {
  width: 64px; /* (48 -> 64: longer bars, the right inset unchanged) */
  height: 40px;
  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. Hover nudges them apart; open turns them into the
   mock's X (icon 1338:2779: 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; /* (2.5 -> 2) with the hairline set; its box is four times the icons' */
  stroke-linecap: round;
  transform-box: view-box;
  transition: transform 0.45s var(--ease-out-2);
}

/* Each bar turns about its OWN centre, then slides to the middle line: with one shared
   origin the rotation swung the bars' midpoints apart and the X crossed 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;
  width: min(480px, 100%); /* (520 -> 480) */
  height: 100%;
  padding: 32px 40px 22px; /* (bottom 40 -> 22) */
  overflow: hidden;
  /* No shadow along its edge: the blur behind the drawer already separates it from the
     page, and the cast darkened the strip of page it was meant to lift away from */
  background: var(--white);
  /* 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 (isolation below). */
.menu-card { isolation: isolate; }
.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 -- the pin nets' dots (legacy-hero.css #hand-pin-net) 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: -62px;
  bottom: -54px;
  width: 280px; /* (340 -> 280) */
  height: 280px;
  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: 40px;
  flex: none;
}

.menu-close {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  margin-right: -8px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink);
  cursor: pointer;
  transition: transform 0.35s var(--ease-out-2);
}

.menu-close:hover { transform: rotate(90deg); }
.menu-close svg { display: block; }

/* ---- 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: 48px; /* room for the legal row on the floor */
  gap: clamp(20px, 3.4vh, 36px);
  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: clamp(4px, 1.4vh, 16px);
  /* 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(38px, 14.2cqw, 94px); /* (13cqw / 85px -> 14.2cqw / 94px: bigger) */
  font-weight: 300;
  line-height: 1;
  letter-spacing: -0.06em; /* (-0.045 -> -0.06: 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: 20px;
  font-weight: 300;
  line-height: 1.3;
  letter-spacing: -0.01em;
  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(110%);
}
.menu-overlay:not(.is-open) .menu-social-note .rli { transform: translateY(110%); }

/* Never both on the line at once: whichever is leaving goes quickly and alone, and the
   other only starts once the line is clear. Both used to inherit the drawer's own 0.7s
   entrance, which a 0.06s stagger could not begin to separate. */
.menu-overlay.is-open .menu-social-row.is-copied .menu-social a .rli {
  transform: translateY(-110%);
  transition: transform 0.32s var(--ease-in);
}

.menu-overlay.is-open .menu-social-row.is-copied .menu-social-note .rli {
  transform: translateY(0);
  transition: transform 0.55s var(--ease-out-soft) 0.34s;
}

/* 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 0.32s var(--ease-in);
}

.menu-overlay.is-open .menu-social-row.has-copied:not(.is-copied) .menu-social a .rli {
  transition: transform 0.55s var(--ease-out-soft) 0.34s;
}
.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: 28px;
}

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

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

/* The offices: two columns, the city against its time (as in the other drafts) */
.menu-clocks {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(8px, 1.4vh, 14px) 24px;
  padding-top: clamp(16px, 3vh, 28px);
  border-top: 1px solid rgba(20, 20, 20, 0.1);
  font-size: 14px;
  letter-spacing: 0.02em;
}

.menu-clocks li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  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; }

/* Privacy and Cookies stand in the foot now, where his name used to: the drawer's last
   line is the small print, not a person */
.menu-legal {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  gap: 18px;
  font-size: 12px;
  font-weight: 300;
  letter-spacing: 0.02em;
  transition: opacity 0.22s ease, translate var(--dur-base) var(--ease-out);
}

/* With the form up they step aside for the way back, the way his name used to */
.menu-card.is-form .menu-legal {
  position: absolute;
  opacity: 0;
  translate: -8px 0;
  pointer-events: none;
}

.menu-legal a { color: var(--muted); text-decoration: none; transition: color 0.25s ease; }
.menu-legal a:hover { color: var(--ink); }

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

/* Out of the flow, at the row's left: in it, its hidden box was a third flex item and the
   space-between row overflowed -- which pushed 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 (it was two nbsp after a "<" glyph) */
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink);
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.12em; /* = 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;
}

/* The team's cards' type (sections.css .face-cap), the name a size up: it is short now */
.menu-foot .label { font-size: 12px; letter-spacing: 0.16em; }
.menu-foot .caption { font-size: 12px; font-weight: 300; color: var(--muted); 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; /* (12) 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: 13px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  /* One declaration only: a second `transition` used to sit at the end of this rule and
     override the first, so the pill's shape snapped instead of easing */
  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; /* (6) photo to the label */
  /* The portrait is a cut-out: on the white pill it had nothing to stand on */
  border-radius: 50%;
  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: 50%;
}

/* 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 the pill's own action, not a label beside a portrait: it carries a little
     more weight than "contact" does (13 -> 15). The mask's width is measured from this
     element in ui.js, so it follows on its own. */
  font-size: 15px;
}

/* (the entrance rule below is four classes deep and was overriding the rest state, so both
   labels stood in the pill at once) */
.menu-overlay.is-open .menu-card:not(.is-form) .menu-swap .rli-back { transform: translateY(110%); }
.menu-overlay:not(.is-open) .menu-swap .rli-back { transform: translateY(110%); }

.menu-overlay.is-open .menu-card.is-form .menu-swap .rli:first-child { transform: translateY(-110%); 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 "contact" was: send is the form's one action (26 -> 42 -> 40 a side) */
  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(110%);
  transition: transform 0.55s var(--ease-in);
  will-change: transform;
}

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

/* Closed: the lines drop back at once (the drawer is off the edge by then). Otherwise a
   reopen within the old slow drop caught them half-way and they twitched 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-social li:nth-child(2) .rli { transition-delay: 0.32s; }
.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-legal li:nth-child(1) .rli { transition-delay: 0.5s; }
.menu-overlay.is-open .menu-legal li: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 it made
   the swap back to "let's talk" wait half a second after the button was 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 ---- */
/* The foot already shows Ivan: the form's own avatar would be the second one */
/* 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: 22px; /* (16 -> 22: the portrait has a ground of its own now and needs the air) */
}

.contact-ava {
  flex: none;
  width: 52px;
  height: 52px;
  /* The portrait is a cut-out: on the white card it had nothing to stand on */
  border-radius: 50%;
  background: var(--ava-ground);
}

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

/* 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 host's .is-form -- the drawer
   here, the presence section later) */
.form-rv .rl {
  display: block;
  overflow: hidden;
  padding-bottom: 0.12em;
  margin-bottom: -0.12em;
}

.form-rv .rli {
  display: block;
  transform: translateY(110%);
  transition: none;
}

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

.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, 40px);
  width: 100%;
}

.contact-head {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

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

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

.contact-fields {
  display: flex;
  flex-direction: column;
  gap: 18px; /* (10 -> 18: lines need air between them, a box's own edges gave 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: 52px;
  padding: 22px 0 6px;
  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 rgba(20, 20, 20, 0.18);
  font: 400 15px/1.3 'Archivo', sans-serif;
  color: var(--ink);
  outline: none;
  transition: box-shadow 0.25s var(--ease-out-soft);
}

.field-input:hover { box-shadow: inset 0 -1px 0 rgba(20, 20, 20, 0.34); }
/* 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: 14px;
  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-soft), translate 0.22s var(--ease-out-soft), 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: 4px;
  translate: 0 0;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink);
}

.field-error {
  position: absolute;
  right: 0;
  top: 4px;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.1em;
  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 (it used to add a blank row under the actions) */
  left: 0;
  right: 0;
  top: 50%;
  translate: 0 -50%;
  display: flex;
  align-items: center;
  gap: 14px;
  font-size: 15px;
  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: 22px;
  height: 22px;
  border-radius: 50%;
  box-shadow: inset 0 0 0 1px var(--ink);
}
.form-done-dot {
  position: absolute;
  inset: 7px;
  border-radius: 50%;
  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;
  gap: 16px;
  height: 64px;
  padding: 2px 40px 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: 13px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.2em;
  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.
   There used to be an expanding ghost of the pill with it. It cannot be part of a shared
   rule: it is an opaque shape half again the button's size, and in the drawer's foot it
   swept straight over Privacy / Cookies. */
: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);
}

/* (.on-white-shadow was a fourth set of values for the same idea; the pair above covers it
   and the class is left inert rather than chased through every template) */

/* One drawing for every icon on the page: a 12x12 box, 1.4 strokes, round caps and joins
   -- the burger and the client pill's arrow were already drawn this way, while the arrows
   and pluses were Poppins glyphs standing in for icons, at the font's weight and with the
   font's ends. They are drawn now too. */
.ico {
  display: block;
  overflow: visible;
}

/* "discover more" — the chevron points down */
.btn-secondary {
  /* (6 -> 14) The old chevron was a ">" in a 21px box with the glyph centred in it, so it
     carried its own slack on both sides. The drawn one fills its box, and both the gap and
     the right padding had to give that space back -- the sides were 40 and 28 for the same
     reason, and now that the icon has no phantom width of its own they are even. */
  gap: 14px;
  height: 56px; /* (64 -> 56: 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 33px 0 36px; /* (40/40 -> 36/33: trimmed by eye, the right a touch more) */
}

/* 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. (A row of rising bars was tried here and read as a signal meter,
   which is a different promise entirely.) */
.btn-meter {
  position: relative;
  display: inline-block;
  width: 17px; /* (24 -> 17) */
  height: 2px; /* (1.5 -> 2) */
  border-radius: 1px;
  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-soft);
}

:is(.btn, .menu-talk):hover .btn-meter::after { width: 100%; }

/* 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: 12px;
}

/* Icon + label + arrow (CTA section) */
.btn-with-icon {
  padding: 2px 30px 0 32px; /* (0 28 0 32 -> 2 30 0 28 -> 2 30 0 32: the 8px dot needs more air than the 24px icon did; keeps the 2px caps nudge) */
}

/* ---- Footer layer ------------------------------------------------------------------
   The footer is a sibling of .page-content (still in the normal flow, so it scrolls with
   the page) but sits UNDER the glass panels and the front canvas: the site reads as
   riding up off it, with the refraction and the drifting chunks passing over.
   --fr 0..1 (ui.js) = how far the footer has come into the viewport; its content lags
   behind the scroll while it rises, so it looks pushed up from below rather than
   scrolled in. */
/* The footer's own layer: over the glass panels, so their seams do not cut across it, and
   over the front canvas too, so no drifting chunk glows on the dark plane.
   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, on
   the dark footer and the dark case cards 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 glass cursor -- a few blue shards that part around the cursor the way the
   Hero's grains do. (It replaced the cursor trail, which never read on the white.) */
.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;   /* (24 -> 20) */
  height: 20px;
  margin: -10px 0 0 -10px;
  border-radius: 50%;
  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-dev) + 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 .45s ease, height .45s ease, margin .45s ease, border-radius .45s 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 inverted the white disc; flipping it late showed 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: 8;
  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. The dark primary button keeps its none. */
/* White on white: 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: against the
     dark plane a dark shadow 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 hand sheds chunks (main.js peel phase), 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; /* (44 -> 78: "Our native language." on one line, ~73vw at 120px) */
  margin-top: -56px; /* (-10 -> -56: the heading + CTA block sits a little higher) */
  pointer-events: auto;
}

.hero-head {
  gap: 24px; /* (20 -> 24) 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, so the button read as indented. A small pull back is enough: -20px overhung the
     gutter too far. */
  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: 15px;
  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;
}

/* Mouse scroll hint — bottom center */

/* ---- 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);
  transition: translate 0.45s var(--ease-out-soft);
  right: 27px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  height: 320px;
  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); hidden until the hero text plays (.is-ready) */
  opacity: 0;
  transition: opacity 0.45s ease;
}

.scroll-progress.is-ready {
  opacity: 1;
}

.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 - 64px - 96px);
  right: 64px;
  height: 96px;
  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;
}

@keyframes cueRun {
  0% { transform: translateY(-22px); }
  100% { transform: translateY(96px); }
}

.sp-label {
  position: absolute;
  right: 12px;
  bottom: 0;
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  line-height: 1;
  /* Pre-inverted for the difference blend: on the #dde0f2 page this reads as ~#83848c grey */
  color: #5a5c66;
  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
   (sections.css only stops the CTA's and the cursor's copy of it -- the menu pill's and
   the contact button's live here). 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; }
}

/* 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;
  min-height: 4px;
  /* A 121px-wide hit area around the 1px track (the track only paints the content box) */
  margin: 0 -60px;
  padding: 0 60px;
  background: #1c1c1c;
  background-clip: content-box;
  pointer-events: auto;
  cursor: pointer;
  transform-origin: center;
  transition: transform 0.25s var(--ease-out-soft);
}

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

/* 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: 16px;
  top: 0;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 5px;
  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-soft); /* 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: 10px;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

/* The one after: plain body type, caption-small and muted */
.sp-now-next {
  font-size: 11px;
  font-weight: 300;
  letter-spacing: 0.02em;
  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) {
  :root {
    --hero-pin: 0px; /* no hand on mobile -- nothing to peel */
  }

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