/* ==========================================================================
   Stacklane Group — global.css
   The single hand-written stylesheet. No framework, no build step.

   Theme: Enterprise. Human. Future-ready.
   Warm neutrals around the logo's green and gold. Light-only by design —
   see the note in section 3.

   Contents
   1.  Font
   2.  Design tokens
   3.  Reset & base
   4.  Typography
   5.  Layout primitives   .container .section .grid .stack .cluster
   6.  Components          .btn .card .media .unit .insight ...
   7.  Header & footer
   8.  Utilities
   9.  Motion
   ========================================================================== */

/* 1. Font ---------------------------------------------------------------- */
/* Cairo (headings) + Inter (body) are loaded via <link> in BaseLayout so
   they can be preconnected. Declared here only as the family stacks. */

/* 2. Design tokens ------------------------------------------------------- */
:root {
  /* -- 2a. Primitives: the generated scales ----------------------------
     Generated by scripts/generate-tokens.py by interpolating L/C/H in
     OKLCh through the logo colours, then gamut-mapping to sRGB. Steps
     marked (brand) are the exact supplied hexes.

     Components must NOT reference these directly — use the semantic
     aliases in 2b, so a palette change is a one-line edit there. */

  /* Stacklane Green — hue ~187 */
  --green-0:         #F8FEFD;
  --green-50:        #E4F2F0;
  --green-100:       #D0E5E3;
  --green-200:       #A8CDCA;
  --green-300:       #80B6B2;
  --green-400:       #569E99;
  --green-500:       #208782;  /* brand */
  --green-600:       #1B6963;
  --green-700:       #154D47;  /* brand */
  --green-800:       #0F3A35;  /* brand */
  --green-900:       #022420;
  --green-950:       #001917;
  --green-1000:      #000F0D;

  /* Stacklane Gold — hue ~87 */
  --gold-0:          #FEFCF6;
  --gold-50:         #FCF5E2;
  --gold-100:        #F9EECD;
  --gold-200:        #F5DEA1;
  --gold-300:        #F2CE72;
  --gold-400:        #EFBC30;  /* brand */
  --gold-500:        #CB9F27;
  --gold-600:        #A8831F;  /* brand */
  --gold-700:        #82620F;
  --gold-800:        #5D4201;
  --gold-900:        #392600;
  --gold-950:        #281900;
  --gold-1000:       #180D00;

  /* Stacklane Terracotta — warm, hue ~41. The third brand colour: eyebrow and
     section-label TEXT on light surfaces, where gold-400 is 1.4-1.8:1 and
     unreadable. 600 is the first AA-safe step and the one in use. */
  --terracotta-0:    #FFFCFA;
  --terracotta-50:   #FAECE5;
  --terracotta-100:  #F2DDD3;
  --terracotta-200:  #E3C0B0;
  --terracotta-300:  #D4A38E;
  --terracotta-400:  #C3866C;
  --terracotta-500:  #B3694C;
  --terracotta-600:  #A14C2A;  /* brand */
  --terracotta-700:  #7E3820;
  --terracotta-800:  #5D2516;
  --terracotta-900:  #3E140B;
  --terracotta-950:  #2F0C06;
  --terracotta-1000: #210503;

  /* Warm Neutral — warm, hue ~105 */
  --neutral-0:       #FFFFFF;  /* brand */
  --neutral-50:      #FCFCFA;  /* brand */
  --neutral-100:     #F7F7F3;  /* brand */
  --neutral-200:     #E6E5DF;  /* brand */
  --neutral-300:     #D8D7CF;  /* brand */
  --neutral-400:     #A7A79E;  /* brand */
  --neutral-500:     #888880;
  --neutral-600:     #6A6A63;  /* brand */
  --neutral-700:     #4D4D47;
  --neutral-800:     #32322C;  /* brand */
  --neutral-900:     #1D1D19;
  --neutral-950:     #141310;
  --neutral-1000:    #0A0A08;

  /* Cool Neutral — hue 220, the second neutral axis.

     The warm neutrals above are the site's EDITORIAL material: page, sections,
     cards, body copy. Warm is the brand and stays that way. But the same greys
     were also doing INTERFACE duty — control borders, code panels, disabled
     states — and a warm grey there reads as paper rather than as chrome. This
     scale takes that second job, so neither has to compromise: warm for what you
     read, cool for what you operate.

     Hue 220 is far enough from the warm 106 to read as another family at a
     glance, and 30 off the brand teal (190) so it relates without being mistaken
     for it. Chroma 0.014 — visibly cool beside the warm greys, still a neutral
     rather than a pale blue. Generated in OKLCh by scripts/generate-tokens.py. */
  --cool-0:            #FFFFFF;
  --cool-50:           #F9FDFE;
  --cool-100:          #F2F8FA;
  --cool-200:          #DDE7EA;
  --cool-300:          #CDD9DD;
  --cool-400:          #A2AEB2;
  --cool-500:          #798488;
  --cool-600:          #5E6B70;
  --cool-700:          #455358;
  --cool-800:          #2C3C41;
  --cool-900:          #15262C;
  --cool-950:          #0C181C;
  --cool-1000:         #050B0E;

  /* Success — hue 148 — deliberately clear of brand green */
  --success-0:       #F9FEFA;
  --success-50:      #E6F3E8;
  --success-100:     #D4E8D7;
  --success-200:     #B0D1B5;
  --success-300:     #8DBB94;
  --success-400:     #69A573;
  --success-500:     #448F52;  /* brand */
  --success-600:     #347441;
  --success-700:     #255931;
  --success-800:     #174021;
  --success-900:     #092912;
  --success-950:     #051D0B;
  --success-1000:    #021305;

  /* Warning — hue 58 — deliberately clear of brand gold */
  --warning-0:       #FFFCF9;
  --warning-50:      #FFF0E2;
  --warning-100:     #FCE4CF;
  --warning-200:     #F6CDAA;
  --warning-300:     #F0B584;
  --warning-400:     #E99D5C;
  --warning-500:     #E2832D;  /* brand */
  --warning-600:     #B86723;
  --warning-700:     #8F4C18;
  --warning-800:     #68330C;
  --warning-900:     #431C03;
  --warning-950:     #321200;
  --warning-1000:    #210800;

  /* Danger — hue 25 */
  --danger-0:        #FFFBFB;
  --danger-50:       #FFE9E8;
  --danger-100:      #FED6D4;
  --danger-200:      #F6B2AF;
  --danger-300:      #EB8D89;
  --danger-400:      #DF6863;
  --danger-500:      #D03C3C;  /* brand */
  --danger-600:      #AA2E2D;
  --danger-700:      #85211F;
  --danger-800:      #621513;
  --danger-900:      #410907;
  --danger-950:      #310504;
  --danger-1000:     #220202;

  /* Info — hue 250 */
  --info-0:          #FAFDFF;
  --info-50:         #E4F0FC;
  --info-100:        #D0E3F6;
  --info-200:        #A9C9EB;
  --info-300:        #82AFDE;
  --info-400:        #5A95D1;
  --info-500:        #2C7BC3;  /* brand */
  --info-600:        #2462A0;
  --info-700:        #1B4B7F;
  --info-800:        #12355E;
  --info-900:        #082040;
  --info-950:        #041731;
  --info-1000:       #020E23;

  /* -- 2b. Semantic aliases --------------------------------------------
     The only colour tokens components should reference. */

  /* Brand greens */
  --green-light:         var(--green-500);    /* Icons, focus ring, decorative accents (3:1 uses) */
  --green-link:          var(--green-600);    /* Body links — AA-safe at 6.30:1 on --bg */
  --green-dark:          var(--green-700);    /* Primary button, headlines, CTA band */
  --green-deep:          var(--green-800);    /* Footer background */
  --green-tint:          var(--green-50);     /* Secondary-button hover, soft fills */
  --green-tint-strong:   var(--green-100);    /* ::selection, stronger fills */

  /* Brand gold — sparingly */
  --gold-light:          var(--gold-400);     /* Decorative accents, active-nav rail, gold on dark */
  /* Was gold-600. Changed to gold-400 by product decision: the brand gold is
     wanted on light surfaces as well as dark, accepting that it is 1.76:1 on
     white and 1.69:1 on --bg, below the 3:1 that WCAG 1.4.11 asks of a
     meaningful icon. Everything fed by this token is affected — arrow links,
     offer ticks, icon chips, the current-page nav marker and the megamenu
     icons. If that is ever revisited, gold-600 (3.54:1 on white) is the
     brightest gold in the scale that clears the floor; gold-500 only reaches
     2.46:1. Gold TEXT went the same way — see --gold-text below. */
  --gold-dark:           var(--gold-400);     /* Brand gold on any surface */
  /* Was gold-700, the first AA-safe step. Changed to gold-400 by the same
     product decision as --gold-dark: the brand gold is wanted on the eyebrows
     everywhere, light surfaces included. This is a larger exception than the
     icon one, because eyebrows are TEXT at 12px semibold and so need 4.5:1:
     1.72:1 on --bg, 1.64:1 on --bg-section, 1.76:1 on white, down from
     5.52/5.28/5.67. Feeds .label and the current-section nav trigger. To
     reverse, set this back to var(--gold-700). */
  --gold-text:           var(--gold-400);     /* Brand gold for eyebrow text */
  /* Terracotta: the third brand colour, for eyebrow text on LIGHT surfaces where
     gold-400 is 1.4-1.8:1 and unreadable. Hue 17deg, so it sits in the same warm
     family as the gold and the warm-tinted neutrals rather than fighting them,
     while being 157deg off the brand green.
     Darkened from #B4552F, which cleared white and the section backgrounds but
     came in at 3.89:1 on the light hero's --neutral-200 and 4.26:1 on
     --green-tint. At this value the worst light surface is 4.64:1.
     NOT for dark green: 1.64:1 on --green-700. Gold keeps those. */
  /* Eyebrow / section-label text on LIGHT surfaces. green-400 by request.
     Renamed from --terracotta-text because it no longer holds a terracotta, and a
     token whose name contradicts its value is worse than a rename.

     MEASURED, AND IT DOES NOT PASS. green-400 is #569E99, a mid-light teal:
       white 3.11   --bg 3.04   --bg-section 2.90   --green-tint 2.71   hero 2.47
     An eyebrow is 13px uppercase, so it is normal text and wants 4.5:1. These are
     short of that on every surface, and short of even the 3:1 large-text floor on
     three of them. The audit reports them as failures rather than reclassifying
     them, so the position stays visible.

     green-600 (#1B6963) is the lightest green that clears it — 5.14:1 worst case,
     and a one-line change here if that is wanted. green-500 does not: 3.44:1. */
  --eyebrow-text:        var(--green-400);    /* Eyebrow text on light — see above */
  --gold-tint:           var(--gold-50);      /* Icon-chip and badge fills */

  /* Warm neutrals */
  /* SURFACES ARE COOL TOO.
     Type moved to the cool scale first and the surfaces were left warm, which
     put cool copy on warm paper — two temperatures arguing. The whole neutral
     family is cool now, and the warm ramp above is kept only for the overlay
     black (--neutral-1000) and as the record of what the site used to be.

     The cool ramp's 50/100/200/300 are pinned to the warm ramp's OKLCh lightness
     to within 0.08 L, so this changed the temperature of every surface and the
     value of none of them: no page got darker, no border got heavier. */
  --white:               var(--cool-0);       /* Card and surface fill */
  --bg:                  var(--cool-50);      /* Page background */
  --bg-section:          var(--cool-100);     /* Section background */
  --border:              var(--cool-200);     /* Default border */
  --divider:             var(--cool-300);     /* Divider, strong border */
  /* TYPE IS COOL, SURFACES ARE WARM.
     The warm neutrals above keep the page, sections, cards, borders and dividers
     — the material the brand's warmth lives in. The type moves to the cool scale,
     which is the split the reference palette specifies: a warm section background
     with cool body copy over it.

     It is also not a downgrade anywhere it matters. Measured on every light
     surface this site paints (white, --bg, --bg-section, --green-tint):
       --text          cool-800  12.18 / 11.86 / 11.34 / 10.59
       --text-secondary cool-600  5.63 /  5.48 /  5.24 /  4.90  (worst was 4.74 warm)
       --text-disabled cool-400   2.83 /  2.76 /  2.64 /  2.46  (exempt; warm was 2.42)
     Secondary text comes out slightly STRONGER than the warm grey it replaces on
     the tightest surface, which is the one that decided the step. */
  --text-disabled:       var(--cool-400);     /* Disabled text, placeholder */
  --text-secondary:      var(--cool-600);     /* Secondary body text — 4.90:1 worst case */
  /* #15262C, supplied as the primary text colour. It pins cool-900, so this stays
     a step reference rather than a loose hex. 15.61:1 on white and 12.41:1 on the
     tightest light surface — stronger than the cool-800 it replaces (12.18 /
     9.65) and than the brand teal the headings used to use (9.61 / 7.64). */
  --text:                var(--cool-900);     /* Primary text and headings — 12.41:1 worst case */

  /* Form controls — a control's boundary must reach 3:1 against its fill
     (WCAG 1.4.11), which --border (1.26:1) and --divider (1.44:1) do not.
     neutral-500 is the first step that clears it, at 3.57:1 on white. */
  /* Interface material, from the cool scale. Deliberately a small set: a role
     with nothing rendering it is just a token nobody can verify. */
  /* One step down from where these started: cool-50 became the page background,
     so a panel drawn in it would be invisible. */
  --ui-surface:          var(--cool-100);     /* Code and pre fill, technical panel */
  --ui-border:           var(--cool-300);     /* Border on a --ui-surface panel */
  /* Was neutral-500 (warm, 3.57:1 on white). cool-500 is 3.84:1, so this both
     reads as chrome and clears WCAG 1.4.11's 3:1 by a wider margin. */
  --border-control:      var(--cool-500);     /* Input, select, textarea, checkbox */
  --border-error:        var(--danger-600);   /* Invalid control, 6.69:1 on white */

  /* On dark green surfaces */
  --on-dark:             var(--green-0);      /* Body text on green-700/800 */
  --on-dark-muted:       var(--green-200);    /* Muted text on dark — 5.61:1 on green-700 */
  --on-dark-border:      var(--green-600);    /* Dividers and rings on dark */

  /* -- 2c. Opacity ------------------------------------------------------
     Percentages so they drop straight into rgb(... / var(--opacity-N)). */
  --opacity-0:       0%;
  --opacity-5:       5%;
  --opacity-10:      10%;
  --opacity-20:      20%;
  --opacity-30:      30%;
  --opacity-40:      40%;
  --opacity-50:      50%;
  --opacity-60:      60%;
  --opacity-70:      70%;
  --opacity-80:      80%;
  --opacity-90:      90%;
  --opacity-100:     100%;

  /* -- Typography ------------------------------------------------------- */
  --font-heading: 'Cairo', -apple-system, BlinkMacSystemFont, 'Segoe UI',
    sans-serif;
  --font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
    Helvetica, Arial, sans-serif;

  /* Scaled up one step across the board for a less traditional, more current
     feel. The brief specified a fixed 16/28 body; this deliberately leaves that
     behind at 18/28.8. Note `html` has no font-size, so 1rem is still 16px —
     every --space-*, radius and max-width in this file is untouched by the
     change, and only type moves. Leading tightens as the sizes grow, which is
     what keeps it from reading as merely "zoomed": big type needs a smaller
     multiplier, not the same one. */
  --text-body: 1.125rem;  /* 18 */
  --leading-body: 1.6;    /* 28.8px at 18px */

  --text-xs:  0.8125rem;  /* 13 */
  --text-sm:  0.9375rem;  /* 15 */
  --text-lg:  1.25rem;    /* 20 */
  --text-xl:  1.375rem;   /* 22 */

  /* Editorial heading scale. Each clamp keeps its old shape — min at ~400px
     viewport, max reached around 1220px — just moved up a step. */
  --text-h1: clamp(2.75rem, 1.9rem + 3.4vw, 4.5rem);   /* 44 → 72 */
  --text-h2: clamp(2.25rem, 1.8rem + 1.9vw, 3rem);     /* 36 → 48 */
  --text-h3: clamp(1.75rem, 1.5rem + 1vw, 2.25rem);    /* 28 → 36 */
  --text-h4: 1.5rem;                                   /* 24 */
  --text-display: clamp(3rem, 2rem + 4.2vw, 5.25rem);  /* 48 → 84 */

  /* 1.1 rather than a tighter display value because every h1–h3 shares this,
     and h3 at 28px still has to survive wrapping to two lines. */
  --leading-tight: 1.1;
  --leading-snug:  1.28;

  --tracking-tight: -0.03em;
  --tracking-label: 0.1em;   /* gold section labels */

  /* -- Space: 8px rhythm ------------------------------------------------ */
  --space-1:  0.25rem;
  --space-2:  0.5rem;
  --space-3:  0.75rem;
  --space-4:  1rem;
  --space-5:  1.25rem;
  --space-6:  1.5rem;
  --space-8:  2rem;
  --space-10: 2.5rem;
  --space-12: 3rem;
  --space-16: 4rem;
  --space-20: 5rem;
  --space-24: 6rem;
  --space-30: 7.5rem; /* 120px — top of the scale, unused since --section-y came down */

  /* Vertical rhythm between sections, and the single lever for it — .section and
     .cta both take their padding-block from here, so nothing else needs touching.

     WHY IT CAME DOWN. It was clamp(4rem, 8vw, 7.5rem): 120px a side at 1600px wide,
     which stacks into a 240px gap between two sections, because the gap a visitor
     sees is one section's bottom padding plus the next one's top. That read as
     disconnected rather than spacious — the home page ran to 9,520px and several
     bands had more air around them than content in them.

     Now clamp(3rem, 6vw, 6rem): 96px a side at the top end, 48px at the bottom, for
     a ~25% tighter gap at every width. Still generous by enterprise-site standards
     and still proportional to the viewport; the sections just stop drifting apart. */
  --section-y: clamp(var(--space-12), 6vw, var(--space-24));
  --card-gap: var(--space-8); /* 32px */

  /* -- Radii ----------------------------------------------------------- */
  --radius-btn:  10px;
  --radius-card: 16px;
  --radius-sm:   6px;
  --radius-full: 9999px;

  /* -- Layout ---------------------------------------------------------- */
  --container-max: 80rem; /* 1280px */
  --container-pad: clamp(var(--space-5), 4vw, var(--space-10));
  --prose-max: 44rem;
  --header-h: 5rem;
  --hero-min-h: clamp(34rem, 74vh, 50rem); /* ~700–800px on desktop */

  /* -- Elevation: very subtle, never heavy ------------------------------ */
  --shadow-xs: 0 1px 2px rgb(50 50 44 / 0.04);
  --shadow-sm: 0 1px 3px rgb(50 50 44 / 0.05), 0 1px 2px rgb(50 50 44 / 0.03);
  --shadow-md: 0 6px 16px rgb(50 50 44 / 0.06), 0 2px 4px rgb(50 50 44 / 0.03);
  --shadow-lg: 0 16px 40px rgb(50 50 44 / 0.08), 0 4px 8px rgb(50 50 44 / 0.03);

  /* -- Motion ---------------------------------------------------------- */
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --dur-btn:    150ms;  /* button background */
  --dur-card:   300ms;  /* card lift, image zoom */
  --dur-reveal: 600ms;  /* hero / section fade-up */

  --z-header: 100;
  --z-overlay: 200;
  /* Above the mobile nav: a toast reports why something just failed, and it is
     no use behind the thing the visitor was using when it did. */
  --z-toast: 250;
  /* The first-paint loader sits above the skip link too — for the ~500ms it is
     up it is the only thing on screen. */
  --z-loader: 300;
}

/* 3. Reset & base -------------------------------------------------------- */
/* Light-only. The brief specifies a single warm palette; enterprise peers
   (Deloitte, IBM, Stripe) ship light-only too, so there is no dark theme
   and no theme toggle. `color-scheme: light` keeps form controls correct
   for visitors whose OS is set to dark. */
:root {
  color-scheme: light;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

* {
  margin: 0;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-h) + var(--space-4));
}

body {
  background-color: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--text-body);
  line-height: var(--leading-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  min-height: 100dvh;
  overflow-x: hidden;
}

body[data-scroll-locked] {
  overflow: hidden;
}

.modal[open] {
  display: flex;
  flex-direction: column;
}

img,
picture,
svg,
video,
canvas {
  display: block;
  max-width: 100%;
  height: auto;
}

svg {
  flex-shrink: 0;
}

input,
button,
textarea,
select {
  font: inherit;
  color: inherit;
}

button {
  background: none;
  border: 0;
  cursor: pointer;
}

ul[role='list'],
ol[role='list'] {
  list-style: none;
  padding: 0;
}

hr {
  border: 0;
  border-top: 1px solid var(--border);
  margin-block: var(--space-10);
}

:focus-visible {
  outline: 2px solid var(--green-light);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

:focus:not(:focus-visible) {
  outline: none;
}

::selection {
  background-color: var(--green-tint-strong);
  color: var(--green-dark);
}

/* 4. Typography ---------------------------------------------------------- */
h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-heading);
  font-weight: 700;
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  text-wrap: balance;
  color: var(--text);
}

h1 { font-size: var(--text-h1); }
h2 { font-size: var(--text-h2); }
h3 { font-size: var(--text-h3); }
h4 { font-size: var(--text-h4); line-height: var(--leading-snug); }
h5, h6 { font-size: var(--text-lg); line-height: var(--leading-snug); }

p {
  text-wrap: pretty;
}

a {
  color: var(--green-link);
  text-decoration: none;
  text-underline-offset: 0.2em;
  transition: color var(--dur-btn) var(--ease-out);
}

a:hover {
  color: var(--green-dark);
  text-decoration: underline;
}

/* A link that wraps a whole card, tile, or row is not a run of inline text —
   the underline above would strike through every line inside it. Declared once
   here rather than per component, and with a class in the selector so it beats
   `a:hover`. Inline links in prose keep their underline. */
a:is(
  .card, .industry, .insight, .brand, .btn, .arrow-link, .nav__link,
  .megamenu__item, .megamenu__link, .megamenu__all, .megamenu__featured,
  .showcase__tab, .showcase__viewall, .map-pin, .breadcrumb a, .info-panel .brand
),
a:is(
  .card, .industry, .insight, .brand, .btn, .arrow-link, .nav__link,
  .megamenu__item, .megamenu__link, .megamenu__all, .megamenu__featured,
  .showcase__tab, .showcase__viewall, .map-pin, .breadcrumb a
):hover {
  text-decoration: none;
}

strong, b { font-weight: 600; }

small { font-size: var(--text-sm); }

/* Gold section label — the brief's "sparingly" accent.
 *
 * TWO COLOURS, split by the surface underneath: terracotta on light, gold on dark
 * green. Gold-400 on a light surface is 1.4-1.8:1 against the 4.5:1 a 12px label
 * needs, and under 3:1 too, so no size or weight change reaches it. Terracotta
 * clears every light surface, worst case 4.64:1 on the light hero.
 * It cannot be used the other way round — terracotta is 1.64:1 on --green-700 —
 * so the dark-surface rules further down opt back into gold, which is 5.45:1 to
 * 9.33:1 there. The base rule below is the LIGHT case because that is the
 * majority of eyebrows; dark surfaces are the exceptions and name themselves.
 *
 * display: block matters. The chip version carried width: fit-content, and a
 * fit-content block with no auto margins sits at its container's left edge —
 * text-align: center then centres the text INSIDE that narrow box rather than
 * moving the box, which left every centred eyebrow, the CTA band's included,
 * hanging off to the left while the heading under it stayed centred.
 */
.label {
  display: block;
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--eyebrow-text);
}

/* The gold label is an eyebrow: it belongs to the heading that follows, so it
   sits tighter than the container's own rhythm would place it. */
.label + h1,
.label + h2,
.label + h3 {
  margin-top: var(--space-3);
}

.lead {
  font-size: var(--text-lg);
  /* 1.55, not the old 1.7: --text-lg went 18 -> 20px, and 1.7 there is a 34px
     line that opens gaps in a two-line intro. */
  line-height: 1.55;
  color: var(--text-secondary);
}

.body-muted {
  color: var(--text-secondary);
}

/* Text link with a trailing arrow that nudges — "Learn More →" */
/* SWAPPED BY REQUEST: this rests on green-500 and darkens to green-700 on hover.
   It was the other way round.

   THE RESTING COLOUR IS BELOW AA, and knowingly. The label is 15px at weight 600 —
   normal text by WCAG's reckoning, not large — so it needs 4.5:1, and green-500
   gives 4.23:1 on --bg, 4.04:1 on --bg-section and 3.44:1 on the light hero. That is
   the same reason --green-light is annotated "3:1 uses only, never body text": it was
   drawn for icons and focus rings. The hover state is the accessible one now
   (green-700, 9.39:1), which inverts the usual arrangement — a keyboard or touch
   visitor never reaches it. Swap the two values back to return to AA. */
.arrow-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-weight: 600;
  font-size: var(--text-sm);
  color: var(--green-light);
}

.arrow-link:hover {
  color: var(--green-dark);
  text-decoration: none;
}

.arrow-link svg {
  width: 1.125em;
  height: 1.125em;
  /* Stacklane gold — see the --gold-dark note in the token block for why this
     is the brand gold-400 despite being 1.76:1 on white. */
  color: var(--gold-dark);
  transition: transform var(--dur-card) var(--ease-out);
}

.arrow-link:hover svg { transform: translateX(4px); }

/* `.arrow-link svg` above styles every svg in the link — it was written when the
   arrow was the only one. A leading icon is not the arrow: it takes the link's own
   colour and stays put on hover. Higher specificity rather than an edit to that
   rule, which every arrow link on the site depends on. */
.arrow-link .arrow-link__icon { color: currentcolor; }
.arrow-link:hover .arrow-link__icon { transform: none; }

/* Long-form content */
.prose {
  max-width: var(--prose-max);
  color: var(--text-secondary);
}

.prose > * + * { margin-top: var(--space-6); }
.prose h2 { margin-top: var(--space-12); font-size: var(--text-h3); }
.prose h3 { margin-top: var(--space-10); font-size: var(--text-xl); }
.prose ul, .prose ol { padding-left: var(--space-6); }
.prose li + li { margin-top: var(--space-2); }
.prose a { text-decoration: underline; }
.prose img { border-radius: var(--radius-card); }
.prose blockquote {
  border-left: 3px solid var(--gold-light);
  padding-left: var(--space-6);
  font-size: var(--text-lg);
  color: var(--text);
}
/* Cool, not the warm --bg-section. Code is the clearest case for the second
   neutral: a monospace block on warm paper reads as a quotation, and on a cool
   panel it reads as something the machine produced. Nothing in the content
   renders <pre> yet — these rules are here for when it does. */
.prose code {
  background-color: var(--ui-surface);
  border-radius: var(--radius-sm);
  padding: 0.15em 0.4em;
  font-size: 0.9em;
}
.prose pre {
  background-color: var(--ui-surface);
  border: 1px solid var(--ui-border);
  border-radius: var(--radius-card);
  padding: var(--space-6);
  overflow-x: auto;
}
.prose pre code { background: none; padding: 0; }

/* 5. Layout primitives --------------------------------------------------- */
.container {
  width: 100%;
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--container-pad);
}

.container--narrow { max-width: 60rem; }
.container--prose  { max-width: calc(var(--prose-max) + var(--container-pad) * 2); }

.section {
  padding-block: var(--section-y);
}

.section--tight { padding-block: var(--space-12); }

.section--alt {
  background-color: var(--bg-section);
}

.section--dark {
  background-color: var(--green-dark);
  color: var(--on-dark);
}

.section--dark h1,
.section--dark h2,
.section--dark h3 { color: var(--white); }

.section--dark p { color: var(--on-dark-muted); }

.stack > * + * { margin-top: var(--stack-gap, var(--space-4)); }
.stack--sm { --stack-gap: var(--space-2); }
.stack--lg { --stack-gap: var(--space-6); }
.stack--xl { --stack-gap: var(--space-10); }
.stack--2xl { --stack-gap: var(--space-16); }

.cluster {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--cluster-gap, var(--space-4));
}

.cluster--center { justify-content: center; }
.cluster--between { justify-content: space-between; }

.grid {
  display: grid;
  gap: var(--card-gap);
  grid-template-columns: repeat(
    auto-fit,
    minmax(min(var(--grid-min, 18rem), 100%), 1fr)
  );
}

/* These three set a track FLOOR and let auto-fit decide how many fit, which is
   the right model for a collection whose length is not known in advance. Note
   that the names describe the floor's intent, not a guaranteed column count:
   .grid--2's 22rem floor fits three across a 1200px container. */
.grid--2 { --grid-min: 22rem; }
.grid--3 { --grid-min: 20rem; }
.grid--4 { --grid-min: 14rem; }

/* Exactly four items, where four is the point rather than a coincidence.
   Auto-fit cannot express that: with four items and a 22rem floor it fits three
   across and leaves the fourth alone on a row of its own. That was every "The
   challenges" and "Business outcomes" band on the six solution pages plus
   Partnership types — thirteen grids, all reading 3 + 1.

   So the count is declared rather than inferred: four across, two by two below
   1088px, stacked on phones. No width produces an orphan.

   THE BREAKPOINT IS 68rem, not the 62rem used elsewhere in this file, because at
   992px four columns leave each card 204px wide against 228px at 1088px, and
   these hold a heading plus three lines. Two by two at 456px reads better than
   four cramped columns.

   ONLY FOR COLLECTIONS THAT ARE FOUR. On five items this reads 4 + 1, which is
   worse than the 3 + 2 auto-fit gives — which is why the call sites test the
   length instead of applying it unconditionally. */
.grid--quad {
  grid-template-columns: minmax(0, 1fr);
}

@media (min-width: 34rem) {
  .grid--quad { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 68rem) {
  .grid--quad { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* Centred heading block above a section's content */
.section-head {
  max-width: 46rem;
  margin-inline: auto;
  text-align: center;
  margin-bottom: var(--space-16);
}

.section-head > * + * { margin-top: var(--space-4); }

/* A qualifying line BELOW a band, not above it: conditions attached to what the
   cards just claimed. Constrained and secondary, but not hidden — it carries
   commercial caveats (licensing, availability, regulatory confirmation), so it
   keeps --text-secondary rather than dropping to a muted grey. */
.section-note {
  max-width: 46rem;
  margin-top: clamp(var(--space-6), 2vw, var(--space-8));
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--text-secondary);
}
/* label + h2 spacing now comes from the global eyebrow rule above. */

/* 6. Components ---------------------------------------------------------- */

/* -- Button ------------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 48px;
  padding-inline: var(--space-6);
  border: 1px solid transparent;
  border-radius: var(--radius-btn);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  line-height: 1.2;
  white-space: nowrap;
  text-align: center;
  cursor: pointer;
  transition:
    background-color var(--dur-btn) var(--ease-out),
    border-color var(--dur-btn) var(--ease-out),
    color var(--dur-btn) var(--ease-out);
}

.btn:hover { text-decoration: none; }

.btn:disabled,
.btn[aria-disabled='true'] {
  background-color: var(--border);
  border-color: var(--border);
  color: var(--text-disabled);
  pointer-events: none;
}

.btn svg { width: 1.125em; height: 1.125em; }

/* Primary: brand yellow, no border, near-black green label.

   THE LABEL
   green-900 at 9.33:1 on the fill and 10.85:1 on the hover fill, up from
   green-dark's 5.45:1. It reads as near-black but stays in the brand green, so
   the button keeps the green-on-yellow pairing rather than going monochrome.

   gold-800 was tried first, to match .btn--gold below. Measured, it is 5.30:1 —
   marginally WORSE than the green-dark it would have replaced, so matching that
   button would have meant changing the colour without improving anything.
   White is not an option on this fill anywhere: 1.76:1.

   NO BORDER — AND WHAT THAT COSTS, ON RECORD
   Removed on request. It was not decoration: the gold-400 fill is 1.72:1 against
   --bg, 1.64:1 against --bg-section, 1.76:1 on white cards and 1.40:1 on the
   hero's --neutral-200. WCAG 1.4.11 asks 3:1 of the boundary that identifies a
   control, so the button's edge is now below that on every light surface it sits
   on, and the shape reads as a soft yellow field rather than a defined control.

   There is no yellow that fixes this. Measured across the ramp, gold-700 is the
   first step to clear 3:1 unaided (4.49:1 worst case) and it is an olive-bronze,
   not the brand yellow the buttons were changed to. The mitigations that keep the
   colour are a border only on pale surfaces, or a dark outer ring — both are the
   thing that was just removed. Restore by putting back
   `border-color: var(--gold-700)`.

   The 1px transparent border from .btn is deliberately left in place so the box
   metrics still match .btn--secondary and .btn--outline-light, which do have
   visible borders. Dropping the border box would shrink this button by 2px. */
.btn--primary {
  background-color: var(--gold-light);
  color: var(--green-900);
}

/* Lightens rather than darkens, which is what the green version it replaces did,
   so hover direction across the site stays consistent. The label holds at
   10.85:1 through the transition, so it needs no swap on hover. */
.btn--primary:hover {
  background-color: var(--gold-300);
  color: var(--green-900);
}

/* Secondary: white, green border, green text; light green on hover */
.btn--secondary {
  background-color: var(--white);
  border-color: var(--green-dark);
  color: var(--green-dark);
}

.btn--secondary:hover {
  background-color: var(--green-tint);
  color: var(--green-dark);
}

/* On the dark green CTA band / footer */
.btn--on-dark {
  background-color: var(--white);
  color: var(--green-dark);
}

.btn--on-dark:hover {
  background-color: var(--green-tint);
  color: var(--green-dark);
}

.btn--outline-light {
  background-color: transparent;
  border-color: rgb(255 255 255 / var(--opacity-50));
  color: var(--white);
}

.btn--outline-light:hover {
  background-color: rgb(255 255 255 / var(--opacity-10));
  border-color: var(--white);
  color: var(--white);
}

/* Small gold utility button — newsletter "GO". Not a primary CTA. */
.btn--gold {
  background-color: var(--gold-light);
  color: var(--gold-800);
  min-height: 44px;
  padding-inline: var(--space-5);
}

/* The one place gold-600 survives, and it has to: --gold-light is gold-400, so
   this button already RESTS on gold-400. Routing the hover through --gold-dark
   (now also gold-400) would make hover identical to rest and the button would
   stop responding to the pointer. Pinned to gold-600 so it still darkens. */
.btn--gold:hover {
  background-color: var(--gold-600);
  color: var(--white);
}

.btn--sm { min-height: 40px; font-size: var(--text-xs); padding-inline: var(--space-4); }
.btn--block { width: 100%; }

/* Long labels on narrow screens.
   Measured: "Talk to Our Facilities Management Team" on the facilities CTA is
   417px wide once uppercased and letter-spaced. In a 390px viewport `nowrap`
   made the button wider than the page, so the whole document scrolled sideways
   by 14px and a horizontal scrollbar appeared. Wrapping the label instead just
   makes the button taller, which nothing here measures against.

   Kept behind a max-width query rather than dropping `nowrap` from .btn: on
   wide layouts nowrap is doing real work, keeping labels off two lines inside
   narrow cards. 30rem is above the ~457px this label needs, so every industry
   CTA clears it. max-width is the backstop for a label with no break
   opportunity at all, which would otherwise still overflow.

   This must stay AFTER the .btn base rule — a media query adds no specificity,
   so at equal specificity source order is what decides. */
@media (max-width: 30rem) {
  .btn {
    white-space: normal;
    max-width: 100%;
  }
}

/* -- Card --------------------------------------------------------------- */
.card {
  display: flex;
  flex-direction: column;
  background-color: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  padding: var(--space-6); /* 24px — reduced from the brief's 32px on
                              client feedback that cards read too heavy */
  box-shadow: var(--shadow-xs);
  transition:
    transform var(--dur-card) var(--ease-out),
    box-shadow var(--dur-card) var(--ease-out),
    border-color var(--dur-card) var(--ease-out);
}

/* Owned margins rather than flex `gap`. The global `.label + h*` eyebrow rule
   adds a margin, and in a flex container gap and margin ADD rather than
   collapse — so an eyebrow sat 28px from its own title. With margins the
   eyebrow rule composes correctly instead of stacking. */
.card > * + * { margin-top: var(--space-3); }

/* The eyebrow belongs to the title it introduces. */
.card .label + .card__title { margin-top: var(--space-1); }

/* Compact: same anatomy, less bulk. For secondary and related card lists that
   should support the page rather than compete with it — the 32px default is
   for primary feature cards. */
.card--compact {
  padding: var(--space-4); /* 16px */
  border-radius: var(--radius-btn);
}

.card--compact > * + * { margin-top: var(--space-2); }

/* A compact card that swaps its two halves: mark and name at rest, description on
   hover, one replacing the other. The name going away costs nothing — the visitor
   read it to decide to hover — but the description has to be reachable without a
   pointer, and that is the part with rules.

   HIDING TEXT ON HOVER IS ONLY SAFE UNDER THREE CONDITIONS, all met here and all
   the same ones the industry film strip works to:
     1. Nothing lives only in the hover. This body is `menuCopy`, which the
        Capabilities mega menu also shows, with the full copy on /capabilities.
     2. Keyboard reaches it — :focus-visible is bound alongside :hover, and the
        card is an <a>, so tabbing reveals it.
     3. Touch gets it outright. `(hover: none)` has no hover state at all, so the
        body is simply always shown there rather than being unreachable.

   A CROSS-FADE IN ONE GRID CELL, not a collapse-and-grow. The two layers are
   stacked in the same cell, so the card is as tall as the taller of them and its
   height never changes — swapping content by animating height would move every
   card in the row on every hover, and in a four-card grid the pointer is nearly
   always next to one of them.

   Height was the first approach and it is a dead end twice over. grid-template-rows
   0fr->1fr collapsed to nothing, because the row is sized from the container's
   height and the container's height from the row: with no definite height to
   resolve against, `1fr` computes to 0 (measured 0px even when forced inline).
   max-height animates, but only to a guessed cap. Stacking needs neither. */
.card--reveal {
  display: grid;
  align-content: start;
}

.card--reveal > * {
  /* Same cell for both layers. */
  grid-area: 1 / 1;
  transition: opacity var(--dur-card) var(--ease-out);
}

/* .card--compact's row gap would push the second layer below the first. */
.card--reveal > * + * { margin-top: 0; }

.card--reveal .card__face {
  display: grid;
  gap: var(--space-2);
  justify-items: start;
  align-content: start;
}

.card--reveal .card__body { opacity: 0; }

.card--reveal:hover .card__face,
.card--reveal:focus-visible .card__face { opacity: 0; }

.card--reveal:hover .card__body,
.card--reveal:focus-visible .card__body { opacity: 1; }

/* One line, so "Stacklane Technologies" — the longest of the four names — does not
   wrap in the narrowest card the grid can make. The extra .card__face in the
   selector is not decoration: .card--compact .card__title sets the size further
   down the file at equal specificity, and would otherwise win on order. */
.card--reveal .card__face .card__title { font-size: var(--text-sm); }

/* No hover to gate on, so nothing can be hidden behind one. Both layers show,
   un-stacked back into their own rows — overlapping them here would print the
   description on top of the name. */
@media (hover: none) {
  .card--reveal > * { grid-area: auto; }
  .card--reveal > * + * { margin-top: var(--space-2); }
  .card--reveal .card__body { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .card--reveal > * { transition: none; }
}

.card--compact .card__title { font-size: var(--text-body); }
.card--compact .card__body { font-size: var(--text-sm); }

/* Cards lift 4px on hover */
.card--interactive:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-md);
  border-color: var(--divider);
}

.card__title {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: var(--text-xl);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-tight);
  color: var(--text);
}

.card__body {
  color: var(--text-secondary);
  font-size: var(--text-sm);
  line-height: 1.7;
}

/* -- Dark card ---------------------------------------------------------- */
/* The dark card variant was here — .card--dark, a --green-dark fill with white
   title, green-200 body and a gold glyph. Retired: cards are one light treatment
   across the site now. The full-bleed dark bands are unaffected, because they are
   their own components rather than cards — .stats-band, .cta, .site-footer and the
   showcase panels are all still dark.

   Recoverable from git if a dark card is ever wanted again. */

/* Brand teal glyph on a green-50 chip — the card icon everywhere on the site.

   Was a gold glyph on a gold-tinted chip, which was also the weakest pairing in
   the system: gold-400 on gold-50 measured 1.62:1, under the 3:1 WCAG 1.4.11 asks
   of a meaningful non-text element. green-500 on green-50 measures 3.77:1.

   green-50 rather than the cool-200 this briefly used: keeping the chip in the
   same hue family as the glyph reads as one mark rather than a teal icon sitting
   in a grey box, and it is the stronger pairing of the two — 3.77:1 against
   3.44:1. Same value as --green-tint, referenced by step here to match the glyph
   beside it. */
.icon-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 3rem;
  height: 3rem;
  border-radius: var(--radius-btn);
  background-color: var(--green-50);
  color: var(--green-500);
}

.icon-chip svg { width: 1.5rem; height: 1.5rem; }

/* Kept as an alias — it named the green variant of a two-colour system that now
   has one colour, and the design-system page still references it. */
.icon-chip--green {
  background-color: var(--green-50);
  color: var(--green-500);
}

/* -- Checklist --------------------------------------------------------- */
/* The global reset sets svg { display: block }, so each row MUST be flex.
   Without it the icon claims a line of its own and the label wraps beneath
   it — which is exactly what happened when these rules went missing. */
.checklist {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  list-style: none;
  padding: 0;
  /* Deliberately no `margin: 0` — it has the same specificity as
     `.stack > * + *` but comes later, so it would cancel the stack gap and
     collide the list with the heading above it. */
}

.checklist li {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--text-secondary);
}

.checklist svg {
  flex-shrink: 0;
  width: 1.125rem;
  height: 1.125rem;
  margin-top: 0.2rem;
  color: var(--green-light);
}

/* -- Numbered steps ---------------------------------------------------- */
.step {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.step__num {
  /* align-self keeps it a circle; a stretched flex item would go oval. */
  align-self: flex-start;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border-radius: var(--radius-full);
  background-color: var(--green-dark);
  color: var(--white);
  font-size: var(--text-sm);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* -- Media frame -------------------------------------------------------- */
/* Wraps photography and UI mockups. Until real assets land it renders a
   labelled placeholder describing the intended shot. */
.media {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-card);
  background-color: var(--bg-section);
  border: 1px solid var(--border);
  aspect-ratio: var(--media-ratio, 4 / 3);
}

.media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--dur-card) var(--ease-out);
}

/* Cards zoom their image to 103% on hover */
.card--interactive:hover .media img,
.media-hover:hover .media img { transform: scale(1.03); }

/* An image slot with no image yet. Designed to read as an intentional brand
   panel rather than as a missing asset, because these are on live pages: the
   mark on the brand gradient, with a hairline grid behind it for texture.

   It deliberately carries NO text. The art-direction note that used to sit here
   is instruction for whoever supplies the picture, and publishing it told
   visitors about our production process instead of showing them anything. It is
   dev-only now — see Media.astro. */
.media__panel {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: var(--space-4);
  padding: var(--space-6);
  text-align: center;
  background-color: var(--bg-section);
  background-image:
    linear-gradient(135deg, var(--green-tint) 0%, transparent 55%),
    linear-gradient(to right, var(--border) 1px, transparent 1px),
    linear-gradient(to bottom, var(--border) 1px, transparent 1px);
  /* The gradient is unsized so it covers; the two hairlines tile as a grid. */
  background-size:
    cover,
    2.5rem 2.5rem,
    2.5rem 2.5rem;
}

/* Sized as a share of the frame, not in em: em resolves against the inherited
   16px everywhere, so one fixed size would be right in a 584px slot and far too
   large in a 240px portrait. A percentage tracks the frame it sits in, and the
   rem cap stops it ballooning in a full-bleed one.

   The mark keeps its own brand colours, so it is held back with opacity rather
   than recoloured — at full strength it competes with the real content beside
   it, and this panel is a holding state. */
.media__mark {
  width: min(24%, 8.5rem);
  height: auto;
  aspect-ratio: 1;
  opacity: 0.55;
}

/* Dev only — Media.astro emits this in `astro dev` and never in a build. */
.media__note {
  font-style: normal;
  font-size: var(--text-sm);
  line-height: 1.45;
  color: var(--text-secondary);
  max-width: 26ch;
}

/* -- Hero -------------------------------------------------------------- */
/* Full-bleed background video with a scrim. The scrim opacity is not cosmetic:
   the video's brightness is unknown, so it is set so that white text clears
   4.5:1 even over a pure-white frame (measured worst case — white reaches
   10:1 at the copy, gold-400 6.5:1). */
.hero {
  position: relative;
  isolation: isolate;
  display: flex;
  align-items: center;
  min-height: var(--hero-min-h);
  /* Trimmed at the top end with --section-y, but not as far: the hero is the page's
     opening statement, it carries a min-height anyway, and its padding is space around
     the headline rather than a seam between two bands. */
  padding-block: clamp(var(--space-16), 6vw, var(--space-20));
  /* Painted before the video decodes, so there is never a white flash. */
  background-color: var(--green-900);
  color: var(--on-dark);
  overflow: hidden;
}

/* The hero video's entrance blur, as a registered custom property.

   WHY A VARIABLE AND NOT `filter` DIRECTLY
   `filter` is one property holding a list, so animating it REPLACES the list —
   and this video already carries `brightness(1.28) saturate(1.06)` above the
   breakpoint and `saturate(1.06)` in the light theme. A keyframe animating
   `filter` would silently drop whichever of those applied, so the footage would
   land at the wrong brightness. Registering the blur on its own means each theme
   keeps its own filter list and exactly one value animates.

   @property is what makes it interpolate: an UNREGISTERED custom property
   animates discretely, flipping at the midpoint, which would pop rather than
   resolve. Where @property is missing (Firefox before 128) that pop is the
   fallback — the opacity half of the animation still runs smoothly, so it reads
   as a fade with one blur step rather than as a broken effect. */
@property --hero-video-blur {
  syntax: '<length>';
  inherits: false;
  initial-value: 0px;
}

.hero__video {
  position: absolute;
  inset: 0;
  z-index: -2;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* ANCHORED RIGHT AT EVERY WIDTH, which is the whole point of it.
     This lived in the 62rem media query and so did nothing on a phone, where
     `object-position` fell back to 50% 50% — the middle of the frame, which on
     this panorama is out-of-focus glass. The subject was cropped out at exactly
     the width where the crop is harshest.

     The frame is 2545x771 (aspect 3.30) against a hero from about 2.08 down to
     0.54, so `cover` scales to height and throws away width: ~840px at 1440 and
     ~1990px at 390. Anchoring right spends all of it on the left, which is sky and
     glass, so the man on the right edge survives every width. */
  object-position: 100% 50%;
  /* Declared here so the blur composes below the breakpoint too, where no other
     filter applies. */
  filter: blur(var(--hero-video-blur, 0px));
}

/* -- Hero: video as a spotlight on the right --------------------------- */
/* The subject of the footage (the sphere) sits at about 49% of the frame, which
   is directly behind the headline. Two things move it out from under the text.

   1. The frame is shifted right. object-position cannot do this: the hero box is
      wider in aspect than the video (2.06 vs 1.78), so cover crops VERTICALLY and
      there is no horizontal overflow to pan. Widening the element and offsetting
      it is the only way to move the frame sideways.
   2. The video is masked rather than covered. A second translucent overlay can
      only ever darken what is beneath it, so the spotlight is cut into the video
      itself — it fades to nothing and the hero's own green-900 shows through.
      A mask ramp has no edge, so there is no seam to see anywhere. */
@media (min-width: 62rem) {
  .hero__video {
    /* NO HORIZONTAL PUSH. The old video footage had its subject at 49% of frame and
       was shifted right (114% wide, offset 14%) to land it near 70% of the hero.
       The photographs that replaced it carry their subject on the right edge
       already, so the shift is not only unnecessary but pushed him off the canvas.
       The frame fills the hero 1:1 and `object-position` below does the work. */
    inset: 0;
    width: 100%;
    /* The reset near the top of this file gives img/video/canvas
       `max-width: 100%`, which silently clamped the width above back to 100% —
       the declaration was in the cascade and parsed, it was the USED value that
       was capped, so the frame never moved at all. */
    max-width: none;
    /* object-position now lives on the base rule so it applies below this
       breakpoint too — see the note there. */

    /* The spotlight. Fully opaque across the core so the footage is untouched
       there, then a long ramp to nothing — the length of that ramp is what makes
       it seamless. Beyond it the video is masked away entirely and the hero's own
       green-900 shows through, so there is no edge anywhere to catch the eye. */
    --hero-spot: radial-gradient(
      ellipse 48% 96% at 76% 50%,
      #000 0%,
      #000 40%,
      rgb(0 0 0 / 0.74) 60%,
      rgb(0 0 0 / 0.3) 80%,
      transparent 97%
    );
    -webkit-mask-image: var(--hero-spot);
    mask-image: var(--hero-spot);

    /* The footage is a dark teal sphere on a dark set, so at native brightness
       the spotlight had nothing much to show. Lifting it is safe here in a way it
       would not normally be: the mask has already removed the video from behind
       the copy, so brightening cannot cost the text any contrast. */
    /* brightness(1.28) removed. It existed because the old footage was a dark teal
       sphere on a dark set with nothing for the spotlight to show; this photograph
       is a bright sky at golden hour. It was in any case already inert in the light
       theme, whose own filter rule overrides this one on specificity. */
    filter: blur(var(--hero-video-blur, 0px)) saturate(1.06);
  }

  /* The scrim is still carrying text contrast, and it has to.
     It is tempting to assume the mask has already removed the video from behind
     the copy — it has not. The copy block reaches 57% of the hero and the mask is
     0.94 opaque there, because a ramp long enough to look seamless cannot also
     finish inside the 7% of width between the text and the spotlight. Measured
     against a blown-out white frame, a light scrim left the headline's right end
     at 1.34:1.
     So: heavy across the copy, then falling away fast to hand the spotlight over.
     The steepness is hidden by the mask ramping up over the same span — the two
     cross in opposite directions and neither edge is visible.
     Worst case with these stops: headline 4.84:1 at its right end against 3:1,
     body copy 12.61:1, eyebrow 9.33:1. The core is 16% veiled and 72% out is
     untouched. */
  .hero__scrim {
    background-color: transparent;
    background-image: linear-gradient(
      to right,
      color-mix(in srgb, var(--green-900) 88%, transparent) 0%,
      color-mix(in srgb, var(--green-900) 86%, transparent) 46%,
      color-mix(in srgb, var(--green-900) 58%, transparent) 58%,
      color-mix(in srgb, var(--green-900) 16%, transparent) 64%,
      transparent 70%
    );
  }
}

.hero__scrim {
  position: absolute;
  inset: 0;
  z-index: -1;
  /* Stacked: copy spans the full width on small screens, so the scrim is
     uniform there and only becomes directional once there is room. */
  background-color: color-mix(in srgb, var(--green-900) 88%, transparent);
}

/* The directional scrim that used to live here — a flat 92% -> 40% ramp across
   the whole hero — is now in the spotlight block above .hero__scrim, because it
   has to be far lighter once the video is masked instead of covered. Two rules
   for the same thing at the same breakpoint would have made source order decide
   the outcome, which is how the first attempt at this silently did nothing. */

/* Kept explicit even though --gold-text now resolves to gold-400 too, so this
   currently changes nothing. It mattered when the base was gold-700 (2.30:1 on
   the dark scrim, unreadable) and it would matter again the moment --gold-text
   goes back: gold-400 is 7.41:1 on the scrim and 9.33:1 on pure green-900. */
.hero[data-hero-theme='dark'] .label { color: var(--gold-light); }

/* -- Hero: capability badges -------------------------------------------- */
/* The four business units' marks in frosted circles, ringing the laptop.

   These replaced a retention figure and an office pill. Circles rather than cards
   because the content is a mark and not a sentence, and because four rectangles
   around a laptop reads as a dashboard while four discs reads as a constellation.

   OFF BY DEFAULT, shown only in the media query at the end. At 1200 the copy's ink
   reaches 59.5% of the hero and at 992 the subject himself is at 42% — below 75rem
   there is no free space around the laptop and a badge would land on the headline. */
.hero__widgets {
  display: none;
}

.hero-badge {
  position: absolute;
  z-index: 1;
  display: grid;
  place-items: center;
  width: clamp(3rem, 2rem + 2.2vw, 4rem);
  aspect-ratio: 1;
  border-radius: var(--radius-full);
  /* 78% white. The marks carry their own fixed fills — a dark gold and a dark
     green among them — so the disc has to be light enough for those to read
     wherever it lands, and one of these sits over his dark shirt. */
  background-color: color-mix(in srgb, var(--white) 78%, transparent);
  -webkit-backdrop-filter: blur(16px) saturate(1.15);
  backdrop-filter: blur(16px) saturate(1.15);
  /* A white hairline, not --border: a cool-grey line vanishes against bright sky. */
  border: 1px solid color-mix(in srgb, var(--white) 82%, transparent);
  box-shadow: var(--shadow-lg);
  /* .hero__widgets turns pointer events off for its whole box, which is wider than
     the hero and lies over the copy. The discs are the only part meant to be
     clickable, so they turn them back on. */
  pointer-events: auto;
  transition:
    transform var(--dur-card) var(--ease-out),
    background-color var(--dur-card) var(--ease-out);
}

.hero-badge:hover {
  transform: translateY(-3px);
  background-color: color-mix(in srgb, var(--white) 92%, transparent);
}

.hero-badge .capability-icon {
  width: 52%;
  height: 52%;
}

/* The unit's name, under the disc, on hover.

   Absolutely positioned rather than in flow, so revealing it cannot move the disc
   it belongs to — the discs are placed against features of the photograph, and a
   disc that shifts when the pointer arrives would slide off the laptop.

   translateX(-50%) centres a pill wider than the 64px disc on the disc's own
   centre. nowrap because "Stacklane Technologies" broken over two lines under a
   small circle reads as a caption for the photograph rather than a label. */
.hero-badge__label {
  position: absolute;
  top: calc(100% + 0.5rem);
  left: 50%;
  white-space: nowrap;
  padding: 0.25rem 0.625rem;
  border-radius: var(--radius-full);
  font-size: var(--text-xs);
  font-weight: 600;
  line-height: 1.4;
  color: var(--text);
  /* Same frosted treatment as the disc, a touch more opaque: this is type at
     13px rather than a mark at 33px, so it needs the backdrop quieter. */
  background-color: color-mix(in srgb, var(--white) 88%, transparent);
  -webkit-backdrop-filter: blur(16px) saturate(1.15);
  backdrop-filter: blur(16px) saturate(1.15);
  border: 1px solid color-mix(in srgb, var(--white) 82%, transparent);
  box-shadow: var(--shadow-md);
  opacity: 0;
  transform: translateX(-50%) translateY(-4px);
  /* Never a hit target of its own — it sits outside the disc, and a pill that
     could take the pointer would make the badge's clickable area an odd L. */
  pointer-events: none;
  transition:
    opacity var(--dur-card) var(--ease-out),
    transform var(--dur-card) var(--ease-out);
}

.hero-badge:hover .hero-badge__label,
.hero-badge:focus-visible .hero-badge__label {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* NO `(hover: none)` branch that shows all four outright, unlike .card--reveal.
   Four pills of ~150px around a laptop overlap each other — consulting's would
   run 70-80.4% of the hero's width against services' 75-85.4% — so showing them
   all would be less legible than showing none. Nothing lives only here: the same
   four units are in the Capabilities mega menu with their full names, and each
   disc links to its section on /capabilities. */

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

/* Technologies is the leftmost disc, so its label is the only one that reaches the
   copy — and only where the hero is narrowest. Measured at 1200px with the label
   centred: its left edge lands 31px INSIDE the last line of "Stacklane Group
   partners with organisations to modernise", the longest line of the paragraph.
   At 1280px the same label clears by 39px and by 1440px by far more, because the
   copy column stops growing while the photograph keeps sliding right.

   So the disc stays where it was asked to be and the LABEL moves instead: below
   86rem it opens rightward from the disc's centre rather than being centred on it,
   which buys back the 86px that used to hang off the left. Only this badge, only
   these widths — the other three have 277px or more of headroom and stay centred. */
@media (max-width: 85.99rem) {
  .hero-badge--technologies .hero-badge__label {
    transform: translateX(0) translateY(-4px);
  }

  .hero-badge--technologies:hover .hero-badge__label,
  .hero-badge--technologies:focus-visible .hero-badge__label {
    transform: translateX(0) translateY(0);
  }
}

@media (min-width: 75rem) {
  /* THE RING'S CONTAINER IS THE PHOTOGRAPH'S BOX, NOT THE HERO'S.

     This is the whole reason the discs stay on the laptop. The photograph is
     `object-fit: cover` at `object-position: 100% 50%`, so as the viewport widens
     the crop changes: at 1200px the image spans -998 to 1200 of the hero, and at
     1920px it spans -361 to 1920. A disc placed at a percentage of the HERO
     therefore lands on a different part of the photograph at every width —
     measured, technologies at `left: 61%` sat over 78.7% of the image at 1200px
     and 67.2% at 1920px, which is the laptop at one end and thin air at the other.

     Sizing this box to the photograph's own aspect ratio, anchored to the same edge
     the image is anchored to, makes it congruent with the rendered image. A
     percentage inside it is then a percentage OF THE PHOTOGRAPH, and the ring
     tracks the laptop for free.

     --hero-img-ratio comes from index.astro, which reads it out of the file. It is
     deliberately not written here: the hero photograph has already been swapped
     twice, and a stale ratio would slide the discs off the laptop with nothing in
     the build to say so.

     pointer-events: none because this box is much wider than the hero and overlaps
     the copy — at 1200px it covers the whole hero, including both CTA buttons. The
     discs turn it back on for themselves. */
  .hero__widgets {
    display: block;
    position: absolute;
    top: 0;
    right: 0;
    height: 100%;
    aspect-ratio: var(--hero-img-ratio);
    pointer-events: none;
  }

  /* Ringing the laptop, as percentages of the photograph. All four sit clear of his
     face, which occupies roughly the top third, and clear of the copy, which the
     photograph's left portion is cropped away from at these widths.

     Vertically these are also hero percentages, and exactly so: `cover` is
     height-driven here, so the image fills the hero's height with nothing cropped
     top or bottom. Only the horizontal needed the container above.

     Each disc spans about 2.8 points of width and 9.3 of height, so the four are
     disjoint horizontally: technologies 73.4-76.2, services 77.2-80.0, consulting
     81.0-83.8, solutions 84.1-86.9.

     All four came 2 points left of where they first landed — about 46px of the
     photograph — which is why none of these figures is round. They are a measured
     position converted into this coordinate system, not a chosen one. */
  .hero-badge--consulting {
    /* The one position not touched by the switch-and-lift. Its figures changed only
       because the coordinate system did — at 1440 it renders exactly where it did
       before, and now holds there as the window resizes. */
    top: 44%;
    left: 80.96%;
  }

  .hero-badge--technologies {
    /* Up 4 points from 58%. */
    top: 54%;
    left: 73.38%;
  }

  .hero-badge--solutions {
    /* Swapped with services, then up 4 from 70%. Rightmost of the four. */
    top: 66%;
    left: 84.11%;
  }

  .hero-badge--services {
    /* Swapped with solutions, then up 4 from 80%. Lowest of the four; its label
       hangs about 14 points below this, landing at ~90% — inside the hero. */
    top: 76%;
    left: 77.17%;
  }

  /* In after the copy has landed — the buttons are last of the staggered blocks at
     390ms — then 90ms apart so the ring assembles rather than appearing at once.
     `backwards` for the same reason the copy uses it: no filter left applied. */
  :root[data-hero-anim]:not([data-hero-in]) .hero-badge {
    opacity: 0;
    filter: blur(10px);
  }

  :root[data-hero-in] .hero-badge {
    animation: hero-in 900ms cubic-bezier(0.25, 0.1, 0.25, 1) backwards;
  }

  :root[data-hero-in] .hero-badge--consulting   { animation-delay: 520ms; }
  :root[data-hero-in] .hero-badge--technologies { animation-delay: 610ms; }
  :root[data-hero-in] .hero-badge--solutions    { animation-delay: 700ms; }
  :root[data-hero-in] .hero-badge--services     { animation-delay: 790ms; }
}

@media (prefers-reduced-motion: reduce) {
  :root[data-hero-anim] .hero-badge {
    opacity: 1;
    filter: none;
    animation: none;
    transition: none;
  }
}

/* Past about 2281px the crop flips: the hero grows wider than the photograph's own
   3.3:1, so `cover` starts scaling to WIDTH and crops top and bottom instead of the
   sides. The container has to flip with it — full width, height derived from the
   ratio, and centred vertically to match `object-position`'s 50%.

   The breakpoint is the photograph's ratio times the hero's settled height
   (3.301 x 691 = 2281px), so it is tied to this photograph. Swap the photograph for
   one of a different shape and this figure needs recomputing; the symptom would be
   the ring drifting on very wide displays only, which is easy to miss. Below it —
   every laptop and most desktops — the rule above is the one that applies. */
@media (min-width: 143rem) {
  .hero__widgets {
    width: 100%;
    height: auto;
    top: 50%;
    transform: translateY(-50%);
  }
}

/* The mega-menu chip KEEPS its green-50 tint behind the brand mark.

   A `background-color: transparent` override was written here first and was dead:
   it has the same specificity as .megamenu__icon, which is declared ~3000 lines
   later, so source order won and the tint stayed. Rendered, the tint is the better
   answer — it gives four differently-shaped marks one consistent container, the way
   .icon-chip does on the cards — so the rule is gone rather than reordered.

   Only the size is set here: the marks have four different viewBoxes, so they need
   an explicit box rather than inheriting the Lucide sizing. */
.megamenu__icon--brand .capability-icon {
  width: 1.375rem;
  height: 1.375rem;
}

/* -- Hero: light variant ----------------------------------------------- */
/* For light-toned footage, with no overlay at all. Text colours are one step
   darker than the site defaults because there is no scrim to lean on:
   neutral-700 for body (the site default neutral-600 is too light here) and
   gold-800 for the label. */
.hero[data-hero-theme='light'] {
  background-color: var(--cool-200);
  color: var(--cool-700);
}

/* This used to carry no overlay at all, on the reasoning that light footage needs
   none. That stopped being safe when the video became a spotlight: the mask is
   0.94 opaque at the copy's right edge, so real footage now sits behind dark
   text. Measured against a black frame, the body copy came out at 1.16:1 and the
   title at 1.28:1 — unreadable.
   So the light theme gets the mirror of the dark one's scrim: the hero's own
   --neutral-200 laid over the copy and clearing by 70% to hand over the
   spotlight. Worst case with these stops: title 3.33:1 at its right end against
   3:1, body 6.06:1, eyebrow 7.41:1. */
/* Below the spotlight breakpoint the copy spans the full width and the video is
   full-bleed and unmasked, so this has to be uniform — a gradient that cleared at
   70% would leave the end of every line sitting on bare footage. */
.hero[data-hero-theme='light'] .hero__scrim {
  /* 86%, down from 90% on request. This flat wash is the binding constraint on how
     thin the overlay can go: it covers the WHOLE hero below 62rem, so the copy has
     no unveiled region to fall back on. At 82% the lead paragraph measured 4.36:1
     against the 4.5 it needs. */
  background-color: color-mix(in srgb, var(--cool-200) 86%, transparent);
  background-image: none;
}

@media (min-width: 62rem) {
  .hero[data-hero-theme='light'] .hero__scrim {
    background-color: transparent;
    background-image: linear-gradient(
      to right,
      /* Stops re-cut for the photograph.
         MEASURED, because the old ones were wrong for it: the copy's widest ink
         reaches 55.5% of the hero at 1600px but 66.3% at 992px — the container
         narrows faster than the type does. The old ramp began falling at 46% and
         was near-transparent by 64%, so at 992px the lead paragraph sat on bare
         photograph and measured 2.19:1 against the 4.5 it needs.
         Held ~88% to 68% instead, which clears the widest ink at every width in
         this range, then out by 76%. The subject occupies 78%-98% of the frame, so
         the ramp finishes just as he begins and he is never veiled.

         PULLED BACK TO 70% ON REQUEST, and 70% is the floor. Five ramp ends were
         measured against the painted pixels, per text LINE rather than per element
         box: at 66% the lead paragraph falls to 3.63:1 at 992px, at 62% to 1.34:1.
         What binds is 992px, where the copy's own ink reaches 66.3% of the hero —
         the container narrows faster than the type does, so the overlay has to
         cover past that or the paragraph sits on bare photograph.

         A 50% overlay was asked for and cannot be reached from here without also
         narrowing .hero__inner from 44rem to about 34rem, which pulls the ink back
         to 43% and makes 52% work at 1200px and up — but not at 992px, where
         right-anchored cropping brings the subject himself to 42% of the hero. That
         is a layout change, so it is not made here. */
      color-mix(in srgb, var(--cool-200) 90%, transparent) 0%,
      color-mix(in srgb, var(--cool-200) 88%, transparent) 54%,
      color-mix(in srgb, var(--cool-200) 78%, transparent) 62%,
      color-mix(in srgb, var(--cool-200) 16%, transparent) 68%,
      transparent 70%
    );
  }
}

/* On a light hero the copy is dark, so brightening the footage works against the
   text rather than for it — and the spotlight no longer needs the lift, because
   dark footage reads perfectly well against a light field. */
.hero[data-hero-theme='light'] .hero__video {
  filter: blur(var(--hero-video-blur, 0px)) saturate(1.06);
}

/* Brand yellow, as asked — the same --gold-400 the eyebrow uses everywhere else,
   so it now matches the dark hero and the rest of the site rather than being the
   one eyebrow in a different colour.
   The cost is real and measured: gold-400 on --neutral-200 is 1.40:1, against the
   4.5:1 a 12px bold label needs. This override previously held --gold-800 (7.41:1)
   for exactly that reason. Nothing lighter than gold-800 clears it — gold-700 is
   4.49:1 and misses — so the yellow cannot be made to pass on this background by
   picking a different step. Keeping the yellow AND legibility needs a dark chip
   behind it, the way the announcement strip carries its gold. */
/* --neutral-200 is a light surface, so this follows the light rule. Gold was
   1.40:1 here, the worst reading anywhere on the site; terracotta is 4.64:1 —
   the tightest of the light surfaces, but past the bar. */
.hero[data-hero-theme='light'] .label { color: var(--eyebrow-text); }
.hero[data-hero-theme='light'] .hero__title { color: var(--text); }
.hero[data-hero-theme='light'] .hero__copy { color: var(--cool-700); }

/* Each theme renders its own CTA pair with the correct Button variant, and the
   inactive pair is display:none — so it is also out of the tab order. */
.hero__cta--light { display: none; }
.hero[data-hero-theme='light'] .hero__cta--dark { display: none; }
.hero[data-hero-theme='light'] .hero__cta--light { display: flex; }

/* -- Hero: variant switcher -------------------------------------------- */
/* Evaluation control so both treatments can be compared against real footage. */
.hero__switcher {
  position: absolute;
  right: var(--container-pad);
  bottom: var(--space-6);
  z-index: 2;
  display: flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1);
  border-radius: var(--radius-full);
  background-color: color-mix(in srgb, var(--neutral-1000) 55%, transparent);
  backdrop-filter: blur(8px);
  border: 1px solid rgb(255 255 255 / var(--opacity-20));
}

.hero[data-hero-theme='light'] .hero__switcher {
  background-color: color-mix(in srgb, var(--cool-0) 70%, transparent);
  border-color: var(--divider);
}

.hero__switcher-label {
  padding-inline: var(--space-2);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: rgb(255 255 255 / 0.65);
}

.hero[data-hero-theme='light'] .hero__switcher-label { color: var(--text-secondary); }

.hero__switcher button {
  min-height: 32px;
  padding-inline: var(--space-4);
  border: 0;
  border-radius: var(--radius-full);
  background: none;
  font-size: var(--text-xs);
  font-weight: 600;
  color: rgb(255 255 255 / 0.75);
  transition:
    background-color var(--dur-btn) var(--ease-out),
    color var(--dur-btn) var(--ease-out);
}

.hero__switcher button:hover { color: var(--white); }

.hero__switcher button[aria-pressed='true'] {
  background-color: var(--white);
  color: var(--green-dark);
}

.hero[data-hero-theme='light'] .hero__switcher button { color: var(--text-secondary); }
.hero[data-hero-theme='light'] .hero__switcher button:hover { color: var(--text); }

.hero[data-hero-theme='light'] .hero__switcher button[aria-pressed='true'] {
  background-color: var(--green-dark);
  color: var(--white);
}

.hero__inner {
  position: relative;
  max-width: 44rem;
}

.hero__title {
  color: var(--white);
  font-size: var(--text-display);
  max-width: 20ch;
}

.hero__copy {
  color: var(--on-dark-muted);
  max-width: 46ch;
}


/* -- Trust marks: inline row ------------------------------------------- */
/* One treatment now. Three others (in the stats panel, a tinted panel, a
   marquee) plus an evaluation switcher and a localStorage guard were removed once
   the inline row was chosen — with them went the `data-trust-view` /
   `data-trust-option` plumbing that kept all four in the DOM at once.
   Nothing here uses a border: the band these replaced was defined entirely by
   `border-block: 1px solid var(--border)`, which is what was being removed. */
.trust-v { padding-block: var(--space-16); }

/* -- The marks themselves (shared) ------------------------------------- */
.trust-marks {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-8) clamp(var(--space-8), 5vw, var(--space-16));
}

.trust-mark {
  display: flex;
  align-items: center;
  /* Desaturated and dimmed by default: these are supporting evidence, not
     features. Full colour on hover for anyone who wants to read them. */
  filter: grayscale(1);
  opacity: 0.65;
  transition:
    opacity var(--dur-btn) var(--ease-out),
    filter var(--dur-btn) var(--ease-out);
}

.trust-mark:hover { opacity: 1; filter: grayscale(0); }

/* Real wordmarks now, and their proportions are nothing like each other — 2.3:1 for
   ZedVue against 5.5:1 for Workvue. A common height with the width capped keeps the
   row optically even rather than mathematically equal: the squarer marks are held by
   the height, the long ones by the width. A height alone would have made Workvue half
   again as wide as anything else.

   A DEFINITE HEIGHT, and object-fit rather than `width: auto; height: auto` with two
   max-* caps. That first version collapsed the Workvue mark to 0x0: its file carries
   only a viewBox, no width or height, so it has an intrinsic RATIO but no intrinsic
   SIZE — and with both dimensions auto there was nothing for the caps to reduce. A
   definite height gives every file something to scale from, whether or not it declares
   its own size, and object-fit means clamping the width letterboxes the mark instead
   of squashing it. */
.trust-mark img {
  height: 1.75rem;
  width: auto;
  max-width: 7.5rem;
  object-fit: contain;
}

/* -- The inline row --------------------------------------------------- */
.trust-inline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-6) clamp(var(--space-8), 4vw, var(--space-12));
}

/* Stacked, and centred with it.
   The row already wrapped on its own somewhere between 860 and 900px, but
   `text-align: start` stayed — so the label's BOX was centred while its two lines
   were still ragged left inside it, which reads as left-aligned. Declaring the
   stack at one breakpoint means the alignment and the wrap can never disagree: a
   free wrap would have needed this query to guess where the flexbox broke.
   56rem also picks up 880-896px, where the label had wrapped but was sitting 3px
   off the container edge. */
@media (max-width: 56rem) {
  .trust-inline {
    flex-direction: column;
    text-align: center;
  }
}

/* Deliberately two lines. "Trusted by organisations across industries" is 42
   characters and 386px on one line, which stretches the row so wide the marks get
   pushed to the far side of the container.

   32ch is measured, not guessed. Sweeping the width on the real page, two lines
   hold anywhere from 228px to 384px and three lines start at 224px and below;
   32ch is 274px, near the middle of that band. `ch` under-counts here because
   tracking-label adds 1.3px per character, which is why a 24ch first attempt gave
   three lines rather than two. text-wrap: balance then evens the pair instead of
   leaving a one-word orphan.

   The max-width has to beat flex: .trust-inline is a flex row, and a flex item's
   base size comes from its content, so without this the label would simply take
   the width its text wants. width: fit-content adds nothing — max-content already
   exceeds the cap, so the box is the cap either way. */
.trust-inline__lead {
  margin: 0;
  max-width: 32ch;
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  text-wrap: balance;
  color: var(--text-secondary);
}

/* Marks sit on one line beside the lead-in, so the icon goes next to the name
   rather than above it. */
.trust-marks--inline {
  gap: var(--space-6) clamp(var(--space-6), 3vw, var(--space-10));
}

.trust-marks--inline .trust-mark {
  flex-direction: row;
  gap: var(--space-2);
}

.trust-marks--inline .trust-mark svg { width: 1.25rem; height: 1.25rem; }

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

/* -- Business unit row -------------------------------------------------- */
.unit {
  display: grid;
  gap: clamp(var(--space-8), 5vw, var(--space-16));
  align-items: center;
  grid-template-columns: 1fr;
  padding-block: var(--space-16);
}

.unit + .unit { border-top: 1px solid var(--border); }

@media (min-width: 62rem) {
  .unit { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }

  /* Alternate: even rows put the media on the left */
  .unit--reverse .unit__media { order: -1; }
}

.unit__body { max-width: 46ch; }

/* -- Home: "What we do" as a 2x2 bento --------------------------------- */
/* Four cells, photograph behind each. Separate from .unit above rather than a
   modifier of it: .unit is a two-column row with the media beside the copy, and
   this is a single stacking context with the media underneath it. Five other
   pages still use .unit, so it is left alone. */
.wwd {
  display: grid;
  gap: var(--card-gap);
  /* minmax(0, …), not a bare 1fr. `1fr` means minmax(auto, 1fr), and the auto
     minimum here is the cell's min-content width — which aspect-ratio derives
     from min-height, giving 22rem x 16/11 = 512px. On a 390px phone that grew
     the track past the viewport and put 142px of horizontal scroll on the page.
     Clamping the minimum to 0 lets the ratio resolve from the width instead. */
  grid-template-columns: minmax(0, 1fr);
}

@media (min-width: 48rem) {
  .wwd { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.wwd__cell {
  position: relative;
  display: block;
  overflow: hidden;
  /* The ratio governs on a wide screen; the min-height takes over on a phone,
     where 16/11 of ~350px would be 240px and the copy would not fit under the
     name.
     max-width is load-bearing, not defensive. With both of the above and no
     width cap, min-height wins the height (352px) and aspect-ratio then grows
     the WIDTH to keep the ratio — 352 x 16/11 = 512px — so each cell burst out
     of its own 350px grid track and put 142px of horizontal scroll on the page.
     Capping the width lets the ratio lose instead, which is the right loser. */
  aspect-ratio: 16 / 11;
  min-height: 22rem;
  max-width: 100%;
  border-radius: var(--radius-card);
  color: var(--white);
  box-shadow: var(--shadow-xs);
  transition:
    transform var(--dur-card) var(--ease-out),
    box-shadow var(--dur-card) var(--ease-out);
}

.wwd__cell:hover { text-decoration: none; }

/* .media carries its own ratio, border and radius. Here the CELL owns the shape
   and the photograph just fills it, so all three are neutralised — same
   arrangement as the industries panels. */
.wwd__cell .media {
  position: absolute;
  inset: 0;
  aspect-ratio: auto;
  height: 100%;
  border: 0;
  border-radius: 0;
  background-color: var(--green-900);
}

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

/* Deep at the base and clear at the top: the copy block always sits on ink dark
   enough to read white against, without flattening the whole photograph. */
.wwd__scrim {
  position: absolute;
  inset: 0;
  background-image: linear-gradient(
    to top,
    rgb(2 36 32 / 0.94) 0%,
    rgb(2 36 32 / 0.78) 32%,
    rgb(2 36 32 / 0.24) 66%,
    rgb(2 36 32 / 0.04) 100%
  );
}

.wwd__body {
  position: absolute;
  inset: auto 0 0 0;
  display: block;
  padding: var(--space-6);
}

/* The eyebrow is white here, not the site's usual gold.
   It sits highest in the copy block, where the scrim is thinnest — measured at
   42% of the cell height the scrim is only 0.62 alpha, and gold on that over a
   pale photograph is 2.70:1 against the 4.5:1 it needs. Reaching 4.5:1 with gold
   would take 0.80 alpha there, obscuring most of the photograph the cell exists
   to show. White measures 4.76:1 on the same background, and the gold accent
   survives on "Learn more" at the base where the scrim is 6.88:1. */
.wwd__body .label {
  color: var(--white);
}

.wwd__name {
  /* Overrides the global h3 size: at --text-2xl the longer names wrapped to
     three lines inside a bento cell. */
  margin: var(--space-2) 0 0;
  font-size: var(--text-xl);
  line-height: var(--leading-snug);
  color: var(--white);
}

.wwd__copy {
  display: block;
  margin-top: var(--space-3);
  max-width: 44ch;
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--green-200);
}

.wwd__more {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-4);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--gold-light);
}

.wwd__more svg {
  width: 1rem;
  height: 1rem;
  transition: transform var(--dur-card) var(--ease-out);
}

@media (hover: hover) and (min-width: 48rem) {
  .wwd__cell:hover,
  .wwd__cell:focus-visible {
    transform: translateY(-4px);
    box-shadow: var(--shadow-md);
  }

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

  .wwd__cell:hover .wwd__more svg { transform: translateX(4px); }
}

@media (prefers-reduced-motion: reduce) {
  .wwd__cell,
  .wwd__more svg,
  .wwd__cell .media img {
    transition: none;
  }

  /* Matching specificity, later in the cascade — the hover rules above are
     (0,2,0) and would otherwise out-rank a bare class here. */
  .wwd__cell:hover,
  .wwd__cell:focus-visible {
    transform: none;
  }

  .wwd__cell:hover .media img,
  .wwd__cell:focus-visible .media img {
    transform: none;
  }
}

/* -- Featured platform: tabbed showcase -------------------------------- */
/* Left = the active solution panel, right = the tab list. Tabs come first in
   the DOM (correct reading and focus order); on desktop the grid places the
   panel back on the left. */
/* One card, not two. The outer container owns the border, radius, and
   clipping; panel and tab list sit flush inside it with a shared divider. */
.showcase {
  display: grid;
  gap: 0;
  grid-template-columns: 1fr;
  align-items: stretch;
  background-color: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-xs);
  overflow: hidden;
}


/* -- Tab list ---------------------------------------------------------- */
.showcase__tabs {
  display: flex;
  flex-direction: column;
  background-color: var(--white);
  /* Stacked (mobile): tabs sit above the panel, so divide below. */
  border-bottom: 1px solid var(--border);
}

@media (min-width: 62rem) {
  /* Side by side: divide on the left instead. */
  .showcase__tabs {
    border-bottom: 0;
    border-left: 1px solid var(--border);
  }
}

/* Fills the height left over after "View all", so the tabs below can share
   it evenly instead of bunching at the top. */
.showcase__tablist {
  display: flex;
  flex-direction: column;
  flex: 1;
}

/* -- Tab pager: prev / current title / next, on stacked layouts -------- */
/* Six full-width buttons is a lot of column to scroll past on a phone, and the
   panel they control sits below them. So below 62rem — wherever the showcase is
   already stacked, rather than at a third breakpoint of its own — the strip
   collapses to one title between two arrows.

   Only the SELECTED tab is shown. The pager's middle is that button, so there is
   no duplicate label to keep in sync and the dwell bar still runs under it. */
.showcase__pager {
  display: flex;
  align-items: stretch;
  border-bottom: 1px solid var(--border);
}

.showcase__pager .showcase__tablist {
  /* min-width: 0 so a long title cannot push the arrows off the row: a flex
     item's floor is its content, and these titles are 700-weight heading type. */
  min-width: 0;
  border-bottom: 0;
}

.showcase__tab[aria-selected='false'] { display: none; }

/* Centred, and without its own trailing arrow — two arrows either side of the
   title plus a third inside it reads as three controls. */
.showcase__pager .showcase__tab[aria-selected='true'] {
  justify-content: center;
  text-align: center;
}

.showcase__pager .showcase__tab > svg { display: none; }

/* No active rail either. It marks which of six rows is selected; with one row
   showing, always the selected one, it marks nothing — and it lands right beside
   the prev arrow, where it reads as a stray line rather than an accent. The tint
   behind the title is left in place to frame it against the two arrows. */
.showcase__pager .showcase__tab[aria-selected='true'] {
  box-shadow: none;
}

.showcase__pager-btn {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  /* 48px: the WCAG 2.5.8 target minimum, and these are the primary way through
     the set on a touch screen. */
  width: 3rem;
  color: var(--green-dark);
  background: none;
  border: 0;
  cursor: pointer;
  transition: background-color var(--dur-btn) var(--ease-out);
}

.showcase__pager-btn:hover { background-color: var(--green-tint); }
.showcase__pager-btn svg { width: 1.25rem; height: 1.25rem; }

/* Above 62rem the pager stops existing as a box: `display: contents` hands the
   tablist straight back to .showcase__tabs, so every desktop rule — the grid
   placement, the dark glass, the hairline border — applies to exactly the tree it
   did before this wrapper was added. */
@media (min-width: 62rem) {
  .showcase__pager {
    display: contents;
  }

  .showcase__pager-btn { display: none; }

  .showcase__tab[aria-selected='false'] { display: flex; }

  .showcase__pager .showcase__tab[aria-selected='true'] {
    justify-content: space-between;
    text-align: left;
    /* Restored, along with the desktop glass variant's own override further
       down — the rail is how the selected row is marked in the six-row list. */
    box-shadow: inset 3px 0 0 var(--gold-light);
  }

  .showcase__pager .showcase__tab > svg { display: block; }
}

.showcase__tab {
  /* The dwell track below is position:absolute. Without this the tab is not a
     containing block, so the track escaped to the initial containing block and
     rendered as a page-wide strip hundreds of pixels below its tab. */
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  width: 100%;
  /* Equal share of the column; padding still sets the floor when stacked. */
  flex: 1;
  padding: var(--space-5) var(--space-6);
  text-align: left;
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: var(--text-body);
  color: var(--text-secondary);
  background: none;
  border: 0;
  cursor: pointer;
  transition:
    background-color var(--dur-btn) var(--ease-out),
    color var(--dur-btn) var(--ease-out),
    box-shadow var(--dur-btn) var(--ease-out);
}

.showcase__tab + .showcase__tab { border-top: 1px solid var(--border); }

/* -- Tab dwell timer ---------------------------------------------------- */
/* A 30s bar along the foot of the selected tab; when it completes the script
   advances to the next panel. Gold, matching the rail on the same tab's left
   edge, so the two accents read as one state rather than two features. */
.showcase {
  --showcase-dwell: 30s;
}

.showcase__progress {
  position: absolute;
  inset: auto 0 0 0;
  height: 3px;
  background-color: var(--border);
  /* Only the selected tab has a timer, so only it shows a track. */
  opacity: 0;
}

.showcase__tab[aria-selected='true'] .showcase__progress { opacity: 1; }

.showcase__progress-fill {
  display: block;
  height: 100%;
  /* green-500, not the gold used elsewhere on this tab. Gold is the site's
     decorative accent and the tab's own active rail already uses it, so a gold
     timer read as more of the same rather than as progress. */
  background-color: var(--green-light);
  transform: scaleX(0);
  transform-origin: left center;
}

/* No CSS animation here on purpose: applied via [aria-selected] it is created
   with a null startTime and never becomes ready, so the bar never moves. The
   script sets transform per frame instead, and --showcase-dwell above stays the
   single place the duration is declared. */

/* Rotation retired after an explicit tab choice. The track STAYS — it keeps the
   tab's shape rather than collapsing it, and reads as a rule under the active
   tab. Only the fill is reset (the script repaints it to zero). */

/* Reduced motion is different: no timer is ever created there, so an empty track
   under the active tab would be an indicator for something that does not exist. */
@media (prefers-reduced-motion: reduce) {
  .showcase__progress { display: none; }
}


.showcase__tab:hover {
  background-color: var(--green-tint);
  color: var(--green-dark);
}

/* Active tab: gold rail, per the brief's "active navigation" accent */
.showcase__tab[aria-selected='true'] {
  background-color: var(--green-tint);
  color: var(--green-dark);
  box-shadow: inset 3px 0 0 var(--gold-light);
}

.showcase__tab svg {
  width: 1.125rem;
  height: 1.125rem;
  opacity: 0;
  color: var(--gold-dark);
  transition:
    opacity var(--dur-btn) var(--ease-out),
    transform var(--dur-card) var(--ease-out);
}

.showcase__tab:hover svg,
.showcase__tab[aria-selected='true'] svg { opacity: 1; }
.showcase__tab:hover svg { transform: translateX(3px); }


/* "View all solutions" — a link, deliberately outside the tablist because it
   navigates rather than switching panels. flex:0 0 auto keeps its natural
   height while the tablist above absorbs the slack. */
.showcase__viewall {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  flex: 0 0 auto;
  padding: var(--space-5) var(--space-6);
  border-top: 1px solid var(--border);
  background-color: var(--bg-section);
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: var(--text-sm);
  color: var(--green-dark);
}

.showcase__viewall:hover {
  background-color: var(--green-tint);
  color: var(--green-dark);
  text-decoration: none;
}

.showcase__viewall svg {
  width: 1.125rem;
  height: 1.125rem;
  color: var(--gold-dark);
  transition: transform var(--dur-card) var(--ease-out);
}

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

/* -- Panels ------------------------------------------------------------ */
/* Flex column so the active panel stretches to whichever side is taller. */
.showcase__panels {
  position: relative;
  display: flex;
  flex-direction: column;
}

.showcase__panel:not([hidden]) { flex: 1; }

.showcase__panel {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  /* Raised from clamp(24rem, 52vw, 30rem) once real photographs went in. The
     copy block is ~410px tall at most widths but ~478px at 992px, right at the
     two-column breakpoint where this column is narrowest and the paragraph
     wraps hardest — against a 480px panel that left 2px, so the photograph was
     entirely behind the text. min-height means the panel GROWS rather than
     clipping, so this was never an overflow bug; it just made the image
     pointless. These numbers keep 60px+ of picture visible above the copy at
     every width. */
  min-height: clamp(26rem, 54vw, 34rem);
  /* No radius of its own — .showcase clips it. */
  overflow: hidden;
  /* Still the panel colour behind a photograph, and the whole surface for any
     solution that has no image yet. */
  background-color: var(--green-dark);
  background-image: linear-gradient(150deg, var(--green-600) 0%, var(--green-700) 58%);
}

.showcase__panel[hidden] { display: none; }

.showcase__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Scrim keeps overlaid copy legible over any photograph or video frame.
   neutral-1000 rather than green-700: a green scrim tinted the footage, which
   read as a colour cast on the video rather than as a darkening layer.

   It also fixed a real weakness. Measured against the worst case — bright
   footage under the thinnest part of the scrim the copy sits on, the 72% stop —
   green-700 gave white text 4.44:1 and the green-200 body copy 2.59:1, i.e. the
   body copy FAILED over a light frame. Black gives 8.21:1 and 4.79:1 there.
   Same alpha stops, so the composition is unchanged. */
.showcase__scrim {
  position: absolute;
  inset: 0;
  background-image: linear-gradient(
    to top,
    color-mix(in srgb, var(--neutral-1000) 94%, transparent) 0%,
    color-mix(in srgb, var(--neutral-1000) 72%, transparent) 42%,
    color-mix(in srgb, var(--neutral-1000) 25%, transparent) 100%
  );
}

.showcase__content {
  position: relative;
  padding: clamp(var(--space-6), 4vw, var(--space-10));
  max-width: 56ch;
  color: var(--on-dark);
}

.showcase__content > * + * { margin-top: var(--space-4); }
.showcase__content h3 { color: var(--white); }
.showcase__content .label { color: var(--gold-light); }
.showcase__content p { color: var(--on-dark-muted); }

/* Bullet list inside a panel */
.showcase__points {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.showcase__points li {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--on-dark);
}

.showcase__points svg {
  width: 1.125rem;
  height: 1.125rem;
  margin-top: 0.2rem;
  color: var(--gold-light);
}

.arrow-link--light { color: var(--white); }
.arrow-link--light:hover { color: var(--gold-light); }
.arrow-link--light svg { color: var(--gold-light); }

/* Corner note naming the intended image; drops out once one is supplied. */
.showcase__note {
  position: absolute;
  top: var(--space-5);
  left: var(--space-5);
  right: var(--space-5);
  font-size: var(--text-xs);
  letter-spacing: 0.04em;
  color: rgb(255 255 255 / var(--opacity-50));
}

/* Desktop: ONE cell, not two columns. The video panel fills the whole card and
   the tab list floats over its right-hand side as frosted glass, rather than the
   two sitting side by side with a divider between them.
   Both children take grid area 1/1; z-index lifts the tabs over the panel, which
   works on a grid item without position because grid items participate in
   z-ordering. */
@media (min-width: 62rem) {
  .showcase { grid-template-columns: minmax(0, 1fr); }

  .showcase__panels {
    grid-column: 1;
    grid-row: 1;
  }

  .showcase__tabs {
    grid-column: 1;
    grid-row: 1;
    justify-self: end;
    width: min(23rem, 40%);
    z-index: 1;

    /* DARK glass, in brand green rather than a neutral black, and thin enough to
       read as glass: a third of the blurred footage comes through.
       0.68 is close to the floor, and the floor is set by the gold arrow, not the
       text. Worst case behind the glass is a white video frame, compositing to
       #536A67 — white labels 5.79:1, gold arrows 3.28:1 against the 3:1 an icon
       needs, the green-200 dwell fill 3.38:1. Solving exactly: white text reaches
       4.5:1 at alpha 0.605, the gold arrow reaches 3:1 only at 0.654, so anything
       thinner than that starts losing the arrows rather than the labels.
       Blur is up to 22px to compensate — the thinner the tint, the more the blur
       has to do to keep it reading as a surface rather than a wash. */
    background-color: color-mix(in srgb, var(--green-900) 68%, transparent);
    -webkit-backdrop-filter: blur(22px) saturate(1.2);
    backdrop-filter: blur(22px) saturate(1.2);
    border-bottom: 0;
    /* A light hairline, so the glass has an edge against the footage without the
       opaque --border line the two-column version used. */
    border-left: 1px solid rgb(255 255 255 / 0.16);
  }

  /* Every label white, selected or not. --green-200 was the unselected tone while
     the glass was at 0.80 (5.09:1); on thinner glass it falls to 3.38:1 and is no
     longer safe for text. Selected still reads without a colour difference —
     there are three other cues doing that work: the white wash behind it, the
     gold inset rail, and the arrow, which is opacity 0 until hover or selection. */
  .showcase__tab {
    color: var(--white);
  }

  /* Opaque --border and --green-tint would sit on the glass as solid blocks and
     kill the effect, so both become translucent white washes. */
  .showcase__tab + .showcase__tab {
    border-top-color: rgb(255 255 255 / 0.14);
  }

  .showcase__tab:hover,
  .showcase__tab[aria-selected='true'] {
    background-color: rgb(255 255 255 / 0.1);
    color: var(--white);
  }

  /* --gold-dark is a mid-tone that disappears on this glass; gold-light is
     4.95:1 there, past the 3:1 an icon needs. */
  .showcase__tab svg {
    color: var(--gold-light);
  }

  /* The dwell timer. --green-light is 2.02:1 on the dark glass — a mid-green on
     dark green — so the bar would have been invisible on the very surface it
     reports progress against. --green-200 is 5.09:1 and still keeps the timer off
     the gold that the active rail already owns. */
  .showcase__progress {
    background-color: rgb(255 255 255 / 0.16);
  }

  .showcase__progress-fill {
    background-color: var(--green-200);
  }

  .showcase__viewall {
    background-color: rgb(255 255 255 / 0.06);
    border-top: 1px solid rgb(255 255 255 / 0.14);
    color: var(--white);
  }

  .showcase__viewall:hover {
    background-color: rgb(255 255 255 / 0.14);
    color: var(--white);
  }

  .showcase__viewall svg { color: var(--gold-light); }

  /* The copy is overlaid on the same panel the glass sits on, so it has to stop
     short of it — without this the paragraph runs underneath the tabs. */
  .showcase__content {
    padding-right: calc(min(23rem, 40%) + var(--space-8));
    max-width: none;
  }
}

/* No backdrop-filter: there is nothing blurring the footage, so the tint alone
   has to carry legibility. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  @media (min-width: 62rem) {
    .showcase__tabs {
      background-color: color-mix(in srgb, var(--green-900) 94%, transparent);
    }
  }
}

/* Someone who has asked for less transparency gets a near-solid panel. */
@media (min-width: 62rem) and (prefers-reduced-transparency: reduce) {
  .showcase__tabs {
    background-color: var(--green-900);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}


/* Fade the panel content in when a tab is activated. */
@media (prefers-reduced-motion: no-preference) {
  .showcase__panel:not([hidden]) .showcase__content {
    animation: panel-in var(--dur-card) var(--ease-out) both;
  }
}

@keyframes panel-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

/* -- Industry card ----------------------------------------------------- */
.industry {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  background-color: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  padding: var(--space-6);
  box-shadow: var(--shadow-xs);
  color: var(--text);
  transition:
    transform var(--dur-card) var(--ease-out),
    box-shadow var(--dur-card) var(--ease-out);
}

.industry:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-md);
  text-decoration: none;
  color: var(--text);
}

.industry__title {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: var(--text-lg);
  color: var(--text);
  margin-top: auto;
}

/* The segment's one-line description, where a brief supplies one. Sits directly
   under the title, so the title loses its `margin-top: auto` push in that case —
   otherwise the pair separates and the body drifts to the card's foot. */

.industry__body {
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--text-secondary);
}

/* Chip and arrow share the card's top row. The row exists even on a card with no
   arrow, so the chip sits at the same height either way. */
.industry__top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
}

/* Title and body as one block, pushed to the card's foot by the auto margin that
   used to sit on the title itself — so the pair travels together instead of the
   title anchoring low and the body hanging off it. */
.industry__text {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: auto;
}

.industry__text .industry__title { margin-top: 0; }

/* The arrow replaces the "Explore" label. Bigger than an inline chevron because
   it is now the card's only affordance, and it nudges on hover so the whole card
   reads as the target rather than the corner. The nudge follows the glyph: an
   up-right arrow sliding along X alone reads as a mismatch. */
.industry__go {
  flex: 0 0 auto;
  width: 1.375rem;
  height: 1.375rem;
  color: var(--green-light);
  transition: transform var(--dur-card) var(--ease-out);
}

.industry:hover .industry__go { transform: translate(3px, -3px); }

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

/* A segment with no page of its own. It keeps the card, and loses everything
   that implies a destination: no lift on hover, no pointer, no arrow. */
.industry--static {
  cursor: default;
}

.industry--static:hover {
  transform: none;
  box-shadow: var(--shadow-xs);
}

/* -- Bento grid (solutions index) --------------------------------------- */
/* Light cards with the image as its own region, not a backdrop with text on it:
   above the copy in the standard cells, beside it in the wide feature. */
.bento {
  display: grid;
  gap: var(--card-gap);
  grid-template-columns: 1fr;
}

/* TWO ROWS OF THREE at desktop. Six divides evenly either way, so nothing is
   left alone on a final row — the orphan the old layout produced came from the
   wide feature cell eating two of three columns, not from the column count.
   Two across on tablet, because three 337px cards do not fit 768px. */
@media (min-width: 48rem) {
  .bento { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 62rem) {
  .bento { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.bento__cell {
  display: flex;
  /* Image above copy, at every width — see the note in solutions/index.astro. */
  flex-direction: column;
  /* A query container so the card's own type scale follows the CARD, not the
     viewport. The same card is ~350px in a two-column tablet layout and ~590px
     at desktop, and one media query cannot tell those apart: at 768px the
     viewport says "big" while the cell is at its narrowest of the whole range. */
  container-type: inline-size;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  background-color: var(--white);
  color: var(--text);
  box-shadow: var(--shadow-xs);
  transition:
    transform var(--dur-card) var(--ease-out),
    box-shadow var(--dur-card) var(--ease-out),
    border-color var(--dur-card) var(--ease-out);
}

.bento__cell:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-md);
  border-color: var(--divider);
  text-decoration: none;
  color: var(--text);
}

.bento__media {
  display: block;
  position: relative;
  overflow: hidden;
  flex: 0 0 auto;
  aspect-ratio: 16 / 10;
  background-color: var(--bg-section);
}

.bento__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--dur-card) var(--ease-out);
}

.bento__cell:hover .bento__img { transform: scale(1.04); }

.bento__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: clamp(var(--space-5), 2vw, var(--space-6));
}

.bento__label {
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  /* light surface (solutions bento cells are --white) */
  color: var(--eyebrow-text);
}

.bento__title {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: var(--text-xl);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-tight);
  color: var(--text);
}

.bento__copy {
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--text-secondary);
}

.bento__points {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-1);
  list-style: none;
  padding: 0;
}

.bento__points li {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--text);
}

.bento__points svg {
  flex: 0 0 auto;
  width: 1.125rem;
  height: 1.125rem;
  margin-top: 0.15rem;
  /* Gold ticks, matching the case-study highlight list. */
  color: var(--gold-dark);
}

.bento__more {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  /* Pushed to the foot so the arrow lines up across a row of cells whose copy
     runs to different lengths. */
  margin-top: auto;
  padding-top: var(--space-2);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--green-dark);
}

.bento__more svg {
  width: 1rem;
  height: 1rem;
  color: var(--gold-dark);
  transition: transform var(--dur-card) var(--ease-out);
}

.bento__cell:hover .bento__more svg { transform: translateX(4px); }

/* -- The feature treatment, now on every card once the card is wide enough -- */
/* This used to be `.bento__cell--feature`, applied to the first solution only,
   and it laid the copy beside the image. Every solution is a feature now, so
   what is left of that treatment is the part that scales: the bigger title,
   body-size copy and roomier padding. The side-by-side axis is not here because
   it cannot survive a half-width cell — see solutions/index.astro.

   26rem (416px) is where the title can take --text-h3 without the shorter names
   breaking badly. Below it the card keeps --text-xl, which is what a ~350px
   tablet cell wants. */
@container (min-width: 26rem) {
  .bento__title { font-size: var(--text-h3); }
  .bento__copy { font-size: var(--text-body); }
  .bento__body { padding: clamp(var(--space-6), 4cqw, var(--space-8)); }
}

@media (prefers-reduced-motion: reduce) {
  .bento__cell,
  .bento__img,
  .bento__more svg { transition: none; }
}

/* -- Article datasheet offer -------------------------------------------- */
/* The .datasheet / .datasheet__body / .datasheet__title / .datasheet__copy rules
   that lived here are gone. The offer moved into the sidebar, where it is an
   .article-aside__card like the newsletter one and needs no type scale, fill or
   flex row of its own — the two share a rail, so they should share a shape.
   All that is left is the gap above the button, matching the newsletter form's. */
.article-aside__card .btn { margin-top: var(--space-4); }

/* -- Modal --------------------------------------------------------------- */
/* A native <dialog>. Almost everything here is undoing the UA defaults — the
   element arrives with a border, a fixed max-width and auto margins — rather than
   building behaviour, because the behaviour is the reason to use it. */
.modal {
  /* margin: auto is what centres a <dialog> in the top layer, and it has to be
     restated: the global reset zeroes margins on everything, so the UA's own
     `margin: auto` is gone by the time this element exists and the dialog pins
     itself to the top-left corner. */
  margin: auto;
  width: min(42rem, calc(100vw - 2 * var(--space-4)));
  max-width: none;
  max-height: calc(100dvh - 2 * var(--space-6));
  padding: 0;
  border: 0;
  border-radius: var(--radius-card);
  background-color: var(--white);
  box-shadow: var(--shadow-lg, var(--shadow-md));
  /* Column so the head can sit still while the panel scrolls under it.
     ON [open] ONLY. Author styles beat the UA's `dialog:not([open]) { display:
     none }` whatever the specificity, so an unscoped `display` here would leave
     a closed dialog on the page. */
  color: var(--text);
  /* THE DIALOG CLIPS, THE PANEL SCROLLS.
     This was `overflow: auto` on the dialog itself, which meant the scrollbar
     was drawn on the element carrying the radius — it sat over the rounded
     corners and squared them off. Moving the scroll inside, to the panel, lets
     the dialog keep clean corners and puts the scrollbar within them.

     NOT `display: flex` on the dialog to achieve it: author styles beat the UA
     `dialog:not([open]) { display: none }` whatever the specificity, so setting
     display here would make a closed dialog visible. The panel carries its own
     max-height instead. */
  overflow: hidden;
}

.modal::backdrop {
  background-color: rgb(15 58 53 / 0.55);
}

.modal__panel {
  padding: clamp(var(--space-6), 4vw, var(--space-8));
  /* flex rather than max-height now: the head takes its share first and the
     panel gets the rest. min-height:0 is what lets a flex child actually
     shrink below its content and scroll — without it the panel keeps its full
     height and the dialog grows past the viewport instead. */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  /* So a flick at the end of the form stops there instead of handing the
     gesture to the article behind it. */
  overscroll-behavior: contain;
  /* Reserve the gutter so content does not shift the moment it starts to
     scroll — a form that jumps sideways as you fill it reads as a glitch. */
  scrollbar-gutter: stable;
}

.modal__headings { min-width: 0; }

.modal__head {
  flex: 0 0 auto;
  padding: var(--space-5) var(--space-5) var(--space-4);
  border-bottom: 1px solid var(--border);
  background-color: var(--white);
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
}

.modal__title {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: var(--text-h3);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-tight);
  color: var(--text);
}

/* 2.75rem, over the 24px WCAG 2.2 target minimum and comfortably over it, because
   this is the control someone reaches for in a hurry. */
.modal__close {
  flex: none;
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  margin: calc(var(--space-2) * -1) calc(var(--space-2) * -1) 0 0;
  border: 0;
  border-radius: var(--radius-full);
  background: none;
  color: var(--text-secondary);
  cursor: pointer;
  transition: background-color var(--dur-btn) var(--ease-out);
}

.modal__close:hover { background-color: var(--bg-section); color: var(--text); }
.modal__close svg { width: 1.25rem; height: 1.25rem; }

.modal__copy {
  margin-top: var(--space-3);
  margin-bottom: var(--space-6);
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--text-secondary);
}

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

/* -- Insight article hero ---------------------------------------------- */
/* Cover, header copy and byline as ONE card: the image is the header's
   background, and the byline is a strip joined to its underside. `overflow:
   hidden` on the wrapper is what clips the cover to the shared radius, so the
   two halves cannot drift apart. */
.article-hero {
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  background-color: var(--white);
  box-shadow: var(--shadow-xs);
}

.article-hero__cover {
  position: relative;
  display: flex;
  flex-direction: column;
  /* Chips to the top, headline to the bottom — the gap only matters when a
     title is long enough to meet them. */
  justify-content: space-between;
  gap: var(--space-10);
  min-height: clamp(20rem, 40vw, 28rem);
  padding: clamp(var(--space-5), 3vw, var(--space-8));
  /* Also the whole surface for an insight with no cover image. */
  background-color: var(--green-900);
  color: var(--on-dark);
}

.article-hero__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Black, matching the solutions showcase: a green scrim tints the photograph
   instead of darkening it. Lighter at the top than that one, because the only
   things up there are the two pills and they carry their own backgrounds. */
.article-hero__scrim {
  position: absolute;
  inset: 0;
  background-image: linear-gradient(
    to top,
    color-mix(in srgb, var(--neutral-1000) 88%, transparent) 0%,
    color-mix(in srgb, var(--neutral-1000) 60%, transparent) 46%,
    color-mix(in srgb, var(--neutral-1000) 28%, transparent) 100%
  );
}

.article-hero__top {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
}

/* Both pills float on their own translucent field, so they stay legible over
   the thin end of the scrim whatever the photograph is doing there. */
.article-hero__back,
.article-hero__chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-full);
  border: 1px solid rgb(255 255 255 / var(--opacity-20));
  background-color: color-mix(in srgb, var(--neutral-1000) 55%, transparent);
  backdrop-filter: blur(8px);
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--white);
}

.article-hero__back svg { width: 1rem; height: 1rem; }

.article-hero__back:hover {
  background-color: color-mix(in srgb, var(--neutral-1000) 75%, transparent);
  color: var(--white);
  text-decoration: none;
}

.article-hero__chip {
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--gold-light);
}

.article-hero__headline {
  position: relative;
  max-width: 46ch;
}

/* h2's size, not h1's. The element is still the page's h1 — but at --text-h1's
   72px ceiling a 43-character title runs to three lines inside this card and
   crowds the pills above it. */
.article-hero__headline h1 {
  font-size: var(--text-h2);
  color: var(--white);
}

.article-hero__headline p {
  margin-top: var(--space-4);
  font-size: var(--text-lg);
  line-height: 1.55;
  color: var(--on-dark-muted);
}

/* Attached, not adjacent: a border-top divides it from the cover rather than a
   gap, and the card's own radius finishes its bottom corners. */
.article-hero__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-5) clamp(var(--space-5), 3vw, var(--space-8));
  border-top: 1px solid var(--border);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

/* The card sits in a wider container than the body, so the body needs its own
   top space rather than a shared stack gap. */
.article-body { margin-top: clamp(var(--space-10), 5vw, var(--space-16)); }

/* A full --section-y above the card is a whole section's worth of air between the
   nav and the first thing on the page. The hero card is its own visual opening, so
   it does not need to be introduced by empty space; a third of that reads as
   deliberate instead. Deliberately not quoting the pixel figure — it tracks
   --section-y, which has changed once already. Bottom padding is untouched. */
.article-page { padding-top: clamp(var(--space-5), 3vw, var(--space-10)); }

/* -- Article body: sticky rail + measure -------------------------------- */
.article-layout {
  display: grid;
  gap: clamp(var(--space-8), 4vw, var(--space-12));
  margin-top: clamp(var(--space-10), 5vw, var(--space-16));
}

@media (min-width: 62rem) {
  .article-layout {
    /* Rail then measure, centred as a pair. The content column is capped at
       --prose-max rather than filling the container: the card above is
       deliberately wider than the text, and letting the text stretch to match
       it would undo that. */
    grid-template-columns: 15rem minmax(0, var(--prose-max));
    justify-content: center;
    align-items: start;
  }

  /* position: sticky needs a scroll container that is not `overflow: hidden`,
     and a top offset clear of the sticky site header. */
  .article-aside__inner {
    position: sticky;
    top: calc(var(--header-h) + var(--space-6));
  }

  /* AND IT NEEDS SOMEWHERE TO TRAVEL. A sticky element moves inside its parent's
     box and never beyond it, and `align-items: start` above sized this column to
     its own content — measured 667px of content in a 667px box, so zero range, and
     the rail scrolled away with the article exactly as if it were static.
     Stretching the column to the row's height is what gives sticky somewhere to
     go; the box paints nothing of its own, so the layout is unchanged. */
  .article-aside {
    align-self: stretch;
  }
}

.article-aside__inner {
  display: grid;
  gap: var(--space-6);
}

.article-aside__card {
  padding: var(--space-5);
  border: 1px solid var(--border);
  border-radius: var(--radius-btn);
  background-color: var(--white);
}

.article-aside__heading,
.share__heading {
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-secondary);
}

.article-aside__copy {
  margin-top: var(--space-3);
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--text-secondary);
}

.article-aside__card .newsletter { margin-top: var(--space-4); }

/* -- Table of contents -------------------------------------------------- */
.toc__list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-3);
  /* Numbers would imply the sections must be read in order. */
  list-style: none;
  padding: 0;
  counter-reset: none;
}

.toc__list a {
  display: block;
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--text-secondary);
  /* A rail this narrow wraps long headings; the rule marks where each starts. */
  padding-left: var(--space-3);
  border-left: 2px solid var(--border);
}

.toc__list a:hover {
  color: var(--green-dark);
  border-left-color: var(--gold-light);
  text-decoration: none;
}

/* The sticky site header would otherwise cover a heading jumped to from here.
   scroll-margin on the target is the fix; :target alone cannot do it. */
.prose :is(h2, h3)[id] { scroll-margin-top: calc(var(--header-h) + var(--space-6)); }

/* -- End-of-article CTA -------------------------------------------------- */
/* The band's own top padding plus the article's bottom padding stacked into an
   unusually deep gap, so the article's is removed on these pages instead. */
.article-page { padding-bottom: 0; }
.article-cta { margin-top: clamp(var(--space-12), 6vw, var(--space-20)); }

/* -- Share rail --------------------------------------------------------- */
/* ONE ROW AT EVERY WIDTH. This used to be a column of four labelled rows in the
   rail and a wrapping toolbar on mobile; with the labels now screen-reader only,
   four icons fit a 15rem rail with room to spare, and the two layouts collapse
   into one rule. */
.share__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-3);
  list-style: none;
  padding: 0;
}

/* A square target rather than a full-width row. 2.5rem clears the 24px WCAG 2.2
   minimum with margin, which matters more now that the icon is the whole control
   and there is no label beside it to aim at. */
.share__item {
  display: grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  padding: 0;
  /* --border-control, not --border. The icon is now the whole control, so this
     outline is the only thing saying "button" — and --border (cool-200) renders
     at 1.23:1 against the rail's #f9fdfe, which is not a boundary, it is a
     rumour. --border-control is the token the site already uses for input and
     select edges, 3.75:1 here, and it means the same thing in both places. */
  border: 1px solid var(--border-control);
  border-radius: var(--radius-btn);
  background-color: var(--white);
  color: var(--text-secondary);
  cursor: pointer;
  transition:
    background-color var(--dur-btn) var(--ease-out),
    border-color var(--dur-btn) var(--ease-out),
    color var(--dur-btn) var(--ease-out);
}

/* The mobile override that used to live here set width: auto so the labelled
   rows could shrink-wrap into a toolbar. The buttons are square at every width
   now, and the white fill and border it also carried are in the base rule, so
   there is nothing left for it to say. */

.share__item:hover {
  background-color: var(--green-tint);
  color: var(--green-dark);
  text-decoration: none;
}

/* Inherits currentColor rather than forcing gold, which is how the footer's
   icon-only social buttons already work. Gold reads at 1.76:1 on white: fine
   when it was a decorative mark beside a "LinkedIn" label carrying the meaning
   at 4.9:1, not fine now that it IS the label. Inheriting gives 5.38:1 at rest
   and lets the existing hover and copied colours actually reach the glyph. */
.share__item svg {
  width: 1.125rem;
  height: 1.125rem;
}

/* The copied state swaps the link icon for a tick IN PLACE. Both children sit in
   the same grid cell so the button does not change size at the moment of
   feedback — a control that resizes as you click it is the one thing worse than
   no feedback at all. */
.share__item[data-share-copy] > * { grid-area: 1 / 1; }
.share__tick { display: none; }

.share__item[data-copied='true'] {
  border-color: var(--green-light);
  color: var(--green-dark);
}

.share__item[data-copied='true'] .share__tick { display: block; }
.share__item[data-copied='true'] > :not(.share__tick) { visibility: hidden; }

/* Copy failed: no tick, because nothing was copied. The border carries it, and
   the title/aria-label the script rewrites carries the actual instruction. */
.share__item[data-copied='false'] { border-color: var(--border-error); }

/* -- End-of-body note --------------------------------------------------- */
/* Was .article-endcta: a tinted panel with a border, a card radius and an inset gold
   rail, a bold heading and an arrow link. All of that made it read as a component
   sitting after the article rather than as the article's last word — and a reader who
   has learned that tinted boxes are adverts skips it without reading.

   It is body content now. Nothing here sets a font: the page's own `body` rule
   already gives --text-body at --leading-body, so matching the prose needs only its
   measure, its muted colour and its paragraph rhythm. */
.article-endnote {
  max-width: var(--prose-max);
  /* The gap .prose puts before an h2, so the close is clearly a new movement rather
     than the next paragraph of the section above it. */
  margin-top: var(--space-12);
  color: var(--text-secondary);
}

.article-endnote > * + * { margin-top: var(--space-6); }

/* The catch line. Heading family and weight so it reads as a title, but at
   --text-lg and full --text colour rather than a section-heading size — the point is
   that it closes the prose, not that it opens a new part of the page. The paragraph
   under it keeps the muted body colour, which is what separates the two. */
.article-endnote__title {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: var(--text-lg);
  line-height: 1.3;
  color: var(--text);
  text-wrap: pretty;
}

/* Tighter than the 24px between the other blocks: the paragraph is this title's
   own sentence, not the next thought. */
.article-endnote__title + p { margin-top: var(--space-3); }

/* Underlined, as the prose's own links are — these are links in running text now,
   not a button pretending to be one. */
.article-endnote__links a { text-decoration: underline; }

.article-endnote__sep {
  margin-inline: var(--space-3);
  color: var(--text-disabled);
}

/* -- Previous / next ---------------------------------------------------- */
.article-nav {
  display: grid;
  gap: var(--card-gap);
  margin-top: clamp(var(--space-12), 6vw, var(--space-20));
  padding-top: clamp(var(--space-8), 4vw, var(--space-10));
  border-top: 1px solid var(--border);
}

@media (min-width: 42rem) {
  .article-nav { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.article-nav__card {
  /* Horizontal and compact: these are a footnote to the article, not a second
     offer of content. Cover on the left at a fixed 6rem, so the card's height
     is set by its two lines of text rather than by a 16/9 crop. */
  display: flex;
  align-items: stretch;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: var(--radius-btn);
  background-color: var(--white);
  box-shadow: var(--shadow-xs);
  color: var(--text);
  transition:
    transform var(--dur-card) var(--ease-out),
    box-shadow var(--dur-card) var(--ease-out);
}

.article-nav__card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
  text-decoration: none;
  color: var(--text);
}

.article-nav__cover {
  flex: 0 0 6rem;
  background-color: var(--bg-section);
}

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

.article-nav__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
  padding: var(--space-4);
}

.article-nav__direction {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--gold-dark);
}

.article-nav__direction svg { width: 0.875rem; height: 0.875rem; }

.article-nav__title {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: var(--text-body);
  line-height: var(--leading-snug);
  color: var(--text);
}

.article-nav__category {
  font-size: var(--text-xs);
  color: var(--text-secondary);
}


/* -- Newsletter on a light surface -------------------------------------- */
/* The base .newsletter__input border is --on-dark-border (green-600), tuned
   for the footer's green. On white that saturated green reads as an error
   state, so the light variant uses the same border the contact inputs use. */
.newsletter--light .newsletter__input { border-color: var(--border-control); }

.newsletter--light .newsletter__input:focus {
  border-color: var(--green-light);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--green-500) 25%, transparent);
}

.newsletter--stacked { flex-direction: column; align-items: stretch; }
.newsletter--stacked .newsletter__input { flex: 0 0 auto; width: 100%; }

/* -- Insight card ------------------------------------------------------ */
.insight {
  display: flex;
  flex-direction: column;
  background-color: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  overflow: hidden;
  box-shadow: var(--shadow-xs);
  color: var(--text);
  transition:
    transform var(--dur-card) var(--ease-out),
    box-shadow var(--dur-card) var(--ease-out);
}

.insight:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-md);
  text-decoration: none;
  color: var(--text);
}

.insight .media {
  border: 0;
  border-radius: 0;
  --media-ratio: 16 / 10;
  background-color: var(--green-dark);
}

/* -- Featured insight: the article-hero treatment ------------------------ */
.insights-listing {
  display: grid;
  gap: clamp(var(--space-8), 4vw, var(--space-12));
}

/* Cover is a positioning context in both variants — the category chip sits on
   the image, not above the copy. */
.insight__cover {
  position: relative;
  display: block;
  overflow: hidden;
}

.insight__chips {
  position: absolute;
  inset: var(--space-4) var(--space-4) auto var(--space-4);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  pointer-events: none;
}

/* Same pill as the article hero: translucent dark field with a blur, so it
   holds over any photograph rather than depending on the scrim. */
.insight__chip {
  padding: 0.15rem var(--space-3);
  border-radius: var(--radius-full);
  border: 1px solid rgb(255 255 255 / var(--opacity-20));
  background-color: color-mix(in srgb, var(--neutral-1000) 55%, transparent);
  backdrop-filter: blur(8px);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--gold-light);
}

.insight__chip--flag { color: var(--white); }

/* Hover accent: a gold rule drawing across the foot of the cover. An inset
   box-shadow rail — the device the showcase tabs use — paints under the <img>,
   so on a card with a cover it would only show beside the copy and read as a
   rendering fault. */
.insight__cover::after {
  content: '';
  position: absolute;
  inset: auto 0 0 0;
  height: 3px;
  background-color: var(--gold-light);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-card) var(--ease-out);
}

.insight:hover .insight__cover::after,
.insight:focus-visible .insight__cover::after {
  transform: scaleX(1);
}

.insight__arrow {
  width: 1.125rem;
  height: 1.125rem;
  margin-left: auto;
  color: var(--gold-light);
  transition: transform var(--dur-card) var(--ease-out);
}

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

@media (min-width: 48rem) {
  /* The copy sits ON the cover, anchored to the bottom. Grid with align-items:
     end rather than a flex column, so the content block does not have to know
     the card's height — which is what left the old version centring its copy
     with a ~100px void above the byline. */
  .insight--featured {
    position: relative;
    display: grid;
    align-items: end;
    min-height: clamp(20rem, 32vw, 26rem);
    color: var(--on-dark);
    /* One inset for all four edges. The chips and the copy are separate boxes —
       an absolute row against the top, a padded grid item against the bottom —
       so without a shared value they drift: the chips sat 16px from the top
       (inherited from the base rule) while everything else sat at 38px. */
    --insight-inset: clamp(var(--space-6), 3vw, var(--space-10));
  }

  .insight--featured .insight__cover {
    position: absolute;
    inset: 0;
  }

  .insight--featured .media {
    height: 100%;
    --media-ratio: auto;
  }

  .insight--featured .insight__scrim {
    position: absolute;
    inset: 0;
    background-image: linear-gradient(
      to top,
      color-mix(in srgb, var(--neutral-1000) 88%, transparent) 0%,
      color-mix(in srgb, var(--neutral-1000) 62%, transparent) 46%,
      color-mix(in srgb, var(--neutral-1000) 26%, transparent) 100%
    );
  }

  .insight--featured .insight__content {
    position: relative;
    /* Keeps the measure readable while the card runs the full container. */
    max-width: 48ch;
    padding: var(--insight-inset);
  }

  .insight--featured .insight__title {
    font-size: var(--text-h3);
    color: var(--white);
  }

  .insight--featured .insight__body { color: var(--on-dark-muted); }

  /* Explicit, to cancel the `margin-top: auto` the grid cards use to push their
     meta to the foot of a flex column. That auto was the void. */
  .insight--featured .insight__meta {
    margin-top: var(--space-5);
    padding-top: 0;
    color: var(--on-dark-muted);
  }

  /* inset, not inset-inline: the shorthand also resets the top, which is the
     edge that was out. */
  .insight--featured .insight__chips {
    inset: var(--insight-inset) var(--insight-inset) auto;
  }
  .insight--featured .insight__cover::after { display: none; }
}

/* -- Pagination --------------------------------------------------------- */
.pagination {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding-top: clamp(var(--space-8), 4vw, var(--space-10));
  border-top: 1px solid var(--border);
}

.pagination__pages {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  list-style: none;
  padding: 0;
  /* Centre the numbers even when one arrow is inert and narrower. */
  margin-inline: auto;
}

.pagination__step,
.pagination__page {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 44px;
  padding-inline: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-btn);
  background-color: var(--white);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--green-dark);
  transition:
    background-color var(--dur-btn) var(--ease-out),
    border-color var(--dur-btn) var(--ease-out),
    color var(--dur-btn) var(--ease-out);
}

.pagination__page { min-width: 44px; padding-inline: var(--space-3); }

.pagination__step svg { width: 1.125rem; height: 1.125rem; }

.pagination__step:hover,
.pagination__page:hover {
  background-color: var(--green-tint);
  border-color: var(--divider);
  text-decoration: none;
  color: var(--green-dark);
}

.pagination__page[aria-current='page'] {
  background-color: var(--green-dark);
  border-color: var(--green-dark);
  color: var(--white);
}

/* Inert rather than absent, so the row keeps its shape across pages. */
.pagination__step[data-disabled] {
  opacity: var(--opacity-50);
  color: var(--text-secondary);
  background-color: transparent;
}

.insight__media-fallback {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  background-color: var(--green-dark);
  color: var(--on-dark-muted);
  font-size: var(--text-sm);
}

.insight__content {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-6);
  flex: 1;
}

.insight__title {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: var(--text-lg);
  line-height: var(--leading-snug);
  color: var(--text);
}

.insight__meta {
  /* Flex, not a plain block: the byline is separate spans with "·" between
     them, and inline they butt straight together as "Technologies·24 Jun". */
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: auto;
  padding-top: var(--space-4);
  font-size: var(--text-xs);
  color: var(--text-secondary);
}

/* -- Case study (customer success) ------------------------------------- */
/* Text on the left, the client headline and the screens on the right. */
.case-study {
  display: grid;
  gap: clamp(var(--space-8), 4vw, var(--space-12));
  grid-template-columns: 1fr;
}

@media (min-width: 62rem) {
  /* Uneven on purpose — the screens carry the section. The preview sizes itself
     from its container width, so the larger share renders it BIGGER rather than
     stretching it. */
  .case-study { grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); }

  /* No explicit placement needed: the feature is first in the DOM and belongs
     in the first column, so source order and visual order agree — which also
     means focus order cannot drift from what is on screen. */
  .case-study__aside {
    /* Centred against the taller feature column. */
    align-self: center;
  }
}

.case-study__title {
  margin-top: var(--space-3);
  /* Space before the screens below it. */
  margin-bottom: clamp(var(--space-6), 2vw, var(--space-8));
  font-size: var(--text-h3);
}

.case-study__copy {
  font-size: var(--text-lg);
  line-height: 1.6;
  color: var(--text-secondary);
}

/* Gold ticks here, not the default green — this is a highlight list rather
   than a checklist of commitments. */
.case-study__highlights svg { color: var(--gold-dark); }

.case-study__meta {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.case-study__meta strong {
  font-weight: 600;
  color: var(--text);
}

/* -- Stats band -------------------------------------------------------- */
/* Two real columns: the map is an element on the left, the figures on the
   right. Nothing is clipped — no overflow:hidden, and the map is sized to fit
   its column rather than bleeding past the container. */
.stats-band {
  display: grid;
  gap: clamp(var(--space-8), 4vw, var(--space-12));
  align-items: center;
  grid-template-columns: 1fr;
  padding: clamp(var(--space-8), 4vw, var(--space-12));
  border-radius: var(--radius-card);
  background-color: var(--green-dark);
  color: var(--on-dark);
}

.stats-band__map-wrap {
  /* Positioning context for the pins, sized exactly by the map image. */
  position: relative;
}

.stats-band__map {
  display: block;
  width: 100%;
  height: auto;
}

/* -- Map pins ---------------------------------------------------------- */
/* Placed with left/top percentages derived from each pin's lat/lon through the
   map's projection, so they stay correct at any container width. The pin list
   and the projection are documented where they are rendered, on the homepage. */
.map-pin {
  position: absolute;
  transform: translate(-50%, -50%);
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border-radius: var(--radius-full);
}

/* Sized for eight pins on a map that renders about 406px wide. At 0.75rem the
   UK and Ireland dots, 7px apart at that width, merged into one blob; the whole
   of the British Isles is only ~14px across here. The 2rem tap target on
   .map-pin is deliberately left alone, so the dot shrinking costs no
   pointer accuracy. */
.map-pin__dot {
  position: relative;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: var(--radius-full);
  background-color: var(--gold-light);
  /* The ring's job is to cut the dot out of the landmass behind it, so it has to
     be the colour of the SURFACE, not a fixed dark green. It was hard-coded to
     --green-dark, which is exactly right on the two dark panels this pin was built
     for and reads as a dark outline on the light contact section. The variable
     keeps those two unchanged and lets a light surface pass its own background in.
     Now that the map is green-300 the ring is doing more work than it used to:
     gold on that landmass is only 1.29:1, so the cut-out is what makes the dot a
     dot rather than a smudge. */
  box-shadow: 0 0 0 2px var(--map-pin-ring, var(--green-dark));
  transition: transform var(--dur-btn) var(--ease-out);
}

/* Slow outward pulse to draw the eye. Purely decorative, so it is dropped
   entirely under reduced motion. */
.map-pin__dot::after {
  content: '';
  position: absolute;
  inset: -0.1875rem;
  border-radius: var(--radius-full);
  border: 1.5px solid var(--gold-light);
  animation: pin-pulse 2.6s var(--ease-out) infinite;
}

@keyframes pin-pulse {
  0%   { transform: scale(0.7); opacity: 0.9; }
  70%  { transform: scale(2.1); opacity: 0; }
  100% { transform: scale(2.1); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .map-pin__dot::after { animation: none; opacity: 0; }
}

.map-pin:hover .map-pin__dot,
.map-pin:focus-visible .map-pin__dot { transform: scale(1.35); }

/* Label sits above the pin, revealed on hover and on keyboard focus.

   Z-INDEX BECAUSE PINS ARE SIBLINGS AND PAINT IN DOM ORDER. Without it a tooltip
   slid under any dot declared after its own — on the contact map, Singapore's
   label went behind the Australia pin, and the Philippines' behind both. Lifting
   the tip alone is not enough either: .map-pin has no z-index of its own, so it
   creates no stacking context and a later sibling's DOT still wins. The hovered
   pin is raised as a whole, which carries its tip with it. */
.map-pin:hover,
.map-pin:focus-within {
  z-index: 2;
}

.map-pin__tip {
  position: absolute;
  z-index: 1;
  bottom: calc(100% + 0.35rem);
  left: 50%;
  transform: translateX(-50%) translateY(0.25rem);
  padding: 0.3rem 0.6rem;
  border-radius: var(--radius-sm);
  background-color: var(--white);
  color: var(--green-dark);
  font-size: var(--text-xs);
  font-weight: 600;
  white-space: nowrap;
  box-shadow: var(--shadow-md);
  opacity: 0;
  pointer-events: none;
  transition:
    opacity var(--dur-btn) var(--ease-out),
    transform var(--dur-btn) var(--ease-out);
}

.map-pin:hover .map-pin__tip,
.map-pin:focus-visible .map-pin__tip {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* A right-hand pin's label would overflow the container, so it ends at the dot
   instead of being centred on it. right: 50% is half of the pin's own 2rem box,
   which lands on the dot itself — anchoring to the box edge (right: 0) instead
   still spilled a few px past the map for a pin at 95% on narrow screens. */
.map-pin--flip-left .map-pin__tip {
  left: auto;
  right: 50%;
  transform: translateX(0) translateY(0.25rem);
}

.map-pin--flip-left:hover .map-pin__tip,
.map-pin--flip-left:focus-visible .map-pin__tip {
  transform: translateX(0) translateY(0);
}

@media (min-width: 62rem) {
  /* 1:1.6 — the stats need the room; the map still renders ~406px wide. */
  .stats-band { grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr); }
}

/* The right-hand column: head above figures. It is the grid item now, so the
   1:1.6 split above applies to this rather than to .stats-band__grid — and the
   band's `align-items: center` centres this whole stack against the map. */
.stats-band__body {
  display: flex;
  flex-direction: column;
  gap: clamp(var(--space-6), 3vw, var(--space-8));
}

.stats-band__head {
  /* Stops the head running the full width of a wide band. The figures below are
     a four-column grid and want all of it; two lines of prose do not. */
  max-width: 46ch;
}

/* Stacklane gold, following every other eyebrow on a dark surface here —
   .cta .label, .page-header .label, .showcase__content .label. 5.45:1 on
   --green-dark.

   700 rather than the .label default of 600. Gold on green is a low-chroma
   pairing, so at 13px with 0.1em tracking the extra weight is what makes it
   read; the colour is the one thing not to change here. */
.stats-band .label {
  color: var(--gold-light);
  font-weight: 700;
}

.stats-band__title {
  margin-top: var(--space-3);
  font-family: var(--font-heading);
  font-weight: 700;
  /* Capped at 30px rather than taking --text-h3's 36px: at 36px the title wraps
     to two lines in this column, and it is written as one statement. */
  font-size: clamp(1.5rem, 1.1rem + 1.2vw, 1.875rem);
  line-height: 1.15;
  letter-spacing: var(--tracking-tight);
  color: var(--white);
  text-wrap: balance;
}

/* --on-dark, not --on-dark-muted: this is the band's lead-in and reads as
   primary copy. The muted tone stays on the figures' captions below, which is
   what keeps the two levels apart without a second type size. */
.stats-band__copy {
  margin-top: var(--space-3);
  font-size: var(--text-body);
  line-height: 1.6;
  color: var(--on-dark);
}

/* Column gap is 16px, not 24px: "End Users Supported" is the widest label at
   ~139px, and the extra 8px per gap is what lets four fit on one line. */
.stats-band__grid {
  display: grid;
  gap: var(--space-8) var(--space-4);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

/* Narrow phones: two across leaves ~120px per stat, under the ~139px the
   widest label needs, so stack them instead. */
@media (max-width: 26.99rem) {
  .stats-band__grid {
    grid-template-columns: 1fr;
    gap: var(--space-6);
  }
}

/* Band is still one column here, so the stats have its full width. */
@media (min-width: 48rem) and (max-width: 61.99rem) {
  .stats-band__grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* Beside the map, four across only once the container reaches full width —
   below that each stat falls under ~140px and the label wraps, so 2x2. */
@media (min-width: 80rem) {
  .stats-band__grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

.stat { text-align: center; }

.stat__value {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: var(--text-h2);
  line-height: 1;
  letter-spacing: var(--tracking-tight);
  color: var(--white);
  /* Fixed-width digits so the counter does not jitter as it climbs. */
  font-variant-numeric: tabular-nums;
}

/* Near-white, not --on-dark-muted.

   The muted green-200 is right for footer links and supporting prose, and it was
   measurably fine here too — 5.61:1, past AA. But a desaturated pale green on a
   saturated dark green is a low CHROMA contrast even when the luminance ratio
   passes, and at 15px/400 it read as washed out. These captions are not
   supporting prose: each one is what its figure MEANS, so "100+" is worthless
   without "Projects Delivered" beside it.

   Near-white takes it to 9.42:1, and the weight bump gives the small size some
   body. The figures stay the loud element by size, not by being the only legible
   one. */
.stat__label {
  margin-top: var(--space-2);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--on-dark);
}

/* -- CTA band ---------------------------------------------------------- */
/* The CTA band now carries a WebGPU shader behind it (ShaderBackdrop), so it has to
   be a positioned, clipped stacking context: positioned so the canvas can be
   inset:0 against it, clipped so the canvas cannot spill into the footer, and
   isolated so it cannot land behind another section's content.

   THE BACKGROUND COLOUR STAYS AND IS NOT DECORATION. It is the fallback: on a
   browser without WebGPU the canvas paints nothing at all, and this is what keeps
   the white heading and the buttons legible. Removing it would leave the band
   transparent on roughly a third of browsers. */
.cta {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background-color: var(--green-dark);
  color: var(--on-dark);
  padding-block: var(--section-y);
  text-align: center;
}

/* The shader's tritone runs to acid green and magenta, which are far too light for
   white type to sit on unaided. This darkens the middle of the band where the copy
   actually is, and leaves the edges to the shader. Measured contrast is in the
   message accompanying this change. */
.cta::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  background-image: radial-gradient(
    ellipse 75% 85% at 50% 50%,
    rgb(4 18 15 / 0.82) 0%,
    rgb(4 18 15 / 0.66) 55%,
    rgb(4 18 15 / 0.34) 100%
  );
  pointer-events: none;
}

/* Above both the canvas and the scrim. */
.cta > .container {
  position: relative;
  z-index: 1;
}

.cta h2 { color: var(--white); }
.cta .label { color: var(--gold-light); }

.cta p {
  color: var(--on-dark-muted);
  max-width: 58ch;
  margin-inline: auto;
}

/* -- Page header (interior pages) -------------------------------------- */
/* Dark banner on every interior page. The homepage has no .page-header — it
   leads with the video .hero instead — so it is unaffected.
   Contrast on green-dark: white 9.61:1, green-200 5.61:1, gold-400 5.45:1. */
.page-header {
  background-color: var(--green-dark);
  color: var(--on-dark);
  padding-block: var(--space-16) var(--space-20);
}

.page-header__inner { max-width: 52rem; }
.page-header h1 { margin-top: var(--space-3); color: var(--white); }
.page-header .lead { margin-top: var(--space-5); color: var(--on-dark-muted); }
.page-header .label { color: var(--gold-light); }

/* Small print in the banner — e.g. the legal pages' "Last updated". */
.page-header .field__hint { color: var(--on-dark-muted); }

/* The named third-party platform a solution runs on — "Powered by …".
   gold-light, the same accent the eyebrow uses, because it is an attribution
   rather than body copy; measured 5.45:1 on green-dark. Not italic and not
   small print: it is a product name people search for. */
.page-header__platform {
  margin-top: var(--space-4);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--gold-light);
}

/* The brand's promise lines under the hero. One per line, quiet, and NOT the
   lead's colour: two short declaratives in --on-dark-muted at lead size would
   read as a continuation of the paragraph above rather than as a strapline. */
.page-header__promise {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin-top: var(--space-4);
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--on-dark-muted);
}

/* Breadcrumb inverts with the banner. */
.page-header .breadcrumb a { color: var(--on-dark-muted); }
.page-header .breadcrumb a:hover { color: var(--white); }
.page-header .breadcrumb [aria-current='page'] { color: var(--white); }
.page-header .breadcrumb svg { color: var(--green-300); }

/* -- Breadcrumb -------------------------------------------------------- */
.breadcrumb ol {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  list-style: none;
  padding: 0;
  font-size: var(--text-sm);
}

.breadcrumb li { display: flex; align-items: center; gap: var(--space-2); }
.breadcrumb a { color: var(--text-secondary); }
.breadcrumb a:hover { color: var(--green-link); text-decoration: none; }

.breadcrumb svg {
  width: 1rem;
  height: 1rem;
  color: var(--text-disabled);
}

.breadcrumb [aria-current='page'] { color: var(--text); font-weight: 500; }

/* -- Form fields ------------------------------------------------------- */
.field { display: flex; flex-direction: column; gap: var(--space-2); }

.field__label {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text);
}

.field__required {
  /* The danger red the error text uses, so the mark and the message a missed field
     produces are visibly the same system. margin-left rather than a space in the
     markup: a text node between the label and the mark is a wrap opportunity, and
     an asterisk alone on the second line reads as a footnote. */
  margin-left: 0.15em;
  color: var(--border-error);
  font-weight: 700;
}

.field__hint {
  font-size: var(--text-xs);
  color: var(--text-secondary);
}

/* Only shown once the control is marked invalid. */
.field__error {
  display: none;
  align-items: flex-start;
  gap: var(--space-2);
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--danger-700);
}

.field__error svg { width: 0.875rem; height: 0.875rem; margin-top: 0.15rem; }

.field:has([aria-invalid='true']) .field__error { display: flex; }

.input,
.textarea,
.select {
  width: 100%;
  min-height: 48px;
  padding: 0.6875rem var(--space-4);
  background-color: var(--white);
  border: 1px solid var(--border-control);
  border-radius: var(--radius-btn);
  color: var(--text);
  font-size: var(--text-body);
  font-family: var(--font-body);
  transition:
    border-color var(--dur-btn) var(--ease-out),
    box-shadow var(--dur-btn) var(--ease-out);
}

.input::placeholder,
.textarea::placeholder { color: var(--text-disabled); }

.input:hover,
.textarea:hover,
.select:hover { border-color: var(--cool-600); }

.input:focus,
.textarea:focus,
.select:focus {
  outline: none;
  border-color: var(--green-light);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--green-light) 30%, transparent);
}

.input[aria-invalid='true'],
.textarea[aria-invalid='true'],
.select[aria-invalid='true'] {
  border-color: var(--border-error);
}

.input[aria-invalid='true']:focus,
.textarea[aria-invalid='true']:focus,
.select[aria-invalid='true']:focus {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger-500) 30%, transparent);
}

/* 6.5rem is about four lines at this line-height, which matches what the field's
   own hint asks for — "a sentence or two on the problem is plenty to start". At
   9rem the box was six lines deep and read as a demand for an essay, and it was
   the tallest thing in the form by some way. Still resize: vertical, so anyone
   with more to say can drag it out. */
.textarea { min-height: 6.5rem; resize: vertical; line-height: 1.6; }

/* Native arrow removed so the control matches the other fields. */
.select {
  appearance: none;
  padding-right: var(--space-10);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235F696D' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-4) center;
  background-size: 1.125rem;
  cursor: pointer;
}

/* NOTE ON ORDER: these modifiers must stay BELOW the base .input / .textarea /
   .select rules. They match at the same specificity (0,1,0), so source order is
   the only thing deciding — and placed above, `.textarea { min-height: 6.5rem }`
   beat `.textarea--sm`'s 4rem and the box rendered 104px tall inside the modal
   instead of 64px. No error, just a silently ignored variant. */
/* SMALL SIZE, the form-control counterpart to .btn--sm.
   48px controls and 24px gaps are right on a full page; inside a dialog they
   are what pushed the download form to 996px against an 852px viewport, and
   the submit button below the fold. Same components, one size down — nothing
   is hidden and no field is dropped.

   36px, not smaller: that is the floor where a control is still a comfortable
   target, and .btn--sm sits at 40px for the same reason. */
.input--sm,
.textarea--sm,
.select--sm {
  min-height: 36px;
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-sm);
}

.select--sm { padding-right: var(--space-8); background-size: 1rem; }

/* rows="" is what actually sizes a textarea, so this needs an explicit height
   rather than a min-height floor — the attribute wins over the floor. */
.textarea--sm { min-height: 0; height: 4rem; }

/* The label/hint pair around a small control, so a stack of them tightens by
   the same amount the controls do. */
.field--sm { gap: var(--space-1); }
.field--sm .field__label { font-size: var(--text-xs); }
.field--sm .field__hint { font-size: 0.6875rem; line-height: 1.45; }



/* -- Checkbox ---------------------------------------------------------- */
.checkbox {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  cursor: pointer;
}

.checkbox input {
  appearance: none;
  flex-shrink: 0;
  width: 1.25rem;
  height: 1.25rem;
  margin-top: 0.1875rem;
  background-color: var(--white);
  border: 1px solid var(--border-control);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition:
    background-color var(--dur-btn) var(--ease-out),
    border-color var(--dur-btn) var(--ease-out);
}

.checkbox input:checked {
  background-color: var(--green-dark);
  border-color: var(--green-dark);
  /* Tick drawn as a background image so no extra element is needed. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
  background-size: 0.875rem;
}

.checkbox input[aria-invalid='true'] { border-color: var(--border-error); }

/* Small checkbox — the label text and the gap only. THE BOX STAYS 20px: the
   whole <label> is the click target, so the effective target is the ~44px block
   rather than the box, and shrinking the box further would cost legibility for
   no gain. Must sit below .checkbox for the same reason .input--sm sits below
   .input — equal specificity, so order decides, and above it this rule did
   nothing at all. */
.checkbox--sm { gap: var(--space-2); font-size: var(--text-xs); line-height: 1.45; }

/* -- Alert ------------------------------------------------------------- */
/* The one place the semantic scales are consumed. Colour is never the only
   signal: every alert carries an icon and a text label. */
.alert {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border: 1px solid;
  border-radius: var(--radius-btn);
  font-size: var(--text-sm);
  line-height: 1.6;
}

/* `display: flex` above is an AUTHOR rule, and the `hidden` attribute is only a
   UA rule (`[hidden] { display: none }`) — so author wins and a hidden alert
   rendered anyway. Both alerts on /contact were visible on every page load,
   telling the visitor "We couldn't send that" and "Message sent" at once before
   they had typed anything.

   Any component that sets `display` and is also toggled by `hidden` needs this
   line; .showcase__panel[hidden] carries it for the same reason. */
.alert[hidden] { display: none; }

.alert__icon { flex-shrink: 0; margin-top: 0.125rem; }
.alert__icon svg { width: 1.125rem; height: 1.125rem; }
.alert__title { font-weight: 600; display: block; margin-bottom: 0.125rem; }
.alert p { margin: 0; }
.alert a { color: inherit; text-decoration: underline; }

/* Text uses the 700 step across all four families: warning-600 is only
   3.76:1 on its own tint, so 700 is the uniform AA-safe choice. */
.alert--success {
  background-color: var(--success-50);
  border-color: var(--success-200);
  color: var(--success-700);
}

.alert--warning {
  background-color: var(--warning-50);
  border-color: var(--warning-200);
  color: var(--warning-700);
}

.alert--danger {
  background-color: var(--danger-50);
  border-color: var(--danger-200);
  color: var(--danger-700);
}

.alert--info {
  background-color: var(--info-50);
  border-color: var(--info-200);
  color: var(--info-700);
}

/* -- Toast -------------------------------------------------------------- */
/* Errors only — see src/components/Toast.astro for why there is no success
   variant. Shares the danger scale with .alert--danger so a failure looks the
   same wherever it is reported. */
.toast-region {
  position: fixed;
  right: var(--container-pad);
  bottom: var(--space-6);
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  width: min(26rem, calc(100vw - var(--container-pad) * 2));
  /* The region is always in the DOM and usually empty; without this it would
     sit over the bottom-right of every page swallowing clicks. */
  pointer-events: none;
}

.toast {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border: 1px solid var(--danger-200);
  border-radius: var(--radius-btn);
  background-color: var(--danger-50);
  color: var(--danger-700);
  font-size: var(--text-sm);
  line-height: 1.6;
  /* Lifted off the page: unlike an alert it is not part of the layout, so it
     needs to read as something sitting on top of it. */
  box-shadow: 0 8px 24px rgb(0 0 0 / 0.12);
  animation: toast-in 200ms ease-out;
}

.toast__icon { flex-shrink: 0; margin-top: 0.125rem; }
.toast__icon svg { width: 1.125rem; height: 1.125rem; }
.toast__text { flex: 1; margin: 0; }

.toast__close {
  flex-shrink: 0;
  display: flex;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  cursor: pointer;
  opacity: 0.7;
}

.toast__close:hover { opacity: 1; }
.toast__close svg { width: 1rem; height: 1rem; }

@keyframes toast-in {
  from { opacity: 0; translate: 0 0.5rem; }
  to { opacity: 1; translate: 0 0; }
}

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

/* -- Contact layout ---------------------------------------------------- */
.contact-grid {
  display: grid;
  gap: clamp(var(--space-8), 5vw, var(--space-16));
  grid-template-columns: 1fr;
  align-items: start;
}

/* Details column first, form second — the form keeps the wider track, it has just
   swapped sides. Reordered in the MARKUP rather than with `order`, so the tab order
   still runs left to right: a keyboard user reaching the form before the phone
   number it sits to the right of would be reading the page in an order nobody can
   see. The cost is that the narrow layout now stacks details above the form. */
@media (min-width: 62rem) {
  .contact-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr); }
}

.form-grid {
  display: grid;
  gap: var(--space-5);
  grid-template-columns: 1fr;
}

@media (min-width: 34rem) {
  .form-grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* DOWNLOAD MODAL LAYOUT.
   The controls themselves are handled by size="sm" on the form — see
   .input--sm. What is left here is the dialog's own furniture: padding, the
   heading, and the gaps between blocks. Keeping the two apart means the small
   control size is reusable anywhere, and this stays what it is: layout for one
   dialog. All of it is scoped to .modal, so /contact and /book-a-demo keep the
   full-size form. */
.modal .modal__panel { padding: var(--space-5); }
.modal .modal__title { font-size: var(--text-h4); line-height: var(--leading-snug); }
.modal .modal__copy { margin-top: var(--space-1); margin-bottom: 0; font-size: var(--text-xs); }

/* The document being sent. Carried by the copy line rather than the heading, so
   the heading can stay generic — but it is the one thing in this dialog the
   reader needs to recognise, so it takes the ink. */
.modal__file { color: var(--text); font-weight: 700; }
.modal .stack--lg > * + * { margin-top: var(--space-4); }
.modal .form-grid { gap: var(--space-4); }

/* PAIRED AT EVERY WIDTH, inside the modal only. The shared .form-grid--2 waits
   for 34rem, so on a phone these four stack and cost ~400px of a 796px budget.
   Paired they cost ~190px. The inputs end up ~150px wide at 390px, which is
   tight to type in but is the price of seeing the send button. */
.modal .form-grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }

/* Short or narrow screens. Keyed off height as well as width, because height is
   the dimension that is actually short on a laptop. Whitespace only. */
@media (max-height: 780px), (max-width: 34rem) {
  .modal { max-height: calc(100dvh - 2 * var(--space-3)); }
  .modal .modal__panel { padding: var(--space-4); }
  .modal .modal__head { padding: var(--space-4); }
  .modal .modal__title { font-size: var(--text-base); }
  .modal .modal__copy { margin-bottom: var(--space-2); line-height: 1.45; }
  .modal .stack--lg > * + * { margin-top: var(--space-2); }
  .modal .form-grid { gap: var(--space-2); }
}

/* Phones under ~670px tall — an iPhone SE, a 640px Android. The last of the
   whitespace; no control shrinks below the 36px the --sm size sets. */
@media (max-height: 700px) {
  .modal { max-height: calc(100dvh - 2 * var(--space-1)); }
  .modal .modal__panel { padding: var(--space-3); }
  .modal .modal__head { padding: var(--space-3); }
  .modal .modal__title { font-size: var(--text-sm); }
  .modal .modal__copy { margin-bottom: var(--space-1); line-height: 1.4; }
  .modal .stack--lg > * + * { margin-top: var(--space-1); }
  .modal .form-grid { gap: var(--space-1) var(--space-2); }
  .modal .textarea--sm { height: 2.5rem; }
  .modal .checkbox--sm { line-height: 1.35; }
  /* 360px is the narrowest of these, and the paired labels wrap there — which
     is what put this one over on its own. */
  .modal .field--sm .field__label { line-height: 1.3; }
}

/* Contact detail panel */
.info-panel {
  background-color: var(--bg-section);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  padding: var(--space-8);
}

.info-block + .info-block {
  margin-top: var(--space-6);
  padding-top: var(--space-6);
  border-top: 1px solid var(--border);
}

.info-block__label {
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-secondary);
  margin-bottom: var(--space-2);
}

.info-block a { font-weight: 500; }

/* Visually hidden honeypot — kept out of the tab order and the a11y tree. */
.honeypot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* reCAPTCHA v3's floating badge. Google's terms allow hiding it provided the
   page names them instead, and /contact carries that line under the send
   button. visibility rather than display: their script measures the element,
   so it has to keep its box. */
.grecaptcha-badge {
  visibility: hidden;
}

/* -- Service list (capability pages) ----------------------------------- */
/* One grid, not two hand-sliced columns: grid rows share a height, so the
   rules line up across columns. Previously each column flowed independently
   and the dividers landed at different heights. */
.service-grid {
  display: grid;
  grid-template-columns: 1fr;
  column-gap: clamp(var(--space-8), 5vw, var(--space-16));
  list-style: none;
  padding: 0;
}

@media (min-width: 48rem) {
  .service-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.service {
  padding-block: var(--space-6);
  border-top: 1px solid var(--border);
}

/* A short gold rule reads as a deliberate marker. The Lucide `dot` icon this
   replaces rendered as a speck floating in a 24px box, and — being inside the
   heading's flex row — indented the title while leaving the body flush left. */
.service__title {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: var(--text-lg);
  line-height: var(--leading-snug);
  color: var(--text);
}

.service__title::before {
  content: '';
  display: block;
  width: 1.5rem;
  height: 2px;
  margin-bottom: var(--space-3);
  background-color: var(--gold-light);
}

.service__body {
  margin-top: var(--space-2);
  max-width: 46ch;
  font-size: var(--text-sm);
  line-height: 1.7;
  color: var(--text-secondary);
}

/* -- Quadrant (company page: our difference) --------------------------- */
/* The wireframe's 2x2 with "YOUR BUSINESS" at the centre. A 3-column grid where
   the middle column is the centre label, rather than four cards with something
   absolutely positioned over them — that way one-column reflow just works and the
   centre never lands on top of a card. */
/* Explicit areas, not auto-placement. The centre spans both rows, and auto
   placement put it in column 1 — the four cards then bunched to its right instead
   of surrounding it. Naming the areas is the only way to say "middle column,
   both rows" unambiguously. */
.quadrant {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  grid-template-rows: auto auto;
  grid-template-areas:
    'tl mid tr'
    'bl mid br';
  gap: var(--space-3);
  align-items: stretch;
  /* For the orbit below. isolation, so its negative z-index stays inside this box
     instead of sliding behind the section's own background. */
  position: relative;
  isolation: isolate;
}

/* -- The orbit ---------------------------------------------------------- *
 * A ring centred on "Your business" with a dot travelling round it, drawn BEHIND
 * the four cards.
 *
 * TWO PSEUDO-ELEMENTS, NOT MARKUP. The cards get their grid areas from
 * .quadrant > :nth-child(1..5), so a real element inside .quadrant would have
 * shifted every card into the wrong quadrant. Pseudo-elements also keep a purely
 * decorative thing out of the accessibility tree without an aria-hidden to
 * remember.
 *
 * SIZED FROM THE HEIGHT, not the width. The block is far wider than it is tall, so
 * a width-based circle would bleed a long way above and below the cards; at 92% of
 * the height the ring instead threads through the four gaps between them and
 * passes behind only the cards' inner corners. Those are opaque, so what a visitor
 * sees is four arcs in the gaps closing around the centre.
 *
 * z-index: -1 puts it under the cards; the centre label has no fill, but the ring's
 * radius clears the label on every side, so nothing crosses the text.            */
.quadrant::before,
.quadrant::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  aspect-ratio: 1;
  translate: -50% -50%;
  pointer-events: none;
  z-index: -1;
}

/* The path. Turning at all is what makes a dashed circle read as motion; one
   revolution a minute is slow enough to be atmosphere rather than a spinner. */
.quadrant::before {
  height: 92%;
  border: 1px dashed var(--green-200);
  border-radius: 50%;
  animation: quadrant-drift 60s linear infinite;
}

/* The travellers. Dots painted by gradients on a slightly larger box and carried
   round by rotating it — the extra 0.4rem of radius is exactly each dot's offset
   from that box's edge, which lands its centre on the ring above. No border-radius
   here on purpose: a round box would clip a dot sitting on its edge in half.

   TWO DOTS, 90 DEGREES APART, and the angle is the point. The cards are opaque and
   cover everything but the gap between the columns, so only about 42% of the ring
   is ever visible — a single dot spent nearly six seconds of every sixteen behind
   a card, which reads as a broken animation rather than an orbit. A second dot a
   quarter-turn away is in the visible arc whenever the first one is not: measured
   against this geometry, at least one is showing 84% of the time.

   Diametrically opposite would NOT have worked. The hidden arcs are the left and
   right sides, so two dots 180 degrees apart hide and reappear together. */
.quadrant::after {
  height: calc(92% + 0.8rem);
  background-image:
    radial-gradient(circle at 50% 0.4rem, var(--gold-dark) 0 0.28rem, transparent 0.3rem),
    radial-gradient(
      circle at calc(100% - 0.4rem) 50%,
      var(--green-light) 0 0.24rem,
      transparent 0.26rem
    );
  animation: quadrant-orbit 16s linear infinite;
}

@keyframes quadrant-drift {
  to { rotate: 360deg; }
}

@keyframes quadrant-orbit {
  to { rotate: 360deg; }
}

/* Decoration is exactly what a reduced-motion preference is asking to stop. The
   ring stays; it just stops moving. */
@media (prefers-reduced-motion: reduce) {
  .quadrant::before,
  .quadrant::after { animation: none; }
}

/* DOM order is transform, build, centre, deliver, scale — so the reading order
   and the stacked mobile order are both correct without reordering markup. */
.quadrant > :nth-child(1) { grid-area: tl; }
.quadrant > :nth-child(2) { grid-area: tr; }
.quadrant > :nth-child(3) { grid-area: mid; }
.quadrant > :nth-child(4) { grid-area: bl; }
.quadrant > :nth-child(5) { grid-area: br; }

.quadrant__cell {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-5);
  border: 1px solid var(--border);
  border-radius: var(--radius-btn);
  background-color: var(--white);
  transition:
    border-color var(--dur-btn) var(--ease-out),
    box-shadow var(--dur-btn) var(--ease-out);
}

.quadrant__cell:hover {
  border-color: var(--green-dark);
  box-shadow: var(--shadow-xs);
  text-decoration: none;
}

.quadrant__verb {
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--eyebrow-text);
}

.quadrant__unit {
  font-family: var(--font-heading);
  font-size: var(--text-lg);
  font-weight: 700;
  color: var(--text);
}

/* Placed in the `mid` area above, which spans both rows. Column, so the building
   sits over the words rather than beside them — beside would widen the middle
   column and push the cards apart. */
.quadrant__centre {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding-inline: var(--space-4);
  max-width: 7rem;
  font-family: var(--font-heading);
  font-size: var(--text-sm);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  text-align: center;
  color: var(--green-dark);
}

.quadrant__centre-icon { color: var(--green-light); }

/* One column below 48rem, with the centre label back in document order between
   the pairs — which is where it belongs when there is no middle column. */
@media (max-width: 48rem) {
  /* No middle to orbit once the grid is a single column: a ring centred on the
     stacked list would circle the second card, not the business. */
  .quadrant::before,
  .quadrant::after { display: none; }

  /* One column, and the areas have to go with it — a named grid keeps its
     placement otherwise and the centre would still try to span two rows. */
  .quadrant {
    grid-template-columns: 1fr;
    grid-template-rows: none;
    grid-template-areas: none;
  }

  .quadrant > :nth-child(n) {
    grid-area: auto;
  }

  .quadrant__centre {
    max-width: none;
    padding-block: var(--space-2);
  }
}

/* -- Numbered value cards (company page) ------------------------------- */
.value {
  display: flex;
  align-items: stretch;
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  background-color: var(--white);
  overflow: hidden;
}

/* The dark number panel from the wireframe. Fixed width so all four align, and
   the gold sits on green-dark at 5.45:1. */
.value__num {
  flex: none;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  width: 5.5rem;
  padding-block: var(--space-5);
  background-color: var(--green-dark);
  font-family: var(--font-heading);
  font-size: var(--text-lg);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  color: var(--gold-light);
}

.value__body {
  padding: var(--space-5) var(--space-6);
}

@media (max-width: 30rem) {
  .value__num {
    width: 4rem;
  }

  .value__body {
    padding: var(--space-4) var(--space-5);
  }
}

/* -- Presence band (company page) -------------------------------------- */
/* Dark band with the world map on one side and the locations on the other.
   Measured on green-dark: the gold label 5.45:1, body and focus lines in
   green-200 5.61:1, white title 9.61:1. */
.presence {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-8);
  align-items: center;
  padding: clamp(var(--space-8), 4vw, var(--space-12));
  border-radius: var(--radius-card);
  background-color: var(--green-dark);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / var(--opacity-10));
}

@media (min-width: 62rem) {
  .presence {
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
    gap: var(--space-12);
  }
}

/* -- Contact: the locations map ---------------------------------------- */
/* Its own wrapper rather than .presence__map-wrap, which is a column of a
   two-column dark panel. Here the map is the full width of a light section, so it
   needs a ceiling: at 1200px the artwork's 3215 x 2119 would run 790px tall and
   push the address cards off the first screen entirely. */
.locations__map-wrap {
  position: relative;
  max-width: 56rem;
  margin-inline: auto;
  /* This map sits on --bg-section, not on a dark panel, so the pins knock out of
     that instead. Same token the section's own background uses, so the two cannot
     drift apart. */
  --map-pin-ring: var(--bg-section);
}

.locations__map {
  display: block;
  width: 100%;
  height: auto;
}

/* -- Contact: a location card ------------------------------------------ */
.location {
  /* Tight to the title. The address is what the title is about, not a second
     section of the card — at --space-4 the pair read as two separate blocks, and
     the card's own padding already separates them from its edges. */
  gap: var(--space-2);
}

/* Dot, then country. Baseline-aligned rather than centred: with a single line of
   heading the two look the same, but a name that wraps to two lines would pull a
   centred dot down to the middle of the block instead of leaving it beside the
   first line where it belongs. */
/* The card as a link. .card already carries the transition for all three of these
   properties, so the hover state only has to name the end values.

   `.card` IS IN THE SELECTOR TO WIN A SPECIFICITY TIE, not for style. The cards
   carry data-reveal, and `[data-reveal][data-visible] { transform: none }` is also
   0,2,0 and sits 1500 lines further down the file — so `.location--link:hover`
   alone lost the transform and the card never lifted. Measured, not assumed: the
   computed transform stayed `none` through a real hover. 0,3,0 settles it. */
.card.location--link:hover,
.card.location--link:focus-visible {
  transform: translateY(-4px);
  box-shadow: var(--shadow-md);
  border-color: var(--divider);
  text-decoration: none;
}

/* The address must not turn blue and underlined just because its card became a
   link — it is an address, not link text, and the card's own lift is what says it
   is clickable. */
.location--link,
.location--link:hover {
  color: var(--text);
}

.location__name {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  font-family: var(--font-heading);
  font-weight: 700;
  /* --text-body (18px), a step down from --text-lg. The card is a postal address
     with a city on top, not a section heading: at 20px the city outweighed the
     address it belongs to. The heading face and 700 weight still separate the two,
     and the address below is --text-sm (15px), so there is a clear step between
     them without the title shouting. */
  font-size: var(--text-body);
  line-height: var(--leading-snug);
  color: var(--text);
}

/* The same mark as .map-pin__dot above it, at the same 0.5rem, so a reader joins
   the card to its pin without being told. Teal rather than the pins' gold: gold on
   white is 1.72:1 and a dot this small would disappear, where --green-light is
   4.23:1. `top` nudges it off the text baseline to sit optically centred on the
   first line — baseline alignment puts a circle's bottom edge on the baseline,
   which reads as slightly low. */
.location__dot {
  position: relative;
  top: -0.2em;
  flex: 0 0 auto;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: var(--radius-full);
  background-color: var(--green-light);
}

/* WHERE TWO PINS SIT ON TOP OF EACH OTHER, the hit target is the dot, not the box.
   .map-pin is a 2rem square centred on a 0.5rem dot — a generous pointer target,
   and deliberately so on the maps it was built for. Here Brisbane and Sydney are
   16px apart at desktop, so Sydney's box covers Brisbane's centre entirely and,
   being the later sibling, wins every hit test: Brisbane's tooltip could not be
   opened at all. Measured, not guessed — elementFromPoint at Brisbane's own centre
   returned the Sydney pin.

   Narrowing the target to the dot makes each pin reachable whenever the dots are
   visibly apart, which is the only case where a reader could be aiming at one
   rather than the other. Scoped to this map: the company and home maps keep their
   2rem targets, and the note on .map-pin__dot explains why that was chosen.

   The pulse ring is excluded too — it scales to 2.1x, so its hit area grows and
   shrinks twice a second, which is not something a pointer should have to chase. */
.locations__map-wrap .map-pin {
  pointer-events: none;
}

.locations__map-wrap .map-pin__dot {
  pointer-events: auto;
}

.locations__map-wrap .map-pin__dot::after {
  pointer-events: none;
}

/* Leaves-the-site arrow, the same mark and nudge the industry cards use. Pushed to
   the end of the title row by margin rather than a spacer, and it is the reason the
   row is baseline-aligned: the arrow sits on the city's baseline instead of
   floating in the middle of the line. */
.location__go {
  flex: 0 0 auto;
  margin-left: auto;
  width: 1.125rem;
  height: 1.125rem;
  color: var(--green-light);
  transition: transform var(--dur-card) var(--ease-out);
}

.location--link:hover .location__go,
.location--link:focus-visible .location__go {
  transform: translate(3px, -3px);
}

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

/* The address, as lines rather than a paragraph: a postal address broken by the
   container instead of by its own line breaks is a different address. */
.location__lines {
  display: flex;
  flex-direction: column;
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--text-secondary);
}

.presence__map-wrap {
  position: relative;
  /* The map is the only thing establishing height here, so it must not shrink
     below something legible. */
  min-width: 0;
}

/* No filter and no opacity change: the SVG already carries its own colour, which
   is why .stats-band__map on the home page sets nothing but the box either. An
   earlier version of this rule invented a hue-rotate filter to "lift it onto dark
   green" — the artwork needs no such help. */
.presence__map {
  display: block;
  width: 100%;
  height: auto;
}

.presence__label {
  color: var(--gold-light);
}

.presence__title {
  font-size: var(--text-h3);
  color: var(--white);
}

.presence__copy {
  margin-top: var(--space-3);
  max-width: 46ch;
  font-size: var(--text-body);
  line-height: 1.7;
  color: var(--on-dark-muted);
}

.presence__locations {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--space-5);
  margin-top: var(--space-8);
}

.presence__place {
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--gold-light);
}

.presence__focus {
  margin-top: var(--space-1);
  font-size: var(--text-sm);
  color: var(--on-dark-muted);
}

/* -- Team grid (company page: leadership) ------------------------------ */
/* Rows of groups. A row holds one group, or two small ones side by side — see
   company.astro for which pair and why.

   The rhythm lives on the row, not on .team-group, because two paired groups are
   siblings inside one row: `.team-group + .team-group` would have pushed the
   second one down by a whole band instead of placing it alongside. It still
   applies below the breakpoint, where a pair stacks and wants that gap back. */
.team-row + .team-row {
  margin-top: clamp(var(--space-10), 4vw, var(--space-16));
}

.team-row--pair > .team-group + .team-group {
  margin-top: clamp(var(--space-10), 4vw, var(--space-16));
}

/* Two columns only once each one can still hold two 12rem portrait cards plus the
   column gap — below that a pair would drop to one card per column and read as
   four groups rather than two. */
@media (min-width: 62rem) {
  .team-row--pair {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: clamp(var(--space-8), 4vw, var(--space-12));
    align-items: start;
  }

  .team-row--pair > .team-group + .team-group { margin-top: 0; }
}

/* A group heading, not a section heading — it labels the row beneath it, so it
   sits on a rule and is centred over the row the way the wireframe has it. */
.team-group__name {
  margin-bottom: var(--space-6);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--border);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  text-align: center;
  color: var(--eyebrow-text);
}

/* NOT the .grid utility. Groups hold two to five people, and .grid's
   `auto-fit` + `1fr` stretches an under-filled row to the full container —
   a two-person group rendered as two 584px portraits. Here the track has a
   fixed cap so a portrait is the same size in every group, and short rows
   centre instead of stretching, as the wireframe shows them.

   THE CAP IS 12rem BECAUSE THE LARGEST GROUP NOW HOLDS FIVE. At the old 15rem
   the five of Business & Client needed 1328px of a 1200px container, so the
   fifth wrapped to a row of its own and sat alone under the first portrait.
   Five 12rem tracks with a 24px column gap need 1056px, which fits with room
   to spare. The arithmetic is worth keeping in view: a sixth person in any one
   group puts this back where it was, and the cap has to come down again.

   Portraits stay 12rem at every width and simply wrap — 5 across, then 4, 3,
   2, 1 — rather than growing on small screens, where a full-width portrait
   would dominate the viewport. The `min(12rem, 100%)` floor is a safety valve
   for a container narrower than the cap (under ~230px of viewport): minmax()
   clamps the track to the floor so it can never overflow.

   Row gap is deliberately larger than the column gap. Wrapped rows butt a
   card's role text against the next card's portrait, and at an equal 24px the
   two rows read as one block. */
.team-grid {
  display: grid;
  column-gap: var(--space-6);
  row-gap: var(--space-10);
  grid-template-columns: repeat(auto-fit, minmax(min(12rem, 100%), 12rem));
  justify-content: center;
}

/* A DECLARED ROW SHAPE, for the group sizes that auto-fit gets wrong — see
   ROW_PATTERN in company.astro, which computes --team-cols on the grid and --row /
   --col on each card.

   THE TRACKS ARE HALF A CARD WIDE and each card spans two of them. That is what
   centres a short row over a long one: with whole columns, two cards above three
   would have to begin half a column in, which grid cannot express, and the row came
   out left-aligned. A card is still 12rem — two tracks plus the gap between them —
   so nothing about the cards themselves changes.

   Gated at 40rem rather than the 48rem the rest of this section uses, and the
   reason is arithmetic: SEVEN CARDS DIVIDE EVENLY INTO NOTHING. Two columns give
   3 + 3 + 1, three give 3 + 3 + 1 as well — every uniform grid leaves one card
   alone, so the declared pattern is the only shape that does not, and it is worth
   holding on to for as far down as it still fits. Six half-tracks plus their gaps
   are 39rem, which clears the container at 40rem; below that the auto-fit rule
   above runs and these custom properties go unread, since nothing sets grid-row or
   grid-column out there.

   minmax(0, …) rather than a fixed track so a pattern wider than its container
   shrinks to fit instead of overflowing it. */
@media (min-width: 40rem) {
  .team-grid[data-pattern] {
    /* Half a card: 12rem less the one gap that sits inside every card's span. */
    --team-track: calc((12rem - var(--space-6)) / 2);
    grid-template-columns: repeat(var(--team-cols), minmax(0, var(--team-track)));
  }

  .team-grid[data-pattern] > .person {
    grid-row: var(--row);
    grid-column: var(--col) / span 2;
  }
}

/* The portrait frame — a circle, which is the convention for a person as against
   the square used for everything else on the site. It holds either a headshot or
   a monogram, and looks the same either way.

   THE FILL IS NOT DECORATION. The headshots are cut-outs on transparency, so
   this brand teal is the background the person is seen against, not something
   hidden behind an opaque photograph. Which is also why it is a flat colour now:
   it was a two-stop gradient, and a gradient behind a cut-out reads as a
   lighting effect on the subject rather than as a backdrop. */
.person__portrait {
  display: grid;
  place-content: center;
  aspect-ratio: 1 / 1;
  border-radius: var(--radius-full);
  border: 1px solid var(--green-100);
  background-color: var(--green-500);
  /* The cut-outs are framed to the bottom edge, so shoulders run past the
     circle. Clipping them is the intent — without this they would spill out
     over the name below. */
  overflow: hidden;
}

/* The photograph fills the circle. width/height are on the element as attributes
   too, from Astro, so the box is reserved before the image loads and the grid
   does not reflow as twelve lazy portraits arrive. */
.person__portrait img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.person__initials {
  font-family: var(--font-heading);
  font-size: clamp(1.75rem, 1rem + 3vw, 2.5rem);
  font-weight: 700;
  /* Tracking opens the pair up — two capitals at this size otherwise read as
     one glyph. The trailing space on the right is cancelled so it stays
     optically centred rather than sitting a half-letter to the left. */
  letter-spacing: 0.08em;
  margin-inline-start: 0.08em;
  /* White, not --gold-light, since the fill became the brand teal. Gold-400 read
     5.45:1 on the dark green gradient this replaced and only 2.46:1 on #208782 —
     under the 3:1 WCAG 1.4.3 asks even of large text, which this is at 40px.
     White measures 4.33:1, the best available; every gold step from 300 down
     fails, and gold-100 only reaches 3.74:1. */
  color: var(--white);
}

/* Name and role centred under the circle. Left-aligned type under a round frame
   left a wedge of space on both sides of the first line and none under the
   widest, so the block never lined up with anything. */
.person {
  text-align: center;
}

.person__name {
  margin-top: var(--space-4);
  font-family: var(--font-heading);
  font-size: var(--text-lg);
  font-weight: 700;
  color: var(--text);
}

.person__role {
  margin-top: var(--space-1);
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--text-secondary);
  /* Three lines reserved whether or not a title needs them, so every card in
     every group is the same height. Titles here run one to three lines — "Head
     of Business Development, state of Victoria in Australia" is the longest —
     and without the reserve the groups came out 292px and 316px, differing by
     exactly one line of role.

     Grid row stretching already equalises cards WITHIN a row, so this is what
     equalises them BETWEEN groups. It also means a title longer than three
     lines makes its card taller again and this number has to go up. */
  min-height: calc(3 * 1.6 * var(--text-sm));
}

/* -- Callout band (company page: careers) ------------------------------ */
.callout {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-8);
  padding: clamp(var(--space-8), 4vw, var(--space-12));
  border-radius: var(--radius-card);
  background-color: var(--green-dark);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / var(--opacity-10));
}

.callout__label {
  color: var(--gold-light);
}

.callout__title {
  font-size: var(--text-h3);
  color: var(--white);
}

.callout__copy {
  max-width: 52ch;
  font-size: var(--text-body);
  line-height: 1.7;
  color: var(--on-dark-muted);
}

/* -- Platform note (solution pages) ------------------------------------ */
/* "Powered by Workvue" and the like: a statement about what the solution runs on,
   set apart from the feature cards above it. Dark green because the section it
   sits in is light and already holds light cards, so another light panel would
   read as one more card. This is a band, not a card, which is why it kept its
   dark treatment when .card--dark was retired.
   Measured on green-700: the label in gold-light is 5.45:1, the body in green-200
   is 5.61:1. */
.platform-note {
  padding: clamp(var(--space-8), 4vw, var(--space-12));
  border-radius: var(--radius-card);
  background-color: var(--green-dark);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / var(--opacity-10));
}

.platform-note__body {
  max-width: 68ch;
  margin-top: var(--space-4);
  font-size: var(--text-body);
  line-height: 1.7;
  color: var(--on-dark-muted);
}

/* Product ownership, stated as label/value pairs rather than prose. The label is
   the quiet half and the value the loud one, because the values — Workvue,
   Stacklane — are the two facts a reader has to leave with. Wraps to stacked
   rows on narrow screens instead of squeezing the pair. */
.platform-note__facts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-8);
  margin-top: var(--space-5);
  padding: 0;
  list-style: none;
}

.platform-note__facts li {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.platform-note__fact-label {
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--on-dark-muted);
}

.platform-note__fact-value {
  font-size: var(--text-body);
  font-weight: 600;
  color: var(--white);
}

/* -- Journey flow (solution pages) ------------------------------------- */
/* The brief's horizontal process: Prepare -> Detect -> Communicate -> ...
   Numbered cards in a row, wrapping rather than scrolling — six steps do not fit
   390px at a legible size, and a horizontally scrolling row hides steps behind a
   gesture nobody is told about.

   There are deliberately no arrows between steps; see the note in
   solutions/[slug].astro for the two layout faults they caused. The numbers carry
   the sequence, which is what the arrows were there for.

   `flex: 1 1 auto` with an unbreakable label: each step sizes to its own content
   and the row wraps between steps. The alternative — equal `flex: 1 1 0` tracks —
   let a long single word overflow its card, measured at 45px of spill for
   "Communicate" at 1088px. Sizing to content cannot do that. */
.journey {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.journey__step {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 1 1 auto;
  padding: var(--space-4) var(--space-5);
  border: 1px solid var(--border);
  border-radius: var(--radius-btn);
  background-color: var(--white);
}

/* The number carries the order, so the sequence survives any wrap. */
.journey__num {
  flex: none;
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  color: var(--eyebrow-text);
}

.journey__label {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
}

/* -- Hand-drawn arrows between the steps -------------------------------- */
/* HIDDEN BY DEFAULT, shown only in the one-row range below. Every width where the
   row wraps therefore has no arrows at all, which is the fault that took them out
   the first time — a wrapped row's last arrow pointed into the margin. */
.journey__arrow { display: none; }

@media (min-width: 72rem) {
  /* The gap has to hold the arrow. At 12px there was nowhere for one to go, which
     is why the old attempt drew it over the boxes uncontrolled; 32px is now the span
     the 48px arrow bridges, overlapping each card by 8px of its 20px padding.

     THIS IS ALSO WHAT MOVES THE WRAP POINT. Six steps and five gaps: adding 16px to
     each gap costs 100px, so the row needs ~1060px of viewport to stay on one line
     against ~960px before. --space-8, NOT --space-7: the scale skips 7, and the
     missing token resolved to nothing — which collapsed the gap to `normal` and
     dropped every arrow onto the next box, the exact fault this positioning exists
     to prevent. 72rem (1152px) clears that with room, and is verified as
     one row on all five row-shaped solutions. */
  .journey { gap: var(--space-8); }

  .journey__step { position: relative; }

  /* Absolutely positioned across the boundary between two steps, so it is outside
     the flex flow entirely — a longer label grows its own box and moves the boundary
     with it rather than colliding with anything.

     IT OVERLAPS BOTH CARDS ON PURPOSE, and the number is what keeps that safe. 48px
     wide across a 32px gap leaves (48 - 32) / 2 = 8px lying over each card, against
     the 20px of horizontal padding .journey__step carries. So the overlap falls on
     padding with 12px to spare and can never reach a label, whatever a brief calls
     its steps. That is the same fault the first attempt at these arrows died of —
     it is prevented by arithmetic here, not by a breakpoint tuned to one wording.

     z-index because the next step is a LATER sibling: at the default z-index its
     white background would paint straight over the half of the arrow lying on it. */
  .journey__arrow {
    display: block;
    position: absolute;
    left: 100%;
    top: 50%;
    z-index: 2;
    width: 48px;
    height: auto;
    /* Half the gap, so it centres on the boundary between the two boxes. */
    transform: translate(calc(var(--space-8) / 2), -50%) translateX(-50%);
    color: var(--green-400);
    pointer-events: none;
  }

  /* The ring already shows its own flow, and a straight arrow on a curve reads as a
     mistake. The loop shape opts out. */
  .journey--loop .journey__arrow { display: none; }
}

/* One step per row on small screens: six side by side is unreadable long before
   the container runs out of width. */
@media (max-width: 48rem) {
  .journey__step {
    flex-basis: 100%;
  }
}

/* The ring's centre label. HIDDEN BY DEFAULT, and that default is the important
   half: every other rule for the hub lives inside the 62rem block below, where the
   ring exists. Without this, a viewport under 62rem got a `display: inline`,
   statically positioned span sitting between the step list and the cards — measured
   828x48px of loose text at 900px wide. The ring is the only thing that gives the
   hub a place to be, so it appears only where the ring does. */
.journey-ring__hub { display: none; }

/* -- Journey as a loop ------------------------------------------------- */
/* For a lifecycle that returns to its first step. Only applied where the brief
   asks for it — see journeyShape in solutions.json.

   Steps are placed by angle from --i and --n, so the ring adapts to however many
   steps a brief has rather than assuming six. The dashed ring behind them is what
   actually carries "this continues"; the cards alone would just be scattered.

   ONLY above 62rem. Below that the ring would have to shrink faster than the text
   inside it, and the steps would collide — so it falls back to the same stacked
   list every other page uses, which loses nothing: the numbers still say it is a
   cycle and the copy says so too. */
@media (min-width: 62rem) {
  .journey--loop {
    /* Not flex here: children are absolutely positioned around the centre. */
    display: block;
    position: relative;
    width: min(100%, 40rem);
    aspect-ratio: 1;
    margin-inline: auto;
    /* Radius of the ring the steps sit on. */
    --r: 13.5rem;
  }

  /* The ring itself. Dashed rather than solid so it reads as a path rather than
     as a container border.

     --border-strong DID NOT EXIST. This rule read
     `var(--border-strong, var(--border))`, and since nothing in the file defines
     that token every build silently took the fallback: --cool-200, which measures
     1.23:1 on the page background. The ring was drawn and then invisible — the
     var() fallback made a missing token look like a working default instead of
     failing loudly.

     --green-light is 4.23:1 here. That is the right bar rather than a decorative
     one, because this ring is what says "cycle": without it the six cards are
     scattered, so it carries meaning and owes the 3:1 that graphical objects do.
     Dashed keeps it quiet at that contrast — the stroke covers about half the
     circumference.

     It turns, slowly. A dashed circle rotating is what makes a static diagram read
     as a loop; 90s per revolution is atmosphere, not a spinner. */
  .journey--loop::before {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: calc(var(--r) * 2);
    height: calc(var(--r) * 2);
    transform: translate(-50%, -50%);
    border: 1px dashed var(--green-light);
    border-radius: 50%;
    opacity: 0.55;
    animation: journey-drift 90s linear infinite;
  }

  /* Two travellers on the path, a third of a turn apart, drawn the same way as the
     company page's orbit: dots painted by gradients onto a box 0.4rem larger than
     the ring, so each dot's centre lands on the stroke.

     TWO, NOT ONE, and for a different reason than the quadrant's. Nothing occludes
     this ring, but a single dot on a 20s orbit leaves most of the circle empty most
     of the time; a second dot means one is always in the reader's field of view
     near the cards they are looking at. No border-radius: a round box would clip a
     dot sitting on its own edge in half. */
  .journey--loop::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: calc(var(--r) * 2 + 0.8rem);
    height: calc(var(--r) * 2 + 0.8rem);
    transform: translate(-50%, -50%);
    pointer-events: none;
    background-image:
      radial-gradient(circle at 50% 0.4rem, var(--gold-dark) 0 0.26rem, transparent 0.28rem),
      radial-gradient(
        circle at calc(100% - 0.4rem) 50%,
        var(--green-light) 0 0.22rem,
        transparent 0.24rem
      );
    animation: journey-orbit 20s linear infinite;
  }

  @keyframes journey-drift {
    to { transform: translate(-50%, -50%) rotate(360deg); }
  }

  @keyframes journey-orbit {
    from { transform: translate(-50%, -50%) rotate(0deg); }
    to { transform: translate(-50%, -50%) rotate(360deg); }
  }

  /*
    translate(-50%,-50%) centres the card on the origin, rotate+translateY swings
    it out to its place on the ring, and the final counter-rotation puts the text
    back upright — without it every card would be tilted by its own angle.
  */
  .journey--loop .journey__step {
    position: absolute;
    top: 50%;
    left: 50%;
    flex: none;
    width: max-content;
    transform: translate(-50%, -50%)
      rotate(calc(var(--i) * (360deg / var(--n))))
      translateY(calc(-1 * var(--r)))
      rotate(calc(-1 * var(--i) * (360deg / var(--n))));
    box-shadow: var(--shadow-xs);
    /* OPACITY ONLY for the entrance. The placement above is a four-part transform
       chain, and animating transform here would fight it — the step would fly in
       from the centre of the ring and land wrong. Fading each one in around the
       circle traces the cycle's direction once, which is the thing the diagram is
       trying to say. */
    opacity: 0;
    transition:
      opacity 420ms var(--ease-out),
      border-color var(--dur-card) var(--ease-out),
      box-shadow var(--dur-card) var(--ease-out);
    transition-delay: calc(var(--i) * 110ms);
  }

  .journey--loop[data-visible] .journey__step { opacity: 1; }

  /* Hover is the only interaction here — these are list items, not links, so there
     is nothing to focus. It confirms which step the pointer is over without
     implying the step goes anywhere. */
  .journey--loop .journey__step:hover {
    border-color: var(--green-light);
    box-shadow: var(--shadow-md);
    transition-delay: 0ms;
  }

  .journey--loop .journey__step:hover .journey__num { color: var(--green-light); }

  /* The hub. The ring had a 432px hole in the middle of a 640px block and nothing
     in it, so the diagram read as unfinished rather than as centred. Naming what
     the cycle IS also gives the six steps something to be about. */
  .journey-ring {
    position: relative;
  }

  .journey-ring__hub {
    display: flex;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    flex-direction: column;
    align-items: center;
    gap: var(--space-3);
    width: max-content;
    max-width: 11rem;
    text-align: center;
    pointer-events: none;
  }

  .journey-ring__hub svg {
    width: 1.75rem;
    height: 1.75rem;
    color: var(--green-light);
  }

  .journey-ring__label {
    font-family: var(--font-heading);
    font-weight: 700;
    font-size: var(--text-lg);
    line-height: var(--leading-snug);
    color: var(--text);
  }

  .journey-ring__sub {
    font-size: var(--text-sm);
    line-height: 1.5;
    color: var(--text-secondary);
  }

  @media (prefers-reduced-motion: reduce) {
    .journey--loop::before,
    .journey--loop::after { animation: none; }

    /* The steps still need to arrive — they start at opacity 0 — but all together
       and without the stagger, which is the part this setting is about. */
    .journey--loop .journey__step { transition-delay: 0ms; }
  }
}

/* -- Offer list (capabilities page) ------------------------------------ */
/* Two columns of ticked items. The ticks are the brand gold-400 via
   --gold-dark — see the note on that token about its contrast on white. */
.offer-list {
  display: grid;
  gap: var(--space-2) var(--space-6);
  grid-template-columns: 1fr;
  list-style: none;
  padding: 0;
}

@media (min-width: 34rem) {
  .offer-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.offer-list li {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  font-size: var(--text-sm);
  line-height: 1.6;
  color: var(--text-secondary);
}

.offer-list svg {
  flex-shrink: 0;
  width: 1rem;
  height: 1rem;
  margin-top: 0.2rem;
  color: var(--gold-dark);
}

.offer-title {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: var(--text-body);
  color: var(--text);
}

/* -- Delivery approach ------------------------------------------------- */
/* The steps themselves are plain .card + .label + .card__title/__body, so
   the only thing owned here is the two-column layout and the list reset. */
.approach {
  display: grid;
  gap: clamp(var(--space-8), 4vw, var(--space-12));
  align-items: center;
  grid-template-columns: 1fr;
}

/* Not an even split. The steps are a 2x2 grid now and need the wider half; the
   diagram is capped below, so an equal column would only pad it with air. */
@media (min-width: 62rem) {
  .approach { grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); }
}

/* 2x2 rather than a column of four. Four steps divide evenly, which is the only
   reason this is safe to hardcode — a fifth would leave an orphan, and the count
   comes from capabilities.json. */
.approach__steps {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  /* 16px, matching the gap .stack gives every other card list. */
  gap: var(--space-4);
  list-style: none;
  padding: 0;
}

@media (min-width: 40rem) {
  .approach__steps { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* Centres the diagram in the right-hand column rather than letting it stretch. */
.approach__figure {
  display: grid;
  place-items: center;
}

/* CAPPED AT ITS NATIVE SIZE. The SVG is viewBox="0 0 400 400" and was being drawn
   at 576px in a half-width column — a 1.44x enlargement, which pushed its 12px
   labels out to 17.3px and made the diagram the loudest thing in the section. At
   25rem it renders 1:1, so the type is the size it was designed at. */
.approach__figure .cycle {
  width: 100%;
  max-width: 25rem;
}

/* -- Why Stacklane tiles ----------------------------------------------- */
.why-tile {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-5);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  background-color: var(--white);
  box-shadow: var(--shadow-xs);
}

.why-tile__title {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: var(--text-body);
  line-height: var(--leading-snug);
  color: var(--text);
}

/* -- Newsletter form --------------------------------------------------- */
.newsletter {
  display: flex;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.newsletter__input {
  flex: 1 1 12rem;
  min-height: 44px;
  padding-inline: var(--space-4);
  background-color: var(--white);
  border: 1px solid var(--on-dark-border);
  border-radius: var(--radius-btn);
  color: var(--text);
  font-size: var(--text-sm);
}

.newsletter__input::placeholder { color: var(--text-disabled); }

.newsletter__input:focus {
  outline: none;
  border-color: var(--gold-light);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold-400) 30%, transparent);
}

/* 7. Header & footer ----------------------------------------------------- */
.site-header {
  position: sticky;
  top: 0;
  z-index: var(--z-header);
  background-color: var(--bg);
  border-bottom: 1px solid transparent;
  transition: border-color var(--dur-btn) var(--ease-out),
    box-shadow var(--dur-btn) var(--ease-out);
}

.site-header[data-scrolled] {
  border-bottom-color: var(--border);
  box-shadow: var(--shadow-xs);
}

.site-header__inner {
  display: flex;
  align-items: center;
  gap: var(--space-8);
  min-height: var(--header-h);
}

.brand {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
}

.brand:hover { text-decoration: none; }

/* The supplied lockup is 1920x710 (~2.7:1). Height drives it; the intrinsic
   width/height attributes on the <img> reserve the right box, so swapping in
   different artwork will not shift the header. */
.brand__logo {
  height: 2.625rem; /* 42px */
  width: auto;
}

.brand__logo--footer { height: 4rem; }

@media (max-width: 30rem) {
  .brand__logo { height: 2.25rem; }
  /* Declared explicitly: .brand__logo above has equal specificity and comes
     later in source order, so it would otherwise clobber the footer size. */
  .brand__logo--footer { height: 3rem; }
}

.nav { display: flex; align-items: center; gap: var(--space-1); }

.nav__link {
  /* Never split a label or the number mid-phrase. */
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text);
  background: none;
  border: 0;
  cursor: pointer;
  transition: color var(--dur-btn) var(--ease-out);
}

.nav__link:hover { color: var(--green-light); text-decoration: none; }

/* Active nav uses the gold accent */
.nav__link[aria-current='page'] {
  color: var(--gold-dark);
  font-weight: 600;
  box-shadow: inset 0 -2px 0 var(--gold-light);
  border-radius: 0;
}

.nav__link svg { width: 1rem; height: 1rem; }

.nav__chevron { transition: transform var(--dur-btn) var(--ease-out); }
[aria-expanded='true'] .nav__chevron { transform: rotate(180deg); }

.site-header__actions {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  margin-left: auto;
}

.header__phone {
  /* Never split a label or the number mid-phrase. */
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--green-dark);
}

.header__phone svg { width: 1rem; height: 1rem; color: var(--green-light); }

/* -- Capabilities mega menu -------------------------------------------- */
/* Animated with opacity/visibility rather than display, so it can transition
   and so hidden descendants stay out of the tab order. */
.megamenu {
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  background-color: var(--white);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  box-shadow: var(--shadow-lg);
  padding-block: var(--space-6);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-8px);
  transition:
    opacity var(--dur-btn) var(--ease-out),
    transform var(--dur-btn) var(--ease-out),
    visibility var(--dur-btn) var(--ease-out);
}

.megamenu[data-open] {
  opacity: 1;
  visibility: visible;
  transform: none;
}

/* Units on the left, a cross-navigation rail on the right. */
.megamenu__grid {
  display: grid;
  gap: var(--space-6);
  grid-template-columns: 1fr;
}

@media (min-width: 60rem) {
  .megamenu__grid { grid-template-columns: minmax(0, 2.2fr) minmax(0, 1fr); }
}

/* Left column: the card grid, then the view-all link. Flex column so neither
   can be stretched — a flex item does not grow without flex-grow. */
.megamenu__main {
  display: flex;
  flex-direction: column;
  /* Cards carry 16px of inline padding for their hover background. Pulling the
     column left by that amount puts the card CONTENT on the container's edge,
     so the icon and "WE TRANSFORM." line up with the logo above. */
  margin-inline-start: calc(var(--space-4) * -1);
}

/* One sentence at the top of a panel saying what the section is, before the
   reader works through the links. It sits on the container edge, which is also
   where the cards' content lands once .megamenu__main's negative margin is
   applied — so the two line up without either needing an indent. */
.megamenu__intro {
  margin: 0 0 var(--space-5);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.megamenu__units {
  display: grid;
  gap: var(--space-2);
  grid-template-columns: 1fr;
  /* Belt and braces: never distribute surplus height between the card rows. */
  align-content: start;
}

/* Industry list in the Solutions rail: seven quiet rows, deliberately lighter
   than .megamenu__link so the column reads as a list rather than seven
   competing calls to action. */
.megamenu__industries {
  display: grid;
  gap: 0.125rem;
  margin-bottom: var(--space-3);
}

.megamenu__industry {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  color: var(--text);
  transition:
    background-color var(--dur-btn) var(--ease-out),
    color var(--dur-btn) var(--ease-out);
}

.megamenu__industry:hover {
  background-color: var(--white);
  color: var(--green-dark);
  text-decoration: none;
}

.megamenu__industry[aria-current='page'] {
  background-color: var(--white);
  color: var(--green-dark);
  font-weight: 600;
  box-shadow: inset 3px 0 0 var(--gold-light);
}

.megamenu__industry-icon {
  display: grid;
  flex: none;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  color: var(--green-600);
}

.megamenu__industry-icon svg { width: 1rem; height: 1rem; }

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

/* "View all capabilities" — below the cards, replacing the rail entry.
   Inline padding matches the cards so its text sits on the same left edge. */
.megamenu__all {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  gap: var(--space-2);
  margin-top: var(--space-3);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-btn);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--green-dark);
}

.megamenu__all:hover {
  background-color: var(--green-tint);
  color: var(--green-dark);
  text-decoration: none;
}

.megamenu__all svg {
  width: 1rem;
  height: 1rem;
  color: var(--gold-dark);
  transition: transform var(--dur-card) var(--ease-out);
}

.megamenu__all:hover svg { transform: translateX(3px); }

@media (min-width: 42rem) {
  .megamenu__units { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.megamenu__item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  /* align-content: start is load-bearing. Cards stretch to the height of the
     tallest one in their row, and this grid's rows are implicit and auto-sized
     — so the default (normal, i.e. stretch) would divide the surplus height
     equally between label, name, and tagline, opening a gap between each.
     Pinning to start keeps the text tight and collects the slack at the
     bottom instead. */
  align-content: start;
  gap: 0 var(--space-3);
  /* Extra bottom padding gives the description room to breathe. */
  padding: var(--space-3) var(--space-4) var(--space-4);
  border-radius: var(--radius-btn);
  border: 1px solid transparent;
  color: var(--text);
  transition:
    background-color var(--dur-btn) var(--ease-out),
    border-color var(--dur-btn) var(--ease-out);
}

/* A card with an icon but no eyebrow label — the solutions, where "SOLUTION"
   above every name would be noise. .megamenu__icon spans three rows to sit
   alongside label + name + description; with only two of those, span 3 opens an
   empty implicit row and the icon drifts down as it centres over it.

   Two classes, so this beats the bare .megamenu__icon rule on specificity and
   does not depend on source order. */
.megamenu__item--no-label .megamenu__icon {
  grid-row: span 2;
}

.megamenu__item:hover {
  background-color: var(--bg-section);
  border-color: var(--border);
  text-decoration: none;
  color: var(--text);
}

/* Current section marked with the gold rail, matching active nav elsewhere. */
.megamenu__item[aria-current='page'] {
  background-color: var(--green-tint);
  border-color: var(--green-100);
  box-shadow: inset 3px 0 0 var(--gold-light);
}

.megamenu__icon {
  /* Spans the label/name/description rows, aligned to the first line. */
  grid-row: span 3;
  align-self: start;
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  margin-top: 0.125rem;
  border-radius: var(--radius-sm);
  /* The same pairing as .icon-chip on the cards — green-50 behind a green-500
     glyph, 3.77:1 — so a menu icon and a card icon read as one component at two
     sizes rather than two different ideas. */
  background-color: var(--green-50);
  color: var(--green-500);
}

.megamenu__icon svg { width: 1.125rem; height: 1.125rem; }

.megamenu__item .label {
  font-size: 0.6875rem;
  line-height: 1.2;
}

.megamenu__name {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: var(--text-body);
  line-height: 1.25;
  color: var(--text);
  margin-top: var(--space-1);
}

.megamenu__desc {
  font-size: var(--text-xs);
  line-height: 1.45;
  color: var(--text-secondary);
  margin-top: var(--space-2);
}

/* -- Rail -------------------------------------------------------------- */
.megamenu__rail {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-4);
  background-color: var(--bg-section);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
}

/* Column heading in a mega menu panel. Gold, like every other eyebrow on the
   site — the grey it used to be made the two panels look like different
   components once Solutions gained labelled columns of its own. */
.megamenu__col-label {
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  /* light surface (the mega menu panel is white) */
  color: var(--eyebrow-text);
}

/* .megamenu__main is pulled 16px left so that card CONTENT lands on the
   container edge. A label has no card padding of its own, so it needs that 16px
   handed back or it hangs off the edge. The rail is a padded flex column with
   its own gap, so it needs neither this nor the margin. */
.megamenu__main > .megamenu__col-label {
  padding-inline-start: var(--space-4);
  margin-bottom: var(--space-3);
}

.megamenu__links {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

/* The Company panel's two-peer-column layout has been removed — .megamenu__col,
   .megamenu__grid--split, .megamenu__item--plain and .megamenu__all--cell. That
   panel is a main column plus a rail like the other two now, so it uses
   .megamenu__main and .megamenu__rail and needs none of them. Everything below is
   still live and shared by all three panels. */

.megamenu__link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text);
}

.megamenu__link:hover {
  background-color: var(--white);
  color: var(--green-dark);
  text-decoration: none;
}

.megamenu__link svg {
  width: 1rem;
  height: 1rem;
  color: var(--gold-dark);
  opacity: 0;
  transition: opacity var(--dur-btn) var(--ease-out),
    transform var(--dur-card) var(--ease-out);
}

.megamenu__link:hover svg { opacity: 1; transform: translateX(3px); }

/* Featured solution card in the rail */
.megamenu__featured {
  display: block;
  padding: var(--space-4);
  border-radius: var(--radius-btn);
  background-color: var(--green-dark);
  color: var(--on-dark);
}

.megamenu__featured .label {
  color: var(--gold-light);
  font-size: 0.6875rem;
  line-height: 1.2;
}

.megamenu__featured-name {
  display: block;
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: var(--text-sm);
  line-height: 1.25;
  color: var(--white);
  margin-top: var(--space-1);
}

.megamenu__featured-copy {
  display: block;
  font-size: var(--text-xs);
  line-height: 1.45;
  color: var(--on-dark-muted);
  margin-top: var(--space-2);
}

/* Trigger for the current section — visual only. aria-current is not used on
   the button because a disclosure button does not navigate; the landing-page
   link inside the panel carries it instead. */
.nav__link[data-section-current] {
  color: var(--gold-text);
  font-weight: 600;
  box-shadow: inset 0 -2px 0 var(--gold-light);
  border-radius: 0;
}

/* Without JS the disclosure button cannot open, so swap it for a plain link. */
.megamenu-fallback { display: none; }

/* -- Responsive header ------------------------------------------------- */
.site-header__desktop {
  display: flex;
  align-items: center;
  gap: var(--space-8);
  margin-left: auto;
}

.site-header__menu-btn {
  display: none;
  margin-left: auto;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--radius-sm);
  color: var(--green-dark);
}

.site-header__menu-btn:hover { background-color: var(--bg-section); }
.site-header__menu-btn svg { width: 1.375rem; height: 1.375rem; }

.mobile-nav {
  display: none;
  border-top: 1px solid var(--border);
  background-color: var(--bg);
  padding-block: var(--space-6) var(--space-8);
}

.mobile-nav[data-open] {
  display: block;
  max-height: calc(100dvh - var(--header-h));
  overflow-y: auto;
  overscroll-behavior: contain;
}

.mobile-nav .nav {
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-1);
}

.mobile-nav .nav__link {
  padding: var(--space-3);
  font-size: var(--text-body);
  justify-content: flex-start;
}

.mobile-nav__group {
  border-top: 1px solid var(--border);
  margin-top: var(--space-4);
  padding-top: var(--space-4);
}

/* 72rem, raised from 68rem when the type scale went up a step: at 1088-1140px
   the 15px nav plus the phone number and CTA no longer fit on one line, and
   "Contact Us" and "0800 345 720" wrapped mid-label. The desktop nav now waits
   until there is room for it. The footer's own 68rem query below is unrelated. */
@media (max-width: 71.99rem) {
  .site-header__desktop { display: none; }
  .site-header__menu-btn { display: inline-flex; }
}

/* -- Footer ------------------------------------------------------------ */
.site-footer {
  background-color: var(--green-deep);
  color: var(--on-dark-muted);
  padding-block: var(--space-20) var(--space-8);
}

.site-footer a { color: var(--on-dark-muted); }
.site-footer a:hover { color: var(--white); text-decoration: none; }


/* Gold, not the muted body colour the lines themselves use — an icon the same
   colour as its label reads as a bullet, and the point of these three is to tell
   the lines apart at a glance. --gold-light is 7.10:1 on --green-deep, the same
   accent the social marks take on hover.

   `flex: none` because .cluster is a flex row: without it the SVG shrinks and the
   longest line, the email address, squeezes its own icon narrower than the other
   two. */
.site-footer__icon {
  flex: none;
  /* 14px against 15px text. At 18px the icon was taller than the line it sat
     beside and read as the louder of the two; a hair under the type size lets it
     mark the line without competing with it. */
  width: 0.875rem;
  height: 0.875rem;
  color: var(--gold-light);
}

.site-footer__grid {
  display: grid;
  gap: var(--space-12);
  grid-template-columns: 1fr;
}

@media (min-width: 48rem) {
  .site-footer__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 68rem) {
  .site-footer__grid {
    grid-template-columns: 1.2fr 1fr 1fr 1.4fr;
    gap: var(--space-10);
  }
}

.site-footer__heading {
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--white);
  margin-bottom: var(--space-5);
}

.site-footer__links {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  font-size: var(--text-sm);
}

.site-footer__bottom {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  margin-top: var(--space-16);
  padding-top: var(--space-6);
  border-top: 1px solid var(--on-dark-border);
  font-size: var(--text-sm);
}

.site-footer__social { display: flex; gap: var(--space-4); }

.site-footer__social a {
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: var(--radius-full);
  border: 1px solid var(--on-dark-border);
}

.site-footer__social a:hover {
  border-color: var(--gold-light);
  color: var(--gold-light);
}

.site-footer__social svg { width: 1.125rem; height: 1.125rem; }

/* 8. Utilities --------------------------------------------------------- */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  top: var(--space-2);
  left: var(--space-2);
  z-index: var(--z-overlay);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-btn);
  background-color: var(--green-dark);
  color: var(--white);
  font-size: var(--text-sm);
  font-weight: 600;
  transform: translateY(-150%);
  transition: transform var(--dur-btn) var(--ease-out);
}

.skip-link:focus { transform: translateY(0); text-decoration: none; }

.text-center { text-align: center; }
.mx-auto    { margin-inline: auto; }
.scroll-x   { overflow-x: auto; overscroll-behavior-x: contain; }

/* 9. Motion ------------------------------------------------------------- */
/* Fade up 40px over 0.6s. The script in BaseLayout adds [data-visible]. */
[data-reveal] {
  opacity: 0;
  transform: translateY(40px);
  transition:
    opacity var(--dur-reveal) var(--ease-out),
    transform var(--dur-reveal) var(--ease-out);
}

[data-reveal][data-visible] {
  opacity: 1;
  transform: none;
}

/* Stagger children of a revealed group */
[data-reveal-delay='1'] { transition-delay: 80ms; }
[data-reveal-delay='2'] { transition-delay: 160ms; }
[data-reveal-delay='3'] { transition-delay: 240ms; }

/* -- Hero entrance ------------------------------------------------------ */
/* A focus-in: each block resolves out of a blur instead of sliding. Measured off
   linear.app rather than eyeballed — its hero animates opacity 0 -> 1 with
   filter blur(10px) -> 0 over 1000ms on cubic-bezier(0.25, 0.1, 0.25, 1), with
   no transform at all, staggered ~33ms between the lines of one headline and
   ~100ms between groups. Those are the numbers used here.

   NO TRANSFORM, deliberately. Adding the site's usual 40px rise on top made it
   read as an ordinary fade-up that happened to be blurry; the blur alone is what
   makes it feel like the type is coming into focus. The scroll-reveal below the
   fold still slides, so the hero reads as the accent rather than the rule.

   The state is driven from :root by the script at the top of index.astro, not
   from a class on the hero, so the from-state is set before the hero is parsed.
   See that script for why, and for the no-JS and failsafe paths. */
:root[data-hero-anim]:not([data-hero-in]) .hero__inner > * {
  opacity: 0;
  filter: blur(10px);
}

/* The footage resolves too, so the whole scene comes into focus rather than
   sharp footage appearing behind blurred type.

   IT FOLLOWS THE COPY. It used to start at 0ms, ahead of everything, which meant
   the type and the footage were both soft over the same stretch and the hero just
   looked out of focus for a second rather than looking like anything was
   arriving. The delay below separates them: the copy has the first ~480ms to
   itself, and the last ~610ms of the backdrop resolving plays alone.

   A bigger radius than the copy's 10px because it is scaled to the element: 18px
   across a 1400px-wide frame is roughly what 10px is across a headline. Blurring
   a full-bleed video is the expensive part of this animation, which is the other
   reason it runs once, on load, and is never re-triggered. */
:root[data-hero-anim]:not([data-hero-in]) .hero__video {
  opacity: 0;
  --hero-video-blur: 18px;
}

:root[data-hero-in] .hero__inner > * {
  /* `backwards`, not `both`: backwards covers the delay with the from-state, and
     then the fill ENDS — so no blur(0px) is left applied afterwards. A filter
     that stays put keeps the element on its own layer and softens text
     antialiasing, which is a real cost to pay for a finished animation. */
  animation: hero-in 1000ms cubic-bezier(0.25, 0.1, 0.25, 1) backwards;
}

/* Eyebrow, headline, lead, then the buttons. 90ms apart, matching Linear's
   group cadence rather than its 33ms per-line one — these are four separate
   blocks, not four lines of one sentence.

   Children 4 and 5 are the dark and light CTA pairs: alternatives, only ever one
   displayed, so they share a delay instead of following each other. */
:root[data-hero-in] .hero__inner > :nth-child(1) { animation-delay: 120ms; }
:root[data-hero-in] .hero__inner > :nth-child(2) { animation-delay: 210ms; }
:root[data-hero-in] .hero__inner > :nth-child(3) { animation-delay: 300ms; }
:root[data-hero-in] .hero__inner > :nth-child(4),
:root[data-hero-in] .hero__inner > :nth-child(5) { animation-delay: 390ms; }

/* 600ms: the eyebrow and headline are past their midpoint by then, so the footage
   starts against type that is already legible. Kept under the copy's 1390ms
   finish on purpose — waiting for the copy to fully land put the hero's total at
   nearly 2.8s, and behind the first-visit curtain that is a long time to look at
   a flat green panel. */
:root[data-hero-in] .hero__video {
  animation: hero-video-in 1400ms cubic-bezier(0.25, 0.1, 0.25, 1) 600ms backwards;
}

@keyframes hero-in {
  from {
    opacity: 0;
    filter: blur(10px);
  }
  to {
    opacity: 1;
    filter: blur(0);
  }
}

/* Only the blur variable and opacity — the theme's own brightness/saturate stay
   in the filter list they were declared in and are never touched here. */
@keyframes hero-video-in {
  from {
    opacity: 0;
    --hero-video-blur: 18px;
  }
  to {
    opacity: 1;
    --hero-video-blur: 0px;
  }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  [data-reveal] { opacity: 1; transform: none; }

  /* The blanket rule above collapses every animation to 0.01ms, which would let
     the hero land in one jump — but only because `backwards` fill happens to
     resolve that way. Stated outright instead of relied on: no animation, no
     from-state, nothing hidden. */
  :root[data-hero-anim] .hero__inner > * {
    opacity: 1;
    filter: none;
    animation: none;
  }

  /* The video's blur is reset through the variable, NOT with `filter: none` —
     that would also strip the brightness and saturate the footage relies on. */
  :root[data-hero-anim] .hero__video {
    opacity: 1;
    --hero-video-blur: 0px;
    animation: none;
  }
}

@media print {
  .site-header,
  .site-footer,
  .cta { display: none; }

  body { background: var(--neutral-0); color: var(--neutral-1000); }
}
