/* ============ START OF MAEZKEV — VANILLA REBUILD ============ */

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

/* ============ START OF CORNER SHAPE — SQUIRCLE ============ */
/* corner-shape only does anything where a border-radius is already declared,
   so this bends every existing corner and invents none. It's set from the base
   selector rather than per component, which keeps the shape one decision
   instead of a hundred, and leaves any rule below free to opt out — a plain
   class or id beats the universal selector. Browsers without CSS Borders 4
   drop the line and keep the circular corner, which is the same shape, softer. */
*,
*::before,
*::after {
  corner-shape: var(--corner);
}

/* ============ END OF CORNER SHAPE ============ */

html {
  font-size: 16px;
  scroll-padding-top: var(--header-h);
  scroll-behavior: smooth;
  /* The scrollbar here is a styled one, so it takes real layout width. Holding
     its gutter open means the lightbox can freeze the page by turning overflow
     off without the content jumping sideways to fill it. Not every browser
     reserves the gutter once overflow is hidden, so the viewer measures what
     it actually got and pays back the difference in padding. */
  scrollbar-gutter: stable;
  overflow-x: hidden;
}

/* Scroll lock, held while the lightbox is up. */
html.has-lightbox {
  overflow: hidden;
}

/* When the eased scroller is driving, hand it the scroll offset outright —
   native smooth behaviour would animate every frame it requests. */
html.has-smooth-scroll {
  scroll-behavior: auto;
}

body {
  margin: 0;
  background: var(--c-bg);
  color: var(--c-white);
  font-family: var(--font);
  font-size: var(--fs-base);
  font-weight: var(--fw-light);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  /* `clip`, not `hidden`: hidden would make body a scroll container and
     break the pinned timeline's position: sticky. */
  overflow-x: clip;
}

/* ============ START OF SCROLLBAR — BLUE HANDLE ON NOTHING ============ */
html {
  scrollbar-color: var(--c-blue) transparent;
  /* Firefox: thumb, track */
  scrollbar-width: thin;
}

::-webkit-scrollbar {
  width: 0.625rem;
  height: 0.625rem;
}

::-webkit-scrollbar-track,
::-webkit-scrollbar-corner {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background: var(--c-blue);
  border-radius: var(--radius-pill);
  /* A transparent border clipped to the content box insets the handle, so it
     reads as a floating pill rather than filling the gutter edge to edge. */
  border: 0.1875rem solid transparent;
  background-clip: content-box;
}

::-webkit-scrollbar-thumb:hover {
  background: var(--c-blue-hover);
  background-clip: content-box;
}

/* ============ END OF SCROLLBAR — BLUE HANDLE ON NOTHING ============ */

/* ============ START OF PAGE TRANSITIONS ============ */
/* The browser holds a snapshot of the page being left over the one arriving
   and animates between the two. It buys no router and no client-side
   rendering — the pages stay separate documents, fetched and parsed the way
   they always were. Both ends of a navigation have to opt in, which every page
   here does by way of this one shared stylesheet; browsers that don't know the
   at-rule drop it and navigate exactly as before. */
@view-transition {
  navigation: auto;
}

/* How far the wave has travelled, as a share of the run it makes across the
   viewport. Everything about the effect is worked out from this one number, so
   the animation has a single thing to drive. */
@property --wave-front {
  syntax: "<percentage>";
  inherits: false;
  /* Past the far end. Nothing animating means the mask stands wide open and
     the page is simply on show — a sweep that never starts costs the effect,
     never the content. */
  initial-value: 200%;
}

/* The grid the wave is cut into. vmin keeps the count much the same whichever
   way the window is turned, and the clamp stops a phone getting four huge
   blocks or a wide monitor getting a mosaic.

   On :root rather than next to what uses them, which is how the rest of this
   codebase would hold a pair like this. The transition pseudo-elements sit in
   a tree of their own and inherit from the root element and nothing else, so
   the root is the only place both they and the fallback veil can read. */
:root {
  --wave-tile: clamp(2.5rem, 5vmin, 3.75rem);
  --wave-block: calc(var(--wave-tile) * .62);
}

/* The incoming page arrives behind a mask in three parts. Ahead of the wave
   nothing is let through and the old page still stands. At the wave itself a
   band of separate squares comes through, each one a tile of the new page
   dropped onto the old. A quarter of a screen behind that, solid coverage
   fills the gaps in and the swap is done.

   So: a ragged edge of blocks running ahead of a solid front, both travelling
   together. The distance between the two fronts is the width of the blocky
   band, and it is the only thing setting how long any one square stands on
   its own before the page closes up around it. Widen it and the wave turns
   into a mesh laid over the whole viewport; this is about as far as it can
   open before it stops reading as something passing through. */
::view-transition-new(root) {
  mask-image:
    /* The trailing front, added over everything under it — this is what
       eventually leaves the page whole. */
    linear-gradient(105deg,
      #000 calc(var(--wave-front) - 26%),
      #0000 calc(var(--wave-front) - 12%)),
    /* The leading front. Soft-edged, and only ever seen through the squares
       below, which is what breaks it into blocks instead of a straight wipe. */
    linear-gradient(105deg,
      #000 var(--wave-front),
      #0000 calc(var(--wave-front) + 16%)),
    /* Columns and rows, crossed into a grid of squares. */
    repeating-linear-gradient(to right,
      #000 0 var(--wave-block),
      #0000 var(--wave-block) var(--wave-tile)),
    repeating-linear-gradient(to bottom,
      #000 0 var(--wave-block),
      #0000 var(--wave-block) var(--wave-tile));
  mask-composite: add, intersect, intersect, intersect;

  /* Linear on purpose. A wave that gathers pace or coasts to a halt reads as a
     wipe with a wobble in it; even travel is what makes it a wave. */
  animation: wave-sweep .75s linear both;
}

/* The page being left is covered rather than faded — it holds whole
   underneath, and is still whole when the last square lands on it. */
::view-transition-old(root) {
  animation: wave-hold .75s linear both;
}

/* Stacked, not blended. plus-lighter is the browser's default because it stops
   a cross-fade dipping through the middle; under a wipe it does the opposite,
   lighting up every square the instant it lands on the page below. */
::view-transition-image-pair(root) {
  isolation: auto;
}

::view-transition-old(root),
::view-transition-new(root) {
  mix-blend-mode: normal;
}

@keyframes wave-sweep {
  from {
    --wave-front: -20%;
  }

  to {
    --wave-front: 130%;
  }
}

@keyframes wave-hold {

  from,
  to {
    opacity: 1;
  }
}

/* The bar is the same on every page, so it is lifted out of the sweep and
   cross-faded where it stands. The wave passing behind a header that holds
   still is the whole reason for naming it separately. */
.site-header {
  view-transition-name: site-header;
}

::view-transition-old(site-header),
::view-transition-new(site-header) {
  animation-duration: .3s;
  animation-timing-function: var(--ease-soft);
}

/* The veil the script falls back to where cross-document transitions aren't
   available. Same wave, laid over the page being left rather than under the
   one arriving, so the last thing seen is the site's own background closing
   over in blocks — which is the colour the next page opens on.

   The mask is spelled out again rather than shared with the rule above: a
   selector list is thrown out whole if any part of it fails to parse, so
   grouping this with a ::view-transition pseudo would take the fallback down
   in exactly the browsers that need it. */
.page-fade {
  position: fixed;
  inset: 0;
  /* Over the custom cursor, which has nothing left to point at. */
  z-index: 10000;
  background: var(--c-bg);
  opacity: 0;
  pointer-events: none;
  /* Only the hard on/off. The sweep below does the shaping — and where
     mask-image isn't understood at all, this is left holding a plain fade. */
  transition: opacity .12s linear;
}

.page-fade.is-in {
  opacity: 1;
  /* Swallows a second click while the first one is still on its way. */
  pointer-events: auto;

  mask-image:
    linear-gradient(105deg,
      #000 calc(var(--wave-front) - 26%),
      #0000 calc(var(--wave-front) - 12%)),
    linear-gradient(105deg,
      #000 var(--wave-front),
      #0000 calc(var(--wave-front) + 16%)),
    repeating-linear-gradient(to right,
      #000 0 var(--wave-block),
      #0000 var(--wave-block) var(--wave-tile)),
    repeating-linear-gradient(to bottom,
      #000 0 var(--wave-block),
      #0000 var(--wave-block) var(--wave-tile));
  mask-composite: add, intersect, intersect, intersect;

  /* Shorter than the transition proper: this one is holding up a navigation
     that hasn't started yet, so it has to be over quickly. */
  animation: wave-sweep .5s linear both;
}

@media (prefers-reduced-motion: reduce) {

  /* The blanket rule further down reaches *::before and *::after; these
     pseudo-elements are neither, so they have to be said outright. With no
     animation left to wait on the transition ends at once and the page simply
     swaps, which is the point. */
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }

  /* With the sweep stood down the mask would otherwise hold wherever it was
     left, so it goes too. */
  ::view-transition-new(root) {
    mask-image: none;
  }
}

/* ============ END OF PAGE TRANSITIONS ============ */

h1,
h2,
h3,
h4,
h5,
h6 {
  margin: 0;
  font-weight: var(--fw-semibold);
  line-height: var(--lh-heading);
}

p {
  margin: 0;
}

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

a {
  color: inherit;
  text-decoration: none;
}

/* ============ START OF LINK UNDERLINE ============ */
/* One shared definition for every plain-text link on the site — grouped onto
   whichever selector needs it rather than repeated per component. .link-underline
   is the class to reach for on any future inline link; the others already carry
   their own selector, so joining them here costs nothing in the markup.

   The line is a pseudo-element, not text-decoration: a native underline can
   only fade in, not grow — and it can only be revealed by hover/focus, not
   animated onto the page in a single call. Sitting a fifth of an em below the
   text rather than tight against it keeps it clear of descenders (g, y, p),
   and scaleX rather than width means revealing it costs a composite, not a
   layout pass. */
.link-underline,
.hero__status-text a,
.footer-list a,
.case-meta__link {
  position: relative;
}

.link-underline::after,
.hero__status-text a::after,
.footer-list a::after,
.case-meta__link::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -0.2em;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .3s var(--ease);
}

.link-underline:hover::after,
.link-underline:focus-visible::after,
.hero__status-text a:hover::after,
.hero__status-text a:focus-visible::after,
.footer-list a:hover::after,
.footer-list a:focus-visible::after,
.case-meta__link:hover::after,
.case-meta__link:focus-visible::after {
  transform: scaleX(1);
}

/* ============ END OF LINK UNDERLINE ============ */

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

.container {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.skip-link {
  position: absolute;
  left: -624.9375rem;
  top: 0;
  z-index: 100;
  padding: var(--space-16) var(--space-24);
  background: var(--c-blue);
  color: var(--c-white);
}

.skip-link:focus {
  left: 0;
}

/* ============ START OF BUTTONS ============ */
.btn {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  /* clips the water to the pill */
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
  /* Bottom padding runs 2px short of the top: Poppins' baseline sits low in the
     line box, so an even pad reads bottom-heavy. */
  padding: var(--space-16) var(--space-24) calc(var(--space-16) - 2px);
  border: 1px solid var(--c-navy);
  border-radius: var(--radius-4xl);
  font-size: var(--fs-base);
  font-weight: var(--fw-regular);
  line-height: var(--lh-none);
  white-space: nowrap;
  cursor: pointer;
  /* The label flips as the water passes it, so it never sits half-legible in a
     blend of the outgoing and incoming colours. */
  transition: color .25s var(--ease) .28s, border-color .5s var(--ease-soft);
}

.btn--sm {
  padding: var(--space-12) var(--space-16);
  border-radius: var(--radius-4xl);
  font-size: var(--fs-sm);
}

/* Water fill: an oversized panel with a domed top, parked below the button and
   rising on hover. The dome reads as a meniscus while it climbs, and the rise
   overshoots so the curve clears the top and the button ends up fully filled.
   The wobble animates border-radius only, so it never fights the transform. */
.btn::before {
  content: "";
  position: absolute;
  left: -20%;
  bottom: -60%;
  width: 140%;
  height: 160%;
  z-index: 0;
  /* Round, not squircle: this radius draws the meniscus dome itself rather
     than easing a corner, and a superellipse would flatten it into a lid. */
  border-radius: 46% 48% 0 0 / 26% 24% 0 0;
  corner-shape: round;
  transform: translateY(100%);
  transition: transform .7s var(--ease-soft);
}

.btn:hover::before,
.btn:focus-visible::before {
  transform: translateY(-18%);
  animation: waterWobble 3.4s ease-in-out infinite;
}

@keyframes waterWobble {

  0%,
  100% {
    border-radius: 46% 48% 0 0 / 26% 24% 0 0;
  }

  50% {
    border-radius: 49% 44% 0 0 / 21% 29% 0 0;
  }
}

.btn>* {
  position: relative;
  z-index: 1;
}

/* label and icon ride above */
.btn__icon {
  display: flex;
}

/* Primary: green fill, water drains it back to an outline.
   Secondary: blue water fills the outline. */
.btn--primary {
  background: var(--c-green);
  color: var(--c-bg);
}

.btn--primary::before {
  background: var(--c-bg);
}

.btn--primary:hover,
.btn--primary:focus-visible {
  color: var(--c-green);
  border-color: var(--c-green);
}

.btn--secondary {
  background: transparent;
  color: var(--c-blue);
}

.btn--secondary::before {
  background: var(--c-blue);
}

.btn--secondary:hover,
.btn--secondary:focus-visible {
  color: var(--c-bg);
  border-color: var(--c-blue);
}

/* ============ END OF BUTTONS ============ */

/* ============ START OF HEADER ============ */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: transparent;
}

/* Progressive blur: four backdrop-filter layers of rising radius, each masked
   to end sooner than the one below it. Where they overlap the blur compounds,
   so it is heaviest at the top and tapers to nothing past the bar. */
.nav-blur {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: calc(var(--header-h) + 4.125rem);
  pointer-events: none;
}

.nav-blur span {
  position: absolute;
  inset: 0;
  display: block;
}

.nav-blur span:nth-child(1) {
  -webkit-backdrop-filter: blur(1px);
  backdrop-filter: blur(1px);
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 70%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 0%, #000 70%, transparent 100%);
}

.nav-blur span:nth-child(2) {
  -webkit-backdrop-filter: blur(0.25rem);
  backdrop-filter: blur(0.25rem);
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 45%, transparent 75%);
  mask-image: linear-gradient(to bottom, #000 0%, #000 45%, transparent 75%);
}

.nav-blur span:nth-child(3) {
  -webkit-backdrop-filter: blur(0.625rem);
  backdrop-filter: blur(0.625rem);
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 25%, transparent 50%);
  mask-image: linear-gradient(to bottom, #000 0%, #000 25%, transparent 50%);
}

.nav-blur span:nth-child(4) {
  -webkit-backdrop-filter: blur(1.25rem);
  backdrop-filter: blur(1.25rem);
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 8%, transparent 28%);
  mask-image: linear-gradient(to bottom, #000 0%, #000 8%, transparent 28%);
}

.header-inner {
  /* Overrides .container's centered 71.25rem wrap: the bar runs closer to the
     viewport edges instead of sitting inside the content measure. */
  max-width: none;
  /* Center the logo and button in the gap between the viewport edges and the main content (71.25rem wide) */
  padding-inline: max(var(--gutter), calc((50vw - 35.625rem) / 2));
  position: relative;
  /* above the blur layers */
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-16);
  min-height: var(--header-h);
}

.brand img {
  width: 3rem;
  height: 3rem;
}


.nav-toggle {
  display: none;
  flex-direction: column;
  justify-content: center;
  /* 2px bar + 0.3125rem gap = the 0.4375rem pitch the open-state transforms assume, so
     this pair stays literal and stays together. */
  gap: 0.3125rem;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
}

.nav-toggle span {
  display: block;
  height: 2px;
  width: 1.5rem;
  margin-inline: auto;
  background: var(--c-white);
  transition: transform .3s var(--ease), opacity .2s linear;
}

.nav-toggle[aria-expanded="true"] span:nth-child(1) {
  transform: translateY(0.4375rem) rotate(45deg);
}

.nav-toggle[aria-expanded="true"] span:nth-child(2) {
  opacity: 0;
}

.nav-toggle[aria-expanded="true"] span:nth-child(3) {
  transform: translateY(-0.4375rem) rotate(-45deg);
}

.mobile-nav {
  position: relative;
  /* above the blur layers */
  z-index: 1;
  border-top: 1px solid var(--c-line);
  padding: var(--space-24) var(--gutter) var(--space-32);
  background: var(--c-bg);
}

.mobile-nav[hidden] {
  display: none;
}

.mobile-nav__actions {
  display: grid;
  gap: var(--space-16);
  justify-items: start;
}

/* ============ END OF HEADER ============ */

/* ============ START OF HERO ============ */
.hero {
  position: relative;
  /* anchors the scroll cue */
  overflow: hidden;
  /* clips the parallax layers as they lag */
  display: flex;
  align-items: stretch;
  min-height: 100vh;
  /* The header is sticky, so it holds 5.25rem of flow above this section. Pull the
     hero back up under it and pad the content down instead — otherwise the top
     of the page is a band of body background rather than the gradient. */
  margin-top: calc(var(--header-h) * -1);
  padding: calc(var(--header-h) + var(--space-48)) var(--gutter) var(--space-64);
}

/* Particle field. pointer-events stay off — the pointer is tracked on the
   section itself, so the canvas never intercepts a click. */
.hero__particles {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

/* Positioned children paint above the canvas without needing a negative index. */
.hero__left,
.hero__right {
  position: relative;
  z-index: 1;
}

.hero__left {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-end;
  flex: 0 0 30%;
  padding-right: var(--space-32);
}

.hero__kicker {
  font-size: var(--fs-base);
  font-weight: var(--fw-regular);
  /* Stated rather than inherited: this was an h5 and took the heading
     line-height with it. As a paragraph it would fall to the body's 1.6 and
     grow its line box, which shifts the whole centred column beneath it. */
  line-height: var(--lh-heading);
  color: var(--c-white);
}

.hero__status {
  display: flex;
  align-items: center;
  gap: var(--space-16);
}

/* Availability dot — solid core with a halo that swells and fades out. The
   halo starts at opacity 0 so it stays invisible under reduced motion. */
.status-dot {
  position: relative;
  flex: none;
  width: 0.625rem;
  height: 0.625rem;
  border-radius: var(--radius-circle);
  corner-shape: round;
  background: var(--c-green);
}

.status-dot::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--radius-circle);
  corner-shape: round;
  background: var(--c-green);
  opacity: 0;
  animation: statusPulse 2.4s ease-out infinite;
}

@keyframes statusPulse {
  0% {
    transform: scale(1);
    opacity: .5;
  }

  70% {
    transform: scale(4);
    opacity: 0;
  }

  100% {
    transform: scale(4);
    opacity: 0;
  }
}

.hero__status-text {
  font-size: var(--fs-3xl);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-none);
  color: var(--c-white);
}

.hero__status-text a {
  transition: color .3s var(--ease);
}

.hero__status-text a:hover {
  color: var(--c-green);
}

.hero__right {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--space-4);
  flex: 1 1 70%;
}

/* The h1 holding the three lines of the name. It stands in for what used to be
   three sibling headings, so it carries the column and the gap they had from
   .hero__right — without it the lines would close up by the gap between them. */
.hero__title {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.hero__name {
  overflow: hidden;
  /* the mask the line rises out of */
  font-size: var(--fs-hero);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-heading);
  color: var(--c-white);
}

.hero__name>span {
  display: block;
}

/* Staged entrance. Pure CSS, so it needs no trigger class and cannot leave the
   hero blank if the script fails. `backwards` rather than `both`: it holds the
   from-state through the delay but releases the element once it lands, leaving
   transform and opacity free for the scroll parallax to drive. */
@media (prefers-reduced-motion: no-preference) {
  .hero__name>span {
    animation: heroLine .9s var(--ease-soft) backwards;
  }

  .hero__name:nth-of-type(1)>span {
    animation-delay: .10s;
  }

  .hero__name:nth-of-type(2)>span {
    animation-delay: .20s;
  }

  .hero__name:nth-of-type(3)>span {
    animation-delay: .30s;
  }

  .hero .rotator {
    animation: heroRise .8s var(--ease-soft) .48s backwards;
  }

  .hero__actions {
    animation: heroRise .8s var(--ease-soft) .60s backwards;
  }

  .hero__left {
    animation: heroRise .8s var(--ease-soft) .70s backwards;
  }

  .hero__cue {
    animation: heroCue .8s var(--ease-soft) .95s backwards;
  }
}

@keyframes heroLine {
  from {
    transform: translateY(110%);
  }

  to {
    transform: translateY(0);
  }
}

@keyframes heroRise {
  from {
    opacity: 0;
    transform: translateY(1.125rem);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

/* Scroll cue — a rail with a light travelling down it. */
.hero__cue {
  position: absolute;
  z-index: 1;
  left: 50%;
  bottom: var(--space-24);
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-8);
  font-size: var(--fs-3xs);
  font-weight: var(--fw-regular);
  letter-spacing: var(--ls-caps-wide);
  text-transform: uppercase;
  color: var(--c-muted);
  transition: color .3s var(--ease);
}

.hero__cue:hover,
.hero__cue:focus-visible {
  color: var(--c-blue);
}

.hero__cue-line {
  position: relative;
  width: 1px;
  height: 2.875rem;
  overflow: hidden;
  background: var(--c-line);
}

.hero__cue-line i {
  position: absolute;
  inset: 0 0 auto 0;
  height: 45%;
  background: var(--c-blue);
  animation: cueTravel 2.1s var(--ease) infinite;
}

@keyframes cueTravel {
  0% {
    transform: translateY(-100%);
  }

  100% {
    transform: translateY(222%);
  }
}

@keyframes heroCue {
  from {
    opacity: 0;
    transform: translate(-50%, 1rem);
  }

  to {
    opacity: 1;
    transform: translate(-50%, 0);
  }
}

.rotator {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  /* em, not a spacing token: the word gap has to track the rotator's own
     font-size, which changes at the mobile breakpoint. */
  gap: .4em;
  font-size: var(--fs-2xl);
  font-weight: var(--fw-semibold);
  text-transform: capitalize;
  margin-top: var(--space-8);
}

.rotator__prefix {
  color: var(--c-white);
}

.rotator__slot {
  display: inline-block;
  overflow: hidden;
  height: 1.4em;
  padding: 0 var(--space-8) 0 0;
  color: var(--c-blue);
  vertical-align: bottom;
}

.rotator__inner {
  display: block;
  transition: transform .6s var(--ease);
}

.rotator__inner b {
  display: block;
  height: 1.4em;
  line-height: 1.4em;
  font-weight: var(--fw-semibold);
}

.hero__actions {
  margin-top: var(--space-24);
}

/* ============ END OF HERO ============ */

/* ============ START OF SECTIONS ============ */
.section {
  padding: var(--section-y) 0;
}

/* A label, not a control — it deliberately shares no styling with .btn, so it
   has no hover, no focus ring and nothing to click. */
.section__tag {
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
  margin-bottom: var(--space-48);
  padding: var(--space-16) var(--space-24) calc(var(--space-16) - 2px);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: var(--radius-4xl);
  background: var(--c-surface-glass);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
  font-size: var(--fs-base);
  font-weight: var(--fw-regular);
  line-height: var(--lh-none);
  white-space: nowrap;
  color: var(--c-white);
}

.section__tag svg {
  flex: none;
}

.section--journey {
  background: radial-gradient(at center right, var(--c-plum) 0%, var(--c-bg) 35%);
}

.section--statement {
  background: radial-gradient(at center center, var(--c-navy) 0%, var(--c-bg) 55%);
}

.section--projects {
  background: radial-gradient(at center center, var(--c-navy) 0%, var(--c-bg) 65%);
}

/* ============ END OF SECTIONS ============ */

/* ============ START OF TIMELINE — PINNED HORIZONTAL SCRUB ============ */
.section--journey {
  padding: 0;
}

.journey-scroll {
  position: relative;
}

/* Base / no-JS / reduced-motion: an ordinary swipeable rail. */
.journey-sticky {
  display: flex;
  flex-direction: column;
  gap: var(--space-32);
  padding-block: var(--section-y);
}

/* Enhanced: JS measured the travel distance and pinned the section. */
.is-pinned .journey-sticky {
  position: sticky;
  top: 0;
  justify-content: center;
  gap: var(--space-48);
  height: 100vh;
  padding-block: calc(var(--header-h) + var(--space-24)) var(--space-48);
  overflow: hidden;
}

.journey-head {
  flex: none;
}

.journey-head .section__tag {
  margin-bottom: 0;
}

/* ============ Track ============ */
.timeline-h {
  position: relative;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}

.timeline-h::-webkit-scrollbar {
  display: none;
}

/* Height comes from the tallest card rather than the viewport, so the
   block can be centred instead of stranding empty space above it. */
.is-pinned .timeline-h {
  flex: 0 0 auto;
  min-height: 0;
  overflow: hidden;
  scroll-snap-type: none;
}

/* Progressive edge blur — the navbar's technique turned on its side. Three
   layers of rising radius per edge, each masked to end sooner than the one
   below, so cards soften as they approach the edge rather than being cut off
   sharply. Only while pinned: unpinned the container is a real scroller, and
   absolutely positioned children would scroll away with the content. */
.timeline-h__fade {
  position: absolute;
  top: 0;
  /* Stops at the underside of the cards. The track carries a transform, so it
     is its own stacking context and the rail, stems and markers inside it can
     never paint above this layer on z-index alone — the only way to keep the
     progress bar sharp is to end the blur before it. Mirrors the entry stack:
     marker (2.8125rem) + the card's bottom margin + the track's floor. */
  bottom: calc(2.8125rem + var(--space-32) + var(--tl-floor));
  z-index: 1;
  /* Reaches the content start line — the same gutter as .container — with a
     floor so narrower desktops still get a readable ramp. */
  width: max(7.5rem, calc(max(0px, (100% - var(--wrap)) / 2) + var(--gutter)));
  pointer-events: none;
  display: none;
}

.is-pinned .timeline-h__fade {
  display: block;
}

.timeline-h__fade--left {
  left: 0;
}

.timeline-h__fade--right {
  right: 0;
}

.timeline-h__fade>span {
  position: absolute;
  inset: 0;
  display: block;
}

.timeline-h__fade--left>span:nth-child(1),
.timeline-h__fade--right>span:nth-child(1) {
  -webkit-backdrop-filter: blur(1.0.3125rem);
  backdrop-filter: blur(1.0.3125rem);
}

.timeline-h__fade--left>span:nth-child(2),
.timeline-h__fade--right>span:nth-child(2) {
  -webkit-backdrop-filter: blur(0.3125rem);
  backdrop-filter: blur(0.3125rem);
}

.timeline-h__fade--left>span:nth-child(3),
.timeline-h__fade--right>span:nth-child(3) {
  -webkit-backdrop-filter: blur(0.75rem);
  backdrop-filter: blur(0.75rem);
}

.timeline-h__fade--left>span:nth-child(1) {
  -webkit-mask-image: linear-gradient(to left, transparent 0%, #000 65%);
  mask-image: linear-gradient(to left, transparent 0%, #000 65%);
}

.timeline-h__fade--left>span:nth-child(2) {
  -webkit-mask-image: linear-gradient(to left, transparent 30%, #000 80%);
  mask-image: linear-gradient(to left, transparent 30%, #000 80%);
}

.timeline-h__fade--left>span:nth-child(3) {
  -webkit-mask-image: linear-gradient(to left, transparent 58%, #000 96%);
  mask-image: linear-gradient(to left, transparent 58%, #000 96%);
}

.timeline-h__fade--right>span:nth-child(1) {
  -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 65%);
  mask-image: linear-gradient(to right, transparent 0%, #000 65%);
}

.timeline-h__fade--right>span:nth-child(2) {
  -webkit-mask-image: linear-gradient(to right, transparent 30%, #000 80%);
  mask-image: linear-gradient(to right, transparent 30%, #000 80%);
}

.timeline-h__fade--right>span:nth-child(3) {
  -webkit-mask-image: linear-gradient(to right, transparent 58%, #000 96%);
  mask-image: linear-gradient(to right, transparent 58%, #000 96%);
}

.timeline-h__track {
  position: relative;
  display: flex;
  align-items: stretch;
  gap: var(--space-64);
  width: max-content;
  /* Same gutter as .container, so the first card lines up with the section tag.
     A percentage resolves against the containing block; 100vw would include the
     scrollbar and land ~0.4375rem out. */
  padding-inline: calc(max(0px, (100% - var(--wrap)) / 2) + var(--gutter));
  /* Clearance under the markers. Without it they sit flush against the
     overflow edge and the active state's scale(1.1) gets clipped. */
  padding-bottom: var(--tl-floor);
  will-change: transform;
}

/* The tail matches the gutter too, so travel ends with the last card on the
   container's right edge rather than leaving a long empty run of rail. */
.is-pinned .timeline-h__track {
  padding-right: calc(max(0px, (100% - var(--wrap)) / 2) + var(--gutter));
}

/* The rail runs along the bottom of the track, level with the icon row.
   `bottom` is measured from the padding box, so the floor has to be added
   back in to keep the line through the middle of the markers. */
.timeline-h__rail {
  position: absolute;
  bottom: calc(2.8125rem / 2 + var(--tl-floor));
  left: 0;
  right: 0;
  z-index: 2;
  /* above the cards it runs behind */
  height: 0.25rem;
  transform: translateY(50%);
  background: var(--c-surface);
}

.timeline-h__fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0;
  background: var(--c-blue);
}

/* Entry — date, card, then the marker sitting on the rail. The label sits
   fixed to the top so every date lines up across entries; the auto margin
   above the card sweeps whatever's left over down to it, keeping the card
   and its stem bottom-aligned regardless of how tall the label or card get. */
.tl-entry {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 26.25rem;
  flex: none;
}

.tl-label {
  align-self: flex-start;
  margin-bottom: var(--space-16);
}

.tl-card {
  margin-top: auto;
  margin-bottom: var(--space-32);
}

.tl-icon {
  flex: none;
}

/* the vertical stem joining a card down to the rail */
.tl-entry::before {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 2.8125rem;
  width: 2px;
  height: 1.75rem;
  z-index: 2;
  /* stays with the rail, not the card */
  background: var(--c-surface);
  transform: translateX(-50%);
  transition: background-color .2s linear;
}

.tl-entry.is-active::before {
  background: var(--c-blue);
}

.tl-label {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  text-align: left;
  padding: var(--space-4) 0;
}

.tl-label__main {
  color: var(--c-blue);
  font-weight: var(--fw-medium);
}

.tl-label__sub {
  color: var(--c-muted);
  font-size: var(--fs-sm);
}

.tl-label__badge {
  display: inline-flex;
  align-items: center;
  margin-top: var(--space-4);
  padding: 0.1875rem var(--space-8);
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius-pill);
  font-size: var(--fs-3xs);
  font-weight: var(--fw-medium);
  letter-spacing: .02em;
  text-transform: uppercase;
  color: var(--c-muted);
}

.tl-label__badge--full-time {
  border-color: var(--c-blue-wash-strong);
  background: var(--c-blue-wash);
  color: var(--c-blue);
}

.tl-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.8125rem;
  height: 2.8125rem;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: var(--radius-circle);
  background: var(--c-surface-glass);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
  color: var(--c-white);
  transition: color .2s linear, border-color .2s linear,
    background-color .2s linear, transform .3s var(--ease);
  z-index: 3;
  /* above the rail it sits on */
}

.tl-entry.is-active .tl-icon {
  color: var(--c-blue);
  border-color: var(--c-blue);
  background: var(--c-surface);
  transform: scale(1.1);
}

.tl-card {
  display: flex;
  align-items: flex-start;
  gap: var(--space-16);
  width: 100%;
  padding: var(--space-24);
  border-radius: var(--radius-xl);
  background: var(--c-surface);
  box-shadow: var(--shadow-card);
  transition: opacity .4s var(--ease), transform .4s var(--ease);
}

/* Cards ahead of the playhead sit back until the scrub reaches them. */
.is-pinned .tl-entry:not(.is-active) .tl-card {
  opacity: .45;
  transform: translateY(0.5rem);
}

.tl-card__media {
  flex: 0 0 2.75rem;
}

.tl-card__media img {
  width: 100%;
  height: auto;
  object-fit: contain;
}

.tl-card__body {
  text-align: left;
  min-width: 0;
}

.tl-card__title {
  font-size: var(--fs-xl);
  font-weight: var(--fw-semibold);
  color: var(--c-white);
  margin-bottom: var(--space-8);
}

.tl-card__desc {
  font-size: var(--fs-base);
  font-weight: var(--fw-regular);
  color: var(--c-muted);
}

/* ============ END OF TIMELINE — PINNED HORIZONTAL SCRUB ============ */

/* ============ START OF STATEMENT ============ */
.statement {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: var(--space-64) var(--space-24);
  border-radius: var(--radius-3xl);
  background: radial-gradient(circle at var(--mouse-x, 50%) var(--mouse-y, 50%), rgba(255, 255, 255, 0.08) 0%, var(--c-surface-glass) 50%);
  box-shadow: 0 0 80px var(--c-blue-wash);
  border: 1px solid var(--c-hairline);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  transition: transform 0.1s;
}

.statement__line {
  max-width: 100%;
}

.statement__line b {
  font-size: var(--fs-statement);
  /* --fw-bold was never defined, so this resolved to nothing and the weight
     fell through to the h3's own 600. Naming the weight it was already
     rendering at changes no pixels, and takes the last claim on Poppins 700
     with it — which is why the font request no longer asks for it. */
  font-weight: var(--fw-semibold);
  text-transform: capitalize;
  background: linear-gradient(135deg, var(--c-white) 20%, var(--c-blue-hover) 50%, var(--c-white) 80%);
  background-size: 200% auto;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  display: inline-block;
  transition: background-position 0.5s var(--ease);
}

.statement:hover .statement__line b {
  background-position: 200% center;
  transition: background-position 2s linear;
}

.clip {
  display: block;
  overflow: hidden;
  padding-bottom: .08em;
}

.clip b {
  display: block;
}

.is-visible .clip b {
  animation: clipIn 1s var(--ease) both;
}

@keyframes clipIn {
  from {
    clip-path: inset(0 100% 0 0);
    transform: translateY(.15em);
  }

  to {
    clip-path: inset(0 0 0 0);
    transform: translateY(0);
  }
}

.statement__line:last-child .clip b {
  animation-delay: .35s;
}

/* ============ END OF STATEMENT ============ */

/* ============ START OF PROJECTS ============ */
.projects {
  display: grid;
  gap: var(--section-y);
}

.project-block {
  display: block;
}

/* A deck of cards rather than a grid. Each one comes to rest under the header
   and stays there while the next climbs over it, so the work is read one at a
   time in the order it is laid out instead of taken in at a glance.

   Nothing here is pinned and no scroll is intercepted — the page moves at its
   own speed the whole way through, and a browser that ignores position: sticky
   is left with the same cards one under another, which is the same reading in
   a plainer form.

   Every card sits a step lower than the one before it, so the cards already
   dealt keep an edge showing above the current one rather than being buried.
   --i is a card's place in its own stack and comes from the markup. */
/* The label comes to rest with the deck rather than scrolling off above it.
   Three cards deep into a stack there is otherwise nothing on screen saying
   which group is being looked at, and the cards themselves never say so —
   naming the group once at the top only works while the top is still in view.

   It has to carry a background now that cards pass beneath it: an outline pill
   with nothing behind it would show whatever screenshot happens to be sliding
   under. Glass rather than a flat fill, so the card is still felt going past.

   A sticky label outlives its own deck if left to itself. A card releases when
   the bottom of its stack reaches the card's resting line plus a whole card
   height; the label releases when that same bottom reaches its own line plus
   fifty-odd pixels. The label therefore stays pinned for roughly a card's
   height after the last card has gone, and rides down over whatever is under
   it. --label-shift is the script carrying it back off in step with the deck,
   which is why the sticky part waits for has-sticky-label: with no script to
   take it away again, a label that never leaves is worse than one that never
   sticks. */
.project-block.has-sticky-label .section__tag {
  position: sticky;
  top: calc(var(--header-h) + var(--space-24));
  /* Above the cards, which are positioned too and would otherwise win it on
     document order alone. Well under the header's own 50. */
  z-index: 3;
  transform: translate3d(0, var(--label-shift, 0px), 0);
  background: var(--c-bg-glass);
  -webkit-backdrop-filter: blur(0.75rem);
  backdrop-filter: blur(0.75rem);
}

.project-block .section__tag {
  margin-bottom: var(--space-24);
}

.pstack {
  /* Clears the label stuck above it. --tag-h is measured by the script, which
     is the only thing that knows what the pill came out at once the webfont
     landed; the fallback is what it is designed to be, so a stack that never
     hears from the script still parks below the label rather than under it. */
  --pstack-top: calc(var(--header-h) + var(--space-24) + var(--tag-h, 3.125rem) + var(--space-16));
  --pstack-step: 0.875rem;
  display: flex;
  flex-direction: column;
  gap: var(--space-32);
}

/* A card can only come to rest for as long as the stack below it has room to
   give, and the last card of a deck has none — it arrives at its line and is
   pushed straight back off, so the single card of a one-card deck could never
   rest at all. This is that room: the last card holds for a beat, which is the
   only reason there is anything for the label to sit over.

   It has to be a box in the flow rather than padding on the stack. A sticky
   element is confined to its containing block's *content* box, so padding
   there buys no travel at all — the card reads as sticky, is handed the right
   resting line, and still never leaves its place in the flow. */
.pstack::after {
  content: "";
  display: block;
  flex: none;
  height: 34vh;
}

.pcard {
  position: sticky;
  top: calc(var(--pstack-top) + var(--i, 0) * var(--pstack-step));
  /* Under the label, over nothing else. */
  z-index: 1;
  display: block;
  /* Kept short enough that the card plus its offset always clears the viewport
     — a sticky box taller than the screen never comes to rest at the top, and
     the whole deal falls apart on a short window. */
  height: clamp(21rem, 62vh, 32rem);
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius-3xl);
  background: var(--c-surface-2);
  /* The frame is the mask. What is inside it is taller than the card and
     drifting, and this is the line that crops it back to a window. */
  overflow: hidden;
  isolation: isolate;
  /* Settles back into the deck as cards arrive over it. --recede is written
     per frame by the script and adds up one layer at a time — 0 while a card
     is on top with nothing yet climbing over it, then a step for each card
     that has — so a card starts full size and only shrinks as it is actually
     covered, ending up smaller the more layers end up on top of it. With no
     script at all every card simply sits at rest. */
  transform: scale(calc(1 - var(--recede, 0) * .05));
  /* From the top edge, which is the sliver that stays on show. */
  transform-origin: 50% 0;
  transition: border-color .4s var(--ease), box-shadow .4s var(--ease);
}

.pcard:hover {
  border-color: var(--c-line);
  box-shadow: var(--shadow-lift);
}

.pcard:focus-visible {
  outline: 2px solid var(--c-blue);
  outline-offset: 3px;
}

.pcard__media {
  position: absolute;
  inset: 0;
  z-index: 0;
}

.pcard__img {
  width: 100%;
  /* The overshoot the drift has to travel through. */
  height: 122%;
  object-fit: cover;
  /* --pan runs 0 to 1 across the card's pass through the viewport, and the
     picture covers its own overshoot in that time. Half by default, so the
     shot is centred whether or not anything ever sets it. */
  transform: translate3d(0, calc(var(--pan, .5) * -18%), 0);
  transition: scale .6s var(--ease);
}

.pcard:hover .pcard__img {
  scale: 1.04;
}

/* Reads the card down as it goes under the next one. Over the body as well as
   the picture, so the type dims with everything else. */
.pcard::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 3;
  background: var(--c-bg);
  opacity: calc(var(--recede, 0) * .6);
  pointer-events: none;
}

.pcard__body {
  position: relative;
  z-index: 2;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: var(--space-16);
  padding: var(--space-32);
  /* Carries the type over whatever the picture happens to be doing behind it,
     and the picture is a screenshot — it can be near-white directly under a
     line of body copy. Ramped rather than flat so the shot is only paid for
     where the words actually are, and held near-opaque across the whole depth
     the copy occupies rather than only at the very bottom. */
  background: linear-gradient(to top,
      rgba(11, 13, 18, .97) 0%,
      rgba(11, 13, 18, .95) 50%,
      rgba(11, 13, 18, .6) 70%,
      rgba(11, 13, 18, .15) 88%,
      transparent 100%);
}

.pcard__title {
  font-size: var(--fs-5xl);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-tight);
  color: var(--c-white);
}

.pcard__desc {
  max-width: 52ch;
  font-size: var(--fs-sm);
  line-height: var(--lh-snug);
  color: var(--c-muted);
}

.pcard__facts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
}

.pcard__facts span {
  padding: var(--space-4) var(--space-8);
  border: 1px solid var(--c-hairline);
  border-radius: var(--radius-pill);
  background: var(--c-surface-glass);
  font-size: var(--fs-2xs);
  color: var(--c-muted);
}

.pcard__cta {
  display: inline-flex;
  align-items: center;
  gap: var(--space-8);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--c-blue-hover);
}

.pcard__cta i {
  font-style: normal;
  transition: translate .3s var(--ease);
}

.pcard:hover .pcard__cta i {
  translate: 0.25rem 0;
}

/* ============ END OF PROJECTS ============ */

/* ============ START OF FOOTER + UTILITIES ============ */
/* The same footer on every page — the home page, the four case studies and the
   seven error pages. It is the only navigation the deeper pages carry besides
   the header's one button, and on an error page it is the only way out at all,
   which is why the whole thing goes there rather than a copyright line. */
.site-footer {
  margin-top: var(--section-y);
  padding-top: var(--space-64);
  border-top: 1px solid var(--c-hairline);
  background: var(--c-bg);
  font-size: var(--fs-sm);
  color: var(--c-muted);
}

.footer-inner {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-48);
  justify-content: space-between;
  padding-bottom: var(--space-64);
}

/* Takes what is left after the columns have had theirs, down to a measure that
   still reads as a sentence rather than a column of two-word lines. */
.footer-cta {
  flex: 1 1 20rem;
  max-width: 28rem;
}

.footer-cta__title {
  margin-bottom: var(--space-24);
  font-size: var(--fs-3xl);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-heading);
  letter-spacing: var(--ls-tight);
  color: var(--c-white);
}

.footer-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-48);
}

.footer-col__title {
  margin-bottom: var(--space-16);
  font-size: var(--fs-2xs);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-caps);
  text-transform: uppercase;
  color: var(--c-white);
}

.footer-list {
  display: grid;
  gap: var(--space-8);
}

.footer-list a {
  color: var(--c-muted);
  transition: color .25s var(--ease);
}

.footer-list a:hover,
.footer-list a:focus-visible {
  color: var(--c-blue-hover);
}

.footer-bottom {
  padding-block: var(--space-24);
  border-top: 1px solid var(--c-hairline);
  font-size: var(--fs-2xs);
}

/* ============ START OF FOOTER RESPONSIVE ============ */
@media (max-width: 55rem) {
  .footer-inner {
    gap: var(--space-32);
  }

  .footer-nav {
    /* Two up rather than three across, which at this width would leave every
       case study title broken over three lines. */
    gap: var(--space-32);
  }

  .footer-col {
    flex: 1 1 9rem;
  }

  .footer-cta__title {
    font-size: var(--fs-2xl);
  }
}

/* ============ END OF FOOTER RESPONSIVE ============ */

.to-top {
  position: fixed;
  right: var(--space-24);
  bottom: var(--space-24);
  z-index: 40;
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  border: 1px solid var(--c-navy);
  border-radius: var(--radius-xl);
  background: var(--c-surface);
  color: var(--c-white);
  cursor: pointer;
  opacity: 0;
  visibility: hidden;
  transform: translateY(0.625rem);
  transition: opacity .3s var(--ease), transform .3s var(--ease), visibility .3s, border-color .5s var(--ease-soft);

  /* Water animation requirements */
  overflow: hidden;
  isolation: isolate;
}

.to-top>* {
  position: relative;
  z-index: 1;
}

.to-top::before {
  content: "";
  position: absolute;
  left: -20%;
  bottom: -60%;
  width: 140%;
  height: 160%;
  z-index: 0;
  /* Round, not squircle: this radius draws the meniscus dome itself rather
     than easing a corner, and a superellipse would flatten it into a lid. */
  border-radius: 46% 48% 0 0 / 26% 24% 0 0;
  corner-shape: round;
  transform: translateY(100%);
  transition: transform .7s var(--ease-soft);
  background: var(--c-surface-2);
}

.to-top.is-visible {
  opacity: 1;
  visibility: visible;
  transform: none;
}

.to-top:hover::before,
.to-top:focus-visible::before {
  transform: translateY(-18%);
  animation: waterWobble 3.4s ease-in-out infinite;
}

.to-top:hover,
.to-top:focus-visible {
  border-color: var(--c-surface-2);
}

/* Scroll reveal.

   An animation rather than a transition, and with no fill mode. A transition
   here would replace whatever `transition` the element already declares — a
   project card's hover lift, a button's fill — and a fill mode would pin
   `transform` afterwards, outranking those same hovers as well as the scale a
   card holds while it sits back in the deck. Without one, the element hands
   control straight back once it has landed. */
.reveal {
  opacity: 0;
}

.reveal.is-visible {
  opacity: 1;
  animation: revealUp .8s var(--ease-soft) var(--reveal-delay, 0s);
}

@keyframes revealUp {
  from {
    opacity: 0;
    transform: translateY(2.5rem);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

/* Custom cursor. The one thing on the site that stays a true circle: the dot
   and the ring read as a pointer, not as a piece of the interface, so they sit
   outside the squircle theme. */
#cursor,
#follower {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 9999;
  pointer-events: none;
  border-radius: var(--radius-circle);
  corner-shape: round;
  opacity: 0;
  transition: opacity .3s linear;
}

#cursor {
  width: 0.5rem;
  height: 0.5rem;
  background: var(--c-blue);
}

#follower {
  width: 2.25rem;
  height: 2.25rem;
  border: 1px solid var(--c-blue);
  transition: opacity .3s linear, width .25s var(--ease), height .25s var(--ease),
    background-color .25s var(--ease);
}

body.has-cursor #cursor,
body.has-cursor #follower {
  opacity: 1;
}

/* The lightbox keeps the site's cursor. It is the only pointer these pages
   have — `cursor: none` is set on everything above — so standing it down over
   the viewer left the controls with nothing under the pointer at all. What the
   native zoom-in and grab cursors were saying, the ring now says itself. */

/* Over something interactive the ring takes over on its own: the dot drops
   out and a faint wash of the accent fills the circle in its place. The
   circle keeps its size — only the fill marks the change. */
body.has-cursor #cursor.is-hover {
  opacity: 0;
}

#follower.is-hover {
  background: var(--c-blue-wash);
}

/* Pressing is the one thing that resizes the ring. */
#follower.is-down {
  width: 1.625rem;
  height: 1.625rem;
}

#follower.is-hover.is-down {
  background: var(--c-blue-wash-strong);
}

/* The custom cursor replaces the native one outright. `has-cursor` is only
   set once the pointer has actually moved, so the arrow is never hidden
   before its stand-in exists. */
@media (hover: hover) and (pointer: fine) {

  body.has-cursor,
  body.has-cursor * {
    cursor: none;
  }
}

/* ============ END OF FOOTER + UTILITIES ============ */

/* ============ START OF CONTEXT MENU ============ */
.ctx-menu {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 9000;
  min-width: 14.5rem;
  padding: var(--space-8);
  border: 1px solid var(--c-navy);
  border-radius: var(--radius-lg);
  background: var(--c-menu-bg);
  -webkit-backdrop-filter: blur(0.875rem);
  backdrop-filter: blur(0.875rem);
  box-shadow: var(--shadow-menu);
  transform-origin: top left;
  animation: ctxIn .14s var(--ease) both;
}

.ctx-menu[hidden] {
  display: none;
}

@keyframes ctxIn {
  from {
    opacity: 0;
    transform: scale(.96);
  }

  to {
    opacity: 1;
    transform: scale(1);
  }
}

.ctx-menu__group[hidden] {
  display: none;
}

/* `:not([hidden])` matters: `+` matches on DOM order, so without it the link
   group still draws a separator above the next group while it is display:none,
   leaving a stray line across the top of the menu. */
.ctx-menu__group:not([hidden])+.ctx-menu__group {
  margin-top: var(--space-8);
  padding-top: var(--space-8);
  border-top: 1px solid var(--c-hairline);
}

.ctx-menu__item {
  display: flex;
  align-items: center;
  gap: var(--space-8);
  width: 100%;
  padding: var(--space-8) var(--space-16);
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--c-white);
  font-family: inherit;
  font-size: var(--fs-sm);
  font-weight: var(--fw-regular);
  line-height: var(--lh-none);
  text-align: left;
  cursor: pointer;
  transition: background-color .15s var(--ease), color .15s var(--ease);
}

.ctx-menu__item:hover,
.ctx-menu__item:focus-visible {
  background: var(--c-navy);
  color: var(--c-blue);
  outline: none;
}

.ctx-menu__item svg {
  flex: none;
  opacity: .6;
}

.ctx-menu__item:hover svg,
.ctx-menu__item:focus-visible svg {
  opacity: 1;
}

.ctx-menu__item.is-done {
  color: var(--c-green);
}

.ctx-menu__item.is-done svg {
  opacity: 1;
}

.ctx-menu__ext {
  margin-left: auto;
  font-size: var(--fs-2xs);
  opacity: .5;
}

/* ============ END OF CONTEXT MENU ============ */

/* ============ START OF RESPONSIVE ============ */
@media (max-width: 55rem) {
  :root {
    --header-h: 4.25rem;
  }

  /* padding-inline is inherited as var(--gutter) to align with container */

  .header-inner>.btn {
    padding: 0.6rem 0.8rem;
    border-radius: var(--radius-sm);
    font-size: 0.7rem;
  }

  .header-inner>.btn svg {
    width: 11px;
    height: 11px;
  }

  /* Hamburger menu and its related display overrides have been removed to show the Work With Me button instead. */

  .hero {
    flex-direction: column;
    justify-content: center;
    gap: var(--space-32);
    padding-top: calc(var(--header-h) + var(--space-24));
  }

  .hero__left {
    order: 2;
    align-items: flex-start;
    flex: none;
    padding-right: 0;
  }

  .hero__right {
    order: 1;
    flex: none;
  }

  .hero__name {
    font-size: var(--fs-4xl);
    line-height: var(--lh-tight);
  }

  /* Viewport-relative on purpose: the address has to shrink to fit one line on
     a 20rem screen, which no fixed step on the scale can promise. */
  .hero__status-text {
    font-size: 5vw;
  }

  .rotator {
    font-size: var(--fs-lg);
  }

  .hero__actions {
    display: none;
  }

  .hero__cue {
    display: none;
  }

  .section {
    padding: var(--section-y-sm) 0;
  }

  /* Timeline: never pinned here — swipe the rail sideways instead.
     The row layout is unchanged; entries stretch to the tallest card so
     every marker still lands on the rail. */
  .journey-sticky {
    gap: var(--space-24);
    padding-block: var(--section-y-sm);
  }

  .timeline-h {
    scroll-snap-type: x mandatory;
  }

  /* No edge blur down here. The ramp is a fixed 7.5rem a side, which is most of
     a phone screen gone soft the moment two of them meet — and what it is
     softening is a snap-scrolled track whose cards are meant to sit dead centre
     and be read. The selector matches the one that switches these on, or it
     would lose to it. */
  .is-pinned .timeline-h__fade {
    display: none;
  }

  .timeline-h__track {
    gap: var(--space-24);
  }

  .tl-entry {
    width: min(18.75rem, 78vw);
    scroll-snap-align: center;
  }

  .tl-card {
    flex-direction: column;
    align-items: center;
    text-align: center;
  }

  .tl-card__media {
    flex: none;
    width: 3.125rem;
  }

  .tl-card__body {
    text-align: center;
  }

  .tl-card__title {
    font-size: var(--fs-lg);
  }

  .tl-card__desc {
    font-size: var(--fs-sm);
  }

  .statement {
    align-items: center;
  }

  /* Shorter cards and a tighter step: the header is smaller down here, and a
     phone has to fit the whole card plus the deal above it on one screen. */
  .pstack {
    --pstack-step: 0.625rem;
    gap: var(--space-24);
  }

  .pcard {
    height: clamp(18rem, 56vh, 25rem);
  }

  .pcard__body {
    gap: var(--space-8);
    padding: var(--space-24);
    /* The card is shorter here but the copy is not, so it reaches further up
       the ramp — far enough on the brighter screenshots to take the body text
       under 4.5:1. The opaque part is carried higher to meet it, which leaves
       the top third of the card as the part still showing its picture. */
    background: linear-gradient(to top,
        rgba(11, 13, 18, .97) 0%,
        rgba(11, 13, 18, .96) 62%,
        rgba(11, 13, 18, .7) 78%,
        rgba(11, 13, 18, .18) 92%,
        transparent 100%);
  }

  .pcard__title {
    font-size: var(--fs-2xl);
  }
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }

  .reveal {
    opacity: 1;
    transform: none;
  }

  /* The script writes neither --pan nor --recede when motion is unwanted, so
     the cards already sit still. The picture is the exception: its overshoot
     exists only to be travelled through, and unused it is just a crop. */
  .pcard__img {
    height: 100%;
    transform: none;
  }
}

/* ============ END OF RESPONSIVE ============ */

/* ============ END OF MAEZKEV — VANILLA REBUILD ============ */