/* ==========================================================================
   GRO - Get Ranked Online
   Landing page styles. Built on the Dark GRO Design System.
   Token values are lifted verbatim from "GRO Brand Guidelines" section 11.
   ========================================================================== */

/* --- Brand webfonts ------------------------------------------------------ */
@font-face{
  font-family:"Space Grotesk";
  src:url("assets/fonts/space-grotesk-500_700.woff2") format("woff2");
  font-weight:300 700;font-style:normal;font-display:swap;
}
@font-face{
  font-family:"Plus Jakarta Sans";
  src:url("assets/fonts/plus-jakarta-sans-400_500_700_800.woff2") format("woff2");
  font-weight:200 800;font-style:normal;font-display:swap;
}

/* --- Tokens -------------------------------------------------------------- */
:root{
  /* brand */
  --gro-jade:#4ADE80;--gro-pine:#0B3D2E;--gro-night:#08110E;--gro-paper:#FAF9F6;
  /* jade ramp */
  --jade-50:#ECFDF3;--jade-100:#D3F9E0;--jade-200:#A7F0C2;--jade-300:#7BE7A3;
  --jade-400:#4ADE80;--jade-500:#2FC468;--jade-600:#1FA153;--jade-700:#17803F;
  /* pine ramp */
  --pine-400:#2E9C74;--pine-500:#248563;--pine-600:#1A6B51;
  --pine-700:#12523E;--pine-800:#0B3D2E;--pine-900:#061F17;
  /* night ramp - green-cast near-blacks */
  --night-950:#050B09;--night-900:#08110E;--night-850:#0B1712;
  --night-800:#101E18;--night-700:#16281F;--night-600:#1F3729;
  /* mist ramp - text on night */
  --mist-100:#EAF2ED;--mist-200:#C8D6CE;--mist-300:#9FB3A9;--mist-400:#748881;--mist-500:#4C5D57;
  /* counterpoints - guidelines s3. For contrast and data only, never as brand
     colour. --ember-400 is used by service.css; the bento swatch strip that
     first needed it is long gone, along with the bento itself. */
  --ember-400:#F5A742;--ion-400:#8098F7;

  /* type */
  --font-display:"Space Grotesk","Segoe UI",system-ui,sans-serif;
  --font-body:"Plus Jakarta Sans","Segoe UI",system-ui,sans-serif;
  --font-mono:ui-monospace,"SFMono-Regular",Menlo,monospace;
  --weight-body:400;--weight-medium:500;--weight-display:700;--weight-black:800;
  --size-display-1:clamp(3.25rem,7vw,5.75rem);
  --size-display-2:clamp(2.5rem,5vw,4rem);
  --size-h1:clamp(2.125rem,3.6vw,3rem);
  --size-h2:1.75rem;--size-h3:1.375rem;--size-h4:1.125rem;
  --size-lead:1.1875rem;--size-body:1rem;--size-sm:.875rem;--size-xs:.8125rem;--size-eyebrow:.75rem;
  --leading-display:.95;--leading-tight:1.1;--leading-snug:1.35;--leading-body:1.6;
  --tracking-display:-.035em;--tracking-tight:-.02em;--tracking-eyebrow:.26em;

  /* space */
  --space-1:4px;--space-2:8px;--space-3:12px;--space-4:16px;--space-5:20px;--space-6:24px;
  --space-8:32px;--space-10:40px;--space-12:48px;--space-16:64px;--space-20:80px;
  --space-24:96px;--space-32:128px;
  --gutter:24px;--section-y:clamp(64px,9vw,128px);--container:1200px;--container-narrow:760px;
  --page-x:clamp(20px,5vw,32px);

  /* radius + line */
  --radius-xs:6px;--radius-sm:10px;--radius-md:14px;--radius-lg:20px;
  --radius-xl:28px;--radius-2xl:36px;--radius-pill:999px;

  /* elevation - on night, depth is black shadow plus a light hairline */
  --shadow-xs:0 1px 2px rgba(0,0,0,.34);
  --shadow-sm:0 2px 8px rgba(0,0,0,.40);
  --shadow-md:0 12px 28px -10px rgba(0,0,0,.62);
  --shadow-lg:0 28px 68px -22px rgba(0,0,0,.78);
  --shadow-jade:0 12px 34px -10px rgba(74,222,128,.42);
  --ring-jade:0 0 0 3px rgba(74,222,128,.38);
  --blur-glass:saturate(150%) blur(14px);

  /* motion */
  --dur-instant:90ms;--dur-fast:160ms;--dur-base:240ms;--dur-slow:420ms;--dur-scene:800ms;
  --ease-out:cubic-bezier(.22,1,.36,1);
  --ease-in-out:cubic-bezier(.65,0,.35,1);
  --ease-spring:cubic-bezier(.34,1.56,.64,1);
  --lift-hover:-2px;--press-scale:.97;

  /* semantic */
  --surface-page:var(--night-900);--surface-card:var(--night-800);--surface-sunken:var(--night-850);
  --surface-raised:var(--night-700);
  --surface-inverse:var(--pine-800);--surface-inverse-deep:var(--pine-900);
  --surface-accent:var(--jade-400);--surface-accent-soft:rgba(74,222,128,.10);
  --text-heading:var(--mist-100);--text-body:var(--mist-300);--text-muted:var(--mist-400);
  --text-on-dark:var(--mist-100);--text-on-dark-muted:rgba(234,242,237,.68);
  --text-on-accent:var(--pine-900);--text-accent:var(--jade-400);
  --border-subtle:rgba(234,242,237,.09);--border-default:rgba(234,242,237,.18);
  --border-strongline:rgba(234,242,237,.32);--border-inverse:rgba(74,222,128,.24);
  --action-primary-bg:var(--jade-400);--action-primary-fg:var(--pine-900);
  --action-primary-hover:var(--jade-300);--action-primary-active:var(--jade-500);
  --focus-ring:var(--ring-jade);

  /* The floating header's whole footprint: its top inset plus the bar itself.
     The butterfly stage is pushed down by exactly this so the header never
     sits over the clip, and the hero clears it by the same amount. Change it
     here and the hero, the stage and the pinned zone all follow. */
  --nav-h:96px;

  /* z scale */
  --z-nav:40;--z-consent:80;--z-drawer:60;--z-skip:100;
}
@media (prefers-reduced-motion:reduce){
  :root{--dur-fast:0ms;--dur-base:0ms;--dur-slow:0ms;--dur-scene:0ms}
}

/* --- Base ---------------------------------------------------------------- */
*,*::before,*::after{box-sizing:border-box}
html{scroll-behavior:smooth;-webkit-text-size-adjust:100%}
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
body{
  margin:0;background:var(--surface-page);color:var(--text-body);
  font-family:var(--font-body);font-size:var(--size-body);line-height:var(--leading-body);
  -webkit-font-smoothing:antialiased;text-wrap:pretty;overflow-x:hidden;
}
h1,h2,h3,h4{
  font-family:var(--font-display);font-weight:var(--weight-display);color:var(--text-heading);
  letter-spacing:var(--tracking-display);line-height:var(--leading-tight);margin:0;
}
p{margin:0}
img,video{max-width:100%;display:block}
a{color:var(--text-accent);text-decoration:none;transition:color var(--dur-fast) var(--ease-out)}
a:hover{color:var(--jade-300)}
::selection{background:var(--jade-400);color:var(--pine-900)}
:focus-visible{outline:none;box-shadow:var(--focus-ring);border-radius:var(--radius-xs)}
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
ul{margin:0;padding:0;list-style:none}

.skip-link{
  position:absolute;left:var(--space-4);top:-100px;z-index:var(--z-skip);
  background:var(--jade-400);color:var(--pine-900);font-weight:700;font-size:var(--size-sm);
  padding:12px 20px;border-radius:var(--radius-pill);transition:top var(--dur-fast) var(--ease-out);
}
.skip-link:focus{top:var(--space-4);color:var(--pine-900)}

/* --- Shared primitives --------------------------------------------------- */
section[id]{scroll-margin-top:96px}
.container{max-width:var(--container);margin:0 auto;padding-inline:var(--page-x)}

.section{padding-block:var(--section-y)}
.section--tight{padding-block:clamp(48px,6vw,80px)}
.section--pine{background:var(--surface-inverse)}
.section--sunken{background:var(--surface-sunken)}
.section--jade{background:var(--surface-accent)}

.eyebrow{
  font-family:var(--font-body);font-weight:var(--weight-display);font-size:var(--size-eyebrow);
  letter-spacing:var(--tracking-eyebrow);text-transform:uppercase;color:var(--jade-400);
  margin:0 0 var(--space-5);display:block;
}
.eyebrow--on-jade{color:var(--pine-700)}

.display-1{font-size:var(--size-display-1);line-height:var(--leading-display)}
.display-2{font-size:var(--size-display-2);line-height:var(--leading-display)}
.h1{font-size:var(--size-h1)}
.h3{font-size:var(--size-h3)}
.h4{font-size:var(--size-h4)}
.jade{color:var(--jade-400)}

/* A link that carries the house arrow. Never underlined on hover, per s7 -
   the 3px kick is the affordance. */
.link-arrow{
  display:inline-flex;align-items:center;gap:var(--space-2);
  /* Padded to a 44px target rather than left at the 22px the type gives. */
  min-height:44px;
  font-weight:700;font-size:var(--size-sm);color:var(--jade-400);
}
.link-arrow:hover{color:var(--jade-300)}
.link-arrow .ic--arrow{transition:transform var(--dur-fast) var(--ease-out)}
.link-arrow:hover .ic--arrow{transform:translate(3px,-3px)}

.lead{font-size:var(--size-lead);line-height:var(--leading-body);color:var(--text-body);max-width:54ch}
.lead--on-jade{color:var(--pine-700)}
.muted{color:var(--text-muted);font-size:var(--size-sm)}

/* --- Buttons ------------------------------------------------------------- */
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:var(--space-2);
  font-family:var(--font-body);font-weight:700;font-size:var(--size-sm);letter-spacing:var(--tracking-tight);
  border-radius:var(--radius-pill);padding:12px 22px;min-height:44px;white-space:nowrap;
  transition:background-color var(--dur-fast) var(--ease-out),color var(--dur-fast) var(--ease-out),
             transform var(--dur-fast) var(--ease-out),box-shadow var(--dur-base) var(--ease-out),
             border-color var(--dur-fast) var(--ease-out);
}
.btn--lg{padding:16px 28px;font-size:var(--size-body);min-height:52px}
.btn .ic{transition:transform var(--dur-fast) var(--ease-out)}
.btn:hover .ic--arrow{transform:translate(3px,-3px)}

.btn--primary{background:var(--action-primary-bg);color:var(--action-primary-fg)}
.btn--primary:hover{background:var(--action-primary-hover);color:var(--action-primary-fg);transform:translateY(var(--lift-hover));box-shadow:var(--shadow-jade)}
.btn--primary:active{background:var(--action-primary-active);transform:scale(var(--press-scale));transition-timing-function:var(--ease-spring)}

.btn--ghost{color:var(--mist-100);border:1px solid var(--border-default);background:transparent}
.btn--ghost:hover{color:var(--jade-300);border-color:var(--border-inverse);background:var(--surface-accent-soft);transform:translateY(var(--lift-hover))}
.btn--ghost:active{transform:scale(var(--press-scale));transition-timing-function:var(--ease-spring)}

/* Quiet - the fourth button in the guidelines' component list. No fill and no
   border at rest; it is for controls that sit beside content rather than
   asking to be pressed, like the filmstrip's replay. */
.btn--quiet{color:var(--text-body);background:transparent;padding-inline:14px}
.btn--quiet:hover{color:var(--jade-300);background:var(--surface-accent-soft)}
.btn--quiet:active{transform:scale(var(--press-scale));transition-timing-function:var(--ease-spring)}

.btn--onjade{background:var(--pine-800);color:var(--mist-100)}
.btn--onjade:hover{background:var(--pine-900);color:var(--mist-100);transform:translateY(var(--lift-hover));box-shadow:var(--shadow-md)}
.btn--onjade:active{transform:scale(var(--press-scale));transition-timing-function:var(--ease-spring)}

.textlink{
  display:inline-flex;align-items:center;gap:6px;font-size:var(--size-xs);font-weight:700;
  color:var(--jade-400);letter-spacing:var(--tracking-tight);
}
.textlink .ic{transition:transform var(--dur-fast) var(--ease-out)}
.textlink:hover .ic{transform:translate(3px,-3px)}

/* --- Icons (Lucide path data, inlined as a sprite) ----------------------- */
.ic{width:1em;height:1em;flex:none;stroke:currentColor;fill:none;stroke-width:2;
    stroke-linecap:round;stroke-linejoin:round;font-size:18px}
.ic--14{font-size:14px}.ic--16{font-size:16px}.ic--20{font-size:20px}.ic--24{font-size:24px}
/* Brand marks that only exist as a filled shape. The X logo is the only one
   on the site: it cannot be drawn at a stroke weight the way the Lucide set
   can, so it opts out of the stroke and fills instead. */
.ic--solid{fill:currentColor;stroke:none}

/* --- Navigation ---------------------------------------------------------- */
/* A floating bar rather than a full-width strip pinned to the top edge: it
   sits inset from the top and sides, on the page rather than clamped to it,
   and the butterfly runs beneath it rather than behind it. The element itself
   still spans the viewport - it is .nav__inner that is the visible bar - so
   the fixed strip stays out of the way of pointer events either side of it. */
.nav{
  position:fixed;top:0;left:0;right:0;z-index:var(--z-nav);
  padding:clamp(12px,1.8vh,18px) var(--page-x);
  pointer-events:none;
  transition:transform var(--dur-base) var(--ease-out);
}
/* Out of the way on the way down, back on the way up. Moving .nav rather than
   .nav__inner takes the whole strip with it - the bar and the air around it -
   so nothing is left hanging at the top of the screen. -100% is its own
   height, which is --nav-h, so it clears completely.

   Under prefers-reduced-motion --dur-base is 0ms, so this becomes a cut
   rather than a slide without needing a rule of its own. */
.nav.is-hidden{transform:translateY(-100%)}
.nav__inner{
  pointer-events:auto;
  max-width:var(--container);margin:0 auto;
  /* Tighter on the right than the left: the CTA is a filled pill and needs to
     sit in from the edge by its own height's worth of air, not the bar's. The
     left is the lockup's clear space, so it is set from the mark rather than
     from the bar - see .nav__logo. */
  padding:9px 10px 9px var(--space-8);
  display:flex;align-items:center;gap:var(--space-6);
  background:rgba(11,61,46,.58);
  border:1px solid var(--border-inverse);
  border-radius:var(--radius-lg);
  backdrop-filter:var(--blur-glass);-webkit-backdrop-filter:var(--blur-glass);
  box-shadow:var(--shadow-md);
  transition:background-color var(--dur-base) var(--ease-out),
             border-color var(--dur-base) var(--ease-out),
             box-shadow var(--dur-base) var(--ease-out);
}
/* Once the page has moved, the bar has content running under it rather than
   the top of the hero, so it firms up. */
.nav.is-stuck .nav__inner{
  background:rgba(11,61,46,.88);
  border-color:var(--border-inverse);
  box-shadow:var(--shadow-lg);
}
/* The full lockup, not the wordmark: GRO over GET RANKED ONLINE. The strapline
   only earns its place if it can actually be read, which sets the floor at
   36px; the bar's own height sets the ceiling. The brand guide asks for the
   cap-height of the G as clear space on all four sides, which no header bar
   can afford literally - 14px of air above and below and 32px to the left is
   the closest honest reading of it at this size, and the mark still sits
   inside the 46px the CTA already claims, so the bar does not grow. */
.nav__logo{display:flex;align-items:center;flex:none;padding:2px 0}
.nav__logo img{display:block;height:36px;width:auto}
/* Centred in the bar rather than pushed right: auto on both sides puts the
   links between the logo and the CTA instead of crowding the button. */
.nav__list{display:flex;align-items:center;gap:var(--space-1);margin-inline:auto}
.nav__link{
  display:inline-flex;align-items:center;gap:6px;padding:10px 14px;min-height:44px;border-radius:var(--radius-pill);
  font-size:var(--size-sm);font-weight:500;color:var(--text-on-dark-muted);letter-spacing:var(--tracking-tight);
  transition:color var(--dur-fast) var(--ease-out),background-color var(--dur-fast) var(--ease-out);
}
.nav__link:hover,.nav__item:focus-within>.nav__link{color:var(--mist-100);background:rgba(234,242,237,.06)}
/* The grant link. It is an offer with a closing date rather than a section of
   the site, so it sits outside .nav__list - out of the centred run of page
   links and paired with the CTA at the end of the bar, where the two asks
   read as a pair: take the offer, or book the audit. Outlined rather than
   filled so the filled button stays the one obvious action of the two.

   It is a direct child of .nav__inner here, not a list item, so it needs its
   own flex:none. The negative right margin pulls it against the button -
   .nav__inner's 24px gap is the spacing between the bar's three parts and is
   too much air between two controls meant to be read together. */
.nav__link--offer{
  flex:none;margin-right:calc(10px - var(--space-6));
  color:var(--jade-400);border:1px solid rgba(61,220,132,.34);
  background:rgba(61,220,132,.08);padding-inline:13px;
  transition:color var(--dur-fast) var(--ease-out),background-color var(--dur-fast) var(--ease-out),
             border-color var(--dur-fast) var(--ease-out);
}
.nav__link--offer:hover{color:var(--jade-300);background:rgba(61,220,132,.16);border-color:rgba(61,220,132,.55)}
.nav__cta{flex:none}
.nav__item{position:relative}
.nav__item>.nav__link .ic{transition:transform var(--dur-fast) var(--ease-out)}
.nav__item:hover>.nav__link .ic,.nav__item:focus-within>.nav__link .ic{transform:rotate(180deg)}

.nav__menu{
  position:absolute;top:calc(100% + 10px);left:0;min-width:300px;
  background:var(--surface-card);border:1px solid var(--border-subtle);border-radius:var(--radius-lg);
  box-shadow:var(--shadow-lg);padding:var(--space-3);
  opacity:0;visibility:hidden;transform:translateY(-6px);
  transition:opacity var(--dur-fast) var(--ease-out),transform var(--dur-fast) var(--ease-out),visibility var(--dur-fast);
}
.nav__item:hover .nav__menu,.nav__item:focus-within .nav__menu{opacity:1;visibility:visible;transform:none}
.nav__menu a{
  display:block;padding:10px 12px;border-radius:var(--radius-md);
  font-size:var(--size-sm);font-weight:600;color:var(--mist-200);
  transition:background-color var(--dur-fast) var(--ease-out),color var(--dur-fast) var(--ease-out);
}
.nav__menu a:hover{background:var(--surface-accent-soft);color:var(--jade-300)}
.nav__menu span{display:block;font-size:var(--size-xs);color:var(--text-muted);margin-top:2px;font-weight:400}

.nav__toggle{
  display:none;margin-left:auto;flex:none;width:44px;height:44px;border-radius:var(--radius-pill);
  align-items:center;justify-content:center;color:var(--mist-100);border:1px solid var(--border-default);
}
.nav__toggle:hover{background:rgba(234,242,237,.06)}

.drawer{
  position:fixed;inset:0;z-index:var(--z-drawer);background:var(--night-950);
  padding:var(--space-6) var(--page-x) var(--space-16);overflow-y:auto;
  display:flex;flex-direction:column;gap:var(--space-8);
  opacity:0;visibility:hidden;transform:translateY(-8px);
  transition:opacity var(--dur-base) var(--ease-out),transform var(--dur-base) var(--ease-out),visibility var(--dur-base);
}
.drawer.is-open{opacity:1;visibility:visible;transform:none}
.drawer__top{display:flex;align-items:center;justify-content:space-between}
.drawer__top img{height:26px;width:auto}
.drawer__close{width:44px;height:44px;border-radius:var(--radius-pill);border:1px solid var(--border-default);
  display:flex;align-items:center;justify-content:center;color:var(--mist-100)}
.drawer nav{display:flex;flex-direction:column}
.drawer nav a{
  padding:16px 0;border-bottom:1px solid var(--border-subtle);
  font-family:var(--font-display);font-weight:700;font-size:1.5rem;letter-spacing:var(--tracking-display);
  color:var(--mist-100);
}
.drawer nav a:hover{color:var(--jade-400)}
.drawer nav a small{display:block;font-family:var(--font-body);font-size:var(--size-xs);font-weight:500;
  letter-spacing:0;color:var(--text-muted);margin-top:4px}
/* Same distinction the bar makes, at drawer scale. The drawer entries are set
   in the display face at 1.5rem with no pill around them, so this one carries
   the colour only - a bordered pill would break the flush stack of rules. */
/* Qualified with .drawer nav so it outweighs the base rule above, which is
   .drawer nav a and would otherwise win on specificity. */
.drawer nav a.drawer__offer{color:var(--jade-400)}
.drawer nav a.drawer__offer:hover{color:var(--jade-300)}

/* --- Butterfly zone ------------------------------------------------------
   One sticky backdrop shared by the hero and the partnership section. The
   stage pins to the viewport for the whole zone; the copy scrolls over it.
   -------------------------------------------------------------------------- */
.bfly{position:relative}
/* Pinned below the floating header, not behind it. top:--nav-h means sticky
   pushes the stage down to clear the bar even at scroll 0, so the clip is
   never cropped by it; the height comes off by the same amount so the stage
   still ends exactly at the fold. .bfly__flow's negative margin has to match
   this height - they are the same number in opposite directions.

   THE MASK GOES HERE, NOT ON THE CANVAS. Two different things meet the page
   ground on this boundary and both of them show it: the clip's own ground is a
   neutral near-black (measured at about rgb(5,5,5)) against a green-cast
   #08110E page, and the scrim over it is rgba(5,11,9,.97) - darker again.
   Masking only the canvas fades the butterfly in but leaves the scrim hitting
   full strength on the line, which is the band you could still see under the
   header. Masking the stage fades both together, so the whole backdrop
   dissolves up into the page.

   The stops ease in rather than ramping straight: the eye catches the start of
   a linear fade on a large flat dark area, so the first third of the distance
   does very little and the change happens lower down where there is already
   picture to hide it. */
.bfly__stage{
  position:sticky;top:var(--nav-h);height:calc(100vh - var(--nav-h));overflow:hidden;z-index:0;
  -webkit-mask-image:linear-gradient(180deg,transparent 0,rgba(0,0,0,.22) 46px,rgba(0,0,0,.72) 100px,#000 148px);
          mask-image:linear-gradient(180deg,transparent 0,rgba(0,0,0,.22) 46px,rgba(0,0,0,.72) 100px,#000 148px);
}
.bfly__canvas{
  position:absolute;inset:0;width:100%;height:100%;display:block;
  opacity:0;transition:opacity var(--dur-scene) var(--ease-out);
}
.bfly__canvas.is-ready{opacity:1}

/* Legibility gradient for the copy column only. Measured, not eyeballed:
   keeps the worst frame above WCAG AA behind the text, and is fully clear
   of the right of the frame so the butterfly itself is never dimmed. */
.bfly__scrim{
  position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(96deg,rgba(5,11,9,.97) 0%,rgba(5,11,9,.95) 34%,rgba(5,11,9,.72) 60%,rgba(5,11,9,0) 84%);
}

/* Pulled back over the pinned stage. */
.bfly__flow{position:relative;z-index:1;margin-top:calc(var(--nav-h) - 100vh)}

/* Dissolve into the page instead of cutting the butterfly off. */
.bfly__fade{height:34vh;background:linear-gradient(180deg,rgba(8,17,14,0) 0%,var(--night-900) 78%)}

/* Wingbeat speed is NOT set by the height of this zone, whatever an older
   version of this note said. The butterfly runs on a time clock - FLAP_FPS at
   the top of main.js is the only speed control, and the sequence was scrubbed
   by scroll position once but is not any more. Zone height decides how long
   the butterfly stays pinned, nothing else, which is why the process section's
   pin runway could be made as long as it needed to be without touching it. */

/* ==========================================================================
   Process steps
   Ported from the Claude Design handoff in Animations/Process Animation/,
   which ships as React (GroProcessSection.tsx + .css). The behaviour is the
   component's, unchanged: the spine draws left to right to each icon, the icon
   pops in, then the number, headline and body fade up behind it, about 1.6s
   end to end.

   Three things differ from the bundle, all because of where it sits here:

     1. No background. The component paints --paper-50 or --pine-900; this one
        sits on the butterfly and paints nothing, taking the dark theme's copy
        colours. That is also why the .gro-dark variants are not carried over -
        there is only one theme on this page.
     2. Width and vertical padding come from .section and .container rather
        than the component's own max-width:1200px and 32px gutter, so the
        columns line up with every other section on the page.
     3. The per-element transition delays were inline styles in the TSX. They
        are the nth-child block below instead, so the markup stays static. The
        numbers are the component's: 400ms between steps, the last spine
        segment 320ms rather than 400 because it only runs half a column.

   The tokens it needs were already here - --jade-400/600, --pine-800,
   --mist-100, --text-on-dark-muted, --border-inverse, --ease-out,
   --ease-spring. Only --paper-50 was missing, and that was the light
   background this port does not use.
   ========================================================================== */
/* --- heading -------------------------------------------------------------
   Centred over the four columns rather than ranged left like the rest of the
   page's section heads, because what sits under it is symmetrical and a
   left-ranged title over a centred row reads as a mistake.

   The cue is deliberately quiet - small, muted, in the body face rather than
   the display one. It is an instruction, not a subheading, and sized like one
   it would compete with the four step numbers underneath. */
.gro-head{text-align:center;max-width:1180px;margin:0 auto clamp(40px,5vw,72px)}
/* Bigger than .display-2 carries on its own, because this section holds the
   page still while it plays and a section-sized head under a pinned run reads
   as undersized. Stops short of --size-display-1 all the same: the hero claim
   should still be the largest thing on the page. */
.gro-title{font-size:clamp(2.5rem,6.2vw,5.25rem)}
@media (max-width:640px){
  /* Natural wrapping again - the forced break would make four lines of it. */
  .gro-title br{display:none}
}

.gro-cue{
  margin-top:var(--space-4);color:var(--text-on-dark-muted);
  font-size:var(--size-sm);letter-spacing:.01em;
}

.gro-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:0}
.gro-col{
  position:relative;display:flex;flex-direction:column;align-items:center;
  text-align:center;padding:0 24px;
}

/* --- the connecting spine ------------------------------------------------
   Two layers per column: .gro-track is the unfilled rail, .gro-line is the
   jade that draws along it. 47px is the badge's vertical centre - half of the
   96px row minus the 2px rule - so the line meets the icon rather than the
   top of it. Move the badge and this number moves with it. */
.gro-seg{position:absolute;top:47px;height:2px;transform-origin:left center}
.gro-col:first-child .gro-seg{left:50%;right:0}
.gro-col:nth-child(2) .gro-seg,
.gro-col:nth-child(3) .gro-seg{left:0;right:0}
.gro-col:last-child .gro-seg{left:0;right:50%}
.gro-track{background:rgba(74,222,128,.16)}
.gro-line{background:var(--jade-400)}

/* --- icon badge --------------------------------------------------------- */
.gro-badgewrap{
  position:relative;display:flex;justify-content:center;align-items:center;
  width:100%;height:96px;
}
.gro-badge{
  position:relative;width:94px;height:94px;border-radius:50%;
  display:grid;place-items:center;
  background:rgba(74,222,128,.10);
  box-shadow:inset 0 0 0 1px var(--border-inverse);
  color:var(--jade-400);
  /* The badge is the one opaque-ish thing in this section, and it sits over
     the clip. Kept translucent with a blur rather than filled, the same way
     .card--onbfly is, so the wing still reads through it. */
  backdrop-filter:var(--blur-glass);-webkit-backdrop-filter:var(--blur-glass);
}
.gro-badge .ic{font-size:36px}

/* --- copy --------------------------------------------------------------- */
.gro-num{
  margin:28px 0 0;color:var(--jade-400);
  font-family:var(--font-display);font-weight:700;font-size:13px;
  letter-spacing:.22em;text-transform:uppercase;
}
.gro-h{
  margin:14px 0 0;color:var(--mist-100);
  font-family:var(--font-display);font-weight:700;font-size:24px;
  line-height:1.12;letter-spacing:-.035em;text-wrap:pretty;
}
.gro-b{
  margin:14px 0 0;color:var(--text-on-dark-muted);
  font-size:15.5px;line-height:1.6;text-wrap:pretty;
}

/* --- reveal ------------------------------------------------------------
   Each column carries .is-on when its step is up, and initProcess() decides
   that from scroll position through .gro-pin. That is the one real departure
   from the handoff component, which ran all four off a single class on a
   timer: here step one is up on arrival and the other three are scrubbed in,
   so the delays below are per column and relative to its own activation
   rather than a growing offset from the start of the section.

   The intervals inside a step are the component's: the icon and its length of
   spine together, then the number at +120ms, the headline at +180ms and the
   body at +240ms. What is gone is the 400ms between steps - the visitor's
   scroll sets that now, and how much scroll a step costs is the thresholds in
   initProcess() against the runway height on .gro-pin. */
.gro-fade{opacity:0;transform:translateY(14px)}
.gro-pop{opacity:0;transform:scale(.8)}
.gro-draw{transform:scaleX(0)}
.is-on .gro-fade{opacity:1;transform:none}
.is-on .gro-pop{opacity:1;transform:scale(1)}
.is-on .gro-draw{transform:scaleX(1)}

.gro-line{transition:transform 400ms linear}
/* Half a column wide rather than a full one, so it draws in proportion. */
.gro-col:last-child .gro-line{transition-duration:320ms}
.gro-badge{transition:opacity 300ms var(--ease-out),transform 460ms var(--ease-spring)}
.gro-num{transition:opacity 420ms var(--ease-out) 120ms,transform 420ms var(--ease-out) 120ms}
.gro-h{transition:opacity 460ms var(--ease-out) 180ms,transform 460ms var(--ease-out) 180ms}
.gro-b{transition:opacity 460ms var(--ease-out) 240ms,transform 460ms var(--ease-out) 240ms}

/* Going back up, the delays would run in reverse and a step would hang around
   for a quarter second after it should have gone. Stepping back is a
   correction, not a beat in the sequence, so it happens at once. */
.gro-col:not(.is-on) .gro-num,
.gro-col:not(.is-on) .gro-h,
.gro-col:not(.is-on) .gro-b{transition-delay:0ms}

/* --- the pin ------------------------------------------------------------
   .gro-pin is scroll runway and nothing else: the section sticks to the top
   of it and the page keeps scrolling underneath until the runway is spent.
   Its height minus the sticky height is the travel the steps are scrubbed
   against, so the two numbers below are the ones to change to make a step
   cost more or less scroll - 250vh against 100vh leaves 150vh of travel, or
   50vh per step for the three that are scrubbed in.

   The section sticks below the nav rather than at 0, the same as
   .bfly__stage, so the bar never covers the first row of icons. It used to
   sit inside the butterfly zone and be nested inside that pin; it runs below
   the zone now, on the page's own ground, so this is the only sticky element
   in play here. */
.gro-pin{height:250vh}
.gro-process{
  position:sticky;top:var(--nav-h);
  min-height:calc(100vh - var(--nav-h));
  display:flex;flex-direction:column;justify-content:center;
  /* Not the section padding the rest of the page uses. A pinned section has to
     fit between the nav and the bottom of the screen or its last line is
     simply never reachable - there is no scrolling within it to go and find
     the rest. Adding the heading was what first pushed it over: two 128px
     paddings plus the head plus the four columns came to 903px in a 900px
     window, and the bottom of it was cut off for the whole pin. The min-height
     above is the real height at any normal viewport; this only has to stay
     small enough not to beat it. */
  padding-block:clamp(24px,4vh,56px);
}

/* --- responsive ---------------------------------------------------------
   Under 768px the row becomes a stack and the spine turns vertical, running
   down the left edge through the badge centres. Same 47px, now horizontal. */
/* 1023 rather than the handoff's 767. Four columns technically fit at 800px,
   at 180px each, but the pinned section then measures 895px against a 900px
   window and loses its bottom edge behind the fold - and 180px is a cramped
   measure for a 24px headline whether it is pinned or not. Moving the stack up
   to 1024 fixes both, and keeps one number in charge: the pin is dropped in the
   same block, so initProcess()'s pinned() check follows it automatically. */
@media (max-width:1023px){
  /* No pin below the breakpoint. Stacked, the four steps run well past a
     screen, so there is nothing to hold still - pinning a section taller than
     the viewport just means the bottom of it can never be read. The steps
     reveal one at a time as they scroll into view instead, which is the same
     promise made a different way. */
  .gro-pin{height:auto}
  .gro-process{position:static;min-height:0;display:block}

  .gro-grid{grid-template-columns:1fr}
  .gro-col{padding:0 0 44px;text-align:left;align-items:flex-start}
  .gro-col:last-child{padding-bottom:0}

  .gro-seg{top:auto;left:47px;right:auto;width:2px;height:auto;transform-origin:center top}
  .gro-draw{transform:scaleY(0)}
  .is-on .gro-draw{transform:scaleY(1)}
  /* left/right have to be restated on every one of these, not just the first.
     The horizontal rules above are `.gro-col:first-child .gro-seg` and friends
     - three classes deep - so they outrank the bare `.gro-seg` that turns the
     spine vertical, and columns two, three and four kept their desktop
     `left:0`. That put their rail hard against the container edge while column
     one's sat correctly at the badge's centre, so the timeline stepped sideways
     under the first icon. `right:auto` goes with it: without it these stay
     over-constrained and only work because width wins the tie-break. */
  .gro-col:first-child .gro-seg{top:47px;bottom:0;left:47px;right:auto}
  .gro-col:nth-child(2) .gro-seg,
  .gro-col:nth-child(3) .gro-seg{top:0;bottom:0;left:47px;right:auto}
  .gro-col:last-child .gro-seg{top:0;height:47px;bottom:auto;left:47px;right:auto}

  .gro-badgewrap{justify-content:flex-start}

  /* Not in the handoff CSS, and it has to be here. Stacked, the spine runs the
     full height of each step at left:47px - the badge's centre - while the copy
     below the badge starts at 0 and spans the column. The component ships that
     way, so the jade rule is drawn straight through the body text. Indenting
     the copy clear of the badge turns it into the vertical timeline the layout
     is obviously reaching for, and costs nothing at this width: 390px still
     leaves 276px of measure. Worth raising against the design rather than
     leaving as a local patch. */
  .gro-num,.gro-h,.gro-b{padding-left:114px}
  .gro-num{margin-top:18px}
}

/* These two are down here, not up with .gro-title where they were written,
   because they override .gro-process - and that is defined with the pin below
   the heading rules. Above it they set font-size and margin fine and their
   padding-block did nothing at all, which is the sort of half-working the
   cascade does quietly. */
/* A pinned section is the one place a short window actually breaks something.
   Everywhere else the page just gets longer; here the section has to fit
   between the nav and the fold or its bottom row of copy is unreachable, and
   the four columns are about 400px of that whatever the height. The crossover
   is around 820px of viewport: above it the min-height governs and there is
   slack, below it the content wins and starts hanging off the bottom.

   So below 880 - early enough to have cleared it before it bites - the head
   takes its size from the height as well as the width. min() rather than a new
   clamp so the tall-window figures above stay exactly as they are; this can
   only ever make them smaller. */
@media (min-width:1024px) and (max-height:880px){
  .gro-title{font-size:min(clamp(2.5rem,6.2vw,5.25rem),8.2vh)}
  .gro-head{margin-bottom:min(clamp(40px,5vw,72px),4vh)}
  .gro-process{padding-block:min(clamp(24px,4vh,56px),2.6vh)}
}

/* Below this there is nothing left to shave. The four columns are about 400px
   of copy on their own and the cue and heading cannot go under a readable
   size, so a window this short cannot hold the section and the nav at once.
   The pin is dropped rather than the section being cut off - same call as the
   width rule above, for the same reason, and the section goes back to the
   page's ordinary padding since it is no longer fitting itself to a screen. */
@media (max-height:759px){
  .gro-pin{height:auto}
  .gro-process{position:static;min-height:0;display:block;padding-block:var(--section-y)}
}

@media (prefers-reduced-motion:reduce){
  /* All four steps are up from the start here, so the cue would be telling
     the visitor to do something that has already happened. */
  .gro-cue{display:none}

  /* The pin goes too, not just the transitions. Holding the page still for
     150vh of scroll is a motion effect in its own right, and a heavier one
     than the fades - someone who has asked for less of it should not have to
     scroll through a section that refuses to move. All four steps are simply
     there. */
  .gro-pin{height:auto}
  .gro-process{position:static;min-height:0;display:block}
  .gro-fade,.gro-pop,.gro-draw{opacity:1!important;transform:none!important;transition:none!important}
}

/* --- Hero ---------------------------------------------------------------- */
/* The top padding is the floating bar plus air, so the copy centres in the
   space under it rather than in the whole viewport - otherwise the header
   crowds the eyebrow. */
.hero{position:relative;min-height:100vh;display:grid;align-items:center;
  padding-block:calc(var(--nav-h) + clamp(8px,3vh,32px)) clamp(64px,12vh,132px)}
/* .container uses margin:0 auto, which makes a grid item shrink-to-fit
   instead of stretching - so the width has to be stated explicitly here. */
.hero>.container{width:100%}
.hero__copy{max-width:640px}
.hero h1{margin-bottom:var(--space-6)}
.hero .lead{max-width:44ch}
.hero__actions{display:flex;gap:var(--space-3);margin-top:var(--space-8);flex-wrap:wrap}
.hero__note{margin-top:var(--space-6);font-size:var(--size-xs);color:var(--text-on-dark-muted)}

/* The "Scroll - watch it GRO" cue and its progress rail used to sit here
   (.hero__cue, .hero__cue-rail, .hero__cue-fill, and #cueFill driven from
   initHeroSequence). Removed. */

/* --- Thesis band --------------------------------------------------------- */

/* --- Stat strip ---------------------------------------------------------- */
.stats{display:grid;grid-template-columns:repeat(3,1fr);border:1px solid var(--border-subtle);
  border-radius:var(--radius-2xl);overflow:hidden;background:var(--surface-card)}
.stat{padding:var(--space-10) var(--space-8);border-right:1px solid var(--border-subtle)}
.stat:last-child{border-right:0}
.stat__num{
  font-family:var(--font-display);font-weight:700;font-size:clamp(2.5rem,4.4vw,3.5rem);
  letter-spacing:var(--tracking-display);line-height:1;color:var(--jade-400);
  font-variant-numeric:tabular-nums;
}
.stat__label{margin-top:var(--space-3);font-size:var(--size-sm);color:var(--text-body);max-width:26ch}

/* The attribution under the strip. These are one client's figures rather than
   a house average, so the source and the link to the full report sit with the
   numbers instead of being left to the work page to explain. Same size and
   colour as .gro-fineprint, which does this job on that report. */
.stats__source{
  margin:var(--space-5) 0 0;font-size:var(--size-xs);line-height:var(--leading-body);
  color:var(--text-muted);max-width:80ch;
}
.stats__source a{color:inherit;text-decoration:underline;text-underline-offset:3px}
.stats__source a:hover{color:var(--jade-400)}

/* --- Two-column block ---------------------------------------------------- */
.split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:clamp(40px,6vw,88px);align-items:center}

/* --- Partnership --------------------------------------------------------- */
/* The headline is pulled out of the left column and run across the whole
   container, so it reads as a banner over the section rather than as the top
   of one of two columns.

   It is sized to fill that width rather than to a step on the type scale:
   the line breaks after "agency." and both halves are close to the same
   length, so the cap is set where the longer line lands just inside the
   container. Changing the wording will change how well it fills - re-measure
   rather than assuming the number still holds.

   No text-wrap:balance here, deliberately. Balance evens the lines out by
   making them shorter, which is the opposite of filling the width. */
/* The partnership section sits on the butterfly and needs a stacking context
   of its own, so the local scrim below lands under its copy rather than over
   it. It is the only section that does now - the process steps carried the
   same pair of rules until they moved off the clip.

   Careful if the process steps ever come back onto it: .gro-process must not
   be named in the position half of a rule like this, because it is sticky and
   position is not additive - the later rule would quietly unstick the pin. */
.partner{position:relative;isolation:isolate}
.partner>.container{position:relative;z-index:1}

/* Local scrim for the partnership section, the one section that sits on the
   butterfly. The reason it exists rather than just widening .bfly__scrim:
   that one is shared with the hero, whose copy is capped at 640px, and pushing
   its fade further right would dim the butterfly across the whole zone for the
   sake of one section.

   It stays at zero until 60% across, because up to there the shared scrim is
   still at .72 or heavier and already carries the text on its own. That matters:
   the butterfly's body and the bright inner wing sit between roughly 30% and
   60%, so starting any earlier would dim the part of the clip most worth
   seeing for no contrast gain. From 60% it ramps to .70, covering exactly the
   strip where the shared scrim has faded out.

   Measured against the brightest pixel the clip reaches, about rgb(227,227,221)
   on a wing highlight: without this layer the headline fell to 1.2:1 and the
   checks to 1.65:1 on that frame, both far under AA. With it the worst case
   across the full width is about 5:1 for body text and better for the display
   sizes. Any change to the stops below wants re-measuring against that frame
   rather than judging by eye - the wing is only that bright for a few frames
   of the 143 and it is easy to scroll straight past them. */
.bfly-veil{
  /* Hung outside the section's own box and faded at both ends, because a layer
     this dark cannot start and stop on a line: at inset:0 its top and bottom
     edges read as two seams straight across the butterfly. The overhang puts
     the whole ramp in the section's padding and the empty space either side of
     it, so the veil is already at full strength by the time the first line of
     type reaches it and has not started to lift by the last.

     --veil-fade is 2.5x the section padding and the overhang is 1.5x, which is
     what makes those two line up: full strength begins exactly 1x --section-y
     into the section, and that is where the padding ends. Change one of the
     three multipliers and the other two need changing with it. */
  --veil-fade:calc(var(--section-y) * 2.5);
  position:absolute;left:0;right:0;z-index:0;pointer-events:none;
  top:calc(var(--section-y) * -1.5);bottom:calc(var(--section-y) * -1.5);
  background:linear-gradient(96deg,
    rgba(5,11,9,0) 0%,rgba(5,11,9,0) 60%,
    rgba(5,11,9,.50) 72%,rgba(5,11,9,.68) 84%,rgba(5,11,9,.70) 100%);
  /* Eased rather than linear, for the reason given on .bfly__stage: the eye
     catches the start of a straight fade on a large flat dark area. */
  -webkit-mask-image:linear-gradient(180deg,
    transparent 0,
    rgba(0,0,0,.25) calc(var(--veil-fade) * .38),
    rgba(0,0,0,.75) calc(var(--veil-fade) * .72),
    #000 var(--veil-fade),
    #000 calc(100% - var(--veil-fade)),
    rgba(0,0,0,.75) calc(100% - var(--veil-fade) * .72),
    rgba(0,0,0,.25) calc(100% - var(--veil-fade) * .38),
    transparent 100%);
          mask-image:linear-gradient(180deg,
    transparent 0,
    rgba(0,0,0,.25) calc(var(--veil-fade) * .38),
    rgba(0,0,0,.75) calc(var(--veil-fade) * .72),
    #000 var(--veil-fade),
    #000 calc(100% - var(--veil-fade)),
    rgba(0,0,0,.75) calc(100% - var(--veil-fade) * .72),
    rgba(0,0,0,.25) calc(100% - var(--veil-fade) * .38),
    transparent 100%);
}

.partner__title{
  margin-top:var(--space-5);
  font-family:var(--font-display);font-weight:var(--weight-display);
  font-size:clamp(2.25rem,8.4vw,6.7rem);
  line-height:var(--leading-display);
  letter-spacing:var(--tracking-display);
  color:var(--text-heading);
}
/* Columns start together under the banner. .split centres by default, which
   was right when the left column carried the headline and was the taller of
   the two - now the right column is taller and centring would leave the body
   copy floating against nothing. */
.partner__body{margin-top:clamp(40px,5vw,72px);align-items:start}
/* Two routes out of the section: the promise in full, and the evidence for it.
   Wraps rather than shrinking, so the two labels never get squeezed onto each
   other on a narrow screen.

   These used to sit below the whole two-column block, which put them nearly
   200px under the left column: the right column is the taller of the two, and
   a sibling after the grid has to clear all of it. That left a hole under the
   body copy and pushed the buttons well past the card they follow.

   They are a grid item now, in column one under the copy, with the right
   column spanning both rows so its height no longer decides where they land.
   Below 900 .split is one column and all three stack in source order - copy,
   proof, then the buttons last, which is the order they should be read in
   anyway. */
.partner__body{grid-template-rows:auto auto}
.partner__body>:nth-child(2){grid-column:2;grid-row:1 / span 2}
/* .split's own gap is 86px at this width, which was set for two columns of
   prose. It is too much in both directions here: across, it left the column
   10px narrower than the two buttons need and they wrapped to two lines;
   down, it is now a gap between the copy and its own buttons rather than
   between columns. Both are tightened, and only for this grid. */
.partner__body{column-gap:clamp(32px,4vw,56px);row-gap:clamp(24px,2.6vw,40px)}
.partner__actions{
  display:flex;flex-wrap:wrap;gap:var(--space-3);
  grid-column:1;grid-row:2;align-self:start;
  /* No margin - the row gap above is the spacing now. */
}
@media (max-width:900px){
  /* Back to ordinary flow - one column means grid placement would only fight
     the source order it already has right, and .split's own gap is the right
     one again once the columns are gone. */
  .partner__body{column-gap:normal;row-gap:normal}
  .partner__body>:nth-child(2),
  .partner__actions{grid-column:auto;grid-row:auto}
}
.partner__body .lead + .lead{margin-top:var(--space-5)}
/* No stacking rule here on purpose - .split already collapses to one column
   at 900px further down this file, and restating it would just be a second
   number to keep in sync. */

/* A seventh stray body stood here, for a translucent glass panel - the
   --border-inverse ring, the 2xl radius and the glass blur. Same story as the
   six below: no selector, no markup anywhere that wants it, nothing to
   recover it from, and removing it changes no rendering because invalid CSS
   was never applying in the first place. */
.badge{
  display:inline-flex;align-items:center;gap:8px;padding:6px 12px;border-radius:var(--radius-pill);
  background:var(--surface-accent-soft);border:1px solid var(--border-inverse);
  font-size:var(--size-xs);font-weight:700;color:var(--jade-300);letter-spacing:var(--tracking-tight);
}
.badge__dot{width:7px;height:7px;border-radius:50%;background:var(--jade-400);animation:pulse 1.8s var(--ease-in-out) infinite}
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.45;transform:scale(.8)}}
@media (prefers-reduced-motion:reduce){.badge__dot{animation:none}}

/* Six stray declaration blocks sat here - bodies with no selectors in front of
   them, for what reads as a list row: a flex row with a hover background, a
   truncating label, tabular figures, a jade number with a spring transform and
   a muted meta line. Whatever they styled, its markup went before they did:
   no class in any page of this site is missing a rule that they would fill,
   and there is no intact copy of them anywhere in the project to recover the
   selectors from.

   They were not harmless. Invalid CSS applies to nothing, so they styled
   nothing - but they put the parser into error recovery, and it stayed there
   long enough to swallow the two rules that followed them. That is how the
   work CTA below arrived with no centring: .cta-work__inner and
   .cta-work__title were dropped on the floor while .cta-work__inner .lead,
   one rule later, survived. Deleting the bodies changes no rendering and
   stops the next rule written here from vanishing the same way. */

/* ==========================================================================
   Work CTA
   A single centred column on the page's own ground - no panel, no border. The
   sections either side of it are grids of cards, and a third bordered thing
   between them would read as more of the same rather than as the break in
   rhythm it is meant to be. The white space is the device.
   ========================================================================== */
.cta-work__inner{max-width:900px;margin:0 auto;text-align:center}
/* Larger than .display-2, smaller than the pinned process head. Both are
   asking for attention on the same scroll and they should not be the same
   size - that one holds the page still, this one just wants a click. */
.cta-work__title{
  /* No eyebrow above it, so nothing to clear. */
  margin-top:0;
  font-family:var(--font-display);font-weight:var(--weight-display);
  font-size:clamp(2.25rem,5.4vw,4.5rem);
  line-height:var(--leading-display);letter-spacing:var(--tracking-display);
  color:var(--text-heading);text-wrap:pretty;
}
.cta-work__inner .lead{margin:var(--space-6) auto 0;max-width:56ch}
.cta-work__actions{display:flex;justify-content:center;margin-top:var(--space-10)}

/* --- Cards --------------------------------------------------------------- */
.grid-4{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--gutter)}
.grid-3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--gutter)}
.grid-2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--gutter)}

.card{
  display:flex;flex-direction:column;gap:var(--space-3);height:100%;
  background:var(--surface-card);border:1px solid var(--border-subtle);
  border-radius:var(--radius-lg);padding:var(--space-8);box-shadow:var(--shadow-xs);
  transition:transform var(--dur-base) var(--ease-out),box-shadow var(--dur-base) var(--ease-out),
             border-color var(--dur-base) var(--ease-out);
}
/* transform for cards with no .reveal on them, --lift for the ones that have
   it. Both are needed; neither covers the other. */
a.card:hover{transform:translateY(-4px);--lift:translateY(-4px);box-shadow:var(--shadow-md);border-color:var(--jade-300)}
a.card:active{transform:translateY(-1px) scale(.995);--lift:translateY(-1px) scale(.995);transition-timing-function:var(--ease-spring)}
.card__icon{
  width:46px;height:46px;border-radius:var(--radius-md);background:var(--surface-accent-soft);
  color:var(--jade-300);display:grid;place-items:center;margin-bottom:var(--space-2);
}
.card h3{color:var(--text-heading)}
.card p{font-size:var(--size-sm);color:var(--text-body)}
.card .textlink{margin-top:auto;padding-top:var(--space-4)}
.card--onpine{background:rgba(250,249,246,.05);border-color:var(--border-inverse)}
/* The one panel in the partnership section, which sits over the butterfly.
   --surface-card is opaque and would punch a rectangle out of the clip, so
   this trades it for a tinted glass: dark enough for AA body text against the
   brightest frame, clear enough that the wing still reads through it. */
.card--onbfly{
  background:rgba(5,11,9,.62);border-color:var(--border-inverse);
  backdrop-filter:var(--blur-glass);-webkit-backdrop-filter:var(--blur-glass);
}

/* --- Process steps ------------------------------------------------------- */
/* list-style:none because .step stays display:list-item inside the grid -
   without it the browser draws its own "1." alongside the "01" that .step::before
   is already drawing, and the rows number themselves twice. */
.steps{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--gutter);
  margin-top:var(--space-16);counter-reset:step;list-style:none;padding:0}
.step{position:relative;padding-top:var(--space-8);border-top:1px solid var(--border-default)}
.step::before{
  counter-increment:step;content:"0" counter(step);
  position:absolute;top:calc(-1 * var(--space-8));left:0;
  font-family:var(--font-display);font-weight:700;font-size:var(--size-sm);
  letter-spacing:var(--tracking-eyebrow);color:var(--jade-400);
}
.step::after{
  content:"";position:absolute;top:-1px;left:0;height:1px;width:0;background:var(--jade-400);
  transition:width var(--dur-scene) var(--ease-out);
}
.step.is-in::after{width:100%}
.step h3{font-size:var(--size-h3);margin-bottom:var(--space-3)}
.step p{font-size:var(--size-sm);color:var(--text-body)}
.step__meta{margin-top:var(--space-4);font-size:var(--size-xs);color:var(--text-muted);
  letter-spacing:var(--tracking-eyebrow);text-transform:uppercase;font-weight:700}

/* --- Checklist ----------------------------------------------------------- */
.checks{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--space-4) var(--space-6);margin-top:var(--space-10)}
.checks li{display:flex;gap:var(--space-3);align-items:flex-start;font-size:var(--size-sm);color:var(--text-on-dark)}
.checks .ic{color:var(--jade-400);margin-top:3px}

/* --- Results ------------------------------------------------------------- */
.result__metric{
  font-family:var(--font-display);font-weight:700;font-size:clamp(2.25rem,3.6vw,3rem);
  letter-spacing:var(--tracking-display);line-height:1;color:var(--jade-400);font-variant-numeric:tabular-nums;
}
.result__sector{font-size:var(--size-xs);letter-spacing:var(--tracking-eyebrow);text-transform:uppercase;
  font-weight:700;color:var(--text-muted);margin-bottom:var(--space-5)}

main:focus-visible{box-shadow:none}

/* --- Footer -------------------------------------------------------------- */
.footer{background:var(--surface-inverse-deep);padding:var(--space-20) 0 var(--space-8)}
.footer__grid{display:grid;grid-template-columns:1.6fr repeat(3,1fr);gap:var(--space-10)}
.footer__brand img{height:64px;width:auto}
.footer__brand p{color:var(--text-on-dark-muted);font-size:var(--size-sm);max-width:32ch;margin-top:var(--space-5)}
.footer__label{font-family:var(--font-body);font-weight:700;font-size:var(--size-eyebrow);
  letter-spacing:var(--tracking-eyebrow);text-transform:uppercase;color:var(--jade-400);margin-bottom:var(--space-5)}
.footer__col a,.footer__col p:not(.footer__label){display:block;font-size:var(--size-sm);color:var(--text-on-dark-muted);padding:5px 0}
.footer__col a:hover{color:var(--jade-300)}

/* The social row, at the foot of the "Get in touch" column. A row of marks
   rather than a list of names: four words stacked under the phone number
   would read as four more pages to visit, and they are the same tap either
   way. The hit area is 40px so it clears the touch target minimum without
   the icons themselves having to grow. */
.footer__social{display:flex;gap:var(--space-2);margin-top:var(--space-3)}
.footer__social a{
  display:grid;place-items:center;width:40px;height:40px;padding:0;
  border:1px solid var(--border-inverse);border-radius:var(--radius-pill);
  color:var(--text-on-dark-muted);
  transition:color var(--dur-fast) var(--ease-out),
             border-color var(--dur-fast) var(--ease-out),
             transform var(--dur-fast) var(--ease-out);
}
.footer__social a:hover{
  color:var(--jade-300);border-color:var(--jade-400);
  transform:translateY(var(--lift-hover));
}

.footer__legal{
  max-width:var(--container);margin:var(--space-16) auto 0;padding:var(--space-6) var(--page-x) 0;
  border-top:1px solid var(--border-inverse);
  display:flex;justify-content:space-between;gap:var(--space-4);flex-wrap:wrap;
  font-size:var(--size-xs);color:var(--text-on-dark-muted);
}
.footer__legal .eyebrow{margin:0;color:var(--text-on-dark-muted)}
/* The privacy link sits in the legal strip rather than in a nav column: it is
   a notice you go to when you want it, not a page anybody browses. Given its
   own rule so it reads as a link against the muted text either side of it. */
/* Both a link and a button carry this: the privacy notice is a page, and
   "Cookie settings" reopens the banner, so it is a real <button>. They are
   given the same treatment so the pair reads as one row rather than as a
   link next to a stray control. */
.footer__legal-link{
  color:var(--text-on-dark-muted);text-decoration:underline;text-underline-offset:3px;
  font:inherit;padding:0;
}
.footer__legal-link:hover{color:var(--jade-300)}

/* --- Cookie consent ------------------------------------------------------ */
/* A bar across the foot rather than a modal over the page. A visitor who has
   just arrived from a search result is trying to read something, and blocking
   the whole page to demand an answer first is how you teach people to click
   Accept without reading. This sits at the bottom, out of the way of the
   copy, and waits.

   ACCEPT AND REJECT ARE THE SAME SIZE AND SIT SIDE BY SIDE. .btn--ghost and
   .btn--primary differ in colour, not in weight or prominence, so neither
   reads as the default. Do not "improve" this by shrinking Reject or turning
   it into a text link: making refusal harder than acceptance is the specific
   thing the regulator objects to, and it would also make the claim on the
   privacy page untrue. */
.consent{
  position:fixed;left:0;right:0;bottom:0;z-index:var(--z-consent);
  padding:var(--space-4);
  transform:translateY(110%);
  transition:transform var(--dur-base) var(--ease-out);
}
.consent.is-in{transform:translateY(0)}
@media (prefers-reduced-motion:reduce){
  .consent{transition:none}
}
.consent__inner{
  max-width:var(--container);margin-inline:auto;
  display:flex;align-items:center;gap:var(--space-6);flex-wrap:wrap;
  background:var(--surface-card);border:1px solid var(--border-default);
  border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);
  padding:var(--space-5) var(--space-6);
}
.consent__copy{flex:1 1 320px;min-width:0}
.consent__title{
  font-family:var(--font-display);font-weight:var(--weight-display);
  color:var(--text-heading);font-size:var(--size-h4);
  letter-spacing:var(--tracking-tight);margin-bottom:var(--space-2);
}
.consent__copy p{font-size:var(--size-sm);color:var(--text-body);max-width:70ch}
.consent__copy a{text-decoration:underline;text-underline-offset:3px}
/* Two equal columns, not a flex row. "Accept" and "Reject" are different
   widths as words, and .btn--ghost carries a 1px border that .btn--primary
   does not, so any content-driven sizing leaves them a couple of pixels
   apart. A 1fr 1fr grid makes them identical at every width, which is the one
   thing this control has to be able to demonstrate: neither button is the
   easier one to press. */
.consent__actions{display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3);flex:0 0 auto}
.consent__actions .btn{justify-content:center;min-width:128px}

@media (max-width:620px){
  .consent{padding:var(--space-3)}
  .consent__inner{padding:var(--space-5);gap:var(--space-4)}
  .consent__actions{width:100%}
}

/* --- Scroll reveal ------------------------------------------------------- */
.js .reveal{opacity:0;transform:translateY(22px);
  transition:opacity var(--dur-slow) var(--ease-out),transform var(--dur-slow) var(--ease-out)}
/* The end state is "no reveal offset", not "no transform at all". Written as
   a custom property so a hover lift can still get through: this selector is
   three classes, so a plain transform here outranks every .thing:hover rule on
   the site and silently cancelled them - a.card:hover has been declaring a
   4px lift that never once applied. --lift defaults to none, and any rule that
   wants to move the element sets the property instead of the transform. */
.js .reveal.is-in{opacity:1;transform:var(--lift,none)}
@media (prefers-reduced-motion:reduce){.js .reveal{opacity:1;transform:none}}

/* --- Responsive ---------------------------------------------------------- */
/* Tablet / small laptop: keep the butterfly clear of the headline column. */
@media (max-width:1200px) and (min-width:901px){
  .bfly__scrim{background:linear-gradient(96deg,rgba(5,11,9,.97) 0%,rgba(5,11,9,.96) 42%,rgba(5,11,9,.78) 68%,rgba(5,11,9,0) 90%)}
}
@media (max-width:1080px){
  .grid-4{grid-template-columns:repeat(2,minmax(0,1fr))}
  .steps{grid-template-columns:repeat(2,minmax(0,1fr));row-gap:var(--space-12)}
  .footer__grid{grid-template-columns:1fr 1fr;row-gap:var(--space-12)}
}
/* The bar collapses on its own breakpoint, ahead of the 900px block below.
   Five page links plus the grant pill plus the CTA need about 928px of
   viewport before the centred list starts colliding with the logo, so the
   burger has to take over above 900px. Everything else in that block is
   about the page rather than the bar and keeps its own number.

   main.js watches (min-width: 981px) to close the drawer on the way back up
   to the full bar. IF YOU CHANGE THIS NUMBER, CHANGE THAT ONE. */
@media (max-width:980px){
  .nav__list,.nav__cta,.nav__link--offer{display:none}
  .nav__toggle{display:flex}
}
@media (max-width:900px){
  /* Portrait: the butterfly is fitted rather than cropped (see main.js), and
     the gradient runs vertically so the copy passing over it stays readable. */
  .bfly__scrim{background:linear-gradient(180deg,rgba(5,11,9,.56) 0%,rgba(5,11,9,.50) 22%,rgba(5,11,9,.84) 54%,rgba(5,11,9,.95) 76%)}
  .hero{align-items:end;padding-block:112px clamp(56px,10vh,96px)}
  .hero__copy{max-width:none}
  .split{grid-template-columns:1fr;gap:var(--space-12)}
  .stats{grid-template-columns:1fr}
  .stat{border-right:0;border-bottom:1px solid var(--border-subtle)}
  .stat:last-child{border-bottom:0}
  .grid-3{grid-template-columns:1fr}
}
@media (max-width:620px){
  .grid-4,.grid-2,.steps,.checks,.footer__grid{grid-template-columns:1fr}
  .card{padding:var(--space-6)}
  .footer__legal{flex-direction:column;gap:var(--space-2)}
}

/* The capability bento (.bento, .bento__cell/go/art/body/title/metric and the
   --lead/--tall/--photo/--light variants) stood here, styling the "How we
   work" section. That section is gone: it carried the same four photographs
   and the same four paragraphs as the service cards, which is a thing to say
   once. The services section says it, and now sits directly under the process
   animation.

   Worth knowing if a grid like it is ever wanted again: the cells were
   translucent with a backdrop blur because they were designed over the
   butterfly, and the photographs needed a measured scrim on top - a cream
   brand sheet under white type does not work by luck. */

/* ==========================================================================
   Device showcase
   Three staggered handsets, drawn in CSS rather than shipped as images so
   they stay crisp at any density. The outer two align and the middle one
   drops, matching the reference composition. Screens are empty swap slots:
   a 9:19.5 image dropped into .device__screen needs no other change.
   ========================================================================== */
.devices{position:relative;overflow:hidden}

.devices__mark{
  display:flex;justify-content:center;color:var(--jade-400);
  margin-bottom:var(--space-8);
}
.devices__mark svg{width:220px;height:54px;display:block}

.devices__head{max-width:640px;margin:0 auto var(--space-16);text-align:center}
.devices__head .lead{margin-top:var(--space-5);margin-inline:auto}

/* Row --------------------------------------------------------------------
   align-items:start so the stagger comes from margin, not from stretching. */
.devices__row{
  display:grid;grid-template-columns:repeat(3,minmax(0,1fr));
  gap:clamp(16px,3vw,44px);align-items:start;justify-items:center;
  padding-bottom:var(--space-12);
}
.device{margin:0;width:100%;max-width:300px}
.device--b{margin-top:clamp(40px,7vw,92px)}   /* middle handset sits lower */

/* Handset ---------------------------------------------------------------- */
.device__frame{
  position:relative;aspect-ratio:9 / 19.5;width:100%;
  border-radius:clamp(26px,3.4vw,44px);
  padding:clamp(6px,0.8vw,10px);
  background:linear-gradient(180deg,var(--night-700),var(--night-850));
  border:1px solid var(--border-subtle);
  box-shadow:
    0 1px 0 rgba(255,255,255,.06) inset,
    0 30px 60px rgba(0,0,0,.55),
    0 0 0 1px rgba(74,222,128,.06);
}
/* Jade edge light - the one brand accent on the hardware */
.device__frame::after{
  content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;
  background:linear-gradient(160deg,rgba(74,222,128,.14),transparent 42%);
  mix-blend-mode:screen;
}

.device__island{
  position:absolute;top:clamp(12px,1.4vw,18px);left:50%;transform:translateX(-50%);
  width:28%;height:clamp(12px,1.4vw,18px);border-radius:var(--radius-pill);
  background:var(--night-950);z-index:2;
}

.device__screen{
  position:relative;height:100%;width:100%;overflow:hidden;
  border-radius:calc(clamp(26px,3.4vw,44px) - clamp(6px,0.8vw,10px));
  background:
    radial-gradient(120% 80% at 50% 0%, rgba(74,222,128,.07), transparent 62%),
    var(--night-900);
  display:grid;place-items:center;
}
/* Real mockups drop straight in and fill the screen */
.device__screen img,.device__screen video{
  position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;
}

.device__slot{
  font-family:var(--font-body);font-weight:700;
  font-size:var(--size-eyebrow);letter-spacing:var(--tracking-eyebrow);
  text-transform:uppercase;color:var(--mist-400);
  border:1px dashed rgba(124,138,147,.5);border-radius:var(--radius-pill);
  padding:8px 14px;
}

/* Responsive --------------------------------------------------------------
   Below 900px the stagger stops earning its keep, so the row becomes a
   scroll strip; on the smallest screens it stacks. */
@media (max-width:900px){
  .devices__row{
    grid-template-columns:repeat(3,minmax(190px,1fr));
    gap:16px;overflow-x:auto;scroll-snap-type:x mandatory;
    padding-inline:var(--page-x);margin-inline:calc(var(--page-x) * -1);
    scrollbar-width:none;
  }
  .devices__row::-webkit-scrollbar{display:none}
  .device{scroll-snap-align:center}
  .device--b{margin-top:0}
}
@media (max-width:560px){
  .devices__row{grid-template-columns:repeat(3,minmax(168px,1fr))}
  /* This block used to swing the head left on the narrowest screens, on the
     idea that centred type is harder to read in a narrow measure. It read as
     a misalignment against the centred arc and the centred device strip below
     it, so the head keeps the centred treatment it has everywhere else and
     only the row narrows here. */
}
@media (prefers-reduced-motion:reduce){
  .devices__row{scroll-behavior:auto}
}

/* The "Our process" timeline (.flow, .flow__head/row/step/node/link/title/body,
   with its 900px and 560px breakpoints) stood here. The section it styled was
   removed - the pinned process steps further up the page make the same
   argument, and having both meant saying it twice. The dashed-connector
   treatment is not used anywhere else, so it went with the markup rather than
   being left as an orphan; the icons it owned (i-search, i-layout-template,
   i-gauge) came out of the sprite in index.html at the same time. */

/* .devices used to trim its bottom padding to close the seam with the process
   timeline below it. That section is gone and the pin runway follows now,
   which brings its own full section padding, so the trim stays: two full
   paddings back to back was the thing being avoided either way. */
.devices{padding-bottom:clamp(32px,4vw,56px)}

/* ==========================================================================
   Book a call
   Same language as the devices and process blocks: page ground, dark
   panel, jade for action only. The form is styled but deliberately unwired -
   see the submit guard in main.js.
   ========================================================================== */
.booking__grid{
  display:grid;grid-template-columns:1.05fr .95fr;gap:clamp(32px,5vw,72px);align-items:start;
}
.booking__copy .badge{margin-bottom:var(--space-5)}
.booking__copy .lead{margin-top:var(--space-6);max-width:46ch}

.booking__points{list-style:none;padding:0;margin:var(--space-8) 0 0;display:grid;gap:var(--space-5)}
.booking__points li{display:grid;grid-template-columns:24px 1fr;gap:var(--space-4);align-items:start}
.booking__points span:last-child{
  color:var(--text-on-dark-muted);font-size:var(--size-sm);line-height:var(--leading-body);
}
.booking__points b{display:block;color:var(--mist-100);font-weight:700}
.booking__tick{
  display:grid;place-items:center;width:24px;height:24px;border-radius:50%;
  background:var(--surface-accent-soft);color:var(--jade-400);
}
.booking__alt{margin-top:var(--space-8);font-size:var(--size-sm);color:var(--text-on-dark-muted)}
.booking__alt a{color:var(--jade-300);text-underline-offset:3px}

/* --- form panel --------------------------------------------------------- */
.booking__form{
  background:var(--surface-card);border:1px solid var(--border-subtle);
  border-radius:var(--radius-lg);padding:clamp(20px,3vw,32px);
  display:grid;gap:var(--space-5);box-shadow:var(--shadow-md,0 24px 48px rgba(0,0,0,.35));
}
.booking__form-head{
  display:flex;align-items:baseline;justify-content:space-between;gap:var(--space-4);
  padding-bottom:var(--space-4);border-bottom:1px solid var(--border-subtle);
}
.booking__form-label{
  font-family:var(--font-body);font-weight:700;font-size:var(--size-eyebrow);
  letter-spacing:var(--tracking-eyebrow);text-transform:uppercase;color:var(--jade-400);
}
.booking__form-meta{font-size:var(--size-xs);color:var(--text-on-dark-muted)}

.field{display:grid;gap:6px}
.field label{font-size:var(--size-xs);font-weight:700;color:var(--mist-100)}
.field input,.field select{
  width:100%;min-height:46px;padding:0 14px;
  background:var(--night-900);color:var(--mist-100);
  border:1px solid var(--border-default);border-radius:var(--radius-sm);
  font-family:var(--font-body);font-size:var(--size-sm);
  transition:border-color var(--dur-fast) var(--ease-out),box-shadow var(--dur-fast) var(--ease-out);
}
.field input::placeholder{color:var(--mist-500)}
.field input:hover,.field select:hover{border-color:var(--mist-400)}
.field input:focus,.field select:focus{
  outline:none;border-color:var(--jade-400);box-shadow:0 0 0 3px rgba(74,222,128,.18);
}
.field select{
  appearance:none;cursor:pointer;padding-right:38px;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%239FB3A9' stroke-width='1.6'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 14px center;
}
.booking__submit{width:100%;justify-content:center;margin-top:var(--space-2,4px)}

.booking__status{
  font-size:var(--size-sm);padding:12px 14px;border-radius:var(--radius-sm);
  background:var(--surface-accent-soft);border:1px solid var(--border-default);color:var(--mist-100);
}
/* Sending, succeeded, failed. This line is the only feedback the form gives,
   so the three states have to be told apart without reading the sentence. */
.booking__status.is-ok{border-color:var(--jade-400);color:var(--jade-200)}
.booking__status.is-err{
  border-color:var(--ember-400);color:var(--ember-400);
  background:rgba(245,167,66,.08);
}
.booking__submit.is-sending{opacity:.7;cursor:progress}
.booking__submit:disabled{cursor:not-allowed}

/* The honeypot. Pulled out of the layout rather than display:none, because
   some bots skip anything set to display:none and the whole point is that
   they fill it in. It is out of the tab order and aria-hidden as well, so
   nobody actually using the form ever meets it. */
.booking__trap{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}
.booking__small{font-size:var(--size-xs);color:var(--text-on-dark-muted);text-align:center}

/* --- responsive --------------------------------------------------------- */
@media (max-width:900px){
  .booking__grid{grid-template-columns:1fr;gap:var(--space-12)}
}

/* iOS Safari zooms the viewport when a focused input is under 16px. The 14px
   scale is right on desktop, so lift it only where the zoom would trigger. */
@media (max-width:640px){
  .field input,.field select{font-size:16px}
}

/* "if you have one" - the site field must not assume they already have one */
.field__opt{font-weight:400;color:var(--text-on-dark-muted);text-transform:none;letter-spacing:0}

/* Screen glare. A real phone screen is glass: without a reflection the mockup
   reads as a flat picture pasted into a rectangle. Kept very low opacity so it
   never competes with the design underneath, and sits above the artwork but
   below the island. */
.device__screen::after{
  content:"";position:absolute;inset:0;z-index:1;pointer-events:none;
  background:linear-gradient(148deg,
    rgba(255,255,255,.10) 0%,
    rgba(255,255,255,.04) 18%,
    rgba(255,255,255,0) 38%,
    rgba(255,255,255,0) 100%);
}
/* Inner shadow along the bezel edge, so the screen sits *in* the frame */
.device__screen::before{
  content:"";position:absolute;inset:0;z-index:2;pointer-events:none;
  border-radius:inherit;
  box-shadow:inset 0 0 0 1px rgba(0,0,0,.55), inset 0 2px 6px rgba(0,0,0,.35);
}

/* ==========================================================================
   Booking butterfly
   The page's second butterfly, and unlike the hero's it belongs to one
   section. It used to be a scroll-scrubbed traverse on a sticky stage that
   ran from the booking block down through the services grid (.fly,
   .fly__stage, .fly__canvas, .fly__scrim, .fly__flow, initFlySequence). That
   is gone. This one hovers on its own clock inside #contact and stops at the
   section's edges.

   Absolute rather than sticky, and `overflow:hidden` on the section, so the
   stage is exactly the booking block - the butterfly cannot appear over the
   growth story above it or the services grid below.

   The frames have a black ground rather than transparency, so screen blending
   drops it out: black contributes nothing, the wings show.
   ========================================================================== */
/* No overflow:hidden here - the stage below already clips the canvas, and
   contact.html reuses .booking without a stage, where hiding overflow would
   be a clipping rule that guards nothing. isolation:isolate stays: it is what
   gives the screen blend a group of its own to work inside. */
.booking{position:relative;isolation:isolate}
.booking__stage{
  position:absolute;inset:0;z-index:0;overflow:hidden;pointer-events:none;
  /* The blend needs a painted backdrop, and isolating the group means the
     page ground on <body> is not it. Without this the clip's black fill has
     nothing to screen against and stays black - a dark panel over the section
     rather than an invisible one. Painting the stage the page colour makes
     black screen to exactly the ground, so the frame has no edge. */
  background:var(--surface-page);
}
.booking__canvas{
  position:absolute;inset:0;width:100%;height:100%;display:block;
  mix-blend-mode:screen;          /* black ground becomes transparent */
  opacity:0;transition:opacity var(--dur-scene) var(--ease-out);
  /* Screening the clip over the ground lifts the whole section slightly -
     about rgb(12,21,18) against the page's rgb(8,17,14) - which draws a hard
     line across the top and bottom of the section, most visible on the right
     where the scrim is thinnest. Tapering the canvas at both edges lets the
     clip arrive and leave rather than switch on at a boundary. */
  -webkit-mask-image:linear-gradient(180deg,transparent 0,#000 110px,#000 calc(100% - 110px),transparent 100%);
          mask-image:linear-gradient(180deg,transparent 0,#000 110px,#000 calc(100% - 110px),transparent 100%);
}
.booking__canvas.is-ready{opacity:1}
/* The clip plays once and then leaves. The fade is slower than the site's
   longest token on purpose - --dur-scene would read as a cut at this size -
   and nothing ever removes this class, so the butterfly does not come back
   and start competing with the form. The duration lives here rather than in
   main.js - the script only adds the class. */
.booking__canvas.is-done{opacity:0;transition:opacity 1400ms var(--ease-out)}

/* Legibility scrim, same device as the hero: heaviest where the copy is,
   thinning where nothing has to be read. The copy column runs to about 49% of
   the section and the form card starts at 54% and is opaque, so past the copy
   there is nothing for the butterfly to make illegible - it is allowed
   through there, and passes behind the card rather than in front of it.
   Measured, not guessed. Against the brightest pixel the clip reaches, the
   ~.79 covering the copy column holds body text at 6.2:1 and the headline at
   8.9:1 - past the site's own 5:1 floor, with the butterfly still reading at
   2:1 against the ground rather than being crushed flat by a heavier veil. */
.booking__scrim{
  position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(96deg,
    rgba(8,17,14,.88) 0%,
    rgba(8,17,14,.80) 40%,
    rgba(8,17,14,.70) 56%,
    rgba(8,17,14,.58) 78%,
    rgba(8,17,14,.54) 100%);
}

/* The section's own content sits over the stage. */
.booking > .container{position:relative;z-index:1}

/* Portrait: the copy runs the full width and the form sits under it, so the
   gradient has to run vertically instead - the same correction the hero zone
   makes. The butterfly must never compete with the form for attention. */
@media (max-width:900px){
  /* :not(.is-done) matters: this rule comes after .is-done in the sheet and
     has the same specificity, so without it the faded-out butterfly would be
     pulled back to .8 on exactly the screens where it is most in the way. */
  .booking__canvas.is-ready:not(.is-done){opacity:.8}
  /* Measured the same way as the landscape scrim, against the same worst-case
     frame: this holds body copy at 6.9:1 with the butterfly at 1.8:1 against
     the ground. The heavier veil it replaced measured 9.8:1 and 1.3:1 - safe
     twice over for text, and too dark to see the butterfly through. */
  .booking__scrim{
    background:linear-gradient(180deg,
      rgba(8,17,14,.72) 0%, rgba(8,17,14,.75) 30%,
      rgba(8,17,14,.81) 62%, rgba(8,17,14,.86) 100%);
  }
}
@media (prefers-reduced-motion:reduce){
  .booking__canvas{transition:none}
}

/* ==========================================================================
   Growth story
   A full-width headline, then the argument in a column beneath it.

   This section used to carry a play-once film band (growth.mp4) with the copy
   laid over its dark left column. The film has been removed. The copy keeps
   the column width and type sizes it had over it - see the 1200px block below
   - so the layout reads the same; it simply sits on the page ground now.
   ========================================================================== */
.growth{padding-block:clamp(40px,5vw,80px) var(--section-y)}
/* Stacked layout (under 1200px): headline first, at full container width, then
   the argument beneath it. Balanced so the lines come out a similar length
   rather than a full one followed by an orphan. */
/* Sized to the container the same way .partner__title is, and for the same
   reason: it is a banner over its section, not a step on the type scale.
   No text-wrap:balance - balance shortens lines, which fights filling the
   width. Measured fill is documented with the cap below. */
.growth__headline{
  margin-top:0;margin-bottom:clamp(32px,4vw,64px);
  font-size:clamp(2.25rem,7.4vw,6.4rem);
  line-height:var(--leading-display);
  letter-spacing:var(--tracking-display);
}
.growth__copy{margin-top:clamp(24px,3vw,40px)}

.growth__cols{
  display:grid;grid-template-columns:1.35fr .9fr;
  gap:clamp(28px,4vw,64px);margin-top:var(--space-8);align-items:start;
}
.growth__cols .lead{max-width:52ch}
.growth__body{
  margin-top:var(--space-5);max-width:60ch;
  color:var(--text-on-dark-muted);line-height:var(--leading-body);
}

.growth__points{list-style:none;padding:0;margin:0;display:grid;gap:var(--space-4);align-content:start}
.growth__points li{
  display:grid;grid-template-columns:24px 1fr;gap:var(--space-4);align-items:center;
  color:var(--mist-100);font-size:var(--size-sm);
}
.growth__tick{
  display:grid;place-items:center;width:24px;height:24px;border-radius:50%;
  background:var(--surface-accent-soft);color:var(--jade-400);
}

/* The copy and the chart. Stacked below 1200px, side by side above it - see
   the block at the foot of this section.

   The clip is 1280x1280, five seconds, green bars drawn on pure black, so it
   screen-blends the same way the two butterfly zones do: black contributes
   nothing, the bars sit straight on the page ground with no panel or edge
   around them. aspect-ratio holds the square before metadata arrives, so the
   section does not jump when the video loads. */
.growth__layout{display:grid;gap:clamp(28px,4vw,56px);align-items:center}
.growth__chart{
  margin:0;max-width:420px;width:100%;justify-self:center;
  /* The blend needs something painted behind it. Every ancestor here is
     transparent - the page ground lives on <body>, and a root background is
     not part of a blend's backdrop, so screening against it would leave the
     clip's black square sitting on the page. Painting the figure the page
     colour and isolating it gives the blend an exact backdrop to work on:
     black screens to --surface-page and the edges of the square disappear. */
  background:var(--surface-page);isolation:isolate;
}
.growth__chart video{
  display:block;width:100%;height:auto;aspect-ratio:1;
  mix-blend-mode:screen;          /* black ground becomes the page ground */
}

@media (max-width:900px){
  .growth__cols{grid-template-columns:1fr;gap:var(--space-8)}
}

/* --------------------------------------------------------------------------
   Growth story - the argument under the headline

   This block sized the copy to fit the film's dark left column, alongside the
   butterfly. The film is gone; the measurements it was built on are gone with
   it. What is kept is the shape they produced, because that is the layout the
   section is meant to have: a narrow left column of smaller type under a
   full-width headline, rather than the wide two-column block used below.

   The right half this leaves is where the growth chart sits. If the chart ever
   comes out and the copy should spread again, this whole block is what to
   delete - the base rules above already lay it out across the full container
   in two columns.
   ========================================================================== */
@media (min-width:1200px){
  /* This width was derived from where the butterfly's wings started; it is now
     simply the measure, and a good one - 452.8px at 1440, around 60 characters
     of the lead. The calc still binds below roughly 1550px and the 470px cap
     takes over above it, so the column grows a little with the viewport and
     then stops. Kept as-is rather than flattened to a constant: the behaviour
     is what the layout was tuned around, and it is the shape being preserved. */
  .growth__copy{
    margin-top:0;
    max-width:min(470px, calc(42vw - max(0px, (100vw - var(--container))/2) - var(--page-x)));
  }
  /* Two halves rather than sizing the first column to the copy: .growth__copy
     already caps itself at the measure above, so the left column is the copy
     at exactly the width it had over the film, and the chart takes the rest.
     Centred against each other - the chart is taller than the copy. */
  .growth__layout{grid-template-columns:1fr 1fr;gap:clamp(32px,4vw,72px)}
  .growth__chart{max-width:none;justify-self:stretch}
  .growth__cols{grid-template-columns:1fr;gap:var(--space-6);margin-top:var(--space-6)}
  .growth__cols .lead{max-width:none;font-size:1.0625rem}
  .growth__body{margin-top:var(--space-4);max-width:none;font-size:var(--size-sm)}
  .growth__points{gap:var(--space-3)}

  /* The .is-told staggered reveal lived here - the copy was held at opacity 0
     until the clip reached its final frame, then popped in a beat apart. With
     no clip there is nothing to wait for, so the copy uses the ordinary
     .reveal scroll behaviour like every other section on the page. */
}

/* ==========================================================================
   Small business grant

   Copy left, art slot right, on the page ground - no band of its own. This
   went pine first, then --night-950, and both drew a visible seam where the
   section met the sections either side of it. The headline is large enough to
   open the section without a colour change behind it, so the ground stays the
   page's and the seam has nothing to draw.

   Not the jade band either: the booking form directly below carries a jade
   primary button, and two loud greens in a row would leave neither of them
   looking like the thing to click.

   The copy column is the wider of the two. The headline is display-2, the
   same size as every other section headline on the page, and it is long
   enough that an even split would run it to six lines.
   ========================================================================== */
/* The headline is a banner across the whole container rather than a step on
   the type scale, the same call .growth__headline makes further up the page.
   It is set to fill the container on one line, so the numbers here are
   measured rather than picked:

   In Space Grotesk bold, "£500 off your website build." is 12.69x the font
   size wide. The container is 1136px inside its padding at 1200px and above,
   which wants 89.5px - so the cap is 5.5rem/88px, a hair under, because a
   headline that fills the line exactly is a headline that wraps the moment a
   fallback font loads. Below 1200 the container tracks the viewport and
   7.15vw holds the same fill, until 2.25rem takes over around 507px and the
   line breaks in two, which is what it should do on a phone.

   No text-wrap:balance: balance shortens lines, which is the opposite of
   filling the width. If the wording changes, re-measure - the ratio above is
   for this string and no other. */
.grant__headline{
  margin:0 0 clamp(28px,3.5vw,52px);
  font-size:clamp(2.25rem,7.15vw,5.5rem);
  line-height:var(--leading-display);
  letter-spacing:var(--tracking-display);
}

/* Copy and art sit under the headline. Aligned to the top rather than centred:
   with the headline lifted out, the two columns are close enough in height
   that centring only shows up as a few stray pixels of drift. */
.grant__grid{
  display:grid;
  grid-template-columns:minmax(0,1.1fr) minmax(0,.9fr);
  gap:clamp(40px,6vw,88px);
  align-items:start;
}
.grant__copy .lead{max-width:52ch}

/* Same shape as .booking__points - a tick in a soft jade disc, a bold claim
   and the qualifier under it. Kept as its own class rather than reusing that
   one: this list sits on the darker ground, so the two text colours differ. */
.grant__points{list-style:none;padding:0;margin:var(--space-8) 0 0;display:grid;gap:var(--space-5)}
.grant__points li{display:grid;grid-template-columns:24px 1fr;gap:var(--space-4);align-items:start}
.grant__points span:last-child{
  font-size:var(--size-sm);line-height:var(--leading-body);color:var(--text-on-dark-muted);
}
.grant__points b{display:block;color:var(--mist-100);font-weight:700}
.grant__tick{
  display:grid;place-items:center;width:24px;height:24px;border-radius:50%;
  background:var(--surface-accent-soft);color:var(--jade-400);
}

.grant__actions{display:flex;flex-wrap:wrap;gap:var(--space-4);margin-top:var(--space-10)}
.grant__note{
  margin:var(--space-6) 0 0;max-width:56ch;
  font-size:var(--size-xs);line-height:var(--leading-body);color:var(--text-on-dark-muted);
}
.grant__note b{color:var(--mist-100);font-weight:700}

/* ---- The laptop --------------------------------------------------------- */
/* No frame or card around it. The reference for this was a laptop floating on
   a dark ground with nothing but its own shadow holding it there, and a box
   drawn around it would undo exactly that.

   Front-on rather than in perspective, which is a deliberate limit: a tilted
   laptop wants real perspective, real materials and a real reflection, and a
   CSS approximation of those reads as a cheap version of the photograph it is
   imitating. Flat and well made beats angled and nearly right - and it is the
   same call the handsets further up the page already make, so the two pieces
   of hardware on this page belong to one family.

   The materials below are the handset's, reused on purpose: same body
   gradient, same jade edge light, same glass glare, same inner bezel shadow.
   Edit them together. */
.grant__art{margin:0;display:grid;place-items:center}

.laptop{
  width:100%;max-width:560px;
  /* drop-shadow rather than box-shadow: the base is a clipped trapezoid, and
     a box shadow would draw the rectangle it was cut from. */
  filter:drop-shadow(0 26px 44px rgba(0,0,0,.5));
}

/* The lid. Top padding is heavier than the sides because that is where the
   camera sits, which is what stops the screen reading as a picture frame. */
.laptop__lid{
  position:relative;
  padding:clamp(11px,2.1vw,17px) clamp(6px,1.2vw,10px) clamp(7px,1.4vw,11px);
  border-radius:clamp(9px,1.5vw,13px);
  background:linear-gradient(180deg,var(--night-700),var(--night-850));
  border:1px solid var(--border-subtle);
  box-shadow:0 1px 0 rgba(255,255,255,.06) inset;
}
/* Jade edge light - the one brand accent on the hardware, as on the handsets */
.laptop__lid::after{
  content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;
  background:linear-gradient(160deg,rgba(74,222,128,.12),transparent 42%);
  mix-blend-mode:screen;
}
.laptop__cam{
  position:absolute;top:clamp(4px,0.75vw,6px);left:50%;transform:translateX(-50%);
  width:4px;height:4px;border-radius:50%;
  background:var(--night-950);box-shadow:0 0 0 1px rgba(255,255,255,.05);
}

.laptop__screen{
  position:relative;aspect-ratio:16 / 10;overflow:hidden;
  border-radius:clamp(3px,0.5vw,5px);
  background:var(--night-950);
}
.laptop__screen img{display:block;width:100%;height:100%;object-fit:cover}
/* Glass. Without a reflection the screen reads as a flat picture pasted into a
   rectangle - the note on .device__screen::after applies word for word. */
.laptop__screen::after{
  content:"";position:absolute;inset:0;z-index:1;pointer-events:none;
  background:linear-gradient(148deg,
    rgba(255,255,255,.09) 0%,
    rgba(255,255,255,.035) 18%,
    rgba(255,255,255,0) 38%,
    rgba(255,255,255,0) 100%);
}
/* Sits the screen *in* the bezel rather than on it */
.laptop__screen::before{
  content:"";position:absolute;inset:0;z-index:2;pointer-events:none;
  border-radius:inherit;
  box-shadow:inset 0 0 0 1px rgba(0,0,0,.6), inset 0 2px 6px rgba(0,0,0,.35);
}

/* The base is wider than the lid and tapers, which is the whole reason a
   front-on laptop reads as a laptop and not as a monitor. */
.laptop__base{
  position:relative;
  width:110%;margin-inline:-5%;
  height:clamp(9px,1.7vw,14px);
  /* The hairline at the top is the hinge catching the light; without it the
     lid and the base read as one extruded block. */
  background:
    linear-gradient(180deg,rgba(255,255,255,.10) 0 1px,transparent 1px),
    linear-gradient(180deg,var(--night-600) 0%,var(--night-800) 46%,var(--night-950) 100%);
  clip-path:polygon(0 0,100% 0,97.8% 100%,2.2% 100%);
}
/* The finger groove on the front edge */
.laptop__notch{
  position:absolute;left:50%;transform:translateX(-50%);bottom:0;
  width:9%;height:42%;border-radius:0 0 5px 5px;
  background:rgba(0,0,0,.5);
}

@media (max-width:900px){
  .grant__grid{grid-template-columns:1fr;gap:var(--space-12)}
  /* Capped rather than left full-bleed: at full width the laptop would push
     the form most of a screen further down on a phone. */
  .laptop{max-width:460px}
}
@media (max-width:560px){
  .grant__actions .btn{width:100%}
}

/* --------------------------------------------------------------------------
   Objections / FAQ
   <details> so it works with the script switched off and is keyboard-operable
   without any of it being written here.

   Lived in service.css until index.html needed it for the grant FAQ. It is
   shared markup now - the service pages, contact.html and the landing page
   all render it from here.
   ========================================================================== */
.faq{margin-top:clamp(40px,5vw,64px);border-top:1px solid var(--border-inverse)}
.faq__item{border-bottom:1px solid var(--border-inverse)}
.faq__q{
  display:flex;align-items:center;justify-content:space-between;gap:var(--space-6);
  padding-block:var(--space-6);cursor:pointer;list-style:none;
  font-family:var(--font-display);font-weight:700;font-size:var(--size-h3);
  letter-spacing:var(--tracking-tight);line-height:var(--leading-tight);
  color:var(--text-heading);
  transition:color var(--dur-fast) var(--ease-out);
}
.faq__q::-webkit-details-marker{display:none}
.faq__q:hover{color:var(--jade-300)}
.faq__q:focus-visible{box-shadow:var(--focus-ring);border-radius:var(--radius-xs)}
.faq__chev{
  flex:none;color:var(--jade-400);
  transition:transform var(--dur-base) var(--ease-out);
}
.faq__item[open] .faq__chev{transform:rotate(180deg)}
.faq__a{padding-bottom:var(--space-6);max-width:68ch}
.faq__a p{color:var(--mist-200);line-height:var(--leading-body)}

@media (max-width:560px){
  .faq__q{font-size:var(--size-h4);gap:var(--space-4)}
}

/* --------------------------------------------------------------------------
   Who we are
   Three columns across the full container: the bio card, the portrait, and a
   pull quote set at display size.

   Shared by about.html, contact.html and the landing page. It lived in
   service.css until index.html needed it; edit it once here and all three
   move together.

   The middle column is the narrowest of the three and that is deliberate. A
   portrait wants to be taller than it is wide, and the other two columns are
   text - giving the picture equal width would either crop it square or push
   the section a screen and a half tall.

   The quote is centred inside its own column, which is the one place that is
   allowed in this section: it is a pull quote, it has no measure to track along,
   and it is the composition the section is built from. The bio beside it stays
   left, because that is body copy and centred body copy is unreadable.
   ========================================================================== */
.who{
  display:grid;
  grid-template-columns:1fr .92fr 1.16fr;
  gap:clamp(20px,2.6vw,40px);
  align-items:start;
  margin-top:clamp(32px,4vw,56px);
}

/* ---- The bio ----------------------------------------------------------- */
.who__card{
  background:var(--surface-card);
  border:1px solid var(--border-subtle);
  border-radius:var(--radius-xl);
  padding:clamp(20px,2vw,28px);
}
.who__avatar{
  display:grid;place-items:center;overflow:hidden;
  width:64px;height:64px;border-radius:50%;
  background:var(--surface-sunken);
  border:1px dashed var(--border-default);color:var(--mist-400);
}
/* Dashed until a real photograph is in it, the same rule every other image
   slot on the site follows. :has() flips it without anyone remembering to. */
.who__avatar:has(img){border-style:solid;border-color:var(--border-subtle)}
.who__avatar img{display:block;width:100%;height:100%;object-fit:cover}
.who__name{
  margin:var(--space-5) 0 0;
  font-family:var(--font-display);font-weight:var(--weight-display);
  font-size:var(--size-h4);letter-spacing:var(--tracking-tight);
  color:var(--text-heading);
}
.who__role{margin-top:2px;font-size:var(--size-sm);color:var(--text-accent)}
.who__bio{margin-top:var(--space-5)}
.who__bio p{
  font-size:var(--size-sm);line-height:var(--leading-body);color:var(--text-body);
}
.who__bio p + p{margin-top:var(--space-4)}

/* ---- The portrait ------------------------------------------------------ */
.who__portrait{
  margin:0;aspect-ratio:4 / 5;
  display:grid;place-items:center;overflow:hidden;
  background:var(--surface-sunken);
  border:1px dashed var(--border-default);
  border-radius:var(--radius-xl);
}
.who__portrait:has(img){border-style:solid;border-color:var(--border-subtle)}
.who__portrait img{display:block;width:100%;height:100%;object-fit:cover}
.who__ph{
  display:flex;flex-direction:column;align-items:center;gap:var(--space-2);
  font-size:var(--size-sm);color:var(--mist-300);text-align:center;padding:var(--space-4);
}
.who__ph .ic{color:var(--mist-400)}

/* ---- The quote --------------------------------------------------------- */
.who__quote{
  margin:0;align-self:center;
  font-family:var(--font-display);font-weight:var(--weight-display);
  font-size:clamp(1.5rem,3.1vw,2.75rem);
  line-height:var(--leading-tight);letter-spacing:var(--tracking-display);
  color:var(--text-heading);text-align:center;text-wrap:balance;
}

/* Two columns before one: the portrait and the bio still read side by side at
   tablet width, and the quote takes the full row underneath where it has room
   to stay at display size instead of shrinking to fit a third of the screen. */
@media (max-width:1040px){
  .who{grid-template-columns:1fr .9fr}
  .who__quote{grid-column:1 / -1;margin-top:var(--space-6)}
}
@media (max-width:720px){
  .who{grid-template-columns:1fr;gap:var(--space-8)}
  .who__quote{margin-top:0}
  .who__portrait{max-width:420px}
}

/* The way out to the full about page. Only index.html has this - about.html is
   the page it points at, and contact.html is deliberately a dead end at that
   point in the journey: the form is right above it there.

   Centred rather than left under the bio card. The quote column is centred and
   is the last thing read on the row, so a centred button carries on from it
   instead of sending the eye back to the far left. */
.who__more{display:flex;justify-content:center;margin-top:clamp(40px,5vw,64px)}

/* ==========================================================================
   Service cards with art

   Rebuilt from the reference: a picture at the top of each card, the heading
   under it, the copy under that. The cards were a four-across row of icon
   tiles before, which sized the art out of the question - four columns at
   1200px leaves 250px a card, and a picture that small is a thumbnail.

   Two across instead of the reference's three, because there are four
   services and not six. Three columns would leave one card stranded on a row
   of its own, and a 2x2 gives each panel around 520px to work with, which is
   a real image rather than a stamp.

   Everything else is .card - the ground, the border, the hover lift, the jade
   edge on hover, the press. This only adds the art panel on top.
   ========================================================================== */
.svc-head{max-width:640px;margin-inline:auto;text-align:center}
.svc-head .lead{margin:var(--space-6) auto 0}

.svc-grid{
  display:grid;grid-template-columns:repeat(2,minmax(0,1fr));
  gap:var(--gutter);margin-top:var(--space-16);
}

/* Sits inside the card's own padding, so the panel, the heading and the copy
   all line up on one left edge. */
.card__art{
  position:relative;aspect-ratio:16 / 9;overflow:hidden;
  display:grid;place-items:center;
  margin-bottom:var(--space-2);
  background:var(--surface-sunken);
  border:1px dashed var(--border-default);
  border-radius:var(--radius-md);
}
/* Dashed while empty, solid once a real picture is in it - the same rule the
   portrait and the phone screens follow, and it flips without anyone having
   to remember to. */
.card__art:has(img){border-style:solid;border-color:var(--border-subtle)}
.card__art img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
.card__ph{
  display:flex;flex-direction:column;align-items:center;gap:var(--space-2);
  font-size:var(--size-sm);color:var(--mist-300);text-align:center;padding:var(--space-4);
}
.card__ph .ic{color:var(--jade-300)}

/* One across before the art panel gets too wide for the copy under it to hold
   a sensible measure. */
@media (max-width:780px){
  .svc-grid{grid-template-columns:1fr}
}

/* --- Articles ---------------------------------------------------------------- */
/* The listing. Three across, because a post card is title plus two lines and
   does not need the width a case-study card does. It breaks a step earlier
   than .grid-3 would on its own so the standfirst never drops to one word a
   line in the middle column. */
.posts{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--gutter)}
.posts > article{display:flex}
.posts .card{width:100%}
@media (max-width:900px){
  .posts{grid-template-columns:repeat(2,minmax(0,1fr))}
}
@media (max-width:620px){
  .posts{grid-template-columns:1fr}
}

/* Date and category, above the title. Small, muted and tabular so a column of
   cards lines up on the day rather than jittering with the digit widths. */
.post__meta{
  display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;
  font-size:var(--size-xs);color:var(--text-muted);font-variant-numeric:tabular-nums;
}
.post .h4{text-wrap:balance}

/* The empty state, for as long as there is nothing to list. Held to a reading
   measure and centred in the container rather than stretched across it: a
   full-width panel with three lines in it reads as a page that failed to
   load, and this one has not failed at all. */
.posts__empty{
  max-width:680px;margin-inline:auto;text-align:center;
  background:var(--surface-card);border:1px solid var(--border-subtle);
  border-radius:var(--radius-2xl);padding:clamp(32px,5vw,56px);
}
.posts__empty p{margin-top:var(--space-4);color:var(--text-body)}
.posts__empty .btn{margin-top:var(--space-8)}
