/* Paws Trend — light product-led landing.
   Independent of assets/css/style.css (dark), which the other pages still use.
   Tokens only below this block: no raw hex in components. */

:root {
  /* surface */
  --bg: #eff0f2;
  --surface: #ffffff;
  --surface-sunken: #f6f7f8;
  --line: rgba(0, 0, 0, 0.08);
  --line-strong: rgba(0, 0, 0, 0.14);

  /* ink */
  --text: #0a0a0b;
  /* Same hue/saturation as the old #6b7280, two steps darker. #6b7280 only made
     4.24:1 on --bg, so body copy failed WCAG AA. This clears 4.5:1 on all three
     light surfaces: 4.76 on --bg, 5.43 on --surface, 5.06 on --surface-sunken. */
  --text-dim: #646a77;
  --ink: #0a0a0b;
  --on-ink: #ffffff;
  --on-ink-dim: rgba(255, 255, 255, 0.62);

  /* accent — one blue. Red only terminates the gradient. */
  --blue: #2f8fd6;
  --blue-soft: rgba(47, 143, 214, 0.10);
  --red: #ff4d4d;
  --gradient: linear-gradient(135deg, var(--blue), var(--red));

  /* pastel chips */
  --chip-green: rgba(52, 168, 83, 0.12);
  --chip-pink: rgba(236, 72, 153, 0.12);
  --chip-purple: rgba(139, 92, 246, 0.12);
  --chip-blue: rgba(47, 143, 214, 0.12);

  /* app-window stage — near-opaque on purpose: the 3D objects were lit for a
     white room and lose contrast if an aurora blob shows through behind them. */
  --surface-stage: rgba(255, 255, 255, 0.92);

  /* ── aurora ──────────────────────────────────────────────────────────────
     Three colour fields drift behind the whole page. The hues are ones the
     system already owns: --blue, the --chip-purple violet, and --red. The veil
     is a fixed white wash painted over all three — it is the contrast floor,
     not a styling choice, and must never be animated or removed. Red rides
     lower than the other two because saturated red reads heavier at equal
     alpha and drags the page toward "warning banner". */
  --aurora-1: rgba(47, 143, 214, 0.45);
  --aurora-2: rgba(139, 92, 246, 0.42);
  --aurora-3: rgba(255, 77, 77, 0.34);
  --aurora-veil: rgba(255, 255, 255, 0.34);
  --aurora-blur: 60px;
  --aurora-blur-sm: 36px;

  /* ── glass ───────────────────────────────────────────────────────────────
     --glass-bg's 0.78 is a contrast floor, not a taste call. The darkest pixel
     the aurora can produce is #b8bbf3 (blue+violet at peak alpha, then the
     veil); saturate(150%) takes it to #b4baff; 78% white over that lands on
     #eff0ff, where --text-dim scores 4.79:1. At 0.65 it would be ~4.3:1 and
     fail AA. Do not lower it without redoing that arithmetic.
     --glass-mask exists so the rim highlight's mask can be written without a
     raw hex below :root. */
  --glass-bg: rgba(255, 255, 255, 0.78);
  --glass-bg-solid: rgba(255, 255, 255, 0.88);
  --glass-bg-nav: rgba(255, 255, 255, 0.72);
  --glass-border: rgba(255, 255, 255, 0.60);
  --glass-rim: rgba(255, 255, 255, 0.90);
  --glass-rim-soft: rgba(255, 255, 255, 0.35);
  --glass-clear: rgba(255, 255, 255, 0);
  --glass-mask: linear-gradient(rgba(0, 0, 0, 1) 0 0);
  --glass-blur: 20px;
  --glass-blur-card: 12px;
  --glass-blur-nav: 14px;
  --glass-blur-nav-lg: 22px;
  --glass-saturate: 150%;
  --glass-saturate-nav: 180%;
  --glass-shadow: 0 1px 2px rgba(0, 0, 0, 0.04),
                  0 12px 32px rgba(16, 24, 48, 0.08),
                  0 32px 64px rgba(16, 24, 48, 0.05);
  --glass-shadow-lift: 0 2px 6px rgba(0, 0, 0, 0.05),
                       0 20px 48px rgba(16, 24, 48, 0.12);
  --shadow-ink: 0 40px 80px -24px rgba(16, 24, 48, 0.35);

  /* ── liquid glass ────────────────────────────────────────────────────────
     Used only by the dark card surfaces (see the .hero-shell/.step/.stat
     token block further down, which is where the bevel is switched on). On
     the light glass these stay unused: a white-on-white bevel is invisible
     and the ripple would read as dirt on the page. */
  --liquid-spec: rgba(255, 255, 255, 0.34);   /* pointer specular on the rim */
  --liquid-spec-soft: rgba(255, 255, 255, 0.10);
  /* Crest alphas are deliberately low and the periods deliberately long. The
     first pass used 0.30/0.26 at 34px/47px and the result read as a radar
     sweep: enough rings to tile the whole card, bright enough to turn a black
     card blue. A ripple is a small disturbance near the cursor, so there
     should only ever be two or three crests visible at once. */
  --liquid-ring-a: rgba(120, 190, 255, 0.16);  /* cool crest */
  --liquid-ring-b: rgba(160, 130, 250, 0.13);  /* violet trough */
  --liquid-ring-period-a: 54px;
  --liquid-ring-period-b: 73px;
  --liquid-ripple-reach: 190px;               /* mask radius: how far crests carry */
  /* The bevel is what gives the card thickness — a bright top edge, a dim
     return along the bottom, and a wide soft inner glow standing in for light
     caught inside the pane. Inset shadows follow border-radius exactly, which
     is why this is a shadow stack and not another masked pseudo-element: a
     wide masked band leaves square inner corners on a rounded card. */
  --liquid-bevel: inset 0 1px 0 rgba(255, 255, 255, 0.30),
                  inset 0 -1px 0 rgba(255, 255, 255, 0.06),
                  inset 0 0 28px 8px rgba(255, 255, 255, 0.040);

  /* ── Resting gloss ───────────────────────────────────────────────────────
     Polished-lacquer reflection for the dark cards, as background-image
     layers so it is present at rest — the WebGL surface only exists while a
     card is hovered, so everything a visitor sees the rest of the time is
     this. Gloss reads from a *structured* reflection with a defined edge, not
     from an overall lift: the body stays near-black and only these highlights
     are bright. Peak is ~0.11 white over the composited card, which lands
     near #333 and holds the body copy at 6.4:1 — the documented figure is
     8.1:1 on the flat card and AA needs 4.5:1. */
  --liquid-gloss:
    /* Thin bright sheen hugging the top edge — the single most lacquer-like
       cue, and the one that costs almost no area. */
    linear-gradient(180deg,
      rgba(255, 255, 255, 0.26) 0px,
      rgba(255, 255, 255, 0.10) 3px,
      rgba(255, 255, 255, 0.035) 12px,
      rgba(255, 255, 255, 0) 30px),
    /* Tight corner softbox. */
    radial-gradient(70% 46% at 14% -10%,
      rgba(255, 255, 255, 0.11), rgba(255, 255, 255, 0) 60%),
    /* Faint cool bounce off the opposite corner. */
    radial-gradient(58% 38% at 92% 110%,
      rgba(128, 168, 255, 0.05), rgba(255, 255, 255, 0) 66%);

  /* ── per-service identity ────────────────────────────────────────────────
     Three of the five hues already exist in the chip palette, so this reads as
     an extension of the system rather than five invented colours.
     --svc-*      vivid: glows, fills, 3D accents.
     --svc-*-ink  darkened text form. ALWAYS use this for text — the vivid
                  values are 2.1–3.5:1 on white and would fail AA. Each -ink
                  below clears 4.5:1 on the worst possible glass surface
                  (#eff0ff), on white, and on its own -soft pill. */
  --svc-build: #2f8fd6;
  --svc-build-ink: #1a5f8c;
  --svc-build-soft: rgba(47, 143, 214, 0.12);
  --svc-build-glow: rgba(47, 143, 214, 0.28);

  --svc-intelligence: #8b5cf6;
  --svc-intelligence-ink: #6d3fd4;
  --svc-intelligence-soft: rgba(139, 92, 246, 0.12);
  --svc-intelligence-glow: rgba(139, 92, 246, 0.28);

  --svc-content: #ec4899;
  --svc-content-ink: #b52568;
  --svc-content-soft: rgba(236, 72, 153, 0.12);
  --svc-content-glow: rgba(236, 72, 153, 0.28);

  --svc-growth: #f59e0b;
  --svc-growth-ink: #8a5502;
  --svc-growth-soft: rgba(245, 158, 11, 0.12);
  --svc-growth-glow: rgba(245, 158, 11, 0.28);

  --svc-foundation: #14b8a6;
  --svc-foundation-ink: #0b7067;
  --svc-foundation-soft: rgba(20, 184, 166, 0.12);
  --svc-foundation-glow: rgba(20, 184, 166, 0.28);

  /* Current-service channel, rebound by [data-service] on .showcase. These
     defaults are what the no-JS page renders. */
  --svc: var(--svc-build);
  --svc-ink: var(--svc-build-ink);
  --svc-soft: var(--svc-build-soft);
  --svc-glow: var(--svc-build-glow);

  /* type */
  --font: 'Inter', system-ui, -apple-system, sans-serif;

  /* spacing — same scale as the dark system */
  --space-1: 1px;  --space-2: 4px;  --space-3: 8px;  --space-4: 15px;
  --space-5: 28px; --space-6: 40px; --space-7: 48px; --space-8: 56px;
  --space-9: 88px; --space-10: 128px;

  /* radius */
  --radius-shell: 40px;
  --radius-card: 32px;
  --radius-window: 20px;
  --radius-sm: 12px;
  --radius-pill: 9999px;

  /* depth — structural, never decorative. No colored glows. */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
  --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.04), 0 8px 24px rgba(0, 0, 0, 0.04);
  --shadow-lift: 0 2px 4px rgba(0, 0, 0, 0.05), 0 16px 40px rgba(0, 0, 0, 0.07);

  /* motion */
  --transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  --transition-slow: 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}

/* ── Reset ──────────────────────────────────────────────────────────────── */

*,
*::before,
*::after {
  box-sizing: border-box;
}

body,
h1, h2, h3, h4, h5, h6,
p,
figure,
blockquote,
ul, ol, dl, dd {
  margin: 0;
}

img,
svg,
video {
  max-width: 100%;
}

img {
  display: block;
  height: auto;
}

/* ── Base ───────────────────────────────────────────────────────────────── */

body {
  background: var(--bg);
  color: var(--text-dim);
  font-family: var(--font);
  font-size: 17px;
  font-weight: 400;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Cards and pinned sections overshoot the viewport during motion; never let
     that turn into a horizontal scrollbar. */
  overflow-x: hidden;
}

/* ── Aurora background ──────────────────────────────────────────────────── */

.aurora {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  /* Isolates this layer's paint and layout from the page. NOT `contain: strict`
     — that includes size containment, which would collapse an inset:0 box. */
  contain: layout paint style;
}

/* Explicit stacking rather than relying on negative z-index against the body's
   propagated background. */
main,
.footer {
  position: relative;
  z-index: 1;
}

/* The blob's gradient, blur and drift all live on ::before, and the parallax
   in motion.js writes transform on the parent. This split is load-bearing: a
   CSS @keyframes animation sits ABOVE the inline-style cascade origin, so a
   drift animation on the same element GSAP writes to would silently win and
   the parallax would look broken with no error anywhere. */
.aurora-blob {
  position: absolute;
  will-change: transform;
}

.aurora-blob::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  /* A radial gradient fading to transparent is already soft; the blur only
     dissolves the banding a field this large shows on 8-bit displays. It is
     rasterised once and then only composited — which is why nothing here ever
     animates the blur radius, the gradient stops, or opacity. */
  filter: blur(var(--aurora-blur));
  will-change: transform;
}

.aurora-blob--1 { width: 72vmax; height: 72vmax; top: -20vmax; left: -16vmax; }
.aurora-blob--2 { width: 64vmax; height: 64vmax; top: 6vmax; right: -18vmax; }
.aurora-blob--3 { width: 56vmax; height: 56vmax; bottom: -22vmax; left: 20vmax; }

.aurora-blob--1::before {
  background: radial-gradient(circle at 50% 50%, var(--aurora-1) 0%, var(--glass-clear) 68%);
  animation: aurora-drift-1 34s ease-in-out infinite alternate;
}

.aurora-blob--2::before {
  background: radial-gradient(circle at 50% 50%, var(--aurora-2) 0%, var(--glass-clear) 68%);
  animation: aurora-drift-2 42s ease-in-out infinite alternate;
}

.aurora-blob--3::before {
  background: radial-gradient(circle at 50% 50%, var(--aurora-3) 0%, var(--glass-clear) 68%);
  animation: aurora-drift-3 28s ease-in-out infinite alternate;
}

/* Three durations that don't share a common factor, so the composite never
   visibly loops. */
@keyframes aurora-drift-1 {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(6vmax, 4vmax, 0) scale(1.12); }
}

@keyframes aurora-drift-2 {
  from { transform: translate3d(0, 0, 0) scale(1.08); }
  to   { transform: translate3d(-7vmax, 5vmax, 0) scale(1); }
}

@keyframes aurora-drift-3 {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(5vmax, -6vmax, 0) scale(1.15); }
}

/* The contrast floor. Fixed opacity, painted over all three blobs, never
   animated, never conditional — this is what makes the glass surfaces above
   it provably clear 4.5:1 no matter where the blobs have drifted to. */
.aurora::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background: var(--aurora-veil);
  pointer-events: none;
}

h1, h2, h3 {
  color: var(--text);
  font-weight: 700;
  letter-spacing: -0.035em;
  line-height: 1.05;
}

/* ── Display type ───────────────────────────────────────────────────────── */

.display-1 { font-size: clamp(40px, 6vw, 76px); font-weight: 800; letter-spacing: -0.045em; }
.display-2 { font-size: clamp(28px, 4vw, 44px); font-weight: 800; letter-spacing: -0.045em; }

.lede {
  max-width: 46ch;
  color: var(--text-dim);
  font-size: clamp(17px, 1.4vw, 19px);
}

/* The blue→red gradient is rationed to exactly two words on the page: the hero
   focal word and the CTA headline focal word. Nowhere else. */
.gradient-word {
  background: var(--gradient);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* ── Pill badge ─────────────────────────────────────────────────────────── */

.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-pill);
  background: var(--glass-bg-solid);
  box-shadow: var(--shadow-sm);
  color: var(--text);
  font-size: 14px;
  font-weight: 500;
  line-height: 1;
}

/* ── Buttons ────────────────────────────────────────────────────────────── */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  min-height: 44px;
  padding: var(--space-3) var(--space-5);
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  font-family: inherit;
  font-size: 16px;
  font-weight: 500;
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  transition: transform var(--transition), box-shadow var(--transition),
              border-color var(--transition), background var(--transition);
}

.btn--primary {
  background: var(--ink);
  color: var(--on-ink);
}

.btn--ghost {
  background: var(--glass-bg-solid);
  border-color: var(--glass-border);
  color: var(--text);
}

.btn--primary:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-lift);
}

.btn--ghost:hover {
  border-color: var(--line-strong);
}

.btn:active {
  transform: none;
  box-shadow: none;
}

.btn:focus-visible {
  outline: 2px solid var(--text);
  outline-offset: 3px;
}

/* ── Glass surfaces ─────────────────────────────────────────────────────────
   .glass        translucent + backdrop blur. For surfaces sitting directly on
                 the aurora.
   .glass-solid  the same look with no backdrop-filter. For surfaces NESTED
                 inside a .glass surface — stacking blurs on the same pixels is
                 the expensive case and, over a smooth gradient, is visually a
                 no-op.
   Budget: at most 5 backdrop-filtered surfaces per viewport, never nested,
   never above 20px. Worst case here is nav + four .step cards. */

.glass,
.glass-solid {
  position: relative;
  border: 1px solid var(--glass-border);
  box-shadow: var(--glass-shadow);
}

.glass { background: var(--glass-bg); }
.glass-solid { background: var(--glass-bg-solid); }

@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .glass {
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
            backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  }
}

/* Inner rim highlight: a 1px gradient border, bright on the top-left edge,
   fading out through the middle, a fainter return bottom-right. This is the
   detail that separates "translucent div" from "glass". Drawn as a masked
   pseudo-element because a border cannot hold a gradient. */
.glass::before,
.glass-solid::before,
.glass-card::before,
.service-card::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: inherit;
  padding: 1px;
  /* Three layers, painted top-down. The base 145deg rim is the original fixed
     highlight and still carries the look with no JS. Above it sit two layers
     that only become visible once a pointer is actually moving over the card:
     a white specular that tracks the cursor along the rim, and a chromatic
     dispersion sweep — the faint blue/violet split real glass throws at its
     edge. --pointer-x/y come from studio/tilt.js on .step/.stat/.tab and from
     card-ripple.js elsewhere; .hero-shell has no tilt but does have
     hero-glow.js, so it falls through to --glow-x/y before the 50% default.
     Every fallback resolves to a centred, static rim, which is exactly the
     no-JS state. */
  background:
    radial-gradient(140% 180% at var(--pointer-x, var(--glow-x, 50%)) var(--pointer-y, var(--glow-y, 50%)),
      var(--liquid-spec-edge, transparent) 0%,
      transparent 42%),
    conic-gradient(from calc(var(--liquid-spin, 0deg) - 90deg) at var(--pointer-x, var(--glow-x, 50%)) var(--pointer-y, var(--glow-y, 50%)),
      var(--liquid-fringe-a, transparent),
      var(--liquid-fringe-b, transparent) 25%,
      var(--liquid-fringe-a, transparent) 50%,
      var(--liquid-fringe-b, transparent) 75%,
      var(--liquid-fringe-a, transparent)),
    linear-gradient(145deg,
      var(--glass-rim) 0%,
      var(--glass-clear) 42%,
      var(--glass-clear) 58%,
      var(--glass-rim-soft) 100%);
  -webkit-mask: var(--glass-mask) content-box, var(--glass-mask);
  -webkit-mask-composite: xor;
          mask: var(--glass-mask) content-box, var(--glass-mask);
          mask-composite: exclude;
  pointer-events: none;
}

/* Without mask compositing the rim paints as a full-surface gradient wash over
   the card's own content, which is much worse than simply not having a rim. */
@supports not ((mask-composite: exclude) or (-webkit-mask-composite: xor)) {
  .glass::before,
  .glass-solid::before,
  .glass-card::before,
  .service-card::before { display: none; }
}

/* ── Glass hover: tilt, lift, coloured glow, cursor sheen ───────────────────
   --tilt-x/--tilt-y/--tilt-scale/--lift/--pointer-x/--pointer-y are written by
   assets/js/studio/tilt.js on pointermove (full tier only). Every one has an
   inline fallback here, so with no JS the transform is an exact no-op and the
   plain :hover lift below still works at every tier. */

.glass-hover {
  /* Default glow is the `build` service's — correct for .step/.stat (which
     carry no data-service) and for the no-JS default state of the tabs. The
     inline fallback is a defensive no-op now that --svc-build-glow is a real
     token (Task 2): kept in case a future .glass-hover surface ever mounts
     before :root finishes parsing in some exotic loading order. */
  --glow: var(--svc-build-glow, rgba(47, 143, 214, 0.28));
  --glow-solid: var(--svc-build, #2f8fd6);
  transform: perspective(900px)
             translateY(var(--lift, 0px))
             rotateX(var(--tilt-x, 0deg))
             rotateY(var(--tilt-y, 0deg))
             scale(var(--tilt-scale, 1));
  transform-style: preserve-3d;
  transition: transform var(--transition), box-shadow var(--transition),
              background var(--transition), color var(--transition);
}


.glass-hover:hover {
  --lift: -6px;
  /* background-color, not the background shorthand: the shorthand would reset
     background-image and take the resting gloss layers with it. */
  background-color: var(--glass-bg-solid);
  /* Solid 2px accent edge, not another soft glow — the one deliberately hard
     cut borrowed from the kinetic reference. Everything else here still eases
     over --transition; this line has no separate timing of its own, so it
     rides the same box-shadow transition as the glow instead of snapping
     independently (a true instant cut looked like a flicker next to the
     eased lift). `inset` keeps the ring painted just inside the card's own
     border box — an outset spread-only shadow (no blur) adds its full spread
     to the painted edge on every side, i.e. it literally enlarges the card
     the instant you hover it, which is exactly the "cards get bigger on
     hover" effect this file must not produce. The glow layer was 48px blur /
     -12px spread -> 12px of sideways reach, which crosses the 15px
     (--space-4) grid gap between .step/.stat cards. Pulling spread to -16px
     brings reach to 32/2 - 16 = 0px while keeping the same 32px blur, so the
     glow still reads as a soft halo, just contained. */
  box-shadow: var(--glass-shadow-lift),
              inset 0 0 0 2px var(--glow-solid),
              0 14px 32px -16px var(--glow);
}

/* Cursor-following sheen — one small repaint on one element, the cheapest
   possible "this card is reacting to me". */
.glass-hover::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: inherit;
  background: radial-gradient(220px circle at var(--pointer-x, 50%) var(--pointer-y, 50%),
                              var(--glow), var(--glass-clear) 72%);
  opacity: 0;
  transition: opacity var(--transition);
  pointer-events: none;
}

.glass-hover:hover::after { opacity: 0.55; }

/* Card content must clear both pseudo-elements. The exclusion keeps this from
   also capturing the liquid-glass canvas, which must stay absolutely
   positioned — see the matching note in pages.css. */
.step > *:not(.liquid-canvas),
.stat > *:not(.liquid-canvas) {
  position: relative;
  z-index: 1;
}

/* ── Hover content inset ────────────────────────────────────────────────────
   The "this card reacts to me" beat, done without touching the card's box.
   The card cannot grow (see the footprint notes on .glass-hover:hover and in
   pages.css), and it cannot animate its own padding either: these cards sit in
   stretched grid rows, so extra padding on the tallest card grows the row and
   every sibling jumps with it. So the content moves instead — a scale on the
   card's own children, which is paint-only and reflows nothing, reading as the
   content pulling in from the edges while the black surface holds still.

   Only the direct children move; the rim (::before) and the ripple (::after)
   are pseudo-elements of the card, so the liquid glass and its crests stay
   pinned to the unchanged edges. */
.step > *:not(.liquid-canvas),
.stat > *:not(.liquid-canvas),
.glass-card > *:not(.liquid-canvas),
.service-card > *:not(.liquid-canvas) {
  transform-origin: center;
  transition: transform var(--transition);
}

.step:hover > *:not(.liquid-canvas),
.stat:hover > *:not(.liquid-canvas),
.glass-card:hover > *:not(.liquid-canvas),
.service-card:hover > *:not(.liquid-canvas) {
  transform: scale(var(--card-inset-scale, 0.95));
}

/* Motion the user did not ask for. The cards keep their hover colour and rim;
   only the inward travel stops. */
@media (prefers-reduced-motion: reduce) {
  .step > *:not(.liquid-canvas),
  .stat > *:not(.liquid-canvas),
  .glass-card > *:not(.liquid-canvas),
  .service-card > *:not(.liquid-canvas) { transition: none; }

  .step:hover > *:not(.liquid-canvas),
  .stat:hover > *:not(.liquid-canvas),
  .glass-card:hover > *:not(.liquid-canvas),
  .service-card:hover > *:not(.liquid-canvas) { transform: none; }
}

/* ── Liquid glass: the pointer ripple ───────────────────────────────────────
   The dark cards' ::after, upgraded from the single sheen blob above into a
   water surface. Two repeating ring sets are stacked at deliberately
   coprime periods (34px and 47px) and animated at different speeds, so the
   crests drift in and out of phase instead of marching in lockstep — that
   interference is what reads as liquid rather than as a sonar ping, and it
   costs two gradients instead of an SVG displacement filter re-rasterising
   the card on every pointer move. Rings carry the two brand hues, so the
   ripple is coloured rather than a grey ripple over a tint.

   A radial mask fades the whole layer out with distance from the cursor:
   without it the rings tile the entire card and the effect stops reading as
   a disturbance at a point. .hero-shell is deliberately excluded — its
   ::after is already the hero cursor glow. */

/* Registered so the offsets can be animated at all: an unregistered custom
   property is an untyped string and interpolates in a single jump. Where
   @property is unsupported the explicit 0px fallbacks below leave the rings
   standing still, which is a perfectly good resting state. */
@property --ripple-a {
  syntax: '<length>';
  inherits: false;
  initial-value: 0px;
}

@property --ripple-b {
  syntax: '<length>';
  inherits: false;
  initial-value: 0px;
}

@keyframes liquid-ripple-a { to { --ripple-a: var(--liquid-ring-period-a); } }
@keyframes liquid-ripple-b { to { --ripple-b: var(--liquid-ring-period-b); } }

.step::after,
.stat::after,
.glass-card::after,
.service-card::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: inherit;
  background:
    repeating-radial-gradient(circle at var(--pointer-x, 50%) var(--pointer-y, 50%),
      transparent calc(var(--ripple-a, 0px)),
      var(--liquid-ring-a) calc(var(--ripple-a, 0px) + 1px),
      var(--liquid-ring-a) calc(var(--ripple-a, 0px) + 2px),
      transparent calc(var(--ripple-a, 0px) + 4px),
      transparent calc(var(--ripple-a, 0px) + var(--liquid-ring-period-a))),
    repeating-radial-gradient(circle at var(--pointer-x, 50%) var(--pointer-y, 50%),
      transparent calc(var(--ripple-b, 0px)),
      var(--liquid-ring-b) calc(var(--ripple-b, 0px) + 2px),
      var(--liquid-ring-b) calc(var(--ripple-b, 0px) + 4px),
      transparent calc(var(--ripple-b, 0px) + 7px),
      transparent calc(var(--ripple-b, 0px) + var(--liquid-ring-period-b))),
    radial-gradient(200px circle at var(--pointer-x, 50%) var(--pointer-y, 50%),
      var(--glow, var(--svc-glow)), transparent 68%);
  -webkit-mask: radial-gradient(var(--liquid-ripple-reach) circle at var(--pointer-x, 50%) var(--pointer-y, 50%),
                  rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.42) 38%, rgba(0, 0, 0, 0) 66%);
          mask: radial-gradient(var(--liquid-ripple-reach) circle at var(--pointer-x, 50%) var(--pointer-y, 50%),
                  rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.42) 38%, rgba(0, 0, 0, 0) 66%);
  opacity: 0;
  transition: opacity var(--transition);
  pointer-events: none;
}

.step:hover::after,
.stat:hover::after,
.glass-card:hover::after,
.service-card:hover::after {
  opacity: 0.45;
  /* Only while hovered: one card at a time, and nothing animates a gradient
     on a card nobody is pointing at. The two durations are not multiples of
     each other, so the crest sets never resynchronise into one clean pulse. */
  animation: liquid-ripple-a 3.4s linear infinite,
             liquid-ripple-b 5.1s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
  .step:hover::after,
  .stat:hover::after,
  .glass-card:hover::after,
  .service-card:hover::after {
    animation: none;
  }
}

/* ── Liquid glass: the WebGL surface ────────────────────────────────────────
   assets/js/liquid/ moves one shared canvas into whichever card is hovered
   and solves an actual wave field across it. Everything above stays exactly
   as it is: it is the surface this replaces only while the canvas is present,
   and the whole page it falls back to if WebGL, a fine pointer or motion is
   unavailable. .has-liquid-gl is set by that module and by nothing else. */
/* Resting gloss. Declared here rather than beside the .glass base rule
   because that rule uses the `background` shorthand, which would reset these
   layers; pages.css re-applies the same token to its own two cards for the
   same reason. .hero-shell and .showcase are excluded deliberately — a
   lacquer sheen across a 1360px panel reads as a gradient wash, not gloss. */
.step,
.stat {
  background-image: var(--liquid-gloss);
}

.liquid-canvas {
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: inherit;
  display: block;
  /* inset:0 is NOT enough on its own here, and the comment that used to say so
     cost us the "cards grow on hover" bug. A canvas is a replaced element with
     an intrinsic size — its width/height attributes, which the renderer sets in
     DEVICE pixels (css * dpr). For an absolutely positioned replaced box,
     width/height:auto resolve to that intrinsic size first, and the
     over-constrained left/right pair is then resolved by ignoring `right`
     rather than by stretching. So on any display with dpr > 1 the canvas
     painted css*dpr wide and tall — 1.25x the card at 125% Windows scaling —
     hanging off the card's right and bottom edges. Since this canvas paints the
     card's whole liquid-glass surface (rounded corners, rim and all), the
     result read exactly like the card itself inflating on hover and overlapping
     its neighbours.

     100%/100% pins both axes to the containing block, which for an abspos child
     is the card's padding box — a definite size in both axes by the time the
     canvas mounts, so the old percentage-height worry does not apply here.
     Percentages rather than inline px from the renderer on purpose: the
     renderer's cssW/cssH are the card's offset box, a border-width larger than
     the padding box this canvas is positioned inside. */
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* The gradient-ring ripple is the stand-in for this canvas, not a companion
   to it — two ripples on one card disagree about where the crests are. */
.has-liquid-gl::after {
  display: none;
}

/* ── Section shell ──────────────────────────────────────────────────────── */

.section {
  max-width: 1200px;
  margin: 0 auto;
  padding: var(--space-10) var(--space-5);
}

@media (max-width: 767px) {
  .section {
    padding-top: var(--space-9);
    padding-bottom: var(--space-9);
  }
}

/* ── Chips ──────────────────────────────────────────────────────────────── */

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-pill);
  font-size: 14px;
  font-weight: 500;
  line-height: 1.4;
  /* Softened ink rather than element opacity, which would also wash out the
     pastel fill behind it. */
  color: var(--text);
}

/* The usual two-declaration fallback doesn't work for color-mix here: the value
   contains var(), so it parses as valid regardless of support, wins the cascade,
   and then fails at computed-value time — leaving `color` inherited rather than
   falling back. @supports is the only form that actually degrades. */
@supports (color: color-mix(in srgb, red 50%, transparent)) {
  .chip { color: color-mix(in srgb, var(--text) 72%, transparent); }
}

.chip--green  { background: var(--chip-green); }
.chip--pink   { background: var(--chip-pink); }
.chip--purple { background: var(--chip-purple); }
.chip--blue   { background: var(--chip-blue); }

/* ── Motion preference ──────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  .btn {
    transition: none;
  }

  .btn--primary:hover {
    transform: none;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   SECTIONS — the landing page. Everything below composes the primitives above;
   no new colors, no raw hex, no spacing outside the --space-* scale.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Shared: focus, links ───────────────────────────────────────────────── */

a:focus-visible,
button:focus-visible {
  outline: 2px solid var(--text);
  outline-offset: 3px;
}

/* Centered display type strands single words on the last line at narrow widths.
   Ignored where unsupported, so it is pure upside. */
.hero h1,
.hero .lede,
.process h2,
.proof h2,
.about p,
.cta h2 {
  text-wrap: balance;
}

/* Arrow links. One shape for the tab panels and the about teaser so the page
   only ever teaches the reader a single "go deeper" affordance. */
.panel-link,
.text-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  /* was var(--blue) — 3.49:1 on white, below AA. --svc-ink is the accessible
     form and inside the showcase it also tracks the active service. */
  color: var(--svc-ink);
  font-size: 15px;
  font-weight: 500;
  text-decoration: none;
}

.panel-link svg,
.text-link svg {
  transition: transform var(--transition);
}

.panel-link:hover,
.text-link:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}

.panel-link:hover svg,
.text-link:hover svg {
  transform: translateX(3px);
}

/* ── Nav ────────────────────────────────────────────────────────────────── */

.navbar {
  position: fixed;
  top: 0;
  right: 0;
  left: 0;
  z-index: 100;
  border-bottom: 1px solid var(--glass-border);
  /* Opaque fallback first; translucency only where backdrop-filter parses, so
     the bar is never unreadable. */
  background: var(--surface);
}

@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .navbar {
    background: var(--glass-bg-nav);
    -webkit-backdrop-filter: blur(var(--glass-blur-nav)) saturate(140%);
            backdrop-filter: blur(var(--glass-blur-nav)) saturate(140%);
  }

  /* Scroll state (class toggled by index.js): the blur and saturation both
     step up, so the bar reads as denser glass over the moving aurora. */
  .navbar.is-scrolled {
    -webkit-backdrop-filter: blur(var(--glass-blur-nav-lg)) saturate(var(--glass-saturate-nav));
            backdrop-filter: blur(var(--glass-blur-nav-lg)) saturate(var(--glass-saturate-nav));
  }
}

.navbar.is-scrolled {
  box-shadow: var(--shadow-sm);
  transition: box-shadow var(--transition), backdrop-filter var(--transition);
}

.navbar .container {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  max-width: 1200px;
  margin: 0 auto;
  padding: var(--space-4) var(--space-5);
  transition: padding var(--transition);
}

.navbar.is-scrolled .container {
  padding-top: var(--space-3);
  padding-bottom: var(--space-3);
}

.navbar .logo {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  transition: transform var(--transition), opacity var(--transition);
}

.navbar .logo:hover {
  transform: scale(1.03);
  opacity: 0.9;
}

.navbar .logo img {
  width: auto;
  height: 52px;
}

.nav-links {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  margin: 0;
  padding: 0;
  list-style: none;
}

.nav-links > li {
  flex: 0 0 auto;
}

/* Plain link (not the CTA button): underline sweeps in from the centre on
   hover and stays put — full width — on the current page, so location in the
   site is visible at rest, not only on interaction (nav-state-active). */
.nav-links > li > a:not(.btn-primary) {
  position: relative;
  padding-bottom: 2px;
}

.nav-links > li > a:not(.btn-primary)::after {
  content: '';
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  height: 1.5px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: center;
  transition: transform var(--transition);
}

.nav-links > li > a:not(.btn-primary):hover::after {
  transform: scaleX(1);
}

.nav-links > li > a.is-active {
  color: var(--text);
}

.nav-links > li > a.is-active::after {
  transform: scaleX(1);
}

.nav-links a {
  color: var(--text-dim);
  font-size: 15px;
  font-weight: 500;
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--transition);
}

.nav-links a:hover {
  color: var(--text);
}

@media (prefers-reduced-motion: reduce) {
  .navbar .logo,
  .nav-links > li > a:not(.btn-primary)::after {
    transition: none;
  }
}

/* The shared nav markup uses the dark stylesheet's `.btn-primary` (single dash),
   not this system's `.btn--primary`. Style it here rather than edit the markup,
   which the other eight pages also render. */
.nav-links .btn-primary {
  min-height: 40px;
  padding: var(--space-3) var(--space-5);
  background: var(--ink);
  color: var(--on-ink);
}

.nav-links .btn-primary:hover {
  color: var(--on-ink);
  transform: translateY(-1px);
  box-shadow: var(--shadow-lift);
}

/* Services dropdown. Opacity + pointer-events rather than visibility/display so
   the links stay in the tab order — tabbing into one fires :focus-within, which
   reveals the menu. visibility:hidden would make :focus-within unreachable. */
.nav-dropdown {
  position: relative;
}

/* The 15px offset below is margin, which is dead space: moving the cursor
   from the parent link down toward the menu crossed unhoverable ground, the
   :hover on .nav-dropdown dropped, and the menu vanished mid-approach. The
   ::before bridge below covers exactly that gap and belongs to the menu, so
   the pointer never leaves the dropdown while travelling to it. */
.nav-dropdown-menu {
  position: absolute;
  top: 100%;
  left: 50%;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 250px;
  margin-top: var(--space-4);
  padding: var(--space-3);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-sm);
  background: var(--glass-bg-solid);
  box-shadow: var(--shadow-card);
  opacity: 0;
  pointer-events: none;
  transform: translateX(-50%) translateY(-4px);
  transition: opacity var(--transition), transform var(--transition);
}

/* Invisible hover bridge across the margin gap. Inherits the menu's own
   pointer-events, so it is inert while the menu is closed. */
.nav-dropdown-menu::before {
  content: '';
  position: absolute;
  right: 0;
  bottom: 100%;
  left: 0;
  height: var(--space-4);
}

.nav-dropdown-menu a {
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-sm);
  transition: background var(--transition), color var(--transition);
}

.nav-dropdown-menu a:hover {
  background: var(--surface-sunken);
}

.nav-dropdown:hover .nav-dropdown-menu,
.nav-dropdown:focus-within .nav-dropdown-menu {
  opacity: 1;
  pointer-events: auto;
  transform: translateX(-50%) translateY(0);
}

/* Desktop: no hamburger. Below 900px it becomes the nav control — see the
   media query further down. The toggle lives in assets/js/nav.js, which every
   page loads. */
.navbar .hamburger {
  display: none;
  width: 44px;
  height: 44px;
  align-items: center;
  justify-content: center;
  gap: 5px;
  flex-direction: column;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  cursor: pointer;
}

.navbar .hamburger span {
  display: block;
  width: 18px;
  height: 2px;
  border-radius: 2px;
  background: var(--text);
  transition: transform var(--transition), opacity var(--transition);
}

/* Three bars fold into a cross. */
.navbar .hamburger.is-open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.navbar .hamburger.is-open span:nth-child(2) { opacity: 0; }
.navbar .hamburger.is-open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

@media (prefers-reduced-motion: reduce) {
  .navbar .hamburger span { transition: none; }
}

/* ── §1 Hero ────────────────────────────────────────────────────────────── */

.hero {
  /* The shell card below is wider than the shared .section column, so this
     section opts out of the 1200px cap and becomes the grey gutter instead. */
  max-width: none;
  /* Clears the fixed nav (~72px) and still leaves the badge real air above it. */
  padding-top: calc(var(--space-10) + var(--space-6));
  padding-bottom: var(--space-9);
  text-align: center;
}

.hero .pill {
  margin-bottom: var(--space-5);
}

.hero h1 {
  margin-bottom: var(--space-5);
}

.hero .lede {
  margin: 0 auto var(--space-6);
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-4);
  margin-bottom: var(--space-9);
}

/* The reference's single giant card: everything above the app window plus the
   window itself lives inside one white shell, inset from the viewport edge by
   the grey .hero gutter around it. */
.hero-shell {
  position: relative;
  max-width: 1360px;
  margin: 0 auto;
  padding: var(--space-8) var(--space-7);
  border-radius: var(--radius-shell);
}

/* Cursor glow trail: assets/js/studio/hero-glow.js writes --glow-x/--glow-y
   (lagged, not 1:1 with the pointer — that lag is the "trail") and toggles
   --glow-opacity on enter/leave. Full tier only; the opacity default of 0
   makes this an exact no-op everywhere else, same contract as tilt.js. Direct
   children get z-index: 1 so the blob — and .glass's own ::before rim, which
   this sits above in paint order — never show through the content. */
.hero-shell > * {
  position: relative;
  z-index: 1;
}

.hero-shell::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: inherit;
  background: radial-gradient(480px circle at var(--glow-x, 50%) var(--glow-y, 50%),
                               var(--svc-build-glow, rgba(47, 143, 214, 0.28)),
                               transparent 70%);
  opacity: var(--glow-opacity, 0);
  transition: opacity 0.4s ease;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .hero-shell::after {
    display: none;
  }
}

/* ── §2 Showcase ────────────────────────────────────────────────────────── */

.showcase {
  position: relative;
  padding: var(--space-5);
  text-align: left;
}

.showcase-tabs {
  display: flex;
  gap: var(--space-2);
  margin-bottom: var(--space-5);
  padding: var(--space-2);
  border-radius: var(--radius-pill);
  /* No visible track behind the row — the reference's tab row is plain; only
     the active tab gets a filled pill (below). */
  /* Tabs never shrink or wrap: when the five labels outgrow the track it scrolls
     inside its own box, so the page never gains a horizontal scrollbar. */
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.showcase-tabs::-webkit-scrollbar {
  display: none;
}

.tab {
  position: relative;
  display: inline-flex;
  flex: 1 0 auto;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  min-height: 44px;
  padding: var(--space-3) var(--space-4);
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--text-dim);
  font-family: inherit;
  font-size: 14px;
  font-weight: 500;
  line-height: 1.2;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--transition), color var(--transition),
              transform var(--transition), box-shadow var(--transition);
}

.tab-icon {
  flex: 0 0 auto;
}

.tab:hover {
  background: var(--surface-sunken);
  color: var(--text);
}

/* Fallback fill for a tab that somehow lacks data-service — the per-service
   rules below are more specific and win whenever data-service is present. */
.tab[aria-selected="true"],
.tab[aria-selected="true"]:hover {
  background: var(--blue-soft);
  color: var(--blue);
}

.tab:focus-visible {
  outline: 2px solid var(--text);
  outline-offset: -2px;
}

/* Each tab carries its own hue from a static attribute, so the active colour
   is correct with JavaScript disabled and needs no JS at all. */
.tab[data-service="build"]        { --glow: var(--svc-build-glow); }
.tab[data-service="intelligence"] { --glow: var(--svc-intelligence-glow); }
.tab[data-service="content"]      { --glow: var(--svc-content-glow); }
.tab[data-service="growth"]       { --glow: var(--svc-growth-glow); }
.tab[data-service="foundation"]   { --glow: var(--svc-foundation-glow); }

.tab[data-service="build"][aria-selected="true"],
.tab[data-service="build"][aria-selected="true"]:hover {
  background: var(--svc-build-soft);
  color: var(--svc-build-ink);
}

.tab[data-service="intelligence"][aria-selected="true"],
.tab[data-service="intelligence"][aria-selected="true"]:hover {
  background: var(--svc-intelligence-soft);
  color: var(--svc-intelligence-ink);
}

.tab[data-service="content"][aria-selected="true"],
.tab[data-service="content"][aria-selected="true"]:hover {
  background: var(--svc-content-soft);
  color: var(--svc-content-ink);
}

.tab[data-service="growth"][aria-selected="true"],
.tab[data-service="growth"][aria-selected="true"]:hover {
  background: var(--svc-growth-soft);
  color: var(--svc-growth-ink);
}

.tab[data-service="foundation"][aria-selected="true"],
.tab[data-service="foundation"][aria-selected="true"]:hover {
  background: var(--svc-foundation-soft);
  color: var(--svc-foundation-ink);
}

/* The showcase rebinds the whole current-service channel, so everything inside
   it — glow, rim tint, panel link — follows the active service. */
.showcase[data-service="build"] {
  --svc: var(--svc-build); --svc-ink: var(--svc-build-ink);
  --svc-soft: var(--svc-build-soft); --svc-glow: var(--svc-build-glow);
}
.showcase[data-service="intelligence"] {
  --svc: var(--svc-intelligence); --svc-ink: var(--svc-intelligence-ink);
  --svc-soft: var(--svc-intelligence-soft); --svc-glow: var(--svc-intelligence-glow);
}
.showcase[data-service="content"] {
  --svc: var(--svc-content); --svc-ink: var(--svc-content-ink);
  --svc-soft: var(--svc-content-soft); --svc-glow: var(--svc-content-glow);
}
.showcase[data-service="growth"] {
  --svc: var(--svc-growth); --svc-ink: var(--svc-growth-ink);
  --svc-soft: var(--svc-growth-soft); --svc-glow: var(--svc-growth-glow);
}
.showcase[data-service="foundation"] {
  --svc: var(--svc-foundation); --svc-ink: var(--svc-foundation-ink);
  --svc-soft: var(--svc-foundation-soft); --svc-glow: var(--svc-foundation-glow);
}

/* Ambient glow in the active service's colour, cross-fading on switch. */
.showcase {
  box-shadow: var(--glass-shadow),
              0 0 0 1px var(--svc-soft),
              0 24px 72px -28px var(--svc-glow);
  transition: box-shadow var(--transition-slow);
}

.showcase-window {
  position: relative;
  display: flex;
  flex-direction: column;
  /* Reserves the stage Phase B mounts a 3D object into, so nothing shifts when
     it arrives. */
  min-height: 420px;
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-window);
  background: var(--glass-bg-solid);
  /* Clips the toolbar's flat top and the stage's flat bottom to the window's
     own rounded corners, and clips the JS-mounted canvas (absolute, inset:0)
     to the same shape. */
  overflow: hidden;
}

/* App-window chrome above the panels: three muted "traffic light" dots plus
   the active service name on the left, an empty slot on the right for the
   render-mode control a later task mounts. Positioned + z-index'd above the
   canvas (which the JS in index.js appends as the last child of
   .showcase-window, absolutely positioned, inset:0, z-index:0) with its own
   opaque background, so the 3D layer always renders beneath this bar rather
   than under it. */
.window-toolbar {
  position: relative;
  z-index: 2;
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  gap: var(--space-4);
  height: 48px;
  padding: 0 var(--space-5);
  border-bottom: 1px solid var(--glass-border);
  background: var(--glass-bg-solid);
}

.window-dots {
  display: flex;
  flex: 0 0 auto;
  gap: var(--space-2);
}

.window-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--line-strong);
}

/* Allowed to shrink and truncate: with the mode control also occupying the
   toolbar (up to ~3 buttons wide) the two together can overflow it on
   narrow cards, which would cause a horizontal scrollbar — the one thing
   this whole system is not allowed to do. */
.window-title {
  overflow: hidden;
  flex: 1 1 auto;
  min-width: 0;
  color: var(--text-dim);
  font-size: 13px;
  font-weight: 500;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* JS mounts the Studio/Wireframe/X-Ray segmented control into this slot
   (index.js, full/lite tiers only) — empty, and stays empty, without JS. */
.window-controls {
  flex: 0 0 auto;
  margin-left: auto;
}

/* Render-mode segmented control. Never present in the no-JS DOM — built and
   injected by index.js. Same pill-track-with-white-chip language as the rest
   of the page's controls, just sunken instead of transparent. */
.mode-control {
  display: flex;
  flex: 0 0 auto;
  gap: 2px;
  padding: 3px;
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
}

.mode-btn {
  border: 0;
  border-radius: var(--radius-pill);
  padding: var(--space-2) var(--space-4);
  background: transparent;
  color: var(--text-dim);
  font-family: inherit;
  font-size: 12px;
  font-weight: 500;
  line-height: 1.4;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--transition), color var(--transition), box-shadow var(--transition);
}

.mode-btn:hover {
  color: var(--text);
}

.mode-btn.is-active {
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-sm);
}

@media (prefers-reduced-motion: reduce) {
  .mode-btn {
    transition: none;
  }
}

/* The panel area proper: a faint dot grid on a barely-off-white surface reads
   as an app canvas rather than blank space, behind both the panel copy and
   wherever the 3D object doesn't cover it. */
.window-stage {
  position: relative;
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  padding: var(--space-6);
  background-color: var(--surface-stage);
  background-image: radial-gradient(var(--line) 1px, transparent 1px);
  background-size: 24px 24px;
}

.panel {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: var(--space-4);
}

/* Without JS every panel renders, stacked — a hairline keeps them legible as
   five separate things. Once tabs.js marks the document, the rule retires. */
html:not(.js-tabs) .panel + .panel {
  margin-top: var(--space-6);
  padding-top: var(--space-6);
  border-top: 1px solid var(--line);
}

.panel h2 {
  font-size: clamp(24px, 3vw, 34px);
}

.panel p {
  max-width: 52ch;
  color: var(--text-dim);
}

.panel-link {
  align-self: flex-start;
}

/* Empty in this phase: it simply absorbs the window's spare height. */
.panel-stage {
  flex: 1 1 auto;
}

/* ── Floating decorative chips ──────────────────────────────────────────── */

/* Reuses .chip's pastel-fill + text treatment (already the page's language for
   a small labeled pill) and layers on absolute positioning, a shadow so it
   reads as a little floating UI card, and a gentle drift. Purely decorative:
   aria-hidden, inert to pointer events, gone under 900px or reduced motion. */
.float-chip {
  position: absolute;
  z-index: 5;
  box-shadow: var(--shadow-sm);
  font-size: 13px;
  white-space: nowrap;
  pointer-events: none;
  animation: chip-float 6s ease-in-out infinite alternate;
}

.float-chip--a {
  top: -16px;
  left: 28px;
  animation-delay: 0s;
}

.float-chip--b {
  top: -16px;
  right: 28px;
  animation-delay: 2s;
}

.float-chip--c {
  top: 42%;
  right: -24px;
  animation-delay: 4s;
}

@keyframes chip-float {
  from { transform: translateY(0) rotate(-1deg); }
  to   { transform: translateY(-8px) rotate(1.5deg); }
}

@media (max-width: 900px) {
  .float-chip {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .float-chip {
    display: none;
  }
}

/* ── §2.5 Marquee ───────────────────────────────────────────────────────────
   Borrowed from the kinetic-typography reference: a constantly-moving strip.
   Kept in-system by staying full-bleed-but-quiet — surface-sunken bg, hairline
   borders, one hue per service pulled from the existing --svc-* tokens, no
   gradient fade at the edges (that part of the reference is right: a hard cut
   reads cleaner than a fade here). Track is duplicated once in the markup and
   the keyframe runs it exactly -50%, so the loop seam is invisible. */

.marquee {
  overflow: hidden;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  background: var(--surface-sunken);
}

.marquee-track {
  display: flex;
  width: max-content;
  gap: var(--space-8);
  padding: var(--space-4) 0;
  animation: marquee-scroll 32s linear infinite;
}

.marquee-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  white-space: nowrap;
  color: var(--text-dim);
  font-size: 15px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.marquee-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--dot, var(--svc-build));
  flex-shrink: 0;
}

@keyframes marquee-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

@media (prefers-reduced-motion: reduce) {
  .marquee-track {
    animation: none;
  }
}

/* ── §3–§5 shared rhythm ────────────────────────────────────────────────── */

/* .section's default --space-10 block padding would put 256px between every
   pair of sections and only 128px before the CTA. Flattening the three middle
   sections to --space-9 gives one even 176px beat all the way down the page. */
.process,
.proof,
.about {
  padding-top: var(--space-9);
  padding-bottom: var(--space-9);
}

/* ── §3 Process ─────────────────────────────────────────────────────────── */

.process h2 {
  margin-bottom: var(--space-8);
  text-align: center;
}

.process-grid {
  position: relative;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--space-4);
}

/* Connector line behind the four step numbers. Sits at the step number's
   vertical centre (card padding-top 40px + half its 40px line-height); the
   opaque card background in front of it means it only reads in the gaps
   between cards, like a line threading the numbers together. Drawn 0→1 by a
   one-shot ScrollTrigger tween in motion.js, not a continuous scrub, so
   scrolling back up never un-draws it. Only makes sense as a single row, so
   it's hidden once the grid wraps to 2/1 columns below. */
.process-line {
  position: absolute;
  z-index: 0;
  top: 60px;
  right: var(--space-5);
  left: var(--space-5);
  height: 2px;
  background: var(--line);
  transform: scaleX(0);
  transform-origin: left center;
}

.step {
  position: relative;
  z-index: 1;
  padding: var(--space-6) var(--space-5);
  border-radius: var(--radius-window);
}

.step-num {
  display: block;
  margin-bottom: var(--space-6);
  color: var(--text-dim);
  font-size: 40px;
  font-weight: 700;
  letter-spacing: -0.04em;
  line-height: 1;
}

.step h3 {
  margin-bottom: var(--space-3);
  font-size: 19px;
}

.step p {
  font-size: 15px;
}

/* ── §4 Proof ───────────────────────────────────────────────────────────── */

.proof h2 {
  margin-bottom: var(--space-8);
  text-align: center;
}

.proof-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--space-4);
}

/* Deliberately unboxed: §3 already spends four cards, and a second row of them
   would read as clutter. The numbers sit straight on the canvas. */
.stat {
  display: flex;
  flex-direction: column;
  align-items: center;
  /* space-between pins the chips to the bottom of the grid row, so the labels
     still line up when the two placeholder values wrap to different heights. */
  justify-content: space-between;
  padding: var(--space-5);
  border-radius: var(--radius-window);
  text-align: center;
}

.stat .chip {
  margin-top: var(--space-4);
}

.stat-value {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: clamp(48px, 9vw, 108px);
  max-width: 100%;
  color: var(--text);
  font-size: clamp(48px, 9vw, 108px);
  font-weight: 800;
  letter-spacing: -0.05em;
  line-height: 1;
}

/* Unfilled client data. Sized to wrap inside its cell and drawn as an obviously
   empty slot, so it cannot be mistaken for a shipped figure. */
.stat-value--input {
  padding: var(--space-3) var(--space-4);
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0;
  line-height: 1.35;
  overflow-wrap: anywhere;
}

/* ── §5 About ───────────────────────────────────────────────────────────── */

.about {
  text-align: center;
}

.about-inner {
  max-width: 54ch;
  margin: 0 auto;
}

.about p {
  margin-bottom: var(--space-5);
  color: var(--text);
  font-size: clamp(19px, 2vw, 24px);
  letter-spacing: -0.015em;
  line-height: 1.45;
}

/* ── §6 CTA ─────────────────────────────────────────────────────────────── */

.cta {
  max-width: 1200px;
  margin: 0 auto;
  padding: var(--space-9) var(--space-5) var(--space-10);
}

/* The page's only dark block. */
.cta-inner {
  padding: var(--space-10) var(--space-5);
  border-radius: var(--radius-card);
  background: var(--ink);
  box-shadow: var(--shadow-ink);
  text-align: center;
}

.cta h2 {
  margin-bottom: var(--space-6);
  color: var(--on-ink);
}

.cta .btn--ghost {
  border-color: transparent;
}

/* The shared focus ring is --text, which vanishes against --ink. */
.cta .btn:focus-visible {
  outline-color: var(--on-ink);
}

/* ── Footer ─────────────────────────────────────────────────────────────── */

.footer {
  border-top: 1px solid var(--glass-border);
  background: var(--glass-bg-solid);
  color: var(--text-dim);
}

.footer .container {
  max-width: 1200px;
  margin: 0 auto;
  padding: var(--space-9) var(--space-5) var(--space-6);
}

.footer-grid {
  display: grid;
  grid-template-columns: 1.5fr 1fr 1fr 1fr;
  gap: var(--space-6);
}

.footer-logo img {
  width: auto;
  height: 30px;
}

.footer-tagline {
  margin-top: var(--space-4);
  font-size: 15px;
}

.footer h4 {
  margin-bottom: var(--space-4);
  color: var(--text);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.01em;
}

.footer ul {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.footer li,
.footer a {
  font-size: 15px;
}

.footer a {
  color: var(--text-dim);
  text-decoration: none;
  transition: color var(--transition);
}

.footer a:hover {
  color: var(--text);
}

.social-icons {
  display: flex;
  gap: var(--space-3);
  margin-top: var(--space-5);
}

.social-icons a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  font-size: 13px;
  font-weight: 600;
  transition: border-color var(--transition), color var(--transition);
}

.social-icons a:hover {
  border-color: var(--line-strong);
}

.footer-bottom {
  margin-top: var(--space-8);
  padding-top: var(--space-5);
  border-top: 1px solid var(--line);
  font-size: 14px;
}

/* ── Responsive ─────────────────────────────────────────────────────────── */

/* Measured natural widths of the five-tab row against the track:
     >=1200  971 / 1086  — icons, 14px, full padding
     1199    901 /  ~985 — padding trimmed
     1099    732 /  ~985 — icons dropped, 13px
      ~830   732 /  ~716 — row stops fitting; the track scrolls from here down.
   Without these two steps the row clips by 9px at 1024 and by 114px at 901. */
@media (max-width: 1199px) {
  .tab {
    padding-right: var(--space-3);
    padding-left: var(--space-3);
  }
}

@media (max-width: 1099px) {
  .tab-icon {
    display: none;
  }

  .tab {
    font-size: 13px;
  }
}

/* ── §2 Service journey: the rail stage ─────────────────────────────────────
   One continuous black stage, not five stacked boxes. The light aurora page
   stays visible down both gutters, so the dark reads as a stage inside the
   page rather than a second page — and, structurally, the rail never has to
   cross a light gap, where its glow would have nothing to bloom against. */

.rail-stage {
  position: relative;
  margin-inline: clamp(16px, 4vw, 64px);
  border-radius: var(--radius-window);
  background: #090a0d;
  box-shadow: var(--liquid-bevel), 0 30px 80px -40px rgba(0, 0, 0, 0.6);
  /* `clip`, NOT `hidden`. Both keep the canvas and the ghost numerals inside
     the rounded corners, but `overflow: hidden` makes this element a scroll
     container, and a scroll container is exactly what kills `position: sticky`
     in its descendants — every beat's pinned composition silently scrolled
     away and the stage rendered as five empty black screens. `overflow: clip`
     clips without creating a scrollport, so the beats stay stuck. */
  overflow: clip;
  /* Dark-surface token overrides, same set .step/.stat carry — every nested
     card, chip and rule inside a beat inherits the dark forms. */
  --text: #f4f6f8;
  --text-dim: rgba(244, 246, 248, 0.62);
  --line: rgba(255, 255, 255, 0.10);
  --line-strong: rgba(255, 255, 255, 0.18);
  --surface: #14161a;
  color: var(--text);
}

/* Sticky, full-viewport, and pulled back out of the flow with the negative
   margin so it costs no layout height — the beats below it are what give the
   stage its length. */
.rail-viewport {
  position: sticky;
  top: 0;
  z-index: 0;
  /* Sticky is a positioned box, so it is already the containing block for the
     two absolutely positioned canvases inside it. */
  height: 100vh;
  margin-bottom: -100vh;
  pointer-events: none;
  isolation: isolate;
}

/* Two stacked transparent surfaces. They cannot share one renderer: the tube
   component ships its own three.js build and a node-based post-processing
   pass, so mixing our meshes into its scene is not safe. One extra context
   buys both systems their native rendering model. */
.rail-tubes-canvas,
.rail-canvas {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.rail-tubes-canvas { z-index: 0; }
.rail-canvas { z-index: 1; }

/* Each beat is two-and-a-bit screens of scroll wrapped around ONE screen of
   composition. The tall .beat gives the rail somewhere to travel; the sticky
   .beat-inner holds the copy still while it does.

   The first version made each beat exactly 100vh, and it was the wrong shape:
   a service went by in a single flick of the wheel, two of them shared the
   viewport at every rest position, and nothing ever held long enough to be
   looked at. If a page is going to be this tall it has to buy dwell time with
   the height, not just spend it. */
.beat {
  position: relative;
  z-index: 1;
  min-height: 230vh;
  /* Deliberately no top border between beats. The dividing line belonged to a
     stack of cards; this is one continuous room and the rail is what marks
     the transitions. */
}

/* One viewport of composition, pinned for the beat's whole length. Native
   sticky, not a ScrollTrigger pin: no pin-spacer injected into the layout, no
   containing-block trap from the stage's own filters, and it survives the JS
   being dead — which is the whole tier-static contract. */
.beat-inner {
  position: sticky;
  top: 0;
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  grid-template-rows: repeat(6, 1fr);
  align-items: center;
  height: 100vh;
  padding: clamp(28px, 6vh, 72px) clamp(24px, 5vw, 104px);
  /* The stage sits on a page whose hero centres its text. Centred body copy at
     this measure is a poster, not something anyone reads. */
  text-align: left;
}

/* ── Beat composition archetypes ────────────────────────────────────────────
   Five beats laid out five different ways, on one 12x6 grid.

   Alternating copy-left/copy-right was the obvious move and it was also the
   boring one: by the third screen the visitor has learned the template and
   stops looking. These vary the copy's column span, its row, its measure and
   its relationship to the object — near/far, above/below, in front/behind —
   so the same information lands differently every time while still coming
   from one system rather than five hand-placed layouts. */

/* The rhythm is left, right, left, right, centre — the copy strictly
   alternates sides so the rail has a real zig-zag to run, and the fifth beat
   breaks the pattern by arriving in the middle. Two consecutive beats on the
   same side (an earlier draft put 03 and 04 both left) reads as a mistake
   rather than as variation, however different the two grids are.

   Alternation is the skeleton; what changes underneath it is the MASS — how
   much width the copy takes, how high it sits, and whether the object is a
   companion beside it, a backdrop behind it, or a floor under it. */

/* Copy alternates left, right, left, right, left, and every beat sits the same
   distance in from its own edge: left-hand copy spans columns 1-6, right-hand
   copy spans 7-12, on a 12-column grid with symmetric padding. Ragged insets
   were the thing making the sections look hand-placed rather than composed.

   What still varies is MASS, not margin: which rows the copy occupies, how
   tall it stands, and whether the object is a companion beside it, a backdrop
   behind it, or a low horizon under it. */

/* 01 — copy low-left, object tall on the right. The establishing shot. */
.beat[data-layout='anchor-right'] .beat-copy { grid-column: 1 / 7; }
.beat[data-layout='anchor-right'] .beat-anchor { grid-column: 7 / 13; grid-row: 1 / 7; }

/* 02 — copy high-right, object dropped low-left. Diagonal opposition. */
.beat[data-layout='anchor-left-low'] .beat-copy { grid-column: 7 / 13; }
.beat[data-layout='anchor-left-low'] .beat-anchor { grid-column: 1 / 7; grid-row: 3 / 7; }

/* 03 — copy left and high, object huge behind and to the right of it. */
.beat[data-layout='anchor-behind'] .beat-copy {
  grid-column: 1 / 7;
  position: relative;
  z-index: 2;
}
.beat[data-layout='anchor-behind'] .beat-anchor { grid-column: 5 / 13; grid-row: 1 / 7; }

/* 04 — copy right and low, object high-left. Mirrors 02 without repeating it:
   there the copy was high, here it is the object. */
.beat[data-layout='anchor-right-high'] .beat-copy { grid-column: 7 / 13; }
.beat[data-layout='anchor-right-high'] .beat-anchor { grid-column: 1 / 7; grid-row: 1 / 5; }

/* 05 — copy left, object wide on the right and set low, so the finale reads as
   a horizon rather than a fifth card. Centring this one put it out of step
   with the other four and broke the left/right rhythm the rail runs on. */
.beat[data-layout='anchor-wide'] .beat-copy { grid-column: 1 / 7; }
.beat[data-layout='anchor-wide'] .beat-anchor { grid-column: 7 / 13; grid-row: 2 / 7; }

/* The finale still gets its own runway — the extra height is what makes the
   last arrival land instead of reading as one more list item. */
.beat[data-layout='anchor-wide'] { min-height: 310vh; }

/* The anchor is a measuring target, not a visible box: the 3D object is
   painted on the canvas behind and positioned from this element's rect. It
   still has to occupy real grid area, because that area IS the composition —
   the object is sized and placed from this rect and nothing else.

   align-self: stretch is load-bearing. The inner grid centres its items, and a
   centred empty div collapses to zero height: the anchor measured 590x0, the
   object was fitted to a height of nothing, and every beat rendered with no
   model at all. Stretching makes it fill its row span, which is what the
   archetype grids were describing in the first place. */
.beat-anchor {
  align-self: stretch;
  min-height: 0;
}

/* The beat number at display scale, sunk into the black. Gives every beat a
   second focal mass so a composition is never just "text and a gap", and
   gives the eye something that visibly changes between beats even before the
   3D lands. */
.beat-ghost {
  position: absolute;
  z-index: 0;
  top: clamp(-2vh, -1vw, 0px);
  font-size: clamp(180px, 34vw, 560px);
  font-weight: 800;
  line-height: 0.78;
  letter-spacing: -0.06em;
  color: transparent;
  -webkit-text-stroke: 1px rgba(255, 255, 255, 0.06);
  pointer-events: none;
  user-select: none;
}

.beat[data-layout='anchor-right'] .beat-ghost,
.beat[data-layout='anchor-right-high'] .beat-ghost { left: clamp(-8px, -1vw, 0px); }
.beat[data-layout='anchor-left-low'] .beat-ghost,
.beat[data-layout='anchor-behind'] .beat-ghost { right: clamp(-8px, -1vw, 0px); }
/* Ghost sits opposite its copy, so it weights the empty half rather than
   crowding the text. The finale's copy moved left with the rest of the
   left-hand beats, so its ghost moves right with them. */
.beat[data-layout='anchor-wide'] .beat-ghost { right: clamp(-8px, -1vw, 0px); }

.beat-num {
  display: block;
  margin-bottom: var(--space-4);
  color: var(--svc);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.14em;
}

.beat h2 {
  margin-bottom: var(--space-4);
  font-size: clamp(32px, 4vw, 56px);
  letter-spacing: -0.03em;
}

/* Above the ghost numeral, which is absolutely positioned at z-index 0.
   Every beat's copy spans the full grid height and centres itself in it, so a
   section that stops always stops with its text on the middle line of the
   screen. Giving each archetype its own row span put some compositions high in
   the frame — they read as stopping at the top edge rather than settling. Only
   the COLUMN varies now; the vertical anchor is constant. */
.beat-copy {
  position: relative;
  z-index: 1;
  grid-row: 1 / 7;
  align-self: center;
}

.beat-copy > p {
  max-width: 46ch;
  margin-bottom: var(--space-5);
  color: var(--text-dim);
  font-size: 18px;
}

.beat-points {
  margin: 0 0 var(--space-6);
  padding: 0;
  list-style: none;
}

.beat-points li {
  position: relative;
  padding-left: var(--space-5);
  margin-bottom: var(--space-3);
  color: var(--text-dim);
  font-size: 15px;
}

.beat-points li::before {
  content: '';
  position: absolute;
  top: 0.6em;
  left: 0;
  width: 8px;
  height: 2px;
  background: var(--svc);
}

.beat-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--svc);
  font-weight: 600;
  text-decoration: none;
}

.beat-link:hover { text-decoration: underline; }

/* Per-beat accent, from the same tokens the rail's colours come from. */
.beat[data-service='foundation'] { --svc: var(--svc-foundation); }
.beat[data-service='build'] { --svc: var(--svc-build); }
.beat[data-service='intelligence'] { --svc: var(--svc-intelligence); }
.beat[data-service='content'] { --svc: var(--svc-content); }
.beat[data-service='growth'] { --svc: var(--svc-growth); }

/* Progress spine. Sticky rather than fixed: it belongs to the stage, and a
   fixed child of a transformed ancestor resolves against the wrong box —
   the same trap documented on the ScrollTrigger pin in motion.js. */
.rail-progress {
  position: sticky;
  top: 50vh;
  z-index: 2;
  float: right;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin: 0 var(--space-5) -40vh 0;
  padding: 0;
  list-style: none;
}

.rail-progress {
  /* The spine sits over whatever the beat is rendering, including a fully lit
     3D object — at plain --text-dim it disappeared into the servers and the
     phone. A dark, blurred pill keeps it readable over anything without
     turning it into a solid panel. */
  padding: var(--space-4) var(--space-3);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 999px;
  background: rgba(9, 10, 13, 0.55);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}

.rail-progress a {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  color: var(--text-dim);
  font-size: 12px;
  text-decoration: none;
}

.rail-progress a::before {
  content: '';
  width: 8px;
  height: 8px;
  border: 1px solid var(--line-strong);
  border-radius: 50%;
  transition: background var(--transition), border-color var(--transition);
}

.rail-progress .is-current a::before {
  background: var(--svc, var(--svc-build));
  border-color: var(--svc, var(--svc-build));
}

/* Label text is for orientation, and colour alone must not be the signal —
   hidden only where there is genuinely no room. */
@media (max-width: 1100px) {
  .rail-progress a span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
}

@media (max-width: 900px) {
  /* Shorter dwell on a phone: the same five beats at 230vh each is a very long
     thumb journey, and the compositions collapse to one column anyway. */
  .beat { min-height: 165vh; }

  .beat-inner {
    grid-template-columns: 1fr;
    grid-template-rows: auto 1fr;
    padding: var(--space-8) var(--space-5);
  }

  /* Every archetype collapses to the same stack: copy first, object under it.
     The five-way variation is a wide-screen idea — on one column it would only
     read as inconsistent spacing. */
  .beat[data-layout] .beat-copy {
    grid-column: 1;
    grid-row: 1;
    text-align: left;
  }

  .beat[data-layout] .beat-anchor {
    grid-column: 1;
    grid-row: 2;
    min-height: 42vh;
  }

  .beat-ghost {
    font-size: clamp(140px, 46vw, 280px);
    top: auto;
    bottom: 4vh;
    left: auto;
    right: -4px;
    transform: none;
  }

  .rail-progress { display: none; }
  .rail-stage { margin-inline: 12px; }
}

/* No canvas at this tier — the beats are the section, and they must not leave
   a viewport of empty space where the object would have been. */
.tier-static .beat-anchor { min-height: 0; }
.tier-static .rail-viewport { display: none; }

@media (max-width: 900px) {
  .process-grid,
  .proof-grid {
    grid-template-columns: repeat(2, 1fr);
  }

  /* A single-row connector no longer makes sense once the grid wraps. */
  .process-line {
    display: none;
  }

  .footer-grid {
    grid-template-columns: repeat(2, 1fr);
    gap: var(--space-8) var(--space-6);
  }
}

@media (max-width: 767px) {
  .navbar .container {
    gap: var(--space-4);
    padding-right: var(--space-4);
    padding-left: var(--space-4);
  }

  .navbar .logo img {
    height: 38px;
  }

  /* Collapsible panel, replacing the horizontally scrolling strip this used to
     be. That approach clipped "Get in Touch" off the right edge and had to hide
     the Home link to fit; both are back. assets/js/nav.js drives .open. */
  .navbar .hamburger {
    display: flex;
  }

  .nav-links {
    position: absolute;
    top: 100%;
    right: var(--space-5);
    left: var(--space-5);
    z-index: 20;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-2);
    max-height: calc(100vh - 100%);
    padding: var(--space-4);
    overflow-y: auto;
    border: 1px solid var(--line);
    border-radius: var(--radius-window);
    background: var(--surface);
    box-shadow: var(--shadow-lift);
    /* Collapsed by default; visibility (not display) so the panel is removed
       from the tab order while closed but can still transition. */
    opacity: 0;
    visibility: hidden;
    transform: translateY(-8px);
    transition: opacity var(--transition), transform var(--transition),
                visibility var(--transition);
  }

  .nav-links.open {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
  }

  .nav-links > li {
    width: 100%;
  }

  .nav-links > li > a {
    width: 100%;
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-sm);
    font-size: 16px;
  }

  .nav-links .btn-primary {
    justify-content: center;
    margin-top: var(--space-2);
  }

  /* The Services submenu is collapsed inside the panel and opens on tap, since
     hover has no touch equivalent. nav.js toggles .open on the parent li. */
  .nav-dropdown-menu {
    position: static;
    display: none;
    min-width: 0;
    margin: var(--space-2) 0 0;
    padding: 0 0 0 var(--space-4);
    border: 0;
    background: none;
    box-shadow: none;
    opacity: 1;
    pointer-events: auto;
    transform: none;
  }

  /* Specificity matters here: the desktop rules
     `.nav-dropdown:hover  .nav-dropdown-menu` and `:focus-within` score
     (0,2,0) and re-apply translateX(-50%). Tapping the parent triggers
     :focus-within on touch, which shifted the submenu half its width to the
     left and clipped every label. These selectors match that specificity so
     the centring is genuinely cancelled. */
  .nav-dropdown .nav-dropdown-menu,
  .nav-dropdown:hover .nav-dropdown-menu,
  .nav-dropdown:focus-within .nav-dropdown-menu {
    left: auto;
    transform: none;
  }

  .nav-dropdown.open .nav-dropdown-menu {
    display: flex;
  }

  .nav-dropdown-menu::before { content: none; }

  .nav-dropdown-menu a {
    padding: var(--space-3) var(--space-4);
    font-size: 15px;
  }

  .hero {
    padding-top: calc(var(--space-9) + var(--space-7));
    padding-bottom: var(--space-8);
  }

  .hero-shell {
    padding: var(--space-6) var(--space-4);
    border-radius: 24px;
  }

  .hero-actions {
    margin-bottom: var(--space-8);
  }

  .hero-actions .btn {
    flex: 1 1 auto;
  }

  /* Same flattening as desktop, one step tighter: 112px between sections. */
  .process,
  .proof,
  .about {
    padding-top: var(--space-8);
    padding-bottom: var(--space-8);
  }

  .cta {
    padding-top: var(--space-8);
    padding-bottom: var(--space-9);
  }

  .showcase {
    padding: var(--space-4);
  }

  /* Was 320px — smaller than desktop's 420px, at exactly the width where the
     panel copy wraps to the most lines and needs the most room. .panel-stage
     is a flex spacer that absorbs whatever height is left after the text, so
     a short window guaranteed the object landed on top of the copy above it
     instead of below it. Confirmed by screenshot on the AI Agents tab, whose
     five-line paragraph left near-zero spacer height. */
  .showcase-window {
    min-height: 640px;
  }

  /* Belt-and-braces: guarantees clearance under the text even on the tab with
     the longest copy, rather than relying on window height alone. */
  .panel-stage {
    min-height: 260px;
  }

  /* The three chrome dots are purely decorative; dropping them on narrow
     cards reclaims room so the title (now ellipsis-truncating) and the
     3-button mode control don't force a horizontal scrollbar. */
  .window-toolbar {
    gap: var(--space-3);
    padding: 0 var(--space-4);
  }

  .window-dots {
    display: none;
  }

  /* At this width the title has already lost the dots' room and still can't
     coexist with the 3-button mode control without truncating to "App…" —
     unreadable and worse than just not being there. The mode control is the
     functional piece; the title is decorative chrome, so it loses first. */
  .window-title {
    display: none;
  }

  .mode-btn {
    padding: var(--space-2) var(--space-3);
    font-size: 11px;
  }

  .window-stage {
    padding: var(--space-5);
  }

  .process h2,
  .proof h2 {
    margin-bottom: var(--space-7);
  }

  .cta-inner {
    padding: var(--space-9) var(--space-5);
  }
}

@media (max-width: 600px) {
  .process-grid,
  .footer-grid {
    grid-template-columns: 1fr;
  }

  /* The stats stay 2-up — four stacked numbers would be a long, empty scroll —
     so the padding has to give instead, or the chip labels wrap to three lines. */
  .stat {
    padding: var(--space-4) var(--space-3);
  }

  .step-num {
    margin-bottom: var(--space-5);
  }
}

/* Blur budget. Behind .step/.stat there is only a smooth gradient, so dropping
   their backdrop-filter on narrow screens and on the touch/low-memory tier
   costs almost nothing visually and removes up to 8 blurred surfaces. */
@media (max-width: 899px) {
  .step,
  .stat {
    -webkit-backdrop-filter: none;
            backdrop-filter: none;
  }
}

@media (max-width: 767px) {
  .aurora-blob--3 { display: none; }

  .aurora-blob::before { filter: blur(var(--aurora-blur-sm)); }
}

html.tier-lite .aurora-blob--3 { display: none; }

html.tier-lite .step,
html.tier-lite .stat {
  -webkit-backdrop-filter: none;
          backdrop-filter: none;
}

@media (prefers-reduced-motion: reduce) {
  .nav-links .btn-primary:hover {
    transform: none;
  }

  .nav-dropdown-menu,
  .panel-link svg,
  .text-link svg,
  .tab,
  .navbar,
  .navbar.is-scrolled,
  .navbar .container {
    transition: none;
  }

  .panel-link:hover svg,
  .text-link:hover svg {
    transform: none;
  }

  /* Aurora keeps its colour and its veil — the page keeps its whole identity,
     it just stops moving. Blur is a static surface property, not motion, so
     the glass itself is deliberately left alone. */
  .aurora-blob,
  .aurora-blob::before {
    animation: none;
    will-change: auto;
  }

  /* Hover lift and glow stay — a state change on hover is an affordance, not
     an animation — but they arrive instantly. */
  .glass-hover,
  .glass-hover::after {
    transition: none;
  }
}

/* --- Tab mode: only active once tabs.js has booted. Without .js-tabs every
   panel stays visible and stacked, which is the no-JS baseline. --- */

.js-tabs .panel[hidden] { display: none; }

.js-tabs .panel {
  animation: panel-in var(--transition) both;
}

@keyframes panel-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .js-tabs .panel { animation: none; }
}

/* ══════════════════════════════════════════════════════════════════════════
   DARK GLASS SURFACES
   ══════════════════════════════════════════════════════════════════════════
   Every glass surface (hero shell, showcase, tabs, step cards, stat cards)
   renders near-black while the page, gutters and aurora stay light. Removing
   this one block restores the all-light page exactly.

   Implemented by rebinding surface + ink tokens on the three container roots
   — .hero-shell (which contains .showcase, its tabs, window and panels),
   .step and .stat — rather than by restyling each component. Descendants
   already read these tokens, so they inherit the dark context automatically.

   The --svc-*-ink rebinding is the important one. Those darkened forms exist
   *only* because the vivid hues score 2.1–3.5:1 on white (see the token
   block at the top of this file). On a near-black surface that reasoning
   inverts: the vivid values are the accessible ones and the darkened forms
   would be the failures, so inside these surfaces the -ink channel points
   back at the vivid hue. Measured on the composited card (~#1b1c1f):
   build 5.6:1, intelligence 4.9:1, content 5.5:1, growth 9.5:1,
   foundation 7.7:1 — all clearing AA. */

.hero-shell,
.step,
.stat {
  /* Alpha is high on purpose: at 0.78 the light aurora behind still washes
     through to a mid-grey. 0.92 composites to ~#1b1c1f over the brightest
     aurora, which reads as black while keeping the backdrop-filter glass. */
  --glass-bg: rgba(9, 10, 13, 0.92);
  --glass-bg-solid: rgba(9, 10, 13, 0.96);
  --glass-border: rgba(255, 255, 255, 0.14);
  --glass-rim: rgba(255, 255, 255, 0.30);
  --glass-rim-soft: rgba(255, 255, 255, 0.08);

  /* Nested light surfaces inside these cards. */
  --surface: #14161a;
  --surface-sunken: #0f1114;
  --surface-strong: rgba(255, 255, 255, 0.08);
  --surface-stage: rgba(9, 10, 13, 0.94);

  --line: rgba(255, 255, 255, 0.10);
  --line-strong: rgba(255, 255, 255, 0.18);

  --text: #fafafa;
  --text-dim: rgba(255, 255, 255, 0.66);

  /* The vivid hue is the accessible one here — see the note above. */
  --svc-build-ink: var(--svc-build);
  --svc-intelligence-ink: var(--svc-intelligence);
  --svc-content-ink: var(--svc-content);
  --svc-growth-ink: var(--svc-growth);
  --svc-foundation-ink: var(--svc-foundation);
  --svc-ink: var(--svc);

  /* The light page's shadows are tuned for dark-on-light and vanish here.
     The bevel leads both stacks so the pane reads as thick glass at rest and
     keeps that thickness through the hover lift — .glass-hover:hover swaps
     the whole box-shadow for --glass-shadow-lift, so carrying the inset in
     the token is the only way it survives the swap.

     These are the --hero-shell values: a large standalone panel with open
     space on every side, so the wide blur/spread pair below is safe — there
     is no neighbour for it to wash over. .step and .stat override both
     tokens further down with a tightened pair, because they sit in a grid
     with only a 15px gap (--space-4) between cards; see that rule for the
     reach arithmetic. */
  --glass-shadow: var(--liquid-bevel),
                  0 1px 2px rgba(0, 0, 0, 0.35),
                  0 12px 32px rgba(0, 0, 0, 0.45),
                  0 32px 64px rgba(0, 0, 0, 0.35);
  --glass-shadow-lift: var(--liquid-bevel),
                       0 2px 6px rgba(0, 0, 0, 0.4),
                       0 20px 48px rgba(0, 0, 0, 0.55);

  /* Switches on the rim's pointer specular and chromatic fringe (see
     .glass::before). Off by default so the light glass keeps its plain rim. */
  --liquid-spec-edge: var(--liquid-spec);
  --liquid-fringe-a: var(--liquid-ring-a);
  --liquid-fringe-b: var(--liquid-ring-b);

  /* Required, not redundant. `body` sets `color: var(--text-dim)`, which
     resolves to the light-page ink *at body* and then inherits down as an
     already-computed value — rebinding --text-dim above cannot reach it.
     Re-declaring color here re-resolves it inside the dark context. Without
     this, body copy in the cards sits at 3.14:1 (measured). */
  color: var(--text-dim);
}

/* .process-grid and .proof-grid (the only grids .step/.stat live in) run a
   15px gap (--space-4). A box-shadow's sideways reach is ~(blur/2 + spread),
   and the --hero-shell values above reach 16px and 32px on their wide
   layers — more than enough to cross a 15px gap and wash over the next
   card. Pull the reach in with negative spread while keeping the same
   offsets/blur so the downward drop still reads: spread cancels most of the
   blur's sideways half before the box is drawn, but the offset has already
   moved the box away from the top edge, so the blur still shows in full
   below the card.
     rest layer 2:  22px blur, -8px spread  -> reach = 22/2 -  8 =  3px
     rest layer 3:  32px blur, -20px spread -> reach = 32/2 - 20 = -4px (0)
     lift layer 2:  28px blur, -14px spread -> reach = 28/2 - 14 =  0px
   Both stay under the ~7px (rest) / ~9px (hover) budget with room to spare. */
.step,
.stat {
  --glass-shadow: var(--liquid-bevel),
                  0 1px 2px rgba(0, 0, 0, 0.35),
                  0 10px 22px -8px rgba(0, 0, 0, 0.45),
                  0 22px 32px -20px rgba(0, 0, 0, 0.35);
  /* Layer 1's blur is pulled back to a 1px reach so it matches the resting
     stack exactly. At `0 2px 6px` it reached 3px against the resting 1px, and
     that 2px difference was the last thing making the painted card wider on
     hover than at rest — measurable as the right edge moving 705 -> 707 with
     the box itself unchanged. Hover must not add a single pixel. */
  --glass-shadow-lift: var(--liquid-bevel),
                       0 2px 6px -4px rgba(0, 0, 0, 0.45),
                       0 14px 28px -14px rgba(0, 0, 0, 0.55);
}

/* .btn--primary is --ink (near-black) on white — invisible on a black card.
   Inside these surfaces it inverts to the light-on-dark equivalent. */
.hero-shell .btn--primary {
  background: var(--text);
  color: #0a0a0b;
}

.hero-shell .btn--ghost {
  border-color: var(--glass-border);
  color: var(--text);
}

/* ══════════════════════════════════════════════════════════════════════════
   TOUCH TARGETS
   ══════════════════════════════════════════════════════════════════════════
   Measured on a real touch viewport: footer and nav links rendered 17–19px
   tall and the header CTA 36px, all under the 44px minimum. Keyed on
   (pointer: coarse) rather than a width breakpoint because this is about the
   input device, not the screen size — a small window on a mouse-driven
   machine does not need fatter targets, and a large tablet does. */

@media (pointer: coarse) {
  /* display:inline-flex is load-bearing, not cosmetic: min-height has no
     effect on a non-replaced inline element, and <a> is inline by default.
     A rule that set only min-height measured 26px in testing. */
  .nav-links > li > a,
  .nav-dropdown-menu a,
  .footer-grid a,
  .footer-bottom a,
  .navbar .logo,
  .mode-btn,
  .panel-link,
  .text-link,
  .contact-aside a[href^="mailto:"],
  .contact-aside a[href^="tel:"] {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }

  /* Genuinely inline links inside a sentence are deliberately NOT inflated —
     WCAG 2.5.5 exempts them, and forcing 44px would break the text flow. The
     selectors above are all standalone controls. */

  /* Small uppercase labels are legible enough on desktop but tight on a
     phone. Body copy is already 15-17px and untouched. */
  .tag,
  .window-title,
  .chip {
    font-size: 14px;
  }

  /* Icon buttons need width as well as height. */
  .social-icons a {
    min-width: 44px;
    min-height: 44px;
  }

  .navbar .btn,
  .nav-links .btn-primary,
  .btn {
    min-height: 44px;
  }

  /* summary is the FAQ's whole hit area. */
  .faq-item summary {
    min-height: 44px;
  }
}
