/* One accent for the whole site: blue. It replaced the old purple/cyan pair —
   every spotlight (buttons, hero CTAs, the service grid, the cards below "what
   we do") is now the same blue glow over the same faint noise, so the
   interactive language reads as one system. --noise is shared by all of them;
   its alpha is deliberately low (.12) so it reads as grain, not static. */
:root {
  --accent: #3884ff;
  --accent-rgb: 56, 132, 255;
  --spotlight: var(--accent-rgb);
  --noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.12 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ============ Blue noise spotlight (shared) ============
   The site's one hover treatment: a cursor-tracked blue radial glow (centred
   on --mx/--my, set by script.js on pointermove) blended with the shared noise
   grain. Tune per-element with --spot-size / --spot-a1 / --spot-a2 — buttons
   and grid cells use the strong default fill; large cards dial it down to a
   soft ambient wash. Content is lifted above the glow via the child rule. */
.noise-spot { position: relative; overflow: hidden; }
.noise-spot::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity .3s ease;
  background:
    radial-gradient(circle var(--spot-size, 220px) at var(--mx, 50%) var(--my, 50%),
      rgba(var(--accent-rgb), var(--spot-a1, .9)),
      rgba(var(--accent-rgb), var(--spot-a2, .32)) 45%,
      transparent 72%),
    var(--noise);
  background-blend-mode: overlay;
}
.noise-spot:hover::before { opacity: 1; }
.noise-spot > * { position: relative; z-index: 1; }

/* ============ Scroll reveal (shared) ============
   Fade + drift up the first time an element scrolls into view. script.js adds
   .revealed via an IntersectionObserver; stagger siblings with an inline
   animation-delay.

   This deliberately uses an animation, NOT a transition: .reveal is applied to
   elements that declare their own transition for hover (.step,
   .btn-primary), and those rules come later in the cascade — a transition-based
   reveal would be overridden by them and snap instead of fading. An animation
   runs on a separate channel, so both work. Don't "simplify" this back into a
   transition. */
.reveal { opacity: 0; }
.reveal.revealed { animation: reveal-in .9s cubic-bezier(0.16, 1, 0.3, 1) both; }
@keyframes reveal-in {
  from { opacity: 0; transform: translateY(36px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; }
  .reveal.revealed { animation: none; }
}
html { scroll-behavior: smooth; }
body { margin: 0; background: #0a0a0c; color: #f2f2f4; font-family: 'Archivo', sans-serif; -webkit-font-smoothing: antialiased; }
a { color: #f2f2f4; text-decoration: none; }
a:hover { color: var(--accent); }
* { box-sizing: border-box; }
input, select, button { font-family: 'Archivo', sans-serif; }
input::placeholder { color: #6a6a72; }

/* ============ HERO panel slide-over ============
   The hero (Spline render + CTAs) rises over the photo section above it like
   a panel: the negative margin-top pulls it up into the previous section, and
   the opaque background + upward shadow make it read as a sheet floating over.
   script.js scrubs --slide-radius on scroll — rounded corners while the panel
   is arriving from below, flattening to 0 once it has scrolled up to fill the
   viewport. Reduced motion just leaves it lightly rounded. */
.hero-panel {
  z-index: 2;
  margin-top: -14vh;
  background: #0a0a0c;
  border-top-left-radius: var(--slide-radius, 32px);
  border-top-right-radius: var(--slide-radius, 32px);
  box-shadow: 0 -34px 70px -24px rgba(0, 0, 0, 0.75);
}

/* ============ TIC-TAC-TOE service board ============
   Replaces the old auto-scrolling ticker. A 3x3 hash grid is pinned in a
   tall section; script.js computes scroll progress through that section and
   toggles .is-placed on each cell, so the nine service tokens pop in one at
   a time as you scroll. Hovering a cell lights a cursor-tracked blue noise
   spotlight (see .ttt__cell::before). */
.ttt {
  position: relative;
  height: 220vh;                 /* scroll room for the pinned board */
  background: #0d0d10;
  border-top: 1px solid #1c1c20;
  border-bottom: 1px solid #1c1c20;
}
.ttt__pin {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  padding: clamp(16px, 2.5vw, 36px);
  box-sizing: border-box;
}
/* The board fills the whole pinned viewport as a full-bleed lattice: script.js
   sizes the columns/rows to the screen and drops empty cells around a centred
   3x3 block, so the nine service tokens sit in the middle of a grid that runs
   edge to edge. The template values below are just a pre-JS fallback. */
.ttt__grid {
  position: relative;
  width: 100%;
  height: 100%;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(3, 1fr);
  will-change: transform, opacity;   /* scaled + faded in by script.js on scroll */
  /* Opacity feather: the lattice is one uniform colour, faded out radially the
     further a cell sits from the centre. The solid core reaches ~62% so the
     centred 3x3 token block (spans ~60% of the shorter side) stays fully
     visible; only the surrounding filler cells fade to the edges. */
  -webkit-mask-image: radial-gradient(ellipse 50% 50% at 50% 50%, #000 62%, transparent 100%);
          mask-image: radial-gradient(ellipse 50% 50% at 50% 50%, #000 62%, transparent 100%);
}
/* Filler cells around the content block: grid lines only — never hover-lit,
   pickable, or animated. */
.ttt__cell--empty { pointer-events: none; }
/* Every cell carries its own full stroke, drawn as an inset box-shadow so
   adjacent edges sit flush (no layout offset, no doubled lines). Every cell —
   filler or placed — uses the one uniform stroke colour; the radial mask on
   .ttt__grid is what feathers them out toward the edges. */
.ttt__cell {
  position: relative;
  overflow: hidden;              /* clips the hover + selected fills to the cell */
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 8px;
  box-shadow: inset 0 0 0 1px #34343f;
  transition: box-shadow .4s ease;
}
.ttt__cell.is-placed {
  z-index: 2;                    /* keep placed cells above their neighbours */
  cursor: pointer;              /* placed cells are pickable */
}

/* Persistent blue fill on a picked cell — the same blue-noise wash the buttons
   flash on hover, but centred, full-cell, and latched on. Lives on ::after so
   it can fade in; the token span sits above it via z-index (below). */
.ttt__cell::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity .4s ease;
  background:
    radial-gradient(circle at 50% 44%,
      rgba(var(--accent-rgb), .62),
      rgba(var(--accent-rgb), .30) 55%,
      rgba(var(--accent-rgb), .12) 100%),
    var(--noise);
  background-blend-mode: overlay;
}
/* Picked: the cell goes the same flat brand blue the pricing band and contact
   section use as their ground, so a selection reads as a solid blue tile
   rather than a glow. Black type on it, matching how every other blue
   surface on the site is set. */
.ttt__cell.is-selected::after {
  opacity: 1;
  background: var(--accent);
}
.ttt__cell.is-selected {
  box-shadow: inset 0 0 0 1.5px var(--accent);
  z-index: 3;                    /* the lit stroke wins over placed neighbours */
}
.ttt__cell.is-selected span { color: #0a0a0c; }
/* Blue noise spotlight on hover: a cursor-tracked radial glow whose centre
   follows --mx/--my (set by script.js on pointermove) blended with a fine
   noise texture — the same trick as the site's buttons, tinted blue and
   scoped to the hovered service cell. The token span sits above it (its own
   transform gives it a stacking context). */
.ttt__cell::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity .3s ease;
  background:
    radial-gradient(circle 170px at var(--mx, 50%) var(--my, 50%), rgba(var(--accent-rgb), .9), rgba(var(--accent-rgb), .32) 45%, transparent 72%),
    var(--noise);
  background-blend-mode: overlay;
}
.ttt__cell:hover::before { opacity: 1; }
.ttt__cell span {
  position: relative;
  z-index: 2;                    /* keep the label above the hover/selected fills */
  font-family: 'IBM Plex Mono', monospace;
  font-size: clamp(10px, 1.1vw, 16px);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #cfcfd4;
  opacity: 0;
  transition: opacity .5s ease,
              color .3s ease;
}
.ttt__cell.is-placed span { opacity: 1; }
.ttt__cell:hover span { color: #ffffff; }

/* No pinning / scrubbing when motion is reduced — show the finished board. */
@media (prefers-reduced-motion: reduce) {
  .ttt { height: auto; }
  .ttt__pin { position: static; height: auto; padding: 96px 0; }
  .ttt__cell span { opacity: 1; transform: none; }
}

/* ============ SERVICE PICKER TRAY ============
   Fixed rail, right-of-centre, that slides in the moment a service is picked
   on the grid and rides the scroll all the way to the contact form. Flat and
   sharp-cornered like the rest of the site, blue-accented, mono labels. The
   selection lives in localStorage; see the #svcTray module in script.js. */
.svc-tray {
  position: fixed;
  top: 50%;
  right: 20px;
  transform: translate(calc(100% + 28px), -50%);   /* parked off-screen right */
  z-index: 40;
  width: 232px;
  max-width: calc(100vw - 40px);
  background: rgba(14, 14, 18, 0.92);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border: 1px solid #26262c;
  box-shadow: 0 20px 50px -20px rgba(0, 0, 0, 0.8);
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  opacity: 0;
  transition: transform .45s cubic-bezier(0.16, 1, 0.3, 1), opacity .3s ease;
}
.svc-tray.is-open { transform: translate(0, -50%); opacity: 1; }
.svc-tray[hidden] { display: none; }        /* fully removed when never opened */

.svc-tray__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding-bottom: 10px;
  border-bottom: 1px solid #26262c;
}
.svc-tray__title {
  font-family: 'IBM Plex Mono', monospace;
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: #9a9aa2;
}
.svc-tray__count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  margin-left: 6px;
  border-radius: 999px;
  background: rgba(var(--accent-rgb), .18);
  border: 1px solid rgba(var(--accent-rgb), .55);
  color: #fff;
  font-size: 12px;
}
.svc-tray__clear {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  background: transparent;
  border: 1px solid #2c2c32;
  color: #cfcfd4;
  cursor: pointer;
  transition: color .15s ease, border-color .15s ease, background .15s ease;
}
.svc-tray__clear:hover,
.svc-tray__clear:focus-visible {
  color: #fff;
  border-color: rgb(var(--accent-rgb));
  background: rgba(var(--accent-rgb), .14);
}

.svc-tray__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  max-height: 40vh;
  overflow-y: auto;
}
.svc-tray__item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 8px 10px;
  background: rgba(var(--accent-rgb), .10);
  border: 1px solid rgba(var(--accent-rgb), .45);
  font-size: 13px;
  color: #eef1f6;
  animation: reveal-in .4s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.svc-tray__item button {
  flex: none;
  width: 18px;
  height: 18px;
  line-height: 1;
  padding: 0;
  background: transparent;
  border: 0;
  color: #9fbcff;
  font-size: 16px;
  cursor: pointer;
  transition: color .15s ease;
}
.svc-tray__item button:hover { color: #fff; }

.svc-tray__cta {
  text-align: center;
  padding: 11px 14px;
  border: 1px solid #2c2c32;
  color: #f2f2f4;
  font-size: 14px;
  font-weight: 600;
  transition: color .15s ease, border-color .15s ease, background .15s ease;
}
.svc-tray__cta:hover {
  color: #fff;
  border-color: rgb(var(--accent-rgb));
  background: rgba(var(--accent-rgb), .14);
}

/* On narrow screens the rail docks to the bottom edge as a full-width bar. */
@media (max-width: 640px) {
  .svc-tray {
    top: auto;
    bottom: 0;
    right: 0;
    left: 0;
    width: 100%;
    max-width: none;
    border-left: 0;
    border-right: 0;
    border-bottom: 0;
    transform: translateY(calc(100% + 28px));
  }
  .svc-tray.is-open { transform: translateY(0); }
  .svc-tray__list { max-height: 30vh; }
}

@media (prefers-reduced-motion: reduce) {
  .svc-tray { transition: opacity .2s ease; }
  .svc-tray__item { animation: none; }
}

/* Summary of the picked services shown inside the contact form. */
/* Picked services, carried down from the board into the contact form. The
   wrapper is deliberately unstyled — the tags themselves are the black
   boxes, so they read directly against the blue of the contact section
   rather than sitting in a black box inside a black box. */
.svc-picked {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 0;
  background: none;
  border: 0;
}
.svc-picked[hidden] { display: none; }
.svc-picked__label {
  font-family: 'IBM Plex Mono', monospace;
  font-size: 13px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: rgba(10, 10, 12, 0.72);
}
.svc-picked__chips { display: flex; flex-wrap: wrap; gap: 10px; }
.svc-picked__chip {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px 12px 20px;
  border-radius: 10px;         /* the one rounded thing on an otherwise sharp page */
  background: #0a0a0c;
  border: 0;
  font-size: 15px;
  font-weight: 500;
  color: var(--accent);
}
/* Per-tag remove, so a service can be dropped without going back to the board. */
.svc-picked__remove {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: rgba(var(--accent-rgb), 0.16);
  color: var(--accent);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  transition: background-color .2s ease, color .2s ease;
}
.svc-picked__remove:hover,
.svc-picked__remove:focus-visible {
  background: var(--accent);
  color: #0a0a0c;
}

/* Interactive elements — base + hover/focus states live here since inline
   styles can't be overridden by pseudo-classes in an external stylesheet.
   Buttons are flat, sharp-cornered rectangles at rest (border-radius: 0 is
   set inline per-instance in the HTML). On hover, a cursor-tracking spotlight
   (cyan on .btn-outline, purple on the rest — see --spotlight override below)
   with a noise texture reveals itself via a ::before layer whose position is
   driven by the --mx/--my custom properties (set by script.js on
   pointermove). isolation: isolate gives each button its own stacking
   context so the ::before's z-index: -1 is scoped to that button (painting
   above its own flat background, below its text) — without it, a negative
   z-index resolves against the nearest ancestor stacking context instead
   (here, effectively the page background), which is why the spotlight was
   only visible on .btn-outline (transparent fill hid nothing) and invisible
   on the other three (their opaque flat fill painted over it). */

.btn-nav-cta, .btn-primary, .btn-outline, .btn-dark {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  transition: background .15s ease, color .15s ease, border-color .15s ease;
}

.btn-nav-cta::before, .btn-primary::before, .btn-outline::before, .btn-dark::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  transition: opacity .25s ease;
  background:
    radial-gradient(circle 140px at var(--mx, 50%) var(--my, 50%), rgba(var(--spotlight), .9), rgba(var(--spotlight), .35) 45%, transparent 72%),
    var(--noise);
  background-blend-mode: overlay;
}
.btn-nav-cta:hover::before, .btn-nav-cta:focus-visible::before,
.btn-primary:hover::before, .btn-primary:focus-visible::before,
.btn-outline:hover::before, .btn-outline:focus-visible::before,
.btn-dark:hover::before, .btn-dark:focus-visible::before {
  opacity: 1;
}

/* All four buttons share one look — the old .btn-outline base (transparent
   fill, thin gray border, light text) — and one blue spotlight, inherited
   from --spotlight in :root. */
.btn-nav-cta,
.btn-primary,
.btn-outline,
.btn-dark {
  background: transparent;
  border-width: 1px;
  border-style: solid;
  border-color: #2c2c32;
  color: #f2f2f4;
}
.btn-nav-cta:hover, .btn-nav-cta:focus-visible,
.btn-primary:hover, .btn-primary:focus-visible,
.btn-outline:hover, .btn-outline:focus-visible,
.btn-dark:hover, .btn-dark:focus-visible {
  border-color: rgb(var(--spotlight));
  color: #ffffff;
}

/* ============ HERO CTAs ============
   Two oversized call-to-action buttons pinned to the bottom-right of the
   hero, overlapping the 3D logo. At rest each shows its text label plus an
   up-right arrow. On hover the label slides up and out of view while an
   animated icon rises into its place — a ringing phone on the purple "fit
   call" button, a pair of blinking eyes on the "see what we do" button.
   The label/icon share one fixed-height slot (.hero-cta__text) and are
   stacked so one exits as the other enters (the morph). */
/* The pair sits side by side, centred, a good drop below the statement, and
   rides the scroll in two steps driven by script.js:
     .is-docked  — pinned flush to the bottom edge of the screen, at full size,
                   picked up as they rise into view and held all through the hero
     .is-compact — scrolled past the hero, so they shrink into a small dock and
                   follow you down the rest of the page
   Fixed rather than sticky, because a sticky element can't outlive its own
   section and these have to. .hero-cta-slot keeps their space in the panel so
   nothing jumps at the moment they go fixed. Scaling (rather than restyling)
   is what shrinks them, so the button internals — the fixed-height morph slot
   included — can't reflow on the way down. */
.hero-cta-slot {
  margin-top: clamp(80px, 16vh, 190px);
}
/* Pinned to the bottom-right of the viewport at all times — visible from the
   moment the page loads through the very bottom of the page, and matches
   .hero-cta-group.is-docked's position exactly so nothing jumps once
   script.js applies that class. Fixed outright (not tucked inside a section)
   so no ancestor's overflow:hidden can ever clip it as you scroll. */
.hero-cta-slot--intro {
  position: fixed;
  right: 40px;
  bottom: 0;
  margin-top: 0;
  width: max-content;          /* shrink to the buttons' own natural size — never crops their text */
  max-width: calc(100vw - 80px);
  z-index: 40;                 /* under the fixed header (50) */
}
.hero-cta-group {
  --dock-scale: 0.62;
  position: relative;
  z-index: 3;
  display: flex;
  gap: clamp(16px, 1.6vw, 24px);
  align-items: stretch;
  justify-content: flex-end;
  margin: 0 0 0 auto;          /* pushed to the right edge of the panel */
  transform-origin: bottom right;
  /* Composed from two variables rather than each state redeclaring transform:
     the shrink (.is-compact) and the drop-away (.is-hidden) can be in effect
     at the same time, and a second transform declaration would just replace
     the first instead of combining with it. */
  transform: translateY(var(--cta-drop, 0)) scale(var(--cta-scale, 1));
  transition: transform .55s cubic-bezier(0.16, 1, 0.3, 1), opacity .4s ease;
}
.hero-cta-group.is-docked {
  position: fixed;
  left: auto;
  right: 40px;                 /* matches the panel's 40px side padding */
  bottom: 0;
  z-index: 40;                 /* under the fixed header (50) */
  pointer-events: none;        /* the strip must not eat clicks */
}
.hero-cta-group.is-compact {
  --cta-scale: var(--dock-scale);
}
/* Contact form in view: the buttons have done their job, so they drop out
   rather than sit on top of the form they were pointing at. */
.hero-cta-group.is-hidden {
  --cta-drop: 160%;
  opacity: 0;
  pointer-events: none;
}
/* .hero-cta re-enables pointer events for the docked strip, so it has to be
   switched off here too — otherwise the faded-out buttons still swallow
   clicks meant for the form underneath. */
.hero-cta-group.is-hidden .hero-cta { pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
  .hero-cta-group { transition: opacity .4s ease; }
}

/* flex: 0 0 auto — each button sizes to its own label, so "book a 15-minute
   fit call" is naturally wider than "see what we do" rather than both being
   forced into an equal share of a fixed row width (that only made sense when
   this sat centered in a wide, shared-width row; parked in a corner now, each
   button should just hug its own text). */
.hero-cta {
  position: relative;
  isolation: isolate;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: clamp(16px, 1.6vw, 40px);
  padding: clamp(22px, 2.2vw, 34px) clamp(24px, 2.6vw, 42px);
  pointer-events: auto;        /* the docked group turns pointer events off */
  background: rgba(13, 13, 16, 0.5);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  color: #f2f2f4;
  font-size: clamp(17px, 1.9vw, 30px);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1;
  white-space: nowrap;
  opacity: 0;
  transform: translateY(42px);
  animation: hero-cta-in .7s cubic-bezier(0.16, 1, 0.3, 1) forwards;
  transition: color .2s ease;
}
/* Rise in one after another, beneath the 3D logo, once the hero settles. */
.hero-cta:nth-child(1) { animation-delay: .35s; }
.hero-cta:nth-child(2) { animation-delay: .5s; }
.hero-cta:hover { color: #ffffff; }

@keyframes hero-cta-in {
  from { opacity: 0; transform: translateY(42px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Resting look: a thin grey stroke drawn by the ::before ring (the "fill the
   box, then punch out the content box" mask trick, so it reads as a border
   without being one). On hover, instead of lighting that stroke, ::after
   fills the button with the same cursor-tracked blue noise spotlight as the
   grid service cells — a blue radial glow centred on --mx/--my (set by
   script.js on pointermove) blended with a faint noise texture. The label and
   arrow sit above the fill via z-index (below). */
.hero-cta::before {
  content: "";
  position: absolute;
  inset: 0;
  padding: 2px;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
  background: #2c2c32;
  pointer-events: none;
}
.hero-cta::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity .3s ease;
  background:
    radial-gradient(circle 240px at var(--mx, 50%) var(--my, 50%), rgba(var(--accent-rgb), .9), rgba(var(--accent-rgb), .32) 45%, transparent 72%),
    var(--noise);
  background-blend-mode: overlay;
}
.hero-cta:hover::after { opacity: 1; }
/* Keep the label/icon and arrow above the blue fill. */
.hero-cta__text { z-index: 1; }
.hero-cta__arrow { position: relative; z-index: 1; }

/* the morph slot — fixed height so the swap doesn't reflow the button;
   overflow: hidden clips the label sliding out and the icon rising in (this
   clip lives here, not on .hero-cta, so the stroke rings aren't cut off). */
.hero-cta__text {
  position: relative;
  flex: 1;
  height: 40px;
  display: flex;
  align-items: center;
  overflow: hidden;
}
.hero-cta__label {
  transition: opacity .25s ease, transform .4s cubic-bezier(0.16, 1, 0.3, 1);
}
.hero-cta:hover .hero-cta__label {
  opacity: 0;
  transform: translateY(-140%);
}
.hero-cta__hover {
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(20px);
  opacity: 0;
  transition: opacity .25s ease, transform .4s cubic-bezier(0.16, 1, 0.3, 1);
}
.hero-cta:hover .hero-cta__hover {
  opacity: 1;
  transform: translateY(-50%);
}
.hero-cta__hover--phone svg { width: 40px; height: 40px; display: block; }
.hero-cta__hover--eyes svg { width: 64px; height: 40px; display: block; }

.hero-cta__arrow svg {
  width: 34px;
  height: 34px;
  display: block;
  flex-shrink: 0;
  transition: transform .3s cubic-bezier(0.16, 1, 0.3, 1);
}
.hero-cta:hover .hero-cta__arrow svg { transform: translate(5px, -5px); }

/* ringing phone: rock rapidly side to side while hovered */
@keyframes phone-ring {
  0%, 62%, 100% { transform: rotate(0deg); }
  8%, 24%, 40%  { transform: rotate(-16deg); }
  16%, 32%, 48% { transform: rotate(16deg); }
}
.hero-cta:hover .hero-cta__hover--phone svg {
  transform-box: fill-box;
  transform-origin: center;
  animation: phone-ring 1.1s ease-in-out infinite;
}

/* blinking eyes: squash vertically for a beat, on a slow loop */
@keyframes eyes-blink {
  0%, 40%, 46%, 100% { transform: scaleY(1); }
  43% { transform: scaleY(0.08); }
}
.hero-cta:hover .hero-cta__hover--eyes svg {
  transform-box: fill-box;
  transform-origin: center;
  animation: eyes-blink 2.8s ease-in-out infinite;
}

/* ============ "What we do" and everything below ============
   This half of the page speaks the same language as the grid and buttons
   above: flat and sharp-cornered (no radii), hairline #26262c rules instead of
   soft filled cards, mono uppercase eyebrows, and the single blue noise
   spotlight on anything hoverable. The spotlight is tuned per surface via
   --spot-size/--spot-a1/--spot-a2 — small and strong on buttons, wide and
   faint on big cards so it reads as an ambient wash following the cursor. */

/* The "what we do" column. The cards used to pin and deal onto a sticky pile;
   now they're a plain flex stack that scrolls normally — 1200px wide,
   left-aligned, 24px between items — and each block fades up on its own as it
   comes into view (.reveal) instead of stacking. */
.svc-stack {
  display: flex;
  flex-flow: column;
  place-content: flex-start;
  align-items: flex-start;
  gap: 24px;
  width: 100%;
  max-width: 1200px;
  height: min-content;
  margin: 0 auto;
  padding: 100px 24px 80px;
  position: relative;
  overflow: visible;
}
/* Each quote block. align-items: flex-start on the stack would otherwise
   shrink these to their content width, so they claim the full column. */
.svc-card {
  --spot-size: 520px;
  --spot-a1: .16;
  --spot-a2: .05;
  width: 100%;
  border: 1px solid #26262c;
  transition: border-color .3s ease;
}
.svc-card:hover { border-color: rgba(var(--accent-rgb), .55); }
/* card 04 is the lead offer — it carries the accent stroke at rest */
.svc-card--lead { border-color: rgba(var(--accent-rgb), .5); }
.svc-card--lead:hover { border-color: var(--accent); }

/* Capability chips: sharp, hairline, they light up individually on hover. */
.svc-tag {
  border: 1px solid #2c2c32;
  padding: 10px 18px;
  font-size: 14px;
  color: #cfcfd4;
  transition: border-color .2s ease, color .2s ease;
}
.svc-tag:hover { border-color: rgba(var(--accent-rgb), .6); color: #ffffff; }

/* Pricing banner: full-bleed, reusing the service slides' blue/black pair
   directly rather than a separate card treatment. It assembles out of blue
   squares on scroll — see the dedicated IIFE in script.js for the one-shot
   .is-armed / .is-open trigger. */
.price-band {
  position: relative;
  overflow: hidden;
  background: var(--accent);
  /* Explicit height rather than letting the copy size it: the tile grid is
     laid out from this box, so a stable band proportion keeps the squares
     close to square across viewports. */
  height: clamp(260px, 22vw, 340px);
  /* Breathing room from the sections above and below. Vertical only — the
     band stays full-bleed edge to edge horizontally. */
  margin: clamp(56px, 6.5vw, 104px) 0;
  display: flex;
  align-items: center;
  padding: 0 40px;
}
.price-band__inner {
  position: relative;
  z-index: 2;                  /* above the tile layer */
  width: 100%;
  max-width: 1240px;
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 40px;
  flex-wrap: wrap;
}
.price-band__title {
  font-size: clamp(32px, 4vw, 56px);
  line-height: 1.05;
  letter-spacing: -0.02em;
  font-weight: 700;
  margin: 0;
  color: #0a0a0c;              /* black type on brand blue, matches .svc-slide--blue */
  max-width: 640px;
}
.price-band__cta {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 18px 36px;
  border-radius: 0;
  font-weight: 600;
  font-size: 17px;
  white-space: nowrap;
  color: #0a0a0c;
  border-color: rgba(10, 10, 12, 0.4);
}

/* ============ Checkerboard reveal (shared) ============
   A grid of blue squares that pop in to tile a blue surface into existence.
   Used by the pricing band and the contact section. script.js builds the
   tiles (the count depends on the host's size) and stamps each one's
   transition-delay, so the two checker passes sweep across in turn.

   The host's background is blue by DEFAULT and only goes dark once script.js
   adds .is-armed — deliberately that way round, so a stale, broken or blocked
   script.js leaves a plain blue surface rather than a dark empty box that
   never fills in. */
.tiles {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: grid;
  pointer-events: none;
}
.tiles__tile {
  background: var(--accent);
  transform: scale(0);
  transition: transform .55s cubic-bezier(0.16, 1, 0.3, 1);
  will-change: transform;
}
/* Slightly over 1 so neighbouring tiles overlap by a hair — at exactly
   scale(1) sub-pixel rounding can leave dark hairlines between them. */
.is-open > .tiles .tiles__tile { transform: scale(1.04); }
@media (prefers-reduced-motion: reduce) {
  .tiles__tile { transition: none; }
}

/* Per-host: the dark ground the tiles assemble over, and the content that
   fades up once the squares have landed. */
.price-band.is-armed,
.contact.is-armed { background: #0b0b0e; }   /* matches .svc-slide--dark */

.price-band.is-armed .price-band__inner,
.contact.is-armed .contact__inner {
  opacity: 0;
  transition: opacity .45s ease;
}
.price-band.is-armed.is-open .price-band__inner,
.contact.is-armed.is-open .contact__inner {
  opacity: 1;
  transition: opacity .5s ease .5s;
}

/* ============ INDUSTRIES scroll toggle ============
   Four hairline-ruled rows; exactly one is active. The active row's name goes
   from grey to white and its copy expands beneath it, the rest stay collapsed
   to just the name. script.js sets .is-active from scroll position and sets
   the open row's max-height in px (measured, so the ease is exact — a fixed
   max-height guess makes the transition stall or snap). */
.ind-list { border-top: 1px solid #232328; }
.ind-row { border-bottom: 1px solid #232328; }

/* A button so the rows are keyboard-reachable and clickable, reset to look
   like plain layout rather than a control. */
.ind-row__head {
  width: 100%;
  background: none;
  border: 0;
  padding: clamp(22px, 2.4vw, 34px) 0;
  display: flex;
  align-items: baseline;
  gap: clamp(16px, 2vw, 32px);
  cursor: pointer;
  text-align: left;
  color: inherit;
}
/* Tally-mark counter instead of a printed number: row one gets one stroke,
   row four gets four. (A fifth would need the diagonal cross-stroke; there
   are only four industries, so plain strokes cover it.) */
.ind-row__tally {
  display: flex;
  align-items: flex-end;
  gap: 6px;
  flex-shrink: 0;
  width: 46px;                 /* fixed, so every name starts on one line */
  height: 30px;
}
.ind-row__tally i {
  display: block;
  width: 3px;
  height: 100%;
  background: #4a4a54;
  transform-origin: bottom;
  transition: background-color .35s ease, transform .35s cubic-bezier(0.16, 1, 0.3, 1);
}
.ind-row.is-active .ind-row__tally i { background: var(--accent); }
/* the strokes stand up one after another as the row becomes active */
.ind-row .ind-row__tally i { transform: scaleY(.68); }
.ind-row.is-active .ind-row__tally i { transform: scaleY(1); }
.ind-row.is-active .ind-row__tally i:nth-child(2) { transition-delay: .05s; }
.ind-row.is-active .ind-row__tally i:nth-child(3) { transition-delay: .1s; }
.ind-row.is-active .ind-row__tally i:nth-child(4) { transition-delay: .15s; }
.ind-row__name {
  font-size: clamp(26px, 3.4vw, 46px);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1;
  color: #6a6a72;
  transition: color .35s ease;
}
.ind-row.is-active .ind-row__name,
.ind-row__head:hover .ind-row__name { color: #f2f2f4; }

/* visibility (not just opacity) so links inside a collapsed row drop out of
   the tab order; it flips instantly on open and only after the close. */
.ind-row__body {
  max-height: 0;
  overflow: hidden;
  opacity: 0;
  visibility: hidden;
  transition: max-height .5s cubic-bezier(0.16, 1, 0.3, 1),
              opacity .3s ease,
              visibility 0s linear .5s;
}
.ind-row.is-active .ind-row__body {
  opacity: 1;
  visibility: visible;
  transition: max-height .5s cubic-bezier(0.16, 1, 0.3, 1),
              opacity .4s ease .1s,
              visibility 0s;
}
.ind-row__inner { padding: 0 0 clamp(24px, 2.6vw, 36px); }

/* Blue arrow closing out each row. margin-left:auto parks it hard against the
   right edge of the rule, so the four line up in a column. */
.ind-row__arrow {
  margin-left: auto;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  color: var(--accent);
  transition: transform .35s cubic-bezier(0.16, 1, 0.3, 1), opacity .35s ease;
  opacity: .55;
}
.ind-row__arrow svg {
  width: clamp(22px, 2.2vw, 32px);
  height: clamp(22px, 2.2vw, 32px);
  display: block;
}
.ind-row.is-active .ind-row__arrow,
.ind-row__head:hover .ind-row__arrow { opacity: 1; }
.ind-row.is-active .ind-row__arrow { transform: translate(4px, -4px); }
.ind-row__head:hover .ind-row__arrow { transform: translate(6px, -6px); }
.ind-row__link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--accent);
  font-weight: 600;
  font-size: 15px;
}

/* Hover cursor: over a row the native pointer is hidden and replaced by a
   blue box carrying that row's copy, tracking the mouse. One element reused
   for all four rows; script.js fills its text and drives --cx/--cy.
   Pointer-fine only, so it never appears on touch (where there is no cursor
   to replace and the box would just obscure the row). */
@media (hover: hover) and (pointer: fine) {
  .ind-row__head { cursor: none; }
}
.ind-cursor {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 60;                 /* over the fixed header (50) */
  pointer-events: none;
  max-width: 300px;
  padding: 14px 18px;
  background: var(--accent);
  color: #0a0a0c;
  font-size: 14px;
  font-weight: 600;
  line-height: 1.4;
  text-wrap: pretty;
  opacity: 0;
  transform: translate3d(var(--cx, 0), var(--cy, 0), 0) scale(.85);
  transform-origin: 0 0;
  transition: opacity .2s ease, scale .25s cubic-bezier(0.16, 1, 0.3, 1);
  will-change: transform;
}
.ind-cursor.is-visible {
  opacity: 1;
  transform: translate3d(var(--cx, 0), var(--cy, 0), 0) scale(1);
}
@media (prefers-reduced-motion: reduce) {
  .ind-cursor { transition: opacity .2s ease; }
}

/* No scroll toggling under reduced motion — every row is simply open. */
.ind-list.is-static .ind-row__body {
  max-height: none;
  opacity: 1;
  visibility: visible;
  transition: none;
}
.ind-list.is-static .ind-row__name { color: #f2f2f4; }

/* Flat panels (the form, and any plain bordered surface down here). */
.panel { background: #0d0d10; border: 1px solid #26262c; }

/* Process steps: hairline-ruled rows, the rule lights up as you pass over. */
.step { border-top: 1px solid #232328; transition: border-color .3s ease; }
.step:last-child { border-bottom: 1px solid #232328; }
.step:hover { border-color: rgba(var(--accent-rgb), .5); }

.form-input {
  border-width: 1px;
  border-style: solid;
  border-color: #2c2c32;
  outline: none;
  transition: border-color .2s ease, box-shadow .2s ease;
}
.form-input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(var(--accent-rgb), .16);
}

/* ============ CONTACT ============
   Full-bleed blue slab carrying the form at full scale: black input boxes,
   blue type inside them, black copy on the blue ground. */
.contact {
  position: relative;          /* containing block for the .tiles layer */
  overflow: hidden;            /* clips the tiles' slight overscale */
  background: var(--accent);
  padding: clamp(80px, 10vw, 160px) clamp(24px, 5vw, 80px);
  scroll-margin-top: 120px;    /* clears the fixed header on #contact jumps */
}
.contact__inner {
  position: relative;
  z-index: 2;                  /* above the tile layer */
  max-width: 1100px;
  margin: 0 auto;
}

/* The oversized line keeps its blurb hidden until hovered, then drops it
   down. Hovering the whole intro (not just the heading) is deliberate: once
   the blurb is down it is part of the hover target, so moving the pointer
   onto it to read doesn't snap it shut. */
.contact__intro { margin: 0 0 clamp(40px, 5vw, 72px); }
.contact__title {
  font-size: clamp(44px, 8vw, 128px);
  line-height: 0.94;
  letter-spacing: -0.035em;
  font-weight: 700;
  color: #0a0a0c;
  margin: 0;
  text-wrap: balance;
  cursor: default;
}
.contact__blurb-wrap {
  overflow: hidden;
  max-height: 0;
  transition: max-height .55s cubic-bezier(0.16, 1, 0.3, 1);
}
.contact__blurb {
  font-size: clamp(17px, 1.7vw, 24px);
  line-height: 1.5;
  font-weight: 500;
  color: rgba(10, 10, 12, 0.72);
  max-width: 620px;
  margin: clamp(18px, 2vw, 28px) 0 0;
  transform: translateY(-14px);
  opacity: 0;
  transition: transform .55s cubic-bezier(0.16, 1, 0.3, 1), opacity .4s ease;
  text-wrap: pretty;
}
.contact__intro:hover .contact__blurb-wrap,
.contact__intro:focus-within .contact__blurb-wrap { max-height: 260px; }
.contact__intro:hover .contact__blurb,
.contact__intro:focus-within .contact__blurb {
  transform: none;
  opacity: 1;
}
/* No hover on touch, so the blurb would never be reachable — show it. */
@media (hover: none) {
  .contact__blurb-wrap { max-height: 260px; }
  .contact__blurb { transform: none; opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .contact__blurb-wrap, .contact__blurb { transition: none; }
}

.contact__form { display: flex; flex-direction: column; gap: clamp(14px, 1.5vw, 20px); }
.contact__row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(14px, 1.5vw, 20px);
}
@media (max-width: 640px) {
  .contact__row { grid-template-columns: 1fr; }
}

/* Black boxes, blue type. */
.contact__input {
  width: 100%;
  background: #0a0a0c;
  border-color: #0a0a0c;
  color: var(--accent);
  font-size: clamp(16px, 1.5vw, 21px);
  font-weight: 500;
  padding: clamp(18px, 1.9vw, 28px) clamp(18px, 1.8vw, 26px);
}
.contact__input::placeholder { color: rgba(var(--accent-rgb), .68); }
.contact__input:focus {
  border-color: #ffffff;
  box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
}
.contact__select {
  appearance: none;
  /* the native arrow disappears with appearance:none, so draw one */
  background-image:
    linear-gradient(45deg, transparent 50%, var(--accent) 50%),
    linear-gradient(135deg, var(--accent) 50%, transparent 50%);
  background-position:
    calc(100% - 26px) calc(50% + 2px),
    calc(100% - 19px) calc(50% + 2px);
  background-size: 7px 7px, 7px 7px;
  background-repeat: no-repeat;
  padding-right: 56px;
}
.contact__select option { background: #0a0a0c; color: var(--accent); }

/* ============ Swipe to submit ============
   Black track, blue handle. script.js drives two custom properties as you
   drag: --swipe-x (the handle's offset in px) and --swipe-p (0 to 1), which
   the fill width and the label's fade read off. Release past the threshold
   and it locks to the end and submits; release short and it springs back.
   The track carries the same cursor-tracked spotlight as the site's buttons,
   but borderless — the black block on blue already reads as a control. */
.swipe {
  --swipe-x: 0px;
  --swipe-p: 0;
  margin-top: clamp(8px, 1vw, 16px);
}
.swipe__track {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: flex;
  align-items: center;
  height: clamp(64px, 6vw, 88px);
  background: #0a0a0c;
  user-select: none;
}
.swipe__track::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  transition: opacity .25s ease;
  background:
    radial-gradient(circle 200px at var(--mx, 50%) var(--my, 50%),
      rgba(var(--accent-rgb), .9),
      rgba(var(--accent-rgb), .35) 45%,
      transparent 72%),
    var(--noise);
  background-blend-mode: overlay;
}
.swipe__track:hover::before { opacity: 1; }

/* The trail the handle leaves behind it. Inset to match the handle, so it
   fills exactly the space the handle has vacated. */
.swipe__fill {
  position: absolute;
  left: 8px;
  top: 8px;
  bottom: 8px;
  width: var(--swipe-x);
  background: rgba(var(--accent-rgb), 0.38);
  pointer-events: none;
}
.swipe__label {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  white-space: nowrap;
  font-size: clamp(14px, 1.3vw, 18px);
  font-weight: 600;
  color: var(--accent);
  opacity: calc(1 - var(--swipe-p) * 1.6);   /* gone well before the end */
  pointer-events: none;
}
/* Inset by 8px so the black track frames the handle on every side. Without
   that frame the blue handle would sit flush to the track's edge and blend
   straight into the blue section behind it. */
.swipe__handle {
  position: relative;
  z-index: 1;
  flex-shrink: 0;
  width: clamp(64px, 6vw, 88px);
  height: calc(100% - 16px);
  margin: 8px;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: var(--accent);
  color: #0a0a0c;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: grab;
  touch-action: none;          /* the drag is ours, not the page's */
  transform: translateX(var(--swipe-x));
  transition: transform .4s cubic-bezier(0.16, 1, 0.3, 1);
}
.swipe__handle svg { width: 26px; height: 26px; display: block; }
.swipe.is-dragging .swipe__handle {
  cursor: grabbing;
  transition: none;            /* follow the finger exactly while dragging */
}
/* Completed: the trail goes solid blue so the handle and the space it came
   from read as one filled bar. The track's black frame deliberately stays,
   or the whole control would disappear into the blue section behind it. */
.swipe.is-complete .swipe__fill { background: var(--accent); }
@media (prefers-reduced-motion: reduce) {
  .swipe__handle { transition: none; }
}

.contact__success { display: none; flex-direction: column; gap: 12px; padding: 32px 0 0; }
.contact__success-tag {
  font-family: 'IBM Plex Mono', monospace;
  font-size: 13px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #0a0a0c;
}
.contact__success-copy {
  font-size: clamp(18px, 1.8vw, 26px);
  line-height: 1.4;
  font-weight: 600;
  color: #0a0a0c;
  margin: 0;
}

/* ============ RANGE SELECTOR text ============
   The hero statement. script.js rebuilds the line as one span per character
   and drives each character's color on scroll, so the colors below are only
   the resting state: plain white if the script never runs or motion is
   reduced. Words are inline-block so a word never breaks across lines once it
   has been split into characters. */
.range-text {
  font-size: clamp(26px, 3.6vw, 60px);
  line-height: 1.2;
  letter-spacing: -0.025em;
  font-weight: 700;
  text-align: center;
  text-wrap: pretty;
  color: #f2f2f4;
  margin: 0;
  max-width: min(1000px, 90vw);
}
/* .range-sweep is the sweep WITHOUT the typography above: for lines that
   already carry their own type styling (the captions over the photo rail,
   the closing headline) and only want the scroll-driven colour animation.
   It deliberately declares nothing — script.js picks it up alongside
   .range-text and the element keeps whatever styling it already had. */
.rt-word {
  display: inline-block;
  white-space: pre;
}
.rt-char {
  will-change: color;
}

/* ============ SERVICE SLIDE PANELS ============
   The boxed .svc-card stack is gone. Each "I want…" statement is a full-bleed
   panel, alternating blue / dark down the column. On the blue panels a solid
   brand-blue sheet wipes in from the left: the panel's own background IS the
   blue, and .svc-slide__cover (page-dark) sits over it and RETRACTS left→right
   as script.js drives --wipe 0→1, so the blue appears to slide in. --wipe
   defaults to 1 (fully retracted → full blue) so with no JS / reduced motion
   the panel simply renders solid blue. The statement is a .range-text line —
   grey→black on the blue panels (data-range="black"), grey→white→blue on the
   dark ones — and the capability lines rule in one after another, hairline
   between each, once the panel reveals. Goth-bold, tight leading. */
#services { padding: 0; }
.svc-intro { max-width: 1200px; margin: 0 auto; padding: 100px 24px clamp(90px, 14vh, 190px); }

/* The panels STACK: every slide is sticky at top:0, so each one pins and the
   next one is painted over it (later in the DOM = on top). script.js drives
   --slide 0→1 as a panel comes up, translating it in from the side — odd
   panels from the right, even from the left (--dir) — so consecutive panels
   overlay each other laterally instead of just scrolling past. overflow-x:
   clip (NOT hidden — hidden would make this a scroll container and kill the
   sticky) hides the off-screen half of an incoming panel. --slide defaults to
   1, so no-JS / reduced-motion renders the plain stack with nothing offset. */
.svc-slides { position: relative; width: 100%; overflow-x: clip; }

.svc-slide {
  position: sticky;
  top: 0;
  overflow: hidden;
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  padding: clamp(72px, 11vh, 132px) clamp(24px, 6vw, 80px);
  --slide: 1;
  --dir: 1;
  transform: translate3d(calc((1 - var(--slide)) * var(--dir) * 100%), 0, 0);
}
.svc-slide:nth-of-type(even) { --dir: -1; }
.svc-slide--dark { background: #0b0b0e; }
.svc-slide--blue { background: var(--accent); }

.svc-slide__cover { display: none; }   /* superseded by the slide-over stack */
.svc-slide__inner { position: relative; z-index: 1; width: 100%; max-width: 1200px; margin: 0 auto; }

/* The statement — goth-bold, tight leading. Colors are the resting state only;
   the range sweep in script.js overrides them on scroll. */
.svc-slide__title {
  font-size: clamp(26px, 4.4vw, 62px);
  line-height: 0.92;
  letter-spacing: -0.035em;
  font-weight: 700;
  text-align: left;               /* overrides .range-text's centering */
  margin: 0 0 clamp(28px, 4vw, 48px);
  max-width: 28ch;                /* longest statement breaks at 2 lines */
  text-wrap: balance;
  color: #f2f2f4;                 /* dark-panel resting */
}
.svc-slide--blue .svc-slide__title { color: #0a0a0c; }   /* blue-panel resting */

/* The statement flips side with the panel color: blue panels read left,
   dark panels read right. margin-left:auto pushes the 18ch block over. */
.svc-slide:nth-of-type(even) .svc-slide__title {
  text-align: right;
  margin-left: auto;
}

/* Capability lines: bold, tight, one paragraph rule between each. */
.svc-slide__lines { list-style: none; margin: 0; padding: 0; max-width: 940px; }
.svc-slide__lines li {
  font-size: clamp(15px, 1.5vw, 21px);
  font-weight: 700;
  line-height: 1.02;
  letter-spacing: -0.01em;
  padding: clamp(14px, 1.6vw, 20px) 0;
}
.svc-slide--dark .svc-slide__lines li { color: #d4d4da; border-top: 1px solid #26262c; }
.svc-slide--dark .svc-slide__lines li:last-child { border-bottom: 1px solid #26262c; }
.svc-slide--blue .svc-slide__lines li { color: #0a0a0c; border-top: 1px solid rgba(10, 10, 12, 0.24); }
.svc-slide--blue .svc-slide__lines li:last-child { border-bottom: 1px solid rgba(10, 10, 12, 0.24); }

/* The lines rule in one after another once the panel reveals — the "sub text
   animating in after" beat. The panel itself only fades (no lift) so the big
   band doesn't jump; the whole-panel .reveal-in would translate it. */
.svc-slide.reveal.revealed { animation: svc-slide-fade 0.8s ease both; }
@keyframes svc-slide-fade { from { opacity: 0; } to { opacity: 1; } }
.svc-slide__lines li { opacity: 0; }
.svc-slide.revealed .svc-slide__lines li { animation: reveal-in 0.7s cubic-bezier(0.16, 1, 0.3, 1) both; }
.svc-slide.revealed .svc-slide__lines li:nth-child(1) { animation-delay: 0.30s; }
.svc-slide.revealed .svc-slide__lines li:nth-child(2) { animation-delay: 0.40s; }
.svc-slide.revealed .svc-slide__lines li:nth-child(3) { animation-delay: 0.50s; }
.svc-slide.revealed .svc-slide__lines li:nth-child(4) { animation-delay: 0.60s; }
.svc-slide.revealed .svc-slide__lines li:nth-child(5) { animation-delay: 0.70s; }
.svc-slide.revealed .svc-slide__lines li:nth-child(6) { animation-delay: 0.80s; }

@media (prefers-reduced-motion: reduce) {
  .svc-slide { position: relative; min-height: 0; }   /* no sticky stacking */
  .svc-slide__lines li { opacity: 1; }
  .svc-slide.revealed .svc-slide__lines li { animation: none; }
}
/* Narrow screens: a panel can be taller than the viewport, and a sticky element
   taller than its pin window strands its own bottom off-screen — so drop the
   stacking here and let the panels flow. The lateral slide-in still runs. */
@media (max-width: 720px) {
  .svc-slide { position: relative; min-height: 0; }
  .svc-slide--dark { border-top: 1px solid #17171b; }
  .svc-slide__title { max-width: none; }
}

/* ============ MAGIC SCROLL (diagonal) ============
   The black & white photo copy and its four photos hang off one diagonal
   rail. The stage pins to the viewport for the length of the (tall) section
   and script.js maps scroll progress through it onto --focus: the rail slot
   currently sitting in the middle of the screen. Slot --i is placed --i steps
   down-and-right of centre and the rail is pulled back by --focus steps, so an
   item's on-screen position is (--i - --focus) steps along the diagonal —
   items arrive from the bottom-right and leave through the top-left. Scale and
   opacity are set per item by script.js from its distance to the centre.

   The base rules below are the no-JS / reduced-motion layout: an ordinary
   centred column. script.js adds .magic-on to the section to switch the
   pinning and the diagonal on, so the rail can never strand the copy
   off-screen if the script doesn't run. */
.magic-scroll {
  position: relative;
  background: #0a0a0c;
  padding: 220px 40px;
}
.magic-scroll__rail {
  max-width: 1400px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10vh;
}
.magic-item--text {
  font-size: clamp(28px, 5.4vw, 96px);
  line-height: 1.0;
  letter-spacing: -0.03em;
  font-weight: 700;
  text-align: center;
  color: #f2f2f4;
  margin: 0;
  max-width: 760px;
}
.magic-item--photo {
  width: clamp(220px, 24vw, 380px);
  aspect-ratio: 3 / 4;
}

/* Pinned diagonal mode (JS only). --step-x/--step-y are one slot's worth of
   travel; the section is tall enough for every slot to cross the screen. */
.magic-scroll.magic-on {
  --step-x: clamp(90px, 17vw, 260px);
  --step-y: 46vh;
  height: 520vh;
  padding: 0;
}
.magic-scroll.magic-on .magic-scroll__stage {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
}
.magic-scroll.magic-on .magic-scroll__rail {
  position: absolute;
  inset: 0;
  max-width: none;
  display: block;
  gap: 0;
  transform: translate3d(
    calc(var(--focus, 0) * var(--step-x) * -1),
    calc(var(--focus, 0) * var(--step-y) * -1),
    0
  );
  will-change: transform;
}
.magic-scroll.magic-on .magic-item {
  position: absolute;
  left: 50%;
  top: 50%;
  transform:
    translate(-50%, -50%)
    translate(calc(var(--i) * var(--step-x)), calc(var(--i) * var(--step-y)))
    scale(var(--s, 1));
  opacity: var(--o, 1);
  will-change: transform, opacity;
}
/* Absolute positioning would otherwise shrink-to-fit against the right half of
   the stage only, so the lines need an explicit width to stay centred. */
.magic-scroll.magic-on .magic-item--text {
  width: min(760px, 82vw);
}

/* Sparkle color-reveal: the color "sparkle" photo sits underneath, and the
   black & white photo on top is masked with a soft circle that follows the
   cursor (--mx/--my, set by script.js on pointermove — same technique as
   the button spotlight), so hovering reveals the color version through a
   hole in the grayscale layer instead of the other way around. */
.photo-spotlight {
  position: relative;
  overflow: hidden;
}
.photo-color-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.photo-bw-mask {
  position: relative;
  z-index: 1;
  mask-image: radial-gradient(circle 120px at var(--mx, -9999px) var(--my, -9999px), transparent 0%, transparent 55%, black 85%);
  -webkit-mask-image: radial-gradient(circle 120px at var(--mx, -9999px) var(--my, -9999px), transparent 0%, transparent 55%, black 85%);
}

/* Burger nav: .nav-item links reuse .btn-outline/.btn-nav-cta for their
   resting text color so they match the rest of the site's buttons, but
   their background/border and their :hover/:focus-visible border-color/
   color change and spotlight glow (::before, from the shared button rule)
   are switched off below — no box/panel behind the group at all, just the
   burger + bare text links, so the only hover feedback on an individual
   item is the slide-replace text. Items are collapsed to nothing
   (scaleX(0) from the right edge, clipped by <nav>'s own overflow: hidden)
   until #siteHeader gets .nav-open (toggled by script.js), fixed-width and
   packed edge-to-edge (gap: 0 on the <nav>) so they read as one continuous
   row once expanded. */
.nav-item {
  height: 64px;
  display: flex;
  align-items: center;
  justify-content: center;
  white-space: nowrap;
  background: transparent;
  border: none;
  transform: scaleX(0);
  transform-origin: right center;
  opacity: 0;
  pointer-events: none;
  transition: transform 0.45s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.3s ease;
}
#siteHeader.nav-open .nav-item {
  transform: scaleX(1);
  opacity: 1;
  pointer-events: auto;
}
.nav-item:hover,
.nav-item:focus-visible {
  background: transparent;
  border: none;
  color: #f2f2f4;
}
.nav-item:hover::before,
.nav-item:focus-visible::before {
  opacity: 0;
}

/* Slide-replace hover: two identical .nav-slide-label spans, absolutely
   positioned to fill the item. The original sits in place at rest; the
   duplicate waits just off the left edge. On hover both slide right: the
   original exits past the right edge, the duplicate slides in from the
   left to take its exact place. Absolute positioning (rather than a wide
   flex track sized in %) avoids sizing the label against .nav-item's own
   indeterminate width, which was cutting text off. */
.nav-slide-label {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform 0.22s ease-in-out;
}
.nav-slide-duplicate {
  transform: translateX(-100%);
}
.nav-item:hover .nav-slide-original,
.nav-item:focus-visible .nav-slide-original {
  transform: translateX(100%);
}
.nav-item:hover .nav-slide-duplicate,
.nav-item:focus-visible .nav-slide-duplicate {
  transform: translateX(0%);
}

.burger-line {
  width: 26px;
  height: 2px;
  background: #f2f2f4;
  display: block;
  transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
}
#siteHeader.nav-open .burger-line:nth-child(1) { transform: translateY(8px) rotate(45deg); }
#siteHeader.nav-open .burger-line:nth-child(2) { opacity: 0; }
#siteHeader.nav-open .burger-line:nth-child(3) { transform: translateY(-8px) rotate(-45deg); }
