/* ═══════════════════════════════════════════════════════════════
   Cleanly — section styles, loaded by every page
   Loader · Nav · Hero · Services · Marquee · Benefits · Who We Service ·
   Clients · Testimonials · CTA · Footer · Site Menu · Mobile ·
   About · Before/After · Case Study · Contact
   ═══════════════════════════════════════════════════════════════ */

/* ─────────────────────────────────────────────────────────────
   LOADER — the homepage boot cover (Figma 1583:1057)
   ─────────────────────────────────────────────────────────────
   `display:none` at rest. It is shown only under html.js-loading, which the
   inline head script adds before first paint — the inverse of the opacity:0
   resting state base.css forbids. No script means no cover at all, and the
   homepage renders exactly as it did before this block existed.

   The choreography is five self-timed animations, not a chain of timers.
   Same reason initHeroIntro() uses an animation further down this file:
   rAF does not run in a hidden tab and background timers are throttled to
   about a second, either of which would shred a sequence this long. main.js
   owns only the three things CSS cannot — the scroll lock, the skip, and
   the teardown.

   Timeline, matching the designer's prototype exactly:
       0ms  white, office photo fades up      300ms
     301ms  tag springs down from above      1600ms
    1901ms  tag hangs                        1000ms
    2901ms  tag falls out of frame            800ms
    3702ms  nav slides down                   600ms
    4302ms  cover fades off the video         300ms
    4602ms  done
   ───────────────────────────────────────────────────────────── */
.loader{display:none}

html.js-loading{
  /* The designer's curve. NOT --ease-inout: the two look adjacent but both
     are steep S-curves, and the middle of a steep S is exactly where small
     control-point differences show. Scoped here so it evaporates with the
     class rather than joining the site's shared vocabulary in base.css. */
  --ease-brand:cubic-bezier(.77,.01,.19,1);

  /* lenis.stop() cannot land until DOMContentLoaded; this holds the scroll
     from the first paint, which is several hundred milliseconds earlier. */
  overflow:hidden;
  background:var(--white);          /* frame 1 is white, not --light-blue */
}

html.js-loading .loader{
  display:block;
  position:fixed; inset:0; z-index:100;
  background:var(--white);
  overflow:hidden;
  /* A touch drag no longer skips the cover, so it must not scroll or
     rubber-band the page behind it either. The nav and the mute button sit
     above the cover and keep their own touch handling. */
  touch-action:none;
  will-change:opacity;
  /* Nothing on the cover can be selected, and no image in it can be dragged,
     clicked through to, long-pressed or right-click-saved, per the
     revisions. The cover itself still takes the pointer, so a click never
     lands on the page underneath; only its images and copy opt out. The
     markup also sets draggable="false", which is what Firefox honours. */
  -webkit-user-select:none; user-select:none;
  -webkit-touch-callout:none;
  animation:loader-cover .3s var(--ease-brand) 4302ms both;
}

/* The sound toggle rides above the cover too. The soundtrack belongs to the
   page, not to the loader, so the control for it cannot be four and a half
   seconds away — a reader who wants it off wants it off now. Above the nav
   as well, since the nav only arrives for the last beat.

   .hero__mute is position:fixed inside .hero, and .hero carries no
   transform, filter or isolation — so it genuinely escapes to the root
   stacking context and a z-index here is enough. It stays `hidden` in the
   markup until initAmbientAudio() un-hides it, which is right: a mute
   button that cannot mute anything should not be painted. */
.loader img{pointer-events:none; -webkit-user-drag:none}

html.js-loading .hero__mute{z-index:102}

/* The nav rides ABOVE the cover, because the storyboard slides it in over
   the office photo while the photo is still there. It drops back to its own
   z-index the moment the class goes; nothing to clean up. */
html.js-loading .nav{
  z-index:101;
  will-change:transform;
  animation:loader-nav .6s var(--ease-brand) 3702ms both;
}

.loader__photo{
  position:absolute; inset:0;
  width:100%; height:100%; object-fit:cover;
  animation:loader-photo .3s var(--ease-brand) both;
}

/* The tag. Figma puts it at x=123 y=-152 in a 1920x1080 frame, 631x1088 —
   so it hangs with its hole near the top edge and its body through frame,
   in the left third. Percentages of the viewport, so it holds at any size.

   `translate` rather than `transform`, so the drop never has to know about
   the tilt — the same reason initFlyIn() writes translate. The tilt belongs
   to the artwork, not the motion: it is identical in every frame of the
   storyboard, so it lives in the shape and on the copy, and the drop stays
   a pure vertical. */
.loader__tag{
  position:absolute;
  left:6.41%;                       /* 123 / 1920 */
  /* Sized off viewport HEIGHT, not width. The tag is 1088 of a 1080-tall
     frame in Figma — it is drawn against the height, so that is what it
     should track. Tying it to width was the thing that made it feel like it
     was growing and shrinking for no reason, and it clipped the lockup on
     any window under about 1000px tall.

     Everything inside is a percentage of this box and the copy is in vh, so
     the whole composition scales as one piece: at any height the tag is the
     same shape with the same type size relative to it.

     x0.9375 against Figma — 0.75 then back up 25%, per the revisions. */
  top:-13.20vh;                     /* -14.07vh x 0.9375 */
  height:94.46vh;                   /* 100.74vh x 0.9375 */
  aspect-ratio:631 / 1088;
  /* NOT rotated: the tilt is drawn into the shape, and the copy carries its own. */
  filter:drop-shadow(0 24px 48px rgba(21,26,90,.22));
  will-change:transform;
  animation:loader-tag 3.4s 301ms both;
}

/* The tag's paper is ONE outline: dome, hole, tangent slopes, rounded
   shoulders and body, drawn upright and rotated -2.46deg inside the SVG —
   the copy's angle — so the whole hanger sits at one angle.

   It used to be two pieces, a punched dome at +1.38deg over a CSS body at
   -2.46deg. The 3.84deg disagreement left a hard corner at each shoulder,
   the dome art had a small step where it met its slope, and the two
   antialiased edges needed a 3px overlap to hide a hairline seam. One path
   has none of those. loader-tag.svg records what it was derived from.

   The SVG is padded 60 units on every side so its rotated corners are not
   clipped, so the image is inset by the same amount: 60 of 631.07 wide and
   60 of 1088.16 tall. Its unpadded area then lines up exactly with the tag
   box, which is what every percentage below is measured against. */
.loader__shape{
  position:absolute; z-index:0;
  left:-9.5077%; top:-5.5139%;
  width:119.0153%; height:111.0278%;
  max-width:none;
}

.loader__line{
  position:absolute; z-index:2;
  left:44.20%; top:48.73%;
  width:75.82%; height:27.62%;
  translate:-50% -50%;
  rotate:-2.46deg;
  margin:0;
  display:flex; align-items:center; justify-content:center;
  font-family:var(--font-display);
  font-weight:700;
  /* 60px on a 1088-tall tag is 5.51% of its height, and the tag is 94.46vh
     — so the copy is in vh too and rides the tag rather than the page. */
  font-size:5.209vh;
  line-height:1.04; letter-spacing:-.02em;
  text-transform:uppercase; text-align:center;
  color:var(--blue);
}

.loader__wordmark{
  position:absolute; z-index:2;
  left:46.24%; top:83.66%;
  width:75.20%;                     /* 474.536 / 631.07 */
  aspect-ratio:474.536 / 199.093;
  translate:-50% -50%;
  rotate:-2.46deg;
}
.loader__tagline{
  position:absolute; z-index:2;
  left:61.22%; top:90.92%;
  width:38.85%;                     /* 245.156 / 631.07 */
  aspect-ratio:245.156 / 44.6366;
  translate:-50% -50%;
  rotate:-2.46deg;
}

@keyframes loader-photo{ from{opacity:0} to{opacity:1} }
@keyframes loader-cover{ from{opacity:1} to{opacity:0} }
@keyframes loader-nav { from{translate:0 -100%} to{translate:none} }

/* One animation for the tag's whole life: drop, hang, fall. A keyframe's
   timing function applies to the segment AFTER it, so each beat carries its
   own curve. 1600 + 1000 + 800 = 3400ms, hence the percentages.

   The drop's easing is the designer's spring, sampled straight off the curve
   in their prototype rather than modelled from a stiffness figure: 41 stops,
   14.5% overshoot peaking at 55%. Values above 1 are the point — they are
   what carries the tag past its landing and back. Written inline because a
   custom property inside a @keyframes block is not reliably resolved. */
@keyframes loader-tag{
  0%{
    translate:0 -90.99%;            /* Figma y=-1142 -> -152 is 990 of 1088 */
    animation-timing-function:linear(
      0, .0099, .042, .1025, .1722, .2512, .3358, .4358, .5247, .6117,
      .6938, .7716, .8494, .9111, .966, 1.0123, 1.0556, 1.0864, 1.1093,
      1.1259, 1.1364, 1.142, 1.1451, 1.142, 1.1364, 1.1284, 1.1185, 1.108,
      1.0963, 1.0833, 1.0716, 1.0605, 1.05, 1.0401, 1.0296, 1.0216, 1.0148,
      1.0123, 1.0062, 1.0012, 1);
  }
  47.06%{ translate:0 0; animation-timing-function:linear }                      /* land */
  76.47%{ translate:0 0; animation-timing-function:cubic-bezier(.77,.01,.19,1) } /* hang */
  /* The exit is a share of the TAG, so it has to be restated whenever the
     tag's size changes: at 94.46vh tall with a top of -13.20vh, clearing
     the bottom edge takes 119.8%. 126% leaves about 6vh of margin. It was
     148% when the tag was a quarter smaller — the same distance, a larger
     fraction of a smaller thing. */
  100%  { translate:0 126% }
}

/* Skip. A transition runs from the before-change style, which includes a
   running animation's current value — so dropping the animation and naming
   the end value crossfades out of wherever the tag happens to be, rather
   than snapping back to its start. */
html.js-loading.is-skipping .loader{
  animation:none; opacity:0; transition:opacity .3s var(--ease);
}
html.js-loading.is-skipping .js-loader-tag{
  animation:none; translate:0 123.07%; transition:translate .3s var(--ease-inout);
}
html.js-loading.is-skipping .nav{
  animation:none; translate:none; transition:translate .3s var(--ease-inout);
}

/* Belt and braces. The cover is never armed under this preference — the head
   script tests it before adding the class, and initLoader() tests the shared
   query again — but a preference that flips mid-sequence must also land, and
   the rule is that it is honoured in both layers. */
@media (prefers-reduced-motion:reduce){
  .loader{display:none !important}
  html.js-loading{overflow:visible; background:var(--light-blue)}
  html.js-loading .nav{animation:none; translate:none; z-index:60}
}

/* ─────────────────────────────────────────────────────────────
   NAV — fixed overlay. Two-layer cream/navy crossfade — see TONE below
   ───────────────────────────────────────────────────────────── */
.nav{
  /* Above .menu (50), not below it. The overlay used to cover the nav, which
     is why it carried a duplicate bar of its own; now the real nav stays put
     and the panel slides down behind it. The loader takes 100, and lifts the
     nav to 101 for as long as it runs — see the LOADER block above. */
  position:fixed; inset:0 0 auto; z-index:60;
  display:grid; grid-template-columns:1fr auto 1fr; align-items:center;
  gap:var(--s-10);
  padding:var(--s-20) var(--gutter);
  pointer-events:none;
}
.nav > *{pointer-events:auto}
.nav__right{justify-self:end}
.nav__menu{
  position:relative;
  width:clamp(28px,2.1vw,40px); aspect-ratio:1;
  transition:opacity .25s var(--ease);
}
.nav__menu img{position:absolute; inset:0; width:100%; height:100%}
.nav__menu:focus-visible{outline:2px solid var(--cream); outline-offset:6px}

/* Hover state for every icon control in the bar, plus the wordmark.
   Gated on a real pointer. On touch there is nothing to leave, so :hover
   latched after a tap and the hamburger sat dimmed at 30% for the rest of
   the session — the same trap .card__plus and .faq__item already guard. */
@media (hover:hover){
  .nav__menu:hover,
  .nav__quote-icon:hover,
  .nav__logo:hover{opacity:.3}
}
.nav__logo,
.nav__quote-icon{transition:opacity .25s var(--ease)}
.nav__quote-icon{display:none}          /* mobile-only; the pill covers desktop */

/* ── Logo lockup — wordmark + tagline, positioned per the Figma
      component (1153:1142). Large 140×60.449, Small 100×43.178. ── */
.nav__logo{
  justify-self:center;
  position:relative;
  display:block;
  width:clamp(156px,11.406vw,219px);  /* +25%, then +25% again beside the big quote button */
  aspect-ratio:140 / 60.449;
}
.nav__logo picture{display:block}
.nav__logo img{width:100%;height:100%;display:block}
.nav__logo__wordmark{
  position:absolute;
  inset:0 0 2.83% 0;            /* Figma: inset-[0_0_2.83%_0] */
}
.nav__logo__tagline{
  position:absolute;
  inset:78.21% 4.99% 0 43.35%;  /* Figma: inset-[78.21%_4.99%_0_43.35%] */
}

/* ── Tone — cream over dark ground, dark over light ──────────
   Cream reads over the hero photograph and the navy footer, and all but
   vanishes on the light-blue page between them. initNavTone() adds
   .is-on-light whenever the nav is over neither (and the menu is shut).

   The dark version is one colour on every page: navy, or --blue under
   .nav--blue. Which one is still being decided, so the temporary
   initNavColourSwitch() in main.js sets it. --nav-dark carries it to every
   mark and the pill.

   Every mark ships as a cream <img> with a .nav__dark twin stacked on it,
   and the change is an opacity trade between the two — DEVELOPMENT.md's
   header crossfade, done per mark so the one real nav keeps its controls
   rather than being duplicated. The twin is a span masked by the very SVG
   the cream <img> draws, filled with --nav-dark: one set of files, and a
   colour change is a single custom property. Mask size is 100% 100%
   because the SVGs are preserveAspectRatio="none" and the <img>s stretch.
   The url()s are written out per mark rather than passed in a custom
   property, whose relative url() resolves against whichever sheet uses
   it — see ico() in initQuoteForm().

   Both layers fade: a dark twin over a cream one left at full opacity
   would leave a cream fringe on every antialiased edge. Scoped to .nav,
   because the hero lockup and the footer reuse the logo classes with no
   twin. The pill is text and a border, so it simply transitions colour. */
.nav{--nav-dark:var(--navy)}
.nav--blue{--nav-dark:var(--blue)}

.nav__quote-icon{position:relative}
.nav .nav__dark{
  position:absolute; inset:0; opacity:0;
  background:var(--nav-dark);
  -webkit-mask:no-repeat center / 100% 100%;
          mask:no-repeat center / 100% 100%;
  transition:opacity .3s var(--ease), background-color .3s var(--ease);
}
.nav__quote-icon .nav__dark{-webkit-mask-image:url(../svg/icon-envelope.svg); mask-image:url(../svg/icon-envelope.svg)}
.nav__logo__wordmark .nav__dark{-webkit-mask-image:url(../svg/logo-wordmark-lg.svg); mask-image:url(../svg/logo-wordmark-lg.svg)}
.nav__logo__tagline .nav__dark{-webkit-mask-image:url(../svg/logo-tagline-lg.svg); mask-image:url(../svg/logo-tagline-lg.svg)}
.nav__ico-bars .nav__dark{-webkit-mask-image:url(../svg/menu-icon.svg); mask-image:url(../svg/menu-icon.svg)}

.nav :is(.nav__quote-icon, .nav__logo, .nav__ico-bars) img{transition:opacity .3s var(--ease)}
.nav.is-on-light .nav__dark{opacity:1}
.nav.is-on-light :is(.nav__quote-icon, .nav__logo, .nav__ico-bars) img{opacity:0}

.nav .nav__quote{transition-duration:.3s}
.nav.is-on-light .nav__quote{
  color:var(--nav-dark); border-color:var(--nav-dark);
  --btn-burst:var(--nav-dark);
}
/* Colour only — the fill is the burst disc (--btn-burst above), growing from
   where the cursor came in. A background here would paint it solid at once. */
.nav.is-on-light .nav__quote:hover,
.nav.is-on-light .nav__quote:focus-visible{color:var(--cream)}
.nav.is-on-light .nav__menu:focus-visible{outline-color:var(--nav-dark)}

/* ─────────────────────────────────────────────────────────────
   HERO — full viewport media that shrinks and rounds on scroll
   ───────────────────────────────────────────────────────────── */
.hero{
  position:relative;
  /* The height is PINNED, in pixels, by initHeroHeight().

     Every viewport unit is wrong here in a different way. `svh` is the small
     viewport, so once iOS collapses its toolbar the hero is ~85px shorter
     than the screen and the next section rides into view at scroll 0. `lvh`
     is the large viewport, so at load it pushes the same ~85px below the
     fold — and all four heroes anchor content to their bottom edge, so that
     cuts the copy. `dvh` tracks the live viewport and is therefore correct
     in both states, but it RESIZES as the toolbar animates: the document
     reflows mid-scroll, Lenis recomputes against a moving height, and the
     scroll goes jumpy. That is what this replaces.

     A box cannot both equal a changing viewport and never change size. So
     it is measured once and only re-measured when the width genuinely
     changes — the same guard initSplitLines() already uses, for the same
     reason: on iOS every toolbar collapse fires a resize.

     The svh fallback is what paints before JS runs, and if JS never runs. */
  height:100svh;
  height:var(--hero-h, 100svh);
  min-height:520px;
  background:var(--light-blue);
  overflow:hidden;
}
.hero__media{
  position:absolute; inset:0;
  overflow:hidden;
  transform-origin:center center;
  will-change:transform,border-radius;
}
.hero__media img,
.hero__media video{
  width:100%; height:100%; object-fit:cover;
  will-change:transform;
}
/* the still is the poster/fallback — it sits under the video and shows
   through whenever the video is missing, still buffering, or blocked */
.hero__video{position:absolute;inset:0;z-index:1}
.hero__still{position:absolute;inset:0;z-index:0}
.hero__scrim{
  position:absolute; inset:0;
  background:linear-gradient(180deg,rgba(21,26,90,.18) 0%,transparent 28%,transparent 62%,rgba(21,26,90,.22) 100%);
  pointer-events:none;
}

/* media controls */
.hero__play[data-disabled]{display:none !important}   /* parked */

.hero__play,
.hero__mute{
  position:absolute; left:50%; transform:translateX(-50%);
  display:grid; place-items:center;
  border-radius:50%;
  transition:transform .3s var(--ease), background-color .3s var(--ease);
}
.hero__play{
  top:50%; margin-top:-2vh;
  width:clamp(48px,3.65vw,70px); aspect-ratio:1;   /* Small 48 → Large 70 */
  background:var(--cream);
  border:clamp(3px,.2775vw,5.25px) solid var(--white);   /* Figma 7.01px, reduced 25% */
  transform:translate(-50%,-50%);
}
@media (hover:hover){ .hero__play:hover{transform:translate(-50%,-50%) scale(1.07)} }
.hero__play img,
.hero__mute img{width:40%;height:40%}          /* 28/70 and 19/48 both = 40% */
/* Sound toggle is page-level, not hero-level: it stays pinned to the
   viewport so the control is reachable wherever you are on the page. */
.hero__mute{
  position:fixed;
  bottom:clamp(20px,4vh,48px);
  z-index:30;
  width:clamp(48px,3.65vw,70px); aspect-ratio:1;   /* Small 48 → Large 70 */
  background:rgba(255,255,255,.1);
  backdrop-filter:blur(8px);
  -webkit-backdrop-filter:blur(8px);
  border:1px solid rgba(255,255,255,.25);
}
@media (hover:hover){ .hero__mute:hover{background:rgba(255,255,255,.2)} }

/* Both states share the glass container (Figma 1494:2803) — only the
   glyph changes: SpeakerSlash when muted, SpeakerHigh when sound is on. */


/* ── Hero wordmark (mobile) ─────────────────────────────────
   The nav lockup at hero scale. Reuses .nav__logo__wordmark /
   __tagline, which carry the lockup's inner geometry as percentages,
   so it composes at any size. */
.hero__mark{display:none}
.hero__mark picture{display:block}
.hero__mark img{width:100%;height:100%;display:block}

/* ── Badges ────────────────────────────────────────────────── */
.hero__badge{
  position:absolute;
  display:grid; place-items:center;
  z-index:3;
}
/* Label gets its own class. Previously this was `.hero__badge span`,
   whose 0-1-1 specificity beat `.badge-spin__star` (0-1-0) and forced
   the star container to position:relative — collapsing it to nothing. */
.badge__label{
  position:relative; z-index:2;
  font-family:var(--font-body);
  font-weight:700;
  font-size:var(--badge-label);      /* scales with --badge-size, see base.css */
  line-height:1.2;
  text-transform:uppercase;
  color:var(--cream);
  text-align:center;
}

/* ── Right badge — a real link. Hover swaps the star to navy,
      spins it 180°, and underlines the label. ───────────────── */
.badge-link{
  /* Bottom matches the other three heroes. They seat their copy with
     .about-hero__copy's padding-block:var(--s-80), so their lowest element
     sits 80px off the hero's edge; this was on clamp(16px,4.4vh,64px),
     which resolves to 47.5px at 1080 and never reaches 80 below a 1455px
     viewport — so the homepage's badges sat a good 30px lower than
     everything else on the site. The horizontal clamp stays: nothing else
     is anchored to the right edge, so there is nothing to match it to. */
  right:clamp(16px,4.2vw,80px); bottom:var(--s-80);
  width:var(--badge-size); aspect-ratio:1;
  text-decoration:none;
  cursor:pointer;
}
/* ── Spinning star badge ─────────────────────────────────────
   Shared by the hero badge and the clients badge. Two stacked stars —
   blue over navy — crossfade while the wrapper turns 180°. The stars
   are different shapes per section, so each supplies its own pair. */
.badge-spin__star{
  position:absolute; inset:4%; z-index:1;   /* star sits 4% in on every side */
  display:grid; place-items:center;
  transition:transform .55s var(--ease);
}
.badge-spin__star img{
  position:absolute; inset:0; width:100%; height:100%;
  object-fit:contain;              /* never stretch on a non-square box */
  transition:opacity .35s var(--ease);
}
.badge-spin__star img.is-hover{opacity:0}          /* navy layer, revealed on hover */

/* Hover is split from focus deliberately. On touch there is no pointer to
   leave, so :hover latched after a tap and the badge stayed stuck in its
   hover state — navy star, underlined label — for the rest of the session.
   Every hover state on the site is gated this way now; the nav icons,
   .card__plus and .faq__item carry the same guard.

   :focus-visible stays OUTSIDE the gate: keyboard users get the state on
   any device, and a touch that does not move focus never triggers it. */
@media (hover:hover){
  .badge-spin:hover .badge-spin__star{ transform:rotate(180deg); }
  .badge-spin:hover .badge-spin__star img.is-hover{ opacity:1; }
  .badge-spin:hover .badge__label{
    color:var(--white);
    text-decoration:underline;
    text-decoration-thickness:.08em;
    text-underline-offset:.18em;
  }
}

.badge-spin:focus-visible .badge-spin__star{ transform:rotate(180deg); }
.badge-spin:focus-visible .badge-spin__star img.is-hover{ opacity:1; }
.badge-spin:focus-visible .badge__label{
  color:var(--white);
  text-decoration:underline;
  text-decoration-thickness:.08em;
  text-underline-offset:.18em;
}

@media (prefers-reduced-motion:reduce){
  .badge-spin__star{transition:none}
  .badge-spin:hover .badge-spin__star,
  .badge-spin:focus-visible .badge-spin__star{transform:none}
}
.badge-link:focus-visible{outline:2px solid var(--white);outline-offset:6px;border-radius:50%}

/* the star also rotates with scroll depth (starHighlight); that lives on
   an inner element so the hover spin above never fights it */
.js-hero-star{will-change:transform}

/* ── Left badge — cream starburst with the building punched out.
      The cut-out is transparent, so the hero media shows through it.
      Geometry mirrors Figma node I1360:8107;1170:349 exactly: the frame
      is 277×225 and the 268.783-tall star deliberately overflows the
      bottom by ~62px and is clipped. ─────────────────────────────── */
.badge-mark{
  --badge-w:var(--badge-size);
  left:var(--gutter); bottom:var(--s-80);   /* same baseline as .badge-link
                                              and the other heroes' copy */
  width:var(--badge-w);
  aspect-ratio:277 / 225;
  display:block;                 /* absolute children, like the Figma frame */
  overflow:hidden;               /* clips the star's lower points */
  pointer-events:none;
}
.badge-mark__star{
  position:absolute;
  left:1.05%; top:8.25%;
  width:97.75%; height:119.46%;  /* taller than the frame — intentionally clipped */
  overflow:visible;
  z-index:1;
}
/* only the star path spins; the mask lives on its parent group, so the
   building cut-out stays upright while the rays rotate behind it */
.badge-mark__ray{
  transform-origin:50% 50%;
  transform-box:fill-box;
  will-change:transform;
}
.badge-mark .badge__label{
  position:absolute;
  /* Figma's hero instance offsets this right of centre (left 50.95%);
     centred on the frame reads better and matches the badge component. */
  left:0; right:0; top:91.86%;
  z-index:2;
  font-family:var(--font-display);
  font-weight:400;
  font-size:calc(var(--badge-w) * 0.0505);   /* 14px at 277px wide */
  letter-spacing:.1em;                        /* Figma tracking 1.4 / 14px */
  line-height:1;
  text-align:center;
}

/* ─────────────────────────────────────────────────────────────
   SERVICES — label + 12-col grid. The list is a tab picker:
   four labels swapping one image (jumpZones pattern).
   ───────────────────────────────────────────────────────────── */
.services__grid{
  display:grid;
  grid-template-columns:repeat(12,minmax(0,1fr));
  gap:var(--s-40);
  align-items:start;
}
.services__col{
  grid-column:1 / span 8;
  display:flex; flex-direction:column;
  gap:var(--s-40);              /* list -> button; Figma 60, tightened to 40 per the revisions */
  align-items:flex-start;
}
.services__list{display:flex; flex-direction:column; gap:var(--s-10); width:100%}

/* Each service is a button holding a title and a collapsed description.
   Active (hover, click, or focus) turns it Cleanly blue and opens the copy.
   JumpFactory does this by measuring scrollHeight and writing an explicit
   max-height; grid-template-rows 0fr -> 1fr gets the same animation with
   no measurement, and survives reflow without needing a resize handler. */
.services__item{
  display:block; width:100%; text-align:left;
  color:var(--navy);
  cursor:pointer;
  transition:color .28s var(--ease);
}
.services__title{
  font-family:var(--font-display);
  font-weight:700;
  font-size:var(--t-service);
  line-height:1;
  letter-spacing:-0.02em;
  text-transform:uppercase;
  margin-block:calc(-0.08em) calc(-0.06em);
  padding-block:.12em;
  transition:color .28s var(--ease);
}
.services__desc{
  display:grid;
  grid-template-rows:0fr;
  opacity:0;
  transition:
    grid-template-rows .5s var(--ease-inout),
    opacity            .35s var(--ease-inout) .05s,
    padding            .5s var(--ease-inout);
}
.services__desc > span{
  overflow:hidden;
  min-height:0;
  font-size:var(--t-body);
  font-weight:700;                   /* bold p, same as .serve__body */
  line-height:1.3;
  max-width:59ch;                    /* Figma: 1037 of 1760 */
  display:block;
}

.services__item:focus-visible,
.services__item.is-active{ color:var(--blue) }
/* Gated on a real pointer: on touch there is nothing to leave, so :hover
   latches after a tap and the state sticks for the session. */
@media (hover:hover){
  .services__item:hover{ color:var(--blue) }
}

.services__item.is-active .services__desc{
  grid-template-rows:1fr;
  opacity:1;
  padding-top:var(--s-20);
  padding-bottom:var(--s-40);        /* Figma pb-[40px] on the open item */
}
/* padding has to start at zero or it jumps open before the row expands */
.services__desc{padding-top:0;padding-bottom:0}
.services__item:focus-visible{outline:2px solid var(--blue);outline-offset:6px}

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

/* stacked panels — grid-stack sizes itself from the tallest,
   which avoids the collapsed-container trap of absolute positioning */
.services__media{
  grid-column:9 / span 4;        /* full column width, unchanged */
  position:relative;
  display:grid;
  border-radius:var(--r-card);
  /* Desktop height is set outright, not derived from width — the column
     can widen without the photo growing taller. */
  height:clamp(400px, 31.5vw, 605px);
  isolation:isolate;
}
.services__panel{
  grid-area:1 / 1;
  border-radius:var(--r-card);
  overflow:hidden;
  opacity:0;
  transition:opacity .35s var(--ease);
}
.services__panel.is-active{opacity:1}
.services__panel img{width:100%;height:100%;object-fit:cover}

/* Sits centred ON the container's bottom edge — half inside the photo,
   half hanging below it. Figma does this with bottom:35.11px plus
   translate-y-full, which works out to ~56/44 across the edge. */
.services__script{
  position:absolute; left:50%; bottom:0;
  transform:translate(-50%, 50%);
  width:max-content; max-width:110%;
  text-align:center;
  font-size:var(--t-script-sm);
  line-height:1;
  z-index:4;
  pointer-events:none;
  /* no glow — the 2px cream stroke on .h-script is the whole treatment */
}
.services__star{
  position:absolute;
  top:clamp(-30px,-3.1vw,-60px); right:clamp(-30px,-3.1vw,-60px);
  width:clamp(90px,10.9vw,210px); aspect-ratio:1;
  z-index:3; pointer-events:none;
}

/* ─────────────────────────────────────────────────────────────
   MARQUEE — CSS-driven. Two copies translated -50%; no cloning,
   so the DOM never grows. (JumpFactory's version leaks nodes.)
   ───────────────────────────────────────────────────────────── */
.marquee{
  display:flex;
  background:var(--blue);
  padding-block:var(--s-32);
  overflow:hidden;
  text-decoration:none;
  color:inherit;
}
.marquee:focus-visible .marquee__item{
  text-decoration:underline;
  text-decoration-thickness:.06em;
  text-underline-offset:.16em;
}
/* Gated on a real pointer: on touch there is nothing to leave, so :hover
   latches after a tap and the state sticks for the session. */
@media (hover:hover){
  .marquee:hover .marquee__item{
    text-decoration:underline;
    text-decoration-thickness:.06em;
    text-underline-offset:.16em;
  }
}
.marquee:focus-visible{outline:3px solid var(--cream);outline-offset:-3px}
.marquee__track{
  display:flex;
  flex-shrink:0;
  min-width:100%;
  gap:0;
  animation:marquee var(--marquee-duration,38s) linear infinite;
  will-change:transform;
}
.marquee__group{
  display:flex; flex-shrink:0; align-items:center;
  gap:clamp(20px,2.6vw,50px);
  padding-right:clamp(20px,2.6vw,50px);
}
.marquee__item{
  font-size:var(--t-body-lg);
  font-weight:700;
  color:var(--cream);
  white-space:nowrap;
  text-decoration:none;
}
/* The separator between phrases — a donut or empanada emoji (marquee.js)
   in place of the pale-blue dot it used to be. Sized off the label token,
   not inherited, because the group itself sits at the page's base size. */
.marquee__emoji{
  flex:0 0 auto;
  font-size:var(--t-body-lg);
  line-height:1;
  align-self:center;
}
@keyframes marquee{ to { transform:translateX(-50%) } }
/* Pause on hover only. On touch this latched, so tapping either band
   stopped it for good and the marquee looked broken rather than paused. */
@media (hover:hover){
  .marquee:hover .marquee__track{animation-play-state:paused}
}
@media (prefers-reduced-motion:reduce){
  .marquee__track{animation:none}
}

/* ─────────────────────────────────────────────────────────────
   BENEFITS — heading with overlapping script, then a horizontal
   card rail. 4 × 870px + gaps overflows the frame by design.
   ───────────────────────────────────────────────────────────── */
/* The script is absolutely positioned, so it contributes nothing to layout —
   the h2 was centring on itself while the wider script hung off-axis. Both are
   now centred on the same axis, and the block reserves room for the overlap. */
.benefits__head{
  position:relative;
  display:grid;
  justify-items:center;
  text-align:center;
}
.benefits__head .h-display{width:100%}
/* The script stays IN FLOW and tucks under the headline with a negative
   margin — the same trick .serve__script uses.
   It used to be absolutely positioned at top:.62em with the head carrying
   a padding-bottom to reserve room. That reservation never matched: at
   1920 the head measured 152px while its own script ran to 194px, so the
   box did not contain its content, .stack's gap measured from the wrong
   edge, and .benefits__row needed a margin-top to paper over it. In flow
   the head auto-sizes and one gap does the whole job.
   -0.1em of the HEADLINE (not the script) lands the script 83.8px below
   the headline's cap at 1920, matching Figma I1466:3621;1251:3227. */
.benefits__script{
  margin-top:calc(var(--t-heading) * -0.1);
  transform:rotate(-1.5deg);
  width:max-content;
  max-width:100%;
  pointer-events:none;
}

/* Centred carousel, matching JumpFactory's `slidesPerView:'auto'` +
   `centeredSlides:true`. The side padding is what lets the first and last
   card reach the middle; without it they can only ever sit at the edges. */
.rail{
  /* Gap scales with the viewport like the type does — 40px at 1920 (Figma
     draws 60; tightened to 40 per the revisions, like the section stacks).
     The card width has to use the SAME value or the two-up maths breaks. */
  --rail-gap:clamp(16px, 2.083vw, 40px);   /* 40px @1920, ~30 @1440, 16 floor */
  /* Sized so two cards span the container between the gutters, which is
     how the Benefits artboard is drawn — (1760 - 60) / 2 = 850 there. The
     850 cap is kept, so with the 40px gap a 1920 pair now leaves 20px spare.
     Floored at 480px per the revisions: below a ~1050px viewport the two-up
     maths made cards too narrow for their copy, so on tablet and small
     desktop a card holds 480 and the next one peeks instead. Phones set
     their own --card-w in the 640px blocks, so the floor stops there. */
  --card-w:max(480px, min(calc((100vw - 2 * var(--gutter) - var(--rail-gap)) / 2), 850px));
  display:flex;
  gap:var(--rail-gap);
  overflow-x:auto;
  padding-inline:calc(50vw - var(--card-w) / 2);
  scrollbar-width:none;
  cursor:move;                            /* JumpFactory uses cursor-move */
  overscroll-behavior-x:contain;
  scroll-behavior:auto;
}
.rail::-webkit-scrollbar{display:none}
.rail.is-dragging{user-select:none}

/* ── Swiper track ──────────────────────────────────────────────
   Once JS has wrapped the cards, the rail stops being a scroll
   container and the track carries the layout. The plain `.rail`
   rules above stay as the no-JS fallback: without the script the
   cards are still a native horizontal scroller.

   `transition-timing-function:ease` is not a stylistic pick — it is
   the CSS `initial` value Swiper's wrapper resolves to, and so the
   curve every JumpFactory slide transition actually runs on. */
.rail.is-swiper{
  display:block;
  overflow:hidden;
  touch-action:pan-y;        /* horizontal belongs to the slider, vertical to the page */
  /* The centring padding moves to the track. Left on both it applies
     twice and every card sits half a gutter right of centre. */
  padding-inline:0;
}
.rail.is-swiper .rail__track{
  display:flex;
  gap:var(--rail-gap);
  padding-inline:calc(50vw - var(--card-w) / 2);
  transition-property:transform;
  transition-timing-function:ease;
  transition-duration:0ms;
  will-change:transform;
}
@media (prefers-reduced-motion:reduce){
  .rail.is-swiper .rail__track{transition-duration:0ms !important}
}

/* ── Card height presets ──────────────────────────────────────
   Three named heights, set on the RAIL so a card reads its own size from
   its container the way it already reads --card-w and --rail-gap.

   These are not new values: they are the three heights that were already
   tuned in this file, previously expressed by reaching in from an ancestor
   (`.industries .card`, `.benefits--wide .card`). Naming them means a slider
   can pick one in the CMS without anyone writing a selector.

   Desktop is fluid, mobile is a fixed Figma figure — the split the whole
   site follows. Mobile values live in the 640px block further down. */
.rail--short{--card-h:clamp(360px,37.5vw,720px)}
.rail--tall {--card-h:clamp(438px,36vw,761px)}

.card{
  position:relative;
  flex:0 0 var(--card-w);
  /* Height comes from the rail's preset. `standard` is the default so a
     rail with no modifier is unchanged from what shipped. */
  height:var(--card-h, clamp(365px,36vw,634px));   /* -20%, then +20% back */
  border-radius:var(--r-card);
  overflow:hidden;
  cursor:inherit;                  /* the rail's `move`, not button pointer */
  display:flex; flex-direction:column; justify-content:space-between;
  align-items:flex-start;
  padding:var(--s-32);
  text-align:left;
  isolation:isolate;
}
.card img{
  position:absolute; inset:0; z-index:-1;
  width:100%; height:100%; object-fit:cover;
  transition:transform .6s var(--ease), filter .5s var(--ease-inout);
}
.card::after{
  content:''; position:absolute; inset:0; z-index:-1;
  background:linear-gradient(180deg,rgba(0,0,0,.28) 0%,transparent 34%,transparent 52%,rgba(0,0,0,.55) 100%);
}
/* An open card dims its photo so the description reads — part of the
   component, so any expandable rail gets it. A filter on the image rather
   than another overlay: ::after is already the gradient, and a ::before at
   the same z-index would paint UNDER the <img> in tree order. Timed to the
   .card__desc reveal (.5s, ease-inout) so the two move together. */
.card.is-open img{transform:scale(1.04); filter:brightness(.6)}
.card:focus-visible{outline:3px solid var(--blue);outline-offset:4px}
/* The number and the plus share one row, which is the only way they stay
   aligned at every width. They used to be independent: the number was an
   in-flow child indented by the card's own padding, the plus was absolutely
   positioned at its own inset — so the two agreed only where those happened
   to match, and on mobile they did not (20px padding against a 16px inset).
   One row means the card's padding places both, and nothing to keep in sync.

   Top-LEFT for the number, matching Figma (I1466:3621;1251:3230). */
.card__head{
  display:flex;
  justify-content:space-between;
  align-items:baseline;
  /* .card is align-items:flex-start, so the row has to opt out of it or it
     shrink-wraps the number and the plus lands beside it, not at the edge. */
  align-self:stretch;
  gap:var(--s-20);
}
.card__num{text-align:left}

.card__num,
.card__title{
  font-size:var(--t-body);        /* was --t-body-lg — too large on the card */
  font-weight:700;
  color:var(--cream);
  line-height:1.2;
  margin:0;
}
.card__body{
  display:flex; flex-direction:column;
  color:var(--cream);
  max-width:62ch;
}
.card__desc{
  display:grid;
  grid-template-rows:1fr;          /* open by default — see the hover gate below */
  font-size:var(--t-body-sm);
  font-weight:400;
  line-height:1.3;
  color:var(--cream);
}
/* The spacing lives inside the collapsing row rather than as a gap on the
   flex parent — a gap would hold its 20px open even with the row at 0fr,
   leaving a dead band under the title while the card is closed. */
.card__desc > span{overflow:hidden;min-height:0;display:block}

/* Collapsed at every width, opened by the plus. It used to reveal on hover
   above 1025px, which meant the copy was unreachable on touch and invisible
   until a cursor happened to land on it — nothing signalled there was more
   to read. The plus signals it, and behaves the same on every device. */
.card__desc{
  grid-template-rows:0fr;
  opacity:0;
  margin-top:0;                         /* collapses flush against the title */
  transition:grid-template-rows .5s var(--ease-inout),
             margin-top        .5s var(--ease-inout),
             opacity           .35s var(--ease-inout);
}
.card.is-open .card__desc{grid-template-rows:1fr; opacity:1; margin-top:var(--s-10)}

/* Phosphor's X at the bold weight, which is the same glyph as its Plus
   turned 45° — so one asset covers both states and the tilt IS the
   transition rather than a crossfade between two files. */
.card__plus{
  width:clamp(18px,1.6vw,26px); aspect-ratio:1;
  flex:none;                            /* never squeezed by a wide number */
  rotate:-45deg;                        /* an X turned 45° reads as a plus */
  transition:rotate .45s var(--ease-inout), opacity .25s var(--ease);
  pointer-events:none;
}
.card__plus img{width:100%; height:100%}
.card.is-open .card__plus{rotate:0deg}  /* …and back to an X when open */
@media (hover:hover){
  .card:hover .card__plus{opacity:.6}   /* latches on touch without the gate */
}
@media (prefers-reduced-motion:reduce){
  .card__plus{transition:none}
}
@media (prefers-reduced-motion:reduce){
  .card__desc{transition:none}
}


/* ─────────────────────────────────────────────────────────────
   Responsive — layout only. Type never needs a breakpoint.
   ───────────────────────────────────────────────────────────── */
/* ── What we offer (1360:8145) — the MOBILE layout, from tablet down ──
   Tablet used to keep the desktop order with the photo stretched full
   width under the list. Per the revisions it takes the mobile layout
   instead, so these rules moved here from the max-width:640px block and
   now cover both. Everything below is in px or fixed spacing tokens, so it
   reads the same at 1024 as at 390; type still follows the fluid tokens.

   Figma orders this label -> IMAGE -> list. The desktop DOM is list-first,
   so the image is pulled above it here.

   The 12-col grid has to go, not just restack. Its gap is a fixed 40px, so
   eleven gutters need 440px before a single track gets a pixel — wider than
   the 334px mobile container, which is why anything spanning 1/-1 measured
   440 and pushed the document to 468px wide. A one-column flex has no
   gutters to pay for. */
@media (max-width:1024px){
  .services__grid{
    display:flex; flex-direction:column;
    gap:var(--s-40);
    /* the base rule's align-items:start is a CROSS-axis value once the
       direction is column — it would size both children to content width
       instead of the container. */
    align-items:stretch;
  }
  /* The ratio has to live on the IMG, not the container. The panels are
     grid-area:1/1 with `height:100%`, so against an auto-height parent that
     percentage resolves to the photo's intrinsic height and pushes the
     container past its own aspect-ratio (384 instead of 225). height:auto
     undoes the desktop clamp(400px, …), which would otherwise floor it. */
  .services__media{order:-1; height:auto; aspect-ratio:auto}
  /* Capped so a landscape tablet does not get a 940x630 photo. It keeps to
     the left edge the label and list share; the star and script are placed
     off this box, so they follow it in. */
  .services__media{max-width:720px}
  .services__panel img{aspect-ratio:333/224; height:auto}   /* Figma 333x224 */
  /* tighter than desktop's 20/40 — the list is only 4px apart here */
  .services__item.is-active .services__desc{
    padding-top:var(--s-10);
    padding-bottom:var(--s-20);
  }
  .services__list{gap:4px}
  .services__col{gap:var(--s-20)}
  /* Star bleeds off the LEFT edge and sits 20px down, not top-right */
  .services__star{
    top:20px; left:-41px; right:auto;
    width:103px;
  }
  /* Script sits INSIDE the image, right-aligned and tilted. Figma's
     bottom:45.05 is on a box that then translates down its own 38.7px
     height, so the painted edge lands ~6px above the photo. */
  .services__script{
    left:auto; right:19px; bottom:6px;
    transform:rotate(-2.89deg);
    text-align:right;
    max-width:none;
  }
}

/* ─────────────────────────────────────────────────────────────
   WHO WE SERVICE  (Figma: Section - Hero Copy, 1360:8110)
   Label · oversized mixed-colour headline · script overlay ·
   12-col CTA row with body copy left and the button far right.
   ───────────────────────────────────────────────────────────── */
.serve__head{
  font-family:var(--font-display);
  font-weight:700;
  font-size:var(--t-heading);         /* matched to .h-display; stays its own class */
  line-height:1;
  letter-spacing:-0.02em;
  text-transform:uppercase;
  color:var(--navy);
  margin:calc(-0.08em) 0 0;
  text-wrap:balance;
}
.serve__head em{font-style:normal;color:var(--blue)}

/* The script sits over the tail of the headline. Figma pins it at a
   fixed top:285px, which only holds at 1920 — flowing it with a
   negative margin keeps the same overlap at every width. */
.serve__script{
  margin-top:calc(var(--t-heading) * -0.12);
  margin-bottom:0;
  line-height:.8;
}

.serve__cta{
  display:grid;
  grid-template-columns:repeat(12,minmax(0,1fr));
  gap:var(--s-20);
  align-items:start;
  width:100%;
}
.serve__body{
  grid-column:1 / span 6;
  font-size:var(--t-body);
  font-weight:700;
  line-height:1.3;
  margin:0;
}
.serve__action{
  grid-column:10 / span 3;
  display:flex; align-items:center; justify-content:flex-end;
}

@media (max-width:1024px){
  .serve__body{grid-column:1 / -1}
  .serve__action{grid-column:1 / -1; justify-content:flex-start}
  .serve__script{margin-top:calc(var(--t-heading) * -0.06)}
}

/* ═════════════════════════════════════════════════════════════
   CLIENTS  (Figma 1360:8115)
   ═════════════════════════════════════════════════════════════ */
/* The one break in the page's fixed 160px section rhythm. The logo band
   below the heading is a single thin strip rather than the two-row block
   the grid used to be, so the section carries far less visual weight than
   its neighbours and the standard gap left the heading crowded from above.
   Scoped to this section — the rhythm elsewhere is deliberate. */
.clients{padding-top:var(--s-80)}

.clients__head{
  display:grid;
  grid-template-columns:repeat(12,minmax(0,1fr));
  gap:var(--s-20);
  align-items:start;
}
.clients__title{grid-column:1 / span 8}
.clients__title em{font-style:normal;color:var(--blue)}

.clients__badge{
  grid-column:11 / span 2;
  position:relative;
  display:grid; place-items:center;
  aspect-ratio:1;
  text-decoration:none;
  justify-self:end;
  width:var(--badge-size);
}
/* Scoped to the label class, never a bare `span`. An element+class selector
   is 0-1-1 and outranks the 0-1-0 `.badge-spin__star`, which forced the star
   wrapper to position:relative and collapsed it to nothing. */

.clients__badge:focus-visible{outline:2px solid var(--navy);outline-offset:6px;border-radius:50%}

/* ── Client logos — marquee ──────────────────────────────────
   Replaces the ruled 4-col grid: the rules read as a static table
   and fought the motion language everywhere else on the page. */
.logos{
  /* Deliberately airier than the grid this replaced. The grid's own rhythm
     (180px between marks, from cellW - logoW) still read as clutter once the
     marks were on one line instead of spread over two rows of four, so the
     gap runs to ~300 @1728 / 334 @1920 — a bit over 1.5 marks of air, which
     puts three on screen at a time rather than four and a half. */
  --logo-gap:clamp(64px,17.4vw,334px);
  --logo-pad:clamp(36px,3.9vw,75px);
  /* MUST stay flex, exactly as .marquee is. The track's translateX(-50%) is
     a percentage of the track's own LAYOUT width — as a flex item that is
     its content width (one full group), but as a plain block-level flex
     container it would be the width of this box instead. Blocked, the strip
     crawls at container/2 per cycle and snaps back mid-group. */
  display:flex;
  margin-inline:calc(var(--gutter) * -1);   /* break the .shell gutter */
  padding-block:var(--logo-pad);
  overflow:hidden;
  /* Both ends fade clean out to nothing. The ramp is long — 14% a side, so
     ~240px @1728 — because a mark this big needs most of its own width to
     disappear over or it reads as a hard cut rather than a fade. */
  -webkit-mask-image:linear-gradient(90deg,
    transparent 0, #000 14%, #000 86%, transparent 100%);
          mask-image:linear-gradient(90deg,
    transparent 0, #000 14%, #000 86%, transparent 100%);
}
/* Same two-copies-translated--50% construction as .marquee above, and it
   borrows that block's @keyframes rather than declaring a second one. */
.logos__track{
  display:flex; flex-shrink:0; min-width:100%;
  animation:marquee var(--marquee-duration,68s) linear infinite;
  will-change:transform;
}
.logos__group{
  display:flex; flex-shrink:0; align-items:center;
  gap:var(--logo-gap);
  padding-right:var(--logo-gap);
}
.logos__item{flex:0 0 auto; display:grid; place-items:center}

/* Every logo is fitted into the SAME box and contained within it, which is
   what makes them read as a set. Constraining one axis only (a shared cap
   height) leaves wide wordmarks looking small next to square marks — the
   box has to bound both. --logo-scale trims any that still sit heavy.
   Carried over from the grid unchanged except for the dropped min(100%,…),
   which existed only to respect a column that no longer exists. */
.logos__item img{
  width:calc(clamp(140px, 12.5vw, 240px) * var(--logo-scale, 1));
  height:calc(clamp(90px, 7.8vw, 150px) * var(--logo-scale, 1));
  object-fit:contain;
  object-position:center;
}
@media (prefers-reduced-motion:reduce){
  .logos__track{animation:none}
}

/* ── Hero intro ────────────────────────────────────────────────
   The start state hangs off `.hero-intro`, which only JS adds — so with
   no script the copy renders solid rather than blank. `.is-in` releases
   it. Lines carry --i from splitLines(), which staggers them.

   `translate` rather than `transform`: the badge and title already carry
   transforms of their own, and translate composes instead of clobbering.
   ────────────────────────────────────────────────────────────── */
/* Pre-paint state, set by the inline <head> script. :where() keeps this at
   one class of specificity so the rules below beat it without a fight. The
   title is hidden as a whole here because its .line__i children do not
   exist until splitLines() has run. */
:where(html.js-intro .js-hero) .about-hero__title,
:where(html.js-intro .js-hero) .about-hero__lede,
:where(html.js-intro .js-hero) .about-hero__badge,
:where(html.js-intro .js-hero) .hero__mark{opacity:0}

/* Once JS has split the heading, the lines own the animation and the
   title container hands its opacity back. */
.hero-intro .about-hero__title{opacity:1}

/* An ANIMATION, not a class-toggled transition. The old release hung off a
   double requestAnimationFrame, and rAF does not run at all while a tab is
   hidden — open one of these pages in a background tab and `is-in` never
   landed, so the copy sat at opacity 0 with nothing to play it back.
   `both` fill means this runs the moment the element is rendered, whenever
   that turns out to be.

   Fade + rise is the site's own language, the same pairing elementBlurin()
   scrubs on scroll — see DEVELOPMENT.md. Lines carry --i from splitLines(),
   which staggers them; the lede follows the last line rather than racing it. */
.hero-intro .about-hero__title .line__i,
.hero-intro .about-hero__lede,
.hero-intro .about-hero__badge,
.hero-intro .hero__mark{
  animation:hero-up .8s var(--ease) both;
  animation-delay:calc(var(--i, 0) * .09s);
  /* Held at the start until the copy is actually on screen. On a phone this
     hero is ~770px tall and the copy sits at its foot, so on load the
     animation was running below the fold and was over before it could be
     seen — `both` fill means the paused state is frame one, not blank. */
  animation-play-state:paused;
}
.hero-intro .about-hero__lede{animation-delay:.22s}
.hero-intro .about-hero__badge{animation-delay:.34s}   /* last in, after the lede */
/* The homepage lockup waits longer than any other hero's copy, so the video
   underneath gets a beat to read as motion before the logo lands on top of
   it. This is the one number here that is a taste call rather than a
   measurement — it replaces the old 2000ms setTimeout in initHeroMark(). */
.hero-intro .hero__mark{animation-delay:.9s}
.hero-intro.is-inview .about-hero__title .line__i,
.hero-intro.is-inview .about-hero__lede,
.hero-intro.is-inview .about-hero__badge,
.hero-intro.is-inview .hero__mark{animation-play-state:running}

@keyframes hero-up{
  from{opacity:0; translate:0 24px}
  to  {opacity:1; translate:none}
}

/* Hand-off to the scroll system. An animation with `both` fill outranks
   inline style for as long as it exists, so on the homepage — where the
   lockup is both the entrance target and initHero()'s scroll target — the
   scroll fade would be silently ignored for good. initHeroIntro() adds
   `is-entered` on animationend; dropping the animation returns the node to
   inline control. opacity/translate are restated because `js-intro` may
   still be on <html> at that moment, and its 0-1-0 rule would otherwise
   take the element straight back to invisible.

   Specificity is matched deliberately: the .line__i selector above is
   0-3-0, so its release has to be 0-4-0 to win. */
.hero-intro .about-hero__title .line__i.is-entered,
.hero-intro .about-hero__lede.is-entered,
.hero-intro .about-hero__badge.is-entered,
.hero-intro .hero__mark.is-entered{
  animation:none; opacity:1; translate:none;
}

/* A resize re-splits the heading, which builds fresh .line__i elements and
   would replay the entrance on every tick. `is-settled` pins them at the
   end state instead. */
.hero-intro.is-settled .about-hero__title .line__i,
.hero-intro.is-settled .about-hero__lede,
.hero-intro.is-settled .about-hero__badge,
.hero-intro.is-settled .hero__mark{
  animation:none; opacity:1; translate:none;
}

@media (prefers-reduced-motion:reduce){
  .hero-intro .about-hero__title .line__i,
  .hero-intro .about-hero__lede,
  .hero-intro .about-hero__badge,
  .hero-intro .hero__mark{animation:none; opacity:1; translate:none}
}

/* ═════════════════════════════════════════════════════════════
   TESTIMONIALS  (Figma 1360:8116)
   ═════════════════════════════════════════════════════════════ */
/* The section is the positioning context for the star, which bleeds past
   the gutter on the left. */
/* .cta carries .is-flush, which cancels the page stack's gap — this was
   the one boundary on the page with zero space, so the quote's last line
   butted straight into the cream. The padding restores the page rhythm
   without giving up the flush background. */
.quotes{position:relative}
.quotes:has(+ .cta.is-flush){padding-bottom:var(--section-gap)}
.quotes__star{
  position:absolute;
  left:0; top:clamp(-60px,-4.5vw,-130px);
  width:clamp(220px,26vw,500px);
  transform:translateX(-34%);
  z-index:0;
  pointer-events:none;
}
.quotes .stack{position:relative;z-index:1}

.quotes__bar{
  display:flex; align-items:flex-end; justify-content:space-between;
  gap:var(--s-40);
  font-size:var(--t-badge);
  font-weight:500;
}
.quotes__nav{display:flex;gap:var(--s-24);align-items:center}
.quotes__nav button{
  font:inherit; color:inherit;
  transition:opacity .25s var(--ease);
}
@media (hover:hover){ .quotes__nav button:hover{opacity:.55} }
.quotes__nav button:focus-visible{outline:2px solid var(--navy);outline-offset:4px}

/* 12-column grid, per the artboard overlay: author in the left three,
   quote from column 4 to the end. Grid placement replaces the ad-hoc
   text-indent that was faking the offset before. */
.quotes__body{
  display:grid;
  grid-template-columns:repeat(12,minmax(0,1fr));
  gap:var(--s-20);
  align-items:start;
}
.quotes__author{grid-column:1 / span 4}
.quotes__text  {grid-column:5 / span 8}

/* Size lives on the classes, not on `.quotes__author p` — an element+class
   selector is 0-1-1 and silently outranks a bare 0-1-0 class, which is why
   the role stayed at 30px. */
.quotes__author p{margin:0;line-height:1.3}
.quotes__name{font-weight:700;font-size:var(--t-body)}      /* 30px @1920 */
.quotes__role{font-weight:400;font-size:var(--t-body-sm)}   /* 20px @1920 */

/* .h-display supplies family, weight, line-height, tracking and casing —
   only the size differs, so it no longer redeclares the primitive. */
.quotes__text{
  font-size:var(--t-quote);
  margin:0;
}


/* Slide swap: height and opacity share one duration and curve so the box
   resizes as the new quote fades in, not before it. JS writes the explicit
   px heights — `auto` is not an animatable value. */
.js-quotes{
  overflow:hidden;
  transition:height .45s var(--ease-inout);
}
.quotes__slide{transition:opacity .45s var(--ease-inout)}
.js-quotes.is-swapping .quotes__slide{opacity:0}

@media (prefers-reduced-motion:reduce){
  .js-quotes,
  .quotes__slide{transition:none}
}

/* ═════════════════════════════════════════════════════════════
   FINAL CTA  (Figma 1360:8108)
   ═════════════════════════════════════════════════════════════ */
.cta{
  position:relative;
  background:var(--offwhite);
  overflow:hidden;
  padding-block:clamp(72px,10.4vw,200px);
  text-align:center;
}
.cta__inner{
  position:relative; z-index:2;
  display:flex; flex-direction:column; align-items:center;
  /* 20 from the script line to the paragraph, per the revisions; the button
     keeps its 40 by taking the other 20 as a margin. Phones reset that
     margin in the 640 block, where both gaps are Figma's 20. */
  gap:var(--s-20);
  margin-inline:auto;
  max-width:1250px;
}
.cta__inner > .btn{margin-top:var(--s-20)}
.cta__body{max-width:56ch;margin:0;font-size:var(--t-body)}
.cta__star{
  position:absolute; z-index:1;
  pointer-events:none;
  width:clamp(180px,20vw,385px);
}
.cta__star--l{left:0; bottom:6%;  transform:translateX(-24%)}
.cta__star--r{right:0; top:-6%;   transform:translateX(26%)}

/* ═════════════════════════════════════════════════════════════
   FOOTER  (Figma 1360:8130)
   ═════════════════════════════════════════════════════════════ */
.foot{
  background:var(--navy);
  color:var(--cream);
  padding-top:clamp(56px,6.25vw,120px);
  padding-bottom:clamp(40px,3.3vw,64px);
  display:flex; flex-direction:column; gap:var(--s-80);
}
.foot__top{
  display:grid;
  grid-template-columns:repeat(4,minmax(0,1fr));
  gap:var(--s-40);
}
.foot__brand{
  grid-column:1;
  display:flex; flex-direction:column; gap:var(--s-32);
}
.foot__logo{position:relative;display:block;width:min(100%,240px);aspect-ratio:140/60.449}
.foot__logo img{width:100%;height:100%}
.foot__logo .nav__logo__wordmark,
.foot__logo .nav__logo__tagline{position:absolute}
.foot__tagline{margin:0;font-size:var(--t-badge);line-height:1.4}

.foot__links{
  grid-column:3 / span 2;
  display:flex; gap:clamp(32px,6.25vw,120px);
  font-size:var(--t-badge);
}
.foot__col{display:flex;flex-direction:column;gap:var(--s-24)}
.foot__col h3{margin:0;font-size:inherit;font-weight:700;text-transform:uppercase}
.foot__col ul{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:var(--s-16)}
.foot__col a{
  text-decoration:none;
  transition:opacity .25s var(--ease);
}
@media (hover:hover){ .foot__col a:hover{opacity:.6;text-decoration:underline;text-underline-offset:.2em} }

.foot__bottom{display:flex;flex-direction:column;gap:var(--s-40)}
.foot__rule{height:1px;background:var(--blue);opacity:.3}
.foot__row{
  display:flex; align-items:center; justify-content:space-between;
  gap:var(--s-24);
  font-size:clamp(12px,.73vw,14px);
  flex-wrap:wrap;
}
.foot__social{display:flex;gap:var(--s-24);align-items:center}
/* Both labels ship and one is hidden per breakpoint, the same way the nav's
   bars/X pair works. The anchor keeps an aria-label, so the abbreviation is
   never what a screen reader announces. */
.foot__social-ab{display:none}
.foot__social a{
  font-size:16px;
  font-weight:700;
  text-transform:uppercase;
  line-height:1;
  text-decoration:none;
  transition:opacity .25s var(--ease);
}
@media (hover:hover){ .foot__social a:hover{opacity:.6;text-decoration:underline;text-underline-offset:.2em} }

/* Cancels the page stack's gap so two sections butt up against each other.
   Only valid on children of <main> — that is where the gap lives. On the
   footer (a sibling of <main>) there is nothing to cancel and it would
   simply drag the element 160px up over the section above. */
.is-flush{margin-top:calc(var(--section-gap) * -1)}

@media (max-width:1024px){
  /* The badge stays top-right of the heading, as on desktop, rather than
     dropping to its own row on the left. Two tracks instead of the 12-col
     split: the heading takes what the badge leaves, so a narrow tablet
     cannot squeeze the badge's column below its 150px floor. The mobile
     block below hides the badge and spans the title across both. */
  .clients__head{grid-template-columns:minmax(0,1fr) auto}
  .clients__title{grid-column:1}
  .clients__badge{grid-column:2;justify-self:end;width:var(--badge-size)}
  .quotes__body{grid-template-columns:1fr;gap:var(--s-32)}
  .quotes__author,
  .quotes__text{grid-column:1 / -1}
  .quotes__star{width:clamp(120px,26vw,200px);transform:translateX(-45%)}
  .foot__top{grid-template-columns:1fr;gap:var(--s-60)}
  .foot__brand,.foot__links{grid-column:1}
  .foot__links{flex-wrap:wrap;gap:var(--s-40)}
}

/* ═════════════════════════════════════════════════════════════
   SITE MENU  (Figma desktop 1360:8950 · mobile 1360:9319)
   Full-screen overlay. Desktop puts the items bottom-left with the
   badge bottom-right; mobile stacks badge → items → socials.
   ═════════════════════════════════════════════════════════════ */
/* Flies in the way JumpFactory's does: the panel itself slides down from
   `top:-100%`, and the content inside fades up on the same 700ms curve —
   two layered transitions, not a single fade. */
.menu{
  position:fixed;
  top:-100%; left:0;
  width:100%; height:100dvh;
  z-index:50;
  background:var(--blue);
  color:var(--cream);
  display:flex; flex-direction:column;
  overflow-y:auto;
  visibility:hidden;
  will-change:top;
  transition:top .7s var(--ease-inout), visibility .7s;
}
.menu.is-open{top:0;visibility:visible}

/* inner fade, staged behind the slide */
.menu__body{
  opacity:0;
  transition:opacity .7s var(--ease-inout);
}
.menu.is-open .menu__body{opacity:1}

/* ── body ─────────────────────────────────────────────────── */
.menu__body{
  flex:1 1 auto;
  display:flex; flex-direction:column; justify-content:flex-end;
  gap:var(--s-60);
  padding:var(--gutter);
  /* Clears the nav, which is no longer part of this panel but floats above
     it — .nav's own padding plus the logo height, so the list starts where
     the old duplicate bar used to end. Must stay after the shorthand. */
  padding-top:calc(var(--s-20) * 2 + clamp(43px, 5.05vw, 97px));
  position:relative;
}
.menu__items{display:flex;flex-direction:column;gap:var(--s-10);max-width:956px}

/* ── one item ─────────────────────────────────────────────── */
.menu__item{
  display:block; width:100%; text-align:left;
  color:var(--cream); text-decoration:none;
  cursor:pointer;
  transition:opacity .25s var(--ease);
}
@media (hover:hover){ .menu__item:hover{opacity:.62} }
.menu__item:focus-visible{outline:2px solid var(--cream);outline-offset:6px}

.menu__row{display:flex;align-items:center;gap:clamp(12px,2vw,40px)}
.menu__label{
  font-family:var(--font-display);
  font-weight:700;
  font-size:var(--t-heading);
  line-height:1;
  letter-spacing:-0.02em;
  text-transform:uppercase;
  margin-block:calc(-0.08em) calc(-0.06em);
  white-space:nowrap;
}
.menu__plus{
  flex:0 0 auto;
  width:clamp(20px,2.2vw,42px); aspect-ratio:1;
  transition:transform .42s var(--ease-inout);
}
.menu__plus img{width:100%;height:100%}
.menu__item[aria-expanded="true"] .menu__plus{transform:rotate(45deg)}   /* + becomes × */

/* ── dropdown — same collapse as .services__desc ──────────── */
.menu__sub{
  display:grid;
  grid-template-rows:0fr;
  opacity:0;
  transition:grid-template-rows .45s var(--ease-inout),
             opacity           .3s var(--ease-inout),
             padding           .45s var(--ease-inout);
  padding-block:0;
}
.menu__sub > div{overflow:hidden;min-height:0;display:flex;flex-direction:column;gap:var(--s-10)}
.menu__item[aria-expanded="true"] + .menu__sub{
  grid-template-rows:1fr;
  opacity:1;
  padding-block:var(--s-20);
}
.menu__sublink{
  display:block;
  padding-left:clamp(20px,2.1vw,40px);
  font-family:var(--font-display);
  font-weight:700;
  font-size:var(--t-body-lg);
  line-height:1;
  letter-spacing:-0.02em;
  text-transform:uppercase;
  color:var(--cream);
  text-decoration:none;
  transition:opacity .25s var(--ease);
}
@media (hover:hover){ .menu__sublink:hover{opacity:.62} }
.menu__sublink:focus-visible{outline:2px solid var(--cream);outline-offset:4px}

/* ── socials ──────────────────────────────────────────────── */
.menu__social{display:flex;gap:var(--s-24);align-items:center}
.menu__social a{
  font-family:var(--font-display);
  font-weight:700;
  font-size:var(--t-body-lg);
  line-height:1;
  letter-spacing:-0.02em;
  text-transform:uppercase;
  color:var(--cream);
  text-decoration:none;
  transition:opacity .25s var(--ease);
}
@media (hover:hover){ .menu__social a:hover{opacity:.62;text-decoration:underline;text-underline-offset:.18em} }

/* ── badge — bottom-right on desktop, above the list on mobile ── */
.menu__badge{
  position:absolute;
  right:var(--gutter); bottom:var(--gutter);
  width:clamp(180px,15.1vw,290px); aspect-ratio:1;
  display:grid; place-items:center;
  text-decoration:none;
}
/* Navy ink, not the cream used on the page badges — and it has to stay navy
   on hover too. `.badge-spin:hover .badge__label` turns it white, which would
   disappear against this badge's light-blue/cream star. */
.menu__badge .badge__label,
.menu__badge:hover .badge__label,
.menu__badge:focus-visible .badge__label{color:var(--navy)}
.menu__badge:focus-visible{outline:2px solid var(--cream);outline-offset:6px;border-radius:50%}
.menu__badge--mobile{display:none}

@media (prefers-reduced-motion:reduce){
  .menu,.menu__sub,.menu__plus,.menu__body{transition:none}
}

/* ── mobile ───────────────────────────────────────────────── */
@media (max-width:1024px){
  .menu__body{
    justify-content:flex-end;
    gap:var(--s-40);
    padding:0 0 var(--s-24);
    padding-top:calc(var(--s-16) * 2 + 43px);   /* clears the fixed nav */
  }
  .menu__items{padding-inline:24px;gap:4px;max-width:none}
  .menu__label{font-size:40px;letter-spacing:-0.02em}
  .menu__sublink{font-size:28px}
  .menu__social{padding-inline:24px;gap:var(--s-16)}
  .menu__social a{font-size:28px}

  /* badge leaves the corner and sits above the list */
  .menu__badge{display:none}
  .menu__badge--mobile{
    display:grid;
    position:relative;          /* NOT static — .badge-spin__star is absolute
                                   and would otherwise resolve against
                                   .menu__body and cover the whole panel */
    inset:auto;
    width:148px; aspect-ratio:1;   /* square: Figma's star is 1:1 inside its
                                      193x148 frame, so match the star not
                                      the frame */
    margin-left:24px;
    place-items:center;
    flex:0 0 auto;
  }
  .menu__badge--mobile .badge__label{font-size:14px;line-height:1.2}
}

/* ═════════════════════════════════════════════════════════════
   MOBILE  (Figma: Homepage / Mobile, 1360:8132 — 390px artboard)

   Everything below 640px. The mobile board is not the desktop board
   scaled down: sections change composition, decorations disappear,
   and several are drawn to fixed pixel sizes rather than a ratio.
   Type sizes are handled by the token block in base.css; this file
   owns geometry only.

   Note the two gutters. Most mobile sections are px-28 (--gutter),
   but the footer and testimonials are drawn at 24 and override it.
   ═════════════════════════════════════════════════════════════ */
@media (max-width:640px){

  /* ── Decorative scripts under headings — SITEWIDE ─────────────
     Every handwritten line that tucks under a heading sits at the same
     offset on mobile. About's two statements had this already and the
     others did not, so "Love Cleanly", "After Cleanly" and "Let Cleanly
     help!" each sat a few pixels differently against their headline.

     The -0.06em is the tuck; the +4px is the correction About worked out —
     at -0.06em of a 40px heading the script lands 2.4px high and reads as
     crowded under the last line. One rule now, so a new script inherits it
     rather than needing its own number.

     This block is otherwise the homepage's, but these three classes appear
     across all four pages and sections.css is loaded by all of them.

     NOT included, and deliberately: .services__script is absolutely
     positioned over the services photo, and .ellipse-head__text is its own
     section. Neither tucks under a heading, so neither wants this. */
  .serve__script,
  .benefits__script,
  .cta__script{
    margin-top:calc(var(--t-heading) * -0.06 + 4px);
  }

  /* ── Loader (mobile_door_hanger_390x844) ──────────────────────
     Mobile is not a scaled desktop. The tag is framed completely
     differently: centred rather than in the left third, three quarters
     of the width rather than a third, and WHOLE — on desktop its top is
     cut off above the viewport, here the dome sits in frame with room
     above it. Measured off the board, not derived.

     Because the whole tag is visible, the travel changes too: it has to
     start and finish entirely off screen, which is further than the
     desktop's -90.99% / 123.07%. The durations and the spring do not
     change — only the distance. */
  .loader__tag{
    /* Sized against viewport WIDTH here, where desktop uses height. On a
       phone the width is what constrains the tag, and the reference frames
       it as a share of the screen: the artwork fills 88% of it, leaving an
       even gutter. A fixed px box was the problem — pinned at 374 it read
       correctly at 390 and shrank to three quarters of the screen on
       anything wider, which is why it looked small.

       94.17vw is the BOX. The artwork is 93.45% of the box, so 88 / 0.9345.

       The margin is not half the width: the artwork sits at 45.215% of its
       own box, not 50%, so centring the box puts it left of the axis. This
       centres the ARTWORK — verified as an even gutter on both sides. */
    left:50%;  margin-left:-42.58vw;
    width:94.17vw;
    height:auto;                            /* desktop sets a vh height */

    /* Rest position, from the reference: the tag HANGS, it is not centred.
       Its bottom sits at 69% of the viewport and the dome runs off the top
       edge — the same relationship desktop has. Anchoring the bottom rather
       than the top is what keeps that true at any phone height. */
    top:auto;  bottom:calc(31% - 100px);   /* 69% of the height, then 100px
                                             lower per the revision */
  }
  /* 9.51% of the box, which is 60px on Figma's 631 — so the copy scales
     with the tag rather than with the page. */
  .loader__line{font-size:8.955vw}

  /* Travel in calc, not percentages of the tag: a percentage is a share of
     the TAG, but the distance needed to clear the screen belongs to the
     VIEWPORT. The tag is 162.37vw tall and its bottom rests at 69vh + 100px.
     The exit keeps its margin either way: dropping the rest position moves
     the tag TOWARDS the bottom edge, so it needs less, not more. */
  @keyframes loader-tag{
    0%{
      /* Carries the 100px the rest position dropped by — without it the
         tag starts 100px short of clearing the top edge. */
      translate:0 calc(-70vh - 110px);       /* bottom above the top edge */
      animation-timing-function:linear(
        0, .0099, .042, .1025, .1722, .2512, .3358, .4358, .5247, .6117,
        .6938, .7716, .8494, .9111, .966, 1.0123, 1.0556, 1.0864, 1.1093,
        1.1259, 1.1364, 1.142, 1.1451, 1.142, 1.1364, 1.1284, 1.1185, 1.108,
        1.0963, 1.0833, 1.0716, 1.0605, 1.05, 1.0401, 1.0296, 1.0216, 1.0148,
        1.0123, 1.0062, 1.0012, 1);
    }
    47.06%{ translate:0 0; animation-timing-function:linear }
    76.47%{ translate:0 0; animation-timing-function:cubic-bezier(.77,.01,.19,1) }
    100%  { translate:0 calc(32vh + 163vw) } /* top below the bottom edge */
  }

  /* ── Nav (1360:8165) ──────────────────────────────────────── */
  .nav{padding:var(--s-16) var(--gutter)}
  .nav__quote{display:none}                    /* pill -> envelope icon */
  .nav__quote-icon{display:block;width:32px}
  .nav__quote-icon img{width:100%;height:auto}
  .nav__menu{width:32px}
  .nav__logo{width:125px}                     /* the same +25% bump desktop
                                                 got, applied here too */

  /* ── Hero (1360:8133) ─────────────────────────────────────── */
  /* The cream building-mask starburst is a desktop-only decoration —
     the mobile hero carries the partner badge and nothing else. */
  .badge-mark{display:none}

  /* Visible at rest. The entrance is the shared `hero-up` animation now,
     armed by `hero-intro` and hidden pre-paint by `js-intro` — both of
     which only exist once script has run. It used to rest at opacity:0 and
     depend on JS adding `.is-in`, so a failed main.js left the lockup
     invisible for good. That is the resting-state rule in base.css and
     DEVELOPMENT.md, and this was the one element breaking it.

     Centred with `inset:0; margin:auto`, NOT the usual
     left/top:50% + translate(-50%,-50%). The scroll coupling writes the
     individual `scale` property, and CSS composes that BEFORE the
     `transform` property — so a percentage translate inside `transform`
     gets scaled along with everything else. Measured: at scale .7 the
     lockup slid 93px right, exactly half its width times .3. Auto margins
     centre without a transform, so there is nothing left to distort. */
  .hero__mark{
    display:block;
    position:absolute;
    inset:0;
    margin:auto;
    width:72%;
    aspect-ratio:140 / 60.449;
    z-index:3;
    pointer-events:none;
  }

  /* Every hero badge sits tilted on mobile */
  .badge-link,
  .about-hero__badge{transform:rotate(-12deg)}
  .badge-link{right:30px;bottom:80px}   /* dropped 20px off the Figma value */

  /* ── What we offer (1360:8145) ────────────────────────────── */
  /* The layout lives in the max-width:1024px block near the top of the
     Responsive section — tablet uses the mobile layout too.

     Phones only: the photo is the same height as the slider cards (.card
     below, 288px) rather than a 333:224 ratio, so the page's photos share
     one height instead of the services one growing with the width. The
     image is object-fit:cover, so the extra width just crops. */
  .services__panel img{aspect-ratio:auto; height:288px}

  /* ── Marquee (1360:8146) ──────────────────────────────────── */
  .marquee{padding-block:var(--s-20)}

  /* ── Benefits (1360:8147) ─────────────────────────────────── */
  /* The script clears the headline by its measured height rather than
     an em guess: at 40px the mobile headline wraps to two lines, and
     Figma pins the script at 75.84 of a 106.8px group — i.e. just
     under those two lines. 2 lines x 1em + a hair is the same thing
     expressed in a way that survives a re-wrap. */
  /* Full width on phones, per the revisions: a card spans the content
     column gutter to gutter, one at a time. It was capped at 332px, which
     left wider phones with a small card and a lot of empty rail. */
  .rail{--rail-gap:var(--s-20); --card-w:calc(100vw - 2 * var(--gutter))}
  .card{
    height:288px;              /* Figma 240 +20% */
    padding:var(--s-20);
  }

  /* ── Clients (1360:8148) ──────────────────────────────────── */
  .clients__title{grid-column:1 / -1;text-align:center}
  .clients__badge{display:none}               /* not in the mobile board */
  /* Each logo is drawn at its own size in Figma — a shared contain-box
     cannot serve a wide wordmark and a tall roundel at once, which is
     what made Toyota read huge and Jetset tiny.

     --m-scale trims a mark that still sits heavy at the Figma size. It is
     deliberately NOT --logo-scale: that one is a desktop trim on Toyota,
     whose mobile size is already right, so reusing it would shrink Toyota
     here too. --mw/--mh stay the untouched Figma reference values. */
  .logos{--logo-gap:72px;--logo-pad:48px}
  .logos__item img{
    width:calc(var(--mw) * var(--m-scale, 1));
    height:calc(var(--mh) * var(--m-scale, 1));
    max-width:none;
  }

  /* ── Testimonials (1360:8150) ─────────────────────────────── */
  .quotes{padding-inline:24px}                /* drawn at 24, not 28 */
  .quotes__star{
    top:24px;                                 /* was -60: the collision */
    width:143px;
    transform:translateX(-37%);
  }
  .quotes__bar > p,
  .quotes__bar .js-quotes-count{opacity:.5}
  .quotes__body{gap:var(--s-40)}
  .quotes__author p{line-height:1.4}
  .quotes__text{text-indent:40px}

  /* ── Final CTA (1360:8163) ────────────────────────────────── */
  /* Desktop centres this block; mobile sets it flush to the gutter. */
  .cta{padding-block:var(--s-80);text-align:left}
  .cta__inner{align-items:flex-start;gap:var(--s-20)}   /* Figma gap-20 */
  .cta__inner > .btn{margin-top:0}                      /* 20 to the button too */
  .cta__body{max-width:none}
  /* Accents are small punctuation here, not the 180px slabs the
     desktop clamp floors at. */
  .cta__star{width:auto}
  .cta__star--l{
    left:20px; top:47px; bottom:auto;
    width:76px; transform:none;
  }
  .cta__star--r{
    right:15px; top:155px;
    width:64px; transform:rotate(45deg);
  }

  /* ── Footer (1360:8164) ───────────────────────────────────── */
  .foot{
    padding:var(--s-40) 24px var(--s-24);     /* drawn at 24, not 28 */
    gap:var(--s-40);
  }
  .foot__top{gap:var(--s-40)}
  /* The same lockup the nav carries. It was set to fill the column, which
     at this width blew it up to ~340px; a fixed 200 keeps it a logo. */
  .foot__logo{width:200px;max-width:none}
  .foot__tagline{font-size:15px;line-height:1.5}
  /* Services | Site on row one, Legal under Services — a 2-col grid,
     not a wrapped flex row with a 40px gap. */
  .foot__links{
    display:grid;
    grid-template-columns:repeat(2,minmax(0,1fr));
    column-gap:8px; row-gap:var(--s-20);
    font-size:15px;
  }
  .foot__col ul{gap:8px}
  .foot__bottom{gap:var(--s-20)}
  /* Abbreviated at this width — "in / tt / li", matching .menu__social. */
  .foot__social-full{display:none}
  .foot__social-ab{display:inline}
  .foot__row{
    flex-direction:column;
    align-items:flex-start;
    gap:var(--s-20);
  }
}

/* ═════════════════════════════════════════════════════════════
   ABOUT US  (Figma: About Us / Desktop 1360:8310, Mobile 1360:8798)

   Only three blocks live here. The intro and vision statements reuse
   .serve__* verbatim, both card rails reuse .rail/.card, and the
   industries header is the homepage's .stack + .t-badge + .h-display
   pattern — its Figma gaps (60 desktop / 20 mobile) are already what
   those rules produce.
   ═════════════════════════════════════════════════════════════ */

/* .card is justify-content:space-between, which only pins the body to
   the bottom while a number is also present. The values and industries
   cards have no number. No-op on the homepage cards. */
.card__body{margin-top:auto}

/* ── Hero (1360:8311) ──────────────────────────────────────────
   Same shell as the homepage hero — .hero supplies the height and the
   scroll choreography — but the copy sits INSIDE it, anchored bottom,
   rather than in a section below. Photo, not video. */
.about-hero{display:flex;flex-direction:column;justify-content:flex-end}
.about-hero__copy{
  position:relative; z-index:3;
  display:flex; align-items:flex-end; gap:var(--s-40);
  padding-block:var(--s-80);
}
.about-hero__text{
  flex:1 1 auto; min-width:0;
  display:flex; flex-direction:column; gap:var(--s-40);
  color:var(--cream);
}
.about-hero__title{font-size:var(--t-heading-lg)}
.about-hero__lede{
  margin:0;
  max-width:61%;                  /* Figma 1079.6 of 1760 */
  font-size:var(--t-body-lg);     /* p/desktop-large 40px @1920 */
  font-weight:700;
  line-height:1.2;
}
/* Not .badge-link — that one is absolutely positioned into the hero's
   corner. This one is a flex sibling of the copy. */
.about-hero__badge{
  position:relative;
  flex:0 0 auto;
  width:var(--badge-size); aspect-ratio:1;
  display:grid; place-items:center;
  text-decoration:none;
}
.about-hero__badge:focus-visible{outline:2px solid var(--white);outline-offset:6px;border-radius:50%}

/* ── Team (1360:8363) ────────────────────────────────────────── */
.team__grid{
  display:grid;
  grid-template-columns:repeat(12,minmax(0,1fr));
  column-gap:var(--s-40); row-gap:var(--s-40);
}
.team__member{
  grid-column:span 4;
  display:flex; flex-direction:column; gap:var(--s-40);
}
.team__photo{
  width:100%; max-width:384px;     /* 320 +20% */
  aspect-ratio:440 / 520;
  object-fit:cover;
  border-radius:var(--r-card);   /* Figma draws 25.5 here; house rule is 10 */
}
.team__bio{display:flex;flex-direction:column;gap:var(--s-40);color:var(--navy)}
.team__name{margin:0;font-size:var(--t-body-lg);font-weight:700;line-height:1.2}
.team__blurb{margin:0;font-size:var(--t-body);font-weight:400;line-height:1.3}
.team__mail{
  font-size:var(--t-body); line-height:1.3;
  text-decoration:underline; text-underline-offset:.15em;
  transition:opacity .25s var(--ease);
}
@media (hover:hover){ .team__mail:hover{opacity:.6} }

/* ── Industries (1360:8389) ──────────────────────────────────── */
.industries{display:flex;flex-direction:column;gap:var(--s-80)}
/* +20% on the shared card height. Scoped so the homepage benefits rail
   and the values rail above keep theirs. */
/* TRANSITIONAL. The About page still emits this ancestor class because its
   template has not moved to content yet. Feeding the same token rather than
   setting height directly keeps one source of truth; delete this line when
   the template emits .rail--tall. */
.industries .rail{--card-h:clamp(438px,36vw,761px)}

/* ── FAQ (1360:8427) ─────────────────────────────────────────── */
.faq{
  display:flex; flex-direction:column;
  gap:var(--s-80);
}
/* .is-flush on the next section cancels the page stack's gap, so whatever
   sits directly before it has to put that space back itself. Scoped with
   :has() — on the case study a marquee follows the FAQ and the padding
   would be dead space. */
.faq:has(+ .cta.is-flush){padding-bottom:var(--section-gap)}
.faq__title{font-size:var(--t-heading-lg)}
.faq__list{display:flex;flex-direction:column;gap:var(--s-20)}
.faq__item{
  display:block; width:100%;
  text-align:left;
  color:var(--navy);
  cursor:pointer;
}
.faq__row{
  display:flex; align-items:center; justify-content:space-between;
  gap:var(--s-20);
  font-family:var(--font-display);
  font-weight:700;
  font-size:var(--t-body-lg);      /* 40px @1920, 20px @390 — both match */
  line-height:1;
  letter-spacing:-0.02em;
  text-transform:uppercase;
}
/* Navy at rest; blue on hover and while open. The answer has no colour
   of its own, so it follows the item. */
.faq__item{transition:color .28s var(--ease)}
.faq__item:focus-visible,
.faq__item[aria-expanded="true"]{color:var(--blue)}
/* Hover only where a cursor actually exists. On touch there is no pointer to
   leave, so :hover latches after a tap and the item stays blue as though it
   were still open. */
@media (hover:hover){
  .faq__item:hover{color:var(--blue)}
}
.faq__plus{flex:0 0 auto;transition:transform .45s var(--ease-inout)}
.faq__item[aria-expanded="true"] .faq__plus{transform:rotate(45deg)}   /* + becomes × */
.faq__item:focus-visible{outline:2px solid var(--blue);outline-offset:6px}

/* Same collapsing row the services list and the menu dropdowns use. */
.faq__a{
  display:grid;
  grid-template-rows:0fr;
  opacity:0;
  padding-block:0;
  transition:grid-template-rows .5s var(--ease-inout),
             opacity           .35s var(--ease-inout) .05s,
             padding           .5s var(--ease-inout);
}
.faq__a > span{
  overflow:hidden; min-height:0; display:block;
  font-family:var(--font-body);
  font-weight:400;
  font-size:var(--t-body);
  line-height:1.3;
  letter-spacing:normal;
  text-transform:none;
  /* no colour of its own — inherits the item's blue, like the question */
  max-width:59ch;
}
.faq__item[aria-expanded="true"] .faq__a{
  grid-template-rows:1fr;
  opacity:1;
  padding-block:var(--s-20);
}
@media (prefers-reduced-motion:reduce){
  .faq__a,.faq__plus{transition:none}
}

@media (max-width:640px){
  /* Figma's About frames are drawn at 24, not the 28 the reused
     components use — same split .quotes and .foot already have. */
  .about-hero__copy,
  .team,
  .faq{padding-inline:24px}

  /* About's two statement blocks (intro and vision). Both are .serve__* ,
     which the homepage's "Who we service" and the case study's "Problem"
     also use — hence the modifier rather than a bare .serve__ override.
     Same precedent as .faq--sm and .benefits--wide.

     The script's 4px drop moved to the sitewide rule at the top of the
     640 block — every heading script shares it now, not just these two.
     The gap to the body copy comes down from the stack's 40 to 20. */
  .serve--tight .stack{gap:var(--s-20)}

  .about-hero__copy{padding-block:24px;gap:0}
  .about-hero__text{gap:var(--s-20)}
  .about-hero__lede{max-width:none;font-size:var(--t-body);line-height:1.4}
  .about-hero__badge{display:none}          /* not on the mobile board */
  /* ...except on Contact, where the badge is part of the hero. It stacks
     above the copy rather than beside it, so the lede keeps full width. */
  .about-hero__copy:has(.about-hero__badge--keep){flex-direction:column;align-items:stretch}
  .about-hero__badge--keep{
    display:grid;
    order:-1;
    align-self:flex-end;
    width:var(--badge-size);
    margin-bottom:var(--s-20);
  }

  .team__grid{grid-template-columns:1fr;row-gap:var(--s-40)}
  .team__member{grid-column:1;gap:var(--s-20)}
  .team__photo{max-width:264px;aspect-ratio:220 / 280}   /* 220 +20% */
  .team__bio{gap:var(--s-10)}
  .team__name{font-size:var(--t-body)}
  .team__blurb{font-weight:500}

  .industries{gap:var(--s-40)}
  /* the desktop clamp floors at 438, which is far too tall here — this is
     the mobile card height (288) with the same +20% applied */
  .rail--tall, .industries .rail{--card-h:346px}

  .faq{gap:var(--s-40)}
  .faq__list{gap:var(--s-10)}
  .faq__item[aria-expanded="true"] .faq__a{padding-block:var(--s-10)}
}

/* ═════════════════════════════════════════════════════════════
   BEFORE / AFTER SLIDER  (Figma: Case Study 1360:8232 / 1360:8717)

   Figma clips a full-width image inside a narrower frame — "Before" is
   880 wide holding an image still drawn at 1760. clip-path:inset() is
   the same idea in one property: it crops without resizing, so the
   before image never squashes as the divider travels.

   One custom property, --pos, drives the clip, the divider and the
   handle together, so they can never disagree.
   ═════════════════════════════════════════════════════════════ */
.ba{
  --pos:50%;
  position:relative;
  aspect-ratio:1760 / 720;
  touch-action:pan-y;          /* vertical page scroll still gets through */
  user-select:none;
}
/* The clip lives on an inner frame, not on .ba, so the handle can sit
   outside it. Clipping on .ba would slice the handle in half whenever
   the divider reached either end. */
.ba__frame{
  position:absolute; inset:0;
  border-radius:var(--r-card);
  overflow:hidden;
  background:var(--navy);
}
/* the three area pairs are stacked and crossfaded, the way
   .services__panel does it — no src swap, so no flash */
.ba__pair{
  position:absolute; inset:0;
  opacity:0;
  transition:opacity .35s var(--ease);
}
.ba__pair.is-active{opacity:1}
.ba__pair img{
  position:absolute; inset:0;
  width:100%; height:100%;
  object-fit:cover;
  pointer-events:none;
}
.ba__before{
  position:absolute; inset:0;
  clip-path:inset(0 calc(100% - var(--pos)) 0 0);
}
.ba__divider{
  position:absolute; top:0; bottom:0; left:var(--pos);
  width:2px; margin-left:-1px;
  background:var(--white);
  pointer-events:none;
  z-index:2;
}
.ba__label{
  position:absolute; top:clamp(16px,1.9vw,36px);
  font-family:var(--font-display);
  font-weight:700;
  font-size:var(--t-body-lg);         /* 40px @1920 — a step down from Headings/desktop-small */
  line-height:1;
  letter-spacing:-0.02em;
  text-transform:uppercase;
  color:var(--offwhite);
  pointer-events:none;
  z-index:2;
}
.ba__label--before{left:clamp(16px,2.64vw,51px)}
.ba__label--after {right:clamp(16px,2.09vw,40px)}

/* Same ellipse geometry as .btn--big (290.5 x 67), with the script face */
.ba__handle{
  position:absolute; left:var(--pos); top:50%;
  transform:translate(-50%,-50%);
  z-index:3;
  display:grid; place-items:center;
  width:clamp(129px,15.13vw,290px);
  aspect-ratio:290.5 / 67;
  border-radius:50%;
  background:var(--blue);
  border:2px solid var(--white);
  color:var(--white);
  font-family:var(--font-script);
  font-size:clamp(19px,2.14vw,41px);
  line-height:1;
  /* Brush Script's glyphs sit high in the em box, so centring the box
     leaves the word riding above the middle of the oval. The padding
     shrinks the content area from the top and pushes the centred text
     down by half of it. */
  padding-top:.30em;
  cursor:ew-resize;
  touch-action:none;
  transition:transform .25s var(--ease);
}
@media (hover:hover){ .ba:hover .ba__handle{transform:translate(-50%,-50%) scale(1.04)} }
.ba__handle:focus-visible{outline:3px solid var(--cream);outline-offset:4px}

.ba__tabs{
  display:flex; flex-wrap:wrap;
  gap:var(--s-20);
  justify-content:center;
}

@media (prefers-reduced-motion:reduce){
  .ba__pair,.ba__handle{transition:none}
}

/* ═════════════════════════════════════════════════════════════
   CASE STUDY  (Figma: Case Study / Desktop 1360:8166, Mobile 1360:8670)

   The hero, problem and solution statements, benefits rail,
   testimonials, FAQ, both marquees, the CTA and the footer are all
   lifted — only the three blocks below are new.
   ═════════════════════════════════════════════════════════════ */

/* The first tinted section on the site (Figma paints Benefits #D2E7F1).
   It bleeds full width, so the pad has to come back on the inside. */
.section--tint{
  padding-block:var(--s-80);
}
/* The property slider drops the band padding. Scoped to .benefits--wide
   rather than loosening .section--tint, which the Contact hours block also
   uses and still wants its pad. The compound selector also outranks the
   mobile .section--tint override further down, so it holds at both widths. */
.section--tint.benefits--wide{padding-block:0}

/* Figma draws these cards at 1160 rather than the homepage's 850 */
.benefits--wide .rail{--card-w:min(calc(100vw - 2 * var(--gutter)), 1160px)}
/* TRANSITIONAL — see the note on .industries .rail above. */
.benefits--wide .rail{--card-h:clamp(360px,37.5vw,720px)}

/* ── Solution — heading with the badge parked at the right ──── */
.solution__head{
  position:relative;
}
.solution__title{color:var(--blue)}
.solution__body{color:var(--blue)}
/* Absolute, like Figma (1360:8189). As a grid column its 276px height
   dictated the whole heading row and added 215px to the section. */
.solution__badge{
  position:absolute; top:0; right:0;
  width:var(--badge-size); aspect-ratio:1;
  display:grid; place-items:center;
  text-decoration:none;
}

/* ── Gameplan (1360:8213) ───────────────────────────────────── */
.gameplan{position:relative;overflow:hidden}
.gameplan__list{
  position:relative; z-index:1;
  display:flex; flex-direction:column; align-items:center;
  gap:var(--s-160);
}
.gameplan__item{
  display:flex; flex-direction:column; align-items:center;
  gap:var(--s-40);
  width:min(100%, 854px);
  text-align:center;
}
.gameplan__icon{width:clamp(28px,5.2vw,100px);height:auto}
.gameplan__title{margin:0}
.gameplan__body{
  margin:0;
  font-size:var(--t-body);
  font-weight:700;
  line-height:1.3;
}
/* Two oversized stars bleed off either side, behind the copy */
/* Offsets are POSITIONAL, never `transform`.

   These images carry `js-hero-star`, and initHero() writes an inline
   `transform: rotate(Ndeg)` to every one of those on each scroll tick.
   Inline style beats the stylesheet, so a `transform: translateX()` here
   was silently discarded the moment JS ran — the stars never moved, at any
   value. left/right are outside that fight.

   The base tilt uses the individual `rotate` property for the same reason:
   it composes with initHero()'s `transform` instead of being clobbered by
   it, so the star sits tilted and still turns with scroll depth.

   Desktop sits FLUSH to both edges. The rule used to read
   `left:0 + translateX(-55%)`, but the translate never survived the inline
   transform above, so flush is what this section has always actually
   rendered — and it is what it should keep rendering. Writing the -55%
   bleed as a real `left` offset would finally apply it and move the
   desktop stars 151px outward, which is a change nobody asked for. The
   Figma bleed is a one-line change here if it is ever wanted. */
.gameplan__star{
  --star-w:clamp(211px,29.3vw,562px);
  position:absolute; z-index:0;
  width:var(--star-w);
  opacity:.5;
  pointer-events:none;
}
.gameplan__star--l{left:0;  top:18%}
.gameplan__star--r{right:0; top:53%; rotate:-6.92deg}

/* ── Ellipse headline (1360:8246) ────────────────────────────
   Figma exports the ring as an SVG, but it is a plain stroked
   ellipse — a border plus a 50% radius reproduces it and scales
   without an asset. */
.ellipse-head{display:grid;place-items:center}
.ellipse-head__text{
  margin:0;
  display:grid; place-items:center;
  width:min(100%, 1718px);
  aspect-ratio:1718 / 263;
  /* An ellipse is only full width across its middle, so the script needs
     an inline pad to stay inside the curve. One line on desktop, two on
     mobile — see the .br-m break in the markup. */
  padding-inline:8%;
  background:var(--offwhite);
  border:2px solid var(--blue);
  border-radius:50%;
  font-size:clamp(40px,5.65vw,108px);
  line-height:1.05;
  text-align:center;
  /* One line above 640, guaranteed. The grid item's shrink-to-fit was
     wrapping it even though the full string measured 1233 against 1436
     of usable width. The mobile break below is a real <br>, which still
     splits the line regardless of this. */
  white-space:nowrap;
  -webkit-text-stroke:0;           /* the ring is the whole treatment */
}

/* ── Before & after — section framing ───────────────────────── */
.ba-section{display:flex;flex-direction:column;gap:var(--s-40)}

/* The case study draws the FAQ title at 100px; About draws it at 121. */
.faq--sm .faq__title{font-size:var(--t-heading)}

@media (max-width:640px){
  .section--tint{padding-block:var(--s-40)}
  .benefits--wide .rail{--card-w:calc(100vw - 2 * var(--gutter))}   /* full width, as every rail on phones */
  .rail--short, .benefits--wide .rail{--card-h:240px}

  .solution__head{grid-template-columns:1fr;gap:var(--s-20)}
  .solution .stack{gap:var(--s-20)}
  .solution__badge{display:none}          /* not on the mobile board */

  .gameplan__list{gap:var(--s-40)}
  .gameplan__item{gap:var(--s-20)}
  .gameplan__star{--star-w:211px;opacity:.45}
  /* Pushed further off each edge than the board draws them: at the Figma
     offsets the stars sat under the centred copy and read as a wash behind
     the text rather than as punctuation at the margin. The tilt on --r is
     inherited from the base rule. */
  .gameplan__star--l{left: -20%;  top:19%}
  .gameplan__star--r{right: -20%; top:56%}

  /* Figma draws the slider full-bleed on mobile (x=0, w=390) */
  .ba-section{padding-inline:0;gap:var(--s-24)}
  .ba{aspect-ratio:390 / 320}
  .ba__frame{border-radius:0}
  /* Figma's mobile tabs are 132px wide, i.e. the small button step — at
     --big they are too wide to sit two-up and wrap 1-then-2. That size is
     now every mobile button's (buttons.css), so the tabs need no rule. */
  .ba__tabs{padding-inline:24px;column-gap:var(--s-16);row-gap:8px}

  .ellipse-head__text{
    aspect-ratio:342 / 86;
    border-width:1.5px;
    padding-inline:13%;
    font-size:24px;          /* two lines at this width */
    white-space:normal;
  }
}

/* ═════════════════════════════════════════════════════════════
   CONTACT  (Figma: Contact / Desktop 1360:8485, Mobile 1360:8574)

   The hero is .about-hero verbatim, the business-hours rows are
   .faq__row, the section ground is .section--tint and the photo is
   .card. The form below is the only new component on the page — and
   the first form on the site.
   ═════════════════════════════════════════════════════════════ */

.contact-grid{
  display:grid;
  grid-template-columns:repeat(12,minmax(0,1fr));
  column-gap:var(--s-20);
}
.contact-grid__form{grid-column:1 / span 5; display:flex; flex-direction:column; gap:var(--s-40)}
.contact-grid__info{grid-column:8 / span 5; display:flex; flex-direction:column; gap:var(--s-40)}
.contact-info__title{margin:0;font-size:var(--t-body-lg);font-weight:700;line-height:1.2}
.contact-info__body{margin:0;font-size:var(--t-body);line-height:1.3}
.contact-info__lines{margin:0;font-size:var(--t-body);line-height:1.3}
.contact-info__lines a{text-decoration:underline;text-underline-offset:.15em}
@media (hover:hover){ .contact-info__lines a:hover{opacity:.6} }

/* ── The form card (1360:8504) ──────────────────────────────── */
.form-card{
  background:var(--offwhite);
  border:2px solid var(--light-blue);
  border-radius:24px;
  padding:var(--s-60);
  box-shadow:0 16px 20px rgba(21,26,90,.1);
  display:flex; flex-direction:column;
  gap:var(--s-40);
}
.form-card__group{display:flex;flex-direction:column;gap:var(--s-24)}

.field{display:flex;flex-direction:column;gap:12px;margin:0}
.field__label{
  display:flex; align-items:center; gap:8px;
  font-family:var(--font-body);
  font-weight:700;
  font-size:clamp(13px,.833vw,16px);      /* Figma 16 @1920, 13 @390 */
  line-height:1.2;
  text-transform:uppercase;
  color:var(--navy);
}
.field__label img{width:clamp(26px,1.667vw,32px);height:auto;flex:0 0 auto}

.field__input,
.field__select{
  width:100%;
  background:var(--white);
  border:1px solid var(--navy);
  border-radius:12px;
  padding:16px 20px;
  font-family:var(--font-body);
  font-size:clamp(15px,.9375vw,18px);     /* Figma 18 @1920, 15 @390 */
  line-height:1.2;
  color:var(--navy);
  transition:border-color .2s var(--ease), box-shadow .2s var(--ease);
}
.field__input::placeholder{color:var(--stone);opacity:.6}
.field__input:focus-visible,
.field__select:focus-visible{
  outline:none;
  border-color:var(--blue);
  box-shadow:0 0 0 3px rgba(56,71,195,.18);
}
/* :user-invalid, not :invalid — an untouched required field must not
   render as an error the moment the page loads. */
.field__input:user-invalid,
.field__select:user-invalid{
  border-color:var(--error);
  box-shadow:0 0 0 3px rgba(179,38,30,.15);
}

/* Strip the OS chrome and draw Figma's own chevron instead */
.field__select{
  appearance:none;
  font-weight:500;
  cursor:pointer;
  padding-right:52px;
  background-image:url('../svg/icon-chevron-down.svg');
  background-repeat:no-repeat;
  background-position:right 20px center;
  background-size:16px 16px;
}

.form-card__submit{display:flex;justify-content:center;padding-top:var(--s-20)}
.form-card__note{margin:0;font-size:clamp(13px,.833vw,16px);line-height:1.3;text-align:center}
.form-card__note--ok{color:var(--blue);font-weight:700}
.form-card__note--err{color:var(--error);font-weight:700}

/* ── Business hours (1360:8546) ─────────────────────────────── */
.hours{display:flex;flex-direction:column;gap:var(--s-80)}
.hours__grid{
  display:grid;
  grid-template-columns:repeat(12,minmax(0,1fr));
  column-gap:var(--s-20); row-gap:var(--s-80);
}
.hours__col{grid-column:1 / span 6; display:flex; flex-direction:column; gap:var(--s-80)}
.hours__block{display:flex;flex-direction:column;gap:var(--s-40)}
.hours__title{margin:0;font-size:var(--t-body-lg);font-weight:700;line-height:1.2;color:var(--navy)}
.hours__list{display:flex;flex-direction:column;gap:var(--s-20);color:var(--blue)}
/* .faq__row centres its two cells, which is right for the FAQ's single-line
   rows. Here the right cell carries a second line (the "within reason"
   footnote), so centring lifted "Every day" clear of "All day". Baseline
   alignment sits the two labels on one line and lets the note hang below.
   Scoped to .hours__list so the FAQ's own rows keep centring. */
.hours__list .faq__row{align-items:baseline}
/* the right-hand side of the "all day" row carries a small footnote,
   centred under it — Figma 1466:4993 is a single centred text block */
.hours__stack{text-align:center}
.hours__note{
  display:block;
  font-size:clamp(11px,1.04vw,20px);   /* 20 @1920, 11 @390 */
  line-height:1.2;
}

/* Star behind the photo card (1360:8548) — 447px, rotated 28.4deg, and
   overhanging the container's right edge by ~39% of its own width. */
.hours{position:relative}
.hours__star{
  position:absolute; z-index:0;
  top:-8px; right:0;
  width:clamp(208px,23.3vw,447px);
  transform:translateX(39%) rotate(28.4deg);
  pointer-events:none;
}
.hours__grid{position:relative;z-index:1}
.hours__card{grid-column:8 / span 5;height:521px}

@media (max-width:640px){
  .contact-grid{display:flex;flex-direction:column;gap:var(--s-80)}
  .contact-grid__form,
  .contact-grid__info{gap:var(--s-20)}

  .form-card{padding:var(--s-20);border-radius:16px;gap:var(--s-20)}
  .form-card__group{gap:var(--s-20)}
  .field{gap:10px}
  .field__label{gap:6px}
  .field__input,
  .field__select{padding:13px 16px}
  .field__select{padding-right:44px;background-position:right 16px center;background-size:13px 13px}
  .form-card__submit{padding-top:var(--s-10)}

  /* Figma parks it fully off-canvas at this width (x=417 of a 390 board) */
  .hours__star{display:none}

  .hours{gap:var(--s-40)}
  .hours__grid{display:flex;flex-direction:column;gap:var(--s-40)}
  .hours__col{gap:var(--s-40)}
  .hours__block{gap:var(--s-20)}
  .hours__list{gap:var(--s-10)}
  .hours__card{height:240px}
}
