.onas-root {
  --onas-ink: var(--color-ink, #191818);
  --onas-paper: var(--color-surface, #f4f4f4);
  --onas-orange: var(--color-orange, #e25327);
  --onas-blue: var(--color-blue, #1b5def);
  --onas-pink: color-mix(in oklch, var(--onas-orange) 50%, var(--onas-blue));
  --onas-purple: color-mix(in oklch, var(--onas-blue) 72%, var(--onas-orange));
  /* Each gradient is declared twice on purpose: the sRGB line is the fallback,
     the oklch line wins where interpolation spaces are supported. */
  --onas-grad: linear-gradient(118deg, var(--onas-orange), var(--onas-blue));
  /* biome-ignore lint/suspicious/noDuplicateCustomProperties: progressive fallback */
  --onas-grad: linear-gradient(118deg in oklch, var(--onas-orange), var(--onas-blue));
  --onas-grad-cool: linear-gradient(135deg, var(--onas-blue), var(--onas-pink));
  /* biome-ignore lint/suspicious/noDuplicateCustomProperties: progressive fallback */
  --onas-grad-cool: linear-gradient(135deg in oklch, var(--onas-blue), var(--onas-pink));
  --onas-line: color-mix(in oklab, var(--onas-ink) 18%, transparent);
  /* Desktop scroll-rhythm spacing tokens, generated from the RHYTHM table so the
     wallpaper triggers (JS) and the section breathing (CSS) share one source. */
  background: var(--onas-paper);
  color: var(--onas-ink);
  overflow-x: clip;
}
.onas-root h1,
.onas-root h2,
.onas-root h3 {
  font-family: var(--font-display), var(--font-sans), sans-serif;
  line-height: 0.95;
  text-wrap: balance;
}
.onas-mono {
  font-family: var(--font-mono), monospace;
  font-size: 0.8rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.onas-mask {
  display: block;
  overflow: hidden;
}
.onas-word {
  display: inline-block;
  white-space: nowrap;
}
/* No permanent will-change: GSAP's force3D promotes each glyph to a GPU layer
   only DURING its reveal tween (translate3d) and reverts to 2D at completion. A
   static will-change would keep every character permanently composited, which on
   high-DPI displays re-rasterizes/re-snaps each glyph every sub-pixel scroll frame
   (the "shimmer"). See docs/onas-smoothness-audit-2026-06-16.md. */
.onas-char {
  display: inline-block;
}
.onas-caption {
  margin-top: 0.75rem;
  opacity: 0.65;
}
.onas-mobile-copy {
  display: none;
}
/* Hero entrance — Exo Ape grammar, now pure CSS (2026-10-09). It runs on the
   FIRST PAINT of the server HTML, so the wordmark is rising while the JS bundle
   is still on the wire, and nothing can strand it hidden: no hydration gate, no
   rAF dependency, no failsafe needed. The dominant text (eyebrow pill + "REL8")
   rises from behind its mask with the signature 7deg tilt — transform only, so
   it is painted from frame one; the secondary copy fades up just behind it.
   `backwards` fill: once finished the elements hold no animated value, so the
   scroll-driven hero exit below can own their wrappers freely.
   2026-10-10: an inline script holds these paused until the hero fonts are in
   and a first frame is on screen (components/o-nas/heroEntrance.ts), and the
   curve now starts at a finite speed. The old cubic-bezier(0, 0.202, 0.204, 1)
   left with a vertical tangent: its first frame alone covered ~14% of the
   rise, so the word appeared half-risen, and any frame dropped at load
   doubled that jump. This one eases in for ~80ms, then decelerates as long. */
@media (prefers-reduced-motion: no-preference) {
  .onas-root [data-hero-block],
  .onas-root [data-hero-title] {
    animation: onas-hero-rise 1.1s cubic-bezier(0.4, 0, 0.1, 1) backwards;
  }
  .onas-root [data-hero-block] {
    animation-delay: 0.05s;
  }
  .onas-root [data-hero-title] {
    animation-delay: 0.15s;
  }
  .onas-root .onas-hero__sub,
  .onas-root .onas-hero__scroll,
  .onas-root .onas-hero__scrollcue,
  .onas-root .onas-hero__footnote {
    animation: onas-hero-soft 0.95s cubic-bezier(0.4, 0, 0.1, 1) backwards;
  }
  .onas-root .onas-hero__sub {
    animation-delay: 0.5s;
  }
  .onas-root .onas-hero__scroll,
  .onas-root .onas-hero__scrollcue {
    animation-delay: 0.6s;
  }
  .onas-root .onas-hero__footnote {
    animation-delay: 0.7s;
  }
  /* Generic rise-in blocks (the "Kim jesteśmy" intro card, section heads, the
     footer sign, etc.) are revealed by gsap.from() in a post-paint effect. Without
     this pre-hide, any [data-rise] sitting at/near the fold on first paint flashes
     in fully, then JS hides it a frame later for the scroll-reveal — seen on mobile
     as the "KIM JESTEŚMY" box flickering as the page loads. Gate it on the
     .onas-hero-ready signal: that class is added in the SAME synchronous effect that
     builds the gsap.from tweens (added just before them), so GSAP takes over the
     hide with no painted gap. Reduced motion never animates, so it is excluded here
     and shows every block immediately; the content stays in the DOM either way. */
  .onas-root:not(.onas-hero-ready) [data-rise] {
    opacity: 0;
  }
  /* After the gate lifts, blocks below the read line stay hidden until their
     reveal tween is built (lazily, as they approach — see ONasExperience). */
  .onas-root .onas-reveal-pending {
    opacity: 0;
  }
}
/* The 7deg tilt lifts each line's leading corner above its mask, so the
   from-state showed white slivers that sat still until the rise began (a
   "stuck frame"). Opacity hides them and fades in over the slow first fifth. */
@keyframes onas-hero-rise {
  from {
    transform: translateY(100%) rotate(7deg);
    opacity: 0;
  }
  20% {
    opacity: 1;
  }
}
@keyframes onas-hero-soft {
  from {
    opacity: 0;
    transform: translateY(34px) rotate(3deg);
  }
}

/* exoape-style: no visible scrollbar on the homepage (the scroll itself is native).
   Scoped via :has(.onas-root) so other pages (blog etc.) keep their thin scrollbar. */
html:has(.onas-root) {
  scrollbar-width: none;
  -ms-overflow-style: none;
}
html:has(.onas-root)::-webkit-scrollbar {
  width: 0;
  height: 0;
  display: none;
}

/* fixed difference-blend background layer (slides in like the original:
   outer slides from above while the inner counter-slides from below). Entry and
   exit are both eased ~1.3s so the wallpaper glides rather than snaps away. */
.onas-bgfx {
  position: fixed;
  inset: 0;
  z-index: 40;
  pointer-events: none;
  overflow: hidden;
  background: var(--onas-grad);
  transform: translateY(-100%);
  transition: transform 1.3s cubic-bezier(0.22, 1, 0.36, 1);
}
.onas-bgfx--on {
  transform: translateY(0);
}
.onas-bgfx__slide {
  /* Overscan must be HEIGHT-relative: margin % resolves against WIDTH, so on
     wide/short viewports margin-top: -6% over-shifted the slide up and (with the
     video's ±4% drift) left the bottom edge uncovered — exposing the CSS
     --onas-grad fallback as a lighter horizontal strip. Centre the 124% slide
     with a height-relative transform so the video covers top+bottom at every
     aspect ratio. */
  height: 124%;
  transform: translateY(100%);
  transition: transform 1.3s cubic-bezier(0.22, 1, 0.36, 1);
}
.onas-bgfx--on .onas-bgfx__slide {
  transform: translateY(-10%);
}
/* Vivid REL8 gradient video, shown in TRUE colour (no blend mode) so the copy
   on top stays solid and crisp. */
.onas-bgfx__video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  will-change: transform;
}

/* Copy painted ABOVE the wallpaper. Everything meant for reading sits on top
   and turns solid white while the wallpaper is up — the reference's recipe for
   legible type over a vivid background. With the wallpaper ON for the hero
   only, this list is the hero copy + the intro caption (which can overlap the
   hero→intro slide). */
.onas-hero__title,
.onas-hero__eyebrow,
.onas-hero__sub,
.onas-hero__scroll,
.onas-hero__footnote {
  position: relative;
  z-index: 45;
}
.onas-hero__title,
.onas-hero__sub,
.onas-hero__scroll,
.onas-hero__scrollcue,
.onas-hero__footnote,
.onas-caption {
  transition:
    color 0.6s ease,
    border-color 0.6s ease;
}
.onas-fxon .onas-hero__title {
  color: #fff;
}
.onas-fxon .onas-hero__sub,
.onas-fxon .onas-hero__scroll {
  color: #fff;
}
.onas-fxon .onas-hero__scrollcue {
  color: #fff;
}
.onas-fxon .onas-hero__footnote {
  color: rgba(255, 255, 255, 0.78);
}
.onas-fxon .onas-hero__footnote a,
.onas-fxon .onas-fn {
  color: #fff;
}
/* F3 — the smaller hero copy sits on the saturated brand gradient while the
   wallpaper is up; a faint shadow lifts it clear of the lightest orange/pink lobe.
   The giant wordmark is left clean (it already passes AA-large at ~3.2:1). Mirrors
   the mobile aurora text-shadow in the <1024px block below. */
.onas-fxon .onas-hero__sub,
.onas-fxon .onas-hero__scroll,
.onas-fxon .onas-hero__scrollcue,
.onas-fxon .onas-hero__footnote {
  text-shadow: 0 1px 16px rgba(8, 6, 12, 0.24);
}
/* During the hero→intro slide-out the intro caption can ride over the leaving
   wallpaper — whiten it while the wallpaper is up (ink on cream the rest of
   the time, when .onas-fxon is absent). */
@media (min-width: 1024px) {
  .onas-fxon .onas-caption {
    color: #fff;
    opacity: 0.8;
  }
}
/* The server HTML ships .onas-fxon (wallpaper ON) so the desktop hero paints
   finished on frame one. Reduced motion never shows the wallpaper, so it keeps
   the editorial ink-on-paper hero instead of white copy on paper. */
@media (min-width: 1024px) and (prefers-reduced-motion: reduce) {
  .onas-fxon .onas-hero__title,
  .onas-fxon .onas-hero__sub,
  .onas-fxon .onas-hero__scroll,
  .onas-fxon .onas-hero__scrollcue {
    color: var(--onas-ink);
    text-shadow: none;
  }
  .onas-fxon .onas-hero__footnote {
    color: color-mix(in oklab, var(--onas-ink) 55%, var(--onas-paper));
    text-shadow: none;
  }
  .onas-fxon .onas-hero__footnote a,
  .onas-fxon .onas-fn {
    color: var(--onas-blue);
  }
  .onas-fxon .onas-caption {
    color: inherit;
    opacity: 0.65;
  }
}

/* Content sections with PHOTOS/cards ride ABOVE the wallpaper so the slide
   transition can never cover them. Their copy stays ink on the cream page. */
.onas-intro {
  position: relative;
  z-index: 47;
}
.onas-projects {
  position: relative;
  z-index: 45;
}

/* ── tile.pt nav 1:1: ONE floating centered box that grows into the menu ──
   Closed: a 70px-tall rounded pill (radius 10px + soft shadow + light glass —
   the rounding and shadow are a DELIBERATE user-requested 1:1 exception to the
   Kinetic Brutalism radius-0 rule, measured off tile.pt 2026-07-24): big REL8
   mark left · contextual chapter label · small "porozmawiajmy" CTA · "+".
   Open: the SAME box floods cobalt and grows taller/wider (tile's transitions:
   background 0.35s, width/height 0.4s cubic-bezier(0.65,0,0.35,1)) with white
   menu rows inside — not a separate panel. The visual body lives on __col (the
   pill row + dropdown share it); __pill is just the 70px header row. */
.onas-menu-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 1001;
  display: flex;
  justify-content: center;
  padding: 1.25rem clamp(0.75rem, 3vw, 2.5rem) 0;
  pointer-events: none;
}
/* No loader to wait out any more: the pill is part of the first paint and
   fades in with the hero, before hydration, instead of after a 3.3s timer. */
@media (prefers-reduced-motion: no-preference) {
  .onas-menu-bar {
    animation: onas-menu-in 0.7s ease 0.35s backwards;
  }
}
@keyframes onas-menu-in {
  from {
    opacity: 0;
    transform: translateY(-0.75rem);
  }
}
.onas-menu-bar__col {
  pointer-events: auto;
  /* NARROW at rest. tile.pt runs 400px -> 719px on a 1440 viewport. Ours is
     27rem, not tile's 25rem: the mark + "REL8", our longer "porozmawiajmy" CTA,
     the "+" and their gaps need ~418px, and at 400px the "+" was clipped and
     the CTA lost its right margin (fixed 2026-10-09). Below ~444px of usable
     width both terms lose to 100%, so phones keep the full-width pill. */
  width: min(100%, 27rem);
  border-radius: 10px;
  overflow: hidden;
  /* tile's exact glass: white at 0.8 alpha, the faint 3px blur, the barely-there
     offset shadow (ink-token mix, no new hex). Over the cobalt chapters the
     alpha drops so the blue tints the glass — see [data-ground="dark"]. */
  background: color-mix(in oklab, var(--onas-paper) 80%, transparent);
  backdrop-filter: blur(3.1px);
  -webkit-backdrop-filter: blur(3.1px);
  box-shadow: -7px 2px 4.7px color-mix(in oklab, var(--onas-ink) 3%, transparent);
  transition:
    width 0.4s cubic-bezier(0.65, 0, 0.35, 1),
    background 0.35s ease,
    box-shadow 0.35s ease,
    backdrop-filter 0.35s ease;
}
/* The ONLY resting-state move the pill makes, and it is tile.pt's own: the box
   grows horizontally about the centre once you leave the hero. The earlier
   "docked right at the top, slides to centre on the first scroll" stage was a
   REL8 addition on top of that and was removed 2026-07-25 — three separate
   moves (dock → slide → widen) read as a restless nav; tile.pt has one. */
.onas-menu-bar[data-wide="true"] .onas-menu-bar__col {
  width: min(100%, 46rem);
}
@media (prefers-reduced-motion: reduce) {
  .onas-menu-bar__col {
    transition-duration: 0s;
  }
}
.onas-menu-bar__pill {
  display: flex;
  align-items: center;
  gap: clamp(0.75rem, 2vw, 1.35rem);
  width: 100%;
  min-height: 4.375rem;
  padding: 0 1.25rem;
}
/* Over the cobalt the glass thins to a quarter, so the blue comes through and the
   pill reads as tinted rather than white — that colour shift IS the effect. The
   blur deepens to keep the busy tide behind it readable through the thin glass.
   The COPY deliberately stays ink. Flipping it to paper was tried and measured
   worse: against the composited glass over cobalt (rgb(79,130,241)) ink scores
   4.9:1 and paper only 3.3:1, and on the lighter tide layers mid-recede paper
   fell to 1.9:1. Ink clears 3:1 in every state this pill is ever painted over. */
.onas-menu-bar[data-ground="dark"] .onas-menu-bar__col {
  background: color-mix(in oklab, var(--onas-paper) 24%, transparent);
  backdrop-filter: blur(15px);
  -webkit-backdrop-filter: blur(15px);
  box-shadow: 0 2px 10px color-mix(in oklab, var(--onas-ink) 12%, transparent);
}
/* The one thing that DOES have to invert: a cobalt-filled CTA on cobalt ground
   disappears into it. Paper fill + cobalt label keeps it the brightest object in
   the pill, which is where the CTA belongs. */
.onas-menu-bar[data-ground="dark"] .onas-menu-bar__contact {
  background: var(--onas-paper);
  color: var(--onas-blue);
}
.onas-menu-bar__logo,
.onas-menu-bar__trigger {
  color: var(--onas-ink);
}

.onas-menu-bar__logo {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  font-family: var(--font-display), sans-serif;
  font-size: 1.45rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  text-decoration: none;
  transition: color 0.35s ease;
}
.onas-menu-bar__logo-mark {
  display: inline-block;
  /* tile's logo runs 40px tall in the 70px row; 55px wide at our mark's ratio. */
  width: 55px;
  aspect-ratio: 1350 / 975;
  background: currentColor;
  -webkit-mask: url(/assets/rel8-mark-mono.svg) center / contain no-repeat;
  mask: url(/assets/rel8-mark-mono.svg) center / contain no-repeat;
}

/* The pill's one flexible slot (counter + label). min-width 0 + clip let it
   collapse to nothing in the narrow pill, so it can never push the CTA and
   the "+" out of the box. */
.onas-menu-bar__where {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 0.9rem;
  overflow: hidden;
}
/* Contextual chapter label — quiet mono wayfinding mid-pill; keyed re-mount
   replays the rise-in on each swap (tile.pt: "our craft" / "our projects"). */
.onas-menu-bar__chapter {
  flex: 1;
  min-width: 0;
  display: inline-grid;
  justify-content: start;
  overflow: hidden;
  font-family: var(--font-mono), monospace;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: color-mix(in oklab, var(--onas-ink) 62%, transparent);
  transition: opacity 0.3s ease;
}
.onas-menu-bar__chapter-text {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  animation: onas-chapter-in 0.45s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
/* Small phones: the pill has no room left for the counter. */
@media (max-width: 379px) {
  .onas-menu-bar .onas-menu-bar__count {
    display: none;
  }
}
@media (prefers-reduced-motion: reduce) {
  .onas-menu-bar__count,
  .onas-menu-bar__count-strip {
    transition: none;
  }
}
@keyframes onas-chapter-in {
  from {
    opacity: 0;
    transform: translateY(80%);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
.onas-menu-bar.is-open .onas-menu-bar__chapter {
  opacity: 0;
}

/* Chapter counter "02 / 03" ahead of the label. The units digit is a 0-9
   strip inside a one-digit window; --count (set by OnasMenu) rolls it like an
   odometer. Transform only, so a chapter change costs no layout. */
.onas-menu-bar__count {
  display: inline-flex;
  align-items: baseline;
  gap: 0.35em;
  flex: none;
  font-family: var(--font-mono), monospace;
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  line-height: 1;
  color: var(--onas-ink);
  opacity: 0;
  transform: translateY(0.4rem);
  transition:
    opacity 0.35s ease,
    transform 0.45s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.onas-menu-bar__count[data-on="true"] {
  opacity: 1;
  transform: none;
}
.onas-menu-bar.is-open .onas-menu-bar__count {
  opacity: 0;
}
.onas-menu-bar__count-num {
  display: inline-flex;
}
.onas-menu-bar__count-roll {
  display: inline-block;
  width: 1ch;
  height: 1em;
  overflow: hidden;
}
.onas-menu-bar__count-strip {
  display: block;
  /* One digit per line: the strip is exactly as wide as a mono digit. */
  width: 1ch;
  word-break: break-all;
  transform: translateY(calc(var(--count, 0) * -1em));
  transition: transform 0.7s cubic-bezier(0.65, 0, 0.35, 1);
}
.onas-menu-bar__count-total {
  color: color-mix(in oklab, var(--onas-ink) 45%, transparent);
}

/* Small solid CTA — tile's "let's talk" 1:1: 35px tall, radius 6px, Clash
   Display 15px/500 lowercase, hover = brightness + gentle scale. (Radius is
   part of the same user-requested tile exception as the pill above.) */
.onas-menu-bar__contact {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 2.1875rem;
  padding: 0 1.5rem;
  border-radius: 6px;
  background: var(--onas-blue);
  color: var(--onas-paper);
  font-family: var(--font-tile), var(--font-sans), sans-serif;
  font-size: 0.9375rem;
  font-weight: 500;
  line-height: 1.2;
  text-transform: lowercase;
  letter-spacing: 0;
  text-decoration: none;
  white-space: nowrap;
  position: relative;
  transition:
    background 0.35s ease,
    color 0.35s ease,
    filter 0.35s ease,
    transform 0.55s cubic-bezier(0.22, 1, 0.36, 1);
}
.onas-menu-bar__contact:hover {
  filter: brightness(1.25);
  transform: scale(1.05);
}

/* "+" trigger — rotates 90° on hover; while open it spins on and the vertical
   bar collapses, leaving tile's "−". */
.onas-menu-bar__trigger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.35rem;
  height: 2.35rem;
  flex: none;
  background: none;
  border: 0;
  cursor: pointer;
  padding: 0;
  transition: color 0.35s ease;
}
.onas-menu-bar__plus {
  position: relative;
  width: 16px;
  height: 16px;
  display: inline-block;
  transition: transform 0.5s cubic-bezier(0.496, 0.004, 0, 1);
}
.onas-menu-bar:not(.is-open) .onas-menu-bar__trigger:hover .onas-menu-bar__plus {
  transform: rotate(90deg);
}
.onas-menu-bar.is-open .onas-menu-bar__plus {
  transform: rotate(180deg);
}
.onas-menu-bar__plus-h,
.onas-menu-bar__plus-v {
  position: absolute;
  background: currentColor;
  transition: transform 0.35s ease;
}
.onas-menu-bar__plus-h {
  left: 0;
  right: 0;
  top: 7px;
  height: 2px;
}
.onas-menu-bar__plus-v {
  top: 0;
  bottom: 0;
  left: 7px;
  width: 2px;
}
.onas-menu-bar.is-open .onas-menu-bar__plus-v {
  transform: scaleY(0);
}

/* curtain overlay — clip-path is GSAP-driven; default collapsed at the top. It is
   visibility:hidden while closed (toggled inline by OnasMenu) so this full-screen
   fixed dark layer is OUT of the paint/compositing tree and can't pressure the hero's
   aurora into the fast-mobile-scroll repaint-skip band. */
.onas-menu {
  position: fixed;
  inset: 0;
  z-index: 1000;
  pointer-events: none;
  visibility: hidden;
  background: var(--onas-ink);
  color: var(--onas-paper);
  clip-path: polygon(0 0, 100% 0, 100% 0, 0 0);
  display: flex;
  align-items: center;
}
.onas-menu__wrap {
  width: 100%;
  max-width: 1320px;
  margin: 0 auto;
  padding: 6rem clamp(1.5rem, 5vw, 5rem) 3rem;
  display: grid;
  gap: clamp(1.75rem, 5vw, 5rem);
  align-items: center;
}
/* Brand backdrop — the REL8 mark sitting large + faint behind the links. Same Exo Ape
   luminance grammar as the loader: a paper fill scales up (bottom→top) through a FIXED
   moose mask over a dim ghost, so the artwork inks in without distorting. Centered in
   the overlay, behind .onas-menu__wrap, and dim enough that the links stay legible. */
.onas-menu__brand {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: clamp(20rem, 62vw, 56rem);
  aspect-ratio: 1350 / 975;
  pointer-events: none;
  opacity: 0.1;
  z-index: 0;
  -webkit-mask: url(/assets/rel8-mark-mono.svg) center / contain no-repeat;
  mask: url(/assets/rel8-mark-mono.svg) center / contain no-repeat;
}
.onas-menu__brand-ghost,
.onas-menu__brand-fill {
  position: absolute;
  inset: 0;
  display: block;
}
.onas-menu__brand-ghost {
  background: color-mix(in oklab, var(--onas-paper) 20%, transparent);
}
.onas-menu__brand-fill {
  background: var(--onas-paper);
  transform-origin: 50% 100%;
  transform: scaleY(0);
}
/* Keep the content above the backdrop (backdrop is z-index:0 in the flex container). */
.onas-menu__wrap {
  position: relative;
  z-index: 1;
}
@media (min-width: 768px) {
  .onas-menu__wrap {
    grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
  }
}
.onas-menu__media {
  position: relative;
  aspect-ratio: 3 / 4;
  width: 100%;
  max-width: 24rem;
  overflow: hidden;
  justify-self: center;
  background: color-mix(in oklab, var(--onas-paper) 8%, transparent);
}
/* Auto-rotating gallery: each entering image crossfades + un-zooms (echoes exoape). */
.onas-menu__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transform: scale(1.08) rotate(1.5deg);
  transition:
    opacity 0.9s ease,
    transform 1.2s cubic-bezier(0, 0.202, 0.204, 1);
}
.onas-menu__img.is-active {
  opacity: 1;
  transform: scale(1) rotate(0deg);
}
.onas-menu__nav {
  display: flex;
  flex-direction: column;
}
.onas-menu__list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.onas-menu__link {
  display: inline-block;
  text-decoration: none;
  color: var(--onas-paper);
  font-family: var(--font-display), sans-serif;
  line-height: 1.05;
  font-size: clamp(2.6rem, 7vw, 5rem);
  letter-spacing: -0.01em;
  /* near-paper white at rest so the links out-contrast the faint moose
     backdrop; hover lifts to full opacity (and nudges right). */
  opacity: 0.85;
  transition:
    opacity 0.3s ease,
    transform 0.4s ease;
}
.onas-menu__link:hover,
.onas-menu__link:focus-visible {
  opacity: 1;
  transform: translateX(0.4rem);
}
/* mask that clips each link/label so it can wipe up out of view on open */
.onas-menu__mask {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
}
.onas-menu__linkinner {
  display: block;
}
.onas-menu__linkedin {
  align-self: start;
  margin-top: clamp(1.5rem, 4vw, 2.6rem);
  text-decoration: none;
  color: color-mix(in oklab, var(--onas-paper) 72%, transparent);
  font-family: var(--font-mono), monospace;
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  transition: color 0.3s ease;
}
.onas-menu__linkedin:hover {
  color: var(--onas-paper);
}
/* Two curtain CTAs (book a call / start a project), below the nav links. Each
   label wipes up out of its mask on open, in step with the LinkedIn line. */
.onas-menu__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-top: clamp(1.6rem, 4vw, 2.8rem);
}
.onas-menu__cta {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.9rem 1.5rem;
  border: 1px solid var(--onas-paper);
  background: transparent;
  color: var(--onas-paper);
  cursor: pointer;
  font-family: var(--font-sans), sans-serif;
  font-weight: 600;
  font-size: 0.95rem;
  transition:
    background 0.25s ease,
    color 0.25s ease,
    border-color 0.25s ease;
}
.onas-menu__cta:hover,
.onas-menu__cta:focus-visible {
  background: var(--onas-paper);
  color: var(--onas-ink);
}
.onas-menu__cta--primary {
  background: var(--onas-blue);
  border-color: var(--onas-blue);
  color: var(--onas-paper);
}
.onas-menu__cta--primary:hover,
.onas-menu__cta--primary:focus-visible {
  background: var(--onas-paper);
  border-color: var(--onas-paper);
  color: var(--onas-ink);
}
@media (prefers-reduced-motion: reduce) {
  .onas-menu-bar,
  .onas-menu-bar__col,
  .onas-menu-bar__pill,
  .onas-menu-bar__plus,
  .onas-menu-bar__plus-h,
  .onas-menu-bar__plus-v,
  .onas-menu-bar__contact,
  .onas-menu-bar__chapter,
  .onas-menu__img,
  .onas-menu__link,
  .onas-menu__linkinner,
  .onas-menu__cta {
    transition: none;
  }
  .onas-menu-bar__chapter-text {
    animation: none;
  }
}

/* hero — paper + ink like the reference; all color arrives via .onas-bgfx
   (difference), so with reduced motion this stays a clean editorial hero */
.onas-hero {
  position: relative;
  min-height: 100svh;
  display: flex;
  align-items: center;
  background: var(--onas-paper);
  padding: 6rem 1.25rem 4rem;
  overflow: clip;
}
@media (min-width: 1024px) {
  .onas-hero {
    padding: 4rem 8rem 4rem 4rem;
  }
}
.onas-hero__inner {
  position: relative;
  max-width: 60rem;
}
/* lamalama cursor-trail canvas: covers the hero, paints ABOVE the fixed
   wallpaper (z-40) and the hero copy (45-47) like lamalama's upper canvas,
   never catches the pointer. Desktop-only — the component also gates itself. */
.onas-hero__dots {
  position: absolute;
  inset: 0;
  z-index: 48;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
@media (max-width: 1023px) {
  .onas-hero__dots {
    display: none;
  }
}
@media (prefers-reduced-motion: reduce) {
  .onas-hero__dots {
    display: none;
  }
}
.onas-hero__eyebrow {
  display: inline-block;
  background: var(--onas-ink);
  color: var(--onas-paper);
  font-family: var(--font-mono), monospace;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  font-size: 0.8rem;
  padding: 0.55rem 0.9rem;
}
.onas-hero__title {
  margin-top: 1.2rem;
  color: var(--onas-ink);
}
.onas-hero__titleline {
  font-size: clamp(5.5rem, 22vw, 19rem);
  letter-spacing: -0.02em;
  line-height: 0.9;
}
/* The "REL8" wordmark rises as one masked line, so the inner node must be
   inline-block for the yPercent/rotation transform (the mask clips the overflow). */
.onas-hero__titleline [data-hero-title] {
  display: inline-block;
  transform-origin: 50% 100%;
}
.onas-hero__sub {
  margin-top: 1.6rem;
  max-width: 34rem;
  color: var(--onas-ink);
  font-size: clamp(1.05rem, 2vw, 1.35rem);
  line-height: 1.5;
}
.onas-hero__scroll {
  display: inline-block;
  margin-top: 2.2rem;
  color: var(--onas-ink);
  background: none;
  border: 0;
  cursor: pointer;
  text-decoration: none;
  font-family: var(--font-mono), monospace;
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  border-bottom: 1px solid currentColor;
  padding: 0 0 0.3rem;
}
/* Desktop informational scroll cue — exoape "use the corners". Non-interactive
   (no underline; it is a label, not a link). Hidden by default so mobile keeps
   the clickable .onas-hero__scroll button; the media rules below reveal it on
   desktop and hide the button. The down-arrow bounces on its own inner span so
   its transform never fights the GSAP reveal on the cue root. */
.onas-hero__scrollcue {
  display: none;
  align-items: center;
  gap: 0.6rem;
  color: var(--onas-ink);
  font-family: var(--font-mono), monospace;
  font-size: 0.8rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  font-weight: 500;
  line-height: 1.3;
  white-space: nowrap;
  z-index: 45;
}
.onas-hero__scrollcue-arrow {
  display: inline-block;
}
@media (prefers-reduced-motion: no-preference) {
  .onas-hero__scrollcue-arrow {
    animation: onas-scrollcue-bob 2s ease-in-out infinite;
  }
}
@keyframes onas-scrollcue-bob {
  0%,
  100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(4px);
  }
}
@media (min-width: 1024px) {
  /* Desktop drops the clickable CTA; the footnote rises to hug the lead. */
  .onas-hero__scroll {
    display: none;
  }
  .onas-hero__footnote {
    margin-top: 1.8rem;
  }
}
@media (min-width: 1024px) and (max-width: 1279px) {
  /* Small-laptop fallback: a corner cue would collide with the footnote, so the
     hero stacks and the cue sits right-aligned just below the content. */
  .onas-hero {
    flex-direction: column;
    align-items: flex-start;
    justify-content: center;
  }
  .onas-hero__scrollcue {
    display: inline-flex;
    align-self: flex-end;
    margin-top: 2rem;
  }
}
@media (min-width: 1280px) {
  /* True bottom-right corner, on the hero's own padding grid (clears the rail). */
  .onas-hero__scrollcue {
    display: inline-flex;
    position: absolute;
    right: 8rem;
    bottom: 4rem;
  }
}
/* Book-style source footnote under the hero stat (the asterisks tie back to the
   8.4% Eurostat / 40% Future Mind figures in the sub-paragraph). */
.onas-hero__footnote {
  display: block;
  margin-top: 2.4rem;
  max-width: 38rem;
  font-size: 0.72rem;
  line-height: 1.6;
  letter-spacing: 0.01em;
  color: color-mix(in oklab, var(--onas-ink) 55%, var(--onas-paper));
}
.onas-hero__footnote a {
  color: var(--onas-blue);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
}
.onas-hero__footnote a:hover {
  text-decoration-thickness: 2px;
}
.onas-fn {
  color: var(--onas-blue);
  font-weight: 700;
}

/* intro */
.onas-intro {
  display: grid;
  gap: 2.5rem;
  padding: 0 1.25rem 6rem;
  /* z-index 47 keeps the intro photo/card ABOVE the leaving wallpaper at the
     hero→intro boundary so the slide transition never inverts it (the
     "overexposed photo" bug). */
  margin-top: -4.5rem;
  position: relative;
  z-index: 47;
}
@media (min-width: 1024px) {
  .onas-intro {
    /* Clean hero→intro seam: drop the base -4.5rem overlap on desktop so the
       sticky read column (≥1280px) sticks AFTER the hero footnote has scrolled
       off the top — otherwise the opaque dark card crosses over the footnote
       citations mid-scroll (z-47 over z-45). */
    margin-top: 0;
    padding: 0 8rem 8rem 4rem;
    gap: 4rem;
    max-width: 78rem;
  }
  .onas-intro__read {
    display: block;
  }
  .onas-intro__card {
    max-width: 62rem;
  }
  .onas-intro__photo {
    max-width: 54rem;
    margin-left: 0;
  }
}
@media (min-width: 1280px) {
  .onas-intro {
    width: min(100%, 128rem);
    max-width: none;
    min-height: clamp(34rem, 68vh, 46rem);
    grid-template-columns: repeat(12, minmax(0, 1fr));
    gap: 0 clamp(1.5rem, 2.6vw, 3.25rem);
    align-items: start;
    padding: 0 clamp(5rem, 7vw, 8rem) 2rem 4rem;
  }
  .onas-intro__read {
    grid-column: 1 / 9;
    grid-row: 1;
    position: sticky;
    top: clamp(5rem, 10vh, 8rem);
    z-index: 3;
    max-width: 62rem;
  }
  .onas-intro__photo {
    grid-column: 8 / 13;
    grid-row: 1;
    z-index: 2;
    position: sticky;
    top: clamp(7rem, 14vh, 10rem);
    align-self: start;
    max-width: min(100%, 50rem);
    margin-top: clamp(5rem, 10vh, 8rem);
    margin-left: auto;
    overflow: clip;
    border: 1px solid var(--onas-line);
    background: transparent;
    will-change: clip-path, transform;
  }
  .onas-intro__photo > div {
    overflow: clip;
  }
  .onas-intro__photo .onas-caption {
    display: none;
  }
}
.onas-intro__card {
  background: var(--onas-ink);
  color: var(--onas-paper);
  padding: 2.5rem 2rem;
  /* Anchors the stretched open-button (mobile overrides with position:sticky
     for the pin choreography, which also anchors absolutely-positioned kids). */
  position: relative;
}
@media (min-width: 1024px) {
  .onas-intro__card {
    padding: 4rem 3.5rem;
  }
}
/* Stretched open-button (zajno clickable-card pattern): covers the whole card
   so any click/tap opens the team overlay; invisible, keyboard-focusable, with
   the focus ring drawn INSIDE the card bounds. The actions row sits above it. */
.onas-intro__card-open {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}
.onas-intro__card-open:focus-visible {
  outline: 2px solid var(--onas-blue);
  outline-offset: -2px;
}
/* The global base sheet sets h1–h4 to ink via a direct element rule, which
   beats the card's inherited light colour — without this the heading would be
   ink-on-ink (invisible). */
.onas-intro__card h2 {
  color: var(--onas-paper);
  font-size: clamp(2rem, 4.5vw, 3.6rem);
  margin: 1.2rem 0;
}
.onas-intro__card p:last-of-type {
  line-height: 1.65;
  opacity: 0.85;
  max-width: 36rem;
}
/* Action buttons under the intro body (team overlay + LinkedIn). Light treatment
   because the card is dark (ink bg). Reset the .onas-btn top margin inside the
   flow row so the two buttons align on one baseline. z-index 2 lifts the row
   above the stretched open-button so the links stay clickable. */
.onas-intro__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.85rem;
  margin-top: 2rem;
  position: relative;
  z-index: 2;
}
.onas-intro__actions .onas-btn {
  margin-top: 0;
}
.onas-intro__photo img {
  width: 100%;
  height: auto;
  display: block;
}

/* section head */
.onas-section-head h2 {
  font-size: clamp(2.2rem, 5.5vw, 4.5rem);
  margin-top: 1rem;
  max-width: 60rem;
  overflow: hidden;
}

/* ── 03b — colour immersion (tile.pt "meet tile." grammar) ──
   The flood/drain scenes are DESKTOP-ONLY decoration (hidden <1024px and under
   reduced motion); the immersed manifesto is real content on every viewport.
   Layer geometry: each layer parks BELOW its sticky viewport (top:100%,
   height:200vh = wave crest + fill) and the scrub slides it up by -62.5% of
   its own height (-125vh), parking the crest above the top edge with margin so
   the fill fully covers the scene; the drain reverses the same numbers. */
.onas-flood,
.onas-drain {
  display: none;
}
.onas-immersed {
  position: relative;
  z-index: 46;
  background: var(--onas-blue);
  color: var(--onas-paper);
  padding: 5rem 1.25rem 4.5rem;
  overflow: clip;
}
.onas-root .onas-immersed__static-greeting {
  position: relative;
  z-index: 1;
  margin: 0 0 clamp(2.5rem, 7vw, 4.5rem);
  color: var(--onas-paper);
  font-family: var(--font-tile), var(--font-display), sans-serif;
  font-size: clamp(2.75rem, 10vw, 7rem);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 0.95;
}
.onas-immersed__static-greeting-line {
  display: block;
}
.onas-immersed__static-greeting-accent {
  color: var(--onas-orange);
}
.onas-immersed__static-greeting em {
  font-style: normal;
  font-weight: inherit;
}
/* Manifesto = dual-layer text-fill (tile.pt): dim base + white copy wiped in
   by the scrub. Default CSS leaves the fill un-clipped, so no-JS, reduced
   motion and ?perf=no-scrub all read full white. */
/* The .onas-root prefix is load-bearing: the .onas-root h1/h2/h3 rule (0,1,1)
   sets font-family AND line-height, and it outranks a bare class (0,1,0) —
   without the extra class this rule's face and leading are silently discarded
   and the manifesto keeps rendering in halfre at line-height 0.95. */
.onas-root .onas-immersed__manifesto {
  position: relative;
  z-index: 1;
  display: grid;
  color: var(--onas-paper); /* wins over the .onas-root h2 ink default */
  max-width: 56rem;
  /* tile.pt's own face and metrics for its manifesto: Clash Display 400,
     line-height 1.0, letter-spacing -0.02em. The accent words carry weight
     instead of the italic we use elsewhere — Clash Display ships no italic, and
     a synthetic oblique on a display face at this size is visibly skewed. */
  font-family: var(--font-tile), var(--font-display), sans-serif;
  /* tile.pt's exact ratio, re-measured 2026-07-19 leaf by leaf: their manifesto
     lines compute to 63.36px on a 1440 viewport = 4.4vw, line-height 1.0,
     letter-spacing -0.02em, weight 400 with accent words at 700. An earlier pass
     read ~86px off the wrong element and sized us UP to 5vw (72px) on it; this
     is the corrected value, so the block gets ~12% smaller, not bigger. */
  font-size: clamp(2.2rem, 4.4vw, 4.4rem);
  line-height: 1.02;
  font-weight: 400;
  letter-spacing: -0.02em;
}
/* 700, matching tile's accent cut ("design", "craftsmanship") against their 400 body. */
.onas-immersed__manifesto em {
  font-style: normal;
  font-weight: 700;
}
.onas-immersed__manifesto-base,
.onas-immersed__manifesto-fill {
  grid-area: 1 / 1;
}
.onas-immersed__manifesto-base {
  color: color-mix(in oklab, var(--onas-ink) 42%, var(--onas-blue));
}
.onas-immersed__manifesto-fill {
  color: var(--onas-paper);
}
/* (Value cards removed 2026-07-24 — the manifesto is the whole beat now. The
   faint bottom-right moose mark went with them: it read as a second moose
   ghosting behind the hanging-tiles etching.) */
/* Underwater ambient layer — desktop only (phones skip the diorama). */
.onas-immersed__sea {
  display: none;
}
.onas-immersed__tide {
  display: none;
}
@media (max-width: 1023px) {
  /* Phone immersion (2026-09-03): the section paints no background of its
     own — the tide layer is the water. Its crest swells continuously and the
     scrub slides the whole layer up as the chapter enters, so the blue rises
     over the paper instead of cutting in. Bubbles drift up through it. */
  .onas-immersed {
    background: transparent;
    padding-top: calc(4rem + 18vh);
  }
  .onas-immersed__tide {
    display: flex;
    flex-direction: column;
    position: absolute;
    inset: 0;
    z-index: 0;
    color: var(--onas-blue);
    pointer-events: none;
    will-change: transform;
  }
  .onas-immersed__tide .onas-flood__wave {
    width: 100%;
    height: 18vh;
    flex: none;
    display: block;
    overflow: visible;
  }
  /* -6px overlap: a 1px seam showed between the crest and the fill at 3x DPR. */
  .onas-immersed__tide-fill {
    flex: 1;
    background: currentColor;
    margin-top: -6px;
  }
  .onas-immersed__tide .onas-flood__swell {
    animation: onas-flood-swell 4.5s ease-in-out infinite alternate;
  }
  .onas-immersed__tide .onas-flood__ripple {
    fill: none;
    stroke: color-mix(in oklab, var(--onas-paper) 55%, transparent);
    stroke-width: 2;
    stroke-linecap: round;
  }
  .onas-immersed__sea {
    display: block;
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
  }
  .onas-sea__etch {
    display: none;
  }
  .onas-sea__bubble {
    position: absolute;
    bottom: -2rem;
    border-radius: 50%;
    border: 1px solid color-mix(in oklab, var(--onas-paper) 50%, transparent);
    animation: onas-sea-bubble 12s linear infinite;
  }
  .onas-sea__bubble--0 {
    left: 18%;
    width: 9px;
    height: 9px;
  }
  .onas-sea__bubble--1 {
    left: 34%;
    width: 6px;
    height: 6px;
    animation-delay: -4s;
    animation-duration: 11s;
  }
  .onas-sea__bubble--2 {
    left: 57%;
    width: 11px;
    height: 11px;
    animation-delay: -8s;
  }
  .onas-sea__bubble--3 {
    left: 74%;
    width: 7px;
    height: 7px;
    animation-delay: -2s;
    animation-duration: 13s;
  }
  .onas-sea__bubble--4 {
    left: 88%;
    width: 5px;
    height: 5px;
    animation-delay: -10s;
    animation-duration: 10s;
  }
  /* Masks for the greeting's character rise. */
  .onas-immersed__static-greeting-line {
    overflow: hidden;
  }
}
@media (prefers-reduced-motion: reduce) {
  .onas-immersed__tide .onas-flood__swell,
  .onas-sea__bubble {
    animation: none;
  }
}
@media (min-width: 1024px) {
  .onas-immersed__static-greeting {
    display: none;
  }
  /* pointer-events:none is load-bearing, not tidiness. Both scenes are
     aria-hidden decoration painted at z-46, ABOVE .onas-craft at z-45 (that
     ordering is what lets the receding tide uncover the section). The drain's
     sticky layer is a full-viewport box, and .onas-craft is lifted up into it by
     the handover — so without this the heading and the top accordion row sit
     under an invisible sheet that eats every hover and click. */
  .onas-flood,
  .onas-drain {
    display: block;
    position: relative;
    z-index: 46;
    pointer-events: none;
  }
  /* 150svh (was 200) shortens the post-watermark pinned tail so the lockup
     interleaves with the rising manifesto (tile.pt "meet tile." grammar) instead
     of holding ~100svh of empty cobalt. Pin travel = 150-100 = 50svh. */
  .onas-flood {
    height: 150svh;
  }
  /* 150svh (was 170) tightens the recede so the tide is already pulling back as
     the section pins (paired with the drain trigger's top 85% pre-roll). */
  .onas-drain {
    height: 150svh;
  }
  .onas-flood__sticky,
  .onas-drain__sticky {
    position: sticky;
    top: 0;
    height: 100svh;
    overflow: clip;
  }
  /* tile.pt watermark: white, revealed with the water (z above the layers),
     plain display prefix + italic brand suffix. */
  .onas-flood__watermark {
    position: absolute;
    inset: 0;
    z-index: 4;
    display: grid;
    place-items: center;
    margin: 0;
    /* tile.pt's "meet tile." verbatim: Clash Display 600, -0.02em, one weight
       across the whole line (their brand word is not set apart either). */
    font-family: var(--font-tile), var(--font-display), sans-serif;
    font-weight: 600;
    /* tile.pt's centre wordmark measures 120px at 1440 = 8.33vw (we were at
       8.8vw / 127px). A small correction — it was already close — but the ask
       was to match both sizes in this chapter, so both are now their ratios. */
    font-size: clamp(4.5rem, 8.33vw, 9.5rem);
    letter-spacing: -0.02em;
    line-height: 1;
    white-space: nowrap;
    color: var(--onas-paper);
    opacity: 0; /* fades in via the flood scrub tween (desktop-only, like the scene) */
  }
  .onas-flood__watermark-greeting {
    color: var(--onas-orange);
  }
  .onas-flood__watermark-line {
    display: block;
  }
  .onas-flood__watermark em {
    font-style: normal;
    font-weight: inherit;
  }
  .onas-flood__layer {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    height: 200vh;
    display: flex;
    flex-direction: column;
    will-change: transform;
  }
  /* Depth palette — brand blue mixed toward paper, no new hex codes. */
  .onas-flood__layer--1 {
    z-index: 1;
    color: color-mix(in oklab, var(--onas-blue) 32%, var(--onas-paper));
  }
  .onas-flood__layer--2 {
    z-index: 2;
    color: color-mix(in oklab, var(--onas-blue) 62%, var(--onas-paper));
  }
  .onas-flood__layer--2 .onas-flood__wave {
    transform: scaleX(-1);
  }
  /* Painted cobalt tide (Veo frame sequence, alpha-keyed to flat --onas-blue).
     Parked below the fold at rest so ?perf=no-scrub never shows it; the scrub
     slides it up, then the in-image water level rises frame by frame. */
  .onas-flood__frames {
    position: absolute;
    inset: 0;
    z-index: 3;
    transform: translateY(101%);
    will-change: transform;
  }
  .onas-flood__frames img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
  }
  .onas-flood__wave {
    width: 100%;
    height: 22vh;
    flex: none;
    display: block;
    overflow: visible;
  }
  /* Tuck the fill UNDER the wave deeper than the swell's peak excursion
     (-20px, see @keyframes onas-flood-swell): a -1px overlap left a full-width
     paper/lighter-layer seam that opened and closed as the idle swell lifted the
     crest group. -26px (= excursion + 6px) keeps the same-currentColor fill
     beneath the crest at peak swell. The crest + ripples sit high in the viewBox
     (y≤224 of 260), so this bottom overlap never covers them. */
  .onas-flood__fill {
    flex: 1;
    background: currentColor;
    margin-top: -26px;
  }
  /* Hand-drawn ripple strokes riding inside the crest (tile's ink details);
     non-scaling stroke keeps them hairline under preserveAspectRatio:none. */
  .onas-flood__ripple {
    fill: none;
    stroke: color-mix(in oklab, var(--onas-ink) 32%, currentColor);
    stroke-width: 2.5;
    stroke-linecap: round;
    vector-effect: non-scaling-stroke;
  }
  /* Slow swell keeps the crest alive between scrub ticks. */
  .onas-flood__swell {
    animation: onas-flood-swell 4.5s ease-in-out infinite alternate;
  }
  .onas-flood__layer--2 .onas-flood__swell {
    animation-duration: 5.5s;
    animation-delay: -3.2s;
  }
  /* padding-top 8svh (was 22): the right-aligned manifesto enters the moment the
     flood unpins — the last painted frame is flat --onas-blue ≈ the immersed
     background, so the hand-off is a seamless continuous cobalt.
     padding-bottom 6svh (was 20): kills the dead flat-cobalt beat so the drain
     starts receding within ~½ viewport of the value cards leaving. */
  /* padding-top 3svh (was 8): the gap between the wordmark's resting baseline and
     the manifesto's first line is what sets how soon the manifesto shows up once
     the title lands. tile.pt runs ~0.44 viewports; 8svh put us at 0.52. */
  .onas-immersed {
    padding: 3svh clamp(3rem, 7vw, 7rem) 6svh;
    /* With the value cards gone (2026-07-24) the manifesto is the whole beat.
       ~55svh keeps one calm blue screen-moment for it — enough hold to read,
       no dead cobalt tail before the drain's 85% pre-roll takes over — and the
       flex centering replaces the mass the cards used to add below it. The
       3svh padding-top stays untouched: HANDOVER_LIFT's geometry counts it. */
    min-height: 55svh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    /* overflow-x stays clipped (the sea etches and the drifting mark are
       deliberately bled past the right edge); overflow-y must be VISIBLE so the
       chapter-handover lift can carry the manifesto up over the flood's cobalt
       before this section's own box reaches the viewport. Re-clipping the Y
       axis silently kills that beat — the manifesto is simply not painted until
       the pin releases, which is the exact defect the lift exists to fix. */
    overflow: clip visible;
  }
  /* Safety net for the bleed that .onas-immersed used to absorb. */
  .onas-immersed__sea {
    overflow: clip;
  }
  .onas-immersed__manifesto {
    margin-left: auto;
    max-width: 62rem;
  }
  /* ── the living sea ── */
  .onas-immersed__sea {
    display: block;
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    color: color-mix(in oklab, var(--onas-ink) 36%, var(--onas-blue));
  }
  .onas-sea__etch {
    position: absolute;
  }
  /* Etchings arrive as ink-on-alpha; invert paints the hairlines white. */
  .onas-sea__etch img {
    width: 100%;
    height: auto;
    display: block;
    filter: invert(1);
  }
  .onas-sea__etch--left {
    left: 1%;
    top: 5%;
    width: clamp(15rem, 22vw, 24rem);
    opacity: 0.72;
  }
  /* Sized by HEIGHT since the 2026-07-24 section shortening (55svh): at a
     fixed 13vw width the tall two-string etching ran ~80px past the section
     bottom and the lower tile was clipped mid-scroll. 78% height + 5% top
     leaves room for the ±5% parallax drift and the pendulum sway, whatever
     the viewport; right: 4.5% (was 2%) keeps the swing off the clipped edge. */
  .onas-sea__etch--right {
    right: 4.5%;
    top: 5%;
    height: 78%;
    width: auto;
    opacity: 0.8;
  }
  .onas-sea__etch--right img {
    height: 100%;
    width: auto;
  }
  .onas-sea__pendulum {
    transform-origin: top center;
    animation: onas-sea-sway 7s ease-in-out infinite alternate;
  }
  /* The moose gets the same "object suspended in water" language as the tiles
     opposite it, but on its own terms: nothing is holding it from a point, so it
     drifts and rolls with the current instead of swinging from a pivot. Plain
     CSS on the <img> — the wrapper is where the JS parallax writes its own
     transform, so animating the wrapper would fight it (this is exactly why the
     tiles animate their inner image too). 13s against the pendulum's 7s so the
     two are never in step and the pair never reads as one mechanism. */
  .onas-sea__drift {
    transform-origin: 58% 42%;
    animation: onas-sea-drift 13s ease-in-out infinite alternate;
  }
  .onas-sea__bubble {
    position: absolute;
    bottom: -2rem;
    border-radius: 50%;
    border: 1px solid color-mix(in oklab, var(--onas-paper) 50%, transparent);
    animation: onas-sea-bubble 12s linear infinite;
  }
  .onas-sea__bubble--0 {
    left: 18%;
    width: 9px;
    height: 9px;
  }
  .onas-sea__bubble--1 {
    left: 34%;
    width: 6px;
    height: 6px;
    animation-delay: -4s;
    animation-duration: 11s;
  }
  .onas-sea__bubble--2 {
    left: 57%;
    width: 11px;
    height: 11px;
    animation-delay: -8s;
  }
  .onas-sea__bubble--3 {
    left: 74%;
    width: 7px;
    height: 7px;
    animation-delay: -2s;
    animation-duration: 13s;
  }
  .onas-sea__bubble--4 {
    left: 88%;
    width: 5px;
    height: 5px;
    animation-delay: -10s;
    animation-duration: 10s;
  }
}
@keyframes onas-flood-swell {
  from {
    transform: translateY(0);
  }
  to {
    transform: translateY(-20px);
  }
}
@keyframes onas-sea-sway {
  from {
    transform: rotate(-2.4deg);
  }
  to {
    transform: rotate(2.2deg);
  }
}
/* Buoyancy, not a swing: a slow lift-and-roll with a touch of scale so it reads
   as drifting nearer, plus a light breathe (multiplied by the wrapper's own 0.72)
   as if the light through the water shifts over it. */
@keyframes onas-sea-drift {
  from {
    transform: translate3d(0, 0, 0) rotate(-1.6deg) scale(1);
    opacity: 0.86;
  }
  to {
    transform: translate3d(1.6%, -3.6%, 0) rotate(1.9deg) scale(1.04);
    opacity: 1;
  }
}
@keyframes onas-sea-bubble {
  0% {
    transform: translateY(0);
    opacity: 0;
  }
  12% {
    opacity: 0.7;
  }
  85% {
    opacity: 0.45;
  }
  100% {
    transform: translateY(-52vh);
    opacity: 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  .onas-flood,
  .onas-drain {
    display: none !important;
  }
  .onas-root .onas-immersed__static-greeting {
    display: block;
  }
  .onas-sea__pendulum,
  .onas-sea__drift,
  .onas-flood__swell {
    animation: none !important;
  }
  .onas-sea__bubble {
    display: none;
  }
}
@media (prefers-reduced-motion: reduce) and (min-width: 1024px) {
  .onas-root .onas-immersed__static-greeting {
    font-size: clamp(4.5rem, 8.33vw, 9.5rem);
  }
}

/* ── 06b — craft accordion (tile.pt "what we do" grammar) ── */
.onas-craft {
  position: relative;
  z-index: 45;
  background: var(--onas-paper);
  padding: 4rem 1.25rem;
}
@media (min-width: 1024px) {
  .onas-craft {
    padding-inline: 4rem 8rem;
    padding-block: var(--onas-beat-craft-in) var(--onas-beat-craft-out);
  }
}
/* tile.pt sets its section headings ("what we do", "latest work") in Clash
   Display with tight tracking against a whisper grey. Our longer localized
   titles use a slightly smaller fluid scale so they stay inside the one-line
   desktop roll mask. line-height is load-bearing: the mask below is exactly
   1.2em tall, so the two have to stay in step. */
.onas-craft .onas-section-head h2 {
  font-family: var(--font-tile), var(--font-display), sans-serif;
  font-size: clamp(2rem, 3.5vw, 4rem);
  line-height: 1.2;
  letter-spacing: -0.02em;
  font-weight: 400;
  color: color-mix(in oklab, var(--onas-ink) 22%, var(--onas-paper));
}
/* Phones show one plain wrapping heading — no mask, no roll (there is no hover
   to drive it, and a one-line mask would clip a wrapped Polish string). */
.onas-craft__head-line--alt {
  display: none;
}
@media (min-width: 1024px) {
  .onas-craft .onas-section-head h2 {
    /* The mask: exactly one line box tall, so only the top line of the stack is
       ever visible. .onas-section-head h2 already sets overflow:hidden. */
    height: 1.2em;
    white-space: nowrap;
    /* Shrink the box to the words so the hover zone is the heading itself, not
       the full-width block it would otherwise stretch to. */
    width: fit-content;
    max-width: 100%;
  }
  .onas-craft__head-roll {
    display: block;
    /* tile.pt's curve, the same one the accordion rows morph on. */
    transition: transform 0.55s cubic-bezier(0.65, 0, 0.35, 1);
  }
  .onas-craft__head-line {
    display: block;
  }
  /* The stack is two line boxes, so -50% is precisely one line. */
  .onas-craft__head-line--alt {
    display: block;
    font-weight: 600;
    color: var(--onas-blue);
  }
  .onas-craft .onas-section-head h2:hover .onas-craft__head-roll,
  .onas-craft .onas-section-head:focus-within .onas-craft__head-roll {
    transform: translateY(-50%);
  }
}
@media (prefers-reduced-motion: reduce) {
  .onas-craft__head-roll {
    transition: none;
  }
}
.onas-craft__list {
  margin-top: 3rem;
  border-top: 1px solid var(--onas-line);
}
/* "zobacz więcej" cursor-follower (tile.pt "see more"): an ink pill lerped to
   the pointer over closed rows; sharp corners per Kinetic Brutalism. */
.onas-craft__cursor {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 55;
  padding: 0.55rem 1rem;
  background: var(--onas-ink);
  color: var(--onas-paper);
  font-family: var(--font-mono), monospace;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  white-space: nowrap;
  /* below-right of the pointer, tile-style; JS owns transform (direct
     translate3d, no lerp — tile.pt), so the offset lives in margins */
  margin: 1.2rem 0 0 0.9rem;
  transform: translate3d(-9999px, -9999px, 0);
  opacity: 0;
  visibility: hidden;
  transition:
    opacity 200ms ease,
    visibility 0s 200ms;
  will-change: transform, opacity;
  pointer-events: none;
}
.onas-craft__cursor[data-visible="1"] {
  opacity: 1;
  visibility: visible;
  transition:
    opacity 200ms ease,
    visibility 0s;
}
/* One pill, two words: a closed row invites ("zobacz więcej"), an open row says
   how to get out ("zamknij"). JS flips data-open on hover and on every toggle. */
.onas-craft__cursor[data-open="0"] .onas-craft__cursor-close,
.onas-craft__cursor[data-open="1"] .onas-craft__cursor-open {
  display: none;
}
@media (pointer: coarse) {
  .onas-craft__cursor {
    display: none;
  }
}
/* tile.pt row anatomy, 1:1: ONE flex container per row that grows via
   min-height (600ms cubic-bezier(0.65,0,0.35,1)); the title goes cobalt, the
   description ink-fills (JS-driven gradient), and a single media slot morphs
   from poster square to playing stage clip. overflow:clip keeps the falling
   tag blocks and the collapsing stage inside the row. */
/* The whole row is the control (owner spec 2026-09-10: "na każdej sekcji …
   gdziekolwiek nie klikniemy otwiera, a potem jak klikniemy drugi raz zamyka").
   The click is handled in CraftAccordion, on the row, so tags, media, poster and
   description all toggle it; the <button> inside stays the keyboard/AT control. */
.onas-craft__row {
  border-bottom: 1px solid var(--onas-line);
  position: relative;
  cursor: pointer;
}
.onas-craft__inner {
  position: relative;
  overflow: clip;
  display: flex;
  flex-direction: column;
  gap: 0.8rem;
  padding: 1.4rem 0;
}
.onas-craft__title {
  margin: 0;
}
.onas-craft__trigger {
  display: inline;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  text-align: left;
  color: var(--onas-ink);
  /* Clash Display, matching the section heading above and tile.pt's own face for
     this block. It also fixes a real bug: halfre ships no Polish diacritics, so
     "gotowe aplikacje dla branż" and "rozwiązania szyte na miarę" were falling
     back mid-word here. */
  font-family: var(--font-tile), var(--font-display), sans-serif;
  font-size: clamp(1.7rem, 3.2vw, 2.9rem);
  line-height: 1.02;
  font-weight: 600;
  letter-spacing: -0.02em;
  transition: color 0.6s cubic-bezier(0.65, 0, 0.35, 1);
}
/* The stretched ::after hit area is gone (2026-09-10). It could not deliver what
   its comment claimed: .onas-craft__inner is a flexbox and its items carry
   z-index 1, which makes each of them a stacking context even at position:static
   — so an overlay living inside the title item painted UNDER the description and
   the media slot. Measured in the browser that day: elementFromPoint over the
   poster returned the <video>, over the paragraph the <p>, and only the empty
   gaps returned the button. Hence only the title ever opened a row. */
/* Clash Display ships no italic and a synthetic oblique on a display face at this
   size is visibly skewed (same call as the cobalt manifesto), so the second half
   of each row title separates by weight instead of slant. */
.onas-craft__title-b {
  font-style: normal;
  font-weight: 400;
}
.onas-craft__row.is-open .onas-craft__trigger,
.onas-craft__row:hover .onas-craft__trigger {
  color: var(--onas-blue);
}
/* Description: transparent text over a JS-animated ink/grey gradient
   (background-clip). The inline background-image holds the resting state. */
.onas-craft__body {
  margin: 0;
  max-width: 36rem;
  color: transparent;
  -webkit-background-clip: text;
  background-clip: text;
  font-size: 0.95rem;
  line-height: 1.6;
}
/* Tag arena: hidden while closed, fades in on open. Phones keep a flex
   "floor" the blocks bounce into; desktop turns it into an absolute physics
   stage (below). */
.onas-craft__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  align-items: flex-end;
  align-content: flex-end;
  max-height: 0;
  overflow: hidden;
  margin: 0;
  opacity: 0;
  visibility: hidden;
  transition:
    opacity 200ms ease,
    visibility 0s 200ms,
    max-height 600ms cubic-bezier(0.65, 0, 0.35, 1);
}
.onas-craft__row.is-open .onas-craft__tags {
  max-height: 14rem;
  min-height: 7rem;
  opacity: 1;
  visibility: visible;
  transition:
    opacity 200ms ease,
    visibility 0s,
    max-height 600ms cubic-bezier(0.65, 0, 0.35, 1);
}
.onas-craft__tag {
  display: inline-block;
  padding: 0.5rem 0.9rem;
  border: 1px solid var(--onas-ink);
  background: var(--onas-paper);
  color: var(--onas-ink);
  font-family: var(--font-mono), monospace;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  white-space: nowrap;
  transition:
    color 0.25s ease,
    border-color 0.25s ease,
    background-color 0.25s ease;
}
/* Hover state 1:1 with tile's pills-going-cobalt. */
.onas-craft__tag:hover {
  color: var(--onas-blue);
  border-color: var(--onas-blue);
  background: color-mix(in oklab, var(--onas-blue) 6%, var(--onas-paper));
}
/* "→ więcej" CTA: slides in 720ms AFTER the stage settles (tile.pt), leaves
   immediately on close (no delay on the closed rule). */
.onas-craft__more {
  display: inline-block;
  margin: 0;
  color: var(--onas-blue);
  text-decoration: none;
  font-family: var(--font-mono), monospace;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  opacity: 0;
  transform: translateY(-4px);
  max-height: 0;
  overflow: hidden;
  pointer-events: none;
  transition:
    opacity 280ms cubic-bezier(0.65, 0, 0.35, 1),
    transform 280ms cubic-bezier(0.65, 0, 0.35, 1),
    max-height 280ms cubic-bezier(0.65, 0, 0.35, 1);
}
.onas-craft__row.is-open .onas-craft__more {
  opacity: 1;
  transform: none;
  max-height: 2rem;
  pointer-events: auto;
  transition-delay: 720ms;
}
.onas-craft__more:hover {
  text-decoration: underline;
}
/* Media slot: ONE morphing box per row (tile.pt) — poster square when closed,
   playing stage clip when open. Collapsed entirely on phones (mobile rows are
   title+body only until opened). */
.onas-craft__side {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.75rem;
}
.onas-craft__media {
  position: relative;
  width: 100%;
  max-width: 26rem;
  height: 0;
  opacity: 0;
  overflow: hidden;
  border: 1px solid var(--onas-line);
  transition:
    height 600ms cubic-bezier(0.65, 0, 0.35, 1),
    opacity 600ms cubic-bezier(0.65, 0, 0.35, 1);
}
.onas-craft__row.is-open .onas-craft__media {
  height: 13rem;
  opacity: 1;
}
.onas-craft__media video {
  position: relative;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: opacity 200ms ease;
}
/* Static poster under the (transparent-when-closed) video — tile.pt's closed
   state. The video fades in only while the row plays. */
.onas-craft__poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
@media (prefers-reduced-motion: reduce) {
  .onas-craft__media video {
    transition: none;
  }
}
/* Phones (OnePlus 13 tester feedback 2026-09-03): (1) closed rows showed no
   sign of being tappable and (2) the rotated tag blocks sat flush against the
   arena's edges, so overflow:hidden sliced their corners. The "+" glyph lives
   in the title's right gutter and rotates into "×" while open; the arena gets
   an inner inset (cancelled horizontally by a negative margin so the blocks
   still align with the title) and a taller cap so nothing is cut. Desktop is
   untouched: the glyph is display:none there and the arena stays absolute. */
.onas-craft__glyph {
  display: none;
}
@media (max-width: 1023px) {
  .onas-craft__title {
    position: relative;
    padding-right: 2.75rem;
  }
  .onas-craft__glyph {
    display: block;
    position: absolute;
    top: 0;
    right: 0;
    width: 2rem;
    height: 2rem;
    line-height: 2rem;
    text-align: center;
    font-family: var(--font-mono), monospace;
    font-weight: 400;
    font-size: 1.9rem;
    letter-spacing: 0;
    color: inherit;
    transform-origin: 50% 50%;
    transition: transform 0.5s cubic-bezier(0.496, 0.004, 0, 1);
  }
  .onas-craft__row.is-open .onas-craft__glyph {
    transform: rotate(45deg);
  }
  .onas-craft__row.is-open .onas-craft__tags {
    max-height: 20rem;
    padding: 1.25rem 0.5rem 0.5rem;
    margin-inline: -0.5rem;
  }
}
@media (prefers-reduced-motion: reduce) {
  .onas-craft__glyph {
    transition: none;
  }
}
@media (min-width: 1024px) {
  /* tile.pt open stage 1:1: the row's flex container grows 96.75px -> 320px;
     the media slot morphs 76.75px -> 440x248 at the right edge; the tag
     blocks rain across the remaining width as absolute physics bodies. */
  .onas-craft__inner {
    flex-direction: row;
    align-items: flex-start;
    gap: 2.75rem;
    padding: 1.2rem 0;
    min-height: 6.05rem;
    transition: min-height 600ms cubic-bezier(0.65, 0, 0.35, 1);
  }
  .onas-craft__row.is-open .onas-craft__inner {
    min-height: 20rem;
  }
  .onas-craft__title {
    flex: 0 0 clamp(16rem, 24vw, 26rem);
    z-index: 1;
  }
  /* Description makes room for the tag arena the instant the row opens
     (width 0ms), then eases back over 600ms on close — tile.pt exactly. */
  .onas-craft__body {
    z-index: 1;
    transition: max-width 600ms cubic-bezier(0.65, 0, 0.35, 1);
  }
  .onas-craft__row.is-open .onas-craft__body {
    max-width: 20rem;
    transition: max-width 0ms;
  }
  .onas-craft__tags {
    position: absolute;
    inset: 0;
    z-index: 2;
    display: block;
    max-height: none;
    overflow: visible;
    pointer-events: none;
  }
  .onas-craft__row.is-open .onas-craft__tags {
    max-height: none;
    min-height: 0;
  }
  .onas-craft__tag {
    pointer-events: auto;
  }
  .onas-craft__side {
    margin-left: auto;
    align-items: flex-end;
    z-index: 1;
  }
  .onas-craft__media {
    width: 4.8rem;
    height: 4.8rem;
    max-width: none;
    opacity: 1;
    transition:
      width 600ms cubic-bezier(0.65, 0, 0.35, 1),
      height 600ms cubic-bezier(0.65, 0, 0.35, 1);
  }
  .onas-craft__row.is-open .onas-craft__media {
    width: clamp(20rem, 32vw, 27.5rem);
    height: 15.5rem;
  }
}
@media (prefers-reduced-motion: reduce) {
  .onas-craft__inner,
  .onas-craft__trigger,
  .onas-craft__body,
  .onas-craft__tags,
  .onas-craft__media,
  .onas-craft__more {
    transition: none;
  }
  .onas-craft__cursor {
    display: none;
  }
}
@media (prefers-reduced-motion: reduce) and (min-width: 1024px) {
  /* Without the physics drop the tag arena falls back to a static flow. */
  .onas-craft__tags {
    position: static;
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
    order: 4;
  }
}

/* Custom cursor (tile.pt grammar, REL8 ink/cobalt). The follower component
   adds .onas-cursor-active on <html> only when mounted on a fine pointer, so
   no-JS and reduced-motion keep the native cursor. */
html.onas-cursor-active,
html.onas-cursor-active * {
  cursor: none !important;
}
.onas-cursor {
  position: fixed;
  top: 0;
  left: 0;
  width: 32px;
  height: 32px;
  z-index: 10000;
  pointer-events: none;
  will-change: transform;
  transition: filter 120ms ease;
}
.onas-cursor.is-invert {
  filter: brightness(0) invert(1);
}
.onas-cursor > svg {
  position: absolute;
  top: 0;
  left: 0;
  width: 32px;
  height: 32px;
  opacity: 0;
  transition: opacity 120ms ease;
}
.onas-cursor__hand {
  width: 52px;
  height: 52px;
}
.onas-cursor__burst {
  top: -20px;
  left: -20px;
  width: 60px;
  height: 60px;
}
.onas-cursor[data-state="base"] .onas-cursor__base,
.onas-cursor[data-state="clickHold"] .onas-cursor__hand,
.onas-cursor[data-state="click"] .onas-cursor__burst,
.onas-cursor[data-state="eye"] .onas-cursor__eye,
.onas-cursor[data-state="lapis"] .onas-cursor__pen {
  opacity: 1;
}

/* makemepulse play disc (2026-09-05): a paper disc with an ink triangle over
   pictures and clips. Links keep the blocky hand; the big clay hand that
   follows the pointer over feature links lives in OnasHand. */
.onas-cursor__disc {
  position: absolute;
  top: 16px;
  left: 16px;
  width: 88px;
  height: 88px;
  margin: -44px 0 0 -44px;
  border-radius: 50%;
  background: color-mix(in oklab, var(--onas-paper) 92%, var(--onas-ink));
  box-shadow:
    0 10px 24px rgba(0, 0, 0, 0.12),
    inset 0 0 0 1px color-mix(in oklab, var(--onas-ink) 12%, transparent);
  opacity: 0;
  transform: scale(0.3);
  transition:
    opacity 160ms ease,
    transform 260ms cubic-bezier(0.22, 1, 0.36, 1);
}
.onas-cursor[data-state="play"] .onas-cursor__disc--play {
  opacity: 1;
  transform: scale(1);
}
.onas-cursor[data-state="play"] .onas-cursor__play {
  opacity: 1;
}
.onas-cursor__play {
  transform: scale(1.35);
  transform-origin: 16px 16px;
}
.onas-cursor.is-invert .onas-cursor__disc {
  filter: brightness(0) invert(1);
}
/* Burst one-shot: the core square pops and holds (long presses keep it), the
   rays fly outward and fade. The svg remounts per mousedown to replay. */
.onas-cursor__burst-core {
  transform-origin: 30px 30px;
  animation: onas-cur-pop 0.2s cubic-bezier(0.22, 1, 0.36, 1) both;
}
.onas-cursor__burst-rays {
  transform-origin: 30px 30px;
  animation: onas-cur-burst 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
}
@keyframes onas-cur-pop {
  from {
    transform: scale(0);
  }
  to {
    transform: scale(1);
  }
}
@keyframes onas-cur-burst {
  0% {
    transform: scale(0.35);
    opacity: 1;
  }
  100% {
    transform: scale(1);
    opacity: 0;
  }
}
/* Blink: a quick vertical squeeze every 3s (mirrors tile's eye.json loop). */
.onas-cursor__eye-blink {
  transform-origin: 16px 16px;
  animation: onas-cur-blink 3s ease-in-out infinite;
}
@keyframes onas-cur-blink {
  0%,
  91%,
  100% {
    transform: scaleY(1);
  }
  95% {
    transform: scaleY(0.08);
  }
}
/* Pencil wiggle: writing jitter around the tip. */
.onas-cursor__pen-wiggle {
  transform-origin: 6px 26px;
  animation: onas-cur-scribble 1.4s ease-in-out infinite;
}
@keyframes onas-cur-scribble {
  0%,
  100% {
    transform: rotate(0deg);
  }
  25% {
    transform: rotate(-7deg);
  }
  75% {
    transform: rotate(5deg);
  }
}
/* Cursor-attached hint label (tile.pt "press for presentation video"): an ink
   pill that rides below-right of the pointer over [data-cursor-label] targets,
   fading in immediately (180ms). Reuses the .onas-craft__cursor recipe; the JS
   owns the transform (translate only, no rotation), so the below-right offset
   lives in the margin. Ink bg / paper text stays legible over the cobalt flood. */
.onas-cursor__label {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 10001;
  margin: 1.2rem 0 0 0.9rem;
  padding: 0.5rem 0.85rem;
  background: var(--onas-ink);
  color: var(--onas-paper);
  font-family: var(--font-mono), monospace;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  white-space: nowrap;
  transform: translate3d(-9999px, -9999px, 0);
  opacity: 0;
  visibility: hidden;
  transition:
    opacity 180ms ease,
    visibility 0s 180ms;
  will-change: transform, opacity;
  pointer-events: none;
}
.onas-cursor__label[data-visible="1"] {
  opacity: 1;
  visibility: visible;
  transition:
    opacity 180ms ease,
    visibility 0s;
}
@media (prefers-reduced-motion: reduce) {
  .onas-cursor,
  .onas-cursor__label {
    display: none;
  }
}

/* team — the gallery lives inside the fullscreen TeamOverlay dialog now (the
   .onas-team section left the page flow on 2026-07-17); these rules style the
   grid/aside/cells rendered by components/o-nas/TeamOverlay.tsx. */
.onas-team__grid {
  display: grid;
  gap: 3rem;
  margin-top: 3.5rem;
}
/* Explicit, line-based placement (not auto-flow) so the photos AND the woven
   text/arrow block land in the SAME cells at every desktop width. Row 2 right
   (cols 7–12) is reserved for the aside, beside the 3rd photo; the wide
   stop-motion finale spans the full row 3. */
@media (min-width: 1024px) {
  .onas-team__grid {
    grid-template-columns: repeat(12, 1fr);
    gap: 4rem 3rem;
    align-items: start;
  }
  .onas-team__cell--0 {
    grid-column: 1 / 8;
    grid-row: 1;
  }
  .onas-team__cell--1 {
    grid-column: 9 / 13;
    grid-row: 1;
    margin-top: 5rem;
  }
  .onas-team__cell--2 {
    grid-column: 1 / 6;
    grid-row: 2;
  }
  .onas-team__cell--3 {
    grid-column: 1 / 13;
    grid-row: 3;
    margin-top: 4rem;
  }
  .onas-team__aside {
    grid-column: 7 / 13;
    grid-row: 2;
    align-self: center;
  }
}
.onas-team__grid img {
  width: 100%;
  height: auto;
  display: block;
}
.onas-team__video {
  width: 100%;
  height: auto;
  display: block;
}
.onas-team__grid > figure > div {
  overflow: clip;
}
/* The wide stop-motion cell bleeds to the dialog's edges on touch widths (the
   overlay scroll container pads 1.25rem / 3.5rem like the old section did). */
.onas-team__cell--3 {
  margin-inline: -1.25rem;
}
.onas-team__cell--3 .onas-caption {
  padding-inline: 1.25rem;
}
@media (min-width: 768px) and (max-width: 1023px) {
  .onas-team__cell--3 {
    margin-inline: -3.5rem;
  }
  .onas-team__cell--3 .onas-caption {
    padding-inline: 3.5rem;
  }
}
@media (min-width: 1024px) {
  .onas-team__cell--3 {
    margin-inline: 0;
  }
  .onas-team__cell--3 .onas-caption {
    padding-inline: 0;
  }
}

/* team overlay — fullscreen dialog (zajno grammar) + the ink wipe layer. The
   wipe sits BELOW the dialog panel (1002 < 1003): it only needs to cover the
   PAGE during the grow/hold phase, and the panel then slides up OVER the
   black. Both out-rank the menu pill (1001); the cursor (10000+) stays on
   top. z-index scale reference: curtain 80 / menu overlay 1000 / menu bar
   1001 / wipe 1002 / dialog 1003. */
html.onas-overlay-lock,
body.onas-overlay-lock {
  overflow: hidden;
  scrollbar-gutter: stable;
}
.onas-team-wipe {
  position: fixed;
  inset: 0;
  z-index: 1002;
  background: var(--onas-ink);
  transform-origin: 0 0;
  opacity: 0;
  pointer-events: none;
}
.onas-team-overlay {
  position: fixed;
  inset: 0;
  z-index: 1003;
  background: var(--onas-paper);
  color: var(--onas-ink);
  visibility: hidden;
}
.onas-team-overlay.is-visible {
  visibility: visible;
}
.onas-team-overlay__scroll {
  height: 100vh;
  /* biome-ignore lint/suspicious/noDuplicateProperties: 100vh fallback for dvh */
  height: 100dvh;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 5rem 1.25rem 4rem;
}
@media (min-width: 768px) and (max-width: 1023px) {
  .onas-team-overlay__scroll {
    padding-inline: 3.5rem;
  }
}
@media (min-width: 1024px) {
  .onas-team-overlay__scroll {
    padding: 7rem 8rem 6rem 4rem;
  }
}
.onas-team-overlay__close {
  position: fixed;
  top: 1.25rem;
  right: 1.25rem;
  z-index: 2;
  display: grid;
  place-items: center;
  width: 3rem;
  height: 3rem;
  padding: 0;
  background: var(--onas-ink);
  color: var(--onas-paper);
  border: 0;
  cursor: pointer;
  transition: background 0.25s ease;
}
.onas-team-overlay__close:hover {
  background: var(--onas-blue);
}
.onas-team-overlay__close:focus-visible {
  outline: 2px solid var(--onas-blue);
  outline-offset: 2px;
}
@media (min-width: 1024px) {
  .onas-team-overlay__close {
    top: 2rem;
    right: 2.5rem;
  }
}
@media (prefers-reduced-motion: reduce) {
  .onas-team-overlay__close {
    transition: none;
  }
}

/* editorial note + green "advance" arrow as one block, woven among the photos —
   the c2mtl pattern: text among the images, then a vivid circular cue that
   carries the reader on to the slogan instead of letting the gallery dead-end. */
.onas-team__aside {
  display: flex;
  flex-direction: column;
  gap: 2rem;
  margin-top: 1rem;
}
@media (min-width: 1024px) {
  .onas-team__aside {
    margin-top: 0;
  }
}
.onas-team__note .onas-mono {
  color: var(--onas-blue);
  margin-bottom: 0.9rem;
}
.onas-team__note p:last-child {
  font-size: 1.2rem;
  line-height: 1.6;
  max-width: 30rem;
}
.onas-team__aside .onas-jump {
  align-self: flex-start;
}
@media (min-width: 1024px) {
  .onas-team__aside .onas-jump {
    align-self: flex-end;
  }
}
.onas-jump {
  display: inline-flex;
  align-items: center;
  gap: 1.2rem;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  color: var(--onas-ink);
}
.onas-jump__label {
  font-family: var(--font-mono), monospace;
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
}
.onas-jump__label em {
  font-style: normal;
  color: var(--color-green, #22c55e);
}
.onas-jump__circle {
  display: grid;
  place-items: center;
  width: 5.5rem;
  height: 5.5rem;
  border-radius: 50%;
  background: var(--color-green, #22c55e);
  color: #06210f;
  transition:
    transform 0.5s cubic-bezier(0.19, 1, 0.22, 1),
    box-shadow 0.5s ease;
}
.onas-jump:hover .onas-jump__circle {
  transform: scale(1.08);
  box-shadow: 0 1rem 2.4rem color-mix(in oklab, var(--color-green, #22c55e) 42%, transparent);
}
.onas-jump__circle svg {
  transition: transform 0.5s cubic-bezier(0.19, 1, 0.22, 1);
}
.onas-jump:hover .onas-jump__circle svg {
  transform: translateY(4px);
}

/* (The stats proof strip and its divider line were removed 2026-07-24.) */

/* projects */
.onas-projects {
  padding: 4rem 1.25rem;
}
@media (min-width: 1024px) {
  .onas-projects {
    padding-inline: 4rem 8rem;
    padding-block: var(--onas-beat-projects-in) var(--onas-beat-projects-out);
  }
}
/* projects outro — quiet contact off-ramp at the proof-hungry moment, after the
   bento cards and before the closing banner (desktop-only; ink on cream). */
.onas-projects__outro {
  display: none;
}
@media (min-width: 1024px) {
  .onas-projects__outro {
    display: inline-block;
    margin-top: 5rem;
    font-size: 1.05rem;
    color: var(--onas-ink);
    text-decoration: none;
    border-bottom: 1px solid color-mix(in oklab, var(--onas-ink) 35%, transparent);
    padding-bottom: 0.25rem;
    /* border-color ONLY — a CSS opacity transition here races the [data-rise]
       gsap.from(): the tween captures the mid-transition value 0 as its end
       opacity and the link reveals to opacity 0 (invisible forever). */
    transition: border-color 0.3s ease;
  }
  .onas-projects__outro:hover {
    border-bottom-color: currentColor;
  }
}

/* buttons */
.onas-btn {
  display: inline-block;
  margin-top: 1.8rem;
  padding: 0.85rem 1.7rem;
  border-radius: 999px;
  border: 1px solid var(--onas-ink);
  font-family: var(--font-mono), monospace;
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--onas-ink);
  text-decoration: none;
  transition:
    background 0.3s,
    color 0.3s;
}
.onas-btn:hover {
  background: var(--onas-ink);
  color: var(--onas-paper);
}
.onas-btn--ghost {
  opacity: 0.5;
  pointer-events: none;
}
.onas-btn--product {
  border-color: var(--onas-blue);
  color: var(--onas-blue);
  background: color-mix(in oklab, var(--onas-blue) 7%, transparent);
}
.onas-btn--product:hover {
  background: var(--onas-blue);
  border-color: var(--onas-blue);
  color: var(--onas-paper);
}
.onas-btn--soon {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  pointer-events: none;
  opacity: 1;
  border-color: transparent;
  color: var(--onas-ink);
  background:
    linear-gradient(var(--onas-paper), var(--onas-paper)) padding-box,
    var(--onas-grad) border-box;
  box-shadow:
    0 0 0 1px color-mix(in oklab, var(--onas-blue) 18%, transparent),
    0 0.8rem 1.9rem color-mix(in oklab, var(--onas-orange) 18%, transparent);
}
.onas-btn--light {
  background: #fff;
  border-color: #fff;
  color: var(--onas-ink);
}
.onas-btn--light:hover {
  background: transparent;
  border-color: #fff;
  color: #fff;
}
.onas-btn--light-outline {
  background: transparent;
  border-color: var(--onas-paper);
  color: var(--onas-paper);
}
.onas-btn--light-outline:hover {
  background: var(--onas-paper);
  border-color: var(--onas-paper);
  color: var(--onas-ink);
}
.onas-btn--dark {
  background: var(--onas-ink);
  border-color: var(--onas-ink);
  color: #fff;
}
.onas-btn--dark:hover {
  background: transparent;
  color: var(--onas-ink);
}
@media (min-width: 1024px) {
  .onas-btn--dark:hover {
    color: #fff;
    border-color: #fff;
  }
}
/* The primary CTA hops gently so it's the obvious next click; pauses on hover. */
.onas-btn--bounce {
  animation: onas-cta-bounce 1.8s ease-in-out infinite;
  will-change: transform;
}
.onas-btn--bounce:hover {
  animation-play-state: paused;
}
@keyframes onas-cta-bounce {
  0%,
  55%,
  100% {
    transform: translateY(0);
  }
  68% {
    transform: translateY(-9px);
  }
  80% {
    transform: translateY(-4px);
  }
}

/* closing — restore the production-like gradient finale behind the current
   title / CTA / contact stack, while keeping the updated copy intact. */
.onas-closing {
  margin-top: 4rem;
  padding: 7rem 1.25rem 0;
  text-align: center;
  background: var(--onas-grad-cool);
  color: #fff;
  overflow: clip;
}
@media (min-width: 1024px) {
  .onas-closing {
    padding-inline: 4rem 8rem;
    padding-block: var(--onas-beat-closing-in) 0;
  }
}
/* The finale is ONE giant localized display statement, set
   exactly like the flood watermark (same face, weight and tracking), so the
   line the reader met underwater comes back as the page's last word. The
   .onas-root prefix outranks both the h2 ink default and the old size rule. */
.onas-root .onas-closing__title {
  margin: 0;
  font-family: var(--font-tile), var(--font-display), sans-serif;
  font-weight: 600;
  font-size: clamp(2.4rem, 4.6vw, 5.2rem);
  line-height: 1.02;
  letter-spacing: -0.025em;
  color: #fff;
  overflow: hidden;
  text-wrap: balance;
}
/* The UK closing title is one long, unbreakable word. Scale it with the phone
   viewport so the split-character animation stays intact without clipping. */
@media (max-width: 767px) {
  .onas-root .onas-closing__title:lang(uk) {
    font-size: clamp(2rem, 11.5vw, 3.2rem);
  }
}
/* The invitation is long; it may wrap on any viewport (it fits one line from
   ~1280px at 4.6vw). Never force nowrap here — overflow:hidden would clip it. */
/* Consultation block (owner feedback 2026-09-10: ONE title, ONE lead, then
   host card left / form right — no eyebrow, no trust list, no pricing link,
   no second contact row). Everything white on brand blue; the fields are
   hairline glass so they read as part of the banner. */
.onas-closing .onas-consult__lead {
  margin: clamp(1.4rem, 3vh, 2.2rem) auto 0;
  max-width: 46ch;
  font-size: clamp(1.05rem, 1.5vw, 1.3rem);
  line-height: 1.45;
  opacity: 0.92;
  text-wrap: pretty;
}
.onas-consult {
  display: grid;
  gap: 2.5rem;
  text-align: left;
  width: min(100%, 64rem);
  margin: clamp(2.5rem, 6vh, 4rem) auto 0;
  padding-bottom: clamp(3.5rem, 9vh, 6rem);
}
@media (min-width: 1024px) {
  .onas-consult {
    grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
    gap: 4.5rem;
    align-items: center;
  }
}
.onas-consult__host {
  display: flex;
  gap: 1.4rem;
  align-items: center;
}
@media (min-width: 1024px) {
  .onas-consult__host {
    flex-direction: column;
    align-items: flex-start;
    gap: 1.6rem;
  }
}
.onas-root .onas-consult__photo {
  width: 128px;
  height: 128px;
  flex: none;
  object-fit: cover;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.85);
  filter: saturate(0.9);
}
@media (min-width: 1024px) {
  .onas-root .onas-consult__photo {
    width: 200px;
    height: 200px;
  }
}
.onas-consult__role {
  display: block;
  font-family: var(--font-mono), monospace;
  font-size: 0.8rem;
  letter-spacing: 0.06em;
}
.onas-consult__facts {
  list-style: none;
  margin: 0.8rem 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem 0.6rem;
  font-family: var(--font-mono), monospace;
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  opacity: 0.9;
}
.onas-consult__facts li {
  padding: 0.3rem 0.7rem;
  border: 1px solid rgba(255, 255, 255, 0.5);
  border-radius: 999px;
}
.onas-consult__form {
  display: grid;
  gap: 1rem;
}
.onas-consult__row {
  display: grid;
  gap: 1rem;
}
@media (min-width: 640px) {
  .onas-consult__row {
    grid-template-columns: 1fr 1fr;
  }
}
.onas-consult__hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
}
.onas-consult__field {
  display: grid;
  gap: 0.4rem;
}
.onas-consult__label {
  font-family: var(--font-mono), monospace;
  font-size: 0.72rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  opacity: 0.85;
}
.onas-consult__input {
  width: 100%;
  padding: 0.85rem 1rem;
  border-radius: 0;
  border: 1px solid rgba(255, 255, 255, 0.55);
  background: rgba(255, 255, 255, 0.08);
  color: #fff;
  font: inherit;
  font-size: 1rem;
  line-height: 1.3;
  transition:
    border-color 0.2s,
    background 0.2s;
}
.onas-consult__input::placeholder {
  color: rgba(255, 255, 255, 0.55);
}
.onas-consult__input:hover {
  border-color: rgba(255, 255, 255, 0.85);
}
.onas-consult__input:focus {
  outline: none;
  border-color: #fff;
  background: rgba(255, 255, 255, 0.14);
}
.onas-consult__input:focus-visible {
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.35);
}
.onas-consult__textarea {
  resize: vertical;
  min-height: 5.5rem;
}
.onas-consult__error {
  margin: 0;
  font-family: var(--font-mono), monospace;
  font-size: 0.8rem;
  letter-spacing: 0.04em;
  color: #fff;
  background: rgba(25, 24, 24, 0.35);
  padding: 0.6rem 0.9rem;
  border-left: 3px solid var(--onas-orange);
}
.onas-root .onas-consult__submit {
  margin-top: 0.4rem;
  justify-self: start;
  cursor: pointer;
}
.onas-root .onas-consult__submit:disabled {
  opacity: 0.6;
  cursor: progress;
}
.onas-consult__consent {
  margin: 0;
  font-size: 0.76rem;
  line-height: 1.5;
  opacity: 0.7;
  max-width: 52ch;
}
.onas-consult__consent a {
  color: inherit;
  text-decoration: underline;
}
.onas-consult__done {
  padding: 1.5rem 0;
  border-top: 1px solid rgba(255, 255, 255, 0.45);
}
.onas-consult__done-title {
  margin: 0;
  font-size: clamp(1.4rem, 2.4vw, 2rem);
  font-weight: 600;
  letter-spacing: -0.01em;
}
.onas-consult__done-body {
  margin: 0.8rem 0 0;
  font-size: 1rem;
  line-height: 1.5;
  opacity: 0.9;
  max-width: 44ch;
}

/* footer — company footer (sellwise grammar) in the page's ink/mono voice:
   brand statement + link columns on top, legal row + languages at the bottom. */
.onas-footer {
  background: var(--onas-ink);
  color: var(--onas-paper);
  padding: clamp(3rem, 6vh, 4.5rem) 1.25rem 1.6rem;
  font-family: var(--font-mono), monospace;
  font-size: 0.8rem;
}
@media (min-width: 1024px) {
  .onas-footer {
    padding-inline: 4rem 8rem;
  }
}
.onas-footer a {
  color: inherit;
  text-decoration: none;
  opacity: 0.75;
  transition: opacity 0.2s;
}
.onas-footer a:hover {
  opacity: 1;
}
.onas-footer__top {
  display: grid;
  gap: 2.5rem;
  padding-bottom: 2.5rem;
  border-bottom: 1px solid color-mix(in oklch, var(--onas-paper) 18%, transparent);
}
@media (min-width: 1024px) {
  .onas-footer__top {
    grid-template-columns: minmax(0, 2fr) minmax(0, 5fr);
    gap: 4rem;
  }
}
/* Footer bookend: the page opens on the moose loader and closes on it too.
   The mark is masked exactly like .onas-curtain__mark (fixed artwork, no
   distortion), rendered as a calm paper sign-off against the dark footer. */
.onas-footer__sign {
  display: inline-flex;
  align-items: center;
  gap: 0.8rem;
}
.onas-root .onas-footer__sign {
  opacity: 1;
}
.onas-footer__mark {
  width: clamp(40px, 4vw, 56px);
  aspect-ratio: 1350 / 975;
  background: var(--onas-paper);
  opacity: 0.9;
  -webkit-mask: url(/assets/rel8-mark-mono.svg) center / contain no-repeat;
  mask: url(/assets/rel8-mark-mono.svg) center / contain no-repeat;
}
.onas-footer__wordmark {
  font-family: var(--font-tile), var(--font-display), sans-serif;
  font-weight: 600;
  font-size: 1.5rem;
  letter-spacing: -0.02em;
}
.onas-footer__wordmark sup {
  font-size: 0.5em;
  vertical-align: super;
  margin-left: 0.1em;
  opacity: 0.7;
}
.onas-footer__statement {
  margin: 1.2rem 0 0;
  font-family: var(--font-sans), sans-serif;
  font-size: 1rem;
  line-height: 1.5;
  opacity: 0.8;
  max-width: 30ch;
  text-wrap: pretty;
}
.onas-footer__cols {
  display: grid;
  gap: 2rem 1.5rem;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (min-width: 1024px) {
  .onas-footer__cols {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}
.onas-footer__social {
  list-style: none;
  margin: 1.2rem 0 0;
  padding: 0;
  display: flex;
  gap: 0.9rem;
}
.onas-footer__social a {
  display: inline-flex;
  width: 40px;
  height: 40px;
  align-items: center;
  justify-content: center;
  border: 1px solid color-mix(in oklch, var(--onas-paper) 30%, transparent);
  opacity: 0.85;
}
.onas-footer__social a:hover {
  opacity: 1;
  border-color: var(--onas-paper);
}
.onas-root .onas-footer__heading {
  margin: 0 0 0.9rem;
  font-family: var(--font-mono), monospace;
  font-size: 0.72rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--onas-paper);
  opacity: 0.55;
}
.onas-footer__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.55rem;
}
.onas-footer__list a {
  display: inline-block;
  font-family: var(--font-sans), sans-serif;
  font-size: 0.9rem;
  line-height: 1.35;
}
.onas-footer__bottom {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 2rem;
  align-items: center;
  justify-content: space-between;
  padding-top: 1.4rem;
  font-size: 0.74rem;
  letter-spacing: 0.04em;
}
.onas-footer__copy {
  opacity: 0.6;
}
.onas-footer__legal {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.4rem;
}
.onas-footer__legal-btn {
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  opacity: 0.75;
  cursor: pointer;
  transition: opacity 0.2s;
}
.onas-footer__legal-btn:hover {
  opacity: 1;
}
.onas-footer__langs {
  display: inline-flex;
  align-items: center;
  gap: 0.65rem;
}
.onas-footer__langs span {
  color: var(--onas-orange);
}
/* Touch targets (mobile UX audit 2026-09-02): block padding enlarges the hit
   box without moving the line; the language switcher gets a wider gap so
   neighbouring hit boxes do not overlap (a mis-tap switches the site language). */
@media (max-width: 1023px) {
  .onas-footer__list a,
  .onas-footer__legal a,
  .onas-footer__legal-btn,
  .onas-footer__langs a {
    padding-block: 0.55rem;
    margin-block: -0.55rem;
  }
  .onas-footer__langs {
    gap: 1.6rem;
  }
  .onas-footer__langs a {
    padding-inline: 0.8rem;
    margin-inline: -0.8rem;
  }
}
/* The global rule (#main-content,main{padding-bottom:28px}) compensates the
   fixed status bar — but this page's footer sits AFTER <main>, so that padding
   only renders as a cream gap between the closing banner and the footer. Zero
   it: the dark footer is this page's bottom element and meets the banner flush. */
.onas-root #main-content {
  padding-bottom: 0;
}
/* The homepage drops the global StatusBar (hidden on o-nas routes), so the 32px
   body reservation for that fixed bar would otherwise leave a cream strip under
   the dark footer. Zero it on this page only (scoped via :has to the o-nas root). */
body:has(.onas-root) {
  padding-bottom: 0 !important;
}

/* Tablet (iPad portrait, big phones landscape): the base layout uses phone
   gutters (1.25rem); 768–1023px gets more breathing room so the big type/cards
   don't hug the edges before the ≥1024px desktop layout takes over. Only inline
   padding changes — vertical rhythm and desktop (≥1024px) are untouched. */
@media (min-width: 768px) and (max-width: 1023px) {
  .onas-hero,
  .onas-intro,
  .onas-projects,
  .onas-closing {
    padding-inline: 3.5rem;
  }
}

/* Living aurora mesh for the mobile hero — brand-hued blobs drift over the base
   gradient on a slow, time-based loop. Only the transform animates, so after the
   blur is rasterized once the browser just composites a cached texture each frame
   (GPU-cheap, no per-frame repaint). Decoupled from scroll entirely. */
@keyframes onas-aurora {
  0% {
    transform: translate3d(-3%, -2%, 0) scale(1.06) rotate(0deg);
  }
  50% {
    transform: translate3d(3%, 1%, 0) scale(1.16) rotate(3deg);
  }
  100% {
    transform: translate3d(-1%, 3%, 0) scale(1.08) rotate(-2deg);
  }
}

/* ----------------------------------------------------------------------------
   MOBILE & TABLET (< 1024px): the desktop difference-blend wallpaper choreography
   is not run here (gsap.matchMedia gate in the effect). The old fallback was a
   single STATIC gradient, which read as dead. Instead the hero now paints a
   self-animating AURORA MESH (see @keyframes onas-aurora): brand-hued blobs drift
   over a base brand gradient, alive on first paint. It is decoupled from scroll
   entirely — no fixed layer, no slide, no per-zone toggle, no hand-tuned scroll
   positions — so a fast touch-flick can't leave white gaps and nudging can't make
   it flicker. The body keeps its self-triggering reveals + parallax. A subtle
   scrim (::after) holds white-copy contrast over the brighter orange lobes; the
   copy is lifted to white because the JS .onas-fxon toggle never fires on mobile.
   Reduced motion freezes the mesh to a still gradient (see the reduce block). */
@media (max-width: 1023px) {
  .onas-bgfx {
    display: none;
  }
  /* Mobile menu pill: compact — the chapter label is dropped (no room), the
     contact CTA takes over the flexible push. The pill keeps its paper+blur
     body over the aurora hero (tile.pt keeps the pill on phones too). */
  .onas-menu-bar {
    padding-top: 0.6rem;
  }
  .onas-menu-bar__pill {
    gap: 0.6rem;
    min-height: 3.5rem;
    padding: 0 0.55rem 0 0.8rem;
  }
  .onas-menu-bar__chapter {
    display: none;
  }
  .onas-menu-bar__count {
    font-size: 0.68rem;
  }
  .onas-menu-bar__logo {
    font-size: 1.15rem;
    position: relative;
  }
  .onas-menu-bar__logo-mark {
    width: 38px;
  }
  .onas-menu-bar__contact {
    margin-left: auto;
    height: 1.9rem;
    padding: 0 0.9rem;
    font-size: 0.82rem;
  }
  /* Touch targets (mobile UX audit 2026-09-02): the pill's visual sizes stay,
     but every control gets an invisible >=44px hit box — the "+" grows its
     own box (glyph stays centred, negative margin keeps the pill geometry),
     the logo and the short CTA extend theirs with a ::before overlay. */
  .onas-menu-bar__trigger {
    width: 2.75rem;
    height: 2.75rem;
    margin: -0.2rem;
  }
  .onas-menu-bar__logo::before,
  .onas-menu-bar__contact::before {
    content: "";
    position: absolute;
    inset: -0.45rem -0.25rem;
  }

  /* Base brand gradient — always-painted under-layer so the drifting blobs can
     never expose a gap, even mid-rotate. overflow:clip confines the aurora
     ::before (inset:-35% overscan, blurred) to the hero box, so an overscroll
     bounce or the mobile URL-bar collapsing on scroll-up can't reveal the blurred
     bleed above the page top (the "strange washed background under the rail" bug).
     The overscan still works — the soft blur edges fall outside the clip. */
  .onas-hero {
    background: var(--onas-grad);
    overflow: clip;
  }
  /* The living layer: blurred brand blobs that drift and breathe. */
  /* The mesh paints behind the hero ONLY — the old colour-block beats that
     shared it (words, stats) are gone / moved to paper, so no phase-shift
     overrides remain. */
  .onas-hero::before {
    content: "";
    position: absolute;
    inset: -35%;
    z-index: 0;
    background:
      radial-gradient(
        38% 44% at 22% 28%,
        color-mix(in oklch, var(--onas-orange) 90%, transparent),
        transparent 62%
      ),
      radial-gradient(
        42% 48% at 80% 22%,
        color-mix(in oklch, var(--onas-blue) 90%, transparent),
        transparent 64%
      ),
      radial-gradient(
        48% 52% at 70% 80%,
        color-mix(in oklch, var(--onas-pink) 86%, transparent),
        transparent 66%
      ),
      radial-gradient(
        44% 46% at 24% 76%,
        color-mix(in oklch, var(--onas-purple) 86%, transparent),
        transparent 64%
      );
    background-repeat: no-repeat;
    /* No blur(): the radial stops already fade to transparent, so a 16px blur
       changed nothing visible but forced the whole 170%-viewport layer to be
       re-filtered every animation frame (phone hero at 4x CPU: 38 -> 61 fps
       without it, measured 2026-10-09). */
    filter: saturate(1.06);
    /* No permanent will-change: a promoted/cached compositor layer was lazily skipping
       its repaint during fast mobile scroll-up + URL-bar collapse, exposing the base
       --onas-grad as a washed band at the page top. A normal paint layer repaints in
       sync with the rest of the page. The 24s drift is cheap enough without the hint. */
    animation: onas-aurora 24s cubic-bezier(0.45, 0, 0.55, 1) infinite alternate;
  }
  /* Contrast scrim — above the blobs (z 1), below the copy (z 45). Kept subtle so
     the gradient stays vivid; weighted top+bottom where the small type lives. */
  .onas-hero::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    background:
      radial-gradient(125% 95% at 22% 32%, transparent 0%, rgba(10, 8, 14, 0.24) 80%),
      linear-gradient(180deg, rgba(10, 8, 14, 0.2) 0%, transparent 34%, rgba(10, 8, 14, 0.22) 100%);
  }
  .onas-hero__title,
  .onas-hero__sub,
  .onas-hero__scroll {
    color: #fff;
  }
  /* Faint shadow keeps the small type legible over the brighter lobes. */
  .onas-hero__sub,
  .onas-hero__scroll {
    text-shadow: 0 1px 14px rgba(8, 6, 12, 0.28);
  }

  /* ---- MOBILE NARRATIVE REDESIGN ----
     The desktop wallpaper choreography that used to carry the rhythm does not run
     on touch, so without it the sections were stranded in white space with leftover
     citations and overlaps. Re-tune the page to read as a tight, self-contained
     editorial narrative with colour-blocked beats (the c2mtl pattern) instead of
     desktop sections ported verbatim. Only vertical rhythm + a few mobile-only
     treatments change; copy and desktop are untouched. */

  /* HERO declutter: the book-style citations + their orphan asterisk are
     desktop-only — on touch they crowded the first screen and overlapped the next
     section on scroll. Tighten the lead and restore a full-screen first impression
     so the aurora owns the phone viewport before the intro card appears. */
  .onas-desktop-copy {
    display: none;
  }
  .onas-mobile-copy {
    display: inline;
  }
  .onas-hero {
    min-height: 100svh;
    align-items: center;
    padding: 4rem 2rem;
  }
  .onas-hero__inner {
    max-width: 24rem;
  }
  .onas-hero__eyebrow {
    /* 12px floor for legibility on phones (mobile UX audit 2026-09-02). */
    font-size: 0.75rem;
    letter-spacing: 0.12em;
    line-height: 1.35;
    padding: 0.48rem 0.7rem;
    max-width: calc(100vw - 4rem);
  }
  .onas-hero__titleline {
    font-size: clamp(6rem, 29vw, 8rem);
  }
  .onas-hero__sub {
    font-size: clamp(1.06rem, 4.8vw, 1.28rem);
    line-height: 1.38;
    margin-top: 1.35rem;
    max-width: 21rem;
  }
  .onas-hero__sub .onas-mobile-copy {
    display: block;
  }
  /* Scroll cue: top padding lifts the hit box to >=44px; the margin drops by the
     same amount so the underline sits where it did. */
  .onas-hero__scroll {
    margin-top: 1.55rem;
    padding-top: 0.35rem;
    min-height: 2.75rem;
  }
  /* Mono section labels (intro / craft / projects) and captions: 12px floor. */
  .onas-root .onas-mono {
    font-size: 0.75rem;
  }
  .onas-hero__footnote {
    display: none;
  }
  .onas-hero__sub .onas-fn {
    display: none;
  }
  /* Hide the dense middle stat sentence on mobile — the hero keeps just the hook +
     payoff. The full lead stays in the DOM (SEO) and renders in full on desktop. */
  .onas-hero__sub-detail {
    display: none;
  }

  /* INTRO: the card overlaps the hero, then gets a protected reading track. The
     photo stays after that track, so it no longer scrolls underneath the card. */
  .onas-intro {
    margin-top: -3.75rem;
    padding-block: 0 3rem;
    gap: 1.5rem;
    z-index: 46;
  }
  .onas-intro__read {
    position: relative;
    isolation: isolate;
  }
  .onas-intro__card {
    border-radius: 16px;
    overflow: hidden;
    padding: 2.6rem 1.55rem;
  }
  .onas-intro__card h2 {
    font-size: clamp(2.1rem, 10vw, 3rem);
    line-height: 0.98;
  }
  .onas-intro__card p:last-child {
    font-size: 0.98rem;
    line-height: 1.6;
  }
  .onas-intro__card p:last-child .onas-mobile-copy {
    display: block;
  }

  /* Kill the oversized desktop vertical padding everywhere — the source of the
     endless white scroll — and tighten the gallery/cards/projects. */
  .onas-team__grid {
    gap: 2rem;
    margin-top: 2rem;
  }
  .onas-projects {
    padding-block: 3rem;
  }
  .onas-closing {
    padding-block: 4.5rem;
    margin-top: 2.5rem;
  }
}

/* Phone-only stats carousel: avoids a vertical stat chimney while keeping a
   visible slice of the next card as the swipe affordance. */
@media (max-width: 767px) {
  /* INTERIM (no pin): clean sequence — card, then the full team photo below it. The
     pinned "team rises under the card" experiments hid the team's faces (the dark card
     sat on top of a landscape photo, so only legs showed), so they are removed. A
     bespoke VERTICAL cover asset is coming (see plan); until it lands this stays simple
     and readable — the photo is shown in full, never behind the card. */
  /* The card pins (sticky) while the team photo pans up into the room below it. Card is
     short (CTAs hidden), so the gap below holds most of the vertical photo — you see the
     team, not legs. min-height is the PIN RUNWAY (= pin scroll distance + card height).
     Kept at 70svh: a longer 80svh hold was tried but, on native mobile scroll (no Lenis
     below 1024px), the extra runway is mostly empty grid — the card sits stuck, then the
     scene lurches, which read as the box "moving too fast / not smooth". 70svh keeps the
     hold continuous and concrete (the timing users preferred). Head landing is set by the
     photo's negative margin below (heads rest ~+30px under the card at release; verified
     360–412px, never clipping) — independent of this value. Tune: min-height = how long
     it pins; photo margin-top = where the heads land. */
  .onas-intro__read {
    position: relative;
    z-index: 2;
    min-height: 70svh;
  }
  .onas-intro__read::before {
    content: "";
    position: absolute;
    inset: 16svh -1.25rem 6svh;
    z-index: -1;
    pointer-events: none;
    background:
      linear-gradient(
        90deg,
        color-mix(in oklab, var(--onas-ink) 4%, transparent) 1px,
        transparent 1px
      ),
      linear-gradient(
        0deg,
        color-mix(in oklab, var(--onas-blue) 5%, transparent) 1px,
        transparent 1px
      ),
      radial-gradient(
        72% 54% at 86% 18%,
        color-mix(in oklab, var(--onas-blue) 7%, transparent),
        transparent 64%
      );
    background-size:
      44px 44px,
      44px 44px,
      auto;
    opacity: 0.5;
  }
  .onas-intro__card {
    position: sticky;
    top: 4.25rem;
    z-index: 1;
    isolation: isolate;
    border: 1px solid color-mix(in oklab, #fff 14%, transparent);
    box-shadow: inset 0 1px 0 color-mix(in oklab, #fff 10%, transparent);
    transition:
      border-color 0.55s cubic-bezier(0.22, 1, 0.36, 1),
      box-shadow 0.55s cubic-bezier(0.22, 1, 0.36, 1);
  }
  .onas-intro__card::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background:
      linear-gradient(
        90deg,
        color-mix(in oklab, var(--onas-blue) 22%, transparent) 1px,
        transparent 1px
      ),
      linear-gradient(0deg, color-mix(in oklab, #fff 7%, transparent) 1px, transparent 1px),
      radial-gradient(
        90% 70% at 86% 16%,
        color-mix(in oklab, var(--onas-blue) 26%, transparent),
        transparent 58%
      ),
      radial-gradient(
        72% 58% at 12% 100%,
        color-mix(in oklab, var(--onas-orange) 18%, transparent),
        transparent 62%
      );
    background-size:
      48px 48px,
      48px 48px,
      auto,
      auto;
    opacity: 0.42;
    transition: opacity 0.55s cubic-bezier(0.22, 1, 0.36, 1);
  }
  .onas-intro__card::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    height: 2px;
    z-index: 1;
    background: var(--onas-grad);
    transform: scaleX(0.18);
    transform-origin: left;
    transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1);
  }
  /* Inner layer carries the pin-settle scale so a transform never lands on the
     sticky card itself (that would offset its pinned position and shift where the
     team's heads rest). At rest it sits a hair large with the copy softened; both
     resolve on THE LAND, so the card reads as composing itself into the lock. */
  .onas-intro__card-inner {
    transform: scale(1.012);
    transform-origin: 50% 0;
    transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1);
    will-change: transform;
  }
  /* Phone motion pass 2026-09-03: the copy is HIDDEN until the card enters
     the viewport (.is-mobile-active at top 78%), then rises in a stagger —
     the section visibly composes itself as you scroll into it. The title's
     characters rise via GSAP at the same moment. */
  .onas-intro__card-inner > * {
    opacity: 0;
    transform: translateY(18px);
    transition:
      opacity 0.7s cubic-bezier(0.22, 1, 0.36, 1),
      transform 0.7s cubic-bezier(0.22, 1, 0.36, 1);
  }
  .onas-intro__card-inner > :nth-child(2) {
    transition-delay: 0.08s;
  }
  .onas-intro__card-inner > :nth-child(3) {
    transition-delay: 0.18s;
  }
  .onas-intro__card-inner > :nth-child(4) {
    transition-delay: 0.28s;
  }
  .onas-intro__card.is-mobile-active .onas-intro__card-inner > * {
    opacity: 0.86;
    transform: translateY(5px);
  }
  /* WARM-UP — continuous, while the card is anywhere on screen: the grid and a soft
     border warm up, but the accent line, depth shadow and copy stay MUTED so the
     full signature plays AT the pin, not on entry (the old bug: is-mobile-active fired
     on entry at "top 78%", long before the lock, so the lock read as a flat stop). */
  .onas-intro__card.is-mobile-active {
    border-color: color-mix(in oklab, var(--onas-blue) 30%, #fff 14%);
  }
  .onas-intro__card.is-mobile-active::before {
    opacity: 0.52;
  }
  /* Team cover reveal (phone motion pass 2026-09-03): the wrapper is clipped
     so the scrubbed image scale never spills; the mask class hosts the
     character rise of the mobile title. */
  .onas-intro__photo > div {
    overflow: clip;
    will-change: clip-path;
  }
  .onas-intro__photo img {
    transform-origin: 50% 60%;
    will-change: transform;
  }
  .onas-intro__card h2 .onas-mobile-copy {
    display: block;
    overflow: hidden;
  }
  /* THE LAND — fired once the card meets its pinned line (top: 4.25rem). One eased
     deceleration: the scale settles, the copy resolves, the accent line draws, and the
     grid + depth bloom — so the pin reads as a deliberate arrival that holds, not a
     mechanical hard-stop. Set on .is-mobile-settled by the pin-line ScrollTrigger. */
  .onas-intro__card.is-mobile-settled .onas-intro__card-inner {
    transform: scale(1);
  }
  .onas-intro__card.is-mobile-settled .onas-intro__card-inner > * {
    opacity: 1;
    transform: translateY(0);
  }
  .onas-intro__card.is-mobile-settled::before {
    opacity: 0.68;
  }
  .onas-intro__card.is-mobile-settled::after {
    transform: scaleX(1);
  }
  .onas-intro__card.is-mobile-settled {
    border-color: color-mix(in oklab, var(--onas-blue) 48%, #fff 14%);
    box-shadow:
      inset 0 1px 0 color-mix(in oklab, #fff 14%, transparent),
      0 1.2rem 2rem color-mix(in oklab, var(--onas-ink) 14%, transparent);
  }
  /* Hide the two CTAs on mobile so the card is short enough to PIN with real room for
     the team below it (CTAs stay on tablet/desktop). A tall card + pin = no room for
     the photo, which is what broke every earlier attempt. */
  .onas-intro__actions {
    display: none;
  }
  /* Full-bleed VERTICAL team photo, pulled UP so it pans into the room below the pinned
     card during the short "stop", then carries on as a full team shot. z-0 (under the
     read track's z-2) so the card always stays on top. Because the card is short and the
     photo is vertical/full-length, the gap shows the TEAM (faces + bodies), not a thin
     legs-only band — that was the old failure with the landscape crop. The mobile
     <source> serves the bespoke 3:4 asset. margin-top tuned with read min-height so the
     team is visible the whole pin. */
  .onas-intro__photo {
    position: relative;
    z-index: 0;
    width: 100vw;
    margin-inline: calc(50% - 50vw);
    margin-top: -16svh;
  }
  .onas-intro__photo > div {
    overflow: hidden;
  }
  .onas-intro__photo img {
    aspect-ratio: 3 / 4;
    object-fit: cover;
  }
  /* Re-pad the caption to the page gutter (the photo block is now full-bleed). */
  .onas-intro__photo .onas-caption {
    padding-inline: 1.25rem;
  }
  .onas-team__aside {
    position: relative;
    isolation: isolate;
    margin-inline: 0;
    padding: 1.2rem 0 1.45rem;
    overflow: hidden;
  }
  .onas-team__aside::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background:
      linear-gradient(
        90deg,
        color-mix(in oklab, var(--onas-blue) 10%, transparent) 1px,
        transparent 1px
      ),
      linear-gradient(
        0deg,
        color-mix(in oklab, var(--onas-ink) 5%, transparent) 1px,
        transparent 1px
      ),
      radial-gradient(
        82% 62% at 100% 0%,
        color-mix(in oklab, var(--onas-blue) 6%, transparent),
        transparent 68%
      );
    background-size:
      42px 42px,
      42px 42px,
      auto;
    opacity: 0.65;
  }
  .onas-team__aside::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    height: 1px;
    background: var(--onas-grad);
    opacity: 0.58;
  }
  /* (The phone stats snap-carousel left with the stats strip, 2026-07-24.) */
}

/* Tablet (768–1023px): the team gallery (inside the overlay) goes 2-up so iPad
   isn't an endless single-file stack of large photos; the wide stop-motion
   finale spans the full row. Phones stay single column; desktop keeps its
   12-col collage. Placed after the mobile block so the 2-col grid wins over
   the single-column gap set above. */
@media (min-width: 768px) and (max-width: 1023px) {
  .onas-team__grid {
    grid-template-columns: 1fr 1fr;
    gap: 1.75rem;
    align-items: start;
  }
  .onas-team__cell--3 {
    grid-column: 1 / -1;
  }
}

/* lenis recommended */
html.lenis,
html.lenis body {
  height: auto;
}
.lenis.lenis-smooth {
  scroll-behavior: auto !important;
}

@media (prefers-reduced-motion: reduce) {
  .onas-intro__card {
    position: static;
    transition: none;
  }
  .onas-intro__card::before,
  .onas-intro__card::after {
    transition: none;
  }
  /* The pin-settle never arms under reduced motion (the ScrollTrigger block is
     gated by the reduced-motion early return), so neutralize the inner layer to
     its rest state — full opacity, no scale, no movement. */
  .onas-intro__card-inner,
  .onas-intro__card-inner > * {
    transform: none;
    opacity: 1;
    transition: none;
  }
  .onas-intro__read {
    position: static;
    min-height: auto;
  }
  .onas-intro__photo {
    position: static;
    transition: none;
  }
  .onas-intro__photo > div {
    transition: none;
  }
  .onas-team__aside {
    position: static;
    transition: none;
  }
  .onas-bgfx {
    display: none;
  }
  /* Freeze the mobile aurora to a still mesh (the blobs stay, they just stop). */
  .onas-hero::before {
    animation: none;
  }
  .onas-jump__circle,
  .onas-jump__circle svg {
    transition: none;
  }
  .onas-btn--bounce {
    animation: none;
  }
}

/* Reduced motion: no pin, so cancel the photo's lift-up (set in the <=767px block) —
   otherwise it would statically overlap the now-static card. Scoped to phones so
   desktop margins stay intact. */
@media (max-width: 767px) and (prefers-reduced-motion: reduce) {
  .onas-intro__photo {
    margin-top: 0;
  }
}

/* ============================================================================
   PERFORMANCE + SCROLL-DRIVEN LAYER (2026-10-09)
   Everything below is compositor-only (transform / opacity) or switches work
   OFF. Scroll-linked pieces use CSS scroll timelines behind @supports, so they
   run off the main thread where supported and simply stay still elsewhere.
   ========================================================================== */

/* Decorative loops pause while their section is off screen. ONasExperience
   flips [data-offscreen] with one IntersectionObserver (15% margin, so they
   resume before they are visible). */
.onas-root [data-offscreen],
.onas-root [data-offscreen]::before,
.onas-root [data-offscreen]::after,
.onas-root [data-offscreen] *,
.onas-root [data-offscreen] *::before,
.onas-root [data-offscreen] *::after {
  animation-play-state: paused !important;
}

/* Lite tier (lib/perfTier.ts): low-power machines keep the page, lose the
   loops that repaint every frame (SVG wave swells, the drifting band gradient,
   the underwater bubbles). */
.onas-root[data-perf="lite"] .onas-flood__swell,
.onas-root[data-perf="lite"] .onas-sea__bubble,
.onas-root[data-perf="lite"] .onas-sea__drift,
.onas-root[data-perf="lite"] .onas-sea__pendulum,
.onas-root[data-perf="lite"] .onas-current__bandshape {
  animation: none;
}

/* Hero departure: as the first screen scrolls away the eyebrow and the tail
   copy lift and fade on the hero's own view timeline (0 at the top of the
   page, 1 when the hero has fully left). The entrance lives on their children,
   so the two animations never fight over one transform. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .onas-hero {
      view-timeline: --onas-hero block;
    }
    /* An animated wrapper is a stacking context; without its own z-index it
       would sink BELOW the z-40 fixed wallpaper and take the copy with it. */
    .onas-hero__inner > .onas-mask,
    .onas-hero__tail {
      position: relative;
      z-index: 45;
      animation: onas-hero-depart linear both;
      animation-timeline: --onas-hero;
      animation-range: exit 0% exit 85%;
    }
  }
}
@keyframes onas-hero-depart {
  to {
    transform: translate3d(0, -16svh, 0);
    opacity: 0;
  }
}

/* The wordmark boards the menu. Scrolling away from the hero, the giant REL8
   shrinks and glides into the pill's logo slot; there it hands over to the
   pill's own "REL8", which fades in as the wordmark fades out. Until then the
   pill shows only the moose, because the hero already says the name.
   ONasExperience measures both boxes and sets the flight on .onas-root:
     --dock-r  scroll distance of flight + hand-over (px)
     --dock-x / --dock-y / --dock-y2 / --dock-s  the title's transforms
   The flight is linear in scroll, so the wordmark tracks the finger exactly.
   Without the measurement (no JS yet) the wordmark stays put and the pill's
   name simply fades in over the first half screen. */
@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    .onas-menu-bar__logo-text {
      animation: onas-dock-name linear both;
      animation-timeline: scroll(root block);
      animation-range: calc(var(--dock-r, 50svh) * 0.85) var(--dock-r, 50svh);
    }
    .onas-hero[data-dock] .onas-hero__title {
      transform-origin: 0 var(--dock-oy, 50%);
      animation: onas-dock-title linear both;
      animation-timeline: scroll(root block);
      animation-range: 0px var(--dock-r, 50svh);
    }
    /* The open menu always names the brand. */
    .onas-menu-bar.is-open .onas-menu-bar__logo-text {
      animation: none;
    }
  }
}
@keyframes onas-dock-name {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
/* 0-85%: the flight. 85-100%: the hand-over, the wordmark held under the
   pill's name (heroDock.ts LAND) while it fades out and the name fades in. */
@keyframes onas-dock-title {
  85% {
    transform: translate3d(var(--dock-x, 0px), var(--dock-y, 0px), 0) scale(var(--dock-s, 1));
    opacity: 1;
  }
  to {
    transform: translate3d(var(--dock-x, 0px), var(--dock-y2, 0px), 0) scale(var(--dock-s, 1));
    opacity: 0;
  }
}

/* Reading-progress filament on the menu pill: a 2px line along its foot that
   fills with the page scroll. Pure scroll timeline, no listener. Ink over
   paper, paper over the cobalt chapters; hidden while the menu is open. */
.onas-menu-bar__col {
  position: relative;
}
.onas-menu-bar__col::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  background: var(--onas-blue);
  transform: scaleX(0);
  transform-origin: 0 50%;
  pointer-events: none;
  transition:
    opacity 0.3s ease,
    background-color 0.35s ease;
}
.onas-menu-bar[data-ground="dark"] .onas-menu-bar__col::after {
  background: var(--onas-paper);
}
.onas-menu-bar.is-open .onas-menu-bar__col::after {
  opacity: 0;
}
@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    .onas-menu-bar__col::after {
      animation: onas-read-progress linear both;
      animation-timeline: scroll(root block);
    }
  }
}
@keyframes onas-read-progress {
  to {
    transform: scaleX(1);
  }
}

/* Tile depth: from 768px up each project tile arrives tipped back and slightly
   small, and straightens toward the reader as it scrolls in — the page gains
   depth without a single frame of JS. It completes once 70% of the tile is in
   view, so the motion happens where the eye is, not below the fold. Phones
   keep their sticky deck (the deck scrub owns the card transform there). */
@supports (animation-timeline: view()) {
  @media (min-width: 768px) and (prefers-reduced-motion: no-preference) {
    .onas-rmg__card {
      transform-origin: 50% 100%;
      animation: onas-tile-depth linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 70%;
    }
  }
}
@keyframes onas-tile-depth {
  from {
    transform: perspective(1600px) translate3d(0, 8vh, 0) rotateX(9deg) scale(0.93);
    opacity: 0.25;
  }
  to {
    transform: none;
    opacity: 1;
  }
}

/* Chapter environments. The page changes its light with the chapter: the
   services chapter reads on paper, the projects chapter goes to night so the
   coloured product tiles glow, and the questions after it return to paper.
   ONasExperience sets .onas-root[data-env] from whichever [data-env] section
   holds the middle of the viewport. Both chapters swap the two ink/paper
   tokens instead of restyling each element; the tokens are registered colours
   so the whole chapter crossfades as one. The tiles and the service demos
   keep the house palette, so they look the same in either light. */
@property --onas-ink {
  syntax: "<color>";
  inherits: true;
  initial-value: #191818;
}
@property --onas-paper {
  syntax: "<color>";
  inherits: true;
  initial-value: #f4f4f4;
}
.onas-craft,
.onas-projects {
  /* Re-derived here so hairlines follow the swapped ink (the root copy is
     already resolved against the root's ink). */
  --onas-line: color-mix(in oklab, var(--onas-ink) 18%, transparent);
  background: var(--onas-paper);
  color: var(--onas-ink);
  transition:
    --onas-ink 0.9s cubic-bezier(0.65, 0, 0.35, 1),
    --onas-paper 0.9s cubic-bezier(0.65, 0, 0.35, 1);
}
.onas-root[data-env="night"] .onas-craft,
.onas-root[data-env="night"] .onas-projects {
  --onas-ink: var(--color-surface, #f4f4f4);
  --onas-paper: color-mix(in oklab, var(--color-ink, #191818) 90%, var(--color-blue, #1b5def));
}
.onas-rmg,
.onas-craft__media {
  --onas-ink: var(--color-ink, #191818);
  --onas-paper: var(--color-surface, #f4f4f4);
}
/* At night the two near-black tiles would melt into the ground: a paper
   hairline keeps their edge. */
.onas-rmg__card {
  transition: box-shadow 0.9s ease;
}
.onas-root[data-env="night"] .onas-rmg__card {
  box-shadow: 0 0 0 1px color-mix(in oklab, var(--color-surface, #f4f4f4) 14%, transparent);
}
/* Headings take a fixed colour from the global base layer; these two follow
   the chapter's ink instead. */
.onas-projects .onas-section-head h2,
.onas-craft__extra h3 {
  color: var(--onas-ink);
}
/* The 3D note under the services list (PL only) follows the tokens too. */
.onas-craft__extra {
  border-color: var(--onas-line);
}
.onas-craft__extra-lead {
  color: color-mix(in oklab, var(--onas-ink) 72%, transparent);
}
.onas-craft__extra-link {
  color: var(--onas-blue);
  border-color: currentColor;
}
.onas-root[data-env="night"] .onas-craft__extra-link {
  color: color-mix(in oklab, var(--onas-blue) 55%, var(--color-surface, #f4f4f4));
}
@media (prefers-reduced-motion: reduce) {
  .onas-craft,
  .onas-projects,
  .onas-rmg__card {
    transition: none;
  }
}
