/* ==========================================================================
   Sections — the twelve scenes
   ========================================================================== */

/* --------------------------------------------------------------------------
   02 · Hero
   -------------------------------------------------------------------------- */
.hero {
  position: relative;
  min-height: 100svh;
  display: flex;
  align-items: center;
  padding-top: calc(var(--nav-h) + var(--sp-6));
  padding-bottom: var(--sp-9);
  background: var(--grad-hero);
  color: var(--on-dark);
  overflow: clip;
  /* Gives .hero__inner a 3D context so its scroll dolly (translateZ) reads as
     depth rather than a flat fade. .hero__stage nests its own for the layers. */
  perspective: 1200px;
}

/* ---- 3D stage ------------------------------------------------------------
   A single perspective wraps every background layer. Each layer declares its
   depth in px via data-depth; JS applies translateZ plus a compensating scale
   so the layer still fills the frame, then tilts the whole space toward the
   pointer. The result is genuine parallax between the footage, the glows and
   the copy — not a 2D fake.
   -------------------------------------------------------------------------- */
.hero__stage {
  position: absolute;
  inset: 0;
  z-index: 0;
  perspective: 1200px;
  perspective-origin: 50% 45%;
  overflow: hidden;
}

.hero__space {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  will-change: transform;
}

/* Composed transform: --py is the scroll parallax offset (GSAP), --z the depth,
   --zs the scale that cancels the size change translateZ would cause. Defaults
   make an untouched layer sit flat and correct, so no-JS is the neutral state. */
.hero__layer {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  backface-visibility: hidden;
  transform: translate3d(0, var(--py, 0px), var(--z, 0px)) scale(var(--zs, 1));
}

/* ---- Establishing shot ---------------------------------------------------- */
.hero__bg {
  overflow: hidden;
}

.hero__bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Footage is warm golden-hour; pull it toward the brand blue and drop the
     exposure so the headline stays legible over it. */
  filter: saturate(0.66) contrast(1.08) brightness(0.6) hue-rotate(-12deg);
  transform: scale(1.08);
}

/* ---- Floating panels ------------------------------------------------------ */
.hero__panels {
  pointer-events: none;
}

.hero-panel {
  position: absolute;
  margin: 0;
  border-radius: var(--r-lg);
  overflow: hidden;
  transform: translate3d(0, var(--py, 0px), var(--z, 0px)) scale(var(--zs, 1)) rotate(var(--rot, 0deg));
  box-shadow: 0 40px 80px -30px rgba(0, 0, 0, 0.75), 0 0 0 1px rgba(var(--rgb-white), 0.09);
  backface-visibility: hidden;
}

.hero-panel img {
  width: 100%;
  height: auto;
  display: block;
  filter: saturate(0.82) contrast(1.05) brightness(0.86);
  border: 1px solid;
}

/* A cool rim so panels read as glass-edged rather than pasted on. */
.hero-panel::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(160deg, rgba(var(--rgb-cyan), 0.14), transparent 45%, rgba(var(--rgb-ink), 0.45));
  pointer-events: none;
}

.hero-panel figcaption {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1;
  padding: var(--sp-5) var(--sp-4) var(--sp-4);
  font-size: var(--fs-caption);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--paper-50);
  background: linear-gradient(transparent, rgba(var(--rgb-ink), 0.85));
}

/* Cascading right-hand cluster. Depth order is set by data-depth in the
   markup; these are just the positions. */
.hero-panel--bathroom {
  top: 17%;
  right: 30%;
  width: 20vw;
  --rot: -3deg;
}

.hero-panel--kitchen {
  top: 31%;
  right: 12%;
  width: 24vw;
  --rot: 2deg;
}

/* Top of the cluster, clear of the team panel: now that that panel is a 4:3
   portrait crop rather than 16:9 it is taller, and at the old 67% this photo
   sat almost entirely behind it. */
.hero-panel--products {
  top: 13%;
  right: 35%;
  width: 14vw;
  --rot: -4deg;
}

/* The only frame with real Living Fresh branding — largest and frontmost. */
.hero-panel--cleaner {
  top: 53%;
  right: 14%;
  width: 30vw;
  --rot: 1.5deg;
}

/* The front photo is the one people actually look at, so it is shown as shot:
   no dimming filter and only a faint rim, where the back panels are toned down
   to read as depth. */
.hero-panel--cleaner img {
  filter: none;
}

.hero-panel--cleaner::after {
  background: linear-gradient(160deg, rgba(var(--rgb-cyan), 0.08), transparent 40%);
}

/* Cinematic vignette — pulls the eye to centre and protects the headline. */
.hero__vignette {
  background: radial-gradient(75% 75% at 50% 45%, transparent 35%, rgba(var(--rgb-ink), 0.55) 78%, rgba(var(--rgb-ink), 0.88) 100%);
  pointer-events: none;
}

/* Copy scrim — the footage is bright golden-hour interiors, and its highlights
   (windows, sunlight) can hit 200+ luminance right behind the headline. This
   left-weighted wedge keeps the text safely readable on any frame, and doubles
   as composition: dark on the left where the copy sits, clear on the right
   where the footage is worth seeing. */
.hero__copyscrim {
  pointer-events: none;
  background: linear-gradient(100deg, rgb(4 16 28 / 62%) 0%, rgb(4 16 28 / 63%) 32%, rgba(var(--rgb-ink), 0.42) 58%, rgba(var(--rgb-ink), 0.06) 80%, transparent 100%);
}

/* Anamorphic light bloom drifting across the frame. */
.hero__bloom {
  pointer-events: none;
  background: linear-gradient(102deg, transparent 34%, rgba(var(--rgb-cyan), 0.16) 47%, rgba(255, 255, 255, 0.1) 50%, rgba(var(--rgb-cyan), 0.16) 53%, transparent 66%);
  mix-blend-mode: screen;
  opacity: 0.75;
  animation: bloom-drift 19s var(--e-inout) infinite alternate;
}

@keyframes bloom-drift {
  from {
    transform: translate3d(-14%, 0, 0) rotate(-1deg);
  }
  to {
    transform: translate3d(14%, 0, 0) rotate(1deg);
  }
}

/* Split layout: copy on the left, panel cluster on the right.
   Two classes, so this beats `.hero h1 { max-width: 16ch }` on specificity —
   without that the headline box extends past the panels and the two overlap.

   The headline must also come down in size. --fs-display-xl tops out at 8vw,
   which is ~102px at 1240 — in a 52% column that puts every single word on its
   own line and pushes the hero past 1100px tall. This scale keeps it to three
   lines with two words on most of them. */
@media (min-width: 1080px) {
  .hero .hero__inner > * {
    max-width: min(100%, 50%);
  }

  .hero h1 {
    font-size: clamp(3rem, 5.4vw, 4rem);
    max-width: min(100%, 50%);
  }

  /* The half-width column changes the vertical budget. At 56px the three trust
     items need 622px in a 592px row and wrap to three lines — 196px of height
     that pushes the CTA below the fold on a 840px-tall laptop. Tightening the
     gap and the block spacing keeps the whole hero inside one viewport. */
  /* Note the `.hero` prefix on both: the base .hero__trust / .hero__cta rules
     are declared later in this file, so at equal specificity they would win. */
  /* The trust strip sits at the bottom of the column, below where the panel
     cluster reaches, so it can run wider than the rest of the copy. It needs
     ~574px for one line; a straight 50% column is only 506px at 1100. */
  .hero .hero__trust {
    gap: clamp(1.25rem, 2.2vw, 2rem);
    max-width: min(100%, 58%);
  }

  .hero .hero__cta {
    margin-bottom: var(--sp-6);
  }

  .hero {
    padding-bottom: var(--sp-7);
  }
}

/* Below 1080px the copy runs full width, so any panel placement collides with
   the headline. The cluster is a wide-viewport flourish — drop it entirely and
   let the establishing shot carry the hero. The team photo still appears in
   the gallery, so nothing is lost from the page. */
@media (max-width: 1079px) {
  .hero__panels {
    display: none;
  }
}

.hero__orb-1 {
  inset: auto; /* .hero__layer sets inset:0; orbs position themselves */
  width: 46vw;
  height: 46vw;
  max-width: 640px;
  max-height: 640px;
  top: -12%;
  right: -6%;
  opacity: 0.5;
  animation: drift-a 38s var(--e-inout) infinite alternate;
}

.hero__orb-2 {
  inset: auto;
  width: 34vw;
  height: 34vw;
  max-width: 460px;
  max-height: 460px;
  bottom: -14%;
  left: -8%;
  opacity: 0.35;
  background: radial-gradient(closest-side, rgba(var(--rgb-mint), 0.45), transparent 70%);
  animation: drift-b 46s var(--e-inout) infinite alternate;
}

@keyframes drift-a {
  to {
    transform: translate3d(-7%, 9%, 0) scale(1.14);
  }
}

@keyframes drift-b {
  to {
    transform: translate3d(9%, -8%, 0) scale(1.2);
  }
}

.hero__inner {
  position: relative;
  z-index: 4;
  width: 100%;
  transform-style: preserve-3d;
}

/* The copy is a parallax layer too — slightly faster than the page, so it
   lifts away ahead of the imagery. Gated on .is-3d so the no-JS and
   reduced-motion states keep it perfectly flat. */
.hero.is-3d .hero__inner {
  transform: translate3d(0, var(--py, 0px), var(--z, 0px)) scale(var(--zs, 1));
  will-change: transform, opacity;
}

.hero__eyebrow {
  color: var(--cyan-300);
  margin-bottom: var(--sp-5);
}

/* Solid white is the safe default and the no-JS state. The gradient is applied
   per-word by animations.js — see applyHeadlineGradient() for why it can't
   live on the h1 itself. If that ever fails, the headline is still readable. */
.hero h1 {
  max-width: 16ch;
  margin-bottom: var(--sp-5);
  color: var(--paper-50);
}

/* Word-by-word masked reveal: each word sits in an overflow-hidden box */
.word {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  padding-bottom: 0.08em;
}

.word > span {
  display: inline-block;
}

.hero__sub {
  font-size: var(--fs-body-lg);
  font-weight: 500;
  color: var(--cyan-300);
  margin-bottom: var(--sp-3);
}

.hero__lede {
  max-width: 52ch;
  color: var(--on-dark);
  opacity: 0.86;
  margin-bottom: var(--sp-6);
}

.hero__cta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
  margin-bottom: var(--sp-8);
}

.hero__trust {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(1.5rem, 4vw, 3.5rem);
  padding-top: var(--sp-5);
  border-top: 1px solid rgba(var(--rgb-white), 0.12);
  max-width: 720px;
  padding: 30px;
  border-radius: 15px;
  background: radial-gradient(closest-side, rgba(var(--rgb-cyan), 0.55), #204b71e0 70%);
  
}

.trust-item {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.trust-item b {
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 2.4vw, 2.1rem);
  font-weight: 700;
  color: var(--paper-50);
  line-height: 1;
}

.trust-item > span {
  /* Direct child only: a descendant selector also matches the counter span
     inside <b>, shrinking "13" to caption size beside a full-size "+". */
  font-size: var(--fs-caption);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--on-dark-muted);
}

/* --------------------------------------------------------------------------
   03 · Intro
   -------------------------------------------------------------------------- */
.intro__grid {
  display: grid;
  grid-template-columns: 1.05fr 1fr;
  gap: clamp(2rem, 6vw, 5.5rem);
  align-items: center;
}

.intro__media {
  position: relative;
  min-height: clamp(360px, 46vw, 620px);
}

/* Oversized ghosted "13" sitting behind the image pair */
.intro__ghost {
  position: absolute;
  left: -4%;
  top: 50%;
  transform: translateY(-50%);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(11rem, 26vw, 26rem);
  line-height: 0.8;
  color: rgba(var(--rgb-blue), 0.06);
  pointer-events: none;
  user-select: none;
  z-index: 0;
}

.intro__img {
  position: absolute;
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--shadow-lg);
  z-index: 1;
}

.intro__img img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: saturate(0.9) contrast(1.04);
}

.intro__img--a {
  width: 62%;
  aspect-ratio: 4 / 5;
  left: 0;
  top: 0;
}

.intro__img--b {
  width: 52%;
  aspect-ratio: 1 / 1;
  right: 0;
  bottom: 0;
  border: 6px solid var(--paper-50);
}

.intro__badge {
  position: absolute;
  left: 44%;
  top: 8%;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: 0.7rem 1.1rem;
  border-radius: var(--r-pill);
  background: var(--paper-50);
  box-shadow: var(--shadow-md);
  font-size: var(--fs-caption);
  font-weight: 700;
  color: var(--slate-700);
  white-space: nowrap;
}

.intro__badge i {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--mint-400);
  box-shadow: 0 0 0 4px rgba(var(--rgb-mint), 0.22);
}
@media(max-width:1500px){
h2.display-lg {
    font-size:3.4rem;
}
}
.intro__copy h2 {
  margin-bottom: var(--sp-5);
}

.intro__copy p + p {
  margin-top: var(--sp-4);
}

.intro__highlight {
  display: block;
  margin-top: var(--sp-5);
  padding: var(--sp-4) var(--sp-5);
  border-left: 3px solid var(--blue-500);
  background: var(--paper-100);
  border-radius: 0 var(--r-md) var(--r-md) 0;
  font-weight: 500;
  color: var(--slate-700);
}

.intro__list {
  display: grid;
  gap: var(--sp-3);
  margin-top: var(--sp-5);
}

.intro__list li {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  font-size: 0.98rem;
}

.intro__list svg {
  flex: none;
  width: 20px;
  height: 20px;
  margin-top: 3px;
  color: var(--blue-500);
}

.intro__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-4);
  margin-top: var(--sp-6);
}

/* --------------------------------------------------------------------------
   04 · Services — card grid on a tinted light band

   Was a GSAP-pinned horizontal track. A pinned section is the most fragile
   thing on a page: it writes a spacer into layout, has to be measured before
   images settle, and competes with every other trigger for refresh order.
   A plain grid has none of that and shows all six services at once.
   -------------------------------------------------------------------------- */
.services {
  position: relative;
  /* Dark band, as before. It sits between band-paper (#fff) and
     band-paper-100 (#f4f8fc), so it is the section that keeps the page's
     light/dark rhythm from flattening into three pale bands in a row.
     The cards stay white and simply pop off it. */
  background: var(--ink-800);
  color: var(--on-dark-muted);
  padding-block: var(--section-y);
  overflow: clip;
}

.services__head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--sp-6);
  padding-bottom: clamp(2rem, 4vw, 3.5rem);
}

.services__head h2 {
  max-width: 16ch;
  color: var(--on-dark);
}

.services__grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(1rem, 1.8vw, 1.75rem);
}

@media (max-width: 1180px) {
  .services__grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

@media (max-width: 900px) {
  .services__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 560px) {
  .services__grid {
    grid-template-columns: 1fr;
  }

  .services__head {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--sp-4);
  }
}

/* --------------------------------------------------------------------------
   05 · Working process — the light path
   -------------------------------------------------------------------------- */
.process {
  position: relative;
}

.process__grid {
  position: relative;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(1.5rem, 4vw, 3.5rem);
  margin-top: clamp(2rem, 4vw, 3rem);
}

.process__path {
  position: absolute;
  inset-inline: 8%;
  top: 3px;
  height: 90px;
  z-index: 0;
  pointer-events: none;
}

/* The SVG is stretched with preserveAspectRatio="none", so the horizontal and
   vertical scales differ and would squash the dots into ellipses at narrower
   widths. non-scaling-stroke renders the stroke in screen units instead, which
   keeps every dot round and evenly spaced at any container size. */
.process__path path {
  fill: none;
  stroke-width: 3;
  stroke-linecap: round;
  stroke-dasharray: 0.1 11; /* near-zero dash + round cap = a dot */
  vector-effect: non-scaling-stroke;
}

.process__path .track {
  stroke: var(--paper-300);
}

/* Dots travel left to right along the path. One full cycle is the dash
   period (0.1 + 11), so the loop is seamless. */
.process__path .draw {
  stroke: url(#processGrad);
  animation: dot-flow 1.1s linear infinite;
}

@keyframes dot-flow {
  to {
    stroke-dashoffset: -11.1;
  }
}

@media (prefers-reduced-motion: reduce) {
  .process__path .draw {
    animation: none;
  }
}

.process__step {
  position: relative;
  z-index: 1;
  text-align: center;
}

.process__node {
  position: relative;
  display: grid;
  place-items: center;
  width: 92px;
  height: 92px;
  margin: 0 auto var(--sp-5);
  border-radius: 50%;
  background: var(--paper-50);
  border: 1px solid var(--paper-200);
  box-shadow: var(--shadow-md);
  color: var(--blue-500);
}

.process__node svg {
  width: 32px;
  height: 32px;
}

.process__node::after {
  content: "";
  position: absolute;
  inset: -8px;
  border-radius: 50%;
  border: 2px solid var(--mint-400);
  opacity: 0;
  transform: scale(0.85);
}

.process__step.is-lit .process__node::after {
  animation: ring 1.1s var(--e-out) forwards;
}

@keyframes ring {
  0% {
    opacity: 0.85;
    transform: scale(0.85);
  }
  100% {
    opacity: 0;
    transform: scale(1.35);
  }
}

/* ---- Node ambience -------------------------------------------------------
   Three layers, all decorative, all independent of the dotted path:

   .process__node::before   a dashed halo ring rotating OUTSIDE the disc. This
                            is the layer you actually notice. Being outside the
                            white circle, it cannot affect icon contrast at all,
                            so it can be as visible as it likes.
   .process__aura::before   a soft light sweeping around INSIDE the disc. Uses
                            cyan/mint only — both are high-luminance, so the
                            wash stays pale and the dark blue icon keeps its
                            contrast. Never blue, which would darken it.
   .process__aura::after    one slow drifting blob, for a bit of organic depth.

   Steps are offset with negative delays so the row never pulses in unison.
   All of it stops under prefers-reduced-motion.
   -------------------------------------------------------------------------- */
.process__node {
  transition: transform var(--d-mid) var(--e-out), box-shadow var(--d-mid) var(--e-out);
}

/* Beacon pulse. Replaces the rotating dashed halo, which read as the dotted
   path spinning around the disc — same motif twice. A ring that expands and
   fades is a different gesture entirely.

   The cycle is 2.8s (~0.36Hz), far below the 3Hz flash threshold, so it reads
   as a slow blink rather than anything strobing. */
.process__node::before {
  content: "";
  position: absolute;
  inset: -4px;
  border-radius: 50%;
  border: 2px solid var(--cyan-400);
  opacity: 0;
  animation: node-ping 2.8s var(--e-out) infinite;
}

@keyframes node-ping {
  0% {
    transform: scale(0.96);
    opacity: 0;
  }
  18% {
    opacity: 0.75;
  }
  100% {
    transform: scale(1.42);
    opacity: 0;
  }
}

/* Inner glow, blinking in sync with the ring. Still ring-masked so the light
   sits around the icon and the centre of the disc stays near-white. */
.process__aura {
  position: absolute;
  inset: 1px;
  border-radius: 50%;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}

.process__aura::before {
  content: "";
  position: absolute;
  inset: 6%;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(var(--rgb-cyan), 0.55), transparent 74%);
  filter: blur(10px);
  -webkit-mask: radial-gradient(closest-side, transparent 40%, #000 72%);
  mask: radial-gradient(closest-side, transparent 40%, #000 72%);
  opacity: 0.25;
  animation: node-glow 2.8s var(--e-inout) infinite;
}

@keyframes node-glow {
  0%,
  100% {
    opacity: 0.22;
  }
  22% {
    opacity: 0.85;
  }
}

/* Slow drifting blob underneath the sweep. */
.process__aura::after {
  content: "";
  position: absolute;
  right: -14%;
  bottom: -16%;
  width: 76%;
  height: 76%;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(var(--rgb-cyan), 0.32), transparent 70%);
  filter: blur(12px);
  animation: aura-drift 17s var(--e-inout) infinite alternate;
}

@keyframes aura-drift {
  to {
    transform: translate(-30%, -24%) scale(1.18);
  }
}

/* Icon rides above both layers and breathes very slightly. */
.process__node svg {
  position: relative;
  z-index: 1;
  animation: node-breathe 2.8s var(--e-inout) infinite;
}

@keyframes node-breathe {
  0%,
  100% {
    transform: scale(1);
  }
  22% {
    transform: scale(1.07);
  }
}

/* Offsets so the three steps never move together. :nth-of-type, not
   :nth-child — the .process__path <svg> is the grid's first child and would
   shift every index by one. */
.process__step:nth-of-type(2) .process__node::before {
  animation-delay: 0.9s;
}
.process__step:nth-of-type(3) .process__node::before {
  animation-delay: 1.8s;
}
.process__step:nth-of-type(2) .process__aura::before {
  animation-delay: 0.9s;
}
.process__step:nth-of-type(3) .process__aura::before {
  animation-delay: 1.8s;
}
.process__step:nth-of-type(2) .process__aura::after {
  animation-delay: -5s;
}
.process__step:nth-of-type(3) .process__aura::after {
  animation-delay: -10s;
}
.process__step:nth-of-type(2) .process__node svg {
  animation-delay: 0.9s;
}
.process__step:nth-of-type(3) .process__node svg {
  animation-delay: 1.8s;
}

/* Hover: lift the node and brighten the halo. */
.process__step:hover .process__node {
  transform: translateY(-4px);
  box-shadow: var(--shadow-lg);
}

.process__step:hover .process__node::before {
  border-color: var(--mint-400);
}

@media (prefers-reduced-motion: reduce) {
  .process__node::before,
  .process__aura::before,
  .process__aura::after,
  .process__node svg {
    animation: none;
  }
}


.process__num {
  display: block;
  font-family: var(--font-display);
  font-size: var(--fs-caption);
  letter-spacing: 0.18em;
  color: var(--blue-400);
  margin-bottom: var(--sp-2);
}

.process__step h3 {
  font-size: var(--fs-heading-sm);
  margin-bottom: var(--sp-3);
}

.process__step p {
  max-width: 34ch;
  margin-inline: auto;
  font-size: 0.95rem;
}

@media (max-width: 860px) {
  .process__grid {
    grid-template-columns: 1fr;
  }
  .process__path {
    display: none;
  }
}

/* --------------------------------------------------------------------------
   06 · Why choose us — sticky scroll-scale reveal

   Borrowed from the sticky-video-scale pattern: a tall scroll region with a
   sticky stage inside it. As you scroll, an image frame opens from a small
   card to full bleed, and the four reasons surface on top of it — an aperture
   letting light into a clean room, which is the page's whole concept.

   The scale is driven by scroll progress, not time, so the user controls it.
   -------------------------------------------------------------------------- */
.why {
  /* This section drops the .section class, so it loses that rule's position and
     clipping. Both are needed: without `position: relative` the absolutely
     positioned .sweep escapes to an outer containing block and the clip below
     never applies to it. Clip the x axis only — `overflow: hidden` here would
     make .why a scroll container and break the sticky stage. */
  position: relative;
  padding-block: 0;
  overflow-x: clip;
}

/* Height is applied by JS (.is-scaling) so the no-JS and reduced-motion states
   stay an ordinary-length section rather than 260vh of dead space. */
.why__scroll {
  position: relative;
}

.why.is-scaling .why__scroll {
  height: 260vh;
}

.why__stage {
  display: grid;
  place-items: center;
  padding-block: var(--section-y);
}

.why.is-scaling .why__stage {
  position: sticky;
  top: 0;
  height: 100svh;
  padding-block: 0;
  overflow: hidden;
}

/* ---- The aperture --------------------------------------------------------- */
.why__frame {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  border-radius: var(--r-xl);
  /* --open: 0 = small card, 1 = full bleed. JS drives it; the default of 1
     means a static section still looks finished. */
  transform: scale(calc(0.46 + 0.54 * var(--open, 1)));
  will-change: transform;
}

/* Without the effect the frame is just a background layer. */
.why:not(.is-scaling) .why__frame {
  display: none;
}

.why__frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: saturate(calc(0.4 + 0.42 * var(--open, 1)))
          brightness(calc(0.34 + 0.26 * var(--open, 1)))
          contrast(1.06);
}

.why__frame-scrim {
  position: absolute;
  inset: 0;
  background: radial-gradient(78% 78% at 50% 42%, transparent 30%, rgba(var(--rgb-ink), 0.6) 76%, rgba(var(--rgb-ink), 0.92) 100%),
    linear-gradient(180deg, rgba(var(--rgb-ink), 0.55), rgba(var(--rgb-ink), 0.2) 40%, rgba(var(--rgb-ink), 0.85));
}

/* ---- Content on top -------------------------------------------------------- */
.why__content {
  position: relative;
  z-index: 1;
  width: 100%;
}

.why__head {
  margin-bottom: clamp(1.75rem, 3.5vw, 3rem);
}

.why__grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: clamp(1rem, 2vw, 1.75rem);
}

/* Tiles rise in sequence once the frame is open. --lift is set per tile by JS. */
.why.is-scaling .why__tile {
  opacity: var(--lift, 1);
  transform: translateY(calc((1 - var(--lift, 1)) * 34px));
  will-change: transform, opacity;
}

/* Over a photograph the tiles need real glass, not a faint wash. */
.why.is-scaling .why__tile {
  background: linear-gradient(160deg, rgba(var(--rgb-ink), 0.72), rgba(var(--rgb-ink), 0.58));
  backdrop-filter: blur(14px) saturate(130%);
  border-color: rgba(var(--rgb-white), 0.14);
}

/* At four across the tiles get tight; drop the body copy to two lines of room. */
@media (min-width: 1200px) {
  .why__grid {
    grid-template-columns: repeat(4, 1fr);
  }

  .why.is-scaling .why__tile {
    padding: clamp(1.25rem, 1.8vw, 1.75rem);
  }

  .why.is-scaling .why__tile h3 {
    font-size: 1.05rem;
  }

  .why.is-scaling .why__tile p {
    font-size: 0.88rem;
  }

  .why.is-scaling .glow-tile__icon {
    width: 46px;
    height: 46px;
    margin-bottom: var(--sp-4);
  }
}

@media (max-width: 760px) {
  .why__grid {
    grid-template-columns: 1fr;
  }
}

/* Scroll-linked text-mask reveal: grey fills to white */
.mask-fill {
  background-image: linear-gradient(90deg, var(--paper-50) 50%, rgba(var(--rgb-white), 0.22) 50%);
  background-size: 200% 100%;
  /* Filled is the rest state, so without JS the heading is solid white rather
     than 22% grey. The tween starts it at 100% and scrubs back to 0. */
  background-position: 0 0;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* --------------------------------------------------------------------------
   07 · Before / after
   -------------------------------------------------------------------------- */
.ba {
  position: relative;
  background: var(--ink-900);
  color: var(--on-dark);
}

.ba__stage {
  position: relative;
  /* The pairs are portrait phone photos (480x528 after trimming their baked-in
     labels), so the stage takes their shape instead of cropping to 16:9. */
  aspect-ratio: 480 / 528;
  width: 100%;
  max-width: 480px;
  justify-self: end;
  border-radius: var(--r-xl);
  overflow: hidden;
  box-shadow: var(--shadow-lg);
  touch-action: pan-y;
  user-select: none;
  cursor: ew-resize;
}

.ba__layer {
  position: absolute;
  inset: 0;
}

.ba__layer img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Both layers are real photographs now, so neither is graded: the "before"
   layer is simply clipped to the left of the handle. */
.ba__before {
  clip-path: inset(0 calc(100% - var(--split, 50%)) 0 0);
}

.ba__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 480px);
  gap: clamp(2rem, 6vw, 5rem);
  align-items: center;
}

.ba__lede {
  max-width: 44ch;
  margin-bottom: var(--sp-6);
  color: var(--on-dark-muted);
}

.ba__picker {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-3);
  max-width: 460px;
}

.ba__pick {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: 0.5rem;
  border-radius: var(--r-md, 14px);
  border: 1px solid rgba(var(--rgb-white), 0.12);
  background: rgba(var(--rgb-white), 0.04);
  color: var(--on-dark);
  font-weight: 600;
  text-align: left;
  transition: border-color var(--d-fast) var(--e-out), background var(--d-fast) var(--e-out);
}

.ba__pick img {
  width: 48px;
  height: 48px;
  flex: none;
  object-fit: cover;
  border-radius: 10px;
}

.ba__pick:hover {
  border-color: rgba(var(--rgb-white), 0.3);
}

.ba__pick[aria-pressed="true"] {
  border-color: var(--cyan-400);
  background: rgba(var(--rgb-cyan), 0.12);
  color: var(--paper-50);
}

@media (max-width: 900px) {
  .ba__grid {
    grid-template-columns: 1fr;
  }

  .ba__stage {
    justify-self: center;
  }

  .ba__picker {
    max-width: none;
  }
}

.ba__tag {
  position: absolute;
  top: var(--sp-5);
  z-index: 3;
  padding: 0.4rem 0.9rem;
  border-radius: var(--r-pill);
  font-size: var(--fs-caption);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  backdrop-filter: blur(8px);
}

.ba__tag--before {
  left: var(--sp-5);
  background: rgba(var(--rgb-ink), 0.6);
  color: var(--paper-50);
  border: 1px solid rgba(var(--rgb-white), 0.2);
}

.ba__tag--after {
  right: var(--sp-5);
  background: rgba(var(--rgb-mint), 0.16);
  color: var(--mint-400);
  border: 1px solid rgba(var(--rgb-mint), 0.4);
}

.ba__handle {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--split, 50%);
  z-index: 4;
  width: 2px;
  background: var(--cyan-300);
  box-shadow: 0 0 22px rgba(var(--rgb-cyan), 0.8);
  transform: translateX(-1px);
}

.ba__grip {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: rgba(var(--rgb-ink), 0.7);
  border: 1px solid var(--cyan-300);
  color: var(--cyan-300);
  backdrop-filter: blur(10px);
  cursor: ew-resize;
  transition: transform var(--d-mid) var(--e-spring), box-shadow var(--d-mid) var(--e-out);
}

.ba__grip:hover,
.ba__stage:focus-within .ba__grip {
  transform: translate(-50%, -50%) scale(1.1);
  box-shadow: var(--glow-cyan);
}

.ba__grip svg {
  width: 22px;
  height: 22px;
}

.ba__caption {
  margin-top: var(--sp-4);
  font-size: var(--fs-caption);
  color: var(--on-dark-muted);
  text-align: center;
}

/* --------------------------------------------------------------------------
   08 · Stats band — lit stage

   Four figures standing in their own columns of light. The band is deliberately
   the darkest thing on the page so the light does the work: a shaft rises
   behind each figure, hairlines frame the band top and bottom, and a beam
   drifts across the whole width. No photograph here on purpose — this sits
   between two image-heavy sections and works better as a cut between them.
   -------------------------------------------------------------------------- */
.stats {
    position: relative;
    background: radial-gradient(120% 140% at 50% 120%, #0a2138 0%, #061627 45%, #24405d 100%);
    padding-block: clamp(2.5rem, 7vw, 0rem);
    overflow: clip;
}

/* Hairlines, brightest at centre — frames the band like a title card. */
.stats::before,
.stats::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(var(--rgb-cyan), 0.5) 50%, transparent);
  pointer-events: none;
  z-index: 2;
}

.stats::before {
  top: 0;
}

.stats::after {
  bottom: 0;
}

/* Ambient key light from below, as if the figures are lit from the floor. */
.stats__glow {
  position: absolute;
  inset-inline: -10%;
  bottom: -50%;
  height: 130%;
  background: radial-gradient(50% 50% at 50% 50%, rgba(var(--rgb-cyan), 0.09), transparent 70%);
  pointer-events: none;
}

/* Anamorphic beam drifting across the band. */
.stats__beam {
  position: absolute;
  inset-block: 0;
  width: 42%;
  background: linear-gradient(100deg, transparent, rgba(var(--rgb-white), 0.05) 45%, rgba(var(--rgb-cyan), 0.1) 50%, rgba(var(--rgb-white), 0.05) 55%, transparent);
  filter: blur(8px);
  pointer-events: none;
  animation: stats-beam 19s var(--e-inout) infinite alternate;
}

@keyframes stats-beam {
  from {
    transform: translateX(-40%);
  }
  to {
    transform: translateX(260%);
  }
}

.stats__grid {
  position: relative;
  z-index: 1;
  display: grid;
  /* minmax(0, 1fr): the default min-content floor lets the widest word
     ("Certified") stretch its column and knock the dividers off rhythm. */
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0;
  text-align: center;
}

.stat {
  position: relative;
  padding-inline: clamp(0.5rem, 1.6vw, 1.5rem);
  padding-block: var(--sp-4);
  transition: transform var(--d-mid) var(--e-out);
}

/* ---- The column of light -------------------------------------------------
   A shaft rising behind each figure. Widest and softest at the bottom, so it
   reads as a beam hitting the floor rather than a flat rectangle.
   -------------------------------------------------------------------------- */
/* Deliberately an <i>, not a <span>: `.stat > span` below styles the label,
   and a span here would match it — picking up position:relative and dropping
   into the flow as a 246px block that shoves the figure out of the band. */
.stat__beam {
  position: absolute;
  left: 50%;
  bottom: -30%;
  width: 78%;
  height: 190%;
  transform: translateX(-50%);
  background: linear-gradient(
    to top,
    rgba(var(--rgb-cyan), 0.13) 0%,
    rgba(var(--rgb-blue), 0.06) 38%,
    transparent 74%
  );
  -webkit-mask: radial-gradient(60% 70% at 50% 100%, #000 20%, transparent 75%);
  mask: radial-gradient(60% 70% at 50% 100%, #000 20%, transparent 75%);
  filter: blur(10px);
  opacity: 0.9;
  pointer-events: none;
  animation: shaft-breathe 7s var(--e-inout) infinite alternate;
}

@keyframes shaft-breathe {
  to {
    opacity: 0.45;
    transform: translateX(-50%) scaleY(1.08);
  }
}

/* Offset so the four shafts never pulse together. */
.stat:nth-child(2) .stat__beam {
  animation-delay: -1.8s;
}
.stat:nth-child(3) .stat__beam {
  animation-delay: -3.5s;
}
.stat:nth-child(4) .stat__beam {
  animation-delay: -5.2s;
}

/* Divider rules run the full height and fade at both ends, so they read as
   shafts of light between the columns rather than table borders. */
.stat + .stat::before {
  content: "";
  position: absolute;
  left: 0;
  top: 6%;
  bottom: 6%;
  width: 1px;
  background: linear-gradient(180deg, transparent, rgba(var(--rgb-cyan), 0.3) 50%, transparent);
}

.stat:hover {
  transform: translateY(-4px);
}

.stat:hover .stat__beam {
  opacity: 1;
  animation-play-state: paused;
}

.stat b {
  position: relative;
  display: block;
  font-family: var(--font-display);
  /* Capped so the longest word stat fits one equal column without wrapping. */
  font-size: clamp(1.9rem, 3.6vw, 3.4rem);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.025em;
  /* Counters must not reflow the layout as digits change width. */
  font-variant-numeric: tabular-nums;
  /* Not --grad-text: its blue end drops to 2.3:1 over the lit shafts. This ramp
     stays light the whole way, so the darkest stop still clears 4.9:1. */
  background: linear-gradient(105deg, #ffffff 0%, #cdeeff 45%, #7dd9ff 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  /* A little bloom under the figure, as if it is emitting light. */
  filter: drop-shadow(0 6px 22px rgba(var(--rgb-cyan), 0.28));
}

/* Direct child only — a descendant selector also matches the counter span
   inside <b>, which shrank "13" to caption size and, with display:block,
   pushed the "+" onto its own line. */
.stat > span {
  position: relative;
  display: block;
  margin-top: var(--sp-4);
  font-size: var(--fs-caption);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: #b9cbda; /* brighter than --on-dark-muted: the shafts lift the bg */
  transition: color var(--d-mid) var(--e-out);
}

/* Small rule under the label, echoing the section hairlines. */
.stat > span::after {
  content: "";
  display: block;
  width: 22px;
  height: 1px;
  margin: var(--sp-4) auto 0;
  background: rgba(var(--rgb-cyan), 0.35);
  transition: width var(--d-mid) var(--e-out), background-color var(--d-mid) var(--e-out);
}

.stat:hover > span {
  color: var(--cyan-300);
}

.stat:hover > span::after {
  width: 44px;
  background: var(--cyan-300);
}

@media (max-width: 860px) {
  .stats__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    row-gap: clamp(2.5rem, 6vw, 3.5rem);
  }

  /* With two columns the divider belongs on the left of every second cell. */
  .stat + .stat::before {
    display: none;
  }

  .stat:nth-child(even)::before {
    content: "";
    /* The rule above hides every divider; this one must switch it back on.
       Same specificity, so declaring `content` alone would not have. */
    display: block;
    position: absolute;
    left: 0;
    top: 6%;
    bottom: 6%;
    width: 1px;
    background: linear-gradient(180deg, transparent, rgba(var(--rgb-cyan), 0.3) 50%, transparent);
  }
}

@media (max-width: 480px) {
  .stats__grid {
    grid-template-columns: 1fr;
  }

  .stat:nth-child(even)::before {
    display: none;
  }

  .stat__beam {
    width: 60%;
  }
}

@media (prefers-reduced-motion: reduce) {
  .stats__beam,
  .stat__beam {
    animation: none;
  }
}

/* --------------------------------------------------------------------------
   09 · Testimonials
   -------------------------------------------------------------------------- */
.quotes {
  text-align: center;
}

/* ---- Interactive reveal background ---------------------------------------
   Two photographs of the same room — empty, and being cleaned. The lower one
   (reveal) is a plain <img>; the upper one (no-reveal) is painted into a canvas
   on top and erased away under the pointer, so wiping the background performs
   the service the section is about.

   Both images are exported at identical pixel dimensions and the canvas uses
   the same cover maths as object-fit: cover, so the two stay locked together
   at any viewport size.

   The section's own layout, cards and spacing are untouched — this sits behind
   .container, which is raised above it below.
   -------------------------------------------------------------------------- */
.reveal-bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}

/* Swap the OS cursor for the drawn one, but only where a pointer exists. */
@media (hover: hover) and (pointer: fine) {
  .quotes.has-reveal {
    cursor: none;
  }
}

.reveal-bg__under,
.reveal-bg__cover {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

.reveal-bg__under {
  object-fit: cover;
  object-position: center;
  /* The revealed photograph is untouched — full colour, full brightness. It is
     the payoff inside the circle, so nothing is laid over it. */
  filter: none;
}

/* The cover is a wrapper, not an image, so the mask cuts through the
   photograph and its white tint together. Tinting the section instead would
   also tint what the circle reveals. */
.reveal-bg__cover {
  --x: 50%;
  --y: 50%;
  --r: 0px;
  -webkit-mask-image: radial-gradient(circle var(--r) at var(--x) var(--y), transparent 0%, transparent 58%, #000 100%);
  mask-image: radial-gradient(circle var(--r) at var(--x) var(--y), transparent 0%, transparent 58%, #000 100%);
  will-change: mask-image;
}

.reveal-bg__cover img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  filter: saturate(0.28) contrast(0.92);
}

/* White wash over the covering photograph only — never over the revealed one.
   Measured sweep against the darkest 5% of the copy band: 0.70 leaves the
   customer names at 4.86:1, 0.76 at 5.36:1, 0.90 at 6.64:1. 0.76 is the
   lightest setting with real headroom, so the covering photograph stays as
   visible as legibility allows. */
.reveal-bg__tint {
  position: absolute;
  inset: 0;
  background: rgba(255, 255, 255, 0.76);
}

/* ---- Wiping-hand cursor --------------------------------------------------
   Desktop only: there is no cursor to replace on touch, and the finger already
   marks the spot. Sits above the wash so it stays legible over the photograph.
   -------------------------------------------------------------------------- */
.reveal-cursor {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 2;
  width: 54px;
  height: 54px;
  margin: -27px 0 0 -27px;
  color: var(--ink-800);
  pointer-events: none;
  opacity: 0;
  transform: translate3d(-100px, -100px, 0) scale(0.8);
  transition: opacity var(--d-mid) var(--e-out), scale var(--d-mid) var(--e-spring);
  filter: drop-shadow(0 3px 10px rgba(255, 255, 255, 0.9));
}

.reveal-cursor svg {
  width: 100%;
  height: 100%;
}

.reveal-bg.is-live .reveal-cursor {
  opacity: 1;
}

/* No pointer: no custom cursor, and the default one stays as it is. */
@media (hover: none), (pointer: coarse) {
  .reveal-cursor {
    display: none;
  }
}

/* Raise the existing content above the background. Nothing else about it
   changes — no new spacing, no new box. */
.quotes > .container {
  position: relative;
  z-index: 1;
}

/* The heading sits straight on the washed photograph, so it keeps a light halo
   to hold the letterforms together while the reveal circle passes under it.
   The testimonials themselves are on opaque cards and need none. */
.quotes .eyebrow,
.quotes h2,
.quotes__intro {
  text-shadow: 0 1px 3px rgba(255, 255, 255, 0.9), 0 2px 12px rgba(255, 255, 255, 0.85),
    0 0 26px rgba(255, 255, 255, 0.7);
}

.quotes__intro {
  max-width: 60ch;
  color: var(--slate-700);
}

/* ---- Slider --------------------------------------------------------------
   A native scroll-snap row: swipe, trackpad and keyboard scrolling come from
   the browser, and it still works with JS off. Each card snaps to the centre,
   and the track's inline padding lets the first and last card centre too, so
   every dot has a reachable position. */
.tslider {
  --slide-w: min(560px, 82vw);
  position: relative;
  /* Full bleed, so neighbouring cards run off the screen edge rather than
     being sliced at the container's edge. */
  margin-inline: calc(50% - 50vw);
}

/* position:relative makes the track the slides' offsetParent, which the
   centring maths in components.js relies on. */
.tslider__track {
  position: relative;
  display: flex;
  gap: clamp(1rem, 2.4vw, 1.75rem);
  margin: 0;
  padding: var(--sp-5) calc(50% - var(--slide-w) / 2) var(--sp-6);
  list-style: none;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
}

.tslider__track::-webkit-scrollbar {
  display: none;
}

.tslide {
  flex: 0 0 var(--slide-w);
  scroll-snap-align: center;
  display: flex;
  opacity: 0.45;
  transform: scale(0.94);
  transition: opacity var(--d-mid) var(--e-out), transform var(--d-mid) var(--e-out);
}

/* Without JS nothing marks the active slide, so show every card at full
   strength rather than dimming all of them. */
.tslider:not(.is-ready) .tslide,
.tslide.is-active {
  opacity: 1;
  transform: none;
}

.tcard {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-4);
  width: 100%;
  margin: 0;
  padding: clamp(1.5rem, 3.5vw, 2.5rem);
  text-align: left;
  background: rgba(255, 255, 255, 0.94);
  border: 1px solid rgba(var(--rgb-ink), 0.06);
  border-radius: var(--r-xl);
  box-shadow: 0 24px 50px -28px rgba(var(--rgb-ink), 0.45);
}

.tcard__mark {
  position: absolute;
  top: 0.1em;
  right: 0.35em;
  font-family: var(--font-display);
  font-size: clamp(5rem, 9vw, 7rem);
  line-height: 1;
  color: var(--blue-500);
  opacity: 0.12;
  pointer-events: none;
}

.tcard__text {
  margin: 0;
  flex: 1;
}

.tcard__text p {
  margin: 0;
  font-size: clamp(1rem, 1.5vw, 1.15rem);
  line-height: 1.65;
  color: var(--slate-700);
}

.tcard__who {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding-top: var(--sp-4);
  width: 100%;
  border-top: 1px solid rgba(var(--rgb-ink), 0.08);
}

.tcard__who img {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  flex: none;
}

.tcard__who span {
  display: flex;
  flex-direction: column;
}

.tcard__who cite {
  font-style: normal;
  font-weight: 700;
  color: var(--blue-600);
}

.tcard__who small {
  font-size: var(--fs-caption);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--slate-500, #64748b);
}

.stars {
  display: flex;
  gap: 4px;
  color: #fbbc04;
}

.stars svg {
  width: 18px;
  height: 18px;
}

.tslider__controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-5);
  margin-top: var(--sp-2);
}

.tslider__arrow {
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  color: var(--blue-600);
  background: var(--paper-50);
  box-shadow: 0 8px 22px -10px rgba(var(--rgb-ink), 0.45);
  transition: transform var(--d-fast) var(--e-out), background var(--d-fast) var(--e-out), color var(--d-fast) var(--e-out);
}

.tslider__arrow:hover {
  background: var(--blue-500);
  color: var(--paper-50);
}

.tslider__arrow svg {
  width: 20px;
  height: 20px;
}

.tslider__arrow--prev svg {
  transform: scaleX(-1);
}

.tslider__dots {
  display: flex;
  gap: 10px;
}

/* The visible dot is 10px, but the button is 24px so it stays tappable. */
.tslider__dot {
  position: relative;
  width: 24px;
  height: 24px;
  padding: 0;
}

.tslider__dot::before {
  content: "";
  position: absolute;
  inset: 7px;
  border-radius: 999px;
  background: rgba(var(--rgb-ink), 0.22);
  transition: inset var(--d-mid) var(--e-out), background var(--d-mid) var(--e-out);
}

.tslider__dot[aria-current="true"]::before {
  inset: 7px -4px;
  background: var(--blue-500);
}

@media (prefers-reduced-motion: reduce) {
  .tslide {
    transition: none;
  }
}

/* --------------------------------------------------------------------------
   10 · Founder
   -------------------------------------------------------------------------- */
.founder__grid {
  display: grid;
  grid-template-columns: 0.85fr 1.15fr;
  gap: clamp(2rem, 6vw, 5rem);
  align-items: center;
}

.founder__media {
  position: relative;
  isolation: isolate;
}

.founder__media img {
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  border-radius: var(--r-xl);
  filter: saturate(0.85) contrast(1.06) brightness(0.95);
}

/* Cyan rim light behind the portrait */
.founder__media::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: -8% -8% -8% 14%;
  border-radius: var(--r-xl);
  background: var(--grad-glow);
  opacity: 0.5;
  filter: blur(10px);
}

.founder__badge {
  position: absolute;
  right: -6%;
  bottom: 8%;
  padding: var(--sp-4) var(--sp-5);
  border-radius: var(--r-lg);
  background: rgba(var(--rgb-ink), 0.72);
  border: 1px solid rgba(var(--rgb-cyan), 0.32);
  backdrop-filter: blur(14px);
  box-shadow: var(--shadow-lg);
}

.founder__badge b {
  display: block;
  font-family: var(--font-display);
  font-size: 1.5rem;
  color: var(--paper-50);
}

.founder__badge span {
  font-size: var(--fs-caption);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--cyan-300);
}

.founder__copy p {
  color: var(--on-dark);
  opacity: 0.86;
}

.founder__copy p + p {
  margin-top: var(--sp-4);
}

.founder__sign {
  margin-top: var(--sp-6);
  padding-top: var(--sp-5);
  border-top: 1px solid rgba(var(--rgb-white), 0.12);
}

.founder__sign b {
  display: block;
  font-family: var(--font-display);
  font-size: 1.15rem;
  color: var(--paper-50);
}

.founder__sign span {
  font-size: var(--fs-caption);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--on-dark-muted);
}

/* --------------------------------------------------------------------------
   11 · Gallery
   -------------------------------------------------------------------------- */
.gallery__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(0.75rem, 1.6vw, 1.25rem);
}

.gallery__item {
  position: relative;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--ink-700);
}

.gallery__item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: saturate(0.78) contrast(1.06) brightness(0.85);
  transition: transform 0.9s var(--e-out), filter var(--d-mid) var(--e-out);
}

.gallery__item:hover img {
  transform: scale(1.07);
  filter: saturate(1) contrast(1.02) brightness(1);
}

/* Rows are sized by items 2/3 (row 1), 4 (row 2) and 6 (row 3). Items 1 and 5
   carry no aspect-ratio so they stretch to fill, leaving no gaps in the grid. */
.gallery__item:nth-child(2),
.gallery__item:nth-child(3) {
  aspect-ratio: 4 / 3;
}
.gallery__item:nth-child(4),
.gallery__item:nth-child(6) {
  grid-column: span 2;
  aspect-ratio: 16 / 7;
}
.gallery__item:nth-child(1) {
  grid-row: span 2;
}

.gallery__cap {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  padding: var(--sp-5);
  font-size: var(--fs-caption);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--paper-50);
  background: linear-gradient(transparent, rgba(var(--rgb-ink), 0.85));
  opacity: 0;
  transform: translateY(8px);
  transition: opacity var(--d-mid) var(--e-out), transform var(--d-mid) var(--e-out);
}

.gallery__item:hover .gallery__cap {
  opacity: 1;
  transform: none;
}

/* On touch there is no hover, so a caption revealed only on hover is content
   the visitor can never reach. Show them permanently wherever hover is absent. */
@media (hover: none), (max-width: 760px) {
  .gallery__cap {
    opacity: 1;
    transform: none;
  }
}

@media (max-width: 760px) {
  .gallery__grid {
    grid-template-columns: repeat(2, 1fr);
  }
  .gallery__item {
    aspect-ratio: 4 / 3;
  }
  .gallery__item:nth-child(1) {
    grid-row: auto;
  }
  .gallery__item:nth-child(4),
  .gallery__item:nth-child(6) {
    grid-column: span 2;
    aspect-ratio: 16 / 7;
  }
}

/* --------------------------------------------------------------------------
   12 · Quote CTA
   -------------------------------------------------------------------------- */
.cta {
  position: relative;
  background: var(--grad-cta);
  color: var(--paper-50);
  overflow: clip;
}

.cta__aurora {
  position: absolute;
  inset: -20%;
  background: radial-gradient(38% 44% at 22% 30%, rgba(var(--rgb-cyan), 0.4), transparent 65%),
    radial-gradient(36% 40% at 78% 66%, rgba(var(--rgb-mint), 0.3), transparent 65%),
    radial-gradient(50% 50% at 50% 100%, rgba(var(--rgb-ink), 0.75), transparent 70%);
  filter: blur(38px);
  animation: aurora 26s var(--e-inout) infinite alternate;
  pointer-events: none;
}

@keyframes aurora {
  to {
    transform: translate3d(4%, -5%, 0) scale(1.12);
  }
}

.cta__grid {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: 0.95fr 1.05fr;
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: center;
}

.cta h2 {
  font-size: var(--fs-display-md);
  text-transform: uppercase;
  margin-bottom: var(--sp-5);
}

.cta__lede {
  color: rgba(var(--rgb-white), 0.82);
  max-width: 44ch;
}

.cta__contacts {
  display: grid;
  gap: var(--sp-4);
  margin-top: var(--sp-7);
}

.cta__contact {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  transition: transform var(--d-fast) var(--e-out);
}

.cta__contact:hover {
  transform: translateX(4px);
}

.cta__contact i {
  display: grid;
  place-items: center;
  width: 46px;
  height: 46px;
  flex: none;
  border-radius: 50%;
  background: rgba(var(--rgb-white), 0.1);
  border: 1px solid rgba(var(--rgb-white), 0.2);
  color: var(--cyan-300);
}

.cta__contact i svg {
  width: 18px;
  height: 18px;
}

.cta__contact span {
  display: block;
  font-size: var(--fs-caption);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(var(--rgb-white), 0.62);
}

.cta__contact b {
  /* The label above is a block, uppercase and muted; the value has to leave
     that flow and reset all three, or it sets on the same line in the same
     small caps and reads as one run-on string ("CALL US0409 151 743"). */
  display: block;
  font-size: 1.02rem;
  font-weight: 700;
  letter-spacing: normal;
  text-transform: none;
  color: var(--paper-50);
  /* The email address is one unbreakable token; without this it sets a
     ~500px min-content width and blows the grid out of the viewport. */
  overflow-wrap: anywhere;
}

/* Grid items default to min-width:auto, which lets long content push past
   the container. Reset it wherever a grid holds free text. */
.cta__grid > *,
.intro__grid > *,
.founder__grid > *,
.why__grid > *,
.process__grid > * {
  min-width: 0;
}

/* --------------------------------------------------------------------------
   Shared responsive collapses
   -------------------------------------------------------------------------- */
@media (max-width: 960px) {
  .intro__grid,
  .founder__grid,
  .cta__grid {
    grid-template-columns: 1fr;
  }

  .founder__media {
    max-width: 460px;
  }

  .founder__badge {
    right: 4%;
  }

  .services__head {
    flex-direction: column;
    align-items: flex-start;
  }
}

@media (max-width: 560px) {
  .intro__media {
    min-height: 420px;
  }

  .intro__img--a {
    width: 72%;
  }

  .intro__img--b {
    width: 56%;
  }

  .intro__badge {
    left: auto;
    right: 0;
    top: 2%;
  }

}


/* ---- Google review badge (hero) ------------------------------------------
   First item in the hero copy, so the rating and a way into the reviews are
   visible before any scrolling, on every screen size. */
.review-badge {
  display: flex;
  width: fit-content;
  align-items: center;
  gap: 0.55rem;
  margin-bottom: var(--sp-5);
  padding: 0.5rem 1rem 0.5rem 0.65rem;
  border-radius: 999px;
  background: var(--paper-50);
  color: var(--ink-800);
  font-size: 0.9rem;
  box-shadow: 0 10px 30px -12px rgba(0, 0, 0, 0.6);
  transition: transform var(--d-fast) var(--e-out), box-shadow var(--d-fast) var(--e-out);
}

.review-badge:hover {
  transform: translateY(-2px);
  box-shadow: 0 14px 34px -12px rgba(0, 0, 0, 0.7);
}

.review-badge svg {
  flex: none;
}

.review-badge__g {
  width: 20px;
  height: 20px;
  color: var(--blue-500);
}

.review-badge__score {
  font-family: var(--font-display);
  font-size: 1.05rem;
  line-height: 1;
}

.review-badge__stars {
  display: inline-flex;
  gap: 1px;
  color: #fbbc04;
}

.review-badge__stars svg {
  width: 15px;
  height: 15px;
}

.review-badge__label {
  font-weight: 700;
  color: var(--blue-600);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.review-badge__arrow {
  width: 16px;
  height: 16px;
  color: var(--blue-600);
}

.review-badge__label {
  white-space: nowrap;
}

/* On a phone the full label wraps to two lines; the Google mark and the stars
   already say what it is. */
@media (max-width: 420px) {
  .review-badge {
    font-size: 0.85rem;
    gap: 0.45rem;
  }

  .review-badge__verb {
    display: none;
  }
}

/* ---- Heading spacing -------------------------------------------------------
   Every heading and display title: 0.1px tracking, 6px between words.
   ":root :is(...)" scores (0,2,0), so it outranks the narrower heading rules
   (.footer h3, .prose h2) without !important. Split headlines keep real
   spaces between their .word spans, so word-spacing still applies to them. */
:root :is(h1, h2, h3, h4, h5, h6, .display-xl, .display-lg, .display-md) {
  letter-spacing: 0.1px;
  word-spacing: 6px;
}

/* Losing the old -0.03em tracking widens "experienced" in the founder heading
   past its column on a phone (it already overhung by 10px; now ~29px). Scale
   that one heading with the viewport below 480px so the word fits. */
@media (max-width: 480px) {
  .founder__copy h2 {
    font-size: 10.5vw;
  }
}

/* ---- Mobile: quote form straight after the hero ---------------------------
   Holder section right under the hero. An inline script in index.html moves
   the existing #quoteForm in here below 768px and back into the Free Quote
   section above it; the heading and contact details stay at the bottom. The
   holder reuses .cta for the same blue ground, so the form looks the same. */
.quote-mobile[hidden] {
  display: none;
}

.quote-mobile {
  padding-block: clamp(2.5rem, 8vw, 3.5rem);
}

/* Above the .cta__aurora glow, as .cta__grid is in the Free Quote section.
   The "Get a Quote" heading picks up the .cta h2 style (uppercase, display-md). */
.quote-mobile > .container {
  position: relative;
  z-index: 1;
}