/* ==========================================================================
   NG PRODUCTION FILMS — V2
   1.  Tokens
   2.  Base
   3.  Header / nav
   4.  Buttons + eyebrow
   5.  Hero
   6.  Sections + typography
   7.  Cards / steps / facts
   8.  Work grid
   9.  Forms
   10. CTA + footer
   11. Utilities (note, sitemap)
   12. Trust band + testimonial
   13. Form CTA
   14. Motion layer  <-- pre-animation states, read this before editing markup
   15. Responsive
   ========================================================================== */


/* ==========================================================================
   1. TOKENS
   ========================================================================== */

:root {
  --bg: #07090d;
  --panel: #0d1118;
  --panel2: #111824;
  --text: #f5f7fb;
  --muted: #a8b0be;
  --line: rgba(255, 255, 255, .12);
  --blue: #4ea5ff;
  --blue2: #0b70ff;
  --max: 1380px;

  /* Display face carries the oversized uppercase headlines; body face is the
     workhorse. Both from Fontshare — see the <link> in index.html. */
  --font-display: 'Satoshi', 'Switzer', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-body: 'Switzer', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;

  --ease-out: cubic-bezier(.22, 1, .36, 1);

  /* Hero headline sits over the playing reel. Holding it under full opacity
     lets some of the footage read through the type instead of the headline
     sealing off that whole area. Single knob — tune here. */
  --hero-title-opacity: .6;

  /* The one warm colour on an otherwise blue/white page: review stars, the
     active carousel dot, and every interactive hover state. */
  --accent: #f4b942;
}


/* ==========================================================================
   2. BASE
   ========================================================================== */

* { box-sizing: border-box }

/* NOTE: `scroll-behavior: smooth` is deliberately absent. Lenis animates the
   scroll position itself; native smooth scrolling fights it and stutters. */
html { -webkit-text-size-adjust: 100% }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

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

.wrap {
  width: min(calc(100% - 42px), var(--max));
  margin: auto;
}

/* Lenis owns the scroll; these keep it from double-scrolling or snagging. */
html.lenis, html.lenis body { height: auto }
.lenis.lenis-smooth { scroll-behavior: auto !important }
.lenis.lenis-stopped { overflow: hidden }


/* ==========================================================================
   3. HEADER / NAV
   ========================================================================== */

/* site.js injects the header into <div data-header>, and that wrapper is
   exactly as tall as the nav inside it. A sticky element can only travel
   within its parent's box, so with zero room the nav scrolled straight off the
   page — which is why the header used to disappear and leave the footer nav as
   the only way back.

   display:contents removes the wrapper's box entirely, making .nav a flow
   child of <body> so it can stick for the whole document. Preferred over
   position:fixed because the nav stays in normal flow: the hero pin measures
   with offsetTop, and taking the header out of flow would shift every one of
   those measurements. */
[data-header] { display: contents }

.nav {
  position: sticky;
  top: 0;
  z-index: 20;
  /* Translucent so content reads through it as it passes underneath, with the
     blur keeping that from turning into noise behind the links. */
  background: rgba(7, 9, 13, .7);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line);
  transition: background .3s var(--ease-out), border-color .3s var(--ease-out);
}

.navin {
  height: 82px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 25px;
  transition: height .35s var(--ease-out);
}

/* Applied by motion.js once the hero is scrolled past. */
.nav.is-condensed { background: rgba(5, 7, 11, .78) }
.nav.is-condensed .navin { height: 64px }
.nav.is-condensed .brand-logo { width: 196px }

.brand {
  display: flex;
  align-items: center;
  gap: 12px;
  font-weight: 900;
  letter-spacing: -.04em;
  font-size: 21px;
}

.brand-logo {
  display: block;
  width: 235px;
  height: auto;
  max-height: 48px;
  object-fit: contain;
  object-position: left center;
  transition: width .35s var(--ease-out);
}

.footer .brand-logo {
  width: 265px;
  max-height: none;
  margin-bottom: 18px;
}

.mark {
  width: 36px;
  height: 36px;
  border-radius: 10px;
  background: linear-gradient(135deg, var(--blue), var(--blue2));
  display: grid;
  place-items: center;
  font-size: 13px;
}

.links {
  display: flex;
  gap: 25px;
  align-items: center;
  font-size: 13px;
  font-weight: 700;
}

/* Text links only — the "Start a Project" .btn beside them keeps its own
   button treatment. :focus-visible matches so keyboard users get the same cue. */
.links a:not(.btn) {
  transition: color .25s var(--ease-out);
}

.links a:not(.btn):hover,
.links a:not(.btn):focus-visible {
  color: var(--accent);
}


/* --------------------------------------------------------------------------
   Collapsed navigation (below 1024px)

   The original design hid the nav links entirely below 900px with no
   replacement, leaving no way to navigate on a phone or small tablet. This is
   the fix.

   Deliberately CSS + a class toggle, with NO GSAP: navigation is core
   functionality and has to keep working when the motion layer bails (CDN
   failure, reduced motion, a stalled ticker).
   -------------------------------------------------------------------------- */

.nav-toggle {
  display: none;              /* desktop: hidden, and out of the tab order */
  position: relative;
  width: 44px;                /* 44px minimum touch target */
  height: 44px;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
  border-radius: 8px;
  -webkit-tap-highlight-color: transparent;
}

.nav-toggle-bar {
  position: absolute;
  left: 11px;
  width: 22px;
  height: 2px;
  border-radius: 2px;
  background: var(--text);
  transition: transform .34s var(--ease-out),
              opacity .2s linear,
              background-color .25s var(--ease-out);
}

/* 6px apart, so the outer bars meet exactly in the middle when it becomes an X */
.nav-toggle-bar:nth-child(1) { top: 15px }
.nav-toggle-bar:nth-child(2) { top: 21px }
.nav-toggle-bar:nth-child(3) { top: 27px }

.nav-toggle:hover .nav-toggle-bar { background: var(--accent) }
.nav-toggle:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px }

/* Open: outer bars converge and cross, middle bar collapses out. */
.nav-toggle[aria-expanded="true"] .nav-toggle-bar:nth-child(1) { transform: translateY(6px) rotate(45deg) }
.nav-toggle[aria-expanded="true"] .nav-toggle-bar:nth-child(2) { opacity: 0; transform: scaleX(.35) }
.nav-toggle[aria-expanded="true"] .nav-toggle-bar:nth-child(3) { transform: translateY(-6px) rotate(-45deg) }

@media (max-width: 1023px) {
  /* No pin below this width, so the frame never travels. motion.js places it
     in the desktop transition's LANDED box (see initHeroFrameStatic). These
     rules are the no-JS fallback for the same intent: centred on the content
     column rather than hanging off the right edge, which is what caused the
     hero to appear to shift.

     The reel plays here too, so legibility is handled by the scrim below
     rather than by fading the whole frame out — a faded frame would fade the
     video with it. */
  .filmframe {
    left: 12px;
    right: 12px;
    width: auto;
    top: 22%;
    transform: none;
    opacity: 1;
  }

  /* The desktop scrim is weighted to the LEFT, where the headline sits, and the
     copy covers only part of the frame. At these widths the copy covers the
     whole frame, so a left-weighted gradient leaves text sitting on bright
     footage. Flat dim plus a bottom lift instead.

     .72 and .55 are measured against the reel NG supplied, not estimated.
     Sampling the footage behind the headline at this width: its median is
     rgb(171,143,125), but its brightest 2% is pure white — a sunlit window the
     type crosses. Against THAT, the old .63 left the headline at 3.16:1, which
     only scrapes past the 3:1 floor large text is held to, and reads as washed
     out because a headline is read against its worst patch, not its average.
     With .72 and the full-strength headline below, the same worst patch comes
     out at 7.96:1 and the body copy at about 6.8:1.

     Tune the first value if the reel ever reads too dark or the copy too weak,
     and re-measure rather than eyeball it — a frame's average flatters it. */
  /* .hero prefix for specificity: this block sits EARLIER in the file than the
     base .filmframe-scrim rule, so equal specificity would lose. */
  .hero .filmframe-scrim {
    background-image:
      linear-gradient(rgba(5, 7, 11, .72), rgba(5, 7, 11, .72)),
      linear-gradient(to top, rgba(5, 7, 11, .55), rgba(5, 7, 11, 0) 42%);
  }

  /* The headline runs at full strength here.

     --hero-title-opacity (.6) exists so the reel can slide in BEHIND the words
     on desktop, where the headline dims as the frame arrives. Below 1024px the
     hero never pins, the frame never travels, and that dim buys nothing — it
     only costs contrast against footage that is now directly behind the type.
     Full strength also means the scrim does not have to carry the whole load,
     so NG's reel stays visible rather than being dimmed into a grey panel. */
  .hero { --hero-title-opacity: 1 }

  html.js .hero:not(.is-revealed) .filmframe { opacity: 0 }

  .nav-toggle { display: block }

  /* .nav is position:sticky, so it is the containing block for this panel. */
  .links {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    display: block;
    /* Opaque, not translucent. .nav already has a backdrop-filter, and a
       parent's backdrop-filter isolates the backdrop group — so a
       backdrop-filter here samples nothing and does not blur. Translucency
       without blur just leaves the hero type legible behind the menu links.
       This is --bg, the same colour the header sits on. */
    background: var(--bg);
    border-top: 1px solid var(--line);
    border-bottom: 1px solid var(--line);
    box-shadow: 0 24px 48px rgba(0, 0, 0, .45);

    max-height: 0;
    overflow: hidden;
    opacity: 0;
    visibility: hidden;       /* keeps the closed panel out of the tab order */
    transition: max-height .38s var(--ease-out),
                opacity .26s var(--ease-out),
                visibility .38s;
  }

  .nav.is-open .links {
    max-height: 78vh;
    overflow-y: auto;
    opacity: 1;
    visibility: visible;
  }

  /* Same 21px gutter as .wrap, hairline separators in the site's own language. */
  .links a:not(.btn) {
    display: block;
    padding: 18px 21px;
    border-top: 1px solid var(--line);
    font-size: 14px;
  }

  .links a:not(.btn):first-child { border-top: 0 }

  /* "Start a Project" sits last and full width, per the brief. */
  .links .btn {
    display: flex;
    width: calc(100% - 42px);
    margin: 18px 21px 22px;
    padding: 16px;
  }
}


/* ==========================================================================
   4. BUTTONS + EYEBROW
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Explicit, because .btn is used on <button> as well as <a>: a button
     element would otherwise take the UA's default grey/white face and light
     text colour, which rendered the work-page filters as white pills. */
  background: transparent;
  color: inherit;
  font: inherit;
  border: 1px solid rgba(255, 255, 255, .42);
  padding: 13px 19px;
  border-radius: 999px;
  font-weight: 700;
  /* Never `transition: .22s` — the bare-duration shorthand means "all", which
     swept in the opacity GSAP drives during the entrance and stranded buttons
     at 0. Opacity is deliberately NOT in this list; transform is safe because
     motion.js clears its inline transform when the entrance completes. */
  transition: background-color .22s var(--ease-out),
              border-color .22s var(--ease-out),
              color .22s var(--ease-out),
              transform .22s var(--ease-out);
}

.btn:hover {
  transform: translateY(-2px);
  background: rgba(255, 255, 255, .07);
}

.btn.primary {
  background: #fff;
  color: #090b10;
  border-color: #fff;
}

/* Colour change only. .btn:hover still supplies translateY(-2px), and the
   transition list on .btn already covers background-color, border-color and
   colour, so this fades rather than snaps. Higher specificity than .btn:hover,
   so it wins the background without needing !important. */
.btn.primary:hover,
.btn.primary:focus-visible {
  background: var(--accent);
  border-color: var(--accent);
  color: #0b0d12;
}

.eyebrow {
  text-transform: uppercase;
  letter-spacing: .16em;
  font-size: 11px;
  font-weight: 700;
  color: #bdc6d3;
}


/* ==========================================================================
   5. HERO
   ========================================================================== */

.hero {
  min-height: 72vh;
  display: flex;
  align-items: flex-end;
  position: relative;
  overflow: hidden;
  background:
    radial-gradient(circle at 76% 30%, rgba(57, 140, 255, .27), transparent 28%),
    linear-gradient(125deg, #101929, #080b10 52%, #030407);
}

.hero:after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to top, var(--bg), transparent 54%),
    repeating-linear-gradient(90deg, rgba(255, 255, 255, .018) 0 1px, transparent 1px 130px);
}

.heroin {
  position: relative;
  z-index: 2;
  padding: 150px 0 80px;
}

.hero h1 {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: clamp(60px, 9vw, 138px);
  line-height: .85;
  letter-spacing: -.067em;
  text-transform: uppercase;
  margin: 15px 0 26px;
  max-width: 1200px;
}

/* The dimmed title only makes sense over the playing reel. Subpage heroes
   have no footage behind the type, so they keep it at full strength. */
.hero:not(.subhero) h1 { opacity: var(--hero-title-opacity) }

.hero p {
  font-size: clamp(18px, 2vw, 23px);
  color: #c3cbd7;
  max-width: 720px;
  margin: 0 0 32px;
}

/* Once the reel frame is on screen it occupies the right ~58% of the hero.
   The headline crossing it reads as deliberate layering, but 23px body copy
   over a photograph does not — so cap the paragraph short of the frame's left
   edge. 34vw tracks the frame, which is itself min(54vw, 830px) at right: 6%.
   Narrowing this does NOT reduce height — below 32vw the copy gains a fifth
   line. 32-34vw is the only band that holds it to four. */
@media (min-width: 1000px) {
  .hero:not(.subhero) p { max-width: min(720px, 34vw) }
}

.actions {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
}

/* ==========================================================================
   5b. SUBPAGE HERO  (.hero.subhero — contact-us, insights, …)
   --------------------------------------------------------------------------
   No reel, no pin. What sets it apart from the rest of the page is a
   dramatic word-by-word title entrance (motion.js initSubHero) over a
   slowly drifting light. Everything here renders as a finished, static
   hero without JS or under reduced motion; motion only moves it.
   ========================================================================== */

/* The static radial glow in .hero's background is replaced by the two
   animatable glow layers below, so it is dropped here to avoid doubling up. */
.subhero {
  background: linear-gradient(125deg, #101929, #080b10 52%, #030407);
}

/* Shorter hero for utility pages (privacy-policy.html). The stub asked for a
   reduced hero via an inline min-height; this is that, as a class. */
.subhero-compact { min-height: 46vh }

/* Sits under .hero:after (the bottom fade + hairline grid, painted later)
   and under .heroin (z-index 2). */
.subhero-bg {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* Glows are animated ONLY with transform/opacity, so the pulse is composited
   and never repaints the hero. closest-side makes 100% = the circle's edge. */
.subhero-glow {
  position: absolute;
  border-radius: 50%;
  will-change: transform;
}

/* Where the original static glow sat: upper right, studio blue. */
.subhero-glow-a {
  /* Centred on 76% / 30% with calc, NOT `translate: -50% -50%` — GSAP writes
     translate:none onto anything it transforms, which would unseat it. */
  width: min(64vw, 980px);
  aspect-ratio: 1;
  left: calc(76% - min(32vw, 490px));
  top: calc(30% - min(32vw, 490px));
  background: radial-gradient(circle closest-side, rgba(57, 140, 255, .40), rgba(57, 140, 255, .14) 50%, transparent);
}

/* A second, dimmer steel-blue fill low on the left, pulsing out of step with
   the first so the light never breathes as one flat blob. */
.subhero-glow-b {
  width: min(48vw, 720px);
  aspect-ratio: 1;
  left: calc(18% - min(24vw, 360px));
  top: calc(88% - min(24vw, 360px));
  background: radial-gradient(circle closest-side, rgba(96, 150, 210, .24), rgba(96, 150, 210, .08) 52%, transparent);
}

/* The grid sweep (a band of light that travelled across the hero's hairline
   grid) was removed at the client review of 18 Sep 2026: glows only now. Its
   .subhero-sweep / .subhero-sweep-grid rules and the motion.js pass went with
   it; the markup is gone from every subpage hero. */

/* Word masks, created by motion.js from the h1's plain text. Each word clips
   its own inner span, which rises out of it. The padding/negative margin pair
   gives the heavy display face (tight tracking, .85 line-height, uppercase)
   room so glyph edges are not clipped, without changing the layout.

   The bottom figure is not a guess. Satoshi Black's descender ink reaches
   .224em below the bottom of the inline box it sits in — measured from the
   font's own metrics (canvas measureText, actualBoundingBoxDescent, against
   this heading's computed font and .85 line-height). The .14em this used to
   carry was .08em short, so every g, j, p, q and y in a masked heading was
   sliced off along one flat line. .26em clears it with room to spare. */
.hero-word {
  display: inline-block;
  overflow: hidden;
  vertical-align: top;
  padding: .08em .1em .26em;
  margin: -.08em -.1em -.26em;
}

.hero-word-inner {
  display: inline-block;
  transform-origin: 0% 100%;
  will-change: transform;
}

/* Homepage-only cinematic reel frame (added by site.js) */
.homehero { min-height: 100vh }

/* Permanent pin spacer around the hero (site.js; see MOTION.md §4b).
   ScrollTrigger copies the hero's own `display: flex` onto its spacer. On every
   refresh the hero is re-measured INSIDE that flex box, shrinks to its content
   width (1216px on a 2560 screen), and gets frozen there as an inline width,
   so the hero, frame and background end up off to one side. The spacer's parent
   is a plain block, so block is the correct display; !important is the only way
   to beat ScrollTrigger's inline style. */
.hero-pin { display: block !important }

/* The landed frame's left edge lines up with .facts (see motion.js endBox()),
   which is also the text column's own left edge — flush, with no breathing
   room. Nudge the readable copy right by 16px so it clears the frame's
   rounded corner instead of touching it; .facts itself stays put since it
   sits BELOW the frame once landed, not beside it, so it keeps the frame's
   exact edge. */
.homehero .heroin { padding-left: 16px }
.homehero .heroin .facts { margin-left: -16px }

.filmframe {
  position: absolute;
  z-index: 1;
  right: 6%;
  top: 16%;
  width: min(54vw, 830px);
  aspect-ratio: 16 / 9;
  border-radius: 28px;
  transform: rotate(-2deg);
  background: linear-gradient(135deg, #1b2c46, #0b1018 55%, #050607);
  border: 1px solid rgba(255, 255, 255, .12);
  box-shadow: 0 60px 120px rgba(0, 0, 0, .55);
  overflow: hidden;
  /* The pin animates left/top/width/height rather than a transform: scaling a
     frame with a video in it would distort the picture and the corner radius.
     Layout animation is the correct trade here, confined to one element. */
  will-change: transform, width, height, left, top;
}

.filmframe:before {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(to top, rgba(5, 7, 11, .55), rgba(5, 7, 11, .12) 60%),
    /* Left-to-right scrim: the headline overlaps the frame's left third, so
       that side is pushed down toward the page ground while the right side —
       where the reel artwork actually lives — stays clear. */
    linear-gradient(to right, rgba(5, 7, 11, .88), rgba(5, 7, 11, .3) 44%, rgba(5, 7, 11, 0) 72%),
    var(--frame-img, none),
    radial-gradient(circle at 66% 35%, rgba(88, 174, 255, .34), transparent 25%),
    linear-gradient(120deg, transparent 0 40%, rgba(255, 255, 255, .06) 41% 42%, transparent 43%);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

/* NG's own reel, a <video> rather than an embed (see ensureVideo in motion.js).
   Fixed pixel dimensions, re-set only on refresh — never per frame — so the
   element does not reflow while the frame's box is being animated. The frame's
   overflow:hidden does the cropping. */
.filmframe-video {
  display: block;
  /* The JS sizing already matches the source's 16:9, so this changes nothing
     today. It is here so that a replacement clip in another ratio crops
     instead of letterboxing — a black bar inside the frame would read as a
     broken player. */
  object-fit: cover;
  object-position: center top;
  position: absolute;
  /* Anchored to the TOP edge, centred horizontally. When the frame is wider
     than 16:9 (the landed frame on shorter screens) the player overflows
     downward, so the crop comes off the bottom of the shot rather than
     splitting top and bottom and cutting off heads. Where it overflows
     sideways instead (portrait-ish frames below 1024) the height fits exactly,
     so top anchoring changes nothing there. */
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  border: 0;
  pointer-events: none;
  z-index: 1;
  /* Baseline so the element can never render at its intrinsic size if the JS
     sizing has not run yet. motion.js then overrides both with pixel values
     that cover the frame at 16:9. */
  width: 100%;
  height: 100%;
}

/* Legibility scrim, ABOVE the video. The gradients baked into .filmframe:before
   sit behind the player and are only visible in the poster fallback, so the
   scrim has to be its own layer once a video is present. Weighted to the left,
   where the headline crosses the frame. */
/* Legibility scrim over the reel.
   Previously .92 black on the left falling only to .20 at 75%, plus .80 along
   the bottom — which left the footage readable in barely a corner of the frame
   and made the reel look like it was playing in a small window. These values
   still carry the headline (which sits left, at 80% opacity, in heavy display
   type) but let the footage read across most of the frame. Raise the first
   stop if any headline ever fights its background. */
.filmframe-scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background-image:
    linear-gradient(to right, rgba(5, 7, 11, .82), rgba(5, 7, 11, .32) 45%, rgba(5, 7, 11, .04) 82%),
    linear-gradient(to top, rgba(5, 7, 11, .5), rgba(5, 7, 11, 0) 42%);
}

/* The frame carried a "2025 DEMO REEL" caption here, bottom-right, as a CSS
   ::after — removed at NG's request on 2 Oct 2026. It named one specific reel,
   which went stale the moment the footage was replaced, and on a phone the
   frame fills the hero so the caption landed on top of the second button.
   The frame plays real footage now and does not need to say that it does. */


/* ==========================================================================
   6. SECTIONS + TYPOGRAPHY
   ========================================================================== */

.section {
  padding: 105px 0;
  border-top: 1px solid var(--line);
}

.title {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: clamp(42px, 6vw, 84px);
  line-height: .94;
  letter-spacing: -.055em;
  margin: 12px 0 24px;
  max-width: 1040px;
}

.lead {
  font-size: 20px;
  color: #b7c0cd;
  max-width: 760px;
}

/* Inline links inside running copy. The global `a` reset strips colour and
   underline, which is right for nav, cards and buttons but leaves a link in a
   sentence indistinguishable from the words around it. White plus an accent
   underline keeps it legible without shouting over the paragraph. */
.lead a {
  color: #fff;
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
  transition: color .25s var(--ease-out), text-decoration-thickness .25s var(--ease-out);
}

.lead a:hover,
.lead a:focus-visible { color: var(--accent); text-decoration-thickness: 2px }

.split {
  display: grid;
  grid-template-columns: 1.1fr .9fr;
  gap: 80px;
  align-items: start;
}

/* .media-right marks the arrangement; it deliberately does NOT resize the
   tracks. An earlier version swapped them to .9fr 1.1fr, so that the image
   stayed the same size whichever side it sat on. That sounded right and it
   is not what the design does: every split in the design gives the FIRST
   column the extra room, so an image on the right is the narrower element
   and the words beside it are the wider one. The swap made the media 130px
   wider than the demo on all seven pages that place it on the right, and
   on four of them the taller image pushed the page down with it. Found by
   tools/migrate/pixel_diff.js, which is the only check that could see it:
   the markup, the classes and every word of copy were already identical. */

/* One section in the design, two blocks here.

   Seven sections in the design hold two parts: an amenity list with a CTA
   band under it, a stat row with a quote, a card grid with a stat row. Each
   part is its own block, because each is its own idea and NG edits them
   separately, and a block is a section. Left alone that puts 105px of
   padding under the first and another 105px over the second, so those
   pages came out 211px taller than the demo with the second part floating
   away from what it belongs to.

   The second block carries .is-joined, from its "Join to the section above"
   field, and these two rules close the gap from both sides. Scoped to a
   joined section that FOLLOWS a section, so a page opening with one is not
   pulled up under the header. :has() does the other half; a browser without
   it simply keeps the gap, which is the right thing to fail to. */
.section + .section.is-joined { padding-top: 0 }
.section:has(+ .section.is-joined) { padding-bottom: 0 }

.all-work-link { margin-top: 28px }

.visual {
  min-height: 560px;
  border: 1px solid var(--line);
  border-radius: 24px;
  background-image:
    var(--visual-img, none),
    radial-gradient(circle at 68% 28%, rgba(78, 165, 255, .25), transparent 23%),
    linear-gradient(145deg, #1b2a41, #0b1018 58%, #060709);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  position: relative;
  overflow: hidden;
}

/* The caption sits on raw photo pixels, so it needs the same scrim every
   other overlaid label in this file already has (.article-media:after,
   .location-map:after). Without it legibility depends on which photo is
   chosen, which is how the placeholder van shot made it unreadable. */
.visual:before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(5, 7, 11, .72), rgba(5, 7, 11, 0) 28%);
  pointer-events: none;
}

.visual:after {
  content: attr(data-label);
  position: absolute;
  left: 24px;
  bottom: 22px;
  font-size: 10px;
  letter-spacing: .15em;
  color: #d4dbe5;
}

/* Sized down from the original clamp(32px, 5vw, 66px): that was tuned for one
   short pull quote, and the real reviews run 50+ words. */
.quote {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(23px, 3.1vw, 42px);
  line-height: 1.17;
  letter-spacing: -.035em;
  max-width: 1080px;
  margin: 0;
}

/* Attributed pull quote: .quote inside a <figure>, with the speaker below.
   The <figure>/<blockquote>/<figcaption> structure is what ties the name to
   the words for assistive tech; the class names only style it. */
.quote-figure { margin: 0 }
.quote-cite {
  margin-top: 26px;
  font-size: 15px;
  color: var(--muted);
}
.quote-cite strong { color: #fff; font-weight: 600 }
.quote-cite:before {
  content: "";
  display: inline-block;
  width: 28px;
  height: 1px;
  margin-right: 12px;
  vertical-align: middle;
  background: var(--accent);
}

.faq { max-width: 960px; margin-top: 45px }
.faq details { border-top: 1px solid var(--line); padding: 22px 0 }

.faq summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  font-size: 19px;
  font-weight: 700;
  cursor: pointer;
  transition: color .25s var(--ease-out);
  /* The row is a click target, so stop a double-click selecting the label. */
  -webkit-user-select: none;
  user-select: none;
}

/* Remove the UA's own disclosure triangle. `display: flex` above already
   suppresses it in most engines, but Safari needs the -webkit- pseudo and
   older WebKit needs list-style — both are harmless elsewhere. */
.faq summary { list-style: none }
.faq summary::-webkit-details-marker { display: none }

.faq summary:hover { color: var(--accent) }

/* Chevron, drawn from two borders rather than loaded as an icon — it is two
   straight lines, and this way it picks up --accent directly and rotates
   about its own centre. */
.faq summary::after {
  content: "";
  flex: none;
  width: 9px;
  height: 9px;
  margin-right: 4px;
  border-right: 2px solid var(--accent);
  border-bottom: 2px solid var(--accent);
  /* A 45deg square reads as a chevron, but its visual centre sits below its
     box centre — the -2px lifts it back onto the text baseline. */
  transform: translateY(-2px) rotate(45deg);
  transition: transform .4s var(--ease-out);
}

.faq details[open] > summary::after { transform: translateY(2px) rotate(-135deg) }

/* Wrapper motion.js adds around each answer so its height can be animated;
   without the motion layer it is an ordinary div and `details` opens
   instantly, as it always did. */
.faq-body { overflow: hidden }

.faq p { color: var(--muted); max-width: 760px }
.faq-body p { margin-bottom: 0 }


/* ==========================================================================
   7. CARDS / STEPS / FACTS
   ========================================================================== */

/* The 1px gap over a --line background fakes the hairline grid. Do not
   translate .card itself on scroll-in — it exposes the backing plate. Animate
   .card > * instead. See motion.js.

   The plate has to stay, and it is worth saying why, because the obvious
   improvement does not work. Having each card paint its own 1px line with a
   box-shadow would mean nothing sits behind an uncovered cell, but --line is
   translucent: two neighbours then paint the same 1px band twice and the
   internal lines come out at rgb(63,64,67) against the rgb(37,38,42) here.
   Flattening the colour fixes that and breaks the frame instead, because the
   plate is painted under the border as well, so the outer edge is a doubled
   line by design and a shadow can only draw one. Tried and measured both
   ways, 17 Oct 2026. What covers the holes is below. */
.cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  margin-top: 50px;
}

.card {
  background: var(--bg);
  padding: 34px;
  min-height: 255px;
  transition: background-color .2s var(--ease-out);
}

.card:hover { background: #0d121a }
.card .num { font-size: 11px; color: #8190a4; margin-bottom: 45px }
.card h3 { font-family: var(--font-display); font-size: 25px; letter-spacing: -.035em; margin: 0 0 10px }
.card p { color: #9fa8b7; margin: 0; font-size: 14px }

/* Two wide cards instead of the usual three-up, for a pair of parallel lists
   (commercials: cable vs broadcast). The blocks add .two from the count, so
   nobody has to choose it. */
.cards.two { grid-template-columns: repeat(2, 1fr) }

/* One card, so one column. A lone card holding a third of the row reads as
   two cards that failed to load, and the row it sits in is drawn with a
   border, which makes the empty two thirds look like a mistake rather than
   like space. Added with .project-grid.one below, from the same helper. */
.cards.one { grid-template-columns: minmax(0, 1fr) }

/* A last row with a hole in it: the last card grows into what is left.
   Without this, five cards in three columns leave a cell with no card in it
   and the backing plate shows through as a grey slab. One, two and four get
   a column count that divides instead, so the counts that reach here are
   five, seven, eight, ten and up.

   Keyed on the card's own position rather than a class, because the column
   count changes at each breakpoint and so does which card is short of
   company: nth-child(3n + 1) is a card alone in a three-up row, 3n + 2 is
   the second of three. The .one and .two grids are excluded because their
   rows are already full, and a span would start pulling cards out of them.

   ⚠ The two breakpoints below repeat this selector exactly, :not()s and all.
   They have to: a span set here and not given back makes a card wider than
   the grid has columns, which pushes a phone back into two of them. Written
   the long way round because the short way lost on specificity and only the
   count of eight showed it. */
.cards:not(.one):not(.two) > .card:last-child:nth-child(3n + 1) { grid-column: 1 / -1 }
.cards:not(.one):not(.two) > .card:last-child:nth-child(3n + 2) { grid-column: span 2 }

.card ul { margin: 12px 0 0; padding: 0 0 0 18px; color: #9fa8b7; font-size: 14px }
.card li { margin: 7px 0 }
.card li::marker { color: var(--accent) }

.arrow {
  color: var(--accent);
  margin-top: 25px;
  font-size: 20px;
  transition: transform .25s var(--ease-out);
}

.card:hover .arrow { transform: translate(4px, -4px) }

.steps {
  counter-reset: step;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 18px;
  margin-top: 50px;
}

.step { border-top: 1px solid var(--line); padding: 25px 5px }
.step:before { counter-increment: step; content: "0" counter(step); color: var(--blue); font-size: 12px }
.step h3 { font-size: 22px; margin: 28px 0 8px }
.step p { font-size: 14px; color: var(--muted) }

.facts {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  border: 1px solid var(--line);
  margin-top: 55px;
}

.fact { padding: 27px; border-right: 1px solid var(--line) }
.fact:last-child { border: 0 }
.fact strong { display: block; font-family: var(--font-display); font-size: 30px; font-variant-numeric: tabular-nums }
/* Direct child only. The label is always a direct span of .fact, and scoping
   it stops the rule reaching a span nested deeper — motion.js wraps a figure
   in one to wipe it, and a descendant selector restyled that 30px figure as
   an 11px uppercase label. */
.fact > span { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .09em }

/* Line mask for a figure with no number to count (motion.js initFacts builds
   the inner span at runtime). Same trick as .mask: the padding stops the
   descenders being clipped and the negative margin takes it back out of the
   layout, so the figure sits exactly where it did before it was wrapped. */
.fact-wipe {
  overflow: hidden;
  padding-bottom: .12em;
  margin-bottom: -.12em;
}

.fact-wipe-inner { display: block }


/* ==========================================================================
   7b. INSIGHTS
   Editorial row list, not a grid. .cards already occupies the "grid of
   equal tiles" slot on this page; repeating it reads as the same module
   twice. Rows use real per-row borders rather than the gap-over-background
   hairline trick, so the row itself is safe to animate.
   ========================================================================== */

.insights-head {
  display: flex;
  justify-content: space-between;
  gap: 30px;
  align-items: end;
}

.insights-head .title { margin-bottom: 0 }

.posts {
  margin-top: 46px;
  border-top: 1px solid var(--line);
}

.post {
  display: grid;
  grid-template-columns: 172px 1fr 60px;
  gap: 40px;
  align-items: start;
  padding: 34px 0;
  border-bottom: 1px solid var(--line);
  transition: background-color .25s var(--ease-out);
}

.post:hover { background: rgba(255, 255, 255, .022) }

.post-date {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: .12em;
  color: var(--muted);
  padding-top: 6px;
}

.post h3 {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(21px, 2.3vw, 30px);
  line-height: 1.16;
  letter-spacing: -.035em;
  margin: 0 0 10px;
  max-width: 780px;
  transition: color .25s var(--ease-out);
}

.post:hover h3 { color: var(--accent) }

.post p {
  color: #9fa8b7;
  font-size: 15px;
  line-height: 1.6;
  margin: 0;
  max-width: 720px;
}

.post-go {
  color: var(--accent);
  font-size: 20px;
  text-align: right;
  padding-top: 2px;
  transition: transform .25s var(--ease-out);
}

.post:hover .post-go { transform: translate(4px, -4px) }


/* ---- Article cards (insights.html) --------------------------------------
   Media-first cards for the post archive: thumbnail, category + date, title,
   short excerpt, "Read article". Unlike .cards (hairline grid faked with a
   1px gap over a background), these have real borders and gaps, so the card
   itself can be animated. Whole card is the link. */

.articles-head {
  display: flex;
  justify-content: space-between;
  align-items: end;
  margin-bottom: 30px;
}

.articles-head h2 { margin: 0 }

.article-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 28px 24px;
}

.article-card {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: 18px;
  overflow: hidden;
  background: #0a0e15;
  /* Border + background only. Opacity/transform are GSAP's during the
     entrance; see the transition note in §4. */
  transition: border-color .3s var(--ease-out), background-color .3s var(--ease-out);
}

.article-card:hover { border-color: rgba(255, 255, 255, .2); background: #0d121a }

.article-card:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* Thumbnails on the live site come in two ratios (16:9 and 3:2), so a fixed
   16:10 box with object-fit: cover keeps every row aligned. */
.article-media {
  position: relative;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background: #0c1119;
}

.article-media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform .9s var(--ease-out);
}

/* Slight dark lift at the bottom so the image meets the card body softly. */
.article-media:after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(10, 14, 21, .45), rgba(10, 14, 21, 0) 40%);
  pointer-events: none;
}

.article-card:hover .article-media img,
.article-card:focus-visible .article-media img { transform: scale(1.05) }

.article-badge {
  position: absolute;
  left: 18px;
  top: 18px;
  z-index: 1;
  padding: 7px 12px;
  border-radius: 999px;
  background: var(--accent);
  color: #0b0d12;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
}

.article-body {
  display: flex;
  flex-direction: column;
  gap: 12px;
  flex: 1;
  padding: 22px 24px 24px;
}

.article-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 12px;
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: #8190a4;
}

.article-cat { color: #d3d9e2; font-weight: 700 }

/* Small dot between category and date. */
.article-cat:after {
  content: "";
  display: inline-block;
  width: 3px;
  height: 3px;
  margin-left: 12px;
  border-radius: 50%;
  background: #56627a;
  vertical-align: middle;
}

.article-title {
  font-family: var(--font-display);
  font-size: 22px;
  line-height: 1.15;
  letter-spacing: -.03em;
  margin: 0;
  transition: color .25s var(--ease-out);
}

.article-card:hover .article-title,
.article-card:focus-visible .article-title { color: var(--accent) }

.article-excerpt {
  margin: 0;
  color: #9fa8b7;
  font-size: 14px;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.article-go {
  margin-top: auto;
  padding-top: 6px;
  font-size: 13px;
  font-weight: 700;
  color: #dfe6ef;
}

.article-go span {
  display: inline-block;
  margin-left: 4px;
  transition: transform .25s var(--ease-out);
}

.article-card:hover .article-go span { transform: translate(3px, -3px) }

/* Featured (newest) post: full width, image beside the copy. */
.article-card.is-featured {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: 1.35fr 1fr;
}

.is-featured .article-media { aspect-ratio: auto; min-height: 420px }
.is-featured .article-media:after { background: linear-gradient(to left, rgba(10, 14, 21, .35), rgba(10, 14, 21, 0) 35%) }
.is-featured .article-body { justify-content: center; gap: 18px; padding: 48px }
.is-featured .article-title { font-size: clamp(30px, 3.1vw, 46px); line-height: 1.02; letter-spacing: -.045em }
.is-featured .article-excerpt { font-size: 16px; -webkit-line-clamp: 4 }
.is-featured .article-go { margin-top: 8px; font-size: 14px }


/* ---- Project grid + filters (work.html) ---------------------------------
   Same card language as the article grid, with a play affordance and a
   filter bar above it. Real borders and gaps, so the card itself animates
   and can be moved by the FLIP re-layout (motion.js initFlip). */

.projects-head {
  display: flex;
  flex-direction: column;
  gap: 18px;
  margin-bottom: 30px;
}

.projects-head h2 { margin: 0 }

.filters {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.filters .btn { cursor: pointer }

/* Active filter: label and border in the accent, matching the hover colour
   used across the site. Only ever one at a time (site.js). .primary sits on
   "All Work" ONLY while it is the active filter, so the white button and the
   accent outline never both read as active. */
.filters .btn.is-active:not(.primary) {
  color: var(--accent);
  border-color: var(--accent);
}

.filters .btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.project-grid {
  position: relative;          /* FLIP parks leaving cards against this */
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 28px 24px;
  /* Cards take their own height rather than stretching to the tallest in the
     row: the double-width featured card would otherwise leave its row-mate
     with a large empty body. */
  align-items: start;
}

.project-card {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: 18px;
  overflow: hidden;
  background: #0a0e15;
  transition: border-color .3s var(--ease-out), background-color .3s var(--ease-out);
}

/* display:flex above would beat the hidden attribute the filter sets. */
.project-card[hidden] { display: none }

.project-card:hover { border-color: rgba(255, 255, 255, .2); background: #0d121a }

.project-card:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.project-media {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: #0c1119;
}

.project-media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform .9s var(--ease-out);
}

.project-card:hover .project-media img,
.project-card:focus-visible .project-media img { transform: scale(1.05) }

/* Play affordance: a ring with a CSS triangle, so there is no icon file. */
.project-play {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 62px;
  height: 62px;
  margin: -31px 0 0 -31px;
  border: 1px solid rgba(255, 255, 255, .55);
  border-radius: 50%;
  background: rgba(8, 11, 17, .42);
  backdrop-filter: blur(2px);
  transition: background-color .3s var(--ease-out), border-color .3s var(--ease-out), transform .4s var(--ease-out);
}

.project-play:after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  margin: -8px 0 0 -5px;
  border-style: solid;
  border-width: 8px 0 8px 13px;
  border-color: transparent transparent transparent #fff;
}

.project-card:hover .project-play,
.project-card:focus-visible .project-play {
  background: var(--accent);
  border-color: var(--accent);
  transform: scale(1.06);
}

.project-card:hover .project-play:after,
.project-card:focus-visible .project-play:after { border-left-color: #0b0d12 }

.project-badge {
  position: absolute;
  left: 18px;
  top: 18px;
  padding: 7px 12px;
  border-radius: 999px;
  background: var(--accent);
  color: #0b0d12;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
}

.project-body {
  display: flex;
  flex-direction: column;
  gap: 10px;
  flex: 1;
  padding: 20px 22px 22px;
}

.project-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 10px;
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: #8190a4;
}

.project-cat { color: #d3d9e2; font-weight: 700 }

.project-time:before,
.project-year:before {
  content: "";
  display: inline-block;
  width: 3px;
  height: 3px;
  margin-right: 10px;
  border-radius: 50%;
  background: #56627a;
  vertical-align: middle;
}

.project-title {
  font-family: var(--font-display);
  font-size: 21px;
  line-height: 1.15;
  letter-spacing: -.03em;
  margin: 0;
  transition: color .25s var(--ease-out);
}

.project-card:hover .project-title,
.project-card:focus-visible .project-title { color: var(--accent) }

.project-go {
  margin-top: auto;
  padding-top: 6px;
  font-size: 13px;
  font-weight: 700;
  color: #dfe6ef;
}

.project-go span {
  display: inline-block;
  margin-left: 4px;
  transition: transform .25s var(--ease-out);
}

.project-card:hover .project-go span { transform: translate(3px, -3px) }

/* Featured card: default view only — any category filter replaces it. */
.project-card.is-featured { grid-column: span 2 }
.is-featured .project-media { aspect-ratio: 16 / 9 }
.is-featured .project-title { font-size: clamp(26px, 2.4vw, 38px); letter-spacing: -.04em }
.is-featured .project-body { padding: 26px 28px 28px }
.is-featured .project-play { width: 78px; height: 78px; margin: -39px 0 0 -39px }

.project-empty {
  margin: 8px 0 0;
  color: #9fa8b7;
}


/* ---- Services: toggle-switch filters + service cards --------------------
   The work page filters are single-choice buttons; services are MULTI-select,
   so they are switches (role="switch"), with the label sitting above each one
   as in the supplied mockup. "All Services" stays a pill button because it is
   a reset action, not a state. */

.filters.switches {
  align-items: flex-end;
  gap: 14px 22px;
}

.switch-field {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

/* Sentence case and the body face, matching the mockup rather than the
   uppercase .eyebrow treatment used elsewhere. */
.switch-label {
  font-size: 13px;
  font-weight: 500;
  color: #c3cad5;
  transition: color .22s var(--ease-out);
}

/* Matches the supplied switch mockup: a white-outlined track with a solid
   white knob carrying a dark cross when off; an accent track with the knob
   slid right carrying a dark tick when on. The marks share one ink colour
   in both states — the primary button's label colour. */
.switch {
  position: relative;
  box-sizing: border-box;
  width: 64px;
  height: 32px;
  padding: 0;
  border: 2px solid #fff;
  border-radius: 999px;
  background: transparent;
  cursor: pointer;
  transition: background-color .28s var(--ease-out), border-color .28s var(--ease-out);
}

.switch:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* Knob: left when off, right when on. 24px inside a 28px inner track leaves
   an even 2px ring all round. */
.switch-knob {
  position: absolute;
  top: 50%;
  left: 2px;
  width: 24px;
  height: 24px;
  margin-top: -12px;
  border-radius: 50%;
  background: #fff;
  transition: transform .28s var(--ease-out);
}

/* The mark inside the knob: a cross when off, a tick when on. Both are the
   same two bars re-shaped, so there is no icon file and nothing to load. */
.switch-knob:before,
.switch-knob:after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  border-radius: 1px;
  background: #090b10;
  transition: width .28s var(--ease-out), height .28s var(--ease-out),
              margin .28s var(--ease-out), transform .28s var(--ease-out);
}

/* Off: two bars crossing. */
.switch-knob:before,
.switch-knob:after {
  width: 11px;
  height: 1.6px;
  margin: -.8px 0 0 -5.5px;
}

.switch-knob:before { transform: rotate(45deg) }
.switch-knob:after { transform: rotate(-45deg) }

/* On: accent track, knob slides right, and the two bars become the short
   and long strokes of a tick. */
.switch[aria-checked="true"] {
  background: var(--accent);
  border-color: var(--accent);
}

.switch[aria-checked="true"] .switch-knob { transform: translateX(32px) }

.switch[aria-checked="true"] .switch-knob:before {
  width: 5px;
  height: 2px;
  margin: 1px 0 0 -4.5px;
  transform: rotate(45deg);
}

.switch[aria-checked="true"] .switch-knob:after {
  width: 9.5px;
  height: 2px;
  margin: -1px 0 0 -2.5px;
  transform: rotate(-45deg);
}

/* The label sits BEFORE the switch, so this needs :has() rather than a
   sibling combinator. Browsers without it simply keep the resting colour. */
.switch-field:has(.switch[aria-checked="true"]) .switch-label,
.switch-field:hover .switch-label { color: #fff }

/* Service cards reuse .project-card; these are the differences. */
.service-num {
  position: absolute;
  left: 16px;
  top: 14px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .16em;
  color: #dfe6ef;
  text-shadow: 0 1px 10px rgba(0, 0, 0, .6);
}

.service-copy {
  margin: 0;
  color: #9fa8b7;
  font-size: 14px;
}

/* A section intro above the grid rather than inside a .projects-head. Keyed
   on the lead's position, so the block needs no class for it. */
.lead:has(+ .project-grid) { margin: 18px 0 44px }

/* Equal-height rows. .project-grid aligns to start so the featured card's
   row-mate is not stretched into a mostly empty box; that concern only
   applies to the first two cards, so everything after them stretches. Keyed
   on the featured card rather than a page-specific grid class, so a block
   that can feature its first card gets this for free. */
.project-grid:has(> .is-featured) > .project-card:nth-child(n+3) { align-self: stretch }

/* .industry-card.is-static was a non-clickable industry card. Every industry
   listed turned out to have a page of its own, so is-static never reached any
   markup and these three rules were dead. Removed 15 Oct 2026 along with the
   .industry-card class, which the card grid block cannot emit. */

/* Studio hub: the photograph that replaces the .visual placeholder, and the
   three-up route cards. */
.studio-visual {
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 20px;
  background: #0a0e15;
  /* The ratio belongs on the WRAPPER, exactly as .project-media and
     .article-media do it. Putting it on the <img> does not work: the width and
     height attributes (kept for layout stability) are presentational hints, so
     .studio-visual img{width:100%} overrides the width hint but nothing
     overrode height:1080px — with both dimensions definite, aspect-ratio is
     ignored and object-fit cropped a 16:9 photo into a 1080px-tall column. */
  aspect-ratio: 16 / 9;
}

.studio-visual img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* .project-grid.studio-grid asked for repeat(3, 1fr), which is this grid's
   base rule anyway, and both breakpoints named the two together. Removed as
   redundant 15 Oct 2026. */

/* Two cards, so two equal columns rather than three with a third of the row
   left empty. The blocks add .two whenever a grid holds exactly two, which is
   why this is not named after the page it first appeared on. .cards.two above
   is the same idea for the text grid. */
.project-grid.two { grid-template-columns: repeat(2, 1fr) }

/* One project. This grid has no backing plate to expose, so a ragged last
   row is just a ragged last row, the way every grid on the web ends. One
   card is the case that reads as broken: a single third filled and two
   thirds of daylight beside it. It takes the width the design already draws
   a featured card at, two of the three columns plus the gap between them,
   and centres it, so no proportion here is new. 200%/3 is two thirds of the
   row; a third of the 24px gap comes back off it. */
.project-grid.one > .project-card {
  grid-column: 1 / -1;
  width: calc(200% / 3 - 8px);
  margin-inline: auto;
}

/* Studio rental: the client rooms. Plain figures — nothing to click — so they
   reuse .studio-visual for the framed 16:9 image instead of .project-media,
   which carries the hover zoom and play badge of a card. */
.rooms {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 22px;
  margin-top: 38px;
}

.room { margin: 0 }

.room-cap {
  margin-top: 14px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .09em;
  color: var(--muted);
}


/* ---------------------------------------------------------------------------
   Lifted and typed body copy.

   Nothing in the design puts a heading, a list, an image or an embed inside
   a Rich text body, so none of this matches anything on the eighteen
   migrated pages. It exists for the 24 industry and service pages, whose
   copy came across from the old site carrying all four, and for anything NG
   types into that field afterwards: the editor offers him headings, lists
   and links whether or not the stylesheet was ready for them, and a client
   who uses a button we shipped should not get a broken page for it.

   The values are the policy page's, which is the one place the design
   already sets prose that has to carry structure. Keyed on the section
   rather than on a wrapper class, so it covers both layouts; the left
   column of the two-column layout holds only an eyebrow and a heading, and
   neither is an h3, a list or an image.
   --------------------------------------------------------------------------- */
[data-section="text"] h3,
[data-section="text"] h4 {
  font-family: var(--font-display);
  font-size: 24px;
  letter-spacing: -.02em;
  margin: 40px 0 12px;
}

[data-section="text"] ul,
[data-section="text"] ol {
  margin: 6px 0 16px;
  padding-left: 20px;
  color: #aab3c0;
}

[data-section="text"] li { margin: 4px 0 }
[data-section="text"] li::marker { color: var(--accent) }

/* A lifted image is whatever size it was uploaded at, which is routinely
   wider than the column it now sits in. */
[data-section="text"] img,
[data-section="text"] iframe,
[data-section="text"] video {
  display: block;
  max-width: 100%;
  height: auto;
  margin: 18px 0;
  border-radius: 14px;
}

/* Studio amenities: NG's own on-site list. Plain rows with hairline rules
   rather than the .cards gap trick, so each item can animate on its own. */
.amenities {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0 34px;
  margin: 26px 0 0;
  padding: 0;
  list-style: none;
}

.amenity {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 17px 0;
  border-top: 1px solid var(--line);
  font-size: 15px;
  font-weight: 600;
  color: #dfe6ef;
}

/* Lucide icons, inlined in the markup. currentColor is set here so the icon
   picks up the accent while the label keeps its own colour; flex:none stops
   the 20px box from being squeezed by a long two-line label. */
.amenity-icon {
  width: 20px;
  height: 20px;
  flex: none;
  color: var(--accent);
  /* Lucide is drawn on a 24px grid at stroke-width 2; scaled to 20px that
     renders at 1.67px, so re-thin it to stay in key with the hairline rules. */
  stroke-width: 1.7;
}

/* Client testimonials: one row per client — the video beside their words.
   A quote page lives or dies on the reading, so the quote is set in the body
   face at a comfortable size rather than in display type: several run past
   a hundred words and would be unreadable as headlines. Rows alternate sides
   so eleven of them do not read as one long list. */
.voices { margin-top: 50px }

.voice {
  display: grid;
  grid-template-columns: 5fr 7fr;
  gap: 56px;
  align-items: center;
  padding: 52px 0;
  border-top: 1px solid var(--line);
}

.voice:nth-child(even) { grid-template-columns: 7fr 5fr }
.voice:nth-child(even) .voice-card { order: 2 }

.voice-body { margin: 0 }
.voice-quote { margin: 0 }
.voice-quote p {
  margin: 0 0 14px;
  font-size: 19px;
  line-height: 1.6;
  color: #e3e8ef;
}

.voice-body figcaption { margin-top: 22px }
.voice-name { display: block; font-family: var(--font-display); font-size: 20px; color: #fff }
.voice-role { display: block; margin-top: 4px; font-size: 13px; color: var(--muted) }

.voice-context {
  margin: 16px 0 0;
  padding-left: 14px;
  border-left: 2px solid var(--accent);
  font-size: 14px;
  color: #aab3c0;
}

/* Caption file formats: an .amenities list whose items carry a name plus the
   file extension in a quieter, fixed-width face, so the extensions read as
   technical detail rather than as more labels. Its only page, post-production,
   was dropped in v1.0.9; the rules stay because the pattern is reusable and
   the captioning content still has to land somewhere. */
.format-intro { margin-top: 46px; color: #b7c0cd; max-width: 760px }
.format-ext {
  margin-left: 6px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px;
  font-weight: 500;
  color: var(--muted);
}

/* Daily rentals offer. */
.rental-band {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 22px 30px;
  margin-top: 46px;
  padding: 30px 34px;
  border: 1px solid var(--line);
  border-radius: 20px;
  background:
    radial-gradient(circle at 12% 25%, rgba(40, 130, 255, .18), transparent 45%),
    #0a0e15;
}

.rental-band h3 {
  font-family: var(--font-display);
  font-size: clamp(24px, 2.4vw, 32px);
  letter-spacing: -.035em;
  margin: 0 0 8px;
}

.rental-band p { margin: 0; color: #9fa8b7 }


/* Supporting section panel */
.approach-copy { padding: 26px 28px 30px }

.approach-copy h3 {
  font-family: var(--font-display);
  font-size: 24px;
  letter-spacing: -.035em;
  margin: 0 0 10px;
}

.approach-copy p { margin: 0 0 20px; color: #9fa8b7; font-size: 15px }


/* ---- Lightbox: project video + case study ------------------------------
   One <dialog> reused for both (site.js). Native showModal() supplies the
   focus trap, the backdrop and Escape. */

.lightbox {
  width: min(1080px, 92vw);
  max-height: 88vh;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text);
  overflow: visible;
}

.lightbox::backdrop {
  background: rgba(3, 5, 8, .84);
  backdrop-filter: blur(6px);
}

.lightbox-inner {
  max-height: 88vh;
  overflow: auto;
  border: 1px solid var(--line);
  border-radius: 20px;
  background: #0a0e15;
  box-shadow: 0 50px 120px rgba(0, 0, 0, .6);
}

.lightbox-close {
  position: absolute;
  right: -6px;
  top: -46px;
  width: 38px;
  height: 38px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: rgba(10, 14, 21, .9);
  color: #dfe6ef;
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  transition: color .22s var(--ease-out), border-color .22s var(--ease-out);
}

.lightbox-close:hover,
.lightbox-close:focus-visible { color: var(--accent); border-color: var(--accent) }

.lightbox-video {
  position: relative;
  aspect-ratio: 16 / 9;
  background: #05070b;
}

.lightbox-video iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

.lightbox-caption { padding: 20px 26px 24px }

.lightbox-cat {
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: #8190a4;
}

.lightbox-title {
  font-family: var(--font-display);
  font-size: 26px;
  letter-spacing: -.035em;
  margin: 8px 0 0;
}

/* Case study panel */
.case { padding: 34px }

.case-title {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: clamp(28px, 3vw, 42px);
  line-height: 1.02;
  letter-spacing: -.045em;
  margin: 10px 0 20px;
}

.case-facts {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 16px;
  margin: 0 0 26px;
  padding: 18px 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

.case-facts dt {
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: #8190a4;
  margin-bottom: 6px;
}

.case-facts dd { margin: 0; color: #dfe6ef }

.case-body h3 {
  font-family: var(--font-display);
  font-size: 15px;
  letter-spacing: .02em;
  margin: 22px 0 6px;
}

.case-body p { margin: 0; color: #9fa8b7; max-width: 62ch }

.case-note {
  margin-top: 22px !important;
  padding: 14px 16px;
  border: 1px dashed var(--line);
  border-radius: 10px;
  font-size: 13px;
}

.case-play { margin-top: 26px; cursor: pointer }


/* ==========================================================================
   8. WORK GRID
   ========================================================================== */

.workgrid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
  margin-top: 45px;
}

.work {
  position: relative;
  /* Behind the photo, so a tile with no image still reads as a tile. */
  background: linear-gradient(145deg, #293b55, #0a1018 60%, #050607);
  min-height: 490px;
  border: 1px solid var(--line);
  border-radius: 22px;
  padding: 30px;
  display: flex;
  align-items: flex-end;
  overflow: hidden;
}

.work.wide { grid-column: 1 / -1; min-height: 610px }

/* The photo is a real <img> rather than a CSS background on a separate
   layer. It was a layer so the artwork could parallax inside a fixed tile;
   the parallax was dropped because the hover zoom is the effect anyone
   notices, and a background image cannot be expressed as an editable image
   field. The scrim moved to .work:before, which is where it belongs now
   that nothing needs to travel underneath it. */
/* Three layers, and the order is load bearing. The old single-element
   version stacked them in one background-image: scrim, photo, fallback.
   Split across elements they need explicit z-index, or the opaque fallback
   paints over the photo and every tile renders as a plain dark panel. */
.work-img {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform .7s cubic-bezier(.22, 1, .36, 1);
}

.work:before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  background: linear-gradient(to top, rgba(5, 7, 11, .94), rgba(5, 7, 11, .28) 55%, rgba(5, 7, 11, .55));
}

.work:hover .work-img,
.work:focus-visible .work-img { transform: scale(1.05) }

.work-meta { position: relative; z-index: 3 }
.work small { letter-spacing: .12em; text-transform: uppercase; color: #c1c9d5 }
.work h3 { font-family: var(--font-display); font-size: 34px; margin: 7px 0 0; letter-spacing: -.03em }


/* ==========================================================================
   9. FORMS
   ========================================================================== */

.form {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  margin-top: 36px;
}

.field { display: flex; flex-direction: column; gap: 8px }
.field.full { grid-column: 1 / -1 }

label {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: .1em;
  color: #c3cad5;
}

input, select, textarea {
  width: 100%;
  background: #0c1119;
  border: 1px solid var(--line);
  border-radius: 10px;
  color: #fff;
  padding: 16px;
  font: inherit;
  transition: border-color .2s, box-shadow .2s;
}

/* Focus ring in the accent (#f4b942 = 244, 185, 66), matching the rest of
   the interactive states; the old blue predates the accent. */
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: rgba(244, 185, 66, .55);
  box-shadow: 0 0 0 3px rgba(244, 185, 66, .12);
}

textarea { min-height: 150px }


/* ---- Location: full-width map + office card (contact-us.html) -----------
   Sits where the generic closing CTA would be. The card overlays the map on
   desktop and drops below it (overlapping the map's lower edge) on smaller
   screens.

   The card sits on the RIGHT, unlike the live page. The keyless embed always
   centres its pin, and a left-hand card lands right next to it; moving the
   pin away would mean pushing the iframe past the right edge, which crops
   Google's zoom controls and attribution (keep those visible). On the right,
   the pin sits clear in the left half with nothing cropped but the embed's
   redundant top-left place box. */

.location {
  position: relative;
  border-top: 1px solid var(--line);
  background: var(--bg);
}

.location-map {
  position: relative;
  height: clamp(540px, 74vh, 700px);
  overflow: hidden;
  background: #0c1119;
}

/* Google's embed cannot be restyled from outside, so it is darkened with a
   filter: invert + hue-rotate(180deg) turns light roads dark while keeping
   hues roughly where they were (water stays blue, the pin stays red);
   saturate/brightness pull it back toward the page's ink blue. */
.location-frame {
  position: absolute;
  /* Extends 130px above the visible box and is clipped by .location-map's
     overflow, which hides the embed's own place box in its top-left corner.
     Right and bottom edges stay flush so Google's controls and attribution
     remain visible. The pin shifts up by half that (65px). */
  top: -130px;
  left: 0;
  width: 100%;
  height: calc(100% + 130px);
  border: 0;
  filter: invert(.92) hue-rotate(180deg) saturate(.55) brightness(.9) contrast(.92);
  pointer-events: none;
}

.location.is-active .location-frame { pointer-events: auto }

/* Blends the map into the page: soft top and bottom edges, plus a shade on
   the left so the card never sits on busy streets. Above the map, below the
   shield and card. */
.location-map:before,
.location-map:after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
}

.location-map:before {
  background:
    linear-gradient(to bottom, var(--bg), rgba(7, 9, 13, 0) 14%),
    /* Shallower at the bottom so Google's attribution stays legible. */
    linear-gradient(to top, rgba(7, 9, 13, .85), rgba(7, 9, 13, 0) 7%);
}

.location-map:after {
  background: linear-gradient(270deg, rgba(5, 7, 11, .72), rgba(5, 7, 11, .25) 38%, rgba(5, 7, 11, 0) 60%);
  transition: opacity .35s var(--ease-out);
}

/* While the visitor is using the map, lift the shade so it reads clearly. */
.location.is-active .location-map:after { opacity: .35 }

/* Transparent cover over the whole map until it is clicked. */
.location-shield {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  /* Bottom centre: Google's own controls occupy the bottom-left (map type)
     and bottom-right (zoom) corners. */
  align-items: flex-end;
  justify-content: center;
  padding: 0 0 48px;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  cursor: pointer;
}

.location-shield-label {
  padding: 10px 16px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: rgba(10, 14, 21, .82);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .06em;
  color: #dfe6ef;
  transition: border-color .22s var(--ease-out), color .22s var(--ease-out);
}

.location-shield:hover .location-shield-label,
.location-shield:focus-visible .location-shield-label {
  border-color: var(--accent);
  color: var(--accent);
}

.location-shield:focus-visible { outline: none }

/* site.js toggles [hidden]; display:flex above would otherwise override it. */
.location-shield[hidden] { display: none }

/* Overlay layer for the card; lets clicks through to the map around it. */
.location-inner {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  pointer-events: none;
}

.location-card {
  width: min(440px, 100%);
  padding: 34px;
  border: 1px solid var(--line);
  border-radius: 20px;
  background: rgba(10, 14, 21, .9);
  box-shadow: 0 35px 80px rgba(0, 0, 0, .45);
  pointer-events: auto;
}

.location-name {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: 34px;
  line-height: 1;
  letter-spacing: -.045em;
  margin: 12px 0 18px;
}

.location-details { font-style: normal }

.location-address {
  margin: 0 0 20px;
  font-size: 17px;
  line-height: 1.5;
  color: #c3cbd7;
}

.location-contact {
  display: grid;
  gap: 10px;
  margin: 0;
  padding: 18px 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

.location-contact > div {
  display: grid;
  grid-template-columns: 64px 1fr;
  align-items: baseline;
  gap: 12px;
}

.location-contact dt {
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: #8190a4;
}

.location-contact dd { margin: 0; min-width: 0; overflow-wrap: anywhere }

.location-contact a {
  color: #eef2f7;
  transition: color .22s var(--ease-out);
}

.location-contact a:hover,
.location-contact a:focus-visible { color: var(--accent) }

/* Social links are deliberately NOT here: they belong in the footer, where
   they are reachable from every page (see MOTION.md §8). */
.location-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin: 22px 0 0;
}


/* ==========================================================================
   10. CTA + FOOTER
   ========================================================================== */

.cta {
  padding: 125px 0;
  background:
    radial-gradient(circle at 30% 30%, rgba(40, 130, 255, .2), transparent 31%),
    #05070b;
  border-top: 1px solid var(--line);
}

.cta h2 {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: clamp(58px, 8vw, 120px);
  line-height: .86;
  letter-spacing: -.064em;
  text-transform: uppercase;
  margin: 12px 0 30px;
  max-width: 1100px;
}

.footer {
  padding: 48px 0;
  border-top: 1px solid var(--line);
  color: #929baa;
  font-size: 13px;
}

.footgrid {
  display: grid;
  grid-template-columns: 2fr repeat(3, 1fr);
  gap: 35px;
}

.footgrid h4 { color: #fff; margin: 0 0 15px }
.footgrid a { display: block; margin: 8px 0; transition: color .25s var(--ease-out) }
.footgrid a:hover,
.footgrid a:focus-visible { color: var(--accent) }

/* Footer bottom bar: copyright + privacy policy. */
.footbar {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 10px 24px;
  margin-top: 42px;
  padding-top: 22px;
  border-top: 1px solid var(--line);
  font-size: 12px;
  color: #7d8796;
}
.footbar a { transition: color .25s var(--ease-out) }
.footbar a:hover, .footbar a:focus-visible { color: var(--accent) }

/* Social row (site.js builds it from the `socials` list; icons are Simple
   Icons, single-path and FILLED — see the comment there). */
.socials {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 22px;
}

/* The display:block on .footgrid a would stack these vertically, so each one
   is reset to a flex box in its own right. Kept at 40px square: these are the
   only tap targets in the footer that are not a line of text, and anything
   smaller is awkward on a phone. */
.socials .social {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  margin: 0;
  border: 1px solid var(--line);
  border-radius: 50%;
  color: #929baa;
  transition: color .25s var(--ease-out), border-color .25s var(--ease-out), background-color .25s var(--ease-out);
}

.socials .social svg { width: 17px; height: 17px; display: block }

.socials .social:hover,
.socials .social:focus-visible {
  color: var(--accent);
  border-color: var(--accent);
  background: rgba(244, 185, 66, .08);
}


/* ==========================================================================
   11. UTILITIES
   ========================================================================== */

/* Legal prose (privacy-policy.html). One narrow reading column in the body
   face: a policy is read top to bottom, so the measure matters more than
   anything else on the page. Headings step down to a size that marks the
   sections without turning a legal document into a marketing layout. */
.legal { max-width: 760px; margin-top: 50px }
.legal-lede { font-size: 20px; color: #b7c0cd; margin: 0 0 10px }
.legal h2 {
  font-family: var(--font-display);
  font-size: 24px;
  letter-spacing: -.02em;
  margin: 46px 0 12px;
  padding-top: 26px;
  border-top: 1px solid var(--line);
}
.legal p, .legal li { color: #aab3c0; font-size: 16px; line-height: 1.7 }
.legal p { margin: 0 0 14px }
.legal ul { margin: 6px 0 16px; padding-left: 20px }
.legal li { margin: 4px 0 }
.legal li::marker { color: var(--accent) }
.legal-contact { font-style: normal; color: #d8dee7; line-height: 1.8; margin: 8px 0 24px }
.legal a {
  color: #fff;
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-underline-offset: 4px;
}
.legal a:hover, .legal a:focus-visible { color: var(--accent) }
.legal-dates { margin-top: 40px; font-size: 13px; color: var(--muted) }

/* The pre-publication review list on the policy page. Uses .note's colours
   so it reads as developer direction, but at the width of the text it
   annotates, and with a real list. */
.legal-review { max-width: 760px; margin-top: 0 }
.legal-review ul { margin: 10px 0 0; padding-left: 18px }
.legal-review li { margin: 6px 0; line-height: 1.55 }

.note {
  margin-top: 35px;
  padding: 17px;
  border: 1px solid rgba(78, 165, 255, .28);
  background: rgba(78, 165, 255, .06);
  color: #b9c9dd;
  font-size: 13px;
}

.sitemap { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; margin-top: 40px }
.sitegroup { border: 1px solid var(--line); border-radius: 16px; padding: 25px; background: var(--panel) }
.sitegroup h3 { margin-top: 0 }
.sitegroup a { display: block; padding: 9px 0; border-top: 1px solid var(--line); color: #c5cdd8 }

.mobile { display: none }


/* ==========================================================================
   12. TRUST BAND + TESTIMONIAL   (both injected by site.js)
   ========================================================================== */

.trust-band {
  padding: 72px 0;
  /* Rows slide in from off to the sides; without this they widen the page. */
  overflow: hidden;
  background: linear-gradient(110deg, #0e1b2e, #07101d 65%, #05070b);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

.trust-head {
  display: flex;
  justify-content: space-between;
  gap: 30px;
  align-items: end;
}

.trust-head h2 {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: clamp(34px, 4vw, 60px);
  line-height: 1;
  letter-spacing: -.05em;
  margin: 10px 0 0;
  max-width: 760px;
}

.client-logos {
  display: grid;
  gap: 10px;
  margin-top: 42px;
}

/* Each row is a rigid track, wider than its container and centred by the
   negative margin, so it overhangs by 35% on BOTH sides. That overhang is what
   keeps the band full edge-to-edge at every point in the slide — the row moves
   as one object and the viewport is always looking at the middle of it.
   Travel must stay well under 35% or an end of the track comes into view. */
.client-row {
  display: grid;
  grid-template-columns: repeat(14, 1fr);
  gap: 10px;
  width: 170%;
  margin-left: -35%;
  will-change: transform;
}

.client-logo {
  /* No tile: no border, no background, no hover. The cell only reserves the
     space and centres the mark, so the band's own gradient shows through
     the logos' transparent backgrounds. Read-only, so no cursor or state
     changes either. */
  min-height: 92px;
  display: grid;
  place-items: center;
  padding: 8px;
}

/* Artwork in assets/logos is already pure white on a transparent background
   and cropped to the mark's edges (see the note on `clients` in site.js), so
   there is no filter here and every logo renders in the same colour. The old
   invert-on-a-black-tile workaround existed only because the hotlinked source
   files had opaque white backgrounds.

   Sizing is optically balanced rather than one fixed box: site.js gives each
   image --logo-k from its aspect ratio (1 for a CNN/JLL-shaped mark, down to
   0.6 for long wordmarks, up to 1.45 for tall marks like UCF), so logos read at a
   similar visual weight. --logo-base scales the whole band; max-width keeps
   the longest wordmarks inside their cell, and object-fit keeps them
   undistorted when that cap bites. opacity sets the shared tone: .82 white
   sits a step under the headings. */
.client-logo img {
  --logo-base: 50px;
  height: calc(var(--logo-base) * var(--logo-k, 1));
  width: auto;
  max-width: 90%;
  object-fit: contain;
  opacity: .82;
}

.testimonial { padding: 105px 0; border-top: 1px solid var(--line) }

/* All slides share one grid cell, so the section is as tall as the longest
   review and nothing shifts as they cycle. */
.testimonial-stack { display: grid; margin-top: 30px }

.testimonial-item {
  grid-area: 1 / 1;
  margin: 0;
  max-width: 1080px;
}

/* Without JS (or under reduced motion) the carousel never runs, so show a
   single static review rather than three stacked on top of each other. */
.testimonial-item:not(:first-child) { display: none }
html.js .testimonial-item:not(:first-child) { display: block }
html.js .testimonial-item { opacity: 0 }

.stars {
  color: var(--accent);
  font-size: 19px;
  letter-spacing: .34em;
  margin-bottom: 22px;
}

/* Dots left, link to the full testimonials page right. margin-left:auto
   rather than space-between keeps the button right-aligned when the dots
   are hidden (no JS / reduced motion) and when the row wraps. */
.testimonial-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 20px 30px;
  margin-top: 42px;
}

.testimonial-foot .btn { margin-left: auto }

.testimonial-dots {
  display: flex;
  gap: 11px;
}

html:not(.js) .testimonial-dots { display: none }

.dot {
  width: 9px;
  height: 9px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  background: rgba(255, 255, 255, .26);
  transition: background .25s var(--ease-out), transform .25s var(--ease-out);
}

.dot:hover { background: rgba(255, 255, 255, .55) }
.dot.is-active { background: var(--accent); transform: scale(1.3) }
.dot:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px }

.testimonial-by {
  color: #aeb6c3;
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: .12em;
  margin-top: 28px;
}


/* ==========================================================================
   13. FORM CTA   (replaces .cta on most pages — see site.js)
   ========================================================================== */

.form-cta {
  padding: 110px 0 125px;
  background:
    radial-gradient(circle at 18% 20%, rgba(40, 130, 255, .18), transparent 30%),
    #05070b;
  border-top: 1px solid var(--line);
}

.form-cta-grid {
  display: grid;
  grid-template-columns: .82fr 1.18fr;
  gap: 80px;
  align-items: start;
}

.form-cta h2 {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: clamp(48px, 6.5vw, 92px);
  line-height: .9;
  letter-spacing: -.058em;
  text-transform: uppercase;
  margin: 13px 0 25px;
}

.form-cta-copy { color: #aeb8c7; font-size: 18px; max-width: 520px }

.quick-form {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px;
  padding: 30px;
  border: 1px solid var(--line);
  border-radius: 20px;
  background: rgba(13, 17, 24, .84);
  box-shadow: 0 35px 80px rgba(0, 0, 0, .3);
}

.quick-form .field.full { grid-column: 1 / -1 }
.quick-form input, .quick-form select, .quick-form textarea { background: #080c12 }
.quick-form textarea { min-height: 112px }
.quick-form button { cursor: pointer }

.form-success {
  display: none;
  grid-column: 1 / -1;
  padding: 18px;
  border: 1px solid rgba(78, 165, 255, .35);
  border-radius: 10px;
  background: rgba(78, 165, 255, .09);
  color: #dbeaff;
}

.form-success.show { display: block }


/* ==========================================================================
   14. MOTION LAYER
   --------------------------------------------------------------------------
   Every rule here is scoped to html.js, which is set by the inline script in
   index.html ONLY when the visitor has not asked for reduced motion, and is
   stripped again if motion.js fails to boot. Without .js the page renders
   fully visible and static — so JS failure degrades to the original design
   rather than to a blank screen.
   ========================================================================== */

/* Line-mask wrapper for the big display headings. */
.mask {
  display: block;
  overflow: hidden;
  /* Padding keeps descenders and the tight tracking from being clipped; the
     negative margin cancels it out of the layout. .24em because these
     headings' descender ink reaches .167em below their inline box (measured
     from Satoshi Black's metrics at their .94 line-height) — .12em clipped
     them. See .hero-word for the same correction at the hero's size. */
  padding-bottom: .24em;
  margin-bottom: -.24em;
}

.mask-inner { display: block; will-change: transform }

/* ---- Pre-animation states -------------------------------------------------
   Each is released by a class motion.js adds when that entrance finishes
   (.is-revealed on the container, or dropping .reveal from the element). The
   DEFAULT state is hidden, so nothing depends on JS having run yet — there is
   no flash. */

html.js .mask:not(.is-revealed) .mask-inner { transform: translateY(110%) }

html.js .reveal { opacity: 0 }

html.js .cards:not(.is-revealed) .card > * { opacity: 0 }
html.js .posts:not(.is-revealed) .post { opacity: 0 }
html.js .hero:not(.is-revealed) [data-hero-item] { opacity: 0 }
html.js .hero:not(.is-revealed) .filmframe { opacity: 0 }
/* Hidden only until motion.js has split it into words (it then sets an
   inline opacity: 1 and hides the words by transform instead). */
html.js .subhero:not(.is-revealed) .hero-title { opacity: 0 }
html.js .subhero:not(.is-revealed) .subhero-glow { opacity: 0 }
html.js .form:not(.is-revealed) .field { opacity: 0 }
html.js .article-card:not(.is-revealed) { opacity: 0 }
html.js .project-card:not(.is-revealed) { opacity: 0 }
/* Scoped to .section so it cannot touch index's facts bar, which lives in the
   hero and is revealed as one [data-hero-item] — its .fact cells are never
   addressed individually and nothing would ever un-hide them. */
html.js .section .facts:not(.is-revealed) .fact { opacity: 0 }


/* ---- Transition suppression -----------------------------------------------
   .btn carries `transition: .22s` (an all-properties shorthand) and .arrow
   transitions transform. Both are GSAP targets during their entrance, so the
   browser would interpolate toward every value GSAP writes — 220ms of lag
   layered over a 900ms tween, which reads as mush. Suppress the element's own
   transitions until its entrance completes, then hand hover states back to
   CSS. motion.js also clears GSAP's inline transform on completion, otherwise
   it would outrank the :hover rules forever. */

html.js .reveal,
html.js .cards:not(.is-revealed) .card > *,
html.js .posts:not(.is-revealed) .post,
html.js .hero:not(.is-revealed) [data-hero-item],
html.js .form:not(.is-revealed) .field,
html.js .form:not(.is-revealed) .field .btn,
html.js .article-card:not(.is-revealed),
html.js .article-card:not(.is-revealed) .article-media img,
html.js .project-card:not(.is-revealed),
html.js .project-card:not(.is-revealed) .project-media img {
  transition: none !important;
}

/* Belt and braces: if the OS asks for reduced motion mid-session, or the
   stylesheet is read without the inline gate, nothing stays hidden. */
@media (prefers-reduced-motion: reduce) {
  html.js .mask .mask-inner,
  html.js .reveal,
  html.js .cards .card > *,
  html.js .posts .post,
  html.js .hero [data-hero-item],
  html.js .hero .filmframe,
  html.js .testimonial-item:first-child,
  html.js .subhero .hero-title,
  html.js .subhero .subhero-glow,
  html.js .form .field,
  html.js .article-card,
  html.js .article-card .article-media img,
  html.js .project-card,
  html.js .project-card .project-media img,
  html.js .section .facts .fact,
  html.js .fact-wipe-inner {
    opacity: 1 !important;
    transform: none !important;
  }
  .btn:hover { transform: none }
}


/* ==========================================================================
   15. RESPONSIVE
   ========================================================================== */

@media (max-width: 900px) {
  .split { grid-template-columns: 1fr; gap: 38px }
  .cards { grid-template-columns: 1fr 1fr }
  /* Two columns now, so a different card is the one left on its own: the
     three-up spans are given back first, then the two-up one is set. */
  .cards:not(.one):not(.two) > .card:last-child:nth-child(3n + 1),
  .cards:not(.one):not(.two) > .card:last-child:nth-child(3n + 2) { grid-column: auto }
  .cards:not(.one):not(.two) > .card:last-child:nth-child(2n + 1) { grid-column: 1 / -1 }
  .post { grid-template-columns: 132px 1fr 40px; gap: 26px }
  .insights-head { flex-direction: column; align-items: flex-start }
  .steps, .facts { grid-template-columns: 1fr 1fr }
  .workgrid { grid-template-columns: 1fr }
  .work.wide { grid-column: auto }
  .footgrid { grid-template-columns: 1fr 1fr }
  .sitemap { grid-template-columns: 1fr 1fr }

  .client-row { grid-template-columns: repeat(9, 1fr) }
  .client-row .client-logo:nth-child(n+10) { display: none }
  .trust-head { align-items: flex-start; flex-direction: column }
  .form-cta-grid { grid-template-columns: 1fr; gap: 38px }

  .location-map { height: 440px }
  .location-inner { position: relative; inset: auto; display: block; margin-top: -90px; padding-bottom: 72px; pointer-events: auto }
  .location-card { width: 100%; max-width: 560px }
  /* The card overlaps the map's bottom 90px here, and the pin sits a little
     above centre, so the label goes in the band between the two. */
  .location-shield { align-items: flex-end; justify-content: center; padding: 0 0 112px }
  .location-map:after { background: none }

  .project-grid { grid-template-columns: 1fr 1fr }
  /* Two columns from here down, so the featured width IS the full row.
     The margin has to go back with the width: auto margins stop a grid item
     stretching, and width:auto on a box that is not stretching means
     shrink to fit, which drew the lone card at its text width. */
  .project-grid.one > .project-card { width: auto; margin-inline: 0 }
  .amenities { grid-template-columns: 1fr 1fr; gap: 0 26px }
  /* Three room figures do not divide into two columns without orphaning one,
     and they are supporting detail — one per row reads better than 2 + 1. */
  .rooms { grid-template-columns: 1fr }
  /* Stack every testimonial the same way: video first, then the words. The
     even-row flip only makes sense side by side. */
  .voice,
  .voice:nth-child(even) { grid-template-columns: 1fr; gap: 26px; padding: 40px 0 }
  .voice:nth-child(even) .voice-card { order: 0 }
  .project-card.is-featured { grid-column: span 2 }
  .case { padding: 26px }
  .case-facts { grid-template-columns: 1fr 1fr }

  .article-grid { grid-template-columns: 1fr 1fr }
  .article-card.is-featured { grid-template-columns: 1fr }
  .is-featured .article-media { aspect-ratio: 16 / 9; min-height: 0 }
  .is-featured .article-media:after { background: linear-gradient(to top, rgba(10, 14, 21, .45), rgba(10, 14, 21, 0) 40%) }
  .is-featured .article-body { padding: 28px }
}

@media (max-width: 600px) {
  .wrap { width: min(calc(100% - 24px), var(--max)) }
  .brand span:last-child { display: none }
  .nav .brand-logo { width: 190px }
  .nav.is-condensed .brand-logo { width: 168px }
  .heroin { padding-top: 110px }
  /* Fluid, not a flat 56px.

     The headline's longest WORD cannot break, and .mask clips what does not
     fit — so an oversized size does not overflow the page, it silently slices
     letters off the right edge. At 375px, "EXPERIENCES." wanted 362px inside a
     335px column and lost its last letters; the same was true of
     "CONVERSATION" on the podcast page.

     12vw is measured, not chosen. Every hero headline was rendered at 320,
     375 and 430px and its longest word compared with its column: the tightest
     case (podcast at 320px) tolerates 12.78vw, so 12vw clears it everywhere
     with room to spare. The 56px cap keeps phones from about 470px up exactly
     where they were. Re-measure if a hero headline ever gains a longer word —
     .mask hides this failure rather than showing it. */
  .hero h1 { font-size: clamp(36px, 12vw, 56px) }
  .section { padding: 75px 0 }
  /* .cards.two is listed explicitly: its base rule is a two-class selector,
     so the bare .cards rule here would not outrank it. */
  .cards, .cards.two, .steps, .facts, .form, .sitemap { grid-template-columns: 1fr }
  /* One column: every row holds one card, so every row is full and every
     span goes back. */
  .cards:not(.one):not(.two) > .card:last-child:nth-child(2n + 1),
  .cards:not(.one):not(.two) > .card:last-child:nth-child(3n + 1),
  .cards:not(.one):not(.two) > .card:last-child:nth-child(3n + 2) { grid-column: auto }
  .post { grid-template-columns: 1fr; gap: 12px; padding: 26px 0 }
  .post-date { padding-top: 0 }
  .post-go { text-align: left }
  .field.full { grid-column: auto }
  .fact { border-right: 0; border-bottom: 1px solid var(--line) }
  .footgrid { grid-template-columns: 1fr }
  .visual { min-height: 420px }
  .work, .work.wide { min-height: 420px }
  .client-row { grid-template-columns: repeat(6, 1fr) }
  .client-row .client-logo:nth-child(n+7) { display: none }
  .client-logo { min-height: 76px }
  .client-logo img { --logo-base: 40px }
  .quick-form { grid-template-columns: 1fr; padding: 20px }
  .quick-form .field.full { grid-column: auto }

  .location-map { height: 380px }
  .location-card { padding: 24px }
  .location-name { font-size: 28px }
  .location-actions .btn { flex: 1 1 auto }

  /* .two is listed explicitly: its base rule is a two-class selector, so the
     bare .project-grid rule here would not outrank it. */
  .project-grid,
  .project-grid.two { grid-template-columns: 1fr; gap: 20px }
  .amenities { grid-template-columns: 1fr }
  .rental-band { padding: 24px }
  .rental-band .btn { flex: 1 1 auto }
  .project-card.is-featured { grid-column: auto }
  .filters { gap: 8px }
  .filters .btn { padding: 11px 15px; font-size: 14px }
  .filters.switches { gap: 14px 18px }
  .filters.switches .btn { width: 100%; margin-bottom: 4px }
  .case-facts { grid-template-columns: 1fr }
  .lightbox-close { top: -44px; right: 0 }

  .article-grid { grid-template-columns: 1fr; gap: 20px }
  .article-body, .is-featured .article-body { padding: 20px }
  .is-featured .article-title { font-size: 28px }
}
