/* ==========================================================================
   THE UNBUILT — style.css
   Design tokens + typography + base styles.
   Built plain CSS (no preprocessor) so it previews directly with Live Server.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. DESIGN TOKENS
   Source: webbrand_colours.png (colour picker export) + Figma "Font styles"
   frame (node 2589:622). Raw brand colours below are the single source of
   truth — everything else (Tailwind config, components) should reference
   these variables rather than hard-coded hex values.
   -------------------------------------------------------------------------- */
:root {
  /* Brand colours */
  --color-black:        #0F0F0F; /* primary dark / background */
  --color-grey:         #6D6D6D; /* muted / secondary text */
  --color-berry:        #BE2056; /* accent — used in the wireframe graphics */
  /* Flat 0.65x scale of --color-berry (R/G/B each × 0.65, rounded) — a
     deliberately darker shade for use as an accent ON TOP OF a berry
     surface, where plain --color-berry itself wouldn't read as a distinct
     colour. First use: the "Get in touch" CTA card's own hover shadow, per
     request ("on hover the colour of the shadow under the card can be a
     dark berry") — that card's surface is already --color-berry, so its
     hover shadow needed its own darker tone to stay visible against it. */
  --color-berry-dark:   #7C1538;
  --color-white:        #FFFFFF;
  --color-grey-light:   #D1D1D1; /* borders, dividers on light sections */
  --color-lime:         #ECFF5B; /* primary accent — CTAs, highlights */
  --color-offwhite:     #f0eee9; /* main page background + body copy on dark backgrounds */
  --color-grey-lighter: var(--color-offwhite); /* merged with offwhite by request — same
     rgb(240 238 233) value, so every former grey-lighter background (body bg-grey-lighter
     on every page, the style-guide swatches, the image-placeholder hatch) now renders as
     offwhite. Kept as its own token (rather than deleted) since bg-grey-lighter/
     text-grey-lighter/border-grey-lighter utilities are still wired up in
     js/tailwind-config.js and used across every page. */

  /* Black at 20% was sampled as a usage example (shadows / overlays), not a
     standalone token — reproduce it with rgba() or Tailwind's `/20` opacity
     modifier on --color-black rather than a separate variable. */
  --color-black-20: rgba(15, 15, 15, 0.2);

  /* --color-black lightened 10% toward white — a solid colour (not an alpha
     tint) for use as its own distinct surface, first requested for the main
     nav's default item background ("black bg colour but 10% lighter than
     the main black"). rgb(15,15,15) + 10% × (255,255,255 − 15,15,15) =
     rgb(39,39,39) = #272727. Kept as a plain hex rather than rgba() so it
     reads as a flat colour regardless of what (if anything) sits behind
     it — unlike --color-black-20 above, which is meant to be seen as a
     tint over something else. */
  --color-black-light10: #272727;

  /* Font families */
  --font-display: 'Bebas Neue', sans-serif;       /* H1 */
  --font-heading: 'Funnel Display', sans-serif;   /* H2–H4 */
  --font-body:    'Raleway', sans-serif;          /* copy + buttons */
}

/* --------------------------------------------------------------------------
   2. BASE / RESET
   -------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

/* Reserves the scrollbar's own width whether or not a scrollbar is actually
   showing, so toggling body { overflow: hidden } for the off-canvas nav
   (see initNav in js/main.js) never shifts the viewport's usable width —
   without this, Firefox in particular re-centers .container-based layouts
   (like .site-nav__top's Close button vs .site-header__inner's Menu button)
   by however wide its scrollbar was, throwing their alignment off by a few
   pixels only while the nav is open. */
html { scrollbar-gutter: stable; }
/* Clips any full-bleed decorative element that deliberately bleeds past the
   viewport edge (the "why work with us" watermark's 110vw bleed, .watermark
   further down this file) — and, per report ("on mobile you can scroll
   sideways as if there is something wider than it should be"), removes the
   actual horizontal scroll/rubber-band range those create on mobile Safari,
   whose root scrolling element is html, not body.

   IMPORTANT — this rule now lives ONLY on html, not body. It used to sit on
   both (see the removed comment on body's own rule below, and the pre-
   existing body-level rule that predates this fix, added for the watermark
   clipping). That combination silently broke position:sticky sitewide
   (reported: "'Recently brought to life' doesn't scroll with the page
   anymore" — .featured-sticky-col stopped pinning and just scrolled off
   with everything else). Root cause, confirmed with Playwright by toggling
   each declaration independently: per the CSS Overflow spec, the UA
   propagates a non-visible overflow to the actual viewport from body if
   body declares one, otherwise from html — but when BOTH declare a non-
   visible overflow, neither propagates; html instead becomes an honest,
   separate scrolling box of its own, which is what breaks every
   position:sticky descendant's "nearest scrolling ancestor" lookup
   sitewide, not just this one section. One declaration (html's, kept here)
   propagates to the viewport exactly as intended — same clipping, no
   competing declaration on body — so this is not the "y-axis only" fix the
   previous comment on this rule described; the actual fix is "only one of
   html/body may set a non-visible overflow, ever, anywhere in this file". */
html { overflow-x: hidden; }
/* Matches body's own offwhite fill, per a follow-up report ("the background
   colour is getting lighter again... while scrolling") that persisted even
   after the .reviews-score-specific fix below (removing its scroll-reveal
   fade) — this points at a different, more systemic cause: html itself had
   no background set (fully transparent), so it fell through to the
   browser's own default canvas colour (white) underneath. On trackpad-
   driven "elastic"/rubber-band overscroll — standard behaviour on macOS in
   both Safari and Chrome, and easy to trigger with an ordinary scroll
   gesture — the viewport briefly scrolls past the document's real content,
   exposing whatever html's own background is; a transparent html meant
   that momentary reveal was stark white against the page's own offwhite,
   reading as the whole background "lightening" while scrolling. Matching
   html's background to body's means that overscroll bounce, if it happens,
   reveals the same offwhite instead of a mismatched white. */
html { background: var(--color-offwhite); }

/* Deliberately NOT html{scroll-behavior:smooth}: GSAP's own docs flag this
   as a known conflict with ScrollTrigger's scrub-linked animations (the
   scroll parallax in js/main.js) — Firefox in particular can end up not
   updating the scrubbed transform at all while native smooth-scroll is
   easing. Nothing else on the site relies on smooth in-page anchor
   scrolling (the nav is an off-canvas overlay, not same-page anchor
   jumps), so there's no loss in removing it. */

/* Default theme is light (matches the homepage's #E8E8E8 / #0F0F0F pairing).
   Individual bands — the photo hero, the final CTA, the site footer's dark
   variant — override background/colour locally rather than the whole page.

   Section backgrounds: sections that are plain offwhite (Explore, Featured,
   Why-Work-with-us, etc.) deliberately do NOT paint their own
   background-color — they rely on this one body fill showing through underneath
   instead of each repainting the identical rgb(240,238,233). Two adjacent,
   separately-painted layers of the "same" colour is exactly the kind of
   thing that can render as a faint seam where they meet (composited on
   their own layer — e.g. one has overflow:hidden to clip a bleeding child
   — a layer can pick up very slightly different rounding than the page
   background beside it, especially on WebKit/Safari); it did on the
   "why work with us" section (previously .bg-offwhite + overflow-hidden,
   to clip the 110vw watermark bleed — see .watermark below), and the fix was
   to stop giving it its own fill at all.

   The full-bleed clipping duty this section used to describe living here
   too (body's own overflow-x:hidden) has since moved to html alone — see
   the long comment on html's overflow-x:hidden above for why: having it on
   both html AND body broke position:sticky sitewide (reported:
   "'Recently brought to life' doesn't scroll with the page anymore"). html
   sits above body in the tree, so its clip still covers the watermark and
   every other full-bleed element exactly as before — nothing about that
   clipping actually needed a declaration on body specifically, that was
   just where it happened to be added originally. Do not re-add
   overflow-x:hidden here without re-reading that comment first. */
body {
  margin: 0;
  background: var(--color-offwhite);
  color: var(--color-black);
  font-family: var(--font-body);
  -webkit-font-smoothing: antialiased;
}

img, svg { max-width: 100%; display: block; }

/* Belt-and-braces alongside the native [hidden] behaviour: an element the
   portfolio filter (js/main.js initPortfolioFilter) hides also carries a
   Tailwind layout class (.flex/.grid/etc, e.g. on a filtered-out project
   card's <a>) with the same (0,0,1,0) specificity as the UA stylesheet's
   own [hidden]{display:none} — as same-specificity author CSS loaded after
   the UA sheet, Tailwind's class was winning the tie and the "hidden" card
   stayed visually laid out. This rule out-specifies both. */
[hidden] { display: none !important; }

a { color: inherit; text-decoration: none; }

/* Text-selection highlight — brand red everywhere, on both dark and light
   sections, rather than each browser's default blue. */
::selection { background: var(--color-berry); color: var(--color-white); }
::-moz-selection { background: var(--color-berry); color: var(--color-white); }

/* --------------------------------------------------------------------------
   3. TYPE SCALE
   Values pulled directly from the Figma "Font styles" frame. Figma only
   defines H1–H4 (no separate H5 style existed in that frame) — .h5 below is
   a proposed in-between size, flagged for approval rather than guessed
   silently into the rest of the system.

   Two Bebas Neue sizes now split what used to be a single H1: .hero-title
   (80px) is reserved for the one true hero headline on the homepage
   ("Bringing Your Unbuilt Environment to Life"); .h1 (56px — the size
   originally used just for the "Explore" heading) is the size for every
   other headline site-wide — section headings, page hero-band titles, CTA
   headlines. This keeps the hierarchy to the same number of distinct
   styles rather than adding a new one.
   -------------------------------------------------------------------------- */

.hero-title {
  font-family: var(--font-display);
  font-weight: 400; /* Bebas Neue ships one weight on Google Fonts */
  font-size: clamp(40px, 7vw, 80px); /* 80px is the Figma spec at desktop container width; scales down so it doesn't overflow on mobile */
  line-height: 0.9;
  letter-spacing: -1.4px;
  margin: 0;
}
/* Narrower wrap box on desktop only, per request ("move environment down...
   reduce the width of the container" rather than a manual line break) —
   forces the natural wrap to break after "Unbuilt" instead of after
   "Environment", without touching the shared max-w-4xl column that also
   holds the pill/copy/button below it. */
@media (min-width: 1143px) { .hero-title { line-height: 72px; max-width: 700px; margin-inline: auto; } }
/* Mobile size bump, per request ("sitewide on mobile - the h1 copy should be
   larger than the h2 ... The only exception being 'visualising tomorrow'
   which should be larger ~48px") — the clamp above already bottoms out at
   40px on narrow viewports (7vw only overtakes that floor above ~571px), so
   this just raises that floor to the requested 48px specifically below the
   site's usual 640px mobile cutoff, leaving the vw-scaling behaviour between
   640px and .hero-title's own 1143px breakpoint untouched. */
@media (max-width: 640px) { .hero-title { font-size: 48px; } }

.h1, h1 {
  font-family: var(--font-display);
  font-weight: 400; /* Bebas Neue ships one weight on Google Fonts */
  font-size: clamp(34px, 5vw, 56px);
  line-height: 0.85;
  letter-spacing: -1.28px;
  margin: 0;
}
@media (min-width: 1143px) { .h1, h1 { line-height: 46px; } }
/* Mobile size bump, per request ("the h1 copy should be larger than the h2.
   can we make it about 40px on mobile") — the clamp above already floors at
   34px on narrow viewports (5vw only overtakes that floor above ~680px),
   already larger than .h2's fixed 26px, but the request asks for a further
   bump to ~40px specifically below 640px. .hero--plain .h1 (page hero
   titles) already resolves to 40px here via its own clamp(40px, 7vw, 80px)
   override further down this file, so this brings the rest of the site's
   .h1 headings in line with that same 40px, not just "bigger than .h2".
   .hero-title (the homepage's "Visualising tomorrow") gets its own, taller
   48px mobile floor just above instead, per the same request's stated
   exception. */
@media (max-width: 640px) { .h1, h1 { font-size: 40px; } }

.h2, h2 {
  font-family: var(--font-heading);
  font-weight: 500;
  font-size: 26px;
  line-height: 32px;
  letter-spacing: -0.13px;
  margin: 0;
}

.h3, h3 {
  font-family: var(--font-heading);
  font-weight: 500;
  font-size: 18px;
  line-height: 24px;
  letter-spacing: 0;
  margin: 0;
}

.h4, h4 {
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 12px;
  line-height: 18px;
  letter-spacing: 1.8px;
  text-transform: uppercase;
  margin: 0;
}

/* Confirmed against the homepage pull (used for testimonial author names) —
   replaces the earlier placeholder. */
.h5, h5 {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 14px;
  line-height: 14px;
  letter-spacing: 1.26px;
  text-transform: uppercase;
  margin: 0;
}

.large-copy {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 18px;
  line-height: 26px;
}

.regular-copy {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 14px;
  line-height: 21px;
  letter-spacing: 0.14px;
}

.button-copy {
  font-family: var(--font-body);
  font-weight: 800;
  font-size: 14px;
  line-height: normal;
  letter-spacing: 1.4px;
  text-transform: uppercase;
}

/* --------------------------------------------------------------------------
   4. BUTTON COMPONENTS
   Source: Figma "buttons" frame (node 2604:655). Combine with .button-copy
   for the text styling — these classes only add colour/shape/spacing.
   -------------------------------------------------------------------------- */

/* Primary — solid berry pill, lime text. e.g. "Get a quote" */
.btn-primary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 20px 32px;
  border-radius: 0;
  background: var(--color-berry);
  color: var(--color-white); /* was lime — changed to white by request for contrast/legibility on the berry fill */
  /* Transparent (not "none") so the border-box takes up the same space at
     rest as it does on hover, matching the .btn-secondary pattern — only
     the colour changes on hover, nothing shifts. */
  border: 1px solid transparent;
  /* Real (zero-offset, zero-alpha) rest value rather than the default
     `none` — see the shared box-shadow note in "4b. BUTTON HOVER" below for
     why this matters: transitioning FROM `none` can't be interpolated at
     all, so without this the hover shadow used to snap straight to its
     full 6px offset instead of growing in step with the lift. */
  box-shadow: 0 0 0 0 transparent;
  cursor: pointer;
  /* 1.05s / 0.35s asymmetric pair — matches .service-card's own hover
     mechanics exactly (see "4b. BUTTON HOVER" below), per request: this
     rest-state rule only governs the restore-to-rest direction (leaving
     hover), the faster 0.35s entering-hover speed lives on the :hover rule
     itself. */
  transition: transform 1.05s ease, box-shadow 1.05s ease, border-color 1.05s ease, background-color 1.05s ease, color 1.05s ease;
}
/* Hover — see the shared "4b. BUTTON HOVER" block below. */

/* .btn-primary--outline-dark and .btn-primary--outline-grey (the two
   modifier variants that used to give "Get in touch" and services.html's
   "Get a quote" their own rest/hover colours) have been removed per request
   ("can all primary ctas follow the same styling (hover and default) as
   'get a quote'") — every .btn-primary now shares one look: solid berry
   fill / white copy at rest (the base rule above), white fill / berry copy
   on hover (see the single .btn-primary:hover rule in "4b" below). The two
   modifier classes have also been removed from index.html's "Get in touch"
   and services.html's "Get a quote" markup. */

/* .btn-primary--white — a further variant added per request ("services
   footer - get a quote button - update so it's another version of the
   primary button on the homepage - so white bg berry text default, on
   hover the chevron appears in berry"). This directly resolves a deviation
   flagged when the outline modifiers above were removed: services.html's
   "Get a quote" sits on a solid berry section background (.hero--berry-bg),
   so plain .btn-primary's own berry-fill rest state had almost no visible
   edge against it until hovered. Rather than a one-off fix, this is its own
   reusable modifier — stacked alongside .btn-primary (e.g. class="btn-
   primary btn-primary--white button-copy"), same markup/chevron/transition
   otherwise.
   Only background-color/color are overridden here, at equal (single-class)
   specificity to the base rule above, so source order is what makes this
   win at rest — this block must stay AFTER the plain .btn-primary rule.
   Deliberately does NOT touch .btn-primary:hover: that shared hover rule
   already sets background-color:white / color:berry, i.e. exactly these
   same rest colours, so hovering a --white button causes no visible colour
   flip at all — only the chevron reveal fires (.btn-primary:hover .btn-
   quote-chevron is keyed off the shared base class, so it still applies
   unchanged), which is exactly "on hover the chevron appears in berry"
   with nothing else moving. Border/shadow/transform stay untouched too
   (transparent border, no shadow/lift, same as every other .btn-primary). */
.btn-primary--white {
  background-color: var(--color-white);
  color: var(--color-berry);
}

/* Secondary — berry text + label, followed by a bare berry caret (no
   background chip around it any more — that container was removed by
   request, along with swapping the icon itself from the lime chevron.svg,
   which needed the berry chip behind it for contrast, to a plain icon that
   reads directly against the page — chevron_berry.svg originally,
   caret-right.svg now). e.g. "View services ›" */
.btn-secondary {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  /* Solid berry fill + berry border at rest now — this used to be the
     :active/click state; the three states were rotated one step by
     request: old click state -> new rest, old hover state -> new click
     state, new hover state -> old click state's look again but with its
     white copy/icon dimmed to 0.65 opacity (see "4b. BUTTON HOVER" below
     for the :hover/:active rules this now pairs with). */
  background: var(--color-berry);
  border: 1px solid var(--color-berry);
  /* Padding + matching negative margin gives that hover border/shadow room
     around the label+icon without shifting the button's own position in
     its layout. */
  padding: 8px 12px;
  margin: -8px -12px;
  border-radius: 0;
  color: var(--color-white);
  cursor: pointer;
  /* Real (zero-offset, zero-alpha) rest value — see the box-shadow note on
     .btn-primary above / "4b. BUTTON HOVER" below for why this is needed
     for the hover shadow to grow in step with the lift instead of snapping
     in at full size. */
  box-shadow: 0 0 0 0 transparent;
  /* Back to the same asymmetric-duration pair .testimonial-card/.service-
     card use, per request ("follow the timing of the testimonial hover
     interactions") — this slower 1.05s rest-side duration governs
     restoring to rest; its own faster 0.35s entering-hover duration is
     restated on :hover below. */
  transition: transform 1.05s ease, box-shadow 1.05s ease, border-color 1.05s ease, background-color 1.05s ease, color 1.05s ease;
}
.btn-secondary .btn-secondary-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.btn-secondary .btn-secondary-icon img {
  /* caret-right.svg (square viewBox, per-request replacement for the old
     chevron.svg/chevron_berry.svg icon set) reads noticeably smaller at a
     given box size than the old chevron did — its own glyph sits inset
     within a square canvas rather than filling a slim rectangle edge to
     edge — so the box has grown from the old 7x13 to keep the same visual
     weight next to the button label. */
  width: 18px;
  height: 18px;
  /* Chevron is an external SVG (flat white fill) rather than inline markup,
     so a plain CSS color/fill can't reach into it — forced white with the
     same brightness(0) + invert(1) filter recipe used sitewide for
     recolouring external icon SVGs (see .site-header__mark/.site-nav__
     social elsewhere in this file), then dimmed to 0.65 opacity to match
     .portfolio-filter__count's own number-badge look at rest, per the
     original request this rotated state design is built on. */
  filter: brightness(0) invert(1);
  opacity: 0.65;
}

/* .btn-secondary--flush-left removed — it only ever corrected a border/
   copy alignment quirk for "View services"/"View all", both since moved
   off .btn-secondary onto .btn-tertiary (which has no border of its own to
   need this fix) per request ("find all buttons that used the secondary
   styling and update to this new styling"). .btn-secondary itself is left
   in place — still used by the style guide's own reference swatch for
   this tier, even though nothing on the live site uses it any more. */

/* --------------------------------------------------------------------------
   4c. TERTIARY BUTTON — bare text + icon link
   Third, lightest-weight tier alongside .btn-primary (solid fill) and
   .btn-secondary (solid fill, berry/black states): no background, border or
   shadow at all, just the label and a caret. Berry copy at rest, per
   request, flipping to black on hover — the same black now used for
   .btn-secondary's own hover border/shadow — with an underline draw-in on
   the label (the same "draw-in from the left" motif the main nav menu's
   own links used to use before their hover effect changed — see
   .service-lede__item::after's own comment for the fuller history — scaled
   down here) as the
   hover cue, since there's no fill/border to carry that job here. The caret
   icon (.btn-tertiary-icon below) follows the copy colour at both rest and
   hover.
   Used for every button on a project detail page (Back to portfolio /
   Previous / All / Next) and the portfolio page's "Projects" quick-links
   (with the --reveal-icon modifier below) — .btn-primary's "Get a quote"
   CTA is untouched, staying the site's one high-emphasis fill button. */
.btn-tertiary {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--color-berry);
  cursor: pointer;
  transition: color 0.2s ease;
}
.btn-tertiary:hover {
  color: var(--color-black);
}
.btn-tertiary-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.btn-tertiary-icon img {
  width: 16px;
  height: 16px;
  /* Source caret/squares SVGs are flat white — recoloured with the same
     brightness(0)-first filter recipe used sitewide for external icon
     SVGs, numerically solved to land on exact --color-berry (see the
     .btn-secondary hover chevron's own comment elsewhere in this file for
     the method/precedent). Hover needs no separate solving — brightness(0)
     alone already yields solid black, matching --color-black exactly. */
  filter: brightness(0) invert(11.6%) sepia(88.4%) saturate(593.5%) hue-rotate(291.5deg) brightness(235.5%) contrast(129.6%);
  transition: filter 0.2s ease;
}
.btn-tertiary:hover .btn-tertiary-icon img {
  filter: brightness(0);
}
/* Underline — the same "draws in from the left" motif the main nav menu's
   own links used to use (see the .btn-tertiary comment above), scaled to
   this component's smaller type. Lives on its own span (not the
   whole flex link) so its width always matches the label text specifically,
   regardless of the icon beside it or which side it sits on. */
.btn-tertiary-text {
  position: relative;
  display: inline-block;
}
.btn-tertiary-text::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -3px;
  width: 100%;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.btn-tertiary:hover .btn-tertiary-text::after {
  transform: scaleX(1);
}
/* --berry-hover — homepage "About us" CTA only, per request ("just berry
   text and a berry chevron... on hover I'd like the underline to be drawn
   in berry... no background shadow or raise on hover"). .btn-tertiary
   already has no fill/border/shadow/lift at rest OR on hover (it's the
   site's bare text-link tier), so the only thing to change is cancelling
   its own hover -> black flip, keeping copy AND chevron berry throughout.
   The underline itself needs no separate override: .btn-tertiary-text::after
   already paints in `currentColor`, so it inherits whatever colour .btn-
   tertiary's own hover state leaves text in — berry once the flip below is
   cancelled — rather than needing its own explicit berry value.
   Compound selectors (.btn-tertiary + this modifier + the pseudo-class)
   deliberately outrank .btn-tertiary:hover's own single-class specificity,
   so this wins regardless of source order. */
.btn-tertiary.btn-tertiary--berry-hover:hover {
  color: var(--color-berry);
}
.btn-tertiary.btn-tertiary--berry-hover:hover .btn-tertiary-icon img {
  filter: brightness(0) invert(11.6%) sepia(88.4%) saturate(593.5%) hue-rotate(291.5deg) brightness(235.5%) contrast(129.6%);
}

/* --reveal-icon — the portfolio sidebar's own quick-link treatment (see
   17b. PORTFOLIO SIDEBAR below): the icon stays collapsed to zero width/
   opacity at rest, expanding in from the left on hover, rather than sitting
   permanently visible the way the project-nav buttons' icons do — this
   suits a repeated 7-item list (only the hovered link's arrow pops in) far
   better than it would a one-off Back/Previous/All/Next control, where a
   persistent icon is the clearer affordance. gap moves from the parent
   (0 here) onto the icon's own margin-right so no gap shows while the icon
   is invisible. */
.btn-tertiary--reveal-icon {
  gap: 0;
}
.btn-tertiary--reveal-icon .btn-tertiary-icon {
  width: 0;
  height: 16px;
  opacity: 0;
  overflow: hidden;
  margin-right: 0;
  transition: width 0.25s ease, opacity 0.25s ease, margin-right 0.25s ease;
}
.btn-tertiary--reveal-icon:hover .btn-tertiary-icon {
  width: 16px;
  opacity: 1;
  margin-right: 8px;
}

/* --------------------------------------------------------------------------
   4d. BREADCRUMB — project detail pages
   Replaces the old plain "‹ Portfolio" back-link at the top of each project
   page, per request ("remove the previous button, add a breadcrumb
   instead"). "Portfolio" is a real .btn-tertiary link (berry, black on
   hover, underline draw-in — no icon needed here, the "/" separators
   already read as a trail); the current page's own name is a plain
   non-interactive final crumb in a quieter grey, matching convention
   (you're already here, there's nothing to click). */
.breadcrumb {
  display: flex;
  align-items: center;
  gap: 8px;
  /* No width/max-width of its own (just the plain mb-6 bottom-margin
     utility in the HTML) — as a block-level flex element it already runs
     the full width of its .container, same as .project-card__media--lead
     directly beneath it, so the two stay left-edge-aligned automatically. */
}
.breadcrumb__sep {
  color: var(--color-grey-light);
}
.breadcrumb__current {
  color: var(--color-grey);
}

/* Project detail pages' first section (breadcrumb + title + lead media).
   Used to compensate for a persistent FIXED header sitting on top of this
   section (150px = header height + a clear 50px gap below it); now that
   these pages use the same static-then-floating .hero-frame header pattern
   as the homepage (see the .hero-frame notes above), the header is back in
   normal document flow and no longer overlaps this section at all — its
   own .hero-frame margin-bottom (32px) already clears it. This just adds
   the remaining 18px on top of that, so the breadcrumb still sits the same
   ~50px below the header per the original request. */
.project-hero-section {
  padding-top: 18px;
}

/* Tightens the gap between a page's H1 (.project-hero-section above) and
   the intro copy section directly below it, per request ("tighten up the
   space between heading and intro copy") — raised on the about page, but
   applied to both it and the services page since they share this exact
   H1-then-intro-section pattern (about.html adopted it from services.html
   in the same request — see .project-hero-section's own comment above and
   about.html's header markup). Without this, the intro section's own
   80px/48px .section padding-top (see ".section" below) was the entire gap
   below the H1 (.project-hero-section carries no bottom padding of its
   own, per that rule's comment) — a bigger gap than intended now the two
   sit this close together with no image/hero band between them any more.
   Two-class selector (.section.section--tight-top) keeps its specificity
   above the plain .section rule regardless of source order, so only
   padding-top is overridden — bottom padding and the mobile padding-block
   value both still come from .section untouched. */
.section.section--tight-top {
  padding-top: 32px;
}
@media (max-width: 768px) {
  .section.section--tight-top {
    padding-top: 24px;
  }
}

/* --------------------------------------------------------------------------
   4b. BUTTON HOVER — raise + berry wireframe echo
   Replaces the old JS outline-draw-then-fill-lime treatment (used to live
   in js/main.js as initHoverReveal — an SVG rectangle stroking in around
   the element on mouseenter, then filling it lime) with the same hover
   language as .testimonial-card: a slight lift plus a duplicate berry-
   outlined "shadow" offset directly beneath the element (straight down,
   no sideways shift), echoing its own shape rather than a soft blur.
   Purely-vertical offset is deliberate — it's meant to read as the button
   rising straight up off a matching base shape still sitting on the page
   (growing out of it), not as sliding away from a shadow cast down-right.
   Pure CSS now — no JS/SVG involved.
   The lift (transform) is shared by every button/nav control site-wide.
   .btn-secondary + the header/nav Menu/Close controls also pick up the
   berry border/shadow (matching .portfolio-filter__btn's own hover state).
   .btn-secondary now also fills white on hover, off its own grey-light
   rest border, per request — see its own :hover rule just below (the
   header/nav Menu/Close controls keep no background-colour change of
   their own, staying transparent at rest and on hover, a fill having been
   tried there previously and reverted by request).
   .btn-primary is already a solid berry fill at rest, so instead of just
   echoing berry again it swaps itself — fill flips to off-white, its lime
   text flips to berry, and the outline/shadow are berry — reading as an
   inverse of its own rest state rather than a repeat of it.

   .btn-secondary never appears on a dark/image background site-wide, so it
   stays berry-only. Neither .site-header__menu-btn nor .site-nav__close
   share this lift/shadow treatment any more — per request ("menu button -
   remove the styling on this (hover and default)", and later "close in the
   nav menu to follow the same styling as 'menu' button. Also, match the
   same hover effects") both now use the same much simpler background-
   colour-only hover as each other, defined together with the rest of
   "4d. HEADER" further down instead of here. The old lime "dark-mode"
   accent that used to live here for .site-nav__close (and, before that, for
   .site-header__menu-btn too) has been removed entirely, per a further
   request ("remove all lime use in the main nav menu"). */
.btn-primary:hover,
.btn-secondary:hover {
  /* -8px lift / 5px shadow offset, and the 0.35s entering-hover speed below
     — all three lifted directly from .service-card:hover, per request
     ("keep the button styling/colours, use the same hover animation as the
     cards"). Was -4px / 6px / (a symmetric 0.3s shared with the rest rule,
     no separate hover-in speed) before. */
  transform: translateY(-8px);
  box-shadow: 0 5px 0 0 var(--color-berry);
  transition: transform 0.35s ease, box-shadow 0.35s ease, border-color 0.35s ease, background-color 0.35s ease, color 0.35s ease;
}
/* Hover — border and shadow flip to black, fill goes to plain berry, copy
   stays lime, and the chevron follows the copy to lime too (see
   .btn-secondary-icon img below — its own rest state stays dimmed white,
   only hover goes lime). */
.btn-secondary:hover {
  /* Border + shadow now black rather than berry, per request — background
     goes to plain --color-berry too (was the lighter --color-berry-
     highlight tint, a derived colour that existed only for this fill and
     has now been removed from :root along with it). */
  border-color: var(--color-black);
  background-color: var(--color-berry);
  color: var(--color-lime);
  /* Slight raise added back, per request — same halved amount as
     .testimonial-card:hover (-4px lift / 2.5px shadow offset, see that
     rule elsewhere in this file), rather than the fuller -8px/5px lift
     .btn-primary still uses. */
  transform: translateY(-4px);
  box-shadow: 0 2.5px 0 0 var(--color-black);
  /* No transition restated here — falls through to the shared 0.35s
     entering-hover speed set on the .btn-primary/.btn-secondary/
     .site-header__menu-btn/.site-nav__close block above, which now
     matches .testimonial-card:hover's own 0.35s entering speed, per
     request ("follow the timing of the testimonial hover interactions"). */
}
/* .btn-secondary--grey-hover (About us's own grey-matched hover, matched
   to .testimonial-card's hover colour) removed — About us has since moved
   to .btn-tertiary entirely (see "4c. TERTIARY BUTTON" below for its own
   --berry-hover modifier), which has no border/shadow of its own for a
   hover colour to apply to. */
.btn-secondary:hover .btn-secondary-icon img {
  /* Chevron follows the copy to lime on hover, per request. Same
     brightness(0)-first approach as the rest-state white filter above (the
     source SVG is a flat berry fill, not inline markup, so plain CSS
     color/fill can't reach it) — brightness(0) flattens it to a black
     silhouette regardless of its original colour, then the
     invert/sepia/saturate/hue-rotate/brightness/contrast chain recolours
     that black to lime. Numerically solved (not guessed) by sampling
     candidate filters' rendered output against the exact target RGB, same
     method as .site-nav__social's own tuned berry filter elsewhere in this
     file — converges to an exact (236, 255, 91) match, i.e. --color-lime
     itself. Full opacity now (was 0.65) — hover is the more prominent
     state, so the chevron reads at full strength rather than staying
     dimmed. */
  filter: brightness(0) invert(77.7%) sepia(70.9%) saturate(1385.6%) hue-rotate(55.2deg) brightness(269.2%) contrast(206.7%);
  opacity: 1;
}
/* Click/press feedback — this is the button's PREVIOUS hover look, per
   request ("make the old hover state the new clicked state"): white fill,
   berry border/copy/chevron. Comes after .btn-secondary:hover above so it
   wins the cascade on the same element while a click lands mid-hover
   (equal specificity, later source order — same tie-break used throughout
   this file). No transition of its own needed: .btn-secondary's base rule
   and :hover above both already set transition:none, so this swap snaps
   instantly too. */
.btn-secondary:active {
  border-color: var(--color-berry);
  background-color: var(--color-white);
  color: var(--color-berry);
}
.btn-secondary:active .btn-secondary-icon img {
  /* Berry, fully opaque, no dimming — matching the old hover state's
     chevron. Needs an explicit filter now (was `none`): that relied on
     chevron_berry.svg's own pre-tinted berry fill, but caret-right.svg
     (its per-request replacement, used everywhere .btn-secondary-icon
     appears) is a flat white source SVG, so reaching berry here takes the
     same numerically-solved brightness(0)-first recipe as the hover
     lime filter above. */
  filter: brightness(0) invert(11.6%) sepia(88.4%) saturate(593.5%) hue-rotate(291.5deg) brightness(235.5%) contrast(129.6%);
  opacity: 1;
}
/* The ONE .btn-primary hover rule — every primary CTA site-wide now follows
   the same styling on hover as the homepage hero's "Get a quote" button,
   per request ("can all primary ctas follow the same styling (hover and
   default) as 'get a quote'"). Used to be two competing rules: a plain
   .btn-primary:hover (white fill, berry text, berry border, kept the shared
   lift/shadow) that only "Get in touch" and services.html's "Get a quote"
   actually reached, plus this higher-specificity :not(.btn-primary--outline-
   dark):not(.btn-primary--outline-grey) version — homepage "Get a quote"
   only — with transparent border, no shadow/lift, and the chevron reveal.
   Now that both modifier classes have been removed sitewide, this single
   rule is what every .btn-primary reaches:
   - background-color goes white (from berry at rest), colour goes berry
     (from white at rest) — the fill/text pairing flips.
   - border-color stays transparent — no visible border in either state.
   - box-shadow/transform are pinned to zero/none, cancelling the shared
     lift/shadow block above — "no drop shadow or raising effects", per the
     original request this behaviour was built for.
   The chevron reveal below (.btn-quote-chevron) started out as markup only
   the homepage hero's "Get a quote" button carried, so it had no effect on
   any other .btn-primary; it's since been added to every other footer/CTA-
   band .btn-primary too (about/case-studies/homepage final CTA/services),
   per request ("can all primary buttons in the footer sections 'get a
   quote' etc have the chevron added on hover") — see each page's own CTA
   section markup. */
.btn-primary:hover {
  background-color: var(--color-white);
  color: var(--color-berry);
  border-color: transparent;
  box-shadow: 0 0 0 0 transparent;
  transform: none;
}
/* The chevron chip itself — collapsed to zero width/opacity at rest,
   revealed on hover, same technique (and the same 0.25s width/opacity/
   margin-left timing) as .service-card-caption__chevron above, "copying"
   that reveal per request. Two deliberate differences from that one,
   both per request: this chevron's own wrapper carries a WHITE background
   (that one has none, relying on its caption chip's own berry hover-fill
   for contrast — this button's fill never changes, so the chevron needs
   its own contrasting backdrop to "pop out" of the berry button), and the
   icon itself is recoloured red/berry instead of white (it now sits on
   white, not berry, so it needs the opposite of that chevron's own white-
   on-berry treatment) — the same numerically-solved brightness(0)-first
   filter recipe used everywhere else in this file for a flat white source
   SVG recoloured to berry. padding is what gives this one visible chip proportions instead
   of just a bare icon (unlike the plain-icon version above) — and, unlike
   that first attempt's assumption, it DOES need its own transition: with
   the global box-sizing:border-box reset, an explicit `width` can never be
   coerced smaller than the element's own horizontal padding (the browser
   floors the content box at 0 and the padding still has to fit inside
   `width`), so a width:0 rest state with a constant 10px/side padding was
   actually rendering at a floored 20px (2x10px), not 0 — verified via
   Playwright (computed width stayed 20px even with min-width:0 added).
   Padding now animates 0 -> 10px alongside width 0 -> 36px so the two
   reach zero together, with plain `overflow:hidden` still doing the same
   job of clipping the icon mid-transition. */
.btn-quote-chevron {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 0;
  min-width: 0;
  /* 16px (just the icon's own size), was 32px — that taller value made this
     invisible-at-rest chevron the tallest flex item in the button's row
     (button copy's own natural line height is well under 32px), which
     forced the WHOLE button noticeably taller than every other .btn-primary
     even though nothing about the chevron was visible yet. Reported as
     "the button is now larger for some reason" — fixed by matching the
     icon's real 16px size instead of an arbitrary bigger box. */
  height: 16px;
  padding: 0;
  opacity: 0;
  overflow: hidden;
  margin-left: 0;
  flex-shrink: 0;
  /* White background REMOVED per request ("there's 2 white backgrounds
     that are out of sync, can the white background in the chevron
     container be removed so it doesn't clash") — now that the whole button
     also turns white on hover (see the .btn-primary:hover:not(...):not(...)
     rule above), this chip painting its own separate white layer on top,
     with its own slightly different transition timing, was visibly
     drifting out of step with the button's own fill as both animated in.
     Leaving this transparent lets the button's single white fill show
     through underneath instead of two overlapping whites. */
  transition: width 0.25s ease, padding 0.25s ease, opacity 0.25s ease, margin-left 0.25s ease;
}
.btn-primary:hover .btn-quote-chevron {
  /* 16px icon + 10px left padding, 0 right padding = 26px (was 28, then 36
     originally) — right-side padding reduced by a further ~4px per a later
     request ("remove about 4px more padding from the right of the chevron
     on the hover state of the 'get a quote' button"), on top of the 8px
     already taken off before. padding-right can't go negative to remove
     any more than the 2px it already had, so the last bit comes from
     margin-right instead: -2px pulls the icon 2px past its own box edge,
     bringing the total reduction on this side to 4px (2px removed from
     padding + 2px pulled in via margin) — tightening the gap to the
     button's own right edge further without touching the button's own
     padding, which would also affect the label-text side. Left padding
     (between the label text and the icon) is untouched. */
  width: 26px;
  padding-left: 10px;
  padding-right: 0;
  margin-right: -2px;
  opacity: 1;
  margin-left: 10px;
}
.btn-quote-chevron img {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  /* Flat white source SVG recoloured to berry — same numerically-solved
     recipe used throughout this file for this exact conversion. */
  filter: brightness(0) invert(11.6%) sepia(88.4%) saturate(593.5%) hue-rotate(291.5deg) brightness(235.5%) contrast(129.6%);
}

/* Contact page "Send message" button icon — same collapsed-width/opacity
   hover-reveal mechanic as .btn-quote-chevron just above, copied onto its
   own class rather than reused directly since it's a different icon (a
   paper plane, not a caret) sitting on a different button, per request
   ("contact page 'Send message' button - instead of the chevron popping up
   on hover can we have the 'paper-plane-tilt.svg' in the svg folder"). Note
   this button had no reveal-icon of any kind before this request — the
   plain .btn-primary:hover rule it already picked up (background/text
   colour flip only, see above) was the only "hover effect" it had. */
.btn-send-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 0;
  min-width: 0;
  height: 16px;
  padding: 0;
  opacity: 0;
  overflow: hidden;
  margin-left: 0;
  flex-shrink: 0;
  transition: width 0.25s ease, padding 0.25s ease, opacity 0.25s ease, margin-left 0.25s ease;
}
.btn-primary:hover .btn-send-icon {
  width: 26px;
  padding-left: 10px;
  padding-right: 0;
  margin-right: -2px;
  opacity: 1;
  margin-left: 10px;
}
.btn-send-icon img {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  /* paper-plane-tilt.svg's own source fill is a flat black (#000000), not
     white like caret-right.svg — doesn't matter here, since brightness(0)
     flattens either starting colour to black first before the rest of the
     chain recolours it, so the exact same numerically-solved berry recipe
     used everywhere else in this file still lands on the same target
     colour regardless of the source SVG's own fill. */
  filter: brightness(0) invert(11.6%) sepia(88.4%) saturate(593.5%) hue-rotate(291.5deg) brightness(235.5%) contrast(129.6%);
}
/* The old lime "dark-mode" .site-nav__close:hover override that used to sit
   here (box-shadow/border-color lime) has been removed entirely — Close no
   longer has a border or shadow at all, let alone a lime one, now that it
   matches .site-header__menu-btn's own simpler background-only hover (see
   "4d. HEADER" further down, and .site-nav__close's own base rule near
   "6. SITE HEADER / NAV" above) per request ("close in the nav menu to
   follow the same styling as 'menu' button. Also, match the same hover
   effects" / "remove all lime use in the main nav menu"). */
/* .btn-primary--outline-dark and .btn-primary--outline-grey's own hover
   overrides have been removed along with the rest-state rules above — every
   .btn-primary now shares the one hover rule above, no modifier-specific
   exceptions left. */

/* --------------------------------------------------------------------------
   5. LAYOUT HELPERS
   -------------------------------------------------------------------------- */
/* .container itself is NOT hand-rolled here — "container" is also
   Tailwind's own built-in utility name, and this site's js/tailwind-config.js
   configures that utility directly (center:true, padding:24px, and its own
   responsive screens tiers — sm:640/md:768/lg:1024/xl:1280/2xl:1440). A
   hand-written `.container{...}` rule used to live here too, competing with
   Tailwind's generated one for the exact same classname — the two disagreed
   (this one applied one flat max-width everywhere; Tailwind's own applies a
   different max-width at each breakpoint) and which one actually won varied
   with cascade/load-order in a way that showed up confusingly in devtools.
   Removed so there's a single source of truth: the config file above. */

/* .section-divider (the two full-bleed 1px white lines around the homepage
   logo marquee) was removed by request — class deleted rather than left
   dead since nothing else used it. */

.section {
  padding-block: 80px;
}
@media (max-width: 768px) {
  .section { padding-block: 48px; }
}

/* Plain white-background modifier for a .container.section band — page bg
   is --color-offwhite (see body{} above) everywhere else, so this is a
   deliberate one-off lift off that background rather than the default. First
   (only, so far) use: each project page's "Get in touch"/"Next case study"
   row, per request ("Make these 2 cards sit on a section with a white
   background") — see project-*.html's own .project-adjacent-section. Generic
   name (not scoped to that one use) so it can be reused anywhere else a
   .container.section band needs the same lift. */
.section--white-bg {
  background: var(--color-white);
}

/* Services page only — each of the 5 service sections' own white card, per
   request ("for the white container envelope - can it have separate ones
   for each section"). Was briefly one single shared .services-white-envelope
   band (built on .section/.section--white-bg above, per an earlier "can the
   5 sections be in a white container that envelopes them all" request) —
   replaced by this simpler per-section wrapper instead, now that each
   .service-detail gets its own box rather than all 5 sharing one. 20px
   padding per the same request ("Can it have 20px padding"). */
.service-detail-card {
  background: var(--color-white);
  padding: 20px;
}
/* The video header bleeds to the top/left/right edges of its own
   .service-detail-card, per the rest of that same request ("but i want the
   video headers to have 0px padding left right and top") — negative
   margins cancel the card's 20px padding on those 3 sides. The bottom edge
   is left alone (margin-bottom:0), so the normal 24px gap down to the copy
   below (.service-detail's own flex gap) is untouched.
   width explicitly set to calc(100% + 40px), not left as auto — .media-
   feature is a flex item of .service-detail (display:flex), and a flex
   item's default stretch sizing computes a FIXED width equal to its
   container's own content width rather than a fluid auto value that grows
   to fill whatever negative margins ask for. With plain width:auto that
   fixed width stayed exactly as wide as it was before the negative
   margins were added, so the whole band just shifted sideways instead of
   also widening — overflowing past the card on one side while falling
   short of the other, per the bug report ("the videos aren't contained in
   the white container... reduce the height if needed"). The explicit
   calc() adds back the two 20px paddings this element is bleeding through,
   so it now lands flush with the card's real edges on all 3 sides with no
   overflow; height (via aspect-ratio) adjusts to match automatically. */
.service-detail-card .media-feature {
  width: calc(100% + 40px);
  margin: -20px -20px 0 -20px;
}

/* MAIN TITLE section (the "Your trusted CGI partner" pill + h1 + copy,
   right under the header) used to carry its own extra padding-top on top
   of .section's own padding-block (180px desktop / 40px mobile, tuned
   across a couple of earlier requests) — removed per a later request
   ("reduce padding above and below this section to make it consistent with
   the rest of the webpage"), leaving it with no rule of its own at all so
   it just inherited .section's plain padding-block (80px desktop / 48px
   mobile) like every other section.

   padding-top added back per a further request ("add padding to above the
   'visualising tomorrow' to make it match the spacing below it and the
   services section") — the full-bleed 100vh video hero directly above this
   section (.hero--photo) contributes no padding/margin of its own, so the
   ONLY gap between the video and the white hero-window box was this
   section's own 80px top padding. The gap on the OTHER side (between this
   section and Explore below it) is that same 80px PLUS Explore's own
   .section top padding — adjacent sections' padding-block values stack
   rather than collapsing, since these are plain padded <section>s with no
   margins involved — so the visual gap below (160px) was exactly double the
   gap above (80px). padding-top:160px (double .section's own 80px, same
   logic mobile: 96px = double 48px) makes the space above match the space
   below, and matches the ~160px gap between any other two adjacent sections
   sitewide. padding-bottom is left alone, still inheriting .section's own
   plain 80px/48px. */
.main-title-section {
  padding-top: 160px;
}
@media (max-width: 768px) {
  .main-title-section { padding-top: 96px; }
}
/* Side gutter moved here from index.html's own px-6/md:px-20 utility
   classes (removed) now that .hero-window has its own width/max-width
   ladder matching the Explore section's .container curve exactly (see
   .hero-window above) — any side padding on this section itself, at
   viewports 640px and up, would shrink the space available to
   .hero-window below each breakpoint's own max-width, stopping it from
   ever reaching the same outer width as Explore's own .container (which
   has no external gutter of its own; its 24px inset is internal padding
   on .container itself, not a squeeze from its parent). Below 640px,
   though, .hero-window has no max-width cap at all and would otherwise
   render flush edge-to-edge with the white 8px border touching the
   viewport — unlike Explore's own section, which has no visible border to
   expose that. Restoring a plain 24px gutter here, but ONLY below 640px,
   keeps that pre-existing comfortable mobile edge margin without
   interfering with the desktop/tablet width-matching above it. */
@media (max-width: 639px) {
  .main-title-section {
    padding-left: 24px;
    padding-right: 24px;
  }
}

/* --------------------------------------------------------------------------
   6. SITE HEADER / NAV
   Sits transparent over each page's hero band (photo on the homepage, a
   solid band on inner pages — see .hero below) so one header treatment
   works everywhere, per the Figma nav-over-photo pattern.
   -------------------------------------------------------------------------- */
/* Fixed (not absolute) so the header stays put — "sticky" — as the page
   scrolls, rather than scrolling away with the hero band it starts out
   overlaying. .site-header--scrolled (toggled by initStickyHeader in
   js/main.js once the page has scrolled a little) gives it a solid dark
   backdrop at that point so it stays legible over the light sections
   further down the page — the logos/Menu label are forced white
   (see below) which would otherwise disappear against those. */
.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 40;
  padding-block: 28px;
  background: transparent;
  transition: background-color 0.3s ease, padding-block 0.3s ease;
}
.site-header--scrolled {
  /* Solid black again, per a later request ("on scroll down, make the nav
     bar background colour black, not berry") — was briefly solid brand
     berry before that (itself replacing an earlier near-black
     rgba(15,15,15,0.92)). Applies everywhere this class is toggled (every
     page's fixed header, plus the homepage's own .site-header--floating bar
     below). Logos/Menu copy already come forced white via
     .site-header__mark/__wordmark img's filter and .site-header's own
     offwhite text colour further up this file — both unconditional, not
     scoped to this class — so nothing else needed to change for legibility
     against the new colour. */
  background: var(--color-black);
  padding-block: 18px;
}

.site-header__inner {
  position: relative; /* positions .site-header__wordmark below, independent of it */
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
}

/* Both header logo SVGs are solid dark shapes with no colour built in, so
   they're forced white with a filter rather than needing separate white
   source files — works reliably since the header always sits on a dark
   photo/scrim (.hero__scrim) or dark hero band (.hero--plain). */
.site-header__mark img,
.site-header__wordmark img {
  filter: brightness(0) invert(1);
  /* No filter transition here (removed a "transition: filter 0.3s ease"
     added for polish, per an earlier request to ease this into the hover
     recolour below) — per request ("the hover should happen instantly -
     currently it's cycling through random colours before hitting the
     required colour"): these are long multi-function filter chains
     (brightness/invert/sepia/saturate/hue-rotate/brightness/contrast), and
     the browser interpolates BETWEEN two such chains function-by-function
     rather than as a single blended colour — visually that reads as a
     rapid "rainbow" flicker through unrelated hues, not a clean crossfade.
     An instant snap has no interpolation to go wrong. */
}

.site-header__mark img { height: 44px; width: auto; }
.site-header__wordmark img { height: 20px; width: auto; }
/* Logomark U + wordmark hover colour — ONE colour, berry, everywhere, per
   request ("all nav bars logo and letter mark hover states - these should
   all be berry on hover - not lime or grey or off white"). This replaces
   what used to be a three-way split: a lime default (dark-backdrop headers
   — homepage/contact's .site-header--on-dark, every page's own
   .site-header--floating scrolled header), a berry exception scoped to
   .hero-frame's light-background static header, and a further
   homepage-only offwhite exception on top of that. All of that
   light/dark-mode-dependent branching is gone — a single rule now covers
   every header instance on every page regardless of backdrop, so there's
   only one hover colour left to ever show. Same tuned black-source-to-berry
   filter recipe .hero-frame used to use on its own (independent brightness
   (0) first, so it reliably overrides whichever filter the logo currently
   shows at rest — white via the base rule above, or none/black via
   .hero-frame's own rest-state override below — rather than combining with
   it). Leading "a" tag kept purely for the specificity bump this always
   needed against .hero-frame .site-header__mark img's own rest-state rule
   (2 classes + 1 type = 0,2,1) — a + img (2 types) makes this 0,2,2. */
a.site-header__mark:hover img,
a.site-header__wordmark:hover img {
  filter: brightness(0) invert(11.6%) sepia(88.4%) saturate(593.5%) hue-rotate(291.5deg) brightness(235.5%) contrast(129.6%);
}
/* Centred on .site-header__inner itself (not flex-distributed between the
   mark and the Menu button) so it stays exactly centred regardless of
   those two elements being different widths — space-between alone would
   otherwise centre it only when both sides happened to match. */
.site-header__wordmark {
  display: none;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}
@media (min-width: 768px) {
  .site-header__wordmark { display: block; }
}

.site-header__menu-btn {
  /* Simplified per request ("menu button - remove the styling on this
     (hover and default) and have default white text (no bg colour) and on
     hover create the berry background colour - use the sizing that is
     currently present") — no border/shadow/lift any more, just a plain
     white-text button at rest that fades to a solid berry background on
     hover (see the single hover rule near "4d. HEADER" below). Padding/
     margin/display/align-items/gap are untouched from before — that's the
     "sizing that is currently present" the request asked to keep. */
  background: none;
  border: 1px solid transparent;
  color: var(--color-white);
  cursor: pointer;
  padding: 10px 16px;
  margin: -10px -16px;
  border-radius: 0;
  transition: background-color 0.3s ease, color 0.3s ease;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
/* Uploaded as assets/svg_graphics/list.svg (a flat single-colour glyph) and
   inlined directly in the markup, rather than used via <img>, specifically
   so fill: currentColor can pick up whatever colour "Menu"'s own text is
   in any given context — the homepage's plain fixed header (offwhite),
   the .hero-frame's boxed nav row (black, see .hero-frame .site-header__
   menu-btn), the scrolled dark backdrop, hover states, all automatically,
   per request ("can the colour match the font colour"). An <img> would
   need a separate hand-tuned CSS filter for every one of those instead. */
.site-header__menu-icon {
  display: block;
  flex-shrink: 0;
  fill: currentColor;
}

.site-nav {
  position: fixed;
  inset: 0;
  z-index: 50;
  background: var(--color-black);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 32px;
  padding-block: 100px 40px; /* keeps content clear of .site-nav__top and gives the contact block room on short viewports */
  overflow-y: auto;
  /* Comes down from the top rather than fading in place — slides the whole
     panel from just above the viewport into position. visibility flips
     after the slide-out finishes closing (transition-delay) so it can't be
     tabbed/clicked into while off-screen, but flips immediately on open. */
  visibility: hidden;
  transform: translateY(-100%);
  transition: transform 0.6s cubic-bezier(0.65, 0, 0.35, 1), visibility 0s linear 0.6s;
}
.site-nav[data-state="open"] {
  visibility: visible;
  transform: translateY(0);
  transition: transform 0.6s cubic-bezier(0.65, 0, 0.35, 1);
}

/* Wraps the 5 primary links only — kept separate from .site-nav__contact
   below so a shared "gap" on .site-nav can't also be read as the space
   between individual link lines. */
/* Full viewport width, per request ("can the items background colours fill
   the whole screen - currently they are chopped off at about half the
   screen width") — was previously capped at min(90vw, 640px), a narrower
   centred column, which is exactly what left each button's background
   filling only that column rather than the full width of the screen behind
   it. Now 100% of .site-nav's own content box (edge to edge, since .site-nav
   itself carries no horizontal padding) — each link's own background (see
   .site-nav__link below) stretches the same full width automatically,
   nothing extra needed. Link text stays visually centred either way, via
   .site-nav__link's own justify-content:center.
   gap stays 4px per request ("between the button rows should be a 4px
   gap") — full-width rows packed this closely still read as adjacent
   "slats"/rows of one stacked menu. */
.site-nav__links {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  width: 100%;
  gap: 4px;
}
/* The chevron this link used to reveal on hover (span.site-nav__link-
   chevron + its own rules) has been removed outright, from every page's nav
   markup and from this stylesheet, per an earlier request — nothing reveals
   on hover any more, just the background swap (see the comment below this
   rule for the actual current default/hover/current-page colour states).

   display/align-items/justify-content/padding/margin all rebuilt per a later
   request ("Each nav item should be a full width button - between the
   button rows should be a 4px gap... can these have equal padding around
   the copy - take into account the line height and compensate for this"):
   - display:flex + justify-content:center (was inline-flex, no
     justify-content) now that .site-nav__links stretches every link to the
     column's own full width (see that rule's own comment) — inline-flex
     would only have mattered for a content-hugging box, and without
     justify-content the text would sit flush left in the new wider box
     instead of keeping its previous visually-centred position.
   - margin:0 (was -12px -28px) — that negative margin existed purely so the
     old text-hugging link's own padding didn't add extra space between
     independently-sized, widely-gapped (32px) rows. With every row now a
     real full-width block packed just 4px apart, a padding-cancelling
     negative margin of this size would make each row's padded background
     bleed vertically into its neighbour's — there's no longer a large gap
     for it to expand into unnoticed, so it's dropped and padding is now
     real, layout-affecting space instead.
   - padding-top/bottom no longer equal (was a flat 12px both sides) — a
     numerically-measured fix for the "unequal padding" report. Bebas Neue's
     own font metrics (hhea ascent 900/descent -300/upm 1000, i.e. a 1.2×
     "normal" line-height with NO further half-leading on top of that) put
     0.2× font-size of empty space above a capital letter's own cap-height
     and 0.3× below the baseline down to the line box's own bottom, purely
     from the font's built-in ascent/descent reservation for accents/
     descenders — confirmed by inspecting the actual Bebas Neue font file's
     OS/2 and glyph-bounds tables (cap-height 700/1000em, baseline 0, no
     glyph in this menu's own copy — Home/Services/Case Studies/About/
     Contact — dips below the baseline). At the old flat 12px/12px padding
     that baked-in 0.1× font-size imbalance (more room below the letters
     than above) read as the text sitting visibly high in its own button.
     padding-top:12px + 0.05×font-size and padding-bottom:12px - 0.05×
     font-size split that same imbalance evenly across both sides instead —
     both then measure out to an equal 12px + 0.25×font-size of visual gap
     above the cap-height and below the baseline (worked through in full,
     with the source numbers, in the project notes for this change). Written
     against the same clamp(32px,6vw,48px) this rule already uses for
     font-size, so the compensation scales fluidly with it rather than being
     tuned for one fixed size. */
/* Colour states. Backgrounds stripped from BOTH the default and hover
   states in an earlier request (see the underline rule below for that
   history); this round ("the active button should not have a light black
   background - only white text... make the default buttons (all but the
   active button) have lighter grey copy colour") removes the remaining
   background too — the current-page link's own var(--color-black-light10)
   — so no .site-nav__link carries a background any more, in any state.
   - default (all links except the current page): --color-grey-light copy
     (was plain white) — matches the muted --color-grey-light already used
     for the contact info/address below (.site-nav__contact), per this same
     request ("make the contact info and address have the lighter grey copy
     colour" — that rule already used this exact token, so no change was
     needed there beyond confirming it matches).
   - hover (any link EXCEPT the current page — see below): berry text, per
     request ("nav - on hover use the berry colour (underline included)") —
     was no colour change at all. The underline needs no separate colour
     rule of its own to follow suit — .site-nav__link-text::after's
     background is already `currentColor` (see that rule's own comment),
     so it inherits whatever this link's own text colour is at any given
     moment and turns berry right along with it automatically.
   - current-page (aria-current="page"): plain white text, no background —
     its only remaining distinguishing feature from a default (now grey)
     link. Hovering it still shows neither the underline nor any colour
     change, same "don't let hover disguise which page you're already on"
     reasoning this file has used for this link's active state in every
     previous round. */
.site-nav__link {
  position: relative;
  font-family: var(--font-display);
  font-size: clamp(32px, 6vw, 48px);
  color: var(--color-grey-light);
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid transparent;
  padding: calc(12px + clamp(32px, 6vw, 48px) * 0.05) 28px calc(12px - clamp(32px, 6vw, 48px) * 0.05);
  margin: 0;
  border-radius: 0;
  box-shadow: 0 0 0 0 transparent;
  cursor: pointer;
  transition: color 0.3s ease;
}
.site-nav__link:hover:not([aria-current="page"]) {
  color: var(--color-berry);
}
.site-nav__link[aria-current="page"] {
  color: var(--color-white);
}
/* Underline hover cue, replacing an earlier background swap. Same "draws
   in from the left" motif as .btn-tertiary-text's own underline (see its
   comment near the top of this file) — which itself traces back to how
   this exact nav menu used to behave, several rounds of background-based
   hover treatments ago. Lives on its own span wrapped around each link's
   label text in the markup (not the whole flex row, which spans the full
   width of the menu) so the underline's width always matches the copy
   itself rather than the full-width button row. Excluded on the
   current-page link's own hover, same reasoning as that link's colour
   above.
   Gap tightened to a true 10px per request ("tighten up the gap between
   nav item and underline - I'd like the gap to be 10px between copy and
   underline underneath it") — a plain bottom:-10px alone (this rule's own
   previous value) actually read as a much bigger gap than 10px, because
   .site-nav__link-text's line box (its default "normal" line-height for
   Bebas Neue, 1.2× font-size — see the padding-calc comment above for this
   same font's own hhea-derived metrics) reserves a further 0.3× font-size
   of empty space below the baseline for descenders this menu's own copy
   (Home/Services/Case Studies/About/Contact) never uses, so bottom:-10px
   was measuring 10px from the BOTTOM OF THAT EMPTY RESERVED SPACE, not
   from the visible letters themselves — roughly 0.3×48px ≈ 14px of extra,
   invisible slack stacked on top of the intended 10px at this rule's own
   largest clamp size alone.
   Fixed two ways together: line-height:1 on the span first removes the
   0.2× font-size of extra "normal" half-leading Bebas Neue's own metrics
   add beyond its raw ascent+descent box (see that same padding-calc
   comment), pulling the box in by 0.1× font-size at the bottom specifically
   (half of that 0.2× removed evenly off both top and bottom) — this alone
   isn't quite enough, since 0.3× (descent) − 0.1× (half-leading removed) =
   0.2× font-size of reserved space still remains below the baseline. The
   underline's own bottom offset then cancels exactly that remaining 0.2em
   with calc(0.2em - 10px) — em here resolves against this span's own
   font-size (inherited from .site-nav__link's fluid clamp value), so
   0.2em always equals the exact remaining slack at any viewport width, not
   just the one this was measured against, leaving a constant, true 10px
   visible gap from the letters to the underline everywhere the clamp
   ranges (worked through with the source ascent/descent numbers in the
   project notes for this change). */
.site-nav__link-text {
  position: relative;
  display: inline-block;
  line-height: 1;
}
.site-nav__link-text::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: calc(0.2em - 10px);
  width: 100%;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.site-nav__link:hover:not([aria-current="page"]) .site-nav__link-text::after {
  transform: scaleX(1);
}
/* Underline shown permanently (not just on hover) on the current-page link,
   per request ("on the active item can we have an underline (white to
   match the copy colour)"). No separate colour rule needed — the ::after
   rule above already sets background:currentColor, and .site-nav__link
   [aria-current="page"]'s own colour is already --color-white, so the
   underline inherits white automatically, same "no separate colour rule
   needed" reasoning as the hover state's own berry underline above. */
.site-nav__link[aria-current="page"] .site-nav__link-text::after {
  transform: scaleX(1);
}

/* Wraps the Close button in a .container, exactly like .site-header wraps
   the Menu button in the same class — both now inherit identical
   max-width/centering/padding, so Close and Menu land at the same right
   edge instead of one being relative to the full viewport and the other to
   the header's centered, padded content column. */
.site-nav__top {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  padding-block: 28px;
  display: flex;
  align-items: center;
  /* Matches .site-header__inner's row height (set by the 44px U-mark logo)
     so Close gets centred against the same 44px line-box that Menu is
     centred against — without this its row is only as tall as the Close
     button itself, which centres it a few pixels higher than Menu and
     throws off their otherwise-matching vertical position. Everything here
     is box-sizing:border-box (see the global reset), so min-height budgets
     in this element's own padding too — the 44px target row height has to
     be added ON TOP of the 2×28px padding-block, not used in place of it,
     or the padding alone (56px) already exceeds a plain 44px and the rule
     does nothing. */
  min-height: calc(44px + 2 * 28px);
  justify-content: flex-end;
}
/* Now styled identically to .site-header__menu-btn (rest AND hover), per
   request ("close in the nav menu to follow the same styling as 'menu'
   button. Also, match the same hover effects") — same background/border/
   colour/padding/margin/transition/layout properties, and the same single
   background-colour-only hover rule just below, rather than the lift/
   shadow/lime treatment this used to have. Padding/margin/display/
   align-items/gap stay the values already tuned to keep Close landing on
   the same right edge as Menu (see .site-nav__top above). */
.site-nav__close {
  background: none;
  border: 1px solid transparent;
  color: var(--color-white);
  font-family: var(--font-body);
  cursor: pointer;
  padding: 10px 16px;
  margin: -10px -16px;
  border-radius: 0;
  transition: background-color 0.3s ease, color 0.3s ease;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
/* Uploaded as assets/svg_graphics/x.svg and inlined directly in the markup
   (fill attribute stripped so fill:currentColor below can take over) —
   same reasoning as .site-header__menu-icon: this automatically matches
   whatever colour "Close"'s own text is (white, at rest and on hover — see
   .site-nav__close:hover below), with no separate hand-tuned filter
   needed. */
.site-nav__close-icon {
  display: block;
  flex-shrink: 0;
  fill: currentColor;
}
/* Berry background on hover, copy/icon colour unchanged — the exact same
   single hover rule as .site-header__menu-btn:hover (see "4d. HEADER"
   further down), per request. Replaces the old lime text/lime border+shadow
   treatment entirely. */
.site-nav__close:hover {
  background-color: var(--color-berry);
}

/* Contact info + socials, matching the footer's content — smaller and more
   muted than the primary links so the hierarchy stays clear. Confirmed
   against a later request ("make the contact info and address have the
   lighter grey copy colour") — already --color-grey-light here from this
   rule's original treatment, the same token the primary nav links above
   have now switched to as well (see .site-nav__link's own comment), so no
   value change was needed, just this note tying the two together. */
.site-nav__contact {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}
.site-nav__contact a,
.site-nav__contact span {
  color: var(--color-grey-light);
}
/* Berry (was lime, reverted per request "remove all lime use in the main
   nav menu" — the site's normal sitewide accent, same as everywhere else
   outside this one nav panel now). */
.site-nav__contact a:hover { color: var(--color-berry); }

.site-nav__social {
  display: flex;
  align-items: center;
  gap: 16px;
}
.site-nav__social a {
  display: inline-flex;
  width: 24px;
  height: 24px;
}
.site-nav__social img {
  /* Source SVGs are dark shapes with no colour built in — forced white
     since the nav sits on a solid black background (same treatment as the
     header logos), unlike the footer's own .site-footer__social, which
     deliberately leaves them at their natural dark colour on a white bg. */
  filter: brightness(0) invert(1);
  transition: filter 0.3s ease;
}
/* Hover: berry again (briefly lime in between, per an earlier request —
   reverted back per a later one, "remove all lime use in the main nav
   menu"), off-canvas nav only. footer's own .site-footer__social (which
   also covers the about page's founder-social block, reusing that class)
   already had its own separate berry hover the whole time, since these are
   two separate rules on two separate classes rather than one shared
   selector — this just brings the nav's own copy back in line with it.
   Standard numerically-solved, brightness(0)-first berry recipe used
   throughout this file. */
.site-nav__social a:hover img {
  filter: brightness(0) invert(11.6%) sepia(88.4%) saturate(593.5%) hue-rotate(291.5deg) brightness(235.5%) contrast(129.6%);
}

/* --------------------------------------------------------------------------
   7. HERO BAND
   .hero--photo (homepage): full-bleed image + dark scrim, header overlaid.
   .hero--plain (inner pages): solid black band with a page title, same
   header treatment, so the pattern is consistent site-wide.
   -------------------------------------------------------------------------- */
.hero {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  overflow: hidden;
}

/* Back in active use on the homepage — was boxed into the (now removed)
   .hero-frame component for a while, then brought back full-bleed per a
   further request ("can the homepage video be full screen up to the top of
   the page... use the same nav bar as on the services page"). Height
   changed from a fixed 840px to a full 100vh for that same request's
   "full screen" — was already full-bleed edge-to-edge before, just not
   full viewport height. */
.hero--photo {
  height: 100vh;
  justify-content: flex-start;
  padding-top: 20px;
  /* The MAIN TITLE section's rotating cube (.hero-canvas-wrap) is allowed to
     overflow its own section so it can flow into whatever sits immediately
     above/below it — but that same overflow means it can also poke up into
     the bottom of this photo hero, were this variant ever used directly
     above Main Title again. .hero already has position:relative; giving
     this variant an explicit z-index promotes it to its own stacking
     context above that section's default (z-index:auto) painting order, so
     the opaque photo always wins where the two visually overlap. */
  z-index: 2;
}
/* Applies to any full-bleed photo band (the main homepage hero and the
   plain .hero + inline-height bands like the final CTA), not just
   .hero--photo, so a background photo always positions correctly.
   Sized taller than its container (with a matching negative top offset) so
   there's headroom for the scroll parallax in js/main.js to pan the image
   within, without ever revealing empty space at the top/bottom edges —
   .hero's own overflow:hidden clips anything that pans past the container.
   video.hero__bg added alongside img.hero__bg once the homepage hero
   swapped its static photo for a looping video — identical box/positioning,
   object-fit:cover keeps the video's own 16:9 source correctly cropped to
   whatever taller/narrower shape .hero actually is. */
.hero img.hero__bg,
.hero video.hero__bg {
  position: absolute;
  top: -8%;
  left: 0;
  width: 100%;
  height: 116%;
  object-fit: cover;
}
.hero__scrim {
  position: absolute;
  inset: 0;
  background: rgba(15, 15, 15, 0.35);
}

/* --------------------------------------------------------------------------
   7a. HERO FRAME HEADER ROW
   Originally a boxed, 1440px-capped alternative to the full-bleed
   .hero--photo band above, holding both a static header row AND the
   homepage's video (.hero-frame__media/__video/__scroll, since removed —
   see the video hero's own comment in index.html for why). The homepage
   has moved back to full-bleed .hero--photo + a single fixed .site-header,
   but project detail pages still use just the header-row half of this
   component for their own static-then-floating dual header (see e.g.
   project-cradel-haus.html) — `.hero-frame` + `.hero-frame .site-header*`
   below stay in place for that; only the now-unused video/scroll-cue
   sub-component rules that followed them have been removed.
   -------------------------------------------------------------------------- */
.hero-frame {
  padding-top: 24px;
}

/* .site-header's own base rule (still used as-is, fixed+full-width+dark-
   backdrop, by every other page) exists purely to stay legible floating
   over a full-bleed photo. Nested inside .hero-frame instead, it's a
   plain static row, so all of that is stripped back out here. */
.hero-frame .site-header {
  position: static;
  background: none;
  padding-block: 0;
  margin-bottom: 32px;
}
.hero-frame .site-header__inner {
  padding-inline: 0; /* .hero-frame (a .container) already supplies this */
}
/* Logos were forced white (see .site-header__mark/__wordmark img above)
   purely to read over a dark photo/scrim — on this row's plain page
   background they go back to black, per request ("default homepage nav -
   make the unbuilt logo, logomark U default colour black and the menu
   button and icon default colour black"), reverting the previous
   berry-tint round. filter:none simply lets the logo files' own natural
   dark artwork show through, rather than tinting to a specific colour. */
.hero-frame .site-header__mark img,
.hero-frame .site-header__wordmark img {
  filter: none;
}
/* This wrapper's own dedicated berry hover override has been removed — the
   base a.site-header__mark:hover img rule above is now berry sitewide, so
   this would just be a redundant duplicate of it (dead weight, not a
   conflict — same colour either way — but per this file's own "remove a
   rule once it has nothing left to do differently" convention). */
/* Menu copy: black at rest — the one deliberate exception to the new
   sitewide "default white text" rule (see the base .site-header__menu-btn
   rule and its own note above), kept specifically because .hero-frame sits
   on the page's own light bg-grey-lighter background: white/off-white text
   there would be barely legible (the two colours are very close), so black
   stays the readable choice in this one light-background context. On hover
   the new sitewide berry-background treatment still applies (see the
   universal .site-header__menu-btn:hover rule further down) — text flips to
   white here specifically so it stays legible against that solid berry
   fill, rather than keeping the black that only works against the light
   backdrop underneath. */
.hero-frame .site-header__menu-btn {
  color: var(--color-black);
}
.hero-frame .site-header__menu-btn:hover {
  background-color: var(--color-berry);
  color: var(--color-white);
}

/* Project detail pages previously used a .site-header--on-light modifier
   here to fake this same light-mode (black logo/menu, berry hover) look on
   their own single always-fixed header, after their photo hero band was
   removed. They now reuse the homepage's actual .hero-frame +
   .site-header--floating dual-header pattern instead — a plain static row
   up top (styled by .hero-frame above) plus a separate persistent header
   that fades in on scroll (styled by .site-header--floating below) — per
   request ("please match/replace the project pages default and scrolling
   navs with the homepage default and scrolling navs"). See each
   project-*.html. .site-header--on-light and its scrolled variant are no
   longer referenced anywhere and have been removed. */

/* Homepage-only: a second, initially-invisible copy of the standard fixed
   .site-header, added per request ("add the scrollable nav bar to the page
   like on the other pages") — the boxed .hero-frame header above is only a
   plain in-flow row at the very top of the page (see "7a. HOMEPAGE HERO
   FRAME"), so once you scroll past it there's normally nothing persistent,
   unlike every other page where the one, always-fixed .site-header stays
   reachable throughout. This element reuses .site-header/.site-header__*
   as-is (same markup as every other page's header) and sits fully
   invisible at rest — it would otherwise double up with the boxed header
   while both are on screen together right at the top — fading in only
   once initStickyHeader (js/main.js) adds .site-header--scrolled to it,
   the same scroll-position toggle every other page's header already gets
   (that script now loops over every .site-header on the page, see its own
   comment). */
.site-header--floating {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease, background-color 0.3s ease, padding-block 0.3s ease;
}
.site-header--floating.site-header--scrolled {
  opacity: 1;
  pointer-events: auto;
}

/* Menu button hover — ONE universal rule now, per request ("menu button -
   remove the styling on this (hover and default) and have default white
   text (no bg colour) and on hover create the berry background colour").
   Used to be three separate rules (a plain white-fill/berry-text version,
   a .site-header--scrolled-scoped lime-text version, and a .site-header--
   on-dark-scoped lime-text version, each fighting over which backdrop got
   which accent colour) — all replaced by this one plain background-colour
   fade, the same in every context. Text stays whatever colour the base
   rule (or .hero-frame's own black-at-rest exception, which overrides this
   specific colour value on hover just above) already set — no colour flip
   needed here since white-on-berry already reads clearly. The icon
   recolours automatically via .site-header__menu-icon's fill:currentColor,
   no separate icon rule needed. Scoped to the Menu button only — the nav's
   Close button keeps its own existing hover treatment untouched. */
.site-header__menu-btn:hover {
  background-color: var(--color-berry);
}

/* .hero-frame__media/__video/__scroll and .scroll-hint--dark (the boxed
   hero-frame's own video slot, its <video>, its in-flow scroll-cue row,
   and that scroll-cue's black-on-light colour variant) removed — the
   homepage video moved back to the full-bleed .hero--photo + .hero__bg
   pattern (see index.html and .hero--photo/.hero__bg above), which needs
   none of them: it's already edge-to-edge with no breakout math required,
   and its scroll cue is the sitewide default .scroll-hint (white-on-dark,
   absolute over the photo/scrim) rather than this in-flow dark variant.
   Nothing else on the site referenced any of the four. */

.hero--plain {
  height: 360px;
  justify-content: center;
  background: var(--color-black);
  text-align: center;
  padding-inline: 24px;
}
/* Pure white (was offwhite) per request ("make header font colour white
   (same on all headers)") — applies to every .hero--plain page (Services,
   About, Contact, Portfolio; project pages no longer use .hero--plain at
   all, having had their own hero band removed in a later request — see
   their own h1's own comment), not just the page it was raised on, since
   .hero--plain is the one shared rule all of them use. */
.hero--plain .h1 { color: var(--color-white); }
/* Bumped up to match .hero-title's own font-size (the homepage's "Bringing
   Your Unbuilt Environment to Life"), per request ("can all page heros use
   the same font size... leave the project page title sizes as they are").
   Only font-size is copied — line-height/letter-spacing stay .h1's own,
   since only a font-size match was asked for. Safe to scope broadly here
   (rather than per-page) for the same reason the colour rule above is:
   .hero--plain no longer reaches the project pages (see that rule's own
   comment), and every OTHER .h1 on these same pages (CTA bands, "Why work
   with us", FAQ, etc.) sits well outside .hero--plain, so this can't reach
   them either. */
.hero--plain .h1 { font-size: clamp(40px, 7vw, 80px); }
/* These ARE the page's hero title (Services, About, Contact, Portfolio),
   so per request ("make the hero titles on all pages...") they take the
   same -1.4px letter-spacing as .hero-title rather than the base .h1's
   -1.28px. (Bold was tried on both .hero-title and .h1 at the same time as
   this letter-spacing, then reverted on both per follow-up request — only
   the letter-spacing tightening stayed.) */
.hero--plain .h1 { letter-spacing: -1.4px; }

/* Portfolio page only — solid berry hero, no photo, per request ("remove
   hero image and make it a solid berry colour"). A modifier layered on top
   of .hero--plain (which still supplies height/centring/padding) rather
   than changing .hero--plain's own background, since contact.html relies
   on that shared black fallback for its own photo-less hero — this keeps
   that page untouched. .hero__bg/.hero__scrim aren't used here at all (no
   photo to show or dim), unlike every other .hero--plain page. */
.hero--berry {
  background: var(--color-berry);
}

/* Contact page only — decorative background graphic (a transparent PNG)
   layered over the existing solid black .hero--plain background, per
   request ("keep the current bg colour as it's a transparent png"). Using
   the background shorthand (colour + image together) means the black still
   shows through the PNG's transparent areas exactly as it did before. */
.hero--contact-bg {
  background: var(--color-black) url('../assets/images/header-bg-3600x1200@2x.png') no-repeat center center / cover;
}

/* Contact page only — the email/phone links in the page body's own contact
   details column go berry on hover, per request ("add a berry hover
   colour to the email and phone number links"). Scoped to this one block
   (see contact.html's .contact-info wrapper) rather than a bare `a:hover`
   so it doesn't touch the off-canvas nav's own contact links (already lime
   on hover, see .site-nav__contact further up) or the shared footer's
   (already berry via .site-footer a:hover).
   display:inline-flex added below so the reveal-chevron (next comment)
   can sit inline before the text without breaking these anchors' own
   flex-column stacking in the wrapper above. */
.contact-info a {
  display: inline-flex;
  align-items: center;
  transition: color 0.3s ease;
}
.contact-info a:hover {
  color: var(--color-berry);
}
/* Chevron reveal on hover, per request ("hover state... where the chevron
   appears - similar to the links on the portfolio page under the projects
   label" — see .btn-tertiary--reveal-icon further up for that original
   component). Same collapsed-to-zero-width-then-expands-in-from-the-left
   technique and the same caret-right.svg/berry-filter recipe, but kept as
   its own scoped rule rather than reusing .btn-tertiary directly, since
   these links keep their own h3/Funnel Display sizing at the request's
   own instruction ("keep the current font style and Funnel Display")
   instead of switching to that component's smaller tertiary-button type. */
.contact-info__chevron {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 0;
  height: 16px;
  opacity: 0;
  overflow: hidden;
  margin-right: 0;
  flex-shrink: 0;
  transition: width 0.25s ease, opacity 0.25s ease, margin-right 0.25s ease;
}
.contact-info a:hover .contact-info__chevron {
  width: 16px;
  opacity: 1;
  margin-right: 8px;
}
.contact-info__chevron img {
  width: 16px;
  height: 16px;
  /* Same numerically-solved berry recipe used sitewide for this icon (see
     .btn-tertiary-icon img elsewhere in this file). */
  filter: brightness(0) invert(11.6%) sepia(88.4%) saturate(593.5%) hue-rotate(291.5deg) brightness(235.5%) contrast(129.6%);
}

/* Services page CTA ("Let's talk about your project") — the same
   decorative wireframe graphic as the contact hero above, but layered over
   this band's own solid berry fill instead of black, per request ("use
   this image as a background image for 'lets talk about your project'").
   A separate modifier from .hero--contact-bg (rather than reusing it)
   since .hero--berry alone is still used plain, with no graphic, on the
   portfolio page hero. */
.hero--berry-bg {
  background: var(--color-berry) url('../assets/images/header-bg-3600x1200@2x.png') no-repeat center center / cover;
}
/* Still used as-is on the homepage and about page's own final CTA sections
   — services.html's own final CTA dropped this modifier (back to
   .hero--berry's plain fill) per a later request to replace its static
   wireframe PNG with a real rotating dodecahedron instead; see that
   section's own comment in services.html and .cta-dodeca-wireframe below. */

/* Services page final CTA ("Let's talk about your project") — decorative
   rotating wireframe dodecahedron, replacing this section's old static
   .hero--berry-bg PNG. Reuses the same .hero-canvas-wrap/initHeroScene
   technique as every other wireframe on the site (see js/main.js).

   This box has been through three revisions — see services.html's own CTA
   comment for the full request-by-request history:
   1. Same width as the "Get a quote" button, top third visible above the
      footer.
   2. 1.5x that width, black, half visible.
   3. Current version — a flat 400x400px box, centred in the section
      ("make the wireframe 400px x 400px, the same dark berry as the stat
      divider lines and centre it in the section"). This dropped the
      earlier two versions' entire premise (sizing/positioning driven by
      the button's own responsive width via JS) in favour of a fixed box
      simply centred on the section, which is why this rule is now plain,
      static CSS with no companion script — top:50%/left:50%/
      translate(-50%,-50%) is the exact same centring technique
      .hero-canvas-wrap itself already uses by default (see that rule
      above), just applied one level up so it centres THIS wrapper (and
      therefore the 400x400 canvas inside it) within the section, rather
      than relying on .hero-canvas-wrap's own default centring against a
      differently-shaped parent.
   Colour (--color-berry-dark, "the same dark berry as the stat divider
   lines" — see .quick-stats-band .quick-stats__item further down this
   file) is passed as a hex literal in the initHeroScene call in
   services.html, same reasoning as every other wireframe colour on this
   site (that JS option can't reference a CSS custom property directly).
   pointer-events:none/z-index come from the shared .hero-canvas-wrap rule
   and this wrapper's own z-index below; aria-hidden is set in the markup —
   purely decorative, never intercepts the button/heading above it. */
.cta-dodeca-wireframe {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 400px;
  height: 400px;
  pointer-events: none;
  z-index: 0;
}
/* Overrides .hero-canvas-wrap's own default fixed min(80vw,787px) square —
   without this, the wireframe would render at that default size and only
   get cropped to this wrapper's 400x400 box, rather than the object itself
   actually rendering at 400x400 (see the long-established note on this
   elsewhere in this file, e.g. .project-card--inverted__wireframe's own
   comment). width/height:100% here makes the actual rendered canvas — and
   therefore the dodecahedron's own apparent size — match this wrapper's
   fixed 400x400px box directly. */
.cta-dodeca-wireframe .hero-canvas-wrap {
  width: 100%;
  height: 100%;
}

/* "Window" frame around the homepage wireframe, per an original request
   ("contain the white rotating wireframe in a rectangle... as if it's a
   window and we're looking at the rotating wireframe on the other side
   through it") — first sized independently off .hero-canvas-wrap's own
   base size (a vw-based calc, needing a manual 100%-cap to avoid overflowing
   narrow viewports). Rebuilt per a follow-up request ("make the white box be
   the same size as the container with the copy - add padding and keep the
   rotating wireframe as the background"): this IS the copy container now
   (index.html moved the w-full/max-w-4xl/text-center/flex-column classes the
   copy block always used onto this element directly, rather than a separate
   div inside it), so its width/height simply come from that content plus
   the padding below, no independent sizing calc needed any more — also
   removes the old mobile-overflow risk that calc's manual cap existed for,
   since w-full/max-w-4xl is already safely responsive sitewide.
   overflow:hidden clips the wireframe to the box's own bounds ("looking
   through a window") — briefly removed per a follow-up request to let the
   wireframe spill past the border, then put back per a further request
   ("please put the clipping back in"), so the wireframe (still its own
   fixed min(80vw,787px) square, sized independently of this box) is cropped
   to this box's edges on every side again, same as the original "window".
   pointer-events, unlike before, is NOT disabled here — this box now
   contains the real pill/heading/copy/button content, which all still need
   clicks/taps to work; only .hero-canvas-wrap below (purely decorative)
   keeps pointer-events:none. */
.hero-window {
  position: relative;
  /* z-index:0 (rather than leaving this at the default auto) is what
     actually makes the wireframe visible: position:relative alone does NOT
     create a new stacking context, so .hero-canvas-wrap's z-index:-1 below
     would otherwise escape past this element entirely and paint behind the
     nearest ancestor that DOES form one — in this case that's all the way
     up at <body>, whose own opaque background (html/body {background:
     var(--color-offwhite)}) then completely hides the wireframe behind the
     page itself. Adding z-index (any integer, 0 is enough) alongside
     position turns .hero-window into its own stacking context, so the
     negative z-index is contained inside it — behind this box's own
     in-flow content (the pill/heading/copy/button) but safely in front of
     the page background outside it. */
  z-index: 0;
  box-sizing: border-box;
  /* Was 4x the wireframe's own front-facing line weight (20px) — set
     explicitly to 8px per request ("make the border width 8px on the main
     container") instead. */
  border: 8px solid var(--color-white);
  /* New: the box used to have no padding of its own (its only content was
     the decorative canvas, centred independently) — now that real copy
     sits directly inside it, per request ("add padding"), so the pill/
     heading/copy/button don't sit flush against the border. */
  padding: 56px 48px;
  overflow: hidden;
  /* Width now matches the Explore section's own .container width exactly,
     per request ("increase the size of the 'visualising tomorrow' section
     so it is the same width as the section below") — was a flat 50vw.
     Rather than putting Tailwind's own `container` utility class directly
     on this element (which would also import its 24px side PADDING,
     fighting with this box's own 56px/48px padding above), the same
     max-width curve is hand-replicated below using the real breakpoints
     from js/tailwind-config.js (sm/md/lg/xl/2xl — 640/768/1024/1280/1440),
     keeping this rule as the only source of padding.
     width:100% here only resolves correctly because .hero-parallax-box
     (this box's immediate parent) now carries its own explicit w-full in
     index.html — without that, .hero-parallax-box (a centred flex column,
     align-items:center) would shrink-wrap to its own content instead of
     stretching full width, so this element's 100% would resolve against
     that shrink-wrapped size instead of the page (confirmed via
     measurement: without w-full upstream, this got stuck at ~812px on
     desktop, short of the 1024px+ breakpoints below). A vw-based width was
     tried instead at first, but 100vw runs long by the scrollbar's own
     width versus the ordinary flow-based width main-title-section itself
     resolves to (confirmed via measurement — a consistent ~15px
     mismatch against Explore's own, non-vw-based .container width), so
     percentage-through-an-explicitly-stretched-ancestor is used instead,
     matching Explore's own sizing exactly rather than approximating it.
     .hero-parallax-box's own items-center still centres this box within
     that now-full-width parent — no separate margin/transform centering
     trick needed. */
  width: 100%;
  max-width: none;
  /* Was a locked aspect-ratio (720:479), which made the box's height track
     its (now much wider) width — with the real pill/heading/copy/button
     content inside, that produced a box far taller than the content needed,
     mostly empty space. Replaced with a flat target height instead, per
     request ("make the box... have a height of around 360px") — min-height
     rather than a hard height so overflow:hidden above can never clip the
     content if it ever needs more room (e.g. the heading wrapping to an
     extra line at an in-between viewport width) than this target. At the
     current copy this resolves to content filling almost exactly 360px, so
     there's normally no slack either. */
  min-height: 360px;
}
@media (min-width: 640px) { .hero-window { max-width: 640px; } }
@media (min-width: 768px) { .hero-window { max-width: 768px; } }
@media (min-width: 1024px) { .hero-window { max-width: 1024px; } }
@media (min-width: 1280px) { .hero-window { max-width: 1280px; } }
@media (min-width: 1440px) { .hero-window { max-width: 1440px; } }
@media (max-width: 640px) {
  .hero-window {
    padding: 40px 24px;
    /* The 360px target above is sized for the desktop-width single-line
       heading/copy. On narrow mobile viewports the smaller clamped heading
       size means 360px would leave visible empty space below the content,
       so this cancels the min-height back to natural, content-driven height
       here, same as this box already did pre-360px on mobile. */
    min-height: 0;
  }
}
/* .large-copy is a plain <p>, so without this it keeps the browser's default
   UA margin (~1em top+bottom, computed off its own 18px font-size) on top of
   this box's own flex `gap-6` (24px) between the pill/heading/copy/button —
   that stacked margin made the gap around the copy line visibly bigger than
   the other two gaps, per request ("even out the padding between the
   trusted pill, visualising tomorrow heading, copy and get a quote
   button"). Scoped to this box only — .large-copy is used elsewhere
   (case-studies/about/project intros, stacked body-copy paragraphs) where
   its default margin is still relied on for spacing between paragraphs. */
.hero-window .large-copy {
  margin: 0;
}
/* Rotated wireframe canvas used behind the homepage headline — Three.js
   scene lives here (see js/main.js initHeroScene). Still centres on
   .hero-window (its nearest positioned ancestor, unchanged), but now sits
   BEHIND the copy that shares the box with it rather than being the only
   thing in there — negative z-index paints it before .hero-window's other,
   non-positioned, in-flow children (the pill/heading/copy/button) in normal
   stacking order, no extra wrapper around the copy needed just to layer
   over it. */
.hero-canvas-wrap {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(80vw, 787px);
  height: min(80vw, 787px);
  z-index: -1;
  pointer-events: none;
  opacity: 0.9;
}
.hero-canvas-wrap canvas { width: 100% !important; height: 100% !important; }
/* #hero-canvas (the actual Three.js mount point) is a plain inner div,
   deliberately separate from .hero-canvas-wrap above — that outer element
   owns the CSS transform:translate(-50%,-50%) used for centring, and the
   mouse-parallax effect (js/main.js initMouseParallax, "hero-parallax-cube")
   animates x/y transform on this inner one instead, so GSAP's own transform
   never fights the percentage-based centring transform on the same element
   (which stays correct across resize, since GSAP never touches it). */
.hero-parallax-cube {
  width: 100%;
  height: 100%;
}

/* Case studies page only — the page's title/intro copy AND its decorative
   "white frame + spinning wireframe" device (modelled on the homepage's own
   .hero-window: same border/overflow/min-height/padding conventions) now
   share ONE full-width bordered window, per request ("move the spinning
   wireframe to the right of the window it is in - expand the window to
   full width and place the 'case studies' and accompanying copy inside the
   thicker border/window. Add padding around the copy"). Used to be a plain
   text column beside a SEPARATE bordered box holding just the wireframe
   (see the git history of this comment for that version's own reasoning) —
   this replaces both with a single flex row: .case-studies-window__copy
   (left) and .case-studies-window__wireframe (right), in that DOM order,
   sharing this one box's border/overflow-clip instead of the wireframe
   having its own. z-index:0 creates the same local stacking context
   .hero-window relies on, for the same reason (see that rule's own
   comment) — not strictly needed here since there's no in-flow sibling
   content layering OVER the canvas the way .hero-window's copy does (the
   copy and wireframe are side-by-side siblings here, not stacked), but kept
   for parity/consistency with the component this is modelled on.

   Border colour switched from white to berry, alongside the wireframe
   object's own colour (see the initHeroScene call in case-studies.html),
   per request ("make rotating white wireframe berry and the border colour
   around case studies container berry") — border-width/overflow/layout
   all untouched, colour only.

   Border colour changed again, from berry to --color-offwhite — the exact
   token html/body itself is painted with (see html{background} above) —
   per request ("make the borders all the same colour as the page bg
   colour (wireframe and container)"). Matching the border to the page's
   own background makes the 8px ring blend into the surrounding page
   instead of reading as a coloured frame, while background:white below
   (new, same request: "make the background colour of this section white")
   still lifts this whole panel visually off the page as a plain white
   card. The wireframe object's own colour gets the same offwhite swap in
   case-studies.html's initHeroScene call, so the rotating shape reads as a
   soft, low-contrast line drawing against the white panel rather than a
   bold berry accent.

   Mobile border colour changed again, to a 20%-opacity black, per request
   ("case studies - top section on mobile ... Make the border colour black
   20 (just the container shape)") — scoped to the same max-width:1023px
   query as this rule's other mobile overrides below, so desktop/tablet
   keep the offwhite border untouched. "Just the container shape" — the
   wireframe object's own colour (initHeroScene's color param in
   case-studies.html) is NOT part of this change, only this box's border.

   Border colour changed once more, to plain white, per request ("case
   studies page - top white section - make the border colour white") — this
   time with no breakpoint qualifier in the request, so it replaces BOTH the
   desktop/tablet offwhite above and the mobile black-20 override below,
   rather than just one of them. Against this box's own white background the
   border now effectively disappears as a visible ring (white-on-white) —
   that reads as intentional given the request's own "white section" framing
   (the border existing mainly to keep the panel a clean rectangle against
   the page, not to be a visible coloured frame) rather than a mistake to
   second-guess. */
.case-studies-window {
  position: relative;
  z-index: 0;
  border: 8px solid var(--color-white);
  background: var(--color-white);
  overflow: hidden;
  display: flex;
  align-items: stretch;
  min-height: 360px; /* matches .hero-window's own target height exactly */
}
@media (max-width: 1023px) {
  .case-studies-window {
    flex-direction: column;
    min-height: 0;
    border-color: var(--color-white);
  }
}
/* Copy panel — padding added around the title/intro copy per request ("Add
   padding around the copy"), matching .hero-window's own 56px/48px exactly
   rather than inventing a new number. flex:1 1 0% takes whatever width the
   wireframe panel beside it doesn't need; justify-content:center vertically
   centres the copy against however tall the window ends up (driven by
   min-height above, or by the wireframe panel's own height at lg+ via
   align-items:stretch on the parent, whichever is taller). */
.case-studies-window__copy {
  flex: 1 1 0%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 32px;
  padding: 56px 48px;
  min-width: 0; /* lets long intro copy wrap instead of forcing the flex item wider than its share */
}
@media (max-width: 1023px) {
  .case-studies-window__copy {
    padding: 40px 24px;
  }
}
/* Wireframe panel — fixed/capped width (not a flex fraction of the row) so
   it stays close to square against the window's own ~360px height, per the
   fix noted on .case-studies-window above: a wide flexible column here was
   exactly what left the wireframe's own small square canvas stranded with a
   lot of dead space around it before it could reach this panel's actual
   edges. min(38vw, 420px) keeps it comfortably squarer than that at any
   viewport width without a hard-coded breakpoint table. */
.case-studies-window__wireframe {
  position: relative;
  flex: 0 0 auto;
  width: min(38vw, 420px);
}
/* Mobile: taken out of the flex flow entirely (was a stacked flex item with
   its own height:min(70vw,360px), which is what made this box's own total
   height wireframe-driven rather than text-driven) — per request ("make the
   container the size of the text (include padding)"). position:absolute +
   inset:0 makes this panel cover the same box .case-studies-window__copy
   now solely determines the height of, with zero contribution of its own
   to that height any more. It still paints behind the copy panel exactly
   as before: .hero-canvas-wrap's own z-index:-1 (see that rule above)
   escapes past this non-stacking-context wrapper up to .case-studies-
   window's own root stacking context (z-index:0), landing behind the
   copy's in-flow text regardless of this panel's own position value — see
   the standing comment on .case-studies-window itself for the full
   mechanics. .hero-canvas-wrap's own centring (top:50%/left:50%/
   translate(-50%,-50%)) still works unchanged against this new absolute
   parent. */
@media (max-width: 1023px) {
  .case-studies-window__wireframe {
    position: absolute;
    inset: 0;
    /* width:auto/height:auto explicitly reset here — inset:0 alone doesn't
       clear the base rule's own explicit width:min(38vw,420px) above (an
       absolutely positioned box with an explicit width keeps it regardless
       of left/right offsets), so without this the panel stayed pinned to
       that same ~148px desktop-column width instead of covering the whole
       (now much shorter) mobile box — confirmed by measurement. auto lets
       the inset:0 offsets alone determine the size, filling the container. */
    width: auto;
    height: auto;
  }
}
/* The wireframe canvas itself — reuses .hero-canvas-wrap/.hero-parallax-cube
   (see above) for the centring/clipping mechanics. height:100% + width:auto
   + aspect-ratio:1 keeps it a true square (no ellipse risk — aspect-ratio
   pins width to height directly, unlike two independent percentages fighting
   a non-square panel) that always fills this panel's own height exactly,
   with as little left/right dead space as the near-square panel width above
   allows. Below lg the panel is a fixed-height full-width strip rather than
   a fixed-width column, so width/height are set directly to match instead
   (aspect-ratio:auto, since height:100% would otherwise fight the panel's
   own explicit height there).

   Sized up to 3× (height:300% here, and the fixed mobile values below ×3)
   per a later request ("the wireframe spinning here is getting cut off in
   its hero-canvas-wrap - can it not get clipped ... just extend the
   hero-canvas-wrap to cover the whole of the window area"). This isn't
   just a bigger box for its own sake — it's one half of a two-part fix (see
   initHeroScene's own `cameraDistance` comment in js/main.js for the other
   half): the real clipping cause was the actual three.js camera's frustum
   being too narrow for the ×3 scale/perspective change from an earlier
   request, so js/main.js now pulls that camera back ×3 too (cameraDistance:
   16.5) to fix it — which on its own would also shrink the object's
   on-screen size ×3 (moving a camera back always does). Growing this wrap
   ×3 in step is what cancels that shrink back out, so the wireframe ends up
   rendered at the exact same apparent size/position it currently has, just
   inside a frustum wide enough to no longer clip it. .hero-canvas-wrap
   itself still just centres this bigger box on the same point (top:50%/
   left:50% + translate(-50%,-50%), from the shared base rule); it's
   .case-studies-window's own overflow:hidden — not this panel, which has
   none of its own — that then crops the now-oversized square back down to
   the window's actual visible border, "covering the whole of the window
   area" with transparent canvas the object never gets close to touching.

   Mobile values halved (was min(210vw,1080px) both dimensions) per a later
   request ("reduce the size of the wireframe to be 50% smaller on
   mobile"). This is a pure CSS pixel-size change, not a scale/perspective/
   cameraDistance change in initHeroScene — the manual-projection object's
   world-space coordinates and the real camera's frustum are completely
   untouched, so the object stays exactly as unclipped as before (same
   safety margin from the cameraDistance fix above), just rendered into a
   smaller canvas — the object's own share of that canvas (and therefore its
   apparent on-screen size) shrinks by exactly the same 50% as the canvas
   itself. Combined with .case-studies-window__wireframe's own mobile
   position:absolute/inset:0 above, the now-much-shorter (text-sized)
   mobile container simply crops more of this canvas away than before —
   intended, not a regression of the earlier clipping fix (that fix was
   about the real WebGL camera clipping the rendered shape itself; this is
   just this box's own ordinary CSS overflow:hidden crop at a smaller box
   size). */
.case-studies-window__wireframe .hero-canvas-wrap {
  height: 300%;
  width: auto;
  aspect-ratio: 1;
}
@media (max-width: 1023px) {
  .case-studies-window__wireframe .hero-canvas-wrap {
    width: min(105vw, 540px);
    height: min(105vw, 540px);
    aspect-ratio: auto;
  }
}

/* Animated scroll hint — a small "Scroll" label above a 1px line with a
   short segment travelling down it on a loop, bottom-center of the hero
   photo. Plain CSS keyframes rather than GSAP/ScrollTrigger since it isn't
   tied to scroll position, just a constant idle-state cue. Sits bare over
   the photo — no framing container/stroke around it (a boxed "brand line
   container" treatment was tried here and then removed by request). */
.scroll-hint {
  position: absolute;
  bottom: 32px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  pointer-events: none;
  z-index: 5;
}
.scroll-hint__label {
  font-family: var(--font-body);
  font-weight: 800;
  font-size: 10px;
  letter-spacing: 2px;
  text-transform: uppercase;
  /* Offwhite again (was briefly black, sat on its own .caption-chip
     background — that boxed treatment was removed by request; itself was
     lime by an even earlier request) — bare over the photo once more. */
  color: var(--color-offwhite);
}
.scroll-hint__line {
  position: relative;
  width: 1px;
  height: 56px;
  overflow: hidden;
}
.scroll-hint__line::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 1px;
  height: 24px;
  background: var(--color-offwhite); /* was lime, by request */
  animation: scroll-hint-drop 2.2s ease-in-out infinite;
}
@keyframes scroll-hint-drop {
  0%   { transform: translateY(-100%); opacity: 0; }
  20%  { opacity: 1; }
  80%  { opacity: 1; }
  100% { transform: translateY(300%); opacity: 0; }
}

/* --------------------------------------------------------------------------
   8. PILL / EYEBROW TAG
   Removed sitewide in an earlier round ("remove all pills"), then restored
   here for the homepage only, per a later request ("add the pill... back
   in to the homepage") — every other page's eyebrow pill (About, Contact,
   Portfolio, Services, every project page) stays removed; only the
   homepage's own markup brings .pill-tag back into use. Rebuilt from this
   section's own removal notes (rounded pill, 20px radius, 10px/18px
   padding, 12px uppercase type at 1.8px tracking) rather than a variant of
   .button-copy — this ran smaller/tighter than that shared button type
   scale even before removal. Grey border+copy (was berry, per request) on
   the homepage's plain offwhite background reads as a muted label, not a
   button, so it isn't filled solid the way .btn-primary/.btn-secondary
   are. There's no .hero
   .pill-tag photo-background variant yet — the homepage's only surviving
   instance sits on the plain "MAIN TITLE" section, not over a photo hero,
   so nothing needs that treatment right now; add one here first if a pill
   ever returns to a photo hero elsewhere. */
.pill-tag {
  display: inline-flex;
  align-items: center;
  /* Berry now (was white), per request ("make 'your trusted cgi partner'...
     border colour berry") — this had gone grey -> white over earlier
     requests; berry is a deliberate departure from the "borders should be
     white/grey" pattern used elsewhere on the homepage, specific to this
     pill. */
  border: 1px solid var(--color-berry);
  border-radius: 14px;
  /* Sized down per request ("make the ... pill smaller") — padding/
     font-size/tracking all reduced together (radius follows padding+
     font-size down too, so it still reads as a fully rounded pill rather
     than going squarer as it shrinks) rather than just scaling one
     property on its own. */
  padding: 6px 14px;
  font-family: var(--font-body);
  font-weight: 800;
  font-size: 10px;
  line-height: normal;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  /* Berry now (was a light grey, #999999, itself lightened over a couple of
     earlier requests from --color-grey), per request ("make 'your trusted
     cgi partner' text berry"). */
  color: var(--color-berry);
}

/* --------------------------------------------------------------------------
   8b. CASE STUDY TITLE TAG
   The category half of every "Client | Category" case study title (e.g.
   "Exterior Visuals" in "Cameron Hall Homes | Exterior Visuals"), per
   request ("can the second part of these list names ... after the vertical
   line be put into a display-only tag instead of the headline font? Do this
   for all projects and instances across the site").

   Restyled per a later request ("under Supporting Figures [...] can we use
   this styling across the site for the display tags. So same positioning
   and no border pill") to match the small uppercase label used for the
   services page's Supporting Figures group headings (.stat-band__label,
   itself just the shared .h4 label style — heading font, 600 weight,
   uppercase, 1.8px tracking — with its colour set to grey) instead of the
   bordered/padded pill this component started as.

   Order flipped per a further request ("switch the order of the display
   tag so it goes before the title") — the tag markup now comes first in
   each heading, ahead of the client/project name, so it reads as an
   eyebrow label above the title rather than a line underneath it. It's
   still display:flex + width:fit-content, which is what drops it onto its
   own row (a flex container is block-level by default, so nested inside
   the heading's text it forces a line break with no wrapper element
   needed) rather than sitting inline beside the name. The spacing rule
   moved from margin-top to margin-bottom to match: since the tag is now
   the first thing in the heading, the 10px gap needs to sit between the
   tag and the name that follows it, not above the tag itself.

   Scope: only the instances that were actually rendering in the headline
   font — each project's own <h1>, and the case studies grid cards
   (<h2 class="h3">). ("Next case study" cards on project pages dropped
   their own tag entirely per a separate later request and never got this
   restyle.) The breadcrumb's current-page text and the case studies mobile
   <select> were never set in the headline font to begin with (already
   .regular-copy/plain body text) and a <select><option> can't hold a
   styled <span> at all, so both keep plain text — the sidebar quick-link
   list and the mobile dropdown also drop the "| Category" half entirely
   rather than keeping it as plain text (see the "Projects" list markup).
   The two case studies without a "Client | Category" name (The Heddon,
   Wynyard; Sands Farm Cottages) have nothing to tag and are untouched. */
.title-tag {
  display: flex;
  width: fit-content;
  align-items: center;
  margin-bottom: 10px;
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 12px;
  line-height: 18px;
  letter-spacing: 1.8px;
  text-transform: uppercase;
  color: var(--color-grey);
  /* Stops the category text from wrapping onto a second line on narrow
     viewports — it already gets its own full row via display:flex above,
     so there's no risk of it colliding with the name text beside it the
     way there was when both shared a line. */
  white-space: nowrap;
}

/* .portfolio-grid .project-card__body h2 (the case studies grid's own
   enlarged card titles) removed — the cards no longer use .project-card--row
   /.project-card__body at all, and the project name they used to hold as a
   plain <h2> now lives as a .service-card__caption overlay chip instead, per
   request ("make the project cards use the same layout as the 3 service
   cards on the homepage"). See .portfolio-grid .service-card* below. */

/* --------------------------------------------------------------------------
   9. CARDS — service items, project items, testimonials
   -------------------------------------------------------------------------- */
.card {
  position: relative;
  background: var(--color-white);
  border: 1px solid var(--color-grey-light);
  border-radius: 0;
}
/* .card--offwhite (the white box/72px-radius/67%-width wrapper previously
   around the homepage Explore section) was removed by request — that
   section now sits directly on the page like every other one, no card
   styling. Class deleted rather than left dead since nothing else used it. */

/* The old 66%-width/centred constraint on .explore-content (from when this
   was a heading+CTA row stacked above a separate 3-card row) was removed
   now that the section is a single 4-column grid (text column + 3 cards,
   see index.html) spanning the full .container width, same as every other
   full-width grid section on the page (e.g. .featured-grid). */
/* Explore's own 3 images specifically — per request ("make image aspect
   ratio 32 / 24 on the 3 service cards"), was 31/15 (itself matching the
   homepage "Recent projects brought to life" cards' proportion before
   this). Scoped to .explore-content rather than changing
   .service-card__media itself, which would also affect services.html. */
.explore-content .service-card__media {
  aspect-ratio: 32 / 24;
}
/* Mobile override — matches the Featured section's own 3 project images
   (.featured-grid .project-card__media, aspect-ratio 31/15) further down
   the page, per request ("3 x service cards on mobile on the homepage can
   these be the same height as the 3 x project images further down the
   page"). Both grids stack to the same 100%-of-.container width on mobile,
   so matching aspect-ratio is what makes the two photo areas the same
   height — only the image crop changes here, the card's own body copy
   underneath is untouched. Scoped below the grid-cols-1/md:grid-cols-3
   breakpoint (768px) these cards actually stack at, not the site's usual
   640px cutoff, so it also applies through the narrow single-column tablet
   range. */
@media (max-width: 767px) {
  .explore-content .service-card__media {
    aspect-ratio: 31 / 15;
  }
}
/* Homepage Explore's own 3 card containers specifically — 37px (was the
   shared 25px .service-card default) per request. Scoped the same way as
   the aspect-ratio override just above, so services.html's own 4 cards
   (which also use .service-card) are unaffected. */
.explore-content .service-card {
  border-radius: 0;
}
/* Equal card heights across the 3 Explore cards, per request ("make the 3
   column item cards ... all be the same height") — the row's own
   items-center (an earlier state of index.html) previously let each card
   size to its own content and sit centred within the row, so a card with a
   longer description paragraph rendered visibly taller than its siblings.
   Grid's default stretch (restored here, scoped to just these 3 cards)
   sizes every item in the row to the tallest one automatically; turning
   this card into a flex column with its body allowed to grow is what lets
   that extra height land as flexible space in the body rather than
   stretching the (fixed aspect-ratio) image or leaving a dead gap after
   the border-radius. (These 3 cards now sit in their own row, separate
   from the heading/copy text column above — see "EXPLORE" in index.html.) */
.explore-content .service-card {
  display: flex;
  flex-direction: column;
  height: 100%;
}
.explore-content .service-card__media {
  flex: none;
}
.explore-content .service-card__body {
  flex: 1 1 auto;
}
/* Smaller copy for these 3 cards specifically, per request ("can the text
   in these 3 cards be smaller copy - 12px") — scoped to .explore-content
   rather than changing .regular-copy itself, which is used sitewide
   (service-card descriptions elsewhere, testimonials, project meta, etc.).
   Line-height scaled down to match .regular-copy's own 14px/21px (1.5x)
   ratio rather than left at 21px, which would read as oddly loose against
   the smaller type. */
.explore-content .service-card__body .regular-copy {
  font-size: 12px;
  line-height: 18px;
}
/* Explore's own 3 card titles float as a caption overlaid on the image,
   same offwhite chip look as .project-card__caption (background/radius/
   padding). Position matches .project-card__caption's own bottom-left
   anchoring exactly, per request ("style the 3 service container titles
   ... using the same styling as the 3 portfolio text containers (so left
   bottom aligned on the image but keep their current font styling)") — was
   centred in the image via top/left 50%+translate instead. Only the
   position/background/radius/padding here changed at the time, and those
   now simply match .project-card__caption's own values rather than
   duplicating them independently. Scoped to .explore-content since this is
   a new class not used anywhere else yet.

   The title's own font styling — untouched by the above — was changed
   directly in index.html per a later, separate request ("update the copy
   in the 3 card titles to match the button copy in 'Our 3D rendering
   services'"): each h3's class swapped from .h3 (Funnel Display, 18px) to
   .button-copy (Raleway, 800 weight, 14px, uppercase, letter-spacing —
   see "3. TYPE SCALE" above), the same class already sitting directly on
   that CTA's own <a>.

   display:inline-flex/align-items added per a later request ("can the
   chevron right appear on the right of the title container") — lays the
   label span and the new .service-card-caption__chevron span (see below)
   out side by side inside the chip; being position:absolute with only
   `left` set (no `right`), this element was already shrink-to-fit width
   like inline-block, so switching its own display to inline-flex doesn't
   change its sizing — it still hugs its content exactly, chevron included,
   which is what lets the chip visibly grow to the right as the chevron
   reveals on hover instead of the icon overflowing a fixed-width box. */
.explore-content .service-card__caption {
  position: absolute;
  left: 16px;
  bottom: 16px;
  display: inline-flex;
  align-items: center;
  background: var(--color-offwhite);
  border-radius: 0;
  padding: 10px 18px;
  /* Colour swap now happens via this transition + the .group:hover rule
     below rather than Tailwind's group-hover: utilities (previously
     group-hover:bg-[var(--color-lime)] inline in index.html) — moved to
     plain CSS so background-color and text colour can animate together on
     the same timing as the chevron reveal beside them. */
  transition: background-color 0.25s ease, color 0.25s ease;
}
/* Hover — fills berry, copy turns white, per request ("on hover can the
   titles ... colour be bg colour berry, text colour white"), replacing the
   previous lime fill / unchanged-black-text hover look. .service-card (the
   parent <a class="service-card group">) is what's actually hovered, not
   just the chip under the cursor, matching the rest of this card's own
   hover language. */
.explore-content .service-card:hover .service-card__caption {
  background-color: var(--color-berry);
  color: var(--color-white);
  /* Right-side padding reduced by 8px (18px -> 10px) in the hover/revealed
     state only, per request ("on all buttons that reveal a chevron on
     hover... reduce the padding on the right side of the chevron by 8px")
     — tightens the gap between the revealed chevron and the chip's own
     right edge. Rest-state padding (10px 18px, all sides) is untouched. */
  padding-right: 10px;
}
/* Chevron — collapsed to zero width/opacity at rest, expanding in from the
   left on hover, same technique as .contact-info__chevron /
   .btn-tertiary--reveal-icon elsewhere in this file. Growing this element's
   width on hover is what visibly extends the chip itself (see the
   inline-flex/shrink-to-fit note above) to "include" the chevron, per
   request, rather than the icon appearing inside a chip that's already
   sized to fit it at rest.
   Sized 16px (was 14px) per a later request ("can the chevron be the same
   size as the one in 'Our 3D rendering services'") — matches
   .btn-tertiary-icon img's own 16px exactly (see "4c. TERTIARY BUTTON"
   above), the chevron on that CTA in the Explore header just above these
   cards. */
.service-card-caption__chevron {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 0;
  height: 16px;
  opacity: 0;
  overflow: hidden;
  margin-left: 0;
  flex-shrink: 0;
  transition: width 0.25s ease, opacity 0.25s ease, margin-left 0.25s ease;
}
.explore-content .service-card:hover .service-card-caption__chevron {
  width: 16px;
  opacity: 1;
  margin-left: 6px;
}
.service-card-caption__chevron img {
  width: 16px;
  height: 16px;
  /* caret-right.svg is a flat white source SVG (see .btn-secondary-icon img
     above for the same asset/recipe) — brightness(0) invert(1) forces solid
     white regardless of its own fill, matching the caption's own hover text
     colour. */
  filter: brightness(0) invert(1);
}
/* .explore-text-col removed — the heading/copy/CTA text column no longer
   sits in the same grid row as the 3 cards (see "EXPLORE" in index.html:
   the section is now 2 stacked rows, a 2-column header row above a
   3-column card row, per request), so there's no longer a shared row
   height for this class to vertically centre against. */

/* .service-card is now the real visible card — a white box with its own
   padding, holding the image at top and the info below it (both always
   visible, no hover-collapse). The image sits INSET inside this box, with
   its own smaller, independent border-radius, rather than being the outer
   shape the text used to float on top of. The scroll wireframe-reveal
   (data-reveal-box-group, applied to this link) picks up this real
   background/radius automatically — see runBoxReveal in js/main.js — so no
   JS changes were needed to make the reveal trace this new shape. */
.service-card {
  display: block;
  background: transparent;
  border: 1px solid var(--color-grey-light);
  border-radius: 0;
  padding: 20px;
  color: var(--color-black);
  /* Real (zero-offset, zero-alpha) rest value — without this, transitioning
     box-shadow FROM the default `none` can't be interpolated at all, so the
     hover shadow used to snap straight to its full offset instead of
     growing in step with the lift (see "4b. BUTTON HOVER" for the same
     fix on buttons). */
  box-shadow: 0 0 0 0 transparent;
  /* 1.05s (1.5x the previous 0.7s, itself 2x the 0.35s hover-in speed
     below), per request — only governs the restore-to-rest direction: a
     property change uses whichever rule's own `transition` applies to the
     state being transitioned TO, so this slower value takes over the
     instant the mouse leaves, while entering hover still uses
     .service-card:hover's own faster 0.35s. */
  transition: transform 1.05s ease, background-color 1.05s ease, border-color 1.05s ease, color 1.05s ease, box-shadow 1.05s ease;
}
/* Same interaction as .testimonial-card's hover (lift + a flat "growing out
   of the page" shadow directly beneath, plus a fill that only appears on
   hover) — but keeps this card's own berry border/copy/shadow language
   (these ARE clickable links, unlike the testimonials) instead of
   testimonial-card's neutral white/grey. color:berry on the card itself
   cascades to .h3 (no colour rule of its own), but .regular-copy sets its
   own colour explicitly, so it needs its own override below to actually
   follow. */
.service-card:hover {
  transform: translateY(-8px);
  background-color: var(--color-white);
  border-color: var(--color-berry);
  color: var(--color-berry);
  box-shadow: 0 5px 0 0 var(--color-berry);
  /* Own (faster) transition for entering hover — see the base rule's
     comment above for why this needs to be restated here rather than just
     relying on .service-card's own transition value. */
  transition: transform 0.35s ease, background-color 0.35s ease, border-color 0.35s ease, color 0.35s ease, box-shadow 0.35s ease;
}
.service-card:hover .regular-copy {
  color: var(--color-berry);
}
.service-card__media {
  position: relative;
  aspect-ratio: 1 / 1;
  border-radius: 0;
  overflow: hidden;
}
/* Sits below the image in normal flow — the gap above is this element's own
   padding-top, matching the 20px the card uses everywhere else. Carries no
   background of its own (deliberately) so .service-card's own hover fill
   shows straight through underneath the text, rather than the text sitting
   in a separately-coloured panel. */
.service-card__body {
  padding-top: 20px;
}

/* Homepage Explore's 3 cards — first tried as a styling experiment on just
   the Exterior card (image flush to the card edges, no border colour at
   rest, plus an extra white stroke on hover), then adopted for all 3 per a
   follow-up request ("remove the white border that appears on hover on
   exterior vis then apply the same hover styling to the other 2 items.
   Remove the gap inbetween these 3 elements") — the white hover stroke is
   gone, and what's left (no padding around the image, no border colour at
   rest) is now standard for all 3 cards via this existing .explore-content
   scope, rather than a per-card modifier class.

   border-bottom-color added back per a later request ("can the default view
   have a 1px berry line on the bottom") — border-color above still zeroes
   out all 4 sides (top/left/right stay invisible, flush against the image),
   this one side is then raised back up, using the 1px width the shared
   .service-card base rule already set. (0,2,0) + a longhand physical
   property beats the shorthand border-color's (0,2,0) regardless of source
   order, since same-specificity longhand-after-shorthand always wins for
   the side it targets.
   Rest colour changed from berry to light grey per a further request ("the
   default card bottom border colour should be light grey and update to
   berry on hover") — berry now lives on the dedicated :hover rule below
   instead.
   border-width added per a further report ("theres a gap between the
   service image cards - 1-2px, could this be removed") — border-color:
   transparent above only ever hid the shared .service-card base rule's 1px
   top/left/right border by colour, not by size: with box-sizing:border-box
   (global reset), an invisible 1px border still occupies 1px of box space
   on those 3 sides, inseting the image away from the card's actual edge by
   that amount even though nothing was visibly drawn there — exactly the
   reported gap. Zeroing the width on those 3 sides (bottom stays 1px, via
   border-bottom-color below still relying on it) removes that space
   entirely rather than just hiding its colour. */
.explore-content .service-card {
  border-width: 0 0 1px 0;
  border-color: transparent;
  /* Light grey again (was briefly turned white per "can you turn all
     borders that are grey on the homepage to white", then reverted back per
     a later request, "make the border at the bottom of the cards light grey
     again (not white)") — becomes berry on hover below exactly as before,
     only the rest-state colour changes. */
  border-bottom-color: var(--color-grey-light);
  /* Was the shared 20px on all sides — zeroed out here (rather than
     cancelling it with a negative margin on .service-card__media, the
     technique used for a single card before). A 10px grid gap was added
     back between the 3 cards per a later request (index.html's gap-[10px],
     was gap-0 — see that grid's own comment), so unlike when this was
     first written the images no longer sit flush edge-to-edge against each
     other; card padding stays 0 regardless, since the 10px now comes from
     the grid gap rather than needing to be clawed back from the card
     itself. */
  padding: 0;
}
.explore-content .service-card__body {
  /* Replaces the shared padding-top:20px-only rule — this now supplies the
     gap below the image (top) AND the left/right/bottom insets around the
     copy that the card's own (now zeroed) padding used to provide. */
  padding: 20px;
}
/* Mobile row gap — the grid's own gap-[1px] (index.html, deliberately tight
   per an earlier request, "make gap between these 1px") reads fine as a
   column gap between 3 cards sitting side by side on desktop, but at
   mobile's single-column stack that same 1px becomes the ROW gap between 3
   stacked cards, leaving them looking glued together. Per request ("can
   these have the same padding between the rows as the case study project
   cards") — matches .featured-media-col's own gap-[10px] between its 3
   project images further down the page. .explore-cards-grid is the stable
   class added to this grid in index.html alongside its existing Tailwind
   utility classes, purely so this override has something reliable to target
   (same convention as .service-lede__copy elsewhere in this file). Scoped
   to the same 767px breakpoint as the two rules just above, matching the
   grid-cols-1/md:grid-cols-3 breakpoint this grid actually stacks at —
   desktop's own 1px column gap is untouched. */
@media (max-width: 767px) {
  .explore-cards-grid {
    gap: 10px;
  }
}
/* Hover — per a further request ("on hover the background colour should
   change to white ... also on hover the card drop shadow should sit on the
   baseline of the other cards ... the default card bottom border colour
   should be light grey and update to berry on hover"):
   - background-color restates the shared .service-card:hover's own white
     fill — not strictly needed for the cascade ((0,3,0) here already beats
     that rule's (0,2,0) on every property), just kept explicit alongside
     the two properties below that ARE specific to these 3 cards, so this
     whole hover look reads as one rule rather than split across two.
   - border-bottom-color goes berry, overriding the rest-state rule above
     that pins this one side to grey.
   - box-shadow's vertical offset is 8px here (was the shared rule's 5px) —
     matching .transform's own -8px lift exactly, rather than falling short
     of it, is what makes the shadow line land precisely on these 3 cards'
     shared rest-state baseline (where the two NOT currently hovered cards'
     own bottom edges still sit) instead of hovering 3px above it. Scoped to
     .explore-content since the shared 5px/-8px pairing is deliberately
     mismatched everywhere else it's used (buttons, services.html's own 4
     cards) and shouldn't change there. */
.explore-content .service-card:hover {
  background-color: var(--color-white);
  border-bottom-color: var(--color-berry);
  box-shadow: 0 8px 0 0 var(--color-berry);
}
/* Title row — puts the chevron (see .project-card__chevron above, reused
   here) at the far right of the heading, same "this is clickable" language
   the portfolio page's row cards use, added by request. */
.service-card__title {
  display: flex;
  align-items: center;
  /* Was space-between (chevron pinned to the card's far right edge) — now
     sits right after the title text instead, per request, matching
     ".project-card--row"'s own chevron placement in spirit even though that
     one IS pinned to the row's right edge (a wider, horizontal layout where
     that reads differently). gap-8 (was 12px) keeps a small but tight
     word-to-icon spacing rather than the wider card-edge gap. */
  gap: 8px;
}

.project-card__media {
  position: relative;
  aspect-ratio: 31 / 20;
  border-radius: 0;
  overflow: hidden;
}
/* Homepage Featured cards only — 75% of the shared aspect-ratio's height
   at the same width (31/20 scaled by 1/0.75 on the height term = 31/15),
   scoped to .featured-grid specifically so the portfolio grid's own use of
   .project-card__media is untouched. */
.featured-grid .project-card__media {
  aspect-ratio: 31 / 15;
}
/* Project pages' first content image (directly under the page's own top
   hero banner, not that banner itself) — 16:9, per request now widened to
   run the full width of its .container (up to 1440px, matching the
   content below it) rather than the earlier, narrower 1200px cap — the
   1200px max-width/centring has been dropped so this is back to plain
   width:100%, same convention as every other .project-card__media use on
   the site. aspect-ratio 16/9 holds that shape responsively at any width. */
.project-card__media--lead {
  aspect-ratio: 16 / 9;
  width: 100%;
}
/* Cameron Hall Homes CGI Suite's lead slot holds a video instead of a photo,
   per request — the parent's own position:relative/aspect-ratio/
   overflow:hidden (all already set above) are exactly what an absolutely-
   positioned, edge-to-edge <video> (or, previously, an iframe) needs to fill
   that same 16:9 shape a plain <img> would, so nothing else about
   .project-card__media--lead needed to change for this. object-fit:cover
   guards against the source's own aspect ratio ever drifting slightly from
   the box's. */
.project-card__media--lead iframe,
.project-card__media--lead video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  object-fit: cover;
}
/* Homepage Featured project titles — same floating-inside-the-image
   treatment as .service-card__body (padding to the image edge, its own
   border-radius), sized to its own text rather than the full width.
   .project-card__caption isn't used anywhere else on the site, so this rule
   was updated in place (rather than adding a new scoped class) to work
   exactly like the Explore section's .service-card__caption tags, per
   request ("can the tag with copy on each work in the same way as the 3
   service tags ... use the same font and styling for these tags") — display:
   inline-flex/align-items added so the label <span> and the new
   .service-card-caption__chevron span (reused as-is — see index.html) lay
   out side by side and the chip can visibly grow to the right as the
   chevron reveals on hover, same recipe as .explore-content
   .service-card__caption. The old fixed lime group-hover fill (Tailwind
   group-hover:bg-[var(--color-lime)] in index.html) is gone — colour now
   animates via this transition + the hover rule below instead. */
.project-card__caption {
  position: absolute;
  left: 16px;
  bottom: 16px;
  display: inline-flex;
  align-items: center;
  background: var(--color-offwhite);
  border-radius: 0;
  padding: 10px 18px;
  transition: background-color 0.25s ease, color 0.25s ease;
}
/* Hover — berry fill, white text, same language as
   .explore-content .service-card:hover .service-card__caption. The whole
   card link (.group) is what's hovered, not just the chip, matching that
   same convention. */
.featured-grid a.group:hover .project-card__caption {
  background-color: var(--color-berry);
  color: var(--color-white);
  /* Same 8px right-padding reduction as .service-card__caption's own hover
     rule above (18px -> 10px), hover-state only. */
  padding-right: 10px;
}
/* Chevron reveal — reuses .service-card-caption__chevron's own base rule
   (collapsed to zero width/opacity at rest, defined once above under the
   Explore section) unchanged; this is just the trigger for this section's
   cards, matching .explore-content .service-card:hover
   .service-card-caption__chevron's values exactly. */
.featured-grid a.group:hover .service-card-caption__chevron {
  width: 16px;
  opacity: 1;
  margin-left: 6px;
}

/* Same offwhite chip look as .project-card__caption above (background/
   radius/padding, no positioning) — reused, per request, on a handful of
   small standalone labels site-wide that want the same treatment: the
   homepage hero's "Scroll" hint, the portfolio sidebar's "Filter"/
   "Projects" labels, and its "Showing N projects" count line. Kept as its
   own class rather than folding these into .project-card__caption itself,
   since that class's own position:absolute/left/bottom only make sense for
   a caption floating over an image. */
.caption-chip {
  display: inline-flex;
  background: var(--color-offwhite);
  border-radius: 0;
  padding: 10px 18px;
}

/* Portfolio grid cards' own floating text panel — image + text still share
   one container the way the homepage Featured cards do (unlike Explore's
   .service-card, which since the redesign above wraps its image and text in
   a real offwhite box rather than floating the text over the photo). Kept
   as its own class rather than reusing .service-card__body since the two
   are laid out completely differently now. */
.project-card__body {
  position: absolute;
  left: 16px;
  right: 16px;
  bottom: 16px;
  background: var(--color-offwhite);
  border-radius: 0;
  padding: 20px;
}

/* Portfolio page only — a single unified horizontal card, the same pattern
   as the homepage Explore .service-card (real background, its own padding
   wrapping everything, image inset inside rather than floating over/beside
   it) turned sideways: image left, copy right, both sitting inside one
   padded box instead of the floating-overlay treatment .project-card__media
   /__body use everywhere else (homepage Featured, project detail pages).
   .project-card--row itself (the <a>) is now both the flex row AND the
   visible card — background/border/padding/radius live here — and carries
   data-reveal-box-group directly (moved off .project-card__media), so the
   wireframe reveal traces this outer padded shape and fades in its two
   children (.project-card__media, .project-card__body) together, exactly
   like .service-card's own reveal. */
.project-card--row {
  display: flex;
  align-items: stretch;
  gap: 24px;
  background: var(--color-white);
  /* No border any more, per request ("remove the borders on the 2 cards at
     the bottom of project pages") — this card (and its
     .project-card--inverted variant just below) used to carry a grey-light
     rest border that turned berry on hover; the lift + shadow below is now
     the only rest-to-hover distinction this card needs. */
  border-radius: 0;
  padding: 20px;
  color: var(--color-black);
  /* Real (zero-offset, zero-alpha) rest value — see the box-shadow note on
     .service-card above for why this is needed for the hover shadow to
     grow in step with the lift instead of snapping in at full size. */
  box-shadow: 0 0 0 0 transparent;
  /* 1.05s / 0.35s asymmetric pair — now matches .service-card's own hover
     mechanics exactly, per request ("the same hover effect... as the
     homepage 3 vertical service cards"): a lift + growing shadow are added
     below, not just the border/copy colour flip this used to be limited
     to. See the same note on .service-card above for why the slower value
     belongs on the base rule (it only governs the restore-to-rest
     direction). */
  transition: transform 1.05s ease, color 1.05s ease, box-shadow 1.05s ease;
}
/* Hover — same lift + flat "growing out of the page" shadow + berry copy as
   .service-card:hover above, per request (border-colour dropped from both
   this and the base rule's own transition list, since there's no border
   left to animate — see the base rule's own comment). No background change
   needed the way .service-card has one (transparent -> white) — this card
   is already solid white at rest. color:berry cascades to .h2 (no colour of
   its own), but .regular-copy sets its own colour explicitly and needs the
   override below to follow; the chevron is already berry at rest so it
   needs nothing further. */
.project-card--row:hover {
  transform: translateY(-8px);
  color: var(--color-berry);
  box-shadow: 0 5px 0 0 var(--color-berry);
  /* Own (faster) transition for entering hover — see .service-card:hover's
     comment above for why this needs restating here. */
  transition: transform 0.35s ease, color 0.35s ease, box-shadow 0.35s ease;
}
.project-card--row:hover .regular-copy {
  color: var(--color-berry);
}
/* .project-card--row .project-card__media (the old flush-image treatment
   for this row card) has been removed outright — .project-card--row now has
   only one live use left, the berry "Have a project in mind?" CTA (see
   .project-card--row.project-card--inverted further down), which has never
   had a .project-card__media child. Its former other use, each project
   page's own "Next case study" card, has moved onto plain .service-card
   instead — see .project-adjacent-grid .service-card* above and each
   project-*.html's own updated markup — so this rule had nothing left to
   match. */
/* Results label + grey line above the case studies grid, per request ("add
   a label above the image cards saying '# results' which updates depending
   on the tag chosen and a grey line running horizontally from the copy to
   the end of the screen (width to match the 3 col layout below)"). Text
   comes from js/main.js's updateCountHint() via [data-project-count].
   Sits inside the same .container as .portfolio-grid below it, so the row
   (and the line filling the rest of it) naturally matches that grid's own
   width rather than running to the actual screen edge — the parenthetical
   in the request ("width to match the 3 col layout below") is what this is
   scoped to. */
.portfolio-count-row {
  display: flex;
  align-items: center;
  gap: 20px;
  margin-bottom: 30px;
}
/* Font styling matches the "Next case study" eyebrow tag's own type
   (.project-adjacent-grid .service-card__eyebrow — Funnel Display 600,
   12px/18px, 1.8px letter-spacing), per an earlier request ("results
   heading, can this be the same font styling as 'next case study'. Only
   keep the colour as is") — only the font properties were copied that
   round, no background chip, padding or position, since those belong to
   that eyebrow's own pill treatment, not its "font styling". Colour now
   berry per this round's own request ("make # results heading use berry
   font colour"), superseding that earlier round's own explicit "keep the
   colour as is" instruction with --color-grey — was --color-grey. */
.portfolio-count-row__label {
  flex: 0 0 auto;
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 12px;
  line-height: 18px;
  text-transform: uppercase;
  letter-spacing: 1.8px;
  color: var(--color-berry);
  white-space: nowrap;
}
/* Line colour changed to berry per request ("# results line - make it
   berry") — was --color-grey-light, matching the label's own colour
   (already berry, from an earlier round) rather than sitting grey next to
   it. */
.portfolio-count-row__line {
  flex: 1 1 auto;
  height: 1px;
  background-color: var(--color-berry);
}
/* Case studies grid cards — switched from the horizontal .project-card--row
   layout to plain .service-card (the same markup/classes as the homepage's
   3 Explore-section cards), per request ("make the project cards use the
   same layout as the 3 service cards on the homepage - only in rows of 2").
   This block mirrors .explore-content .service-card* almost exactly (see
   that section's own, more detailed comments for the reasoning behind each
   piece) — .portfolio-grid keeps it scoped to this page's own grid, since
   .service-card is also the shared base class services.html doesn't
   currently use unscoped elsewhere.
   Equal-height cards per row, same technique as .explore-content's own
   (display:flex column + height:100%, relying on the grid's own default
   stretch for the row height itself) — a shorter description shouldn't
   leave one card in a row visibly shorter than its neighbour.

   .project-adjacent-grid added to every selector below alongside
   .portfolio-grid, per request ("for the next case study card could we use
   the same structure, layout and hover effects but use the image and name
   tag only from the case study page version (not the copy underneath).
   make it 1/3 col width"): each project detail page's own "Next case
   study" card now reuses this exact .service-card markup (media + caption
   chip only, .service-card__body/.title-tag left out of its markup
   entirely rather than just hidden — see project-*.html) inside a grid
   wrapper carrying this new class, so it gets byte-for-byte the same
   structure/hover feel as the case-studies grid without duplicating any of
   these rules. Only the .service-card__body/.title-tag selectors further
   down are NOT extended to .project-adjacent-grid, since that card's
   markup has no body/title-tag element for them to ever match. */
.portfolio-grid .service-card,
.project-adjacent-grid .service-card {
  display: flex;
  flex-direction: column;
  height: 100%;
  padding: 0;
  border-width: 0 0 1px 0;
  border-color: transparent;
  border-bottom-color: var(--color-grey-light);
  transition: transform 1.05s ease, background-color 1.05s ease, border-color 1.05s ease, color 1.05s ease, box-shadow 1.05s ease;
}
.portfolio-grid .service-card:hover,
.project-adjacent-grid .service-card:hover {
  background-color: var(--color-white);
  border-bottom-color: var(--color-berry);
  color: var(--color-berry);
  /* Matches the -8px lift exactly, same as .explore-content
     .service-card:hover's own 8px (not the base, unscoped
     .service-card:hover's mismatched 5px) — see that rule's own comment for
     why an exact match reads better for a card grid like this one. */
  box-shadow: 0 8px 0 0 var(--color-berry);
  transition: transform 0.35s ease, background-color 0.35s ease, border-color 0.35s ease, color 0.35s ease, box-shadow 0.35s ease;
}
.portfolio-grid .service-card:hover .regular-copy,
.project-adjacent-grid .service-card:hover .regular-copy {
  color: var(--color-berry);
}
.portfolio-grid .service-card__media,
.project-adjacent-grid .service-card__media {
  /* Same proportion as the homepage's own 3 cards, per "same layout"; was
     .project-card--row's old 31/20 inset-image ratio, no longer relevant
     now the image bleeds to the card's own edges like .service-card's
     always have. */
  aspect-ratio: 32 / 24;
  flex: none;
}
/* "Next case study" card only — its own aspect ratio, per request ("Make
   the Next case study card have a 32/18 aspect ratio"), overriding the
   32/24 shared with the case-studies grid above (that grid's own cards are
   untouched). Since this card's markup has no .service-card__body/title
   below the image (see the comment on .project-adjacent-grid further up),
   this media element IS the whole visible card — its aspect-ratio is
   effectively the card's own.
   .project-card--row.project-card--inverted just below gets a matching
   64/18 (= 32/9) ratio for the "Have a project in mind?" card beside it, per
   the same request's "'have a project in mind' card should have the same
   height" — 64/18 is exactly double the WIDTH term of this card's 32/18,
   which lines up with the two cards sitting 2fr/1fr in the same 3-column
   .project-adjacent-grid row (see each project-*.html's markup): at twice
   the width, doubling the ratio's width term keeps the resulting height
   identical to this card's own, with no need to fight or duplicate the
   grid's own default row-stretch behaviour (which still runs as a harmless,
   redundant safety net for the sub-pixel rounding the grid's own 4px gap
   introduces between the two column widths). */
.project-adjacent-grid .service-card__media {
  aspect-ratio: 32 / 18;
}
.portfolio-grid .service-card__body {
  padding: 20px;
  flex: 1 1 auto;
}
/* The project name — was a plain <h2> in the body, before that a floating
   tag over the image — now the .service-card__caption overlay chip, exactly
   like the homepage cards' own service-name chip. .explore-content scopes
   this same treatment for the homepage; .portfolio-grid does the same job
   here so this page's cards get an identical look without touching
   .service-card__caption's own (currently .explore-content-only) base
   rule, which stays specific to that page. */
.portfolio-grid .service-card__caption,
.project-adjacent-grid .service-card__caption {
  position: absolute;
  left: 16px;
  bottom: 16px;
  display: inline-flex;
  align-items: center;
  background: var(--color-offwhite);
  border-radius: 0;
  padding: 10px 18px;
  transition: background-color 0.25s ease, color 0.25s ease;
}
.portfolio-grid .service-card:hover .service-card__caption,
.project-adjacent-grid .service-card:hover .service-card__caption {
  background-color: var(--color-berry);
  color: var(--color-white);
  padding-right: 10px;
}
.portfolio-grid .service-card:hover .service-card__caption .service-card-caption__chevron,
.project-adjacent-grid .service-card:hover .service-card__caption .service-card-caption__chevron {
  width: 16px;
  opacity: 1;
  margin-left: 6px;
}
/* The category tag ("Full Marketing" etc.) stays in the body under the
   copy — this request was only about the overall card treatment, not about
   moving the category tag again (see the matching, now-superseded comment
   just above the old .project-card--row rules this block replaces for that
   tag's fuller history). Scoped the same way as .service-card__caption
   above so the SAME .title-tag class reused elsewhere (each project page's
   own <h1>, the "Client | Category" markup that isn't inside an image)
   stays untouched.
   Simplified to a plain pill per a later request ("remove the display tags
   hover effect and make it a simple pill that changes to berry colour when
   the card is hovered over") — border-radius added so it now reads as a
   true rounded pill rather than the sharp-cornered chip it was; the hover
   effect is now just the one colour flip below, no padding-right shift or
   chevron reveal any more (that chevron span has also been removed from
   each card's markup in case-studies.html, since it no longer does
   anything without the reveal).

   Changed from a solid offwhite fill to an outlined pill per a further
   request ("filter tags in the cards - make these have a border and be
   like the pill on the homepage (with no background colour). on hover
   turn the border and text berry colour") — border/radius/no-background
   shape borrowed from the homepage's own .pill-tag, but the colours follow
   .portfolio-filter__btn's own rest/hover pattern instead of .pill-tag's
   (which sits berry at rest): grey-light border + the base .title-tag
   grey text at rest (background removed so that grey text shows straight
   against the card, same as .portfolio-filter__btn's own white-background
   rest state), border and text flipping to berry only on card hover — so
   there's still a visible colour change to react to, which a permanently-
   berry rest state (copying .pill-tag exactly) wouldn't have given.

   Moved out of the card body and onto the image itself per a further
   request ("move the image card pill tags to the top right of the image
   (make these have minimal padding and have 5px border radius)") — was an
   absolute-positioned chip inside .service-card__media (which already has
   position:relative + overflow:hidden from the base .service-card__media
   rule, same as .service-card__caption's own bottom-left chip below uses
   for its positioning), top-right instead of bottom-left, 16px inset to
   match that chip's own offset, with padding cut right down and
   border-radius dropped to 5px.

   Moved again per a further request ("remove padding, remove the border,
   use the light grey colour, use font size 12px and remove text-transform.
   position it above the supporting copy (outside of the image)") — back
   into .service-card__body (case-studies.html: each <span class="title-
   tag"> now sits as the FIRST child of .service-card__body, right before
   its <p>, rather than after its <img>/.img-placeholder inside .service-
   card__media), so it now reads as a plain label above the description
   instead of a chip on the photo. No longer a pill/chip at all: border and
   padding both removed outright (was 1px grey-light border + 4px/10px
   padding), colour set to the lighter --color-grey-light (was --color-grey,
   inherited from the base .title-tag rule below), and text-transform
   overridden to none (was uppercase, also from that base rule) — font-size
   was already 12px on the base rule, restated here for clarity now this is
   a fuller override. margin-bottom stays the base rule's own 10px (not
   overridden here), giving the usual gap before the copy. No position/
   inset properties any more — it's a normal in-flow element again, first
   in the body's own stacking order. Still flips to berry on card hover
   (now just a colour change, no border left to flip alongside it).

   Latest tweak ("make card filter tags have 0 letter spacing and a darker
   grey"): colour swapped from --color-grey-light to the plain --color-grey
   (darker), and letter-spacing explicitly zeroed (the base .title-tag rule
   below sets its own letter-spacing for the uppercase pill style used
   elsewhere on the site; this override removes it here since this tag is
   no longer uppercase). */
.portfolio-grid .service-card .title-tag {
  border: none;
  padding: 0;
  font-size: 12px;
  text-transform: none;
  letter-spacing: 0;
  color: var(--color-grey);
  transition: color 0.25s ease;
}
.portfolio-grid .service-card:hover .title-tag {
  color: var(--color-berry);
}
/* No background/padding/position of its own any more — those all moved up
   to .project-card--row itself. Just lays the title+copy out, justified to
   the top of the card per request ("'have a project in mind' card - justify
   the title and sub copy to the top") — was justify-content:center (vertical
   centring against the image beside it); now flex-start instead. This also
   sets up the "next case study card have a 32/18 aspect ratio... 'have a
   project in mind' card should have the same height" request just below:
   once this card gets a fixed aspect-ratio (making it taller than its own
   title+copy actually need), top-justifying is what keeps that copy sitting
   naturally under the heading instead of drifting to an odd centred spot in
   the middle of the new, taller box. */
.project-card--row .project-card__body {
  position: static;
  background: none;
  border-radius: 0;
  padding: 0;
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
}
/* .project-card__chevron/.project-card__chevron img (the bare chevron
   icon this card's own body used to end with) have been removed outright,
   per request ("On both the cards at the bottom of the project pages
   remove the current chevrons") — the markup <span> is gone from every
   project-*.html, and with that this was its only remaining use anywhere on
   the site, so the rule had nothing left to match. See .btn-secondary:
   active's own chevron for the same brightness(0)-first berry recipe, if a
   bare chevron like this is needed again elsewhere.
   The mobile (max-width:640px) rules that used to stack .project-card--row,
   flush its image to the top edge, and hide this chevron have also gone —
   .project-card--row now has only one live use (the berry CTA below, no
   image, no chevron), so none of those three rules had anything left to
   do either. */

/* Project detail pages' "Get in touch" CTA and "Next case study" card.
   "Next case study" no longer uses .project-card--row at all — it's been
   rebuilt on plain .service-card (media + name-tag caption only, no body
   copy) per request ("for the next case study card could we use the same
   structure, layout and hover effects but use the image and name tag only
   from the case study page version (not the copy underneath). make it 1/3
   col width") — see .project-adjacent-grid .service-card* above and each
   project-*.html's own updated markup. "Get in touch" stays a
   .project-card--row.project-card--inverted (title/copy, berry background,
   white text, unchanged per that same request's "keep the colour styling"),
   now with a decorative offwhite wireframe background added behind its copy
   — see .project-card--inverted__wireframe below — and its own former bare
   chevron removed per the note just above. */

/* Both "Get in touch" and "Next case study" sit in the same 3-column grid
   row (see the project pages' markup), so the grid's own default
   align-items:stretch already equalises the height of their two flex-col
   *wrapper* divs to whichever is naturally taller. What doesn't happen by
   default is the card <a> itself growing to fill that stretched wrapper —
   flex children don't stretch along the main axis without being told to —
   so without this, the shorter of the two cards would sit with dead space
   below it inside an equally-tall column. flex:1 here (scoped to just this
   two-card row via its shared "flex flex-col gap-6" wrapper, not
   .project-card--row generally, which also covers the unrelated portfolio
   grid) keeps both cards matched in height, per request.
   .service-card added alongside .project-card--row now that "Next case
   study" has moved onto that class instead (see the comment above) — this
   selector otherwise wouldn't reach it any more, and the two cards would
   drift back out of sync in height the next time their copy lengths
   differ. */
.flex.flex-col.gap-6 > .project-card--row,
.flex.flex-col.gap-6 > .service-card {
  flex: 1 1 auto;
  min-height: 0;
}

/* "Get in touch" CTA — colour-inverted variant of .project-card--row: the
   card surface becomes berry and its default copy becomes white (was
   black), per request. Kept as an additional modifier class (rather than
   changing .project-card--row itself) so "Next case study" right beside it
   keeps its normal white-card styling untouched.

   Hover REBUILT per a later request ("remove current hover interaction and
   replace with the same hover interaction as the next case study card") —
   previously dropped the lift+shadow entirely and just turned the copy lime
   (see the git history/prior comment here); now instead borrows
   .project-adjacent-grid .service-card:hover's own lift + growing-shadow
   mechanic verbatim (translateY(-8px), matching 8px shadow offset), just
   with --color-berry-dark in place of plain --color-berry for the shadow
   colour specifically (per that same request: "the colour of the shadow
   under the card can be a dark berry") — this card's own surface IS
   --color-berry, so the ordinary berry shadow the other card gets would all
   but disappear against it; the darker tone stays visible. Copy no longer
   changes colour on hover (this card's white-on-berry palette doesn't have
   a lime-equivalent "next" step the way the white .service-card's black
   text does) — the title-button chip below now carries the hover feedback
   that used to live in the copy colour flip.

   position:relative + z-index:0 + overflow:hidden added per a later request
   ("add a wireframe background in a similar layout to the case studies
   header ... positioned to the right of the card") — the same "own local
   stacking context + clip the canvas to this box" convention used
   throughout this file (.case-studies-window, .hero-window): z-index:0
   contains .hero-canvas-wrap's own z-index:-1 (see
   .project-card--inverted__wireframe below) so it paints behind this card's
   in-flow copy instead of escaping all the way to the page background, and
   overflow:hidden crops .hero-canvas-wrap's own fixed, deliberately-
   oversized min(80vw,787px) canvas down to this card's actual bounds — also
   what keeps the wireframe's own hover zoom (see
   .project-card--inverted__wireframe below) from spilling past the card's
   edge. */
.project-card--row.project-card--inverted {
  position: relative;
  z-index: 0;
  overflow: hidden;
  background: var(--color-berry);
  color: var(--color-white);
  /* 64/18 = double the width term of the "next case study" card's own
     32/18 ratio beside it — see that rule's own comment
     (.project-adjacent-grid .service-card__media) for why this makes the
     two cards' heights match exactly, per request. */
  aspect-ratio: 64 / 18;
}
/* Mobile override — 64/18 above was tuned only for the desktop md:col-span-2
   (2/3-width) column this card sits in beside the 1/3-width "Next case
   study" card (32/18, see .project-adjacent-grid .service-card__media
   above); at mobile's single-column stack both cards share the same 100%
   width, so the untouched 64/18 ratio rendered this one at roughly HALF the
   "Next case study" card's height — clipping the heading/copy/footer inside
   given this card's own overflow:hidden, per report ("can this have enough
   height to make all copy visible. Make it the same height as the 'next
   case study' block"). Matching that sibling's own 32/18 ratio here makes
   both cards the same height again now that they're the same width — no
   change needed to .project-card--row .project-card__body's existing
   top-justified layout, there's simply more room underneath it now. */
@media (max-width: 767px) {
  .project-card--row.project-card--inverted {
    aspect-ratio: 32 / 18;
  }
}
.project-card--row.project-card--inverted:hover {
  transform: translateY(-8px);
  box-shadow: 0 8px 0 0 var(--color-berry-dark);
}
/* The subtitle uses the sitewide .text-grey utility (dark grey, meant for a
   white card) — overridden to a constant white instead, matching the rest
   of this card's copy (no hover colour change any more — see this card's
   own hover comment above). */
.project-card--row.project-card--inverted .text-grey {
  color: var(--color-white);
}
/* The base .project-card--row:hover rule further up turns ALL its
   .regular-copy text berry on hover (for its other, plain-white use
   elsewhere) — this card's own copy (the "have a project in mind?"
   paragraph, and the email/phone beside the button) needs to stay white
   instead, per request ("on hover the email and telephone copy is turning
   berry - can this just stay white on hover"). Higher specificity than
   that base rule (two classes here vs one there) so it wins regardless of
   source order. */
.project-card--row.project-card--inverted:hover .regular-copy {
  color: var(--color-white);
}
/* Decorative wireframe background panel — reuses the exact .hero-canvas-wrap
   mount point/technique the homepage hero and case-studies header both use
   (see initHeroScene in js/main.js and this card's own initHeroScene call in
   each project-*.html), rather than a bespoke canvas setup. Anchored to the
   card's right side per "positioned to the right of the card" (the homepage
   hero's own .hero-canvas-wrap use centres itself instead — this is the same
   mount point, just given an off-centre positioned ancestor here); width
   capped well below the card's own typical width so it reads as an accent
   in the right portion of the card rather than dominating it edge-to-edge,
   similar in spirit to (though smaller than) .case-studies-window__wireframe
   right beside its own copy. No position of its own needed beyond this —
   .hero-canvas-wrap's existing top:50%/left:50%/translate(-50%,-50%) already
   centres the actual canvas within whichever box it's dropped into. */
.project-card--inverted__wireframe {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(45%, 260px);
  pointer-events: none;
  /* Slight zoom on hover, per request ("slight zoom on the image can now be
     a slight zoom on the wireframe") — same scale/timing as the "next case
     study" card's own image zoom (.service-card__media img/.img-placeholder
     transition-transform duration-500 ease-out group-hover:scale-105).
     Scaling this outer element (rather than .hero-canvas-wrap inside it,
     which already carries its own translate(-50%,-50%) centring transform)
     keeps the two transforms from having to be combined by hand; any
     overflow the 5% growth causes is cropped by this card's own
     overflow:hidden (see .project-card--row.project-card--inverted above),
     same as the wireframe's own oversized canvas already relies on. */
  transition: transform 0.5s ease-out;
}
.project-card--row.project-card--inverted:hover .project-card--inverted__wireframe {
  transform: scale(1.05);
}
/* Mobile override — moved to the bottom-right corner, at half size, per
   request ("have a project in mind block - on mobile - move the rotating
   wireframe to the bottom right, make it half the size"). Two separate
   moves are needed for "half the size" to actually shrink the visible
   shape rather than just crop it differently: this box's own width/height
   only ever move the CENTRE POINT .hero-canvas-wrap positions itself
   against (top:50%/left:50%/translate(-50%,-50%) — see that rule's own
   comment), they don't scale the wireframe itself, since .hero-canvas-wrap
   carries its own fixed, independent-of-parent min(80vw,787px) canvas size
   (this card has always just cropped a portion of that fixed-size canvas,
   same as every other .hero-canvas-wrap use except
   .case-studies-window__wireframe's own scoped override). So: the box below
   is shrunk and pinned to the card's bottom-right corner (top/left swapped
   for bottom/right, width/height down to a 45% corner region) to relocate
   WHERE it centres, and a scale(0.5) added to .hero-canvas-wrap's own
   transform (stacked after its existing centring translate, not replacing
   it) is what actually halves the rendered wireframe itself. overflow:
   hidden on the card (.project-card--row.project-card--inverted) still
   crops anything spilling past the card's own edge; the footer row below
   (later in DOM, so painted on top regardless of any overlap here) stays
   legible over it exactly as it already does on desktop. */
@media (max-width: 767px) {
  .project-card--inverted__wireframe {
    top: auto;
    left: auto;
    right: 0;
    bottom: 0;
    width: 45%;
    height: 45%;
  }
  .project-card--inverted__wireframe .hero-canvas-wrap {
    transform: translate(-50%, -50%) scale(0.5);
  }
}
/* Bottom-left footer row — title-button chip + the email/phone contact
   info beside it, per a follow-up request ("Align the email and phone
   number to the right of the get in touch button"): both used to live
   separately (the chip absolutely positioned bottom-left on its own, the
   contact info stacked inside .project-card__body above) — now they're
   flex-row siblings inside this one absolutely-positioned wrapper instead,
   so the contact info sits right beside the chip rather than up in the
   body copy. right:16px (not just left:16px) caps this row's own width to
   the card's inner bounds and lets it wrap onto a second line on a narrow
   card rather than overflowing the edge, should the chip + full contact
   info ever not fit on one line. */
.project-card--inverted__footer {
  position: absolute;
  left: 16px;
  right: 16px;
  bottom: 16px;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 16px;
}
/* Title-button chip — same chip/chevron-reveal pattern as the "next case
   study" card's own .service-card__caption beside it (base rule duplicated
   and scoped here rather than shared, following this file's own
   established convention for each new context — see .portfolio-grid
   .service-card__caption/.explore-content .service-card__caption for the
   same "same look, new scope" pattern), per request ("add a title button
   that on hover adds a chevron (use copy "get in touch) and align it to the
   bottom left plus padding"). Colours flipped from the original offwhite-
   bg/black-text-at-rest → berry-bg/white-text-on-hover pattern (copied
   verbatim from the next-case-study card) to a constant white-bg/berry-text
   in EVERY state, per a follow-up request ("the get in touch button - can
   it have a white background, berry text - on hover can the background stay
   white, copy stay berry and chevron be berry") — no colour transition
   needed any more, only the chevron itself still reveals on hover (that
   part of the mechanic is unchanged). position:static (was absolute) now
   that .project-card--inverted__footer above owns the positioning instead.
   .service-card-caption__chevron itself is already a generic, unscoped
   class, reused as-is; only its icon colour needs a scoped override here
   (see below) since its own base rule forces plain white, which would
   vanish against this chip's own white background. */
.project-card--row.project-card--inverted .service-card__caption {
  position: static;
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  background: var(--color-white);
  color: var(--color-berry);
  border-radius: 0;
  padding: 10px 18px;
}
.project-card--row.project-card--inverted:hover .service-card__caption {
  padding-right: 10px;
}
.project-card--row.project-card--inverted:hover .service-card__caption .service-card-caption__chevron {
  width: 16px;
  opacity: 1;
  margin-left: 6px;
}
/* Chevron icon colour — berry instead of the generic .service-card-
   caption__chevron img base rule's plain white (see that rule's own
   comment: solved for sitting on a berry hover-fill elsewhere, which isn't
   this chip's own look any more). Same tuned black-source-to-berry filter
   recipe used throughout this file (independent brightness(0) first, so it
   reliably overrides the base rule's own filter rather than combining with
   it). */
.project-card--row.project-card--inverted .service-card-caption__chevron img {
  filter: brightness(0) invert(11.6%) sepia(88.4%) saturate(593.5%) hue-rotate(291.5deg) brightness(235.5%) contrast(129.6%);
}
/* "Next case study" eyebrow tag — used to be a plain <span class="h4"> sat
   above this card (mirroring "Get in touch"'s own removed label above the
   CTA card beside it), moved onto the image itself per a follow-up request
   ("Remove get in touch label and move next case study label to inside the
   image card - find a good way to display and position it"). Top-left,
   opposite corner from that same image's own .service-card__caption
   name+chevron chip at bottom-left, so the two labels never compete for the
   same corner. Plain, static label (no hover treatment of its own) — unlike
   the caption chip beside it, this one isn't a second call-to-action, just
   context for what the card below it is. */
.project-adjacent-grid .service-card__eyebrow {
  position: absolute;
  top: 16px;
  left: 16px;
  /* White text on black, per request ("make 'next case study' tag have
     white text and black background colour") — was offwhite bg/grey text. */
  background: var(--color-black);
  padding: 6px 14px;
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 12px;
  line-height: 18px;
  letter-spacing: 1.8px;
  text-transform: uppercase;
  color: var(--color-white);
}
/* Removes this card's own default (rest-state) bottom border, per request
   ("remove default bottom border from the next case study card") —
   overrides the shared .portfolio-grid/.project-adjacent-grid .service-card
   rule's own border-bottom-color:grey-light with transparent, the same
   "reserve the 1px but make it invisible" convention that rule already uses
   for its rest state (avoids a layout shift when the border reappears on
   hover). The hover-state berry border-bottom-color is untouched — this
   request was specifically about the DEFAULT border, and that hover colour
   change is this card's own existing hover feedback, shared with the
   case-studies grid's cards, not a leftover default border. */
.project-adjacent-grid .service-card {
  border-bottom-color: transparent;
}
/* Divider between the email and phone number, now sharing one row per
   request ("put the email and phone number on one row with a divider in
   between them"). background:currentColor rather than a fixed colour so
   it automatically follows the same white-at-rest/lime-on-hover copy
   colour above it, with no separate hover rule of its own needed. */
.contact-divider {
  width: 1px;
  height: 14px;
  background-color: currentColor;
  opacity: 0.4;
  flex-shrink: 0;
}

/* --------------------------------------------------------------------------
   9b0. SERVICES PAGE — top band
   Used to carry a full-bleed white background behind the hero window and
   the 5-shortcut lede row beneath it, per an earlier request ("contain the
   top sections in a white container (down to below the 5 shortcut links)").
   Removed per a later request ("remove the white background colour in the
   top section") — the wrapper element itself is kept (services.html still
   wraps both of those sections' own full .container elements in it), since
   removing the div would mean re-touching that markup for no benefit; it
   now simply carries no background of its own, so the sitewide page
   background (--color-grey-lighter, set on <body>) shows through instead. */
.services-top-band {
}

/* --------------------------------------------------------------------------
   9b1. SERVICES PAGE — Hero (wireframe window)
   Replaces the previous plain left-aligned H1 ("Services") plus a separate
   INTRO section below it (h2 subtitle + intro paragraph), per request
   ("rework the hero to use a wireframe container like on the other pages...
   2 x rotating berry wireframes in this one (use the cube) one on the left
   and one on the right with the copy all centered in the middle. Copy
   includes 'Services', the subtitle and supporting copy, all center
   aligned"). Modelled directly on .hero-window/.case-studies-window (same
   border/overflow/min-height conventions, same 56px/48px copy padding) —
   see those rules' own comments for the mechanics this borrows rather than
   reinvents (position:relative+z-index:0 for the wireframe's stacking
   context, overflow:hidden as the "window" crop, min-height:360px as the
   shared target height for this component type sitewide).
   Three flex children instead of .case-studies-window's two: a wireframe
   panel, the copy, then a second wireframe panel, in that DOM/reading order,
   so the copy sits centred between both per the request's own "one on the
   left and one on the right ... copy ... centered in the middle" wording —
   .case-studies-window only ever needed one wireframe (copy left, wireframe
   right), so this is a genuinely new arrangement rather than a copy-paste of
   that one.
   Border colour was berry (--color-berry) at first, a one-off for this
   component family (.hero-window/.case-studies-window are both white-
   bordered — see their own colour-history comments). Switched to white
   per a later request ("make the wireframe window border colour white,
   wireframe colours white and the wireframe window background colour
   off-white") — bringing it back in line with that shared family's usual
   white border, with the window's own fill now off-white (--color-offwhite,
   was plain white) so it still reads as its own distinct panel rather than
   disappearing into the new .services-top-band white wrapper it now sits
   inside (see that class below) — a white-on-white window would have had
   no visible edge at all. The two cube wireframes themselves are recoloured
   white via the initHeroScene calls near the end of this page. */
.services-hero-window {
  position: relative;
  z-index: 0;
  border: 8px solid var(--color-white);
  background: var(--color-offwhite);
  overflow: hidden;
  display: flex;
  align-items: stretch;
  min-height: 360px;
}
@media (max-width: 1023px) {
  .services-hero-window {
    flex-direction: column;
    min-height: 0;
  }
}
/* Centre column — "the copy all centered in the middle... all center
   aligned": align-items:center (cross-axis) + text-align:center (wraps text
   within each line) together centre the H1/subtitle/paragraph both as a
   block and within their own width, matching the request's "all center
   aligned" literally rather than just centring the column as a whole. flex:1
   takes whatever width the two fixed-width wireframe panels beside it don't
   need, same "flexible middle, fixed sides" logic as .case-studies-window__
   copy's own flex:1 (there, alongside one fixed wireframe column instead of
   two). */
.services-hero-window__copy {
  flex: 1 1 0%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 24px;
  padding: 56px 48px;
  text-align: center;
  min-width: 0;
}
@media (max-width: 1023px) {
  .services-hero-window__copy {
    padding: 40px 24px;
  }
}
/* Side wireframe panels — fixed/capped width rather than a flex fraction,
   same reasoning as .case-studies-window__wireframe (keeps each canvas
   close to square against the window's own height instead of stranded with
   dead space in an overly wide flexible column). Smaller cap than that
   rule's own min(38vw,420px) since there are now TWO of these sharing the
   row with the copy instead of one — min(22vw,280px) keeps both comfortably
   inside their own panel while still leaving the centre copy column the
   larger share of the window's width. */
.services-hero-window__wireframe {
  position: relative;
  flex: 0 0 auto;
  width: min(22vw, 280px);
}
/* Mobile: same treatment as .case-studies-window__wireframe's own mobile
   override — taken out of the flex flow, position:absolute+inset:0 layering
   both wireframe panels directly behind the now solely height-determining
   copy column instead of stacking as their own flex rows. With both panels
   sharing the exact same centred canvas position (.hero-canvas-wrap's own
   centring, untouched), the two rotating cubes simply overlap into what
   reads as one centred wireframe behind the copy — the same "wireframe
   backdrop" look .hero-window uses natively, arrived at here via two reused
   panels rather than a bespoke mobile-only markup path. */
@media (max-width: 1023px) {
  .services-hero-window__wireframe {
    position: absolute;
    inset: 0;
    width: auto;
  }
}

/* --------------------------------------------------------------------------
   9b2. SERVICES PAGE — Exterior/Interior intro (slider + heading + copy)
   Was 2 alternating left/right text+image rows, then a 2-column .service-
   duo grid (Exterior/Interior side by side, each stacking its own before/
   after slider above its own heading+copy). That outer 2-column grid
   (.service-duo) is gone — Exterior and Interior no longer sit side by
   side, each is its own full-width .service-detail section (slider+copy
   intro leading into that topic's own accordion below, per services.html's
   own comment). .service-duo__item itself has swung back and forth a
   couple of times since: standalone flex-column stack, briefly promoted to
   a whole left/right split against the accordion, now settled per request
   ("revert back to the row version. But put the slider and headline and
   supporting copy in columns above the accordion in each section") as a
   2-column row IN ITS OWN RIGHT — slider in one column, heading+copy in
   the other, side by side, sitting above the full-width accordion rather
   than splitting against it. Same media/text flex ratio (1 : 1.3) as
   .service-split below, since it's the same "image beside copy" idea, and
   the same 768px stack-to-column breakpoint for consistency across the
   page. .before-after's own aspect-ratio always has a real width to
   compute height from regardless of what kind of parent it sits in. */
.service-duo__item {
  display: flex;
  align-items: center;
  gap: 64px;
}
.service-duo__media {
  flex: 1 1 0;
  min-width: 0;
}
.service-duo__text {
  flex: 1.3 1 0;
  min-width: 0;
}
@media (max-width: 768px) {
  .service-duo__item {
    flex-direction: column;
    gap: 24px;
  }
  .service-duo__media,
  .service-duo__text {
    flex: none;
    width: 100%;
  }
}

/* Before/after compare slider: two full-bleed photos in the same box, the
   top one ("before") revealed only up to --reveal via clip-path rather
   than a shrinking wrapper — clip-path leaves the image itself at its
   full native size throughout, so it never squashes/stretches as --reveal
   changes the way a width-based wrapper would. Dragging is handled by
   initBeforeAfterSliders() in js/main.js: pointer events on THIS element
   (not the handle) so the whole image is draggable, not just the thin
   handle — the handle/divider both have pointer-events:none so they never
   intercept the drag themselves. */
.before-after {
  position: relative;
  aspect-ratio: 31 / 20;
  border-radius: 0;
  overflow: hidden;
  cursor: ew-resize;
  -webkit-user-select: none;
  user-select: none;
  touch-action: pan-y; /* leaves vertical page-scroll alone; horizontal drags are read by JS */
  --reveal: 50%;
}
.before-after__img {
  position: absolute;
  inset: 0;
}
.before-after__img img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  pointer-events: none;
}
/* Only the "before" layer is clipped — "after" sits underneath at full
   size throughout, so whatever --reveal doesn't show of "before" reveals
   "after" beneath it automatically, with no clip-path of its own needed. */
.before-after__img--before {
  clip-path: inset(0 calc(100% - var(--reveal)) 0 0);
}
.before-after__divider {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--reveal);
  /* 3px -> 1px sitewide, per request ("all image sliders across the
     website, can the slider bar be 1px wide") — this one shared class
     drives every before/after slider on the site (services.html's
     Exterior/Interior sliders, the padel project page's aerial gallery
     slider), so a single change here reaches all of them with nothing
     page-specific needed. */
  width: 1px;
  background: var(--color-white);
  box-shadow: 0 0 8px rgba(15, 15, 15, 0.35);
  transform: translateX(-50%);
  pointer-events: none;
}
.before-after__handle {
  position: absolute;
  top: 50%;
  left: var(--reveal);
  transform: translate(-50%, -50%);
  width: 48px;
  height: 48px;
  border-radius: 0;
  background: var(--color-white);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4px 14px rgba(15, 15, 15, 0.3);
  pointer-events: none;
}
/* caret-circle-left-right-fill.svg — a flat-white drag-handle icon, per
   request ("can this be the icon in the middle of the vertical slider
   bar... berry coloured") — recoloured with the same numerically-solved
   brightness(0)-first recipe used throughout this file for a flat-white
   source SVG, rather than a second one-off filter.
   Selector bumped from ".before-after__handle img" to
   ".before-after .before-after__handle img" per request ("the chevron
   icons are much bigger in the padel sliders than on the services page -
   can we make the size consistent") — on the padel gallery this handle
   sits inside .project-card__media, whose own "real photography fills
   these containers edge-to-edge" img rule (width:100%;height:100%, see
   9a-ish block below) carried equal specificity and happened to come
   later in the file, so it was winning and stretching the icon to fill
   the full 48px handle instead of staying at 24px — services.html's
   .service-columns__media wrapper has no such competing img rule, which
   is why only the padel page's icon looked oversized. The extra
   .before-after ancestor class here raises this rule's specificity
   above that one regardless of source order, so the fix holds sitewide
   without relying on where either rule happens to sit in the file. */
.before-after .before-after__handle img {
  width: 24px;
  height: 24px;
  filter: brightness(0) invert(11.6%) sepia(88.4%) saturate(593.5%) hue-rotate(291.5deg) brightness(235.5%) contrast(129.6%);
}

/* Small kicker title ("The Unbuilt Suite") sitting above the 5-column lede
   row, per request ("before the chapter links can we have a title like
   'the unbuilt suite'"). Reuses .h4's shared small-caps label style (12px,
   600 weight, 1.8px letter-spacing — the same recipe as
   .portfolio-count-row__label on the case studies page) rather than a new
   size, coloured berry to match that same "# results" kicker rather than
   .service-detail__eyebrow's grey (that one sits as a secondary label
   BELOW a real h1 elsewhere on this page; this is the only heading this
   row has, so it gets the brand-colour treatment instead).
   Centred with a line either side per a follow-up request ("center 'The
   Unbuilt Suite' and draw a line either side of it (styled like the #
   results line)") — .service-lede__eyebrow-row is the flex wrapper (label
   fixed-width in the middle, two .service-lede__eyebrow-line elements
   filling the remaining space either side), reusing
   .portfolio-count-row__line's own 1px height recipe for the lines rather
   than a new one. Colour changed to berry per a further request ("make the
   lines either side of 'the unbuilt suite' berry") — was --color-grey-light
   (deliberately left grey at the time even after .portfolio-count-row__line
   itself turned berry on the case studies page, since that earlier request
   was scoped there only — this one now asks for it here too). margin-bottom
   moved onto the row now that the label itself sits inside it. */
.service-lede__eyebrow-row {
  display: flex;
  align-items: center;
  gap: 20px;
  margin-bottom: 24px;
}
.service-lede__eyebrow-line {
  flex: 1 1 auto;
  height: 1px;
  background-color: var(--color-berry);
}
.service-lede__eyebrow {
  flex: 0 0 auto;
  color: var(--color-berry);
  margin-bottom: 0;
}

/* --------------------------------------------------------------------------
   9b3. SERVICES PAGE — 5-column lede
   Replaces the old logo marquee, per request — a scannable icon + one-line
   summary per service, each linking down to its fuller section below. Icons
   are flat brand SVGs (building-office/couch/film-strip/virtual-reality/
   drone.svg) rather than the live wireframe polygons used first time round
   — recoloured berry with the same numerically-solved brightness(0)-first
   filter recipe used throughout this file (see .before-after__handle img
   above for the same recipe on a different flat-white source SVG).
   Title/description typography now matches the portfolio page's own
   project-card hierarchy — h3 title + regular-copy description, both
   turning berry together on hover (see .project-card--row:hover and
   .project-card--row:hover .regular-copy for the pattern this mirrors) —
   per request, rather than the small-caps label style used first time
   round. Clicking a link smooth-scrolls rather than jumping — see
   initSmoothAnchorLinks in js/main.js; scroll-margin-top on each target
   section (.service-duo__item, .service-split, .aerial-feature below)
   keeps the fixed header clear of the heading being scrolled to. */
/* Hover now reuses .service-card's own "lift + flat dropshadow + white fill"
   recipe, per request ("can these have the same hover effect the cards do.
   So dropshadow appears and the content container slightly raises,
   background colour becomes white, copy becomes red") — same -8px lift and
   0 5px 0 0 berry shadow values as .service-card:hover, just applied to
   this flex column directly rather than a bordered card, since this
   component never had its own border/background at rest. padding/
   background/box-shadow are all declared here (at rest, transparent/no-
   shadow) rather than only on the :hover rule below, so the box's own
   size is reserved up front and the hover transition doesn't jump the
   surrounding grid row around it when the padding "appears". */
.service-lede__item {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
  padding: 24px 20px;
  text-align: center;
  color: inherit;
  background-color: transparent;
  box-shadow: 0 0 0 0 transparent;
  /* Thin grey border-bottom for the default (rest) view, per request ("add
     a thin grey border below each of the 5 shortcut links for the default
     view (like in the case study cards)") — same "reserve the 1px space,
     zero out the other 3 sides by colour rather than width" convention the
     case-studies grid's own .service-card uses (see .portfolio-grid
     .service-card above), and the same rest colour (--color-grey-light).
     Turns berry on hover below, exactly like that same card pattern,
     rather than clashing with the lift/shadow hover treatment already in
     place here. */
  border-width: 0 0 1px 0;
  border-style: solid;
  border-color: transparent;
  border-bottom-color: var(--color-grey-light);
  transition: transform 0.35s ease, background-color 0.35s ease, box-shadow 0.35s ease, border-color 0.35s ease;
}
.service-lede__item:hover {
  transform: translateY(-8px);
  background-color: var(--color-white);
  box-shadow: 0 5px 0 0 var(--color-berry);
  border-bottom-color: var(--color-berry);
}
.service-lede__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
}
.service-lede__icon img {
  width: 40px;
  height: 40px;
  filter: brightness(0) invert(11.6%) sepia(88.4%) saturate(593.5%) hue-rotate(291.5deg) brightness(235.5%) contrast(129.6%);
  transition: transform 0.3s ease;
}
/* Icon still scales up slightly on hover — colour itself no longer changes
   on hover, per request ("for the hover state don't change the icon
   colours"); the border-bottom below now carries the hover cue instead. */
.service-lede__item:hover .service-lede__icon img {
  transform: scale(1.1);
}
.service-lede__title {
  transition: color 0.3s ease;
}
.service-lede__item:hover .service-lede__title {
  color: var(--color-berry);
}
.service-lede__item:hover .regular-copy {
  color: var(--color-berry);
}
/* Small chapter number (01–05, matching each link's own section further
   down the page) — briefly sat crossfaded with the chevron near the
   bottom of each item (see this rule's own git history in earlier rounds),
   moved up above the icon per a follow-up request ("quick links - move the
   numbers to above the icons"). Now a plain first child of .service-lede__
   item (a flex column, align-items:center, gap:20px — see above), so it's
   centred and spaced for free with no positioning of its own needed;
   the chevron below (next rule) no longer shares this slot, so it's back
   to a simple standalone hover reveal rather than a crossfade partner. */
.service-lede__num {
  display: block;
}
/* Chevron (caret-down.svg) moved from under the icon to under each copy
   block, per request ("move the caret down... to be under each copy").
   Sits inside the same gap-2 flex column as the title/description, right
   after the paragraph, rather than the item's own larger 20px gap. Own
   filter now (was inherited via .service-lede__icon img when nested
   there) — same berry recipe, unchanged at rest or on hover, matching the
   icon's own no-colour-change-on-hover behaviour above.
   Hidden at rest, fades/slides in on hover — per an earlier request
   ("chevron appears below this time") — back to this plain version (no
   absolute-position crossfade) now that the chapter number above has moved
   away from this slot. */
.service-lede__chevron {
  width: 16px !important;
  height: 16px !important;
  margin-top: 4px;
  filter: brightness(0) invert(11.6%) sepia(88.4%) saturate(593.5%) hue-rotate(291.5deg) brightness(235.5%) contrast(129.6%);
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity 0.3s ease, transform 0.3s ease;
}
.service-lede__item:hover .service-lede__chevron {
  opacity: 1;
  transform: translateY(0);
}
/* Border-bottom hover cue — RETIRED per this round's new card-style hover
   above (dropshadow + lift + white fill already gives this item a much
   bolder hover cue of its own; a thin underline drawn just past the item's
   own edge would either sit oddly under a now-raised white box or get
   hidden beneath it). Rule kept below, just permanently switched off via
   the item's own now-hidden ::after content, rather than deleted outright,
   in case a future request wants the underline motif back on top of the
   card treatment. Was the same "draws in from the left" motif the main nav
   menu's own links used at the time this was built ("a border bottom drawn
   on hover, similar to the main nav menu hover effect") — the nav's own
   hover effect has since changed several times more (a full-viewport-width
   background band, then a .btn-primary-style white/berry fill, now a plain
   berry-background-only hover — see .site-nav__link's own comment). Sits
   under the whole card rather than just the title, since this item is a
   block (icon + title + copy + chevron) rather than a single line of text
   like the nav link. Berry (not lime, per request)
   — matches the title/copy's own hover colour above instead of the nav
   link's lime. */
.service-lede__item::after {
  content: none;
}
/* Mobile scrollable single-row carousel — RETIRED per request ("can we fix
   the 5 shortcut links at the top of the page - put these into rows"), which
   asked for the opposite of what this block did (one long horizontally-
   scrolling row) — the 5 items now wrap into proper stacked rows on mobile
   instead. Left here commented out rather than deleted, same "retire, don't
   delete" convention used elsewhere in this file, in case a scrollable
   mobile row is wanted again later. Its old rules, for reference:

   @media (max-width: 639px) {
     .service-lede__row {
       display: flex !important;
       overflow-x: auto;
       overflow-y: hidden;
       scroll-snap-type: x proximity;
       -webkit-overflow-scrolling: touch;
       margin-inline: -24px;
       padding-inline: 24px;
       padding-bottom: 20px;
     }
     .service-lede__row::-webkit-scrollbar { display: none; }
     .service-lede__row { scrollbar-width: none; }
     .service-lede__row .service-lede__item {
       flex: 0 0 auto;
       width: 180px;
       scroll-snap-align: start;
     }
     .service-lede__row { cursor: grab; }
     .service-lede__row.is-dragging { cursor: grabbing; }
   }

   Removing this hands mobile straight back to the plain Tailwind utility
   classes already on .service-lede__row in the markup (grid grid-cols-2
   sm:grid-cols-3 lg:grid-cols-5 gap-[1px]) — a 2-column grid below the sm
   breakpoint, wrapping the 5 items into rows of 2/2/1 — with nothing here
   left to fight it for `display`, so the !important the old rule needed is
   no longer needed either. initDragScroll(document.querySelector(
   '.service-lede__row')) in js/main.js is now inert on mobile rather than
   removed — it already no-ops whenever el.scrollWidth <= el.clientWidth
   (nothing to drag-scroll), which is always true now that the row no
   longer overflows horizontally; still fine to leave wired up for the
   desktop `.portfolio-sidebar` case it also handles. */

/* Mobile row layout — a further pass on top of the 2/2/1 grid the removal
   above left behind, per a follow-up request ("can the mobile shortcut
   links have their layouts updated to be rows instead of blocks. can it go
   number | icon | then title and supporting copy | chevron - see how that
   fits"): the 2-column grid of centred, vertically-stacked "block" cards
   becomes a single column of full-width rows, each one laid out
   left-to-right instead — number, then icon, then the title+copy stacked
   and left-aligned, then the chevron pinned to the row's own right edge.
   grid-template-columns: 1fr needs !important for the same reason the old
   carousel rule above did (see that block's own comment) — fighting
   Tailwind's own grid-cols-2 utility class for the same property at equal
   specificity, with the live site's Tailwind Play CDN stylesheet not
   guaranteed to stay ordered before this file's own.
   .service-lede__item flips from its shared (all-breakpoints)
   flex-direction:column/align-items:center/text-align:center (see that
   rule's own definition above) to a row here; .service-lede__copy (the
   title+copy+chevron wrapper — a new class added to services.html's own
   markup purely so this rule has something stable to target, no structural
   change) follows suit for its own two text children, and gets its own
   position:relative + right-side padding so the chevron below can be
   pulled out of flow and pinned to the row's true right edge without
   disturbing the title/copy's own left-aligned flow beside it. */
@media (max-width: 640px) {
  .service-lede__row {
    grid-template-columns: 1fr !important;
  }
  .service-lede__item {
    flex-direction: row;
    align-items: center;
    text-align: left;
    gap: 16px;
    padding: 16px 20px;
  }
  .service-lede__copy {
    position: relative;
    align-items: flex-start;
    text-align: left;
    flex: 1 1 auto;
    min-width: 0;
    padding-right: 28px;
  }
}

/* --------------------------------------------------------------------------
   9b4/9b5. SERVICES PAGE — media feature bands (Animation / Virtual Tours /
   Aerial Footage)
   Animation and Virtual Tours were two side-by-side "alternating" rows
   (.service-split/.service-split--reverse — image one side, copy the
   other, GIF just sitting plainly beside the text) — restyled to match the
   Aerial Footage section's own look instead, per request ("Animation and
   Virtual Tours sections, can these be updated to look like the aerial
   footage section"): each is now a full-width rounded band with its GIF as
   a cover-fit background, a dark scrim, and centred white heading+copy —
   .service-split/.service-split--reverse (and .service-split__media/__text)
   are gone outright, replaced by this shared component on all three
   sections rather than kept as unused dead classes.
   Renamed from .aerial-feature to the more generic .media-feature now that
   three different topics share it (Aerial Footage's own markup below has
   been updated to match) — same scrim/white-text approach as the site's
   photo heroes (.hero img.hero__bg + .hero__scrim) either way, just with
   an <img> (a looping GIF, for Animation/Virtual Tours, or a still) or a
   <video> (Aerial Footage's real drone footage) standing in as the
   background media, both absolutely covering the same box beneath the
   scrim/content layers. aspect-ratio: 16/9 suits a GIF/image band; Aerial
   Footage keeps its own taller 32/9 via .media-feature--wide (see that
   modifier below) to match its existing video's own proportions exactly,
   unchanged from before this rename. */
.media-feature {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  width: 100%;
  aspect-ratio: 16 / 9;
  min-height: 320px;
  border-radius: 0;
  overflow: hidden;
}
/* Aerial Footage only — taller band matching its own video's proportions,
   unchanged from this component's pre-rename .aerial-feature sizing (see
   this section's own history in earlier rounds for why 32/9 specifically:
   doubled once already per a direct request to increase the video band's
   height). */
.media-feature--wide {
  aspect-ratio: 32 / 9;
  min-height: 420px;
}
/* Reduced on mobile per request ("reduce the height of the section hero
   videos on mobile") — below this breakpoint aspect-ratio alone would
   already bring both variants in well under their own min-height (a
   390px-wide phone at 32/9 only wants ~110px), so min-height is what was
   actually forcing every one of these bands to their full desktop-sized
   height on a phone; these two lower values are what mobile is really
   rendering at instead. */
@media (max-width: 640px) {
  .media-feature {
    min-height: 260px;
  }
  .media-feature--wide {
    min-height: 300px;
  }
}
.media-feature__bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.media-feature__scrim {
  position: absolute;
  inset: 0;
  background: rgba(15, 15, 15, 0.45);
}
.media-feature__content {
  position: relative;
  z-index: 1;
  max-width: 640px;
  padding: 0 24px;
}
/* Exterior/Interior Visualisation intro bands — per request ("for the
   'exterior visualisation' copy overlay it onto an image - use the same
   layout as aerial footage (though this uses a video instead)... Do the
   same for interior visuals section"), reusing this exact .media-feature/
   --wide component with a plain <img> standing in for Aerial Footage's own
   <video>, rather than building a second image-band component from
   scratch. Interior Visuals.jpg is a square (2000x2000) source photo —
   object-fit:cover's default centred crop at this band's 32:9 ratio cuts
   straight through mostly ceiling and wall, missing the bed and dining
   nook that actually make the room read (checked against a real center-
   crop render, not assumed) — shifted down the source photo instead so
   both sit inside the visible band. Exterior Visuals.jpg's own proportions
   already suit a centred crop with no override needed. */
.media-feature--interior-visuals .media-feature__bg {
  object-position: center 75%;
}

/* Anchor targets for the 5-column lede's links above — a consistent
   scroll-margin-top on each so the fixed .site-header never covers the
   heading being scrolled to, whether the browser jumps straight there or
   initSmoothAnchorLinks (js/main.js) scrolls it into view smoothly.
   #exterior-visualisation/#interior-visualisation (was .service-duo__item,
   back when the id sat on that div directly) now target the combined
   .service-detail wrapper each id moved to — see services.html's own
   comment on why. .service-split replaced by .media-feature above (now
   the target for #animation/#virtual-tours too, alongside #aerial-footage
   which already used this rule). */
#exterior-visualisation,
#interior-visualisation,
.media-feature {
  scroll-margin-top: 110px;
}

/* --------------------------------------------------------------------------
   9b6. SERVICES PAGE — expanded service detail (v2 redesign)
   First pass (per "lay the content out and design it up as you see fit")
   put all of this new copy into plain text grids — a flat 2x2 of paragraph+
   bullet-list cards for Exterior/Interior, and a checklist+3-column list for
   Aerial Footage/Virtual Tours. Reads fine but is a lot of grey text in a
   row with nothing to look at, for a studio whose whole product is imagery.
   Redesigned per a follow-up request to review it with fresh UX/UI eyes:

   - Exterior/Interior's 4 sub-services each become a .service-accordion
     item — collapsed by default (bar first item, open so the pattern is
     obvious on load) so the section reads as 4 scannable titles instead of
     4 full paragraphs stacked at once, with a placeholder image thumbnail
     on every row so the list has something to look at even collapsed. Built
     on native <details>/<summary> — no JS needed, keyboard/AT accessible
     for free, degrades to "just readable" with CSS off.
   - "Key Benefits" (Aerial Footage/Virtual Tours) moves from a plain
     checklist to .benefit-tiles — bordered cards on the page's white .card
     surface, so it reads as a featured grid rather than a form.
   - "Supporting Figures" moves from 3 plain stat lists to .stat-band cards,
     each pulling its group's first figure out as a large --font-display
     headline number (the same display font the site uses for big
     standalone numerals) with that stat's full sentence underneath as a
     caption and the rest of the group's stats below as a normal .stat-list
     — turns "a list with 3 numbers in it" into 3 numbers that actually look
     like the headline they are.
   - New .service-banner-media placeholder image breaks up the Aerial
     Footage/Virtual Tours paragraph-then-list run with a 21:9 banner, using
     the same .img-placeholder "swap me" pattern already used across the
     project pages for missing photography, so these slot in identically
     once real stills/screenshots exist.

   .stat-list itself is unchanged from the first pass (still used inside
   both the accordion panels and the new stat-band cards); .service-detail
   is still the shared top-border/spacing wrapper around all of this,
   whichever pattern it holds.
   -------------------------------------------------------------------------- */
/* border-top removed per request ("remove border line before exterior
   visualisation and interior... remove this line on other sections too") —
   was a 1px grey-light rule sitting above every .service-detail section
   (Exterior, Interior, and the Virtual Tours/Aerial Footage "in detail"
   blocks below their own video/GIF bands).
   margin-top:56px/padding-top:48px REMOVED per a follow-up request ("make
   the padding between sections equal down the page") — .service-detail now
   wraps each ENTIRE major section (chapter number + video/GIF band + all of
   that topic's own detail content underneath, sub-columns or benefits-row/
   stat-band alike — see services.html, Virtual Tours/Aerial Footage were
   restructured this round to match Exterior/Interior/Animation's existing
   single-wrapper pattern instead of splitting the band and its "in detail"
   copy across two separate top-level items). That extra 104px was stacking
   ON TOP OF the outer 80px flex gap (.flex.gap-20 further up the page)
   wherever .service-detail sat as a flex child of it — Exterior→Interior
   read as 184px while Interior→Animation/Animation→Virtual Tours read as
   80px, and Virtual Tours' own video-to-copy gap read as a further 184px
   internally. Dropping it makes every one of those gaps resolve to the same
   80px (from the shared flex gap / each section's own container padding),
   with each section's OWN internal video-to-content spacing now handled
   consistently by this rule's own gap:24px below instead — see the same
   request's other half ("tighten up padding between virtual tours header
   video and the copy underneath... same for aerial footage"). */
.service-detail {
  display: flex;
  flex-direction: column;
  gap: 24px;
}

/* Small connective label ("Explore our Exterior Visualisation services")
   sitting between a service's top intro and its accordion — originally
   .h4's small-caps label style (already used for the footer/nav contact
   details) rather than a second full h2, so it didn't compete with the
   real section heading above it. Later reused for the Animation/Virtual
   Tours/Aerial Footage "in detail" subtitles ("The Same 3D Assets, In
   Motion" etc, added in a follow-up round) — switched from .h4 to .h3 in
   the markup per a further request ("use the same font style as
   'Marketing & Sales Interior Visualisations'", one of the Interior
   section's own sub-service titles, itself a plain .h3 — see
   .service-columns__title above) — colour changed to match that same
   title's own black (was --color-grey, sized/cased for the smaller .h4
   label this class used to pair with). */
.service-detail__eyebrow {
  color: var(--color-black);
}

/* Chapter markers (01-05) ahead of each major section's own heading —
   Exterior/Interior/Animation/Virtual Tours/Aerial Footage, matching the
   5-column lede's own order — per request ("can we use the same font as
   'From concept to completion' and create clear chapter markers down the
   page. Use numbers for each section and format like 01, 02, 03 etc using
   Bebas Neue font for the numbers"). "From concept to completion" is
   already a plain .h2 (Funnel Display, --font-heading) exactly like every
   one of these 5 section headings, so no font change was needed on the
   heading itself to match it — the actual new piece is just the number,
   which deliberately uses --font-display (Bebas Neue) instead, per the
   request's own second half. --color-berry-dark for the plain-background
   headings (Exterior/Interior/Animation) reads clearly against white/off-
   white without competing with the berry accents already used for links
   and CTAs nearby; --modifier switches to white for the two sections that
   sit on a dark/scrimmed background (Virtual Tours' and Aerial Footage's
   own bands — see .chapter-marker--on-dark below and 9b4/9b5's own
   comments for that redesign). */
.chapter-marker {
  display: flex;
  align-items: baseline;
  gap: 18px;
}
.chapter-marker__num {
  flex-shrink: 0;
  font-family: var(--font-display);
  font-size: clamp(32px, 4vw, 44px);
  line-height: 1;
  color: var(--color-berry-dark);
}
.chapter-marker--on-dark .chapter-marker__num {
  color: var(--color-white);
}

/* Stat list — the 3-4 supporting-figure lines inside each stat-band card
   (previously also used inside the now-removed .service-tabs accordion
   panels — see 9b7 above). Marker was a short solid berry dash, then an
   outlined rounded bullet ("instead of - bullet points can we use outlined
   rounded bullet points" — a hollow berry ring, border only, transparent
   centre), now a solid filled circle per a further request ("make the
   circular outline bullet points a solid fill bullet point") — same size/
   colour/position, just background:berry instead of a transparent centre,
   with the border dropped since a filled circle doesn't need one. The lead
   figure in each line stays bolded in black for scannability, rather than
   every word carrying equal weight. */
.stat-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.stat-list li {
  position: relative;
  padding-left: 22px;
  font-family: var(--font-body);
  font-size: 14px;
  line-height: 20px;
  color: var(--color-grey);
}
.stat-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 6px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--color-berry);
}
.stat-list li strong {
  color: var(--color-black);
  font-weight: 800;
}

/* --------------------------------------------------------------------------
   9b7. SERVICES PAGE — sub-service columns (Exterior/Interior)
   REPLACES the previous .service-tabs* tabbed interface (nav row of buttons
   + one switched-in panel at a time) per request ("make the tabs become 4
   columns with an image on the top and supporting copy underneath") — all
   4 sub-services per topic are now always visible side by side as their own
   column, rather than one at a time behind a click, so initServiceTabs in
   js/main.js is now inert dead code (its querySelectorAll('[data-service-
   tabs]') simply finds nothing to attach to, no errors) rather than
   removed outright, in case a future request wants tab-switching back.
   Each column is image-on-top (.service-columns__media, same img-placeholder
   "swap me" convention as everywhere else) then the sub-service's own title
   and one-line supporting copy underneath — the longer explanatory
   paragraph and "Supporting Figures" stat-list each sub-service used to
   carry inside its tab panel have been left out of this compact column
   layout (four columns of full paragraphs + 4-line stat lists each would
   be a wall of text at this width) rather than deleted outright — see each
   topic's own HTML comment in services.html for that original copy,
   preserved verbatim in case Simon wants it reinstated elsewhere (e.g. its
   own page, or a future expanded/detail view per sub-service). */
.service-columns {
  gap: 32px;
}
.service-columns__item {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
/* "Family tree" wireframe connectors (horizontal trunk + vertical stems
   above the 4 columns) REMOVED per a follow-up request ("remove family tree
   line idea") — was a decorative grey line pair added the previous round
   per "make it similar to a family tree design... wireframe lines running
   down into the 4 sub sections". .service-columns/.service-columns__item
   are back to the plain rules above with no ::before connectors or the
   position/padding-top this used to need to make room for them. */
/* Small numbered sub-category label — briefly lived here above each of the
   4 columns (per request "use small numbers for the sub categories (styled
   like the #results on the case studies page)", then "add the line after
   the number... lay it out like the case study # results line"), then
   REMOVED per a follow-up request ("4 x column numbers - remove these and
   use this styling to create the 5 chapter numbers"): the typography
   itself (below, now .service-chapter-num/.service-lede__num) moved to the
   5 major-section chapter numbers instead — see 9b1 (.service-lede__num)
   and 9b6 (.service-chapter-num, above each of the 5 media-feature bands)
   — and the line (.service-columns__num-line) wasn't carried over to
   either of those, so it's gone rather than orphaned. */
.service-chapter-num,
.service-lede__num {
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 12px;
  line-height: 18px;
  text-transform: uppercase;
  letter-spacing: 1.8px;
  color: var(--color-berry);
  white-space: nowrap;
}
/* .service-chapter-row/__line (number + grey line sitting above each
   section's video band) REMOVED per a follow-up request ("remove section
   numbers, instead use the following icons inside the hero videos - centre
   it above the titles - in white"). .service-chapter-num/.service-lede__num
   above is UNCHANGED and still live — it's a shared rule, and the 5-column
   lede's own small "01"-"05" labels above its icons (.service-lede__num)
   still use it; only the bare <span class="service-chapter-num"> that used
   to sit above each video band, and its line, are gone. See
   .service-chapter-icon below for the icon that replaces both. */
/* Number-circle icon sitting inside each of the 5 major sections' own
   .media-feature__content — centred above the heading via that flex-col's
   existing gap-4, per request ("use the following icons inside the hero
   videos - centre it above the titles - in white. 'number-circle-one.svg'
   'number-circle-two.svg' etc"). The 5 SVGs (assets/svg_graphics/number-
   circle-one.svg through -five.svg, Phosphor Icons' regular weight) already
   carry fill="#ffffff" baked in, same convention as this project's other
   icon assets (e.g. building-office.svg), so no recolour filter is needed
   here — just sizing. */
.service-chapter-icon {
  width: 40px;
  height: 40px;
}
.service-columns__media {
  position: relative;
  width: 100%;
  aspect-ratio: 31 / 20;
  overflow: hidden;
}
.service-columns__title {
  color: var(--color-black);
}
/* "Custom lighting" tag on the Marketing/Design Development columns' own
   before-after sliders (moved here from the Exterior/Interior intro rows
   per request), styled to match the project pages' "Next case study" tag
   exactly — black background, white text, top-left — see
   .project-adjacent-grid .service-card__eyebrow above for that same
   recipe. A new class rather than reusing that one directly, since this
   sits in a different scope (.service-columns, not .project-adjacent-grid)
   with no other coupling between the two pages' components. */
.service-columns__tag {
  position: absolute;
  top: 16px;
  left: 16px;
  z-index: 1;
  background: var(--color-black);
  padding: 6px 14px;
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 12px;
  line-height: 18px;
  letter-spacing: 1.8px;
  text-transform: uppercase;
  color: var(--color-white);
}
@media (max-width: 1023px) {
  .service-columns {
    gap: 40px 24px;
  }
}

/* Exterior Visualisation's own 2x2 layout trial — "the 4 columns can we try
   2x2 grid with image to the left and copy to right in each grid", per
   request. Scoped to Exterior only via these two modifier classes rather
   than changing the shared .service-columns/__item rules everywhere, since
   Interior's own 4-column grid wasn't part of the request and stays exactly
   as it was (image-on-top, plain .service-columns__item). If Simon likes
   it, the same two classes can just be added to Interior's grid too.
   .service-columns--split overrides nothing of its own (grid-cols-1/sm:
   grid-cols-2 is set directly in services.html's own Tailwind classes,
   replacing the plain version's grid-cols-2/lg:grid-cols-4) — it only
   exists so this comment has somewhere to live and so a future rule can
   hook onto it without touching the shared base class. */
.service-columns__item--split {
  display: grid;
  grid-template-columns: 2fr 3fr;
  align-items: center;
  gap: 20px;
}
.service-columns__copy {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
@media (max-width: 639px) {
  .service-columns__item--split {
    grid-template-columns: 1fr;
  }
}

/* --------------------------------------------------------------------------
   9b8. SERVICES PAGE — supporting banner image
   One placeholder image dropped into the Aerial Footage/Virtual Tours
   detail sections, between the overview paragraphs and Key Benefits, so
   the run of paragraph-then-list has a visual break — same aspect-ratio
   language as the rest of the page's photography but wider/shorter (21:9)
   to read as a supporting still rather than another hero.
   -------------------------------------------------------------------------- */
.service-banner-media {
  width: 100%;
  aspect-ratio: 21 / 9;
  overflow: hidden;
}
/* Animation-only exception, per request ("could we pull out on this image
   so we can see the top and bottom of the box?") — the shared 21:9 ratio
   above was cropping the top and bottom off the Wonky Coffee product shot
   used here. Rather than crop the source image less (object-fit:cover
   would still have to crop it down to whatever box it's given), this
   modifier gives the Animation banner its own, taller aspect ratio that
   matches the source photo's own native framing (1280x717) almost exactly,
   so effectively none of it is cropped away and the whole box, top to
   bottom, is visible. Scoped to this one banner via a modifier class, same
   pattern as .service-columns--split's own Exterior-only scoping above —
   Virtual Tours' and Aerial Footage's own banners keep the wider 21:9. */
.service-banner-media--animation {
  aspect-ratio: 1280 / 717;
}

/* --------------------------------------------------------------------------
   9b9. SERVICES PAGE — Key Benefits checklist (Aerial Footage/Virtual Tours)
   Was a 3-column grid of 6 bordered "featured card" tiles, each its own
   icon+paragraph block — reduced down to a small checklist per request
   ("key benefits — can this be reduced down to a small list using
   check.svg as the bullet. Move it to the right of the body copy in each
   section"). check.svg (a plain flat check mark) replaces check-circle.svg
   as the bullet now that this is a compact list rather than a grid of
   cards — same recolour-berry filter recipe as everywhere else on the
   page for a flat white/black source SVG. .service-benefits-row (below)
   is the new 2-column wrapper that actually moves this list to sit beside
   (not below) that section's own body-copy paragraphs — see that rule's
   own comment. -------------------------------------------------------------------------- */
/* White background/border removed per request ("key benefit list - remove
   white containers and border for these") — was a bordered white card
   around the list (padding:24px to sit clear of that border); now a plain
   list sitting directly on the page background like every other text
   block on this page, so the padding that used to keep text off the
   border is dropped too rather than left as orphaned empty space. */
/* Smaller text/berry copy per request ("for the tick lists, make these go
   into 2 columns, smaller text and berry colour copy") — was a
   single-column stack, 14px black copy. Icon size trimmed slightly (16px
   to 14px) to match the smaller text rather than reading oversized next to
   it; the same berry recolour filter is unchanged.
   Back to a single column per a follow-up request ("put the berry tick
   list in one column") — the 2-column grid above is gone; kept as a
   single-column list (grid-template-columns removed, gap now just a
   single row value) rather than reinstating the old flex/block stack, so
   the smaller/berry styling below is untouched. */
.benefit-checklist {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr;
  gap: 12px;
}
.benefit-checklist li {
  display: flex;
  align-items: flex-start;
  gap: 8px;
}
.benefit-checklist__icon {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  margin-top: 3px;
  filter: brightness(0) invert(11.6%) sepia(88.4%) saturate(593.5%) hue-rotate(291.5deg) brightness(235.5%) contrast(129.6%);
}
.benefit-checklist p {
  margin: 0;
  font-family: var(--font-body);
  font-size: 13px;
  line-height: 18px;
  color: var(--color-berry);
}

/* Modifier: "row cards" treatment, per request ("for the animation section
   can the berry list be separated out into row cards") — clarified via a
   follow-up question to mean each item gets its own bordered card (icon +
   text still side by side, so each card reads as a "row"), stacked in the
   same single-column spot the plain list already occupied, rather than
   rearranging into a multi-column grid. Scoped to Animation only via this
   modifier class on that section's own <ul> — every other section keeps
   the plain borderless .benefit-checklist above untouched.
   border-radius:0 to match the site's squared-off convention used
   everywhere else (buttons, nav, .service-detail-card etc.); background is
   --color-offwhite (not white) so each card still reads as a distinct
   surface sitting on the white .service-detail-card behind it. */
.benefit-checklist--cards {
  gap: 10px;
}
.benefit-checklist--cards li {
  padding: 14px 16px;
  border: 1px solid var(--color-grey-light);
  border-radius: 0;
  background: var(--color-offwhite);
}

/* Modifier: icon-tile grid, per request ("make the berry list fit into a 3
   x 2 grid, in each cell centre the icon in a grey square on the left
   third, then have the copy on the right 2 thirds of the cell. Make the
   icon white, and the copy grey"). No section was named, so — same
   "applies sitewide" treatment every other unscoped list/stat request on
   this page has had — this supersedes .benefit-checklist--cards on
   Animation too (that modifier is left in place above, unused, per this
   file's own "retire, don't delete" convention) as well as the plain
   .benefit-checklist styling on the other 4 sections; every section's <ul>
   now carries this modifier instead.
   Column count and colours both changed by a later request ("make the 6
   icons berry and the squares they are in off white. make the offwhite
   squares half the size and list ordered 2 columns of 3"):
   grid-template-columns dropped from repeat(3,1fr) to repeat(2,1fr) — same
   6 <li> items per section, now 2 columns of 3 rows instead of 3 columns
   of 2 (default row-major grid flow already reads item 1/2 on row one,
   3/4 on row two, 5/6 on row three, so no grid-auto-flow change was
   needed). Icon-box background swapped from --color-grey to
   --color-offwhite, and the icon's own filter swapped from the flat
   brightness(0) invert(1) white recipe to the tuned berry recipe used
   throughout this file (see e.g. .before-after__handle img) — off-white
   square, berry icon, the reverse of the previous grey-square/white-icon
   pairing. flex-basis halved from 33.333% to 16.667% ("half the size") —
   kept as a fraction of the LI's own width (not a fixed px value) so it
   keeps sizing itself consistently at every viewport, same reasoning the
   original 33.333% value used.
   Squares/icons shrunk again per a later request ("make the 6 icons and
   their containers smaller") — flex-basis taken down from 16.667% to 10%
   (still a fraction of the LI's own width, same reasoning as above) and
   the icon itself from 28px to 20px so it still sits comfortably inside
   the now-smaller box. */
.benefit-checklist--grid {
  grid-template-columns: repeat(2, 1fr);
  gap: 16px;
}
.benefit-checklist--grid li {
  display: flex;
  align-items: center;
  gap: 0;
}
.benefit-checklist--grid .benefit-checklist__icon-box {
  flex: 0 0 10%;
  aspect-ratio: 1;
  background: var(--color-offwhite);
  display: flex;
  align-items: center;
  justify-content: center;
}
.benefit-checklist--grid .benefit-checklist__icon {
  width: 20px;
  height: 20px;
  margin-top: 0;
  filter: brightness(0) invert(11.6%) sepia(88.4%) saturate(593.5%) hue-rotate(291.5deg) brightness(235.5%) contrast(129.6%);
}
.benefit-checklist--grid p {
  flex: 1 1 0;
  min-width: 0;
  padding: 0 12px;
  color: var(--color-grey);
}
@media (max-width: 640px) {
  .benefit-checklist--grid {
    grid-template-columns: 1fr;
  }
}

/* Back to a single stacked column — checklist under the copy, not beside
   it — per request ("move berry list under the main copy in each
   section"). This reverses the 2-column (copy left, checklist right) shape
   this rule had (see the flex ratio/align-items reasoning that used to sit
   in this comment for that version's own logic), applied to every section
   since the request named no section in particular, same "applies
   sitewide" treatment given to every other unscoped quick-stats/checklist
   request. The old flex:1.4/1 ratio and align-items:flex-start (needed to
   stop a shorter checklist column drifting to vertical-centre against a
   taller copy column beside it) are gone now there's nothing beside
   anything any more; the max-width:768px breakpoint that used to switch to
   this same stacked layout on mobile only is folded into the base rule
   instead, since it's now the only layout at any width. */
.service-benefits-row {
  display: flex;
  flex-direction: column;
  gap: 24px;
}
.service-benefits-row__copy {
  display: flex;
  flex-direction: column;
  gap: 24px;
}
.service-benefits-row__list {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* Modifier: copy + checklist side by side, per request ("Move the 2 x icon
   columns to the right of the copy section (so they all form a row)") —
   scoped to Interior only for now, per the answer to a follow-up question
   ("interior first - once nailed down we'll put it to the other
   sections"), so the base .service-benefits-row above (plain stacked
   column) is untouched for Exterior/Animation/Virtual Tours/Aerial
   Footage. Same 1fr/1fr/48px/align-items:start shape .service-split-row
   used to give this section against the old slider, now applied one level
   in — to this row's own two children — since that outer pairing is gone
   (see .service-frame-row below, and the markup comment in services.html
   for the full story of what replaced it). */
.service-benefits-row--split {
  flex-direction: row;
  align-items: flex-start;
  gap: 48px;
  /* "Increase the padding of the copy and icon sections (try 40px)" — added
     once .service-frame-row (above) went full-bleed edge-to-edge, so this
     row needed its own breathing room reinstated independently of the
     card's base 20px padding. */
  padding: 40px;
}
.service-benefits-row--split .service-benefits-row__copy,
.service-benefits-row--split .service-benefits-row__list {
  flex: 1 1 0;
  min-width: 0;
}
/* Padding pulled back down to a more mobile-appropriate 20px per request
   ("the copy section, reduce the padding around the copy - i think it's
   40px currently but make it more consistent with mobile sized padding"),
   then widened from its own original max-width:640px cutoff to this same
   900px one per a follow-up request repeating the ask ("in each section
   can the copy section with white background have smaller padding around,
   use 20px") — this row already drops to a single stacked column at 900px
   (see flex-direction:column just below), so anywhere it's already reading
   as a "mobile-style" block it should also be carrying the smaller
   "mobile-style" padding, not just below 640px specifically; there's no
   width range left where it's stacked single-column but still padded for
   the wide desktop/tablet row it came from. 20px matches the card's own
   base .service-detail-card padding (see "9b6" above) — the same "mobile
   sized" padding every other edge of this card already uses — rather than
   picking a new bespoke value. */
@media (max-width: 900px) {
  .service-benefits-row--split {
    flex-direction: column;
    padding: 20px;
  }
}

/* Static row of the sub-service photos, replacing the old .service-slider
   (tabs, then a one-at-a-time crossfading slider with an autoplay timer)
   per request ("I love the progress slider but it's not working... use
   the layout of the slider frames and put those in a row (1px apart)
   underneath the copy & 6 x icon sections") — all 4 images visible at
   once, side by side, 1px apart, rather than cycling through one at a
   time. Each .service-frame-row__item reuses .service-columns__media
   completely unchanged (same 31/20 aspect-ratio, same overflow:hidden). No
   JS at all — nothing to "not work" — just four flex items sat in a row.

   Reworked again per a later request ("move the 4 col images above the
   copy and icon section - make it fill the container (no padding right
   and left), have a 1px gap between the 4 images and 1px gap above.
   Increase the padding of the copy and icon sections... Use the black tag
   for the image titles... Remove the sub copy on the images... Remove
   black grad on images"):
     - Moved above .service-benefits-row--split in the markup (was below
       it) — see services.html.
     - Made full-bleed within .service-detail-card (which has its own 20px
       padding) via the same negative-margin technique already used for
       .media-feature above, in .service-detail-card .service-frame-row
       below — except margin-top is -23px, not -20px, because the parent
       .service-detail's own `gap: 24px` between stacked children already
       supplies 24px above this block; pulling it up by 23 of those 24px
       (instead of the full 24, or the full 20+24 .media-feature effectively
       cancels) leaves exactly the requested 1px gap showing above the row,
       while left/right still cancel the 20px card padding completely for
       an edge-to-edge fill.
     - .service-slider__scrim (dark gradient) and .service-slider__copy
       (title + sub-copy overlay) — reused here from the old slider in the
       previous version of this block — are both removed per "Remove
       black grad on images" and "Remove the sub copy on the images (as i
       think this is repeated a lot anyway)". They're now fully retired;
       see the 9b16 block below.
     - Replaced with .service-frame-row__tag: a small black chip holding
       just the sub-service title, per "Use the black tag for the image
       titles... use the same font as '# results' on the case studies
       page." That font recipe (.portfolio-count-row__label on
       case-studies.html) is character-for-character identical to this
       site's existing .title-tag class (font-heading, 600, 12px/18px,
       1.8px tracking, uppercase) bar colour, so .service-frame-row__tag
       reuses .title-tag directly rather than duplicating the recipe under
       a new name — just overridden to white-on-black instead of its
       default grey-on-transparent. */
.service-frame-row {
  display: flex;
  gap: 1px;
}
/* Full-bleed within the white .service-detail-card, edge-to-edge left and
   right, with a 1px gap preserved above — see this block's own comment
   above for the -23px reasoning. */
.service-detail-card .service-frame-row {
  width: calc(100% + 40px);
  margin: -23px -20px 0 -20px;
}
.service-frame-row__item {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
}
/* The black tag — see this block's own comment above. .title-tag's own
   defaults (grey text, 10px bottom margin) are for a label sitting inline
   above other copy; both are overridden here since this is a single
   self-contained chip overlaid on a photo. */
.service-frame-row__tag {
  position: absolute;
  left: 20px;
  bottom: 20px;
  z-index: 1;
  display: inline-flex;
  /* Caps the chip so it never runs past the image's own right edge (4-up on
     desktop makes each frame fairly narrow, and titles like "Marketing &
     Sales Interior Visualisations" are long) — .title-tag's own
     white-space:nowrap is overridden below so long titles wrap onto a
     second line inside the chip instead of overflowing/getting clipped by
     .service-columns__media's overflow:hidden. */
  max-width: calc(100% - 40px);
  background: var(--color-black);
  padding: 8px 12px;
}
.service-frame-row__tag .title-tag {
  margin-bottom: 0;
  color: var(--color-white);
  white-space: normal;
}
/* A 2-column wrap was tried first here but the longer sub-service titles
   that existed at the time ("Marketing & Sales Interior Visualisations")
   wrapped to 3 lines inside the resulting ~165px-wide frame and overflowed
   the top of the image, getting clipped by .service-columns__media's own
   overflow:hidden — confirmed via a computed-style check in the verify
   screenshots, not just a look — so this fell back to a single full-width
   stacked column instead.

   2x2 GRID RESTORED per a later request ("For the 4 categories in each
   section - on mobile can they be put into a 2 x 2 grid"), directly
   reversing that single-column stack. The tag copy that caused the
   original overflow is gone now — these are the short "Marketing visuals"/
   "Photo montage"-style labels, not the old full "___ Visualisations"
   titles — but as a safety net against the longest survivor ("Accurate
   verified views (AVRs)") ever wrapping past 2 lines inside a narrower
   half-width frame, this pass also: shrinks the chip's own type/padding/
   insets a little on mobile, gives each frame a taller (4/3, up from
   31/20) aspect-ratio for more vertical headroom, and hard-clamps the
   label to 2 lines with an ellipsis so a worst-case label truncates
   cleanly instead of ever overflowing .service-columns__media's own
   overflow:hidden edge the way the old one did. */
@media (max-width: 640px) {
  .service-frame-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1px;
  }
  .service-frame-row__item .service-columns__media {
    aspect-ratio: 4 / 3;
  }
  .service-frame-row__tag {
    left: 10px;
    bottom: 10px;
    max-width: calc(100% - 20px);
    padding: 6px 8px;
  }
  .service-frame-row__tag .title-tag {
    font-size: 10px;
    line-height: 13px;
    letter-spacing: 1px;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
}

/* --------------------------------------------------------------------------
   9b10. SERVICES PAGE — Supporting Figures stat band (Aerial/Virtual Tours)
   Was 3 plain columns of label + stat-list — now 3 cards, each pulling its
   group's first figure out as a large --font-display number (the site's
   display font, otherwise only used for h1s/big standalone numerals) so
   the headline stat in each group actually reads as a headline, with the
   rest of that group's stats still available underneath as a normal
   .stat-list rather than lost.
   REPLACED per a follow-up request ("for the 3 stat info areas on each
   section, can these be just 3 x quick stats (number and one/two sentences)
   and placed under the tick lists") — see .quick-stats below, now used in
   Animation/Virtual Tours/Aerial Footage instead of this bordered-card
   grid. Left here rather than deleted, same "keep it in case a future
   request wants it back" convention as this file's other retired
   components (.service-duo__item, etc). -------------------------------------------------------------------------- */
.stat-band {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
}
@media (max-width: 900px) {
  .stat-band {
    grid-template-columns: 1fr;
  }
}
.stat-band__card {
  background: var(--color-white);
  border: 1px solid var(--color-grey-light);
  padding: 28px 24px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.stat-band__label {
  color: var(--color-grey);
}
.stat-band__figure {
  font-family: var(--font-display);
  font-size: 42px;
  line-height: 1;
  color: var(--color-berry);
}
.stat-band__card > p {
  margin: 0;
}
.stat-band__card .stat-list {
  margin-top: 4px;
  padding-top: 12px;
  border-top: 1px solid var(--color-grey-light);
}

/* --------------------------------------------------------------------------
   9b11. SERVICES PAGE — quick stats (Animation/Virtual Tours/Aerial Footage)
   Replaces .stat-band's 3 bordered white cards, per request ("for the 3
   stat info areas on each section, can these be just 3 x quick stats
   (number and one/two sentences) and placed under the tick lists"). This
   base (stacked, vertical-divider) version was originally used inside
   .service-benefits-row__list, directly below .benefit-checklist — left
   here for reference/reuse, but every section on the page now uses the
   .quick-stats--row modifier below instead (see that rule's own comment).
   Each card's own .h4 label and second stat-list bullet were dropped when
   this replaced .stat-band; the figure and the card's own descriptive
   sentence(s) carried over as-is at that point (since simplified further
   — see .quick-stats--row). */
.quick-stats {
  display: flex;
  flex-direction: column;
  gap: 16px;
  margin-top: 24px;
}
.quick-stats__item {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding-top: 16px;
  border-top: 1px solid var(--color-grey-light);
}
.quick-stats__item:first-child {
  padding-top: 0;
  border-top: none;
}
/* Colour changed to black per request ("on the stats, make the numbers
   black") — was --color-berry, matching the checklist text beside it;
   now reads as a plain figure rather than a brand-coloured highlight. */
.quick-stats__figure {
  font-family: var(--font-display);
  font-size: 28px;
  line-height: 1;
  color: var(--color-black);
}
.quick-stats__caption {
  margin: 0;
  font-family: var(--font-body);
  font-size: 13px;
  line-height: 18px;
  color: var(--color-grey);
}

/* Row layout for the 3 quick stats, per a follow-up request ("simplify the
   stats even more, I want the number and then around 5 words for each.
   Put these 3 in a row under the large copy section in each section") —
   each caption is now a short ~5-word phrase rather than a sentence or
   two, so 3 side by side reads clean rather than the earlier vertical
   stack (kept above as .quick-stats' own base styling). Moved out of
   .service-benefits-row__list (the checklist column) and into
   .service-benefits-row__copy instead, as the last item in that column —
   "the large copy section" — sitting below the two overview paragraphs
   and the banner image. Same thin --color-grey-light top rule as the
   stacked version, just running above all 3 items now instead of skipping
   the first (equal specificity + later in the file, so this overrides
   .quick-stats__item:first-child's border removal). Drops to a single
   column on narrow screens, where 3 abreast would squeeze each figure too
   tight to read. */
/* Gap reduced to 1px per a follow-up request ("can the spacing between
   the 3 stats be 1px") — was 16px/24px, matching the same tight-grid
   convention used elsewhere on the site (e.g. the 5-column lede row and
   the homepage testimonials' own gap-[1px]); each item's own top border
   (below) still reads as its divider regardless of how tight the gap is. */
.quick-stats--row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1px;
  margin-top: 32px;
}
.quick-stats--row .quick-stats__item {
  padding-top: 16px;
  border-top: 1px solid var(--color-grey-light);
}
.quick-stats--row .quick-stats__figure {
  font-size: 24px;
}
.quick-stats--row .quick-stats__caption {
  font-size: 12px;
  line-height: 16px;
}
@media (max-width: 640px) {
  .quick-stats--row {
    grid-template-columns: 1fr;
  }
}

/* --------------------------------------------------------------------------
   9b12. SERVICES PAGE — full-width berry quick-stats band
   Per request ("the 3 stats, can these spread the whole width in 3 columns,
   make the numbers larger and centre align the copy. Make this section have
   a berry background and white copy with off white divider lines between
   the columns"). Moves the existing .quick-stats.quick-stats--row markup
   out of .service-benefits-row__copy (where it sat squeezed into that
   column's ~58% share of the width, below the two paragraphs) into this new
   wrapper, placed as its own full-width block directly below the whole
   .service-benefits-row (copy + checklist together) in every section's
   markup — see services.html. Applied to all 5 sections (Exterior/Interior/
   Animation/Virtual Tours/Aerial Footage) since the stats component itself,
   not any one section, was named — same "applies sitewide" treatment the
   quick-stats figures/gap have had in every previous request about them.
   Bleeds to the white .service-detail-card's own outer edges (clarified via
   a quick question — full-bleed vs staying inside the 20px padding) using
   the exact same calc(100% + 40px)/-20px-margins technique as
   .service-detail-card .media-feature above; margin-bottom:-20px only
   (no margin-top) since .service-detail's own flex gap:24px already spaces
   it from .service-benefits-row above, and this band is always the last
   child so -20px on the bottom alone reaches the card's bottom edge.
   All the overrides below share equal specificity (2 classes) with the
   base .quick-stats--row rules above, so — same "later wins" convention
   used throughout this file — they're placed after that block on purpose
   rather than needing !important or extra qualifiers.
   Divider lines run as a left border between columns rather than each
   item's own top border, since a horizontal row of equal columns reads
   with vertical dividers; falls back to a top border on the single mobile
   column, where a left border would have nothing to divide. Colour was
   --color-offwhite at first per the original request's own wording, then
   changed to --color-berry-dark by a follow-up request ("use berry dark
   for stat dividers") — see that rule's own comment below. */
.quick-stats-band {
  width: calc(100% + 40px);
  margin: 0 -20px -20px -20px;
  background: var(--color-berry);
  padding: 40px 20px;
}
.quick-stats-band .quick-stats--row {
  margin-top: 0;
  gap: 0;
}
/* Divider colour changed to --color-berry-dark per request ("use berry
   dark for stat dividers") — was --color-offwhite, which read quite faint
   against the berry background; berry-dark (the same darker accent used
   for the "Get in touch" CTA card's own hover shadow, see its own comment
   near the top of this file) gives a subtler, more visible-on-berry line. */
.quick-stats-band .quick-stats__item {
  padding: 0 24px;
  border-top: none;
  border-left: 1px solid var(--color-berry-dark);
  text-align: center;
  align-items: center;
}
.quick-stats-band .quick-stats__item:first-child {
  border-left: none;
}
.quick-stats-band .quick-stats__figure {
  font-size: 48px;
  color: var(--color-white);
}
.quick-stats-band .quick-stats__caption {
  color: var(--color-white);
}
/* Mobile layout — went through two passes. First tried a left-aligned
   number-beside-caption single column (numbers smaller, copy running to the
   right over multiple lines), which needed each item's own top/bottom
   padding to carry the divider spacing (a plain top-border stack has no
   other gap to work with) — but a first-child-only "zero the top" rule left
   the whole band's padding asymmetric (32px top above item 1, but 52px
   below the last item once its own bottom padding stacked on the band's
   own 32px), reported back as "padding on the bottom of this section
   doesn't match the top".
   REVERTED per that same follow-up ("can it be consistent all around. Can
   we try 3 columns rather than 3 rows, stat above and copy below all
   centre aligned") back to the desktop band's own shape — 3 columns,
   figure above caption, both centred, vertical dividers between columns —
   just sized down for a narrow screen instead of restructured. Overriding
   .quick-stats--row's own generic mobile collapse (grid-template-columns:
   1fr, see "9b11" above) back to 3 columns here is scoped to this band only
   (higher specificity, so it wins over that generic rule); the bare
   .quick-stats--row instance the generic rule was written for doesn't
   actually exist in this page's markup any more (every quick-stats--row on
   this page now lives inside a .quick-stats-band — see services.html), but
   that generic rule is left as-is/unscoped rather than deleted in case a
   future section reuses it outside a band.
   With no more per-item padding needed for divider spacing (the divider is
   a border-left between columns again, which needs no padding of its own
   to stay even), the band's own single `padding: 32px 20px` below is now
   the only source of top/bottom space and is naturally identical on both
   edges — "consistent all around" falls out of the layout change itself
   rather than needing a separate fix. */
@media (max-width: 640px) {
  .quick-stats-band {
    padding: 32px 20px;
  }
  .quick-stats-band .quick-stats--row {
    grid-template-columns: repeat(3, 1fr);
  }
  .quick-stats-band .quick-stats__item {
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 8px;
    padding: 0 10px;
  }
  .quick-stats-band .quick-stats__item:first-child {
    padding-left: 0;
  }
  .quick-stats-band .quick-stats__item:last-child {
    padding-right: 0;
  }
  /* Sized down from the desktop band's own 48px/14px so 3 columns still
     fit a phone-width row without the figure wrapping and the caption
     staying readable at its own narrower column width. */
  .quick-stats-band .quick-stats__figure {
    font-size: 22px;
  }
  .quick-stats-band .quick-stats__caption {
    font-size: 11px;
    line-height: 15px;
  }
}

/* --------------------------------------------------------------------------
   9b13. SERVICES PAGE — Exterior/Interior split layout (copy+list | 4-col grid)
   Per request ("move this copy & list block to a 1 fr left and move the 4
   col list of image & copy to the 1 fr right"). Wraps .service-columns (the
   4-item image+copy grid) and .service-benefits-row (now a single stacked
   copy-then-list column, see that rule's own comment above) side by side in
   a plain 2-column CSS grid, 1fr each — copy+list on the left, the 4-item
   grid on the right — reversing their previous top-to-bottom order (grid
   above, benefits row below).
   Scoped to Exterior and Interior only, the only 2 sections with a 4-item
   image+copy grid to pair this with; Animation/Virtual Tours/Aerial
   Footage have no such grid, so their .service-benefits-row stays as its
   own full-width block (just stacked now, per the request above).
   Interior's own .service-columns grid-cols utility classes (in
   services.html) were dropped from lg:grid-cols-4 to sm:grid-cols-2 to
   match Exterior's own 2-column cap — a 4-across grid squeezed into one
   half of the page (rather than the page's former full width) would read
   far too cramped. Exterior's grid was already capped at 2 columns from
   its own earlier "2x2 layout" request, so this just brings Interior in
   line with it now they share the same available width.
   align-items:start (not stretch) so the shorter of the two columns
   doesn't get pulled down to match the taller one's height — each side
   should sit level with the top of the row, same reasoning
   .service-benefits-row itself used to use for its own two columns before
   this request restacked them. */
/* RETIRED — was Exterior's own copy-vs-accordion 1fr/1fr pairing (see
   9b15's own comment below, and services.html's Exterior "in detail"
   comment) until a later request ("update the exterior vis section to
   match the layout of the interior vis section - remove the accordion")
   replaced that whole pairing with Interior's .service-frame-row +
   .service-benefits-row--split pattern instead — nothing in services.html
   carries .service-split-row any more. Kept per this file's "retire, don't
   delete" convention rather than removed outright. */
.service-split-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 48px;
  align-items: start;
}
@media (max-width: 900px) {
  .service-split-row {
    grid-template-columns: 1fr;
  }
}

/* RETIRED along with .service-split-row above and 9b15's whole
   .service-accordion block below — Exterior's .service-benefits-row now
   carries the shared .service-benefits-row--split modifier instead (same
   class Interior uses), which already supplies its own 40px padding (see
   that rule's own comment further up), making this scoped duplicate
   redundant at desktop widths (both resolved to the same 40px, so it was
   invisible either way).
   Actually commented out now (previously left "live" as a harmless
   duplicate) — its #exterior-visualisation ID selector outranks the plain
   .service-benefits-row--split class selector's own mobile padding
   override below (see the "mobile sized padding" request further up this
   file), so while both agreed at 40px this sat unnoticed, but it silently
   overrode Exterior's own padding back to 40px on mobile the moment that
   override tried to drop it to 20px. Confirmed via computed-style check
   that Interior/Animation/Virtual Tours/Aerial Footage (no such ID rule)
   all picked up the 20px override correctly, while Exterior alone stayed
   pinned at 40px until this was disabled.
#exterior-visualisation .service-benefits-row {
  padding: 40px;
}
*/

/* --------------------------------------------------------------------------
   9b14. SERVICES PAGE — Interior's 4 sub-services back into tabs
   Per request ("for interior vis - can we put the 4 x cards into a tabbed
   section"). Reuses initServiceTabs in js/main.js untouched — that handler
   was left wired up to [data-service-tabs]/.service-tabs__btn/
   .service-tabs__panel/data-tab-target/is-active when Exterior's own tabs
   were replaced by static columns a few rounds back (see 9b7's comment
   above, "now inert dead code... in case a future request wants
   tab-switching back") — this is that future request, for Interior only;
   Exterior stays as individual rows (see its own request further up).
   The old .service-tabs* CSS itself was fully removed back when 9b7
   replaced it, so this is a fresh build against the same markup contract,
   not a restore — nav row of text buttons with a berry underline on the
   active one (border-bottom swap, not a background fill, to match the
   understated underline language already used for nav-link hover and the
   results-line elsewhere on the site), and one panel visible at a time
   below it (plain display:none/flex toggle, not an animated height, same
   "a tab switches, it doesn't expand" reasoning 9b7's own predecessor
   used). Each panel reuses .service-columns__media/__title directly so
   the image + heading look identical to how they read in Exterior's own
   rows and Interior's old static grid — only the layout mechanism (one at
   a time vs. all-at-once) has changed, not the content styling.
   First button/panel in the markup already carries is-active (see
   services.html) so the section isn't empty before any click, same
   convention the old accordion/tabs used. */
.service-tabs__nav {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 28px;
  border-bottom: 1px solid var(--color-grey-light);
  margin-bottom: 24px;
}
.service-tabs__btn {
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  padding: 0 0 12px 0;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 14px;
  line-height: 20px;
  text-align: left;
  color: var(--color-grey);
  cursor: pointer;
  transition: color 0.2s ease, border-color 0.2s ease;
}
.service-tabs__btn:hover {
  color: var(--color-black);
}
.service-tabs__btn.is-active {
  color: var(--color-black);
  border-bottom-color: var(--color-berry);
}
.service-tabs__panel {
  display: none;
  flex-direction: column;
  gap: 12px;
}
.service-tabs__panel.is-active {
  display: flex;
}
/* Retired, not removed — Interior's tabs (above) were replaced by
   .service-slider below per a later request ("a slider with pagination on
   interior"), leaving nothing on the page still using .service-tabs__* or
   [data-service-tabs]/initServiceTabs. Kept in place exactly like 9b7's own
   predecessor was, in case a future request wants tabs back a third time. */

/* --------------------------------------------------------------------------
   9b15. SERVICES PAGE — Exterior's 4 sub-services into an accordion
   Per request ("can we try an accordion on exterior"). Rather than a fresh
   component, this reuses the About page's FAQ accordion wholesale —
   .faq-list/.faq-item/.faq-item__trigger/.faq-item__panel/.faq-item__icon
   (section 13 above) and initFaqAccordion() in js/main.js, both completely
   untouched, so Exterior's accordion gets that component's single-open-at-
   a-time behaviour and GSAP height() animation for free, and any future
   tweak to the FAQ's look/feel (colours, spacing, icon) applies here too
   automatically. These rules only adapt .faq-item__panel-inner to hold an
   image above the copy, scoped under .service-accordion so the About page's
   own FAQ (plain paragraph, no image, narrower max-width) is untouched.

   RETIRED — the whole 9b15 block below (.service-accordion
   .faq-item__panel-inner, .service-accordion's own 40px padding, and the
   two border-top fixes) is now orphaned: a later request ("update the
   exterior vis section to match the layout of the interior vis section -
   remove the accordion") replaced Exterior's accordion markup entirely
   with Interior's .service-frame-row + .service-benefits-row--split
   pattern, so nothing in services.html carries .service-accordion any
   more. The reused About-page FAQ component itself
   (.faq-list/.faq-item/.faq-item__trigger/.faq-item__panel/.faq-item__icon,
   section 13, and initFaqAccordion() in js/main.js) is untouched and still
   actively serves the About page's own FAQ — only these Exterior-scoped
   adaptations are dead. Kept per this file's "retire, don't delete"
   convention rather than removed outright. */
.service-accordion .faq-item__panel-inner {
  max-width: none;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
/* Matches the 40px padding added to its sibling copy+checklist box
   (.service-benefits-row above) and to Interior's own
   .service-benefits-row--split, per request ("add padding to the exterior
   vis copy and accordion that matches the interior (i think it's 40px?)").
   .service-accordion is only ever used here (Exterior), so no extra
   scoping is needed. */
.service-accordion {
  padding: 40px;
}
/* Fixes a follow-up report ("the top border line on the accordion is out
   of alignment - can this be vertically aligned with the 'One Scheme,
   Every Stage From Planning To Sale' heading and the same size as the
   other accordion lines"). .faq-list's own border-top (below, shared with
   the About page's plain unpadded FAQ) draws at this box's outer edge —
   i.e. above the 40px padding just added — so once padding pushed the
   heading opposite it down 40px, that top line was left sitting 40px
   above the heading instead of level with it, and it spans the full
   un-padded box width rather than the padded content width every other
   divider between items measures across (each .faq-item's own
   border-bottom, inside the padding). Cancelled here for this scoped
   instance and moved onto the first .faq-item's own border-top instead —
   same colour/weight as those other dividers by construction, sitting
   right at the padding edge (level with the heading) and exactly as wide
   as they are, since both now measure the same padded content box. The
   About page's own FAQ is untouched (targeted here via the combined
   .service-accordion.faq-list selector, which only this element carries). */
.service-accordion.faq-list {
  border-top: none;
}
.service-accordion .faq-item:first-child {
  border-top: 1px solid var(--color-grey-light);
}

/* --------------------------------------------------------------------------
   9b16. SERVICES PAGE — Interior's 4 sub-services into a slider + pagination
   Per request ("a slider with pagination on interior"), replacing the tabs
   built for 9b14 above. One slide visible at a time — a plain .is-active
   class toggle exactly like the tabs it replaces (see .service-tabs__panel
   above), not a transform/translate carousel, so it needed no JS width/
   drag math, just initServiceSlider() in js/main.js swapping which slide
   carries .is-active.
   Originally shipped with the copy sitting below the image and the
   controls centred underneath (like the tabs it replaced), then reworked
   per a follow-up request ("put the copy into a container inside the
   image, position it left and bottom with a 20px padding. put the
   chevrons in the bottom right of the image... Then update the pagination
   to be 4 elongated rectangles spanning the bottom of the image - no
   padding clearance") into the overlay layout below:
     - .service-slider__copy lives INSIDE each .service-columns__media (so
       it swaps with whichever slide is active automatically) — the image
       itself is the container, not a separate box beside it.
     - .service-slider__scrim (also per-slide, inside the media) is a
       bottom-anchored dark gradient behind the copy — not asked for
       explicitly, but without it white copy over a bright photo (e.g. the
       kitchen island shot) isn't reliably legible; kept minimal (fades to
       fully transparent well above the copy) so it doesn't flatten the
       photo the way a full-image scrim (.media-feature__scrim elsewhere on
       this page) would.
     - .service-slider__nav (the two arrows) and .service-slider__pagination
       (the 4 bars) are NOT duplicated per slide — they're shared, sitting
       as direct children of .service-slider__track (position:relative)
       alongside the 4 .service-slider__slide elements.
   Arrow icons reuse caret-left.svg/caret-right.svg with the same berry
   recolour filter used throughout this file (e.g. .before-after__handle
   img below); sitting directly on a photo now (not the page's white
   background), they get that same handle's white-circle-on-photo
   treatment — solid white fill plus a soft drop shadow — swapping to a
   solid berry fill with the icon inverted white on hover, so they stay
   legible over any photo and still read as "the same control family" as
   the rest of the site. Pagination is 4 flex-1 bars (not the plain small
   squares dots elsewhere on the site use) spanning the full width flush
   against the image's bottom edge, matching "elongated rectangles...no
   padding clearance" exactly.

   Two more reworks layered on top of that, both per the same later
   request:

   1. Autoplay timer, "visually represented by the active pagination bar
      filling up" — each .service-slider__dot got a .service-slider__dot-
      fill child. initServiceSlider() (js/main.js) sets a
      --service-slider-duration custom property (its own DURATION
      constant, currently 5s) on the root, then the dot-fill for whichever
      dot is .is-active runs the service-slider-fill keyframe left-to-right
      over exactly that duration, restarting from a clean class toggle
      every time the JS timer (or a manual dot/arrow click) advances the
      slide — a plain CSS animation, not JS-driven width math. Bars for
      slides already passed in the CURRENT lap (index < the active one)
      sit permanently full via .is-complete rather than empty, the same
      "stories" reading the elongated-rectangle bars already implied;
      upcoming ones stay at their dim resting colour. prefers-reduced-
      motion turns the autoplay off entirely (js/main.js) and snaps the
      active bar straight to full via the media query at the bottom of
      this block, rather than animating it, matching the "respect motion
      preference completely" convention used sitewide (see initScrollReveal
      etc.).
   2. The reported "flashes white" switching between slides — traced to
      .service-slider__slide's old display:none/block toggle: the outgoing
      image vanished instantly while the incoming one's fade-in animation
      started from opacity:0, so for the first chunk of that 0.35s there
      was a genuine gap with nothing opaque covering the box, exposing the
      page's own background. Fixed by turning this into a real crossfade
      instead — every slide now stays in the DOM as an absolutely-
      positioned layer stacked on the others (.service-slider__track
      itself carries the 31/20 aspect-ratio that used to come from
      whichever slide happened to be display:block, since with all 4
      stacked absolutely none of them contribute layout height any more),
      with only `opacity` toggling on .is-active — the outgoing and
      incoming images are always both partway opaque at the same moment,
      never both fully transparent together. This also happens to be what
      makes the images load ahead of time ("preload the next one already
      in the sequence") without any extra JS: display:none elements can be
      deprioritised for image loading, but these are just opacity:0 now —
      still fully part of the render tree — so all 4 images request/decode
      together on page load rather than only once a slide is first shown.
      is-active slides are excluded from the tab order/hit-testing
      (pointer-events:none while transparent) and from assistive tech
      (aria-hidden, toggled in JS) exactly as display:none used to achieve
      structurally, just via different properties.

   RETIRED per a later request ("I love the progress slider but it's not
   working... use the layout of the slider frames and put those in a row
   (1px apart)") — reported unreliable in practice, and replaced outright
   with .service-frame-row above: a plain static row of the same 4 images,
   no JS, nothing to time or crossfade. .service-slider__track/__slide (and
   its is-active/crossfade/aria-hidden machinery), __nav, __arrow,
   __pagination and __dot/__dot-fill below are all dead now — nothing in
   services.html still carries [data-service-slider] — kept per this
   file's "retire, don't delete" convention rather than deleted outright.
   .service-slider__scrim and .service-slider__copy were reused for a
   while inside .service-frame-row__item's own .service-columns__media
   (still live at that point), but a later request ("Remove black grad on
   images", "Remove the sub copy on the images") removed that last usage
   too, replacing it with .service-frame-row__tag (see that block above)
   — so as of now __scrim and __copy (and __copy's h3/p children and mobile
   override) are fully retired along with everything else in this block. */
.service-slider__track {
  position: relative;
  aspect-ratio: 31 / 20;
  overflow: hidden;
}
.service-slider__slide {
  position: absolute;
  inset: 0;
  z-index: 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.4s ease;
}
.service-slider__slide.is-active {
  z-index: 1;
  opacity: 1;
  pointer-events: auto;
}
/* Superseded by the opacity crossfade above (see this block's own comment)
   — kept rather than deleted per this file's "retire, don't delete"
   convention, in case a hard cut back to a display:none swap is ever
   wanted again. */
@keyframes service-slider-fade {
  from { opacity: 0; }
  to { opacity: 1; }
}
.service-slider__slide .service-columns__media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  aspect-ratio: auto;
}
/* Retired along with __track/__slide above — see this block's own comment. */
.service-slider__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(15, 15, 15, 0.7) 0%, rgba(15, 15, 15, 0) 55%);
  pointer-events: none;
}
.service-slider__copy {
  position: absolute;
  left: 20px;
  bottom: 20px;
  max-width: 60%;
  display: flex;
  flex-direction: column;
  gap: 6px;
  z-index: 1;
}
.service-slider__copy h3 {
  color: var(--color-white);
}
.service-slider__copy p {
  color: var(--color-offwhite);
}
/* Retired along with __track/__slide above. */
.service-slider__nav {
  position: absolute;
  right: 20px;
  bottom: 20px;
  z-index: 2;
  display: flex;
  gap: 8px;
}
.service-slider__arrow {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  padding: 0;
  background: var(--color-white);
  border: none;
  border-radius: 0;
  box-shadow: 0 4px 14px rgba(15, 15, 15, 0.3);
  cursor: pointer;
  transition: background 0.2s ease;
}
.service-slider__arrow img {
  width: 9px;
  height: 9px;
  filter: brightness(0) invert(11.6%) sepia(88.4%) saturate(593.5%) hue-rotate(291.5deg) brightness(235.5%) contrast(129.6%);
  transition: filter 0.2s ease;
}
.service-slider__arrow:hover {
  background: var(--color-berry);
}
.service-slider__arrow:hover img {
  filter: brightness(0) invert(1);
}
.service-slider__pagination {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 2;
  display: flex;
  gap: 2px;
}
.service-slider__dot {
  position: relative;
  flex: 1 1 0;
  height: 5px;
  padding: 0;
  background: rgba(240, 238, 233, 0.4);
  border: none;
  border-radius: 0;
  overflow: hidden;
  cursor: pointer;
  transition: background 0.2s ease;
}
.service-slider__dot:hover {
  background: rgba(240, 238, 233, 0.7);
}
.service-slider__dot-fill {
  position: absolute;
  inset: 0;
  background: var(--color-berry);
  transform: scaleX(0);
  transform-origin: left;
}
.service-slider__dot.is-complete .service-slider__dot-fill {
  transform: scaleX(1);
}
.service-slider__dot.is-active .service-slider__dot-fill {
  animation: service-slider-fill var(--service-slider-duration, 5s) linear forwards;
}
@keyframes service-slider-fill {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}
@media (prefers-reduced-motion: reduce) {
  .service-slider__dot.is-active .service-slider__dot-fill {
    animation: none;
    transform: scaleX(1);
  }
}
/* Retired along with __track/__slide above — see this block's own comment. */
@media (max-width: 640px) {
  .service-slider__copy {
    max-width: 70%;
  }
}

/* --------------------------------------------------------------------------
   9c. FEATURED SECTION — sticky copy column
   The "Projects / Featured" pill+heading+progress+CTA column sticks
   vertically centred in the viewport while the (much taller) stacked
   project images beside it scroll past, then settles flush with the row's
   bottom — level with the last image's own bottom — as the section finishes
   scrolling. `top` is set by js/main.js (initFeaturedProgress /
   setStickyOffset) to `calc(50vh - <half the column's own rendered
   height>px)`, computed once the column has laid out (and recomputed on
   resize) — see that function for why this single computed offset, rather
   than the more common top:50vh + transform:translateY(-50%) pairing, is
   what makes both ends of the scroll line up correctly. No value is set
   here beyond position/align-self; the offset itself only ever comes from
   that inline style.
   Only kicks in at the same breakpoint the grid itself switches to two
   columns — meaningless once everything's stacked in one column on mobile.

   align-self:start matters more than it looks: a grid item's sticky
   containing block is its grid AREA (the full row height) regardless of
   its own alignment, so the sticky/centring behaviour works identically
   with or without this — but grid's default align-items:stretch would
   otherwise inflate this element's own rendered box to the row's full
   height (matching the tall image stack beside it), which turned into a
   huge, mostly-empty box that intercepted hover/clicks over content far
   above and below the actual visible text — confirmed via Playwright (a
   hover test on the unrelated Explore section above got blocked by this
   element's stretched hit area). align-self:start keeps the box sized to
   its own small content instead, which is also what lets the JS above
   measure a meaningful offsetHeight for it. */
@media (min-width: 1024px) {
  .featured-sticky-col {
    position: sticky;
    align-self: start;
  }
}

/* Scroll progress indicator — a vertical track that fills top-down as the
   visitor scrolls through the Featured section, so "how much further" is
   visible at a glance while the copy column is pinned centre-screen. Moved
   (by request) from sitting under the heading in the sticky copy column to
   its own line running down the gutter immediately LEFT of the image stack
   — a child of .featured-media-col now, absolutely positioned against that
   column's own full height rather than flowing inline, so its three step
   dots can line up with each image's true vertical centre (measured, not
   fixed thirds — see initFeaturedProgress in js/main.js) regardless of how
   tall the images actually render. js/main.js still drives
   .featured-progress__fill's own height (0%–100%) directly from
   .featured-grid's scroll position, exactly as before — only the track's
   position and the step dots' placement changed. Hidden below lg: with
   everything stacked in one column on mobile there's no image column for
   it to sit beside. */
.featured-media-col {
  position: relative;
}
.featured-progress {
  display: none;
}
@media (min-width: 1024px) {
  .featured-progress {
    display: block;
    position: absolute;
    top: 0;
    bottom: 0;
    /* Centred in the 64px gap (.featured-grid's gap-x-16) between the sticky
       copy column and this one. */
    left: -33px;
    width: 1px;
  }
}
/* top/height set inline by initFeaturedProgress (main.js) — the track now
   runs exactly from the top image's own vertical centre to the bottom
   image's, not the column's full 0–100% (which used to overshoot past
   both ends, into the column's own top/bottom padding-less edges). */
.featured-progress__track {
  position: absolute;
  left: 0;
  width: 1px;
  background: var(--color-grey-light);
}
.featured-progress__fill {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 0%;
  background: var(--color-berry);
}
/* Three markers — one per featured project — sat above the fill (z-index)
   so they stay visible as it rises past them. Each one's own `top` is set
   inline by initFeaturedProgress (main.js), measured from that image's own
   vertical centre relative to this track's full height, rather than a
   fixed 0/50/100% split — the three images don't necessarily divide the
   stack's height into even thirds (gaps between them, and any future
   image of a different aspect ratio, would throw fixed thirds off). */
.featured-progress__step {
  position: absolute;
  left: 50%;
  z-index: 1;
  /* Doubled from the original 5px, per request. */
  width: 10px;
  height: 10px;
  border-radius: 0;
  background: var(--color-grey-light);
  transform: translate(-50%, -50%);
  transition: background-color 0.3s ease;
}
/* Toggled by initFeaturedProgress in js/main.js as the fill line rises
   past each marker's own position on the track. */
.featured-progress__step.is-active {
  background: var(--color-berry);
}

/* The "View all" CTA sits directly under the heading in the sticky copy
   column now that the scroll line has moved out to sit beside the images
   instead — see the .is-pinned-center comment above for why that column
   tracks centre-screen while scrolling, then releases at the end of the
   row exactly in step with the last project image's own bottom edge.
   Visibility between this copy and the plain in-flow one after the image
   stack (for layouts below lg, where the sticky column isn't pinned) is
   toggled with Tailwind's hidden/lg: utilities directly in the markup
   rather than a dedicated class here — this rule only supplies the gap
   above the CTA, matching the 16px gap the heading block above it already
   uses (data-reveal's own flex gap-4). */
.featured-progress-cta {
  margin-top: 16px;
}
/* Mobile/tablet-only "View case studies" CTA (index.html's lg:hidden sibling
   below the 3 stacked project images) — per request ("centre 'View case
   studies' button and give more padding above it - match the 'learn more'
   button layout under the testimonials"). That testimonials CTA
   (.testimonials-cta) gets its whole 40px gap from its own flex-column
   parent's gap-10, with no margin of its own; this CTA instead sits inside
   .featured-media-col's tight gap-[10px] (10px, shared with the gap between
   the 3 images above it — see that column's own comment) alongside the
   images, so 30px is added here on top of that existing 10px to reach the
   same 40px total, rather than overriding the shared gap value itself. */
.mobile-case-studies-cta {
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 30px;
}

/* Hover effect (lift + outlined shadow) and border colour both removed, and
   the background pinned permanently to white, per request ("Remove hover
   effect and border colour - make the background colours white") — this
   card no longer reacts to the cursor at all; it just sits as a plain white
   panel throughout. border-width itself stays at the 5px set by an earlier
   request (only the border's COLOUR is being removed here) but with no
   colour to paint, a transparent border still occupies that same 5px of
   box space (box-sizing:border-box) — kept deliberately so the padding/
   content box this card's copy sizing was tuned against doesn't shift. */
.testimonial-card {
  position: relative;
  background: var(--color-white);
  border: 5px solid transparent;
  border-radius: 0;
  /* Was 32px 36px — brought down to the flat 20px the 3 Explore service
     cards use for their own body padding (.explore-content
     .service-card__body), per request ("match padding ... to the 3 service
     cards above"). */
  padding: 20px;
}
/* Quote copy sized down to 12px/18px to match the 3 Explore service cards'
   own body copy (.explore-content .service-card__body .regular-copy), per
   request ("match ... font size to the 3 service cards above"). Scoped to
   the blockquote specifically (rather than .testimonial-card .regular-copy)
   so the name/company text in the figcaption below keeps its own size. */
.testimonial-card blockquote.regular-copy {
  font-size: 12px;
  line-height: 18px;
}
.testimonial-card__avatar {
  width: 48px;
  height: 48px;
  /* Circular per request ("round the project testimonials profile
     pictures - make them circles") — was 0 (square), matching every other
     photo frame site-wide at the time. overflow:hidden below already clips
     the <img> to whatever shape this is, so 50% alone is enough; no change
     needed to the shared .testimonial-card__avatar img object-fit:cover
     rule further down this file. */
  border-radius: 50%;
  border: 2px solid var(--color-black);
  overflow: hidden;
  flex-shrink: 0;
}
/* Quote mark — quotes-fill.svg, per request replacing the old
   speechmarks.svg (a wider double-block mark at 24x20, no filter needed
   since it was already pre-tinted berry). The new icon is a square glyph,
   sized up slightly to 24x24 for the closest visual-weight match, and
   recoloured to berry with the same numerically-solved brightness(0)-first
   filter recipe used throughout this file for external icon SVGs (a flat
   white source here, same as the caret/squares-four icon set).
   Briefly duplicated + paired with an unrotated copy per a request ("can
   the quote icon be duplicated and rotated... so it looks like 6699"), then
   reverted back to this single rotated icon per a later request ("revert
   back to the single icon 66") — .testimonial-card__quote-group and
   .testimonial-card__quote--second (the wrapper + second-icon classes that
   pairing needed) were removed along with it. */
.testimonial-card__quote {
  width: 24px;
  height: 24px;
  flex-shrink: 0;
  filter: brightness(0) invert(11.6%) sepia(88.4%) saturate(593.5%) hue-rotate(291.5deg) brightness(235.5%) contrast(129.6%);
  /* Flipped 180° per request ("rotate the quote mark icons 180 degrees so
     they look more like 66 than 99") — the source SVG is a closing-quote
     ("99") glyph; rotating it end-for-end reads as an opening quote ("66")
     instead, which is the correct mark to lead a quoted block with. */
  transform: rotate(180deg);
}

/* Google reviews score card — sits just under "What our clients say", per
   request ("add a score card that shows 3 x 5 star reviews"). Same offwhite
   chip look as .caption-chip/.project-card__caption (background/padding),
   but its own pill radius and internal flex layout (star row + label), so
   kept as its own class rather than reusing those. star-fill.svg was
   uploaded to assets/svg_graphics and is inlined 5 times directly in the
   markup (fill attribute stripped), matching the same fill:currentColor
   convention already used for the Menu/Close icons — lets the berry colour
   be set once here via .reviews-score__stars rather than a hand-tuned
   image filter repeated 5 times. */
/* No [data-reveal] fade-in on this element (removed from its markup in
   index.html) — per request/screenshot, a faint colour seam was visible
   between this chip and the page background while scrolling, even though
   both use the exact same --color-offwhite token and body's own background
   is confirmed constant at every scroll position (no separately-painted
   ancestor layer sits between them — see the "Section backgrounds" note on
   body above for that same class of bug, previously fixed on the "why work
   with us" section). Here the cause is different: GSAP's opacity fade
   (initScrollReveal's animateSimple, used by every [data-reveal] element)
   promotes the element to its own GPU-composited layer for the duration of
   the animation, and that layer's rounded-corner edge (border-radius:100px
   here) can end up blended with very slightly different rounding than the
   plain page background beside it, especially on WebKit/Safari — the same
   "faint seam" mechanism, just triggered by a temporary animation layer
   instead of a second painted background. Dropping the fade for this one
   chip (everything else in this section still reveals as before) avoids
   the layer promotion entirely rather than trying to time around it.

   Follow-up report ("the container doesn't match the body bg colour ...
   when the case studies scroll animation starts it slightly adjusts the bg
   colour"): the fade above is this chip's OWN animation and was already
   removed, so a seam appearing now points at the same root structural
   cause the "why work with us" section itself was fixed for (see the
   "Section backgrounds" note on body above) rather than a new animation —
   this chip was still painting its own SEPARATE background-color, even
   though it's the exact same --color-offwhite token as body's single fill
   showing through everywhere around it. Two independently-painted layers of
   an identical colour are exactly what that note warns can rasterize with a
   faint boundary between them on some browsers/GPUs, especially once
   ANYTHING nearby on the page triggers a repaint/composite pass (any
   scroll-driven GSAP animation, including the case-studies box-reveal
   higher up the page, qualifies) — border-radius no longer being involved
   (this chip is sharp-cornered now, border-radius:0) only removes the
   curved-edge version of that same mechanism, not the flat-edge one.
   background removed entirely below, same fix as "why work with us": this
   chip now has no fill of its own at all, so body's own single background
   layer shows straight through with nothing beside it to seam against. */
.reviews-score {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  border-radius: 0;
  padding: 14px 26px;
}
.reviews-score__stars {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: var(--color-berry);
  flex-shrink: 0;
}
.reviews-score__star {
  display: block;
  flex-shrink: 0;
  fill: currentColor;
}
.reviews-score__value {
  color: var(--color-black);
  font-weight: 800;
}
@media (max-width: 480px) {
  /* Chip's own text wraps awkwardly around the star row at very narrow
     widths — stacks the label under the stars instead, still centred. */
  .reviews-score {
    flex-direction: column;
    text-align: center;
    gap: 10px;
  }
}

/* Real photography fills these containers edge-to-edge; the container
   itself (aspect-ratio / overflow:hidden / border-radius) handles cropping. */
.service-card__media img,
.service-card__media video,
.project-card__media img,
.testimonial-card__avatar img,
.img-placeholder-fill {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* --------------------------------------------------------------------------
   9a2. ABOUT — BENTO GRID (intro + founder story + wireframe)
   Was a 3-column layout (3fr/1fr/3fr) with the wireframe as a narrow
   vertical strip spanning both rows, sticky-pinned to the viewport's
   vertical centre as you scrolled past it. Restructured per an earlier
   request ("remove the scroll follow of the wireframe. Move the wireframe
   and surrounding window to cut through the bento grid horizontally
   instead") into 2 columns × 3 rows with "wire" as its own full-width row
   BETWEEN the two content rows, then moved again per this round's own
   request ("move the wireframe window section to underneath the Simon
   green section") to sit AFTER both content rows instead:
     "copy1    img1"
     "portrait copy2"
     "wire     wire"
   — copy1/img1 keep their original top-row pairing, portrait/copy2 (the
   "Simon green section") their original pairing now directly below it, and
   "wire" (the same area name repeated across both columns, spanning it
   edge-to-edge) now trails both instead of dividing them.
   grid-template-rows: 1fr 1fr auto keeps the equal-height trick between the
   two CONTENT rows (see that rule's own comment below) while letting the
   wire row size to its own content, now last rather than in the middle —
   only same-factor fr tracks get equalised against each other, so moving
   the plain "auto" track to a different position changes nothing about how
   it's sized, only where it sits.
   Below lg, collapses to a single column exactly as before, with "wire" now
   sitting last in the stacking order too, matching its new "trails
   everything" role on desktop (was between the two content pairs). */
.about-bento {
  display: grid;
  grid-template-columns: 1fr 1fr;
  /* Equal-height CONTENT rows, per an earlier request ("make the top row in
     the bento grid the same height as the bottom row") — was implicit auto/
     auto sizing, which let the portrait/copy2 row (with its longer "Turning
     ideas..." copy, name/title and social icons) come out taller than the
     copy1/img1 row on its own content. 1fr on an otherwise intrinsically-
     sized grid still resolves both same-factor tracks to the size of the
     TALLER one's own content (the standard CSS Grid equal-fr-tracks trick —
     with no extra free space to distribute, each fr track is sized to the
     largest base size any track with that flex factor needs), so the first
     row grows to match the second rather than drifting to its own shorter
     natural height. The trailing "auto" row (the wireframe band) sits
     outside that equalisation and just sizes to its own content/min-height,
     same as when it sat between the two content rows instead. */
  grid-template-rows: 1fr 1fr auto;
  grid-template-areas:
    "copy1    img1"
    "portrait copy2"
    "wire     wire";
  /* Column gap stays the deliberate 1px from an earlier request. Row gap
     — the space between this grid's own 3 stacked rows — raised from an
     earlier 10px to match .section's own padding-block value (80px
     desktop / 48px mobile, set in the media query below) per request
     ("the first 3 sections can the row padding be the same as elsewhere on
     the page - currently it's too tight"): read as being about this grid's
     3 rows specifically (copy1/img1, portrait/copy2, wire — this section's
     own only content, so "the first 3 sections" of the page in effect),
     rather than the OUTER .about-bento section's own padding-block, which
     was already a plain, unmodified .section (see that section's own
     comment further up) — i.e. already 80px, already matching "elsewhere
     on the page", with nothing to tighten. 10px between 3 full rows (one
     now carrying an h1-sized headline and a doubled wireframe) read
     cramped by comparison; 80px brings it in line with the vertical rhythm
     used between every other major block on this page and site-wide. */
  column-gap: 1px;
  row-gap: 80px;
}
@media (max-width: 1023px) {
  .about-bento {
    grid-template-columns: 1fr;
    row-gap: 48px;
    /* Back to natural auto-height rows for this 5-way single-column stack —
       the desktop rule's grid-template-rows:1fr 1fr auto above is
       specifically an equal-height trick for the desktop layout's two
       content rows, unrelated to the wireframe band's own position; left
       as-is here it would only apply to the first stacked area, an
       arbitrary mismatch this reset avoids. */
    grid-template-rows: auto;
    grid-template-areas:
      "copy1"
      "img1"
      "portrait"
      "copy2"
      "wire";
  }
}
.about-bento__cell {
  min-width: 0;
}
/* Text cells used to sit in their own solid white card (this site's
   standard .card look) so each read as a distinct bento tile rather than
   copy floating directly on the page's own offwhite background — background
   removed per request ("Remove the white solid backgrounds on this section
   and the next copy section"), naming copy1 ("A Studio Built on
   Understanding") and copy2 ("Turning ideas...") specifically; both share
   this one class, so dropping it here clears both cells' backgrounds in one
   place. Copy now sits directly on the page's own offwhite background,
   matching how "Why work with the unbuilt" and the FAQ section below already
   do (see that section's own comment for the same "no longer its own white
   band" history in reverse — the intro/story section this class serves was
   the one that GAINED a white band in that earlier swap; this request now
   removes it again).
   justify-content:center vertically centres each cell's own text, per
   request ("vertically centre the text in both 'A Studio Built on
   Understanding' and 'Turning ideas into visuals that sell' containers") —
   both cells already lay their own content out as a flex column via the
   Tailwind flex/flex-col utility classes on each in about.html, so this only
   needs to set the column's cross-axis-perpendicular (main-axis) alignment
   here rather than restructuring either cell's markup. Matters most for
   copy1, now that the equal-row-height rule above can leave it with more
   vertical space than its own (shorter) heading+paragraph need. */
.about-bento__cell--copy {
  padding: 40px;
  justify-content: center;
}
@media (max-width: 1023px) {
  .about-bento__cell--copy {
    padding: 24px;
  }
}
/* Left-aligned per the latest request ("move the 'studio behind the
   renders' headline to above 'a studio built...' then left align this copy
   section") — was briefly right-aligned per an earlier, since-superseded
   request ("right align 'A Studio Built on Understanding' text"); that
   text-align:right/align-items:flex-end pair is dropped entirely rather than
   set explicitly back to left, so this cell now falls back to the same
   plain left-aligned default copy2 ("Turning ideas...") already uses,
   keeping both cells visually consistent. grid-area is all this rule needs
   now. */
.about-bento__copy1 { grid-area: copy1; }
.about-bento__copy2 { grid-area: copy2; }
/* Image cells themselves (the grid areas) are UNCHANGED by the "2/3 size"
   request just below — they still stretch to fill their own grid cell
   (CSS grid's own default stretch alignment) exactly as before, keeping
   the overall bento layout/proportions intact per that request's own
   "keep... layout" condition. Confirmed against an earlier request
   ("reduce the size of the images to be the height of the copy sitting to
   the side of them. Make them both the same height though (so go with the
   biggest height between the two)") — already exactly what happens here
   with no rule change needed: the equal-fr-tracks trick on .about-bento's
   own grid-template-rows (see that rule's own comment) already sizes BOTH
   content rows to whichever of copy1/copy2's own natural heights is
   taller, and these cells' plain grid-stretch alignment fills that same
   shared height exactly, verified by measuring the actual rendered boxes.
   display:flex + centring added per THIS round's own request ("make the 2
   images 2/3rds the size - keep the aspect ratio and layout") — the actual
   <img> inside each cell is now sized down to 2/3 (see the shared img rule
   below) rather than filling 100% of the cell, so the cell needs to centre
   that now-smaller image within its own unchanged bounds instead of the
   image just stretching edge-to-edge as before. */
.about-bento__img1 {
  grid-area: img1;
  overflow: hidden;
  min-height: 220px;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Portrait — per the layout comment above; still square corners (no
   border-radius), matching the "no rounded eddges" request this photo's
   treatment was originally built for. Height/centring behaviour matches
   .about-bento__img1 above — see that rule's own comment. */
.about-bento__portrait {
  grid-area: portrait;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* The two bento photos, sized to 2/3 of their previous 100%-of-cell size
   per request ("make the 2 images 2/3rds the size - keep the aspect ratio
   and layout") — was width:100%/height:100% via the shared, sitewide
   .img-placeholder-fill class (see that rule); overridden here at higher
   specificity (one class + one type selector beats .img-placeholder-fill's
   own single class) so only these two photos shrink, not every other use
   of that shared class elsewhere on the site. object-fit:cover is kept, so
   the photo still crops to fill its own now-smaller box without ever
   stretching/distorting out of its natural proportions — "keep the aspect
   ratio" was already guaranteed by object-fit:cover regardless of the box
   size, since cover only ever crops, never distorts. The surrounding cell
   (the grid layout itself) is untouched — see .about-bento__img1/
   .about-bento__portrait's own comments above — so this reads as the same
   two photos simply floating smaller and centred within their original,
   unchanged layout slots, per that request's own "keep... layout"
   condition. */
.about-bento__img1 img,
.about-bento__portrait img {
  width: 66.6667%;
  height: 66.6667%;
  object-fit: cover;
}
/* Mobile override — full width instead of the desktop 2/3-size/centred
   treatment above, per request ("about mobile - can the 2 first images be
   full width - use the same proportions as the images in the homepage
   service card images"). "The images in the homepage service card images"
   = .explore-content .service-card__media's own mobile aspect-ratio
   (31/15 — see that rule's own mobile override), reused here directly
   rather than guessed independently. Same 1023px breakpoint the rest of
   this grid already collapses to a single column at (see .about-bento's
   own mobile rule above), not the site's usual 640px cutoff, so both
   photos go full-width as soon as they stack, not just on narrow phones.
   min-height:0 on the two cells (below) — their desktop min-height:220px/
   flex-centring was sized for the smaller 2/3-scale photo; at 100% width
   and this shorter 31/15 ratio the image is often under 220px tall itself,
   so left in place that floor would leave dead offwhite space around a
   centred, still-not-full-bleed image instead of true full width/height. */
@media (max-width: 1023px) {
  .about-bento__img1,
  .about-bento__portrait {
    min-height: 0;
  }
  .about-bento__img1 img,
  .about-bento__portrait img {
    width: 100%;
    height: auto;
    aspect-ratio: 31 / 15;
  }
}
/* Simon's own portrait specifically, per request ("can we crop the photo
   of simon so he's 2/3rds up the image") — object-position's vertical
   term shifts the crop window up the source photo (keeping less headroom
   above him, more of his crossed-arms/torso trimmed off the bottom
   instead), which moves where HE lands within the visible box, not where
   the box sits on the page. Tuned by eye against the actual source photo
   (simon_green.jpg) and this cell's own rendered crop, not guessed from
   the CSS alone. */
.about-bento__portrait img {
  object-position: center 15%;
}
/* Wireframe band — now a full-width horizontal strip (see the layout
   comment above for its current position, trailing both content rows),
   rather than a narrow sticky column running down the middle. position:
   sticky and the clip-path:inset(0) crop it required (sticky needs an
   ancestor with no overflow/clip of any kind, or it stops sticking — see
   this rule's own history further down) are BOTH gone now that the
   wireframe no longer scroll-follows at all, per an earlier request
   ("remove the scroll follow of the wireframe"): overflow:hidden is back to
   being the plain, ordinary crop for this window, same as every other
   wireframe window on the site (.hero-window/.case-studies-window/
   .services-hero-window).
   Border/wireframe colour — white, per the latest request ("make these
   white" — the window and the wireframe both — was berry, from an earlier
   request). background:transparent still lets the page's own offwhite show
   through behind the white border, the same "white border, offwhite fill"
   look already used for .services-hero-window; the wireframe graphic itself
   is now coloured white too via the initHeroScene call near the end of
   about.html (color: 0xFFFFFF, was 0xBE2056).
   display:flex + align-items/justify-content:center replaces the previous
   flex-for-sticky-centring workaround (no longer needed now nothing here is
   sticky) — this now just centres the canvas normally, both axes, the same
   way every other wireframe window on the site does it. min-height sets
   this band's own height (the grid's new "auto" wire row sizes to this) —
   halved to 210px (was 420px) per request ("Half the height of the
   window"), with the canvas size below (and the mobile min-height/canvas
   pair further down) scaled down by the same half to keep the wireframe's
   own proportions matching the now-shorter band rather than just getting
   cropped tighter by the unchanged overflow:hidden. */
.about-bento__wireframe {
  grid-area: wire;
  position: relative;
  z-index: 0;
  overflow: hidden;
  background: transparent;
  border: 8px solid var(--color-white);
  min-height: 210px;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Headline overlaid on top of the wireframe. Absolutely positioned over the
   full band (inset:0 + flex-centred) rather than laid out as a normal flex
   sibling of .hero-canvas-wrap, so it overlaps the canvas dead-centre
   instead of sitting beside it in the parent's own flex row. No z-index of
   its own needed to land on top of the canvas — .hero-canvas-wrap's own
   z-index:-1 (set sitewide, see that base rule) already paints it behind
   anything else in this band's stacking context (established by
   .about-bento__wireframe's own position:relative + z-index:0 above), so
   this plain in-flow-stacked heading sits above it automatically.
   pointer-events:none since it's purely a decorative label over a
   decorative wireframe, same as the wireframe graphic itself (aria-hidden
   in the markup). Kept as an <h2> (not <h1>) in the markup to avoid a
   second h1 on the page (the real page H1, "The studio behind the
   renders", lives in copy1 above) — sized like one instead via the plain
   .h1 utility class added alongside this one in the markup, per request
   ("the copy inside it should be the h1 size"; was sized like this page's
   other H2s). Text colour is left at its plain inherited black (this
   page's own body colour) rather than white, since white text over a
   white/offwhite band would have almost no contrast. */
.about-bento__wireframe-headline {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 0 32px;
  pointer-events: none;
}
/* Sized to comfortably fill the horizontal band above rather than the tall
   vertical strip's own min(80vw, 787px) — that expression was tuned for a
   narrow column spanning two whole grid rows (often taller than it is
   wide); this band is the opposite shape (wide, fixed-height), so the
   canvas is capped by height first and allowed to stay square via
   aspect-ratio, rather than ballooning to 80% of the viewport's WIDTH
   inside a much shorter box. min(60vh, 460px) is back to this rule's own
   pre-"half the height" value — doubled again per this round's own request
   ("make the rotating white shape twice the size") from the min(30vh,230px)
   that request had halved it to. The band's own min-height below is
   UNCHANGED at 210px this round (that was a separate, explicit "half the
   height of the window" request, not reopened here), so a canvas this much
   taller than its own window now crops more than it did last round —
   the same plain overflow:hidden crop this window (and every other
   wireframe window on the site) has always used, just cropping a larger
   canvas down to the same band height rather than a smaller one. Plain
   static/in-flow (was position:sticky/top:50vh/transform:translateY(-50%))
   — the parent's flex centring above handles both axes directly. */
.about-bento__wireframe .hero-canvas-wrap {
  width: min(60vh, 460px);
  height: min(60vh, 460px);
  aspect-ratio: 1 / 1;
}
@media (max-width: 1023px) {
  .about-bento__wireframe {
    min-height: 160px;
  }
  .about-bento__wireframe .hero-canvas-wrap {
    width: min(70vw, 320px);
    height: min(70vw, 320px);
  }
}

/* --------------------------------------------------------------------------
   10. PLACEHOLDER IMAGE PATTERN
   Used everywhere real photography/renders will eventually go — a diagonal
   brand-coloured hatch with a caption, so it reads as "swap me" rather than
   a broken image.
   -------------------------------------------------------------------------- */
.img-placeholder {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  background:
    repeating-linear-gradient(
      45deg,
      var(--color-grey-lighter),
      var(--color-grey-lighter) 10px,
      var(--color-grey-light) 10px,
      var(--color-grey-light) 20px
    );
}
.img-placeholder span {
  background: var(--color-black);
  color: var(--color-offwhite);
  font-family: var(--font-body);
  font-size: 11px;
  letter-spacing: 1px;
  text-transform: uppercase;
  padding: 6px 12px;
  border-radius: 0;
}

.avatar-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  background: var(--color-grey);
  color: var(--color-offwhite);
  font-family: var(--font-heading);
  font-size: 16px;
}

/* --------------------------------------------------------------------------
   11. LOGO WATERMARK (decorative, sits behind the "why work with us" heading,
   intro copy AND 4-up icon row — .why-icons-wrap wraps all three so the
   watermark centres itself on that whole block, not just the icon row).
   The watermark itself breaks out to 110% of the viewport width via the
   left:50% + negative-margin trick, independent of the container's own
   (narrower) max-width.

   .watermark centres itself (both axes) with top/left + margin-left/
   transform — deliberately NOT overflow:hidden and NOT pinned to top:0/
   bottom:0, so the logo's full height always renders regardless of how
   tall 110vw makes it relative to its wrapper — it used to get clipped top
   and bottom by exactly that. The vertical centering lives on this
   wrapper's own transform so the <img>'s transform stays free for the
   mouse-parallax effect in js/main.js. */
.why-icons-wrap {
  position: relative;
}
/* The watermark's own row — wraps the "Why work with us" heading/pill/
   intro-copy block AND the 4 icon items (but NOT the testimonials below),
   so the watermark centres itself on that combined heading+icons block and
   sits visibly behind both the text and the icon row. padding-block gives
   the watermark's own bleed (see .watermark's comment — deliberately not
   clipped) equal, guaranteed clearance above AND below before the
   testimonials row starts, so they can never overlap it regardless of
   viewport width / how tall 110vw renders the logo. */
.why-icons-row {
  position: relative;
  padding-block: 56px;
}
.watermark {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 110vw;
  margin-left: -55vw;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  z-index: 0;
}
.watermark img {
  width: 100%;
  height: auto;
  /* Source SVG has no colour built in — forced white to match the request;
     kept semi-transparent since a fully-opaque white mark would nearly
     disappear against the section's #F3F3F3 background. */
  filter: brightness(0) invert(1);
  opacity: 0.6;
  will-change: transform;
}
/* Mobile-only watermark clone — see .why-icons-grid's own markup comment in
   index.html. The plain .watermark above centres itself on the WHOLE
   heading+copy+icon-grid block (.why-icons-row), which reads fine on
   desktop but on mobile the heading/intro-copy stack tall enough above the
   now-2-column icon grid that the mark visibly centres well above the
   wireframes instead of behind them. Per request ("can this be 1.5x larger
   and centered inbetween the grid of 4 wireframes on mobile") this second,
   hidden-by-default copy of the same mark becomes the only one shown below
   768px (see the media query further down, which also hides the original
   there) — nested inside .why-icons-grid instead of .why-icons-row so its
   own centring lands on just the 4 cells. Hidden here (desktop/rest) so it
   never doubles up with the original above 768px. Shares the base .watermark
   class (and so this same img rule/filter/opacity) since both are still the
   same decorative mark, just centred on a different box at different sizes. */
.watermark--grid {
  display: none;
}
@media (max-width: 767px) {
  /* Hide the original (centres on the whole heading+grid block) so only the
     .why-icons-grid-scoped clone below shows — matches the grid-cols-2/
     md:grid-cols-4 breakpoint the icon row itself switches to 2-up at. */
  .why-icons-row > .watermark:not(.watermark--grid) {
    display: none;
  }
  .why-icons-grid {
    position: relative;
    /* Establishes a local stacking context so the negative z-index below
       only needs to out-rank this grid's own 4 (static, non-positioned)
       .why-icon-cell children, not anything else on the page. */
    z-index: 0;
  }
  .watermark--grid {
    display: flex;
    position: absolute;
    top: 50%;
    left: 50%;
    /* 1.5x the original's 110vw bleed width, per request ("1.5x larger"). */
    width: 165vw;
    margin-left: -82.5vw;
    transform: translateY(-50%);
    align-items: center;
    justify-content: center;
    pointer-events: none;
    /* Negative, not 0 — .why-icon-cell's own children (why-icon-wrap/
       why-icon-bg) are otherwise still static/non-positioned like the cells
       themselves, so a mark at z-index:0 here would still paint above them
       (any positioned element outranks non-positioned in-flow siblings
       regardless of z-index value) instead of behind, per request
       ("centered inbetween the grid of 4 wireframes"). */
    z-index: -1;
  }
}

/* "Why work with us" icons — each is a tiny live Three.js wireframe scene
   (see js/main.js section 3), static until hovered, at which point it
   starts rotating. .why-icon-bg is the full-radius disc behind it, faded/
   scaled in on hover only — berry (was lime), per request ("can these be a
   berry bg colour instead of lime") — the wireframe itself now also turns
   white on hover to match (see initWhyIcons's rig.setColor() calls in
   js/main.js), since a berry wireframe would otherwise vanish against this
   same-colour disc. Shared by both the homepage and About pages' identical
   4-icon rows, so the change applies to both. */
.why-icon-wrap {
  position: relative;
  width: 96px; /* matches the icons' old w-24 h-24 (96px) Tailwind size */
  height: 96px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.why-icon-bg {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 136px;
  height: 136px;
  margin: -68px 0 0 -68px;
  border-radius: 0;
  background: var(--color-berry);
  opacity: 0;
  transform: scale(0.75);
  transition: opacity 0.3s ease, transform 0.3s ease;
  z-index: 0;
}
.why-icon-cell:hover .why-icon-bg {
  opacity: 1;
  transform: scale(1);
}
/* The heading beneath each icon (a bare .h2, no colour of its own) turns
   berry on hover too, alongside the lime disc — same "responds to you"
   language used on every other hoverable element site-wide. Scoped to .h2
   specifically (was the broader ".why-icon-cell p", back when the heading
   was the only paragraph in the cell) now that each cell also carries its
   own expanded .regular-copy description below the heading — that stays
   its normal grey throughout, not tied to hover. */
.why-icon-cell .h2 {
  transition: color 0.3s ease;
}
.why-icon-cell:hover .h2 {
  color: var(--color-berry);
}
/* Originally homepage-only — this 4-icon row went through a full-column
   berry background-fill hover treatment for a while (added per "on hover
   make the berry background colour span the column width", since removed
   per a later request, "4 process animations. remove the current berry box
   background hover effect"). What's left: default text is plain black (the
   shared .h2 styling already gives it that with no override needed) and the
   wireframe itself is black by default too, via data-color="000000" on the
   page's 4 .why-icon-canvas elements (see initWhyIcons in js/main.js) — was
   berry by default like every other caller of that component. On hover both
   turn berry instead: the heading via the shared, unscoped
   .why-icon-cell:hover .h2 rule above (already berry, needed no
   page-specific override), the wireframe via a matching
   data-hover-color="be2056" on the same elements (was white).
   About page's own 4 icons have since been brought in line with this same
   treatment too, per request ("update the 4 x 'why work with the unbuilt'
   section points on the about page to match the homepage 4 x process
   points") — its markup now carries the identical data-color/
   data-hover-color attributes, and its own grid wrapper now also carries
   this ".why-icons-row" class specifically so the opacity:0 rule just below
   reaches it too (see that page's own fuller comment on its markup). Every
   other caller of initWhyIcons elsewhere on the site (project pages' CTA
   icons etc.) is untouched and keeps the original berry-default/white-hover
   look, since none of them carry this class. */
.why-icons-row .why-icon-cell:hover .why-icon-bg {
  /* The small square .why-icon-bg disc behind the wireframe still fires on
     hover via the shared, unscoped .why-icon-cell:hover rule above (opacity
     1) — hidden again here wherever this class is applied, same as when
     this was fixed for the previous full-column background (per "4 x
     process columns - on hover there's a clash with berry background
     colours here too. Can the one that's containing the wireframe be
     removed"): now that the wireframe itself turns berry on hover too, a
     visible berry disc behind it would make the two the same colour and the
     wireframe would vanish into its own backdrop. */
  opacity: 0;
}
/* Mobile only (matches the grid-cols-2 → md:grid-cols-4 breakpoint above):
   this label was reading bigger/heavier than the page's actual h1 titles
   on narrow viewports, per request ("fix the sizing of the 4 ... They're
   currently bigger than the main titles on the page"). Not a case of .h2
   itself being mis-sized in the type scale — it's a fixed 26px while .h1
   scales down with clamp(), so on mobile they sit close together (34px vs
   26px), and Funnel Display's 500-weight renders visually heavier than
   Bebas Neue at a similar size — compounded by "Direct collaboration with
   your team" wrapping across up to 4 lines in the narrow 2-col mobile
   grid. Scoped to this instance only, not .h2 generally, which is sized
   correctly everywhere else it's used (section subheadings). */
@media (max-width: 767px) {
  .why-icon-cell .h2 {
    font-size: 20px;
    line-height: 26px;
  }
}
.why-icon-canvas {
  position: relative;
  z-index: 1;
  width: 96px;
  height: 96px;
}
.why-icon-canvas canvas {
  width: 100% !important;
  height: 100% !important;
  display: block;
}

/* --------------------------------------------------------------------------
   12. FOOTER
   -------------------------------------------------------------------------- */
/* Inverted per request ("make the footer inverted? black background colour
   and everything on it white - social hover colour light grey") — was a
   white background with black/grey text and dark social icons. Everything
   below that isn't plain inherited text colour (the logo image, the grey
   contact/copyright copy, the social icons and their hover) needs its own
   explicit override, since none of those follow `color` by inheritance. */
.site-footer {
  background: var(--color-black);
  color: var(--color-white);
  padding-block: 50px;
}
.site-footer a:hover { color: var(--color-berry); }
/* The footer logo (UB_Final_logo-02.svg) is the same kind of colourless
   dark-shape source as every other logo file in this family (see
   .site-header__mark/__wordmark img and .watermark img elsewhere in this
   file, both "no colour built in, forced white with a filter") — targeted
   via .container's direct-child img rather than a class, since the logo
   <img> in the markup carries none, just Tailwind sizing utilities; the
   social icons are nested two levels deeper (inside .site-footer__meta >
   .site-footer__social), so this direct-child selector reaches only the
   logo. */
.site-footer .container > img {
  filter: brightness(0) invert(1);
}
/* Contact info + copyright previously read at .text-grey (mid-grey) against
   the old white background — now forced to white so "everything on it" is
   white as requested, rather than left at a grey that would low-contrast
   against the new black fill. Scoped to .site-footer (rather than
   redefining .text-grey itself, a sitewide utility used all over the rest
   of the page) — this compound selector is also more specific than the
   bare Tailwind utility class in the markup, so it wins outright rather
   than needing the injection-order tie-break .site-footer .site-footer__
   contact/__meta rely on further down this section. */
.site-footer .text-grey {
  color: var(--color-white);
}
.site-footer__social a {
  display: inline-flex;
  width: 24px;
  height: 24px;
}
/* Social icons — same "dark shape, no colour built in" source as the
   off-canvas nav's own .site-nav__social (see that rule's comment), now
   forced white the same way since this footer sits on a solid black
   background too. Scoped to .site-footer specifically (rather than the
   bare .site-footer__social class the rule used to be written on) because
   the about page's founder-social block reuses this same class on its own
   light background and must keep its original dark-on-light look —
   compounding with .site-footer here leaves that other usage matched by
   nothing but its own original (unscoped) rules, unaffected. */
.site-footer .site-footer__social img {
  filter: brightness(0) invert(1);
  transition: filter 0.3s ease;
}
/* Hover: light grey (was berry), per request ("social hover colour light
   grey") — same brightness(0)-first recipe technique as .site-nav__social's
   own hover (re-flattening the now-white icon back to black before
   re-tinting), but landing on a plain neutral --color-grey-light
   (#D1D1D1) rather than a hued colour, so no sepia/saturate/hue-rotate step
   is needed at all: brightness(0) → black, invert(1) → white, then a single
   brightness(81.9608%) scales that white down to exactly rgb(209,209,209)
   (209/255), since a pure grey needs only dimming, no hue shift. Scoped to
   .site-footer for the same reason as the rest-state rule above — the about
   page's founder-social keeps its own original berry hover untouched. */
.site-footer .site-footer__social a:hover img {
  filter: brightness(0) invert(1) brightness(81.9608%);
}
/* Unscoped legacy rule, still matched by the about page's founder-social
   block only (the .site-footer descendant now always wins inside the real
   footer via the scoped rules above) — dark icons at rest, berry on hover,
   unchanged from before this section's footer became inverted. */
.site-footer__social img {
  filter: none;
  transition: filter 0.3s ease;
}
.site-footer__social a:hover img {
  filter: invert(21%) sepia(80%) saturate(2500%) hue-rotate(320deg) brightness(83%) contrast(95%);
}
/* "Members of..." accreditation logo — moved into the social icons row
   and resized to match them, per request ("move the north east logo and
   align it to the top right where the social icons are, reduce the height
   of the logo to match the social icons"). Previously sat as its own
   sibling next to the contact info column (40px-tall, matching the main
   footer logo); now a leading item inside .site-footer__social itself (see
   each HTML file's footer markup) so it shares that row's flex layout —
   its own top-right position and vertical centring against the icons come
   from .site-footer__social's existing "flex items-center" utilities, with
   no extra positioning rule needed here. */
.site-footer__members {
  /* Matches .site-footer__social a's own 24px icon size exactly, rather
     than the old 30px. */
  height: 24px;
  width: auto;
  /* Extra breathing room to the icon after it, per follow-up request ("add
     a bit more padding between social icons and member logo") — on top of
     .site-footer__social's own 16px flex gap (Tailwind's gap-4, shared by
     every item in that row), for ~32px total between this logo and the
     first icon. margin-right rather than raising that shared gap itself,
     so the two social icons stay exactly as close together as before. */
  margin-right: 16px;
}
/* The footer's own flex row can run out of horizontal room at medium
   widths now that the new "Members of" logo joins it above — without this,
   every item was forced into a single row (this container has no wrap of
   its own) and the social/copyright column got pushed off the right edge
   of the viewport around tablet widths. row-gap is set explicitly smaller
   than the row's own 96px column gap (Tailwind's gap-24) so a wrapped
   second line doesn't sit unusually far below the first. */
.site-footer .container {
  flex-wrap: wrap;
  row-gap: 24px;
}
/* Mobile — centre the contact info, social icons and copyright line, per
   request ("center contact info, social icons and copyright number").
   Above md, .container's own md:items-start/md:items-end classes already
   left/right-align these two groups; below md the flex-col default
   (align-items:stretch) instead stretches each text/icon row to the full
   column width, which left-aligns everything by default rather than
   centring it. text-align covers the multi-line contact block; align-items
   re-centres the icon row and copyright line as blocks rather than letting
   them stretch full-width and sit flush left.
   Selectors compounded with .site-footer (rather than bare .site-footer__
   contact/__meta) deliberately outrank the Tailwind CDN's own `.items-start`
   utility already sitting on .site-footer__meta in the markup — same single-
   class specificity as a bare rule here would have, but Tailwind's <style>
   tag is injected at runtime (after this stylesheet loads), so on a
   specificity tie it would otherwise win the cascade by coming later and
   the social icons/copyright stayed left-aligned despite this rule. */
@media (max-width: 767px) {
  .site-footer .site-footer__contact {
    align-items: center;
    text-align: center;
  }
  .site-footer .site-footer__meta {
    align-items: center;
    text-align: center;
  }
}

/* --------------------------------------------------------------------------
   13. FAQ ACCORDION
   Single-open accordion (js/main.js initFaqAccordion) — the trigger row is
   plain CSS/markup, the panel's open/close is a height animation handled
   in JS (GSAP animates straight to height:'auto', so no manual max-height
   guessing). padding lives on .faq-item__panel-inner rather than the panel
   itself — box-sizing:border-box means padding on an element with an
   explicit height:0 target can't actually collapse to 0, it fights the
   animation and leaves a visible sliver.
   -------------------------------------------------------------------------- */
.faq-list { border-top: 1px solid var(--color-grey-light); }
.faq-item { border-bottom: 1px solid var(--color-grey-light); }
.faq-item__trigger {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding-block: 28px;
  background: none;
  border: none;
  text-align: left;
  cursor: pointer;
  color: inherit;
  font: inherit;
}
.faq-item__icon {
  position: relative;
  flex-shrink: 0;
  width: 20px;
  height: 20px;
}
.faq-item__icon::before,
.faq-item__icon::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  background: var(--color-berry);
  transform: translate(-50%, -50%);
  transition: transform 0.3s ease;
}
.faq-item__icon::before { width: 20px; height: 2px; }
.faq-item__icon::after { width: 2px; height: 20px; }
/* Vertical bar rotates flat onto the horizontal one — plus becomes minus. */
.faq-item[data-open="true"] .faq-item__icon::after { transform: translate(-50%, -50%) rotate(90deg); }
.faq-item__panel { height: 0; overflow: hidden; }
.faq-item__panel-inner { max-width: 640px; padding-bottom: 28px; }

/* --------------------------------------------------------------------------
   15. LOGO MARQUEE
   Pure-CSS infinite scroll: .marquee__track holds the item set twice back
   to back and animates from translateX(0) to translateX(var(--marquee-distance))
   — since the two halves are identical, the moment the first half has
   scrolled fully past it looks indistinguishable from the start, so the
   loop reads as continuous rather than snapping.

   --marquee-distance used to just be a flat -50%, which judders on repeat
   now and again: -50% is resolved against the track's own rendered (and
   sub-pixel-rounded) width, which doesn't reliably land on the exact
   boundary between the two identical item sets, so the loop occasionally
   snaps a fraction of a pixel short/long. js/main.js (initMarquee) now
   computes the first set's precise pixel width itself (same measurement it
   already uses for --marquee-duration) and sets --marquee-distance to that
   exact px value, so the animation always travels precisely one set's
   width with no rounding drift.

   --marquee-gap / --marquee-duration / --marquee-distance are set on
   .marquee__track by js/main.js (initMarquee) on load + resize: it
   measures the container and the logos' own rendered width, then solves
   for the gap that lands ~7 logos in view at once at the current width,
   and a duration that keeps scroll speed (px/s) constant no matter how
   that gap changes — a wide desktop viewport isn't just "the same speed
   for longer", it'd otherwise look faster once the gap grows. The values
   below are the fallback for when JS is unavailable. */
.marquee {
  overflow: hidden;
  width: 100%;
  -webkit-mask-image: linear-gradient(90deg, transparent, black 6%, black 94%, transparent);
  mask-image: linear-gradient(90deg, transparent, black 6%, black 94%, transparent);
}
.marquee__track {
  display: flex;
  align-items: center;
  gap: var(--marquee-gap, 64px);
  width: max-content;
  animation: marquee-scroll var(--marquee-duration, 34s) linear infinite;
}
.marquee:hover .marquee__track,
.marquee--paused .marquee__track { animation-play-state: paused; }
/* Real partner logos vary a lot in native aspect ratio (compact square
   marks vs wide wordmarks — see assets/partner_logos), so height is the
   only dimension pinned here; width stays auto and follows from it, which
   is what makes every logo read as "the same size" despite very different
   shapes. Logos sit dimmed at rest and lift to full strength + a slight
   scale on hover/focus — reads as considerably more polished than a static
   flat row, and gives the individual logo a "response" beyond the whole
   track just pausing. */
.marquee__item {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 72px;
}
.marquee__item img {
  height: 44px;
  width: auto;
  max-width: 184px;
  object-fit: contain;
  opacity: 0.55;
  transition: opacity 0.3s ease, transform 0.3s ease;
}
.marquee__item:hover img,
.marquee__item:focus-within img {
  opacity: 1;
  transform: scale(1.08);
}
@keyframes marquee-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(var(--marquee-distance, -50%)); }
}
@media (prefers-reduced-motion: reduce) {
  .marquee__track { animation: none; }
  .marquee__item img { opacity: 0.85; }
}

/* --------------------------------------------------------------------------
   17. PORTFOLIO FILTER
   -------------------------------------------------------------------------- */
.portfolio-filter {
  display: flex;
  /* flex-wrap:wrap (the previous value) let this row shrink to fit inside
     .case-studies-window__copy's own narrower flex share of the window,
     wrapping the 4 buttons onto a second line once they didn't all fit —
     per request ("can the tags run across the width of the page in the
     container (currently the 4 are wrapping) i don't mind if they go over
     the wireframe"), nowrap + width:max-content instead let this row size
     to its own natural (unwrapped) content width regardless of its flex
     parent's width, so it now overflows rightward past the copy column's
     own edge, over the wireframe panel beside it, rather than wrapping.
     Nothing clips that overflow: .case-studies-window__copy itself sets no
     overflow of its own, and .case-studies-window's own overflow:hidden
     only clips at the FULL window's outer edge (see that rule's own
     comment) — well past where these 4 buttons now reach. The wireframe's
     own canvas paints at z-index:-1 inside that same window (see
     .hero-canvas-wrap), so this row's own default (auto) stacking always
     paints in front of it wherever the two overlap, exactly per "I don't
     mind if they go over the wireframe" (over, not under). */
  flex-wrap: nowrap;
  width: max-content;
  max-width: none;
  gap: 12px;
}
/* Below 1024px .case-studies-window itself switches to a stacked column
   (copy above wireframe, see that rule's own mobile override) and the copy
   panel goes full viewport width instead of sharing the row with the
   wireframe — nowrap's own reason for existing (avoiding the wireframe
   panel eating into this row's available width) no longer applies there,
   and an unwrapped max-content row of all 4 (fairly long) filter labels
   would instead overflow the viewport itself and force horizontal page
   scroll. Wrapping is restored at this breakpoint to avoid that regression
   — the "run across the width of the page ... over the wireframe" request
   was about the desktop side-by-side layout specifically. */
@media (max-width: 1023px) {
  .portfolio-filter {
    flex-wrap: wrap;
    width: auto;
  }
}
/* Sized down to match the homepage eyebrow pill (.pill-tag), per request
   ("make the filter pills smaller (match size of the homepage eyebrow
   pill)") — padding/font-size scaled down the same way .pill-tag's own
   size-down pass did (radius eased down alongside them too, so these still
   read as fully rounded capsules rather than going squarer as they shrink,
   same reasoning as .pill-tag's own radius). Colour/border and the active/
   hover states below are untouched — those aren't part of "size".
   Typography bumped back up slightly per a later request ("make the tag
   filters slightly bigger using the font styling of the 'filter' label") —
   font-size/weight/letter-spacing/text-transform now match .h4 exactly (the
   removed "Filter" label's own style, see the PROJECTS section in
   case-studies.html), rather than this component's smaller bespoke values.
   Padding/radius/border/colours stay exactly as the size-down pass left
   them — this is a typography-only change, not a full revert.

   border-radius bumped to 999px per request ("make the filter pills ...
   have fully rounded corners so they are cylindrical") — 14px read as
   softened corners rather than a true capsule at this padding/font-size;
   999px guarantees a full cylinder regardless of the button's rendered
   height. (At the time this was also "the same convention as .portfolio-
   grid .service-card .title-tag's own fully-rounded pill" — that card pill
   has since moved onto the image itself and shrunk to a 5px-radius chip,
   per a later request, so the two no longer match; this filter pill's own
   999px radius is untouched by that.)

   Padding bumped back up to 10px 18px per a further request ("can the
   filter tags be larger (like the ones in the image cards)") — this is
   the exact padding .portfolio-grid .service-card .title-tag used at the
   time of that request (before it moved onto the image and shrank, see
   that rule's own comment) — i.e. "larger" meant "back to that size",
   not a new bespoke value. Border-radius/border/colours/typography are
   all untouched, still exactly as the two passes above left them.

   border-radius removed per a further request ("remove border radius on
   portfolio filter tags") — square corners now, reversing the earlier
   "make it fully rounded/cylindrical" pass above. Padding/border/colours/
   typography all untouched — square corners is the only change here. */
.portfolio-filter__btn {
  padding: 10px 18px;
  border-radius: 0;
  border: 1px solid var(--color-grey-light);
  background: var(--color-white);
  color: var(--color-grey);
  font-family: var(--font-heading);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 1.8px;
  text-transform: uppercase;
  cursor: pointer;
  transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
.portfolio-filter__btn:hover { border-color: var(--color-berry); color: var(--color-berry); }
.portfolio-filter__btn[aria-pressed="true"] {
  background: var(--color-berry);
  border-color: var(--color-berry);
  color: var(--color-white); /* was lime — changed to white by request for contrast/legibility on the berry fill */
}
/* Project-count badge — appended by js/main.js (initPortfolioFilter) after
   each button's own label, e.g. "Exterior Visualisation (3)". Inherits the
   button's current colour (grey/berry/lime depending on state) via
   color:inherit, just dimmed slightly so the label itself stays primary. */
.portfolio-filter__count {
  margin-left: 6px;
  opacity: 0.65;
  font-weight: 400;
}

/* --------------------------------------------------------------------------
   17b. PORTFOLIO SIDEBAR
   Quick-link list of every project, jumping straight to its own page. Used
   to also hold the category filter (grouped together as "ways to navigate
   the projects"), but that moved out to its own row above this whole
   layout per request ("move the filter options to a row under the page
   intro copy") — see .portfolio-filter above. Was also a sticky column
   sharing a 2-col grid row with the card grid beside it; per a later
   request ("move projects list to underneath the filter options") it now
   sits in its own full-width row above the grid instead, so position:sticky
   no longer applies here (there's nothing beside it for it to stay in view
   alongside any more) — dropped entirely rather than left dead. Still just
   the one quick-link group it started as; name kept as-is (not renamed to
   match, e.g., ".portfolio-projects-list") since it's an internal class/
   file-name identifier, not on-page copy — see case-studies.html's own
   note on this.
   -------------------------------------------------------------------------- */
.portfolio-sidebar {
  display: flex;
  flex-direction: column;
}
/* The quick-links themselves are .btn-tertiary btn-tertiary--reveal-icon
   (see "4c. TERTIARY BUTTON" above) rather than a bespoke class — berry
   copy at rest, black on hover, underline draw-in, caret reveals in from
   the left on hover. No sidebar-specific override needed; the shared
   component covers this use exactly as built. */

/* Mobile/tablet-only project dropdown (below lg) — replaces the sidebar's
   plain link list, per request ("can projects be in a dropdown menu").
   Native <select> rather than a custom-built disclosure: full built-in
   keyboard/screen-reader support and the OS's own picker UI for free,
   which matters more here than matching the site's bespoke component
   styling pixel-for-pixel. appearance:none removes the browser's own
   arrow; the chevron is a separate element (.portfolio-mobile-select__
   chevron below) rather than living directly on the <select> itself,
   because caret-right.svg is drawn pointing right and a background-image
   can't be transformed on its own independent of its element — as a real
   element it can be rotated 90deg to point down and read clearly as "this
   opens a dropdown". The OS still owns the open dropdown list itself. */
.portfolio-mobile-select-wrap {
  position: relative;
}
.portfolio-mobile-select {
  display: block;
  width: 100%;
  appearance: none;
  -webkit-appearance: none;
  background: var(--color-white);
  border: 1px solid var(--color-grey-light);
  border-radius: 0;
  padding: 14px 44px 14px 20px;
  font-family: var(--font-heading);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.4px;
  color: var(--color-black);
  cursor: pointer;
  transition: border-color 0.2s ease;
}
.portfolio-mobile-select:hover,
.portfolio-mobile-select:focus {
  border-color: var(--color-berry);
}
.portfolio-mobile-select__chevron {
  position: absolute;
  top: 50%;
  right: 20px;
  /* caret-right.svg, per-request replacement for chevron_berry.svg — sized
     up from the old 7x13 for the same reason as .btn-secondary-icon above
     (square source glyph reads smaller at a given box size than the old
     slim rectangular chevron did). */
  width: 18px;
  height: 18px;
  transform: translateY(-50%) rotate(90deg);
  background: url('../assets/svg_graphics/caret-right.svg') no-repeat center / 18px 18px;
  /* Source SVG is flat white — recoloured to berry with the same
     numerically-solved brightness(0)-first recipe used throughout this
     file; `filter` works on background-image content same as on an <img>,
     since it's a whole-element effect rather than one scoped to <img>. */
  filter: brightness(0) invert(11.6%) sepia(88.4%) saturate(593.5%) hue-rotate(291.5deg) brightness(235.5%) contrast(129.6%);
  pointer-events: none;
}

/* Small "back to portfolio" link used at the top of each project page. */
.back-link {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--color-grey);
  transition: color 0.2s ease;
}
.back-link:hover { color: var(--color-berry); }

/* --------------------------------------------------------------------------
   18. SCROLL-REVEAL HOOKS
   Everything below starts visible in plain HTML/CSS (no-JS / no-motion
   fallback) — js/main.js sets the real initial state via GSAP before
   animating in on scroll, so nothing is ever permanently hidden if a
   script fails to load.

   [data-reveal] / [data-reveal-group="x"]         — simple fade in, no
   [data-reveal-box] / [data-reveal-box-group="x"] — movement, for either
     text (headings, paragraphs, captions) or containers/images (cards,
     media tiles, photo placeholders) — see initScrollReveal in js/main.js.
     (An earlier version of this drew a wireframe border in before fading,
     removed site-wide by request: "I just want them to fade in but be
     staggered" — .media-loading just below is a distinct, later feature:
     an actual image-loading indicator, not a decorative reveal effect.)
   -------------------------------------------------------------------------- */
[data-reveal],
[data-reveal-box] { will-change: opacity; }
@media (prefers-reduced-motion: reduce) {
  [data-reveal],
  [data-reveal-group],
  [data-reveal-box],
  [data-reveal-box-group] { opacity: 1 !important; transform: none !important; }
  .scroll-hint__line::before { animation: none; opacity: 1; }
}

/* Image loading placeholder — see initMediaLoadBorders in js/main.js, which
   toggles this class on a photo frame for exactly as long as its <img> is
   still downloading. ::before rather than the frame's own border/box-
   shadow so it can't collide with any hover box-shadow a frame's outer
   card already animates (.project-card--row, .service-card, etc. both
   tween their own box-shadow on hover) or shift the frame's box size the
   way a real border would. border-radius:inherit keeps it flush with
   whatever radius the specific frame already uses (25px cards, 10px
   portrait, 100px avatar circle, ...) with no value needed here. */
.media-loading {
  position: relative;
}
.media-loading::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 2;
  border-radius: inherit;
  box-shadow: inset 0 0 0 1px var(--color-grey-light);
  pointer-events: none;
}

/* [data-lightbox] — set by initProjectLightbox() on each wrapper it wires
   up (the lead media + gallery tiles that hold a real <img>), giving a
   zoom-in cursor and a gentle hover scale so the images read as clickable
   before the overlay itself is ever touched. Kept as its own attribute
   rather than piggybacking on .project-card__media's existing hover states,
   since other uses of that class (the "next case study" row, the "have a
   project in mind" icon cell) aren't lightbox triggers and shouldn't gain
   this affordance. */
[data-lightbox] img {
  cursor: zoom-in;
  transition: transform 0.5s ease;
}
[data-lightbox]:hover img {
  transform: scale(1.04);
}

/* --------------------------------------------------------------------------
   19. PROJECT GALLERY LIGHTBOX (project-*.html)
   Full-screen click-to-expand slideshow for a case study's lead photo +
   gallery grid, per request ("on the project pages can we click on the
   images to expand them into a slideshow/gallery"). Markup is built and
   appended to <body> by initProjectLightbox() in js/main.js — see that
   function for which images are wired up (real photos only; placeholder
   tiles and the one page with a lead video are skipped).

   z-index 60 sits just above .site-nav's 50, so opening the lightbox from a
   project page never gets hidden behind the off-canvas nav layer, and the
   dark backdrop + fade/scale-in treatment intentionally echoes .site-nav's
   own black backdrop + transition timing so the two full-screen overlays
   feel like the same system rather than two different components. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(15, 15, 15, 0.96);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.35s ease, visibility 0s linear 0.35s;
}
.lightbox.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.35s ease, visibility 0s linear 0s;
}
.lightbox__figure {
  position: relative;
  max-width: min(1200px, 86vw);
  max-height: 80vh;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 16px;
  transform: scale(0.97);
  transition: transform 0.35s ease;
}
.lightbox.is-open .lightbox__figure {
  transform: scale(1);
}
.lightbox__img {
  display: block;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 80vh;
  object-fit: contain;
  border-radius: 0;
}
.lightbox__caption {
  color: var(--color-offwhite);
  font-family: var(--font-body);
  font-size: 14px;
  text-align: center;
}
.lightbox__caption:empty { display: none; }
.lightbox__count {
  position: absolute;
  bottom: 24px;
  left: 50%;
  transform: translateX(-50%);
  color: var(--color-grey-light);
  font-size: 13px;
}
/* Close + prev/next controls share the same transparent-until-hover treatment
   as .site-nav__close (see "6. SITE HEADER / NAV" above) so this overlay's
   buttons read as part of the same visual language. */
.lightbox__close,
.lightbox__nav {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  border: 1px solid var(--color-grey);
  background: none;
  color: var(--color-offwhite);
  cursor: pointer;
  transition: border-color 0.3s ease, color 0.3s ease, background-color 0.3s ease;
}
.lightbox__close:hover,
.lightbox__nav:hover {
  border-color: var(--color-lime);
  color: var(--color-lime);
}
.lightbox__close {
  top: 24px;
  right: 24px;
}
.lightbox__close svg {
  fill: currentColor;
}
.lightbox__nav {
  top: 50%;
  transform: translateY(-50%);
}
.lightbox__nav img {
  width: 14px;
  height: 14px;
  /* caret-right.svg is already berry-coloured art rather than a plain
     currentColor glyph, so the hover state above only moves the ring/border
     to lime — filtered to offwhite here just to sit correctly on the dark
     backdrop at rest. */
  filter: brightness(0) saturate(100%) invert(93%) sepia(4%) saturate(147%) hue-rotate(315deg) brightness(97%);
}
.lightbox__nav--prev {
  left: 24px;
}
.lightbox__nav--prev img {
  transform: scaleX(-1);
}
.lightbox__nav--next {
  right: 24px;
}
/* Single-image pages (nothing to page through) hide the two arrows entirely
   rather than leave a prev/next pair that only ever cycles back to the same
   photo — see initProjectLightbox's single-image branch in js/main.js. */
.lightbox__nav[hidden] {
  display: none;
}
@media (max-width: 640px) {
  .lightbox__close,
  .lightbox__nav {
    width: 40px;
    height: 40px;
  }
  .lightbox__close { top: 12px; right: 12px; }
  .lightbox__nav--prev { left: 12px; }
  .lightbox__nav--next { right: 12px; }
  .lightbox__count { bottom: 12px; }
}

/* --------------------------------------------------------------------------
   20. MOBILE — always-visible CTA reveal icons
   Per request ("on all ctas that have an icon appear on hover (chevron/
   envelope paper plane etc) can these be visible at all times on mobile -
   as we aren't able to hover on mobile screens"). Every "icon that pops in
   on :hover" component sitewide is collapsed to zero width/opacity at rest
   and only reaches its visible size via a `:hover` selector — which a touch
   screen never triggers, so mobile visitors never see (or discover) any of
   these at all. Each one below is forced straight to its own existing
   hover end-state permanently under this breakpoint, copying that
   component's own hover values exactly rather than inventing new ones, so
   mobile gets the identical finished look desktop only reaches on hover.
   Matches the site's usual 640px mobile cutoff, used everywhere else this
   file needs one.

   Several of these also need their icon recoloured here, not just
   revealed, per a follow-up request ("on all buttons on mobile where the
   icon is present can we make sure it's the same colour as the copy") —
   every one of these icons is normally only visible ALONGSIDE whatever
   colour change its own :hover state also makes (the button/chip fill
   flips, or the copy itself turns berry), so simply un-collapsing the icon
   without also fixing its colour risks it landing in whatever colour its
   rest-state filter happened to be tuned for on desktop, not the colour
   its neighbouring copy is actually sitting in now that hover never fires:
     - .btn-quote-chevron/.btn-send-icon sit on .btn-primary's own berry
       fill, where the copy is white at rest (only turning berry on the
       hover that also whites-out the fill) — recoloured white to match.
     - .service-card-caption__chevron's copy is plain .text-black at rest
       in every context except one (only turning white together with the
       chip's own hover-berry fill) — recoloured black (brightness(0)) to
       match, EXCEPT .project-card--row.project-card--inverted's own copy,
       which is berry at rest, not black; that variant already has its own
       higher-specificity berry icon rule further up this file (see its own
       comment there), so it's untouched by the generic black override here.
     - .contact-info__chevron and .service-lede__chevron's copy is also
       plain black at rest (only turning berry on hover) even though both
       icons carry an unconditional berry filter in their own base rule
       (tuned for sitting beside hover-berry copy) — recoloured black here
       to match what the copy actually is now that hover never fires.
   .btn-tertiary-icon needs no recolour — .btn-tertiary's own copy is
   already berry AT REST (not just on hover), matching its icon's own
   unconditional berry filter already, so simply un-collapsing it is
   enough. */
@media (max-width: 640px) {
  /* Portfolio sidebar quick-links arrow (.btn-tertiary--reveal-icon) —
     already berry to match its own already-berry-at-rest copy. */
  .btn-tertiary--reveal-icon .btn-tertiary-icon {
    width: 16px;
    opacity: 1;
    margin-right: 8px;
  }
  /* "Get a quote" / footer CTA arrow — recoloured white to match the
     button's own white-at-rest copy, rather than its own unconditional
     berry filter (tuned for the white hover fill this never reaches on
     touch). */
  .btn-quote-chevron {
    width: 26px;
    padding-left: 10px;
    padding-right: 0;
    margin-right: -2px;
    opacity: 1;
    margin-left: 10px;
  }
  .btn-quote-chevron img {
    filter: brightness(0) invert(1);
  }
  /* .btn-primary--white buttons (services.html's "Get a quote", the
     homepage/about footer "Get in touch" CTAs) are white-bg/berry-copy at
     rest, not white-copy-on-berry like every other .btn-primary — see
     .btn-primary--white's own comment in "4. BUTTON COMPONENTS" above. The
     generic white recolour just above (tuned for those other buttons'
     white-at-rest copy) instead makes this variant's chevron invisible
     against its own white fill, per report ("the chevron is not the same
     colour as the copy - or it's not visible"). Reapplies the exact berry
     filter the base (desktop) .btn-quote-chevron img rule already carries —
     this variant's copy is berry in every state, so no hover-specific value
     is needed. Compound selector always outranks the plain .btn-quote-
     chevron img rule above regardless of source order. */
  .btn-primary--white .btn-quote-chevron img {
    filter: brightness(0) invert(11.6%) sepia(88.4%) saturate(593.5%) hue-rotate(291.5deg) brightness(235.5%) contrast(129.6%);
  }
  /* Contact page "Send message" paper-plane icon — same recolour reasoning
     as the chevron just above. */
  .btn-send-icon {
    width: 26px;
    padding-left: 10px;
    padding-right: 0;
    margin-right: -2px;
    opacity: 1;
    margin-left: 10px;
  }
  .btn-send-icon img {
    filter: brightness(0) invert(1);
  }
  /* Contact info links (email/phone) arrow — recoloured black
     (brightness(0), the same flat-conversion recipe .btn-tertiary:hover
     .btn-tertiary-icon img already uses elsewhere in this file) to match
     its own copy's black-at-rest colour, rather than its base rule's
     unconditional berry (tuned for the hover-berry copy this never reaches
     on touch). */
  .contact-info__chevron {
    width: 16px;
    opacity: 1;
    margin-right: 8px;
  }
  .contact-info__chevron img {
    filter: brightness(0);
  }
  /* Card caption chevrons — Explore section, homepage featured grid,
     portfolio grid and project-adjacent cards all share this one class.
     Recoloured black to match their own .text-black copy at rest, rather
     than berry — this is a lower-specificity duplicate of the
     .project-card--row.project-card--inverted variant's own (still berry,
     to match ITS berry-at-rest copy) rule further up this file, so it
     changes nothing there — only the other (black-copy) contexts pick up
     this black version. */
  .service-card-caption__chevron {
    width: 16px;
    opacity: 1;
    margin-left: 6px;
  }
  .service-card-caption__chevron img {
    filter: brightness(0);
  }
  /* Services page 5-shortcut lede chevrons — recoloured black to match
     .service-lede__title's own black-at-rest copy, rather than its base
     rule's unconditional berry (tuned for the hover-berry title this never
     reaches on touch).
     Also repositioned here (rather than left inline at the bottom of
     .service-lede__copy's own stacked title+paragraph, its plain DOM
     position) per the "number | icon | title+copy | chevron" row layout
     request just above in this file (see .service-lede__copy's own mobile
     rule, "9b1 SERVICE LEDE" further up) — pulled out of flow and pinned to
     the vertical centre of that wrapper's own right edge (which
     .service-lede__copy's position:relative + padding-right there reserves
     room for) rather than the desktop rule's translateY(-4px)/(0) hover
     slide, which only makes sense for an icon sitting inline below text. */
  .service-lede__chevron {
    opacity: 1;
    position: absolute;
    top: 50%;
    right: 0;
    margin-top: 0;
    transform: translateY(-50%);
    filter: brightness(0);
  }
}

/* Testimonial names sit on the card base so they align across all three cards, whatever the quote length. */
.testimonial-card figcaption {
  margin-top: auto;
}

/* Services banners that are links/embeds: animation banner links to the showreel with a play badge; virtual tours holds the live splat viewer (taller than the 21:9 still so it is usable). */
.service-banner-media--link { position: relative; display: block; }
.service-banner-media__play {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  background: rgba(0, 0, 0, 0.18); transition: background 0.3s ease;
}
.service-banner-media__play img { width: 72px; height: 72px; filter: invert(1); transition: transform 0.3s ease; }
.service-banner-media--link:hover .service-banner-media__play { background: rgba(0, 0, 0, 0.32); }
.service-banner-media--link:hover .service-banner-media__play img { transform: scale(1.08); }
.service-banner-media--viewer { aspect-ratio: 16 / 10; }


/* Lead video in a project gallery spans the full grid width. */
.project-card__media--video { grid-column: 1 / -1; aspect-ratio: 16 / 9; }

/* Mobile side-scroll fix: full-bleed decorative bits (e.g. the 165vw .watermark--grid on the homepage) were widening the phone layout viewport even though html clips on desktop. overflow-x:clip on each top-level section trims them without making a scroll container, so position:sticky is unaffected (see the html overflow note above). */
body > section { overflow-x: clip; }

.service-banner-media__play { border: 0; width: 100%; padding: 0; cursor: pointer; }

/* About: How we work accordion. Reuses the FAQ accordion; each row leads with a berry pill and fills solid berry when open. */
.process-trigger { display: flex; align-items: center; gap: 20px; }
.process-trigger .pill-tag { flex-shrink: 0; min-width: 84px; justify-content: center; transition: background 0.3s ease, color 0.3s ease; }
.process-list .faq-item__trigger:hover .h3 { color: var(--color-berry); }
.process-list .faq-item[data-open="true"] .pill-tag { background: var(--color-berry); color: var(--color-white); }
.process-body .process-label { color: var(--color-berry); }
.process-body ul { list-style: none; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.process-body li { position: relative; padding-left: 22px; }
.process-body li::before { content: ''; position: absolute; left: 0; top: 0.55em; width: 8px; height: 8px; border-radius: 50%; background: var(--color-berry); }
.process-list .faq-item:last-child[data-open="true"] .faq-item__panel-inner { padding-bottom: 28px; }
@media (max-width: 640px) { .process-trigger { flex-direction: column; align-items: flex-start; gap: 10px; } }

/* Pink legal bar under the black footer. */
.site-legal { background: var(--color-berry); color: var(--color-white); padding-block: 16px; font-size: 12px; letter-spacing: 0.5px; }
.site-legal__inner { display: flex; flex-wrap: wrap; gap: 8px 24px; align-items: center; justify-content: space-between; }
.site-legal nav { display: flex; gap: 12px; align-items: center; }
.site-legal a { color: inherit; text-decoration: underline; text-underline-offset: 3px; }
.site-legal a:hover { color: var(--color-white); text-decoration-thickness: 2px; }

/* Mobile services shortcut list: Tailwind's items-center on .service-lede__copy beat the left-align rule above, so one-word titles centred while wrapped ones sat left. */
@media (max-width: 640px) {
  .service-lede__copy { align-items: flex-start !important; }
  .service-lede__copy .service-lede__title { text-align: left; }
}
@media (max-width: 640px) {
  /* Fixed icon column so every title starts at the same x. */
  .service-lede__icon { width: 56px; flex: none; display: flex; justify-content: center; }
}
