/* ===========================================================================
   TYPEFACE — SELF-HOSTED

   Plus Jakarta Sans, latin subset, as ONE variable font file covering every
   weight the site uses. Google was serving the same 27KB file five times over
   under five @font-face rules; only the CSS wrapper differed.

   Self-hosted because a render-blocking stylesheet on a third-party origin
   costs a DNS lookup, a TLS handshake and a round trip before the first
   paragraph can paint, and because it removes fonts.googleapis.com and
   fonts.gstatic.com from the Content-Security-Policy entirely.

   font-display: swap keeps text readable while it loads; the preload in the
   head means the swap window is normally zero. Plus Jakarta Sans is OFL
   licensed, so self-hosting is permitted.
   =========================================================================== */
@font-face {
  font-family: 'Plus Jakarta Sans';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('/assets/fonts/plus-jakarta-sans-variable.woff2') format('woff2-variations'),
       url('/assets/fonts/plus-jakarta-sans-variable.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                 U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
                 U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ==========================================================================
   EQVANTO — DESIGN SYSTEM
   --------------------------------------------------------------------------
   Editorial, warm and confident: a serif display face against a precise sans,
   deep teal and terracotta on warm paper, and depth built from layered light
   rather than from decoration.

   The motion is deliberate. Everything moves on the same two easings, entrances
   are staggered rather than simultaneous, and every animation is cancelled under
   prefers-reduced-motion. Nothing here moves for its own sake.

   Contents
     1.  Tokens
     2.  Reset & base
     3.  Typography
     4.  Layout & rhythm
     5.  Motion primitives
     6.  Buttons
     7.  Header & navigation
     8.  Mobile navigation
     9.  Hero
     10. Content typography
     11. Cards & components
     12. Forms
     13. Sticky lead capture
     14. Footer
     15. Utilities
     16. Legal placeholders
     17. Reduced motion & print
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. TOKENS
   -------------------------------------------------------------------------- */
:root {
  /* ---------------------------------------------------------------------
     PALETTE — two colours and a set of neutrals, nothing more.

     The previous scheme ran navy, blue AND gold at equal strength, which is
     what made every band shout at the one above it. A designer collapses that:
     one ink for everything structural, one blue that only ever means "this is
     interactive or this is the point", and warm-grey paper so the cool blue has
     something to sit against. Warm neutrals under a cool accent is the whole
     trick — an all-cool page reads as an untouched framework default.
     --------------------------------------------------------------------- */

  /* Ink — #000E22, sampled directly from the logo sheet: it is the field colour
     of the dark app-icon tile. Text belongs to the palette rather than sitting
     on top of it, and the footer closes the page on the brand's own black. */
  --color-ink-900: #000E22;
  --color-ink-800: #0B2141;
  --color-ink-700: #3B4A60;   /* body copy — 9.2:1 on paper */
  --color-ink-500: #5C6A82;   /* darkened so it clears AA on the band's lightest stop too */   /* muted copy — 4.9:1 on paper, 4.7:1 on the band */
  --color-ink-300: #D6DDE8;   /* hairlines: the only border weight in the system */
  --color-ink-100: #EAEFF6;

  /* Blue — #0051FC, sampled from the logo: the field of the blue app-icon tile
     and the chevron in the mark. Not a colour anyone picked; it is the brand's,
     and the rest of the scale is built around it. It measures 5.9:1 on paper,
     so it carries small text and takes white on top at the same ratio — rare
     for a blue this saturated, and the reason it can be the button fill, the
     link colour and the accent all at once. */
  --color-primary-900: #001034;
  --color-primary-800: #001C55;
  --color-primary-700: #0033A0;   /* 10.1:1 on paper — safe for small text */
  --color-primary-600: #0041CE;
  --color-primary-500: #0051FC;   /* the accent — 6.4:1 on paper */
  --color-primary-400: #4D85FD;
  --color-primary-300: #99B8FE;
  --color-primary-75:  #E8EFFF;   /* gradient stop - muted text clears AA on it */
  --color-primary-100: #D9E5FF;
  --color-primary-50:  #EFF4FF;

  /* The amber is gone. It was a reasonable second colour while the brand had no
     colours of its own, but the logo arrived carrying exactly two — this blue
     and that ink — and a third hue invented to sit beside them is the kind of
     thing that makes a site and its logo look like they came from different
     places. The accent names stay pointed at the blue so no rule breaks, and so
     the palette cannot quietly regrow a colour the brand does not have. */
  --color-accent-700: var(--color-primary-700);
  --color-accent-600: var(--color-primary-600);
  --color-accent-500: var(--color-primary-500);
  --color-accent-400: var(--color-primary-400);
  --color-accent-300: var(--color-primary-300);
  --color-accent-100: var(--color-primary-100);
  --color-accent-50:  var(--color-primary-50);
  --color-accent-text: var(--color-primary-700);

  --color-sun-600: var(--color-accent-600);
  --color-sun-500: var(--color-accent-500);
  --color-sun-300: var(--color-accent-300);
  --color-sun-100: var(--color-accent-100);

  --color-sky-600: var(--color-primary-600);
  --color-sky-500: var(--color-primary-500);
  --color-sky-300: var(--color-primary-300);
  --color-sky-100: var(--color-primary-100);

  --color-gold-500: var(--color-accent-500);
  --color-gold-100: var(--color-accent-100);

  /* Surfaces — white for content, a warm grey for alternating bands. Two
     surfaces total. Every extra tint was a place for the eye to trip. */
  --color-paper:     #FFFFFF;
  --color-bg:        var(--color-paper);
  --color-bg-alt:    #F4F7FC;   /* faintest brand blue */
  --color-bg-tint:   #FAF7F2;   /* warm, to break the run of cool */
  --color-bg-sky:    #EFF4FF;
  --color-bg-panel:  #FFFFFF;

  /* Hairlines are drawn with --color-ink-300. --color-stroke survives only as a
     compatibility name and now points at the field border, so that if anything
     ever draws a real control boundary with it, that boundary meets the 3:1
     minimum by construction rather than by luck. */
  --color-stroke: var(--color-border-field);
  --color-border-field: #7F8CA3;   /* 3.3:1 — meets the UI-boundary minimum */
  --color-white: #FFFFFF;

  --color-success:    #0A7346;
  --color-success-bg: #E6F5EE;
  --color-error:      #B3261E;
  --color-error-bg:   #FCEBEA;
  --color-overlay:    rgba(0, 14, 34, .58);

  /* ---------------------------------------------------------------------
     TYPE — one family, hierarchy from size and weight alone.
     --------------------------------------------------------------------- */
  --font-display: 'Plus Jakarta Sans', 'Segoe UI', system-ui, -apple-system, sans-serif;
  --font-body: 'Plus Jakarta Sans', 'Segoe UI', system-ui, -apple-system, sans-serif;

  /* Raised across the board. Body copy was 17px and 14% of all the text on the
     site was set at 15px - card descriptions, form labels, spec lists, list
     items - which is small for reading on a laptop and smaller than it looks
     in a design tool. Every step moved together so the relationships between
     them are unchanged; only the starting point is bigger.

     Body at 19px also fixes something else: the reading column was running to
     88 characters a line, past the point where a line is comfortable to
     follow. The same column at 19px holds about 79. */
  --fs-xs:   0.875rem;      /* 14px - tags, meta, form notes */
  --fs-sm:   1.0625rem;     /* 17px - card copy, labels, lists */
  --fs-base: 1.1875rem;     /* 19px - body */
  --fs-lg:   1.375rem;      /* 22px - card titles, h3 */
  --fs-xl:   1.625rem;      /* 26px */
  --fs-2xl:  2.25rem;
  --fs-3xl:  2.75rem;
  --fs-4xl:  3.75rem;
  --fs-5xl:  4.75rem;

  /* Large text in a geometric sans falls apart without negative tracking; the
     bigger it gets, the tighter it needs to be. This is the single detail that
     most separates typeset headlines from default ones. */
  --tracking-tight:  -0.035em;
  --tracking-snug:   -0.022em;
  --tracking-normal: -0.011em;
  --tracking-wide:    0.02em;

  --lh-tight:   1.05;
  --lh-snug:    1.18;
  --lh-normal:  1.65;
  --lh-relaxed: 1.75;

  /* Space — 8pt scale */
  --sp-1: 4px;   --sp-2: 8px;   --sp-3: 12px;  --sp-4: 16px;
  --sp-5: 24px;  --sp-6: 32px;  --sp-7: 48px;  --sp-8: 64px;
  --sp-9: 80px;  --sp-10: 96px; --sp-11: 128px; --sp-12: 160px;

  /* Radii — small and consistent. Large radii read as friendly-generic. */
  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-xl: 16px;
  --radius-full: 999px;

  /* Elevation — near-invisible, and only where something genuinely floats
     (menus, the header once it detaches). Flat surfaces are separated by
     hairlines and whitespace instead. */
  --shadow-soft-sm: 0 1px 2px rgba(13, 21, 32, .04);
  --shadow-soft:    0 1px 3px rgba(13, 21, 32, .05), 0 6px 16px rgba(13, 21, 32, .05);
  --shadow-soft-lg: 0 2px 6px rgba(13, 21, 32, .05), 0 16px 40px rgba(13, 21, 32, .08);
  --shadow-soft-xl: 0 4px 12px rgba(13, 21, 32, .06), 0 28px 64px rgba(13, 21, 32, .10);

  /* The offset "sticker" shadow is retired. The names stay so no rule breaks;
     they simply draw nothing now. Depth comes from hairlines and space. */
  --shadow-hard-sm:    none;
  --shadow-hard:       none;
  --shadow-hard-lg:    none;
  --shadow-hard-teal:  none;
  --shadow-hard-coral: none;

  /* Motion — one pair of easings across the whole system */
  --ease-out:  cubic-bezier(.22, .61, .36, 1);
  --ease-snap: cubic-bezier(.34, 1.56, .64, 1);
  --duration-fast: 140ms;
  --duration-base: 260ms;
  --duration-slow: 460ms;

  --color-focus: var(--color-primary-500);

  /* Reading measure. The ch unit is sized on the "0" glyph, which in this
     family is far wider than the average lowercase letter — 68ch measured out
     at over 1000px, roughly 110 characters a line. These are in rem so the
     result is the width actually intended. */
  /* 42rem, measured rather than estimated. 47rem was set on the assumption that
     bigger type would shorten the line; measuring a real line in the real font
     said otherwise - it was still running to 88 characters, the same as before,
     which is well past the 65-75 a reader follows comfortably. 42rem at 19px is
     about 78. The column is 32px narrower than it was at the old size, so the
     band down the right barely moves, and the line is ten characters shorter. */
  --measure:         42rem;
  --measure-lede:    44rem;   /* the opening paragraph, set larger */
  --measure-heading: 20rem;

  /* ---------------------------------------------------------------------
     GRADIENTS
     Depth, not decoration. These are long, shallow and directional: a page
     that lifts slightly from bottom to top, a hero band that resolves into
     the paper below it, a button with a light source. What they are NOT is
     the radial colour blobs that used to sit behind the hero and the footer -
     those fill space without meaning anything and are the clearest visual
     signature of a generated page.

     Every stop is inside the palette, and the lightest stop of the hero band
     is chosen so muted text still clears AA against it.
     --------------------------------------------------------------------- */
  --gradient-page:  linear-gradient(180deg, #F4F8FF 0%, var(--color-paper) 38%);
  --gradient-band:  linear-gradient(168deg, var(--color-primary-75) 0%, var(--color-primary-50) 58%, #F7FAFF 100%);
  --gradient-brand: linear-gradient(180deg, #0B5CFF 0%, var(--color-primary-500) 55%, #0043D6 100%);
  --gradient-ink:   linear-gradient(180deg, #041428 0%, var(--color-ink-900) 55%, #00081A 100%);

  /* The editorial grid: the heading column's width and the gap beside it.
     Loose prose is indented by exactly these two, so a paragraph that belongs
     to no section still lines up with the prose that does. Kept as tokens
     because they were duplicated as literals, drifted apart when the column was
     widened, and left three different left edges on one page. */
  --block-col: 20rem;
  --block-gap: var(--sp-8);

  --container-max: 1200px;
  --container-narrow: 720px;
  --container-wide: 1400px;
  --header-height: 76px;
  --header-height-scrolled: 62px;
  --sticky-mobile-bar-height: 64px;
}

/* --------------------------------------------------------------------------
   2. RESET & BASE
   -------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

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

/* A very fine grain over the whole page. It is a single inline SVG turbulence
   filter — no image request — at an opacity you notice only as texture. Flat
   colour fills are what make a page look printed from a template; a little
   material noise is what makes it feel made. */
body::before {
  content: ''; position: fixed; inset: 0; z-index: 1; pointer-events: none;
  opacity: .028;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

body {
  background-image: var(--gradient-page);
  background-repeat: no-repeat;
  background-attachment: fixed;
  font-family: var(--font-body);
  font-size: var(--fs-base);
  line-height: var(--lh-normal);
  color: var(--color-ink-800);
  background: var(--color-bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-wrap: break-word;
  overflow-x: hidden;
}

img, picture, svg, video, canvas { display: block; max-width: 100%; height: auto; }
/* A neutral block behind a photograph, so a slow-loading image reserves its
   space visibly rather than flashing white. Excluded: SVGs drawn with
   transparency — a logo is not a photograph, and the tint showed as a grey
   box around the brand mark in the header and footer. */
img:not(.no-plate) { background: var(--color-ink-100); }
/* Brand marks and icons are transparent artwork, not photographs: the plate
   would draw a grey box around them. This used to key off the .svg extension,
   which stopped working the moment the real logo arrived as a PNG. */
.logo img, .footer-brand img { background: none; }
input, button, textarea, select { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: none; padding: 0; }
a { color: inherit; text-decoration: none; }
ul, ol { padding-left: 1.25em; }
table { border-collapse: collapse; width: 100%; }
::selection { background: var(--color-accent-300); color: var(--color-ink-900); }

/* A single, consistent focus ring everywhere. */
:focus-visible {
  outline: 3px solid var(--color-focus);
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 999;
  background: var(--color-ink-900); color: #fff;
  padding: var(--sp-3) var(--sp-5);
  border-radius: 0 0 var(--radius-md) 0;
  font-weight: 600;
}
.skip-link:focus { left: 0; }

main:focus { outline: none; }
main:focus-visible { outline: 3px solid var(--color-focus); outline-offset: -3px; }

/* --------------------------------------------------------------------------
   3. TYPOGRAPHY
   Fluid from a phone to a desktop with no breakpoints to maintain, and
   text-wrap:balance so long headlines never strand a single word.
   -------------------------------------------------------------------------- */
h1, h2, h3, h4 {
  font-family: var(--font-display);
  color: var(--color-ink-900);
  letter-spacing: var(--tracking-snug);
  text-wrap: balance;
}

/* Weight carries the hierarchy, so the family never has to change. 800 at
   display size, 700 for section heads, 600 inside cards — three steps, and the
   tracking tightens as the size grows because a geometric sans set large and
   loose looks like nobody typeset it. */
h1 {
  font-size: clamp(2.35rem, 1.15rem + 4.4vw, var(--fs-4xl));
  font-weight: 800;
  line-height: var(--lh-tight);
  letter-spacing: var(--tracking-tight);
}
h2 {
  /* One size for every h2 in the page body. Section headings inside a .block
     had their own larger clamp from 1040px up, so an h2 was 37.6px inside a
     section and 32px outside one - on the same page, at the same level. The
     larger of the two won: a section heading should be prominent, and the
     handful of h2s that sit outside a block are no less important. */
  font-size: clamp(1.8rem, 1.15rem + 2.1vw, 2.35rem);
  font-weight: 700;
  line-height: var(--lh-snug);
  letter-spacing: var(--tracking-snug);
  margin-top: var(--sp-9);
  margin-bottom: var(--sp-4);
}
h3 {
  font-size: clamp(1.15rem, 1.05rem + 0.5vw, var(--fs-lg));
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: var(--tracking-normal);
  margin-top: var(--sp-6);
  margin-bottom: var(--sp-3);
}
h4 {
  font-size: var(--fs-base);
  font-weight: 700;
  line-height: 1.35;
  letter-spacing: var(--tracking-normal);
}

p { margin-bottom: var(--sp-4); color: var(--color-ink-700); text-wrap: pretty; }
p:last-child { margin-bottom: 0; }
strong { color: var(--color-ink-900); font-weight: 700; }

/* Eyebrow — a plain label, not a decoration.

   It used to be uppercase at 0.14em tracking in the accent colour, which is one
   of the most recognisable tells of a generated layout: real editorial design
   reserves that treatment for a masthead, not for a caption above every single
   heading on a site. What remains is a quiet piece of metadata that answers
   "what kind of page am I on" and then gets out of the way. */
.eyebrow {
  display: block;
  font-size: var(--fs-sm);
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--color-ink-500);
  margin-bottom: var(--sp-3);
}

/* The paragraphs that open a page.

   Every one of them, not just :first-child. An over-long opening paragraph is
   split at its sentence boundaries into several elements that all carry this
   class, so sizing only the first put a visible step change in the middle of
   what the reader sees as one continuous thought. The run is one statement and
   is set as one.

   No font-size here any more. A page opener used to be set two steps above a
   section opener wearing the identical class, which the reader has no way to
   account for. It still reads as an opening statement: it is darker, wider,
   and it is the first thing under the H1. */
.content-body > .section-lede {
  line-height: 1.42;
  letter-spacing: var(--tracking-snug);
  color: var(--color-ink-800);
  max-width: 52rem;
  margin-bottom: var(--sp-6);
}

/* The opening paragraph of a page or a section. Larger than body copy and a
   touch tighter, so the eye is given somewhere to land after the headline.

   ONE size, and it has to be. This class was rendering at three: 25.6px as a
   page opener, 20.8px as a section opener, and 19.2px wherever one landed
   inside a hero, where `.hero p` outranks it. Same class, same job, three
   sizes, which is precisely what "the font sizes are inconsistent" looks like
   from the reader's side of the screen. */
.section-lede {
  /* 1.5rem, not 1.45: at 1.45 a lede sat 1.2px off an h3, which is close
     enough to read as a mistake rather than a distinction. */
  font-size: clamp(1.25rem, 1.05rem + 0.7vw, 1.5rem);
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: var(--tracking-normal);
  color: var(--color-ink-700);
  max-width: var(--measure-lede);
  margin-bottom: var(--sp-6);
}

/* --------------------------------------------------------------------------
   4. LAYOUT & RHYTHM
   -------------------------------------------------------------------------- */
.container {
  width: 100%;
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--sp-5);
}
@media (min-width: 768px) { .container { padding-inline: var(--sp-6); } }

/* Sections are given room. At clamp(80px, 7vw, 128px) a long page read as one
   continuous column of text with no sense of where one argument ended and the
   next began; whitespace is the only thing separating them, so there has to be
   enough of it to register as a break. */
.section { padding-block: clamp(var(--sp-10), 9vw, var(--sp-12)); position: relative; }
/* The lead-capture band gets a tinted surface and a soft top edge, so the page
   has a rhythm of light and lighter rather than one flat run of paper. */
.lead-form-section::after {
  content: ''; position: absolute; left: 0; right: 0; top: 0; height: 1px;
  background: linear-gradient(90deg, transparent, var(--color-primary-300), transparent);
}
/* The hero already provides the top breathing room. */
.hero + .section, .hero--compact + .section { padding-top: clamp(var(--sp-7), 5vw, var(--sp-9)); }

.grid { display: grid; gap: var(--sp-5); align-items: stretch; }
/* Every card in a row is the same height, and each card is a column with its
   footer pushed to the bottom — so headings start on one line and the calls to
   action finish on one line, whatever the copy length. */
.grid > .card,
.grid > .index-card,
.grid > .card--industry,
.grid > .card--editorial { height: 100%; }
.grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
@media (min-width: 640px) {
  .grid-2 { grid-template-columns: repeat(2, 1fr); }
  .grid-4 { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 960px) {
  .grid-3 { grid-template-columns: repeat(3, 1fr); }
  .grid-4 { grid-template-columns: repeat(4, 1fr); }
}
/* A last row that stops short is the white space people actually notice.

   Five cards in three columns leaves the third slot of the second row empty -
   a card-shaped hole roughly 390px wide, on /blog/, /industries/ and
   /services/. The eye reads it as a card that failed to load, because a grid
   sets the expectation that a row finishes.

   The row is re-cut rather than the cards re-sized by hand: the track count
   goes to six, a card takes two of them, and a card in a short last row takes
   three. Both edges stay exactly where they were and the gap closes. Four
   items get a row of their own where there is width for it, and split 2 + 2
   where there is not. */
@media (min-width: 1180px) {
  .grid-3:has(> :nth-child(4):last-child) { grid-template-columns: repeat(4, 1fr); }
}
@media (min-width: 960px) and (max-width: 1179.98px) {
  .grid-3:has(> :nth-child(4):last-child) { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 960px) {
  .grid-3:has(> :nth-child(5):last-child) { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .grid-3:has(> :nth-child(5):last-child) > * { grid-column: span 2; }
  .grid-3:has(> :nth-child(5):last-child) > :nth-child(n+4) { grid-column: span 3; }
}

/* --------------------------------------------------------------------------
   5. MOTION PRIMITIVES
   Entrance animation is opt-in per element and staggered by a custom property,
   so a group arrives in sequence rather than all at once. The `.reveal` class is
   only added by JS after it confirms IntersectionObserver support, so content is
   never invisible when scripting is unavailable.
   -------------------------------------------------------------------------- */
.reveal {
  opacity: 0;
  transform: translateY(22px);
  transition:
    opacity var(--duration-slow) var(--ease-out) var(--reveal-delay, 0ms),
    transform var(--duration-slow) var(--ease-out) var(--reveal-delay, 0ms);
  will-change: opacity, transform;
}
.reveal.is-visible { opacity: 1; transform: none; will-change: auto; }

@keyframes eqv-float {
  0%, 100% { transform: translate(0, 0); }
  50%      { transform: translate(0, -12px); }
}
@keyframes eqv-sheen {
  from { transform: translateX(-120%) skewX(-18deg); }
  to   { transform: translateX(220%) skewX(-18deg); }
}
@keyframes eqv-fade-up {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: none; }
}

/* --------------------------------------------------------------------------
   6. BUTTONS
   The signature detail: a flat, hard-edged offset shadow that flattens on press
   like a physical key. Cheap to paint, and unmistakably ours.
   -------------------------------------------------------------------------- */
.btn {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  font-family: var(--font-body);
  font-weight: 600; font-size: var(--fs-sm); line-height: 1;
  letter-spacing: var(--tracking-normal);
  padding: 0.95em 1.6em;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  background: var(--color-bg-panel);
  color: var(--color-ink-900);
  white-space: nowrap;
  transition:
    background-color var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out),
    box-shadow var(--duration-fast) var(--ease-out);
}

/* The primary action is ink, not colour.

   A saturated fill on the main CTA is the single most template-looking choice
   available, because every starter kit ships one. Near-black reads as the more
   expensive decision and, usefully, leaves the blue free to mean one thing only:
   this is a link. Depth comes from the fill alone — no offset shadow, no
   translate on hover, no sheen sweeping across the face. */
.btn--primary {
  background: var(--gradient-brand);
  color: var(--color-white);
  border-color: var(--color-primary-600);
}
.btn--primary:hover { background: var(--color-primary-600); border-color: var(--color-primary-600); }
.btn--primary:active { background: var(--color-primary-700); border-color: var(--color-primary-700); }

/* The ink button is kept for places where a second, quieter solid is needed
   next to the coloured one — the header's contact button, mostly. */
.btn--ink {
  background: var(--color-ink-900);
  color: var(--color-white);
  border-color: var(--color-ink-900);
}
.btn--ink:hover { background: var(--color-ink-800); border-color: var(--color-ink-800); }

.btn--secondary {
  background: var(--color-primary-500);
  color: var(--color-white);
  border-color: var(--color-primary-500);
}
.btn--secondary:hover { background: var(--color-primary-600); border-color: var(--color-primary-600); }

/* The quiet partner to the primary: a hairline box that darkens on hover. */
.btn--outline {
  background: transparent;
  color: var(--color-ink-900);
  border-color: var(--color-ink-300);
}
.btn--outline:hover { border-color: var(--color-ink-900); background: transparent; }

.btn--ghost {
  background: transparent;
  border-color: transparent;
  color: var(--color-ink-700);
  padding-inline: var(--sp-3);
}
.btn--ghost:hover { background: var(--color-ink-100); color: var(--color-ink-900); }

.btn--sm { padding: 0.62em 1.1em; font-size: var(--fs-xs); border-radius: var(--radius-sm); }
.btn--lg { padding: 1.05em 2em; font-size: var(--fs-base); }
.btn--block { width: 100%; }

/* --------------------------------------------------------------------------
   7. HEADER & NAVIGATION
   Sticky, and it transforms as you scroll: the bar tightens, a translucent
   blurred surface appears behind it, and a reading-progress rule fills across
   the bottom edge. Height is animated on a fixed element only, so nothing in
   the page below it reflows.
   -------------------------------------------------------------------------- */
.site-header {
  position: sticky; top: 0; z-index: 500;
  height: var(--header-height);
  /* Opaque, and carrying its own hairline. Transparent meant the bar had no
     edge of its own until you scrolled, so on inner pages it ran straight into
     the tinted breadcrumb band with nothing to separate them. */
  background: var(--color-paper);
  border-bottom: 1px solid var(--color-ink-300);
  transition:
    height var(--duration-base) var(--ease-out),
    background-color var(--duration-base) var(--ease-out),
    box-shadow var(--duration-base) var(--ease-out);
}
.site-header::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: color-mix(in srgb, var(--color-paper) 82%, transparent);
  -webkit-backdrop-filter: saturate(180%) blur(14px);
  backdrop-filter: saturate(180%) blur(14px);
  border-bottom: 1px solid transparent;
  opacity: 0;
  transition: opacity var(--duration-base) var(--ease-out), border-color var(--duration-base) var(--ease-out);
}
.site-header.is-scrolled {
  height: var(--header-height-scrolled);
  box-shadow: var(--shadow-soft-sm);
}
.site-header.is-scrolled::before { opacity: 1; border-bottom-color: var(--color-ink-300); }
/* The mark leads very slightly on hover — enough to feel alive, not enough to
   look like it is trying to. */
.logo:hover { transform: none; }
.logo img { transition: transform var(--duration-base) var(--ease-out), height var(--duration-base) var(--ease-out); }
.logo:hover img { transform: translateX(2px); }

/* Reading progress, drawn on the header's bottom edge. */
.site-header::after {
  content: ''; position: absolute; left: 0; bottom: -1px; height: 2px;
  width: var(--scroll-progress, 0%);
  background: linear-gradient(90deg, var(--color-primary-500), var(--color-accent-500));
  transition: width 80ms linear;
  border-radius: 0 2px 2px 0;
}

.site-header .container {
  height: 100%;
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-5);
}

.logo {
  display: flex; align-items: center; gap: var(--sp-2);
  font-family: var(--font-display); font-weight: 600; font-size: var(--fs-lg);
  color: var(--color-ink-900);
  transition: transform var(--duration-base) var(--ease-snap);
}
.logo:hover { transform: scale(1.03); }
.logo img, .logo svg { height: 30px; width: auto; transition: height var(--duration-base) var(--ease-out); }
.is-scrolled .logo img { height: 26px; }

/* --- Desktop navigation --- */
.primary-nav { display: none; }
@media (min-width: 1024px) {
  .primary-nav { display: flex; align-items: center; height: 100%; }
  .primary-nav__list {
    display: flex; align-items: center; height: 100%;
    gap: clamp(var(--sp-3), 1.4vw, var(--sp-5));
    list-style: none; padding: 0;
  }
  .primary-nav__item { position: relative; height: 100%; display: flex; align-items: center; }
  .primary-nav__link {
    position: relative;
    display: flex; align-items: center; gap: 5px;
    white-space: nowrap;
    font-weight: 550;
    /* A fixed size, not --fs-sm. The header is navigation chrome, not reading
       matter: when the body scale went up it took the nav with it and the row
       stopped fitting - 12px past the viewport at 1024 and 10px at 1200, on
       every page. Chrome should hold still while the text it sits above grows. */
    font-size: 0.9375rem;
    color: var(--color-ink-700);
    padding: var(--sp-2) 2px;
    transition: color var(--duration-fast) var(--ease-out);
  }
  /* Underline grows from the centre on hover, and stays for the current page. */
  .primary-nav__link::after {
    content: ''; position: absolute; left: 0; right: 0; bottom: -2px; height: 2px;
    background: var(--color-accent-500);
    border-radius: 2px;
    transform: scaleX(0);
    transform-origin: center;
    transition: transform var(--duration-base) var(--ease-out);
  }
  .primary-nav__link:hover,
  .primary-nav__link[aria-current="page"] { color: var(--color-primary-700); }
  .primary-nav__link:hover::after,
  .primary-nav__link[aria-current="page"]::after { transform: scaleX(1); }
  .primary-nav__link svg { transition: transform var(--duration-base) var(--ease-out); }
  .has-dropdown:hover .primary-nav__link svg,
  .has-dropdown:focus-within .primary-nav__link svg { transform: rotate(180deg); }

  /* Mega panel. Anchored to the item's left edge, never centred — a centred
     640px panel pushes its left edge off-screen at 1024–1366px. */
  .has-dropdown .dropdown-panel {
    position: absolute; top: 100%; left: 0;
    min-width: min(620px, calc(100vw - 2 * var(--sp-5)));
    display: grid; grid-template-columns: repeat(2, 1fr);
    gap: var(--sp-1) var(--sp-5);
    padding: var(--sp-5);
    background: var(--color-bg-panel);
    border: 1px solid var(--color-ink-300);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-soft-xl);
    opacity: 0; visibility: hidden;
    transform: translateY(10px) scale(.985);
    transform-origin: top left;
    transition:
      opacity var(--duration-base) var(--ease-out),
      transform var(--duration-base) var(--ease-out),
      visibility 0s linear var(--duration-base);
  }
  .has-dropdown:hover .dropdown-panel,
  .has-dropdown:focus-within .dropdown-panel {
    opacity: 1; visibility: visible;
    transform: translateY(6px) scale(1);
    transition:
      opacity var(--duration-base) var(--ease-out),
      transform var(--duration-base) var(--ease-snap),
      visibility 0s;
  }
  .dropdown-panel a {
    position: relative;
    padding: var(--sp-3);
    border-radius: var(--radius-md);
    font-size: var(--fs-sm); font-weight: 550;
    color: var(--color-ink-700);
    transition: background-color var(--duration-fast) var(--ease-out),
                color var(--duration-fast) var(--ease-out),
                transform var(--duration-fast) var(--ease-out);
  }
  .dropdown-panel a:hover {
    background: var(--color-primary-50);
    color: var(--color-primary-700);
    transform: translateX(3px);
  }
  .dropdown-panel__all {
    grid-column: 1 / -1;
    display: flex; align-items: center; gap: var(--sp-2);
    margin-top: var(--sp-2);
    padding-top: var(--sp-4) !important;
    border-top: 1px solid var(--color-ink-300);
    border-radius: 0 !important;
    color: var(--color-accent-text) !important;
    font-weight: 700 !important;
  }
  .dropdown-panel__all svg { transition: transform var(--duration-base) var(--ease-out); }
  .dropdown-panel__all:hover { background: transparent !important; }
  .dropdown-panel__all:hover svg { transform: translateX(5px); }
}

.header-cta { display: flex; align-items: center; gap: var(--sp-2); }
.header-cta .btn { display: none; }
@media (min-width: 640px) { .header-cta .btn { display: inline-flex; } }

/* Hamburger — three bars drawn from one element, morphing to a cross. */
.nav-toggle {
  position: relative;
  width: 44px; height: 44px;
  display: grid; place-items: center;
  border-radius: var(--radius-full);
  transition: background-color var(--duration-fast) var(--ease-out);
}
.nav-toggle:hover { background: var(--color-ink-100); }
@media (min-width: 1024px) { .nav-toggle { display: none; } }

.nav-toggle__bar,
.nav-toggle__bar::before,
.nav-toggle__bar::after {
  content: ''; display: block;
  width: 22px; height: 2px;
  background: var(--color-ink-900);
  border-radius: 2px;
  transition: transform var(--duration-base) var(--ease-snap),
              background-color var(--duration-fast) var(--ease-out),
              top var(--duration-base) var(--ease-out);
}
.nav-toggle__bar { position: relative; }
.nav-toggle__bar::before { position: absolute; top: -7px; }
.nav-toggle__bar::after  { position: absolute; top: 7px; }
.nav-toggle[aria-expanded="true"] .nav-toggle__bar { background: transparent; }
.nav-toggle[aria-expanded="true"] .nav-toggle__bar::before { top: 0; transform: rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle__bar::after  { top: 0; transform: rotate(-45deg); }

/* --------------------------------------------------------------------------
   8. MOBILE NAVIGATION
   visibility, not just transform: off-screen has to also mean out of the tab
   order, or 22 invisible links stay focusable behind the page.
   -------------------------------------------------------------------------- */
.mobile-nav {
  position: fixed; inset: var(--header-height) 0 0 0;
  z-index: 480;
  padding: var(--sp-6) var(--sp-5) var(--sp-10);
  background: var(--color-paper);
  overflow-y: auto;
  overscroll-behavior: contain;
  transform: translateX(100%);
  visibility: hidden;
  transition:
    transform var(--duration-base) var(--ease-out),
    visibility 0s linear var(--duration-base);
}
.mobile-nav.is-open {
  transform: translateX(0);
  visibility: visible;
  transition: transform var(--duration-base) var(--ease-out), visibility 0s;
}
@media (min-width: 1024px) { .mobile-nav { display: none; } }

.mobile-nav__list { list-style: none; padding: 0; display: flex; flex-direction: column; }
.mobile-nav__link {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--sp-4) 0;
  font-family: var(--font-display); font-weight: 600; font-size: var(--fs-lg);
  color: var(--color-ink-900);
  border-bottom: 1px solid var(--color-ink-300);
  transition: color var(--duration-fast) var(--ease-out), padding-left var(--duration-base) var(--ease-out);
}
.mobile-nav__link:hover { color: var(--color-accent-text); padding-left: var(--sp-2); }
.mobile-nav__group-label {
  font-size: var(--fs-xs); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.12em;
  color: var(--color-accent-text);
  margin-top: var(--sp-6); margin-bottom: var(--sp-1);
}

/* --------------------------------------------------------------------------
   9. HERO
   Two columns on desktop: text against an offset-framed image. The frame is a
   drawn outline offset behind the photo — a designed edge, not a blurred glow.
   -------------------------------------------------------------------------- */
.hero {
  position: relative;
  padding-block: clamp(var(--sp-6), 4vw, var(--sp-8)) clamp(var(--sp-7), 5vw, var(--sp-9));
  overflow: hidden;
}
/* The hero sits on a flat tinted band with a hard bottom edge.

   Note what this is NOT: the two radial gradient washes and the 68px graph-paper
   grid that used to live here. A blurred colour blob is the visual signature of
   a generated page — it fills space without meaning anything and puts noise
   behind the one sentence that has to land. A flat panel of colour with a clean
   edge is the opposite: it states where the introduction ends and the argument
   begins, which is a job worth doing. */
.hero {
  background: var(--gradient-band);
  border-bottom: 1px solid var(--color-ink-300);
}
.hero--compact { background: var(--gradient-band); }
/* The breadcrumb strip is the top of the same band, so it takes the first
   part of the gradient rather than a flat fill that would band against it. */
.breadcrumbs-band { background: linear-gradient(168deg, var(--color-primary-75), var(--color-primary-75)); }

.hero .container {
  display: grid; gap: var(--sp-8);
  align-items: center;
}
@media (min-width: 960px) {
  /* Close to an even split, and hanging from a shared top line rather than
     vertically centred, so the headline starts where the eye already is. The
     media column used to take 0.8 against the text's 1.2 - a picture at 40% of
     the page reads as an afterthought beside a headline this size. */
  .hero .container { grid-template-columns: minmax(0, 1fr) minmax(0, 0.94fr); gap: var(--sp-8); align-items: start; }
}
@media (min-width: 1280px) {
  .hero .container { gap: var(--sp-9); }
}

.hero h1 { margin-bottom: var(--sp-5); }
/* Blog posts published from the admin panel wrap their hero copy in this, so
   the column behaves the same as the site build's unwrapped hero text. */
.hero__text { min-width: 0; }
.hero__eyebrow { animation: eqv-fade-up var(--duration-slow) var(--ease-out) both; }
/* :not(.eyebrow):not(.section-lede), because both of those are <p> too.
   `.hero p` outranks `.eyebrow` on specificity, so the small over-line label
   was rendering at 19.2px in the hero and 15px in every other place the same
   class is used - and it was LARGER than the body copy it is supposed to sit
   quietly above. A lede landing in a hero was shrunk the other way, to 19.2px,
   for no reason a reader could see. A component is one size or it is not a
   component. */
.hero p:not(.eyebrow):not(.section-lede) { font-size: clamp(1.05rem, 1rem + 0.35vw, 1.2rem); line-height: 1.55; color: var(--color-ink-700); max-width: 52ch; }

.hero__ctas {
  display: flex; flex-wrap: wrap; gap: var(--sp-3);
  margin-top: var(--sp-6);
}

.hero__trust {
  display: flex; flex-wrap: wrap; gap: var(--sp-3) var(--sp-6);
  margin-top: var(--sp-7);
  padding-top: var(--sp-5);
  border-top: 1px solid var(--color-ink-300);
  font-size: var(--fs-sm); color: var(--color-ink-500);
}
/* Each item flows as ordinary text with the dot hung outside it. As a flex row
   the bold lead-in and the phrase after it were two separate flex items, so on a
   narrow screen "Founder-run." broke across two lines while its description sat
   beside it in a second column. */
.hero__trust span {
  display: block;
  position: relative;
  padding-left: var(--sp-4);
}
.hero__trust span::before {
  content: ''; position: absolute; left: 0; top: 0.58em;
  width: 6px; height: 6px;
  border-radius: 50%; background: var(--color-primary-500);
}
.hero__trust strong { color: var(--color-ink-900); }

.hero__media { position: relative; }
.hero__media-frame { position: relative; }
/* The picture is the picture. An offset outline behind it and a floating
   circle on the corner were both decoration standing in for art direction —
   once there is a real photograph here, neither has anything to add. */
.hero__media img {
  position: relative; z-index: 2;
  width: 100%; aspect-ratio: 16 / 10; object-fit: cover;   /* the manifest ships 1600x1000 */
  border-radius: var(--radius-lg);
  border: 1px solid var(--color-ink-300);
}

/* Compact hero — pages that lead straight into a grid or a form. */
.hero--compact {
  padding-block: clamp(var(--sp-6), 4vw, var(--sp-8)) clamp(var(--sp-4), 2vw, var(--sp-5));
  text-align: left;
}
.hero--compact .container { display: block; max-width: var(--container-max); }
.hero--compact h1 { font-size: clamp(2rem, 1.3rem + 3vw, var(--fs-3xl)); max-width: 20ch; }

/* Breadcrumbs — a trail of links, set as typography.

   No pill, no chip, no panel around the whole thing: putting wayfinding in a
   box turns it into decoration, and a boxed breadcrumb is one of the most
   template-looking components there is. The interest comes from craft instead —
   a house glyph on the root, a chevron that points rather than a slash that
   merely divides, an underline that draws itself under an ancestor as you
   approach it, and the page you are actually on carrying the brand colour.

   It sits INSIDE the hero band. Floating on white between the header and the
   tinted band it belonged to neither and left a stranded strip across the top
   of every inner page. */
.breadcrumbs {
  padding-block: var(--sp-5) var(--sp-2);
  font-size: var(--fs-sm);
}
.breadcrumbs ol {
  list-style: none; padding: 0; margin: 0;
  display: flex; align-items: center;
  gap: 0;
  font-weight: 500;
  letter-spacing: 0;
  /* A deep trail scrolls sideways rather than wrapping into a second line that
     shunts the H1 down the page. The scrollbar is hidden; the fade at the end
     is what says there is more. */
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  /* The fade only shows when the trail actually reaches the edge: where the
     crumbs end short of it there is nothing under the gradient to fade. */
  -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 32px), transparent);
  mask-image: linear-gradient(90deg, #000 calc(100% - 32px), transparent);
  padding-right: var(--sp-6);
}
.breadcrumbs ol::-webkit-scrollbar { display: none; }
.breadcrumbs li { display: flex; align-items: center; flex: none; }

/* The chevron, drawn from a rotated corner so it keeps its weight at any size
   instead of inheriting whatever the font decides > should look like. */
.breadcrumbs li + li::before {
  content: '';
  width: 5px; height: 5px;
  margin-inline: var(--sp-3);
  border-right: 1.5px solid var(--color-primary-300);
  border-bottom: 1.5px solid var(--color-primary-300);
  transform: rotate(-45deg);
  flex: none;
}

.breadcrumbs a,
.breadcrumbs__current {
  position: relative;
  display: inline-flex; align-items: center; gap: var(--sp-2);
  padding-block: var(--sp-1);
  white-space: nowrap;
}
.breadcrumbs a {
  color: var(--color-ink-500);
  transition: color var(--duration-fast) var(--ease-out);
}
/* The underline grows from the left, matching the link treatment used in the
   prose. It is drawn on a pseudo-element rather than as a border so the trail's
   baseline never shifts by a pixel when it appears. */
.breadcrumbs a::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 1.5px;
  background: var(--color-primary-500);
  transform: scaleX(0); transform-origin: left;
  transition: transform var(--duration-base) var(--ease-out);
}
.breadcrumbs a:hover { color: var(--color-primary-700); }
.breadcrumbs a:hover::after { transform: scaleX(1); }
.breadcrumbs a:focus-visible { outline: 3px solid var(--color-focus); outline-offset: 3px; border-radius: 2px; }

/* The house sits a hair above the optical centre of the cap height. */
.breadcrumbs__home { flex: none; margin-top: -1px; }

/* The page you are on: the one crumb worth reading at full strength, and the
   only place the brand colour appears in the trail. */
.breadcrumbs__current {
  color: var(--color-primary-700);
  font-weight: 600;
}

/* --------------------------------------------------------------------------
   10. CONTENT TYPOGRAPHY
   -------------------------------------------------------------------------- */
/* The content column is NOT a centred 760px block. Centring the prose while the
   page's H1 sits at the container's left edge puts two different left margins on
   the same page — the misalignment you feel before you can name it. Everything
   shares the container's left edge, and only the elements that actually need a
   reading measure get one. Grids and rails then use the full width for free,
   with no negative-margin breakout to go wrong at any viewport. */
.content-body { max-width: none; margin-inline: 0; }
.content-body > p,
.content-body > ul,
.content-body > ol,
.content-body > blockquote { max-width: var(--measure); }
/* The opening lede's width is set once, in the typography section. Repeating it
   here at equal specificity but later in the file silently overrode it, so the
   page introduction was set at the section-lede measure instead of the wider
   opening one. */

/* ---------------------------------------------------------------------------
   The section block — the layout that makes the page use its width.

   Everything used to stack in one 40rem column, which left roughly six hundred
   pixels of empty paper down the right of every page and squeezed section
   headings into four cramped lines. This is the arrangement an editorial
   designer reaches for instead: the heading holds a column of its own on the
   left, its prose sits beside it, and anything structural — a card grid, a
   rail, a spec list, a figure — breaks out across the full measure underneath.

   One heading per block is what makes it work: each element is assigned its
   column explicitly, so there is never an ambiguous auto-placement to lose.
   --------------------------------------------------------------------------- */
/* minmax(0, 1fr), not the implicit auto column.
   A grid item's default min-width is auto, which means "at least as wide as my
   content" - so the scroll rail inside a block refused to shrink below the
   combined width of its cards and pushed the page out to 1170px on a 390px
   screen. body{overflow-x:hidden} hid the scrollbar but not the effect: the
   viewport still shifted under a finger dragged near the right edge, which is
   exactly what it looked like. This is the fix; the clip was only a plaster. */
.block { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-4); }
.block + .block { margin-top: clamp(var(--sp-9), 7vw, var(--sp-11)); }
.block > h2 { margin-top: 0; }
/* A reading measure belongs on prose, and a component is not prose. Capping
   every ol here also capped .process-timeline, so a full-width step sequence was
   drawn at 640px with the rest of the row empty. Only unclassed lists — the ones
   that really are body copy — are constrained. */
.block > p,
.block > ul:not([class]),
.block > ol:not([class]),
.block > blockquote { max-width: var(--measure); }

/* The prose run is one element, so it is one grid cell: the heading beside it
   can be any number of lines tall without pushing the paragraphs apart. */
.block__body { min-width: 0; }
.block__body > * { max-width: var(--measure); }
.block__body > .section-lede { max-width: var(--measure-lede); }
.block__body > h3:first-child { margin-top: 0; }

/* ONE left edge for the whole page.

   This was a two-column grid: heading in a narrow left column, prose beside it.
   It filled the width, and it generated a complaint about alignment on almost
   every page it touched - because a page then has two left edges and two right
   edges, and every element that does not belong to a section (the opening
   statement, a closing paragraph) has to pick one and looks wrong against the
   other. Indenting those to match only moved the problem: the opener ended up
   with the entire left half of the page empty beside it.

   A single column cannot be misaligned. The heading sits above the prose it
   introduces, everything starts at the same x, and the width that the second
   column used to occupy goes to the measure and to the components - which are
   the things that actually benefit from it.
   --------------------------------------------------------------------------- */
@media (min-width: 1040px) {
  /* Width only. The size now comes from the one h2 rule, so there is a single
     place to change it and no way for the two to drift apart again. */
  .block > h2 { max-width: 30rem; }
  .block > h3 { max-width: 28rem; }
}

/* Headings carry themselves on scale and weight. No decorative rule above them:
   a marker applied by formula to every heading is the clearest tell of a layout
   nobody actually art-directed. */
.content-body h2:first-child { margin-top: 0; }

/* Prose lists — bullets and numbers for lists that are body copy.

   Every selector here is :not([class]). These were plain descendant selectors,
   so they also styled any component that happens to be built on a <ul> or <ol>:
   .process-timeline is an <ol>, and it was getting this counter drawn down its
   left edge on top of the numbered circle it renders itself, so every step
   showed its number twice. A list carrying a class is a component and brings
   its own presentation; only an unclassed list is prose. */
.content-body ul:not([class]),
.content-body ol:not([class]) { margin-bottom: var(--sp-5); }
.content-body ul:not([class]) > li,
.content-body ol:not([class]) > li { margin-bottom: var(--sp-2); color: var(--color-ink-700); }

.content-body ul:not([class]) { list-style: none; padding-left: 0; }
.content-body ul:not([class]) > li { position: relative; padding-left: var(--sp-6); }
.content-body ul:not([class]) > li::before {
  content: ''; position: absolute; left: 4px; top: 0.62em;
  width: 7px; height: 7px; border-radius: 2px;
  background: var(--color-primary-400);
  transform: rotate(45deg);
}
.content-body ol:not([class]) { counter-reset: eqv-ol; list-style: none; padding-left: 0; }
.content-body ol:not([class]) > li { position: relative; padding-left: var(--sp-7); counter-increment: eqv-ol; }
.content-body ol:not([class]) > li::before {
  content: counter(eqv-ol, decimal-leading-zero);
  position: absolute; left: 0; top: 0.05em;
  font-family: var(--font-display); font-weight: 600; font-size: var(--fs-sm);
  color: var(--color-accent-700);
}

/* Inline links inside prose get an animated underline.

   The exclusions matter more than the underline does. This treatment paints a
   background-image, and the previous approach was to let it apply to everything
   and then undo it on components with a blanket
   ".content-body .btn { background-image: none }".

   That worked right up until the primary button's fill became a gradient -
   which is also a background-image - and the reset silently erased it, leaving
   every CTA inside a content area as an outlined ghost. Not applying the rule
   is safer than applying it and taking it back, because an undo has to be kept
   in step with every future change to what it is undoing. */
.content-body a:not(.btn):not(.chip):not(.index-list__item):not(.index-card__link):not(.card--editorial__link):not(.card--industry__link):not(.card--sub__link):not(.footer-col a):not(.breadcrumbs a) {
  color: var(--color-primary-700);
  font-weight: 550;
  background-image: linear-gradient(var(--color-accent-500), var(--color-accent-500));
  background-size: 0% 2px;
  background-repeat: no-repeat;
  background-position: 0 100%;
  box-shadow: inset 0 -2px 0 0 var(--color-accent-100);
  transition: background-size var(--duration-base) var(--ease-out), color var(--duration-fast) var(--ease-out);
}
.content-body a:not(.btn):not(.chip):not(.index-list__item):not(.index-card__link):not(.card--editorial__link):not(.card--industry__link):not(.card--sub__link):not(.footer-col a):not(.breadcrumbs a):hover { color: var(--color-accent-700); background-size: 100% 2px; }

/* The blanket undo that used to live here is gone: the rule above simply does
   not match these components any more. */

.content-body blockquote {
  border-left: 3px solid var(--color-accent-500);
  padding-left: var(--sp-5);
  margin-block: var(--sp-6);
  font-family: var(--font-display);
  font-size: var(--fs-lg);
  line-height: var(--lh-relaxed);
  color: var(--color-ink-700);
}
.content-body code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.9em;
  background: var(--color-ink-100);
  padding: 0.15em 0.4em;
  border-radius: var(--radius-sm);
}

.content-image {
  margin-block: var(--sp-7);
  border-radius: var(--radius-lg);
  overflow: hidden;
  border: 1px solid var(--color-ink-300);
  box-shadow: var(--shadow-soft-lg);
}
.content-image img { width: 100%; }
.content-image figcaption {
  font-size: var(--fs-xs); color: var(--color-ink-500);
  padding: var(--sp-3) var(--sp-4);
  background: var(--color-bg-panel);
}

/* --------------------------------------------------------------------------
   11. CARDS & COMPONENTS
   Five distinct treatments, one per content shape — deliberately not one card
   component reused everywhere.
   -------------------------------------------------------------------------- */
.card {
  background: var(--color-bg-panel);
  border-radius: var(--radius-md);
  padding: var(--sp-5);
  transition: border-color var(--duration-base) var(--ease-out),
              background-color var(--duration-base) var(--ease-out);
}

/* Sub-card — a column of text under a rule, not a box.

   It used to be a bordered, rounded panel with a 3px coloured bar across the
   top that wiped in on hover. A row of those is the most recognisable component
   on the template web: the box adds no information, and the coloured bar is
   decoration applied by formula to every item alike. What separates one column
   from the next here is a hairline and the space between them — which is how a
   printed page has done it for four hundred years. */
.card--sub {
  display: flex; flex-direction: column;
  padding: var(--sp-5) 0 0;
  border: 0;
  border-top: 1px solid var(--color-ink-900);
  border-radius: 0;
  background: transparent;
  position: relative;
}
.card--sub h3 {
  margin-top: 0;
  margin-bottom: var(--sp-3);
  font-size: var(--fs-lg);
}
.card--sub p { color: var(--color-ink-700); }
/* The rule is the only thing that reacts, and only by thickening. */
.card--sub::after {
  content: ''; position: absolute; inset: -1px 0 auto 0; height: 2px;
  background: var(--color-ink-900);
  transform: scaleX(0); transform-origin: left;
  transition: transform var(--duration-base) var(--ease-out);
}
.card--sub:hover::after { transform: scaleX(1); }
.card--sub h3 { transition: color var(--duration-fast) var(--ease-out); }
.card--sub:hover h3 { color: var(--color-primary-700); }

/* A linked card. The heading is the real anchor and its pseudo-element covers
   the whole card, so the tile is clickable without nesting an <a> inside an
   <a> - which is invalid, and makes the parser tear the card apart. */
.card--sub--linked { position: relative; cursor: pointer; }
.card--sub__link { color: inherit; }
.card--sub__link::after { content: ''; position: absolute; inset: 0; z-index: 1; }
.card--sub__link:focus-visible { outline: none; }
.card--sub--linked:focus-within { outline: 3px solid var(--color-focus); outline-offset: 6px; }

/* The affordance: a quiet label that gains its arrow on hover. Marked
   aria-hidden, because the heading link already says where it goes. */
.card--sub__more {
  margin-top: auto;
  padding-top: var(--sp-4);
  font-size: var(--fs-sm); font-weight: 600;
  color: var(--color-primary-700);
  display: inline-flex; align-items: center; gap: var(--sp-2);
  transition: gap var(--duration-base) var(--ease-out);
}
.card--sub__more::after {
  content: '→';   /* the literal arrow, not a CSS escape - see below */
  transition: transform var(--duration-base) var(--ease-out);
}
.card--sub--linked:hover .card--sub__more { gap: var(--sp-3); }
.card--sub--linked:hover .card--sub__more::after { transform: translateX(3px); }

/* Numbered index list — the services hub. */
.index-list { display: grid; gap: var(--sp-3); margin-block: var(--sp-5); }
.index-list__item {
  position: relative;
  display: grid; grid-template-columns: auto 1fr auto;
  align-items: start; gap: var(--sp-4);
  padding: var(--sp-5);
  border: 1px solid var(--color-ink-300);
  border-radius: var(--radius-lg);
  background: var(--color-bg-panel);
  overflow: hidden;
  transition: transform var(--duration-base) var(--ease-out),
              border-color var(--duration-base) var(--ease-out),
              box-shadow var(--duration-base) var(--ease-out);
}
/* A teal wash wipes in from the left on hover. */
.index-list__item::before {
  content: ''; position: absolute; inset: 0; z-index: 0;
  background: linear-gradient(90deg, var(--color-primary-50), transparent 62%);
  opacity: 0;
  transition: opacity var(--duration-base) var(--ease-out);
}
.index-list__item > * { position: relative; z-index: 1; }
.index-list__item:hover {
  transform: translateX(6px);
  border-color: var(--color-primary-500);
  box-shadow: var(--shadow-soft);
}
.index-list__item:hover::before { opacity: 1; }
.index-list__num {
  font-family: var(--font-display);
  font-size: var(--fs-2xl); font-weight: 600; line-height: 1;
  color: var(--color-accent-300);
  transition: color var(--duration-base) var(--ease-out);
}
.index-list__item:hover .index-list__num { color: var(--color-accent-500); }
.index-list__item h3 { margin: 0 0 var(--sp-1); font-size: var(--fs-lg); }
.index-list__item p { margin: 0; font-size: var(--fs-sm); color: var(--color-ink-700); }
.index-list__arrow {
  align-self: center;
  color: var(--color-ink-500); font-size: var(--fs-lg);
  transition: transform var(--duration-base) var(--ease-snap), color var(--duration-base) var(--ease-out);
}
.index-list__item:hover .index-list__arrow { transform: translateX(6px); color: var(--color-accent-500); }

/* Index card — a link-led list item rendered as a picture card. The services
   hub is a catalogue, so it gets thumbnails and a real card rather than a text
   row. Grid items align to the start, so a short description never inflates its
   neighbour, and the media block is a fixed ratio so every card lines up. */
.index-list { margin-block: var(--sp-6); }
.index-card {
  position: relative;
  display: flex; flex-direction: column;
  overflow: hidden;
  background: var(--color-bg-panel);
  border: 1.5px solid var(--color-ink-300);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-soft-sm);
  transition: transform var(--duration-base) var(--ease-out),
              box-shadow var(--duration-base) var(--ease-out),
              border-color var(--duration-base) var(--ease-out);
}
.index-card:hover {
  transform: translateY(-6px);
  border-color: var(--color-primary-500);
  box-shadow: var(--shadow-soft-lg);
}
.index-card__media { position: relative; aspect-ratio: 16 / 10; overflow: hidden; background: var(--color-ink-100); }
.index-card__media img {
  width: 100%; height: 100%; object-fit: cover;
  transition: transform var(--duration-slow) var(--ease-out);
}
.index-card:hover .index-card__media img { transform: scale(1.05); }
.index-card__num {
  position: absolute; left: 0; bottom: 0;
  padding: var(--sp-2) var(--sp-4);
  background: var(--color-primary-700);
  color: #fff;
  font-family: var(--font-display); font-weight: 600; font-size: var(--fs-sm);
  border-top-right-radius: var(--radius-md);
  font-variant-numeric: tabular-nums;
}
.index-card__body { display: flex; flex-direction: column; gap: var(--sp-2); padding: var(--sp-5); flex: 1; }
.index-card__body h3 { margin: 0; font-size: var(--fs-lg); }
.index-card__body p { margin: 0; font-size: var(--fs-sm); color: var(--color-ink-700); }
.index-card__link {
  margin-top: auto; padding-top: var(--sp-3);
  display: inline-flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-sm); font-weight: 650;
  color: var(--color-primary-700);
}
.index-card__arrow { transition: transform var(--duration-base) var(--ease-snap); }
.index-card:hover .index-card__arrow { transform: translateX(5px); }

/* Spec list — bold-led price and failure-mode paragraphs.
   Deliberately NOT a card grid. Boxes in a grid stretch to the tallest sibling,
   so a two-line entry next to a ten-line one is mostly empty space; and a row of
   identical rounded rectangles is the most generic layout on the web. This is a
   ledger instead: a hairline rule per row, an oversized index numeral, and the
   label set in the display face. Rows are as tall as their content, always. */
.spec-list { counter-reset: eqv-spec; margin-block: var(--sp-6); border-top: 1px solid var(--color-ink-300); }
.spec-list__item {
  counter-increment: eqv-spec;
  position: relative;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--sp-3) var(--sp-5);
  align-items: baseline;
  padding: var(--sp-5) var(--sp-2);
  border-bottom: 1px solid var(--color-ink-300);
  transition: background-color var(--duration-base) var(--ease-out),
              padding-left var(--duration-base) var(--ease-out);
}
.spec-list__item::before {
  content: counter(eqv-spec, decimal-leading-zero);
  font-family: var(--font-display); font-weight: 600;
  font-size: var(--fs-lg); line-height: 1;
  color: var(--color-accent-700);
  font-variant-numeric: tabular-nums;
}
.spec-list__item:hover { background: var(--color-primary-50); padding-left: var(--sp-4); }
.spec-list__item h3 { grid-column: 2; margin: 0 0 var(--sp-2); font-size: var(--fs-lg); }
.spec-list__item p  { grid-column: 2; margin: 0; font-size: var(--fs-sm); max-width: var(--measure); }
/* A long definition is split at its sentence boundaries into several
   paragraphs, so every one after the first needs its own breathing room. */
.spec-list__item p + p { margin-top: var(--sp-3); }
/* An entry with no label at all still lines up under the numeral.

   This must be :only-child. As p:first-of-type:last-child it also matched a
   labelled entry whose description happens to be a single paragraph — and at
   (0,3,1) it outranked the (0,1,1) rule in the media query below, so those rows
   put their description in the label column and left the whole third column
   empty. Media queries carry no specificity of their own, which is what made a
   rule written for one case quietly win in another.

   It also has to SPAN to the end, not just start at column 2. An entry with no
   label has nothing in the label column, so pinning its text there left it
   wrapping inside 22 characters with the whole third column empty beside it. */
.spec-list__item > p:only-child { grid-column: 2 / -1; }
@media (min-width: 860px) {
  .spec-list__item { grid-template-columns: 3.5rem minmax(0, 22ch) minmax(0, 1fr); gap: var(--sp-5); }
  .spec-list__item h3 { grid-column: 2; margin-bottom: 0; }
  .spec-list__item p  { grid-column: 3; }
}

/* Industry card — a bright duotone photo above a white body, with an oversized
   watermark numeral. Each card in the grid picks up a different brand tint, so
   the row reads as a set rather than five copies of one thing. */
.card--industry {
  position: relative;
  display: flex; flex-direction: column;
  overflow: hidden;
  border-radius: var(--radius-lg);
  border: 1px solid var(--color-ink-300);
  background: var(--color-bg-panel);
  box-shadow: var(--shadow-hard-sm);
  transition: transform var(--duration-base) var(--ease-out),
              box-shadow var(--duration-base) var(--ease-out);
}
.card--industry:hover { border-color: var(--color-ink-900); }
.card--industry__media { position: relative; aspect-ratio: 16 / 10; overflow: hidden; }
.card--industry__media img {
  width: 100%; height: 100%; object-fit: cover;
  filter: saturate(1.15) contrast(1.04);
  transition: transform var(--duration-slow) var(--ease-out);
}
.card--industry:hover .card--industry__media img { transform: scale(1.06); }
/* Bright duotone wash — colour, not darkness. */
.card--industry__media::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(150deg,
    color-mix(in srgb, var(--card-tint, var(--color-primary-500)) 62%, transparent),
    color-mix(in srgb, var(--color-sun-500) 34%, transparent));
  mix-blend-mode: screen;
}
.card--industry__body { position: relative; flex: 1; display: flex; flex-direction: column; gap: var(--sp-2); padding: var(--sp-5); background: var(--color-bg-panel); }
/* Stretched link: the heading is the real anchor, and its pseudo-element covers
   the whole card so the tile is clickable — without nesting an <a> inside an
   <a>, which is invalid and makes the parser tear the card apart. */
.card--industry__link { color: inherit; }
.card--industry__link::after { content: ''; position: absolute; inset: 0; z-index: 1; }
/* Links inside the approved copy sit above that overlay and stay clickable. */
.card--industry__body a:not(.card--industry__link) {
  position: relative; z-index: 2;
  color: var(--color-primary-700); font-weight: 600;
}
.card--industry__body h2,
.card--industry__body h3 {
  color: var(--color-ink-900);
  margin: 0 0 var(--sp-2); font-size: var(--fs-lg);
  transition: color var(--duration-fast) var(--ease-out);
}
.card--industry:hover .card--industry__body h2,
.card--industry:hover .card--industry__body h3 { color: var(--color-accent-text); }
.card--industry__body p { color: var(--color-ink-700); font-size: var(--fs-sm); margin: 0; }
.card--industry__watermark {
  position: absolute; top: -22px; right: 12px;
  font-family: var(--font-display); font-size: 5.4rem; font-weight: 600; line-height: 1;
  color: color-mix(in srgb, var(--card-tint, var(--color-primary-500)) 22%, transparent);
  pointer-events: none;
  z-index: 1;
}
/* One tint per position in the grid. */
.card--industry:nth-child(5n+1) { --card-tint: var(--color-primary-500); }
.card--industry:nth-child(5n+2) { --card-tint: var(--color-accent-500); }
.card--industry:nth-child(5n+3) { --card-tint: var(--color-sky-500); }
.card--industry:nth-child(5n+4) { --card-tint: var(--color-sun-500); }
.card--industry:nth-child(5n+5) { --card-tint: var(--color-primary-400); }

/* Editorial card — the blog hub. No border, no shadow: a rule and a photo. */
.card--editorial {
  display: flex; flex-direction: column;
  padding-bottom: var(--sp-5);
  border-bottom: 1px solid var(--color-ink-300);
  transition: transform var(--duration-base) var(--ease-out);
}
.card--editorial:hover { transform: translateY(-4px); }
.card--editorial__media {
  overflow: hidden;
  border-radius: var(--radius-md);
  margin-bottom: var(--sp-4);
  aspect-ratio: 16 / 9;
}
.card--editorial__media img {
  width: 100%; height: 100%; object-fit: cover;
  transition: transform var(--duration-slow) var(--ease-out);
}
.card--editorial:hover .card--editorial__media img { transform: scale(1.05); }
.card--editorial__tag {
  display: inline-block;
  font-size: var(--fs-xs); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--color-accent-text);
  margin-bottom: var(--sp-2);
}
/* --fs-lg, like every other card on the site.
   The blog cards were the only family at --fs-xl, so an identical component -
   a title over a description in a grid - was set at 24px on /blog/ and 20px on
   /services/, /industries/, /case-studies/ and the packages. */
.card--editorial h3 {
  margin: 0 0 var(--sp-2); font-size: var(--fs-lg);
  transition: color var(--duration-fast) var(--ease-out);
}
.card--editorial:hover h3 { color: var(--color-accent-text); }
.card--editorial__meta { font-size: var(--fs-xs); color: var(--color-ink-500); margin: auto 0 0; padding-top: var(--sp-2); }


/* --------------------------------------------------------------------------
   CAROUSEL
   A real horizontal scroll container with snap points, not a JS slideshow.
   It works with a trackpad, a swipe, the scrollbar and the keyboard before any
   script runs; main.js only adds the arrow buttons and hides them when the
   content already fits. Every card keeps one fixed width, which is what makes a
   set of uneven copy line up perfectly — something a wrapping grid cannot do.
   -------------------------------------------------------------------------- */
.carousel { position: relative; margin-block: var(--sp-6); min-width: 0; }
/* A fade over the right edge, so the card the rail cuts through reads as "there
   is more this way" rather than as a card that failed to finish rendering.
   pointer-events:none, or it would swallow clicks on the card underneath it.
   It sits above the cards and below the arrows. */
.carousel::after {
  content: '';
  position: absolute; top: 0; bottom: var(--sp-6); right: 0;
  width: clamp(40px, 7vw, 90px);
  background: linear-gradient(90deg, transparent, var(--color-bg));
  pointer-events: none;
  z-index: 1;
}
/* On a tinted band the fade has to end in that band's colour, not in paper. */
.section--tint .carousel::after,
.hero .carousel::after { background: linear-gradient(90deg, transparent, var(--color-primary-50)); }

.carousel__viewport {
  min-width: 0;
  max-width: 100%;
  display: flex;
  align-items: stretch;
  gap: var(--sp-5);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  overscroll-behavior-x: contain;
  /* Room for the offset shadow and the lift on hover, which would otherwise be
     clipped by the scroll container. */
  padding: var(--sp-2) var(--sp-2) var(--sp-6);
  scrollbar-width: thin;
  scrollbar-color: var(--color-ink-300) transparent;
}
/* Slim and quiet. The rail is scrolled by dragging, by the arrows, or by a
   trackpad; the bar is a position indicator, not a control that needs to
   advertise itself at 8px of brand colour. */
.carousel__viewport::-webkit-scrollbar { height: 4px; }
.carousel__viewport::-webkit-scrollbar-track { background: transparent; }
.carousel__viewport::-webkit-scrollbar-thumb { background: var(--color-ink-300); border-radius: var(--radius-full); }
.carousel:hover .carousel__viewport::-webkit-scrollbar-thumb { background: var(--color-primary-300); }
.carousel__viewport::-webkit-scrollbar-thumb:hover { background: var(--color-primary-500); }
.carousel__viewport:focus-visible { outline: 3px solid var(--color-focus); outline-offset: 4px; border-radius: var(--radius-lg); }

/* One fixed width per card — the whole point of the rail. */
.carousel__viewport > * {
  flex: 0 0 auto;
  width: clamp(258px, 78vw, 320px);
  scroll-snap-align: start;
  margin: 0;
}
@media (min-width: 640px)  { .carousel__viewport > * { width: 300px; } }
@media (min-width: 1024px) { .carousel__viewport > * { width: 326px; } }

.carousel__head {
  display: flex; align-items: center; justify-content: flex-end;
  gap: var(--sp-2);
  margin-bottom: var(--sp-3);
  position: relative; z-index: 2;   /* above the edge fade */
}
.carousel__btn {
  width: 44px; height: 44px; border-radius: 50%;
  display: none; place-items: center;
  background: var(--color-bg-panel);
  border: 1px solid var(--color-ink-300);
  color: var(--color-ink-900);
  box-shadow: none;
  transition: transform var(--duration-base) var(--ease-snap),
              box-shadow var(--duration-base) var(--ease-snap),
              opacity var(--duration-base) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}
.carousel.is-scrollable .carousel__btn { display: grid; }
.carousel__btn:hover {
  background: var(--color-primary-600); color: #fff;
  border-color: var(--color-ink-900);
}
.carousel__btn[disabled] { opacity: .3; pointer-events: none; box-shadow: none; }

/* A quiet hint that there is more to the right, faded out at the end. */
.carousel::after {
  content: ''; position: absolute; top: 0; bottom: var(--sp-6); right: 0; width: 56px;
  pointer-events: none;
  background: linear-gradient(90deg, transparent, var(--color-bg));
  opacity: 0;
  transition: opacity var(--duration-base) var(--ease-out);
}
.carousel.is-scrollable:not(.is-at-end)::after { opacity: 1; }

/* Cards on a rail are full-height by definition, so the editorial card needs
   its own footing rather than the bottom rule it uses in a stacked list. */
.carousel--editorial .card--editorial { border-bottom: none; padding-bottom: 0; }
/* Sub-cards on a rail keep an even height so the row reads as one band. */
/* Sub-cards are short by nature, so their rail runs a little narrower. */
.carousel--sub .carousel__viewport > * { width: clamp(250px, 74vw, 300px); }
.carousel__viewport > .card,
.carousel__viewport > .index-card,
.carousel__viewport > .card--industry,
.carousel__viewport > .card--editorial { align-self: stretch; }

@media (prefers-reduced-motion: reduce) {
  .carousel__viewport { scroll-behavior: auto; }
}

/* Process timeline — a connected vertical line with numbered nodes. */
.process-timeline { list-style: none; padding: 0; margin-block: var(--sp-6); position: relative; }
.process-timeline::before {
  content: ''; position: absolute; left: 21px; top: 12px; bottom: 12px;
  width: 2px;
  background: linear-gradient(180deg, var(--color-accent-500), var(--color-primary-300));
  border-radius: 2px;
}
.process-step {
  position: relative;
  display: grid; grid-template-columns: 44px 1fr; gap: var(--sp-4);
  padding-bottom: var(--sp-6);
}
.process-step:last-child { padding-bottom: 0; }
.process-step__num {
  position: relative; z-index: 1;
  width: 44px; height: 44px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--color-bg-panel);
  border: 1px solid var(--color-ink-300);
  font-family: var(--font-display); font-weight: 600; font-size: var(--fs-sm);
  color: var(--color-accent-text);
  transition: transform var(--duration-base) var(--ease-snap),
              background-color var(--duration-base) var(--ease-out),
              color var(--duration-base) var(--ease-out);
}
.process-step:hover .process-step__num {
  transform: scale(1.1);
  background: var(--color-accent-500);
  color: var(--color-white);
}
.process-step__body h3 { margin: var(--sp-2) 0 var(--sp-1); font-size: var(--fs-lg); }
.process-step__body p { margin: 0; font-size: var(--fs-sm); }

/* FAQ — native details/summary, so it works with zero JS. */
.faq-list { margin-block: var(--sp-5); border-top: 1px solid var(--color-ink-300); }
.faq-item { border-bottom: 1px solid var(--color-ink-300); }
.faq-item__question {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4);
  padding: var(--sp-5) 0;
  cursor: pointer;
  font-family: var(--font-display); font-weight: 600; font-size: var(--fs-lg);
  color: var(--color-ink-900);
  list-style: none;
  transition: color var(--duration-fast) var(--ease-out);
}
.faq-item__question::-webkit-details-marker { display: none; }
.faq-item__question:hover { color: var(--color-accent-text); }
.faq-item__icon {
  position: relative; flex: none;
  width: 26px; height: 26px; border-radius: 50%;
  border: 1px solid var(--color-ink-300);
  transition: transform var(--duration-base) var(--ease-snap),
              background-color var(--duration-base) var(--ease-out);
}
.faq-item__icon::before,
.faq-item__icon::after {
  content: ''; position: absolute; top: 50%; left: 50%;
  width: 11px; height: 2px; border-radius: 2px;
  background: var(--color-ink-900);
  transform: translate(-50%, -50%);
  transition: transform var(--duration-base) var(--ease-out), background-color var(--duration-base) var(--ease-out);
}
.faq-item__icon::after { transform: translate(-50%, -50%) rotate(90deg); }
.faq-item[open] .faq-item__icon { background: var(--color-accent-500); transform: rotate(180deg); }
.faq-item[open] .faq-item__icon::before,
.faq-item[open] .faq-item__icon::after { background: var(--color-white); }
.faq-item[open] .faq-item__icon::after { transform: translate(-50%, -50%) rotate(0deg); }
.faq-item__answer { padding-bottom: var(--sp-5); max-width: 68ch; animation: eqv-fade-up var(--duration-base) var(--ease-out); }
.faq-item__answer p { font-size: var(--fs-sm); }

/* Chips — a cross-link row. */
.chip-row { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-block: var(--sp-4) var(--sp-6); }
.chip {
  display: inline-flex; align-items: center;
  padding: 0.5em 1.1em;
  border-radius: var(--radius-full);
  border: 1.5px solid var(--color-ink-300);
  background: var(--color-bg-panel);
  font-size: var(--fs-sm); font-weight: 550;
  color: var(--color-ink-700);
  transition: transform var(--duration-base) var(--ease-snap),
              border-color var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}
.chip:hover {
  transform: translateY(-2px);
  border-color: var(--color-primary-500);
  background: var(--color-primary-50);
  color: var(--color-primary-700);
}

/* Reserved variants — documented in the build guide, not currently emitted. */
.card--pricing { border: 1px solid var(--color-ink-300); border-radius: var(--radius-lg); padding: var(--sp-6); background: var(--color-bg-panel); box-shadow: var(--shadow-hard-sm); }
.card--pricing.is-featured { box-shadow: var(--shadow-hard); border-color: var(--color-accent-500); }
.card--service { position: relative; padding: var(--sp-5); border-radius: var(--radius-lg); background: var(--color-bg-panel); border: 1px solid var(--color-ink-300); }
.card__index { font-family: var(--font-display); font-size: var(--fs-2xl); color: var(--color-accent-300); }
.card__link { color: var(--color-accent-text); font-weight: 600; font-size: var(--fs-sm); }
.badge { display: inline-block; padding: 0.3em 0.9em; border-radius: var(--radius-full); background: var(--color-accent-500); border: 1px solid var(--color-primary-600); color: var(--color-white); font-size: var(--fs-xs); font-weight: 700; }
.trust-panel { background: linear-gradient(140deg, var(--color-primary-100), var(--color-sun-100)); color: var(--color-ink-800); border: 1px solid var(--color-ink-300); border-radius: var(--radius-xl); padding: var(--sp-8); box-shadow: var(--shadow-hard); }
.trust-panel h2, .trust-panel h3 { color: var(--color-ink-900); }
.trust-panel p { color: var(--color-ink-700); }
.price { font-family: var(--font-display); font-size: var(--fs-3xl); font-weight: 600; color: var(--color-ink-900); }
.price small { font-size: var(--fs-sm); color: var(--color-ink-500); font-family: var(--font-body); }
.bento { display: grid; gap: var(--sp-5); }
@media (min-width: 860px) { .bento { grid-template-columns: repeat(3, 1fr); } .bento > :first-child { grid-column: span 2; grid-row: span 2; } }
.card-link-grid { display: grid; gap: var(--sp-4); }
@media (min-width: 640px) { .card-link-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 960px) { .card-link-grid.cols-3 { grid-template-columns: repeat(3, 1fr); } }
.stat-marquee { display: grid; gap: var(--sp-5); grid-template-columns: repeat(2, 1fr); }
@media (min-width: 768px) { .stat-marquee { grid-template-columns: repeat(4, 1fr); } }
.stat-marquee__item { text-align: center; }
.stat-marquee__num { font-family: var(--font-display); font-size: var(--fs-3xl); font-weight: 600; color: var(--color-ink-900); display: block; }
.stat-marquee__label { font-size: var(--fs-xs); color: var(--color-ink-500); text-transform: uppercase; letter-spacing: 0.08em; }
.section--alt { background: var(--color-bg-alt); }
.section--tint { background: var(--color-bg-tint); }
.section--dark { background: linear-gradient(150deg, var(--color-primary-100), var(--color-sky-100)); }
.section--dark h2, .section--dark h3 { color: var(--color-ink-900); }
.section--tight { padding-block: var(--sp-7); }
.underline-accent { box-shadow: inset 0 -0.35em 0 0 var(--color-accent-100); }
.divider { height: 1px; background: var(--color-ink-300); border: none; margin-block: var(--sp-8); }

/* --------------------------------------------------------------------------
   12. FORMS
   Contrast note: --color-ink-500 is AA on paper; field borders use
   --color-border-field, which clears the 3:1 that WCAG 1.4.11 requires of a
   control's only visual boundary.
   -------------------------------------------------------------------------- */
.form-field { margin-bottom: var(--sp-4); }
.form-field label { display: block; font-weight: 650; font-size: var(--fs-sm); margin-bottom: var(--sp-1); }
.form-field .hint, .field__hint { font-size: var(--fs-xs); color: var(--color-ink-500); margin-top: 4px; display: block; }

.form-field input,
.form-field select,
.form-field textarea {
  width: 100%;
  padding: 0.8em 1em;
  border: 2px solid var(--color-border-field);
  border-radius: var(--radius-md);
  background: var(--color-white);
  font-size: var(--fs-sm);
  transition: border-color var(--duration-fast) var(--ease-out),
              box-shadow var(--duration-fast) var(--ease-out);
}
.form-field input:focus,
.form-field select:focus,
.form-field textarea:focus {
  border-color: var(--color-primary-500);
  box-shadow: 0 0 0 4px var(--color-primary-50);
}
.form-field textarea { min-height: 110px; resize: vertical; }

.form-field.has-error input,
.form-field.has-error textarea,
.form-field.has-error select { border-color: var(--color-error); }
.error-msg { display: none; }
.form-field.has-error .error-msg {
  display: block; color: var(--color-error);
  font-size: var(--fs-xs); font-weight: 550; margin-top: 4px;
}

.form-row { display: grid; gap: var(--sp-4); }
@media (min-width: 560px) { .form-row--2 { grid-template-columns: 1fr 1fr; } }

.form-hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* Kept in the accessibility tree at all times so role="status" is registered
   as a live region from load; an empty one just occupies no space. */
.form-status {
  height: 0; overflow: hidden; padding: 0;
  border-radius: var(--radius-md);
  font-size: var(--fs-sm);
}
.form-status.is-success,
.form-status.is-error {
  height: auto; padding: var(--sp-3) var(--sp-4); margin-top: var(--sp-3);
  animation: eqv-fade-up var(--duration-base) var(--ease-out);
}
.form-status.is-success { background: var(--color-success-bg); color: var(--color-success); }
.form-status.is-error { background: var(--color-error-bg); color: var(--color-error); }

.lead-form {
  background: var(--color-white);
  border: 1px solid var(--color-ink-300);
  border-radius: var(--radius-xl);
  padding: var(--sp-6);
  box-shadow: var(--shadow-hard);
}
.lead-form h3 { margin-top: 0; }
.lead-form__title {
  font-family: var(--font-display); font-weight: 600;
  font-size: var(--fs-lg); line-height: var(--lh-snug);
  color: var(--color-ink-900);
  margin: 0 0 var(--sp-2);
}
.lead-form__note { font-size: var(--fs-xs); color: var(--color-ink-500); margin-top: var(--sp-3); text-align: center; }

.lead-form-section { background: var(--color-bg-tint); position: relative; overflow: hidden; }
.lead-form-section::before {
  content: ''; position: absolute; inset: 0; z-index: 0;
  background: radial-gradient(ellipse 60% 60% at 50% 0%, rgba(255,255,255,.7), transparent 70%);
}
.lead-form-section .container { position: relative; z-index: 1; max-width: var(--container-narrow); }

/* --------------------------------------------------------------------------
   14. FOOTER
   A closing statement, not a sitemap dump: a large brand line, then the
   navigation columns, then a quiet legal bar.
   -------------------------------------------------------------------------- */
/* Ink, flat, with nothing drawn on it.

   It carried three radial colour washes and a 72px grid — the same decoration
   that was removed from the hero, warmed up. A dark close is the stronger move
   anyway: after a page of light surfaces the footer states plainly that the
   document has ended, and the one warm accent finally has somewhere dark to sit
   against. */
.site-footer {
  position: relative;
  background: var(--gradient-ink);
  color: rgba(255, 255, 255, .72);
  padding-block: clamp(var(--sp-9), 7vw, var(--sp-11)) var(--sp-6);
  overflow: hidden;
  /* A brand rule across the very top: the one place the blue gets to run edge
     to edge, and the line that tells you the document has ended. */
  border-top: 3px solid var(--color-primary-500);
}
/* The mark, very large and very faint, anchored to the corner. At 5% it is
   texture rather than an image - you notice the footer has weight, not that
   there is a logo behind it. */
.site-footer::after {
  content: '';
  position: absolute; right: -6%; bottom: -32%;
  width: min(560px, 52vw); aspect-ratio: 528 / 512;
  background: url('/assets/images/mark-light.png') no-repeat center / contain;
  opacity: .05;
  pointer-events: none;
}
.site-footer .container { position: relative; z-index: 1; }
.site-footer a { color: rgba(255, 255, 255, .82); transition: color var(--duration-fast) var(--ease-out); }
.site-footer a:hover { color: var(--color-white); }
.site-footer p { color: rgba(255, 255, 255, .72); }
/* Headings default to ink-900 site-wide, which on an ink footer is ink on ink.
   Everything reversed out is declared here rather than relying on inheritance. */
.site-footer h1, .site-footer h2, .site-footer h3, .site-footer h4 { color: var(--color-white); }
.site-footer strong { color: var(--color-white); }
.site-footer .eyebrow { color: rgba(255, 255, 255, .62); }
.site-footer .btn--outline { color: var(--color-white); border-color: rgba(255, 255, 255, .38); }
.site-footer .btn--outline:hover { border-color: var(--color-white); background: rgba(255, 255, 255, .08); }

/* Closing CTA band — the footer opens with an invitation, then the navigation. */
/* Not a white card floating on a patterned ground — the invitation is simply
   the first thing in the footer, separated from the navigation by a rule. */
.footer-cta {
  display: grid; gap: var(--sp-6);
  align-items: center;
  padding: 0 0 clamp(var(--sp-6), 4vw, var(--sp-8));
  margin-bottom: var(--sp-8);
  background: transparent;
  border: 0;
  border-bottom: 1px solid rgba(255, 255, 255, .14);
  border-radius: 0;
}
@media (min-width: 860px) { .footer-cta { grid-template-columns: 1.35fr auto; gap: var(--sp-8); } }
.footer-cta h2 {
  margin: 0 0 var(--sp-3);
  font-size: clamp(1.7rem, 1.2rem + 2vw, var(--fs-3xl));
}
.footer-cta p { color: rgba(255, 255, 255, .72); max-width: 46rem; margin: 0; }
.footer-cta__text { min-width: 0; }
.footer-cta__actions { display: flex; flex-wrap: wrap; gap: var(--sp-3); }

.footer-grid {
  display: grid; gap: var(--sp-7);
  grid-template-columns: 1fr;
  padding-bottom: var(--sp-7);
}
@media (min-width: 768px) { .footer-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .footer-grid { grid-template-columns: 1.5fr repeat(3, 1fr); gap: var(--sp-8); } }

.footer-brand {
  display: flex; align-items: center; gap: var(--sp-2);
  font-family: var(--font-display); font-weight: 700; font-size: var(--fs-xl);
  color: var(--color-white);
  margin-bottom: var(--sp-4);
}
.footer-col p { color: rgba(255, 255, 255, .70); font-size: var(--fs-sm); max-width: 34rem; }

/* A column label, set like one. The uppercase, letterspaced, amber-underlined
   version was the eyebrow treatment again, applied four times across the
   footer — decoration standing in for hierarchy that white-on-ink already
   provides. */
.footer-col__title {
  color: var(--color-white);
  font-family: var(--font-body); font-weight: 600; font-size: var(--fs-sm);
  text-transform: none; letter-spacing: 0;
  margin-bottom: var(--sp-4);
  padding-bottom: 0;
  border-bottom: 0;
}
.footer-col ul {
  list-style: none; padding: 0;
  display: flex; flex-direction: column; gap: var(--sp-2);
  font-size: var(--fs-sm);
}
.footer-col ul a {
  display: inline-block;
  transition: color var(--duration-fast) var(--ease-out), transform var(--duration-base) var(--ease-out);
}
.footer-col ul a { position: relative; }
.footer-col ul a:hover { transform: translateX(3px); color: var(--color-white); }
/* A short blue rule slides out from the left as the link moves. */
.footer-col ul a::before {
  content: ''; position: absolute; left: -12px; top: 50%;
  width: 6px; height: 2px; margin-top: -1px;
  background: var(--color-primary-400);
  transform: scaleX(0); transform-origin: right;
  transition: transform var(--duration-base) var(--ease-out);
}
.footer-col ul a:hover::before { transform: scaleX(1); }

.footer-contact {
  list-style: none; padding: 0; margin-top: var(--sp-4);
  display: flex; flex-direction: column; gap: var(--sp-2);
  font-size: var(--fs-sm);
}
.footer-contact address { font-style: normal; }
/* Marks the toll-free number so the two phone lines are told apart at a glance. */
.footer-contact__tag {
  display: inline-block;
  margin-left: var(--sp-2);
  padding: 1px 8px;
  border-radius: var(--radius-full);
  background: rgba(255, 255, 255, .14);
  color: var(--color-white);
  font-size: 0.72rem; font-weight: 600;
  text-transform: none; letter-spacing: 0;
  vertical-align: 1px;
}

.social-links { display: flex; gap: var(--sp-3); margin-top: var(--sp-5); }
.social-links a {
  width: 42px; height: 42px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--color-bg-panel);
  border: 1px solid var(--color-ink-300);
  color: var(--color-ink-900);
  box-shadow: none;
  transition: background-color var(--duration-base) var(--ease-out),
              transform var(--duration-base) var(--ease-snap),
              color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out);
}
.social-links a:hover {
  background: var(--color-accent-500);
  color: var(--color-white);
  transform: translate(-2px, -3px);
  box-shadow: none;
}

.footer-newsletter input {
  background: var(--color-bg-panel);
  border: 1px solid var(--color-ink-300);
  color: var(--color-ink-900);
}
.footer-newsletter input::placeholder { color: var(--color-ink-500); }

.footer-bottom {
  display: flex; flex-wrap: wrap; gap: var(--sp-3);
  justify-content: space-between; align-items: center;
  margin-top: var(--sp-6);
  padding-top: var(--sp-5);
  border-top: 1px solid var(--color-ink-300);
  font-size: var(--fs-xs);
  color: var(--color-ink-500);
}

/* --------------------------------------------------------------------------
   15. UTILITIES
   -------------------------------------------------------------------------- */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}
.text-center { text-align: center; }
.flex { display: flex; }
.flex-center { display: flex; align-items: center; justify-content: center; }
.flex-between { display: flex; align-items: center; justify-content: space-between; }
.flex-wrap { flex-wrap: wrap; }
.gap-2 { gap: var(--sp-2); }
.gap-3 { gap: var(--sp-3); }
.gap-4 { gap: var(--sp-4); }
.gap-5 { gap: var(--sp-5); }
.container--narrow { max-width: var(--container-narrow); }

/* --------------------------------------------------------------------------
   16. LEGAL PAGE PLACEHOLDERS
   Styled to read as unmistakably provisional — nobody should mistake these for
   publishable legal copy.
   -------------------------------------------------------------------------- */
.counsel-placeholder {
  color: var(--color-ink-500); font-style: italic;
  border-left: 3px solid var(--color-ink-300);
  padding-left: var(--sp-3);
  margin-block: var(--sp-3) var(--sp-6);
}
.callout-warn {
  border: 2px solid var(--color-error);
  background: var(--color-error-bg);
  border-radius: var(--radius-md);
  padding: var(--sp-5);
  margin-block: var(--sp-6);
}
.callout-warn h3 { margin-top: 0; margin-bottom: var(--sp-3); font-size: var(--fs-lg); color: var(--color-error); }
.callout-warn ul { margin: 0; padding-left: var(--sp-5); display: flex; flex-direction: column; gap: var(--sp-2); }
.callout-warn strong { color: var(--color-error); }

/* --------------------------------------------------------------------------
   17. REDUCED MOTION & PRINT
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
  /* Entrance animation must never leave content invisible when it can't run. */
  .reveal { opacity: 1; transform: none; }
}

@media print {
  .site-header, .mobile-nav, .sticky-sidebar-form, .sticky-mobile-bar,
  .back-to-top, .modal-overlay, .lead-form-section { display: none !important; }
  /* Entrance animation must never hide content on paper. */
  .reveal { opacity: 1 !important; transform: none !important; }
  body { background: #fff; padding-bottom: 0; }
  .hero::before, .hero::after, .site-footer::before, .site-footer::after { display: none; }
  a[href^="http"]::after { content: ' (' attr(href) ')'; font-size: 0.85em; color: #555; }
}

/* --------------------------------------------------------------------------
   18. FLOATING WHATSAPP ACTION
   Rendered only when siteConfig has a WhatsApp number. Sits above the mobile
   bar's reserved space, and expands to show its label on hover so the resting
   state stays a small, quiet target rather than a permanent banner.
   -------------------------------------------------------------------------- */
/* --------------------------------------------------------------------------
   MODAL, BACK-TO-TOP AND THE FLOATING RAIL

   Recovered. An earlier edit removed the retired sticky-sidebar-form section by
   slicing from its heading to the next comment banner, and took the modal and
   back-to-top rules with it. Without them .modal-overlay fell back to
   position:static, so the dialog rendered in normal flow after the footer and
   "open the form" scrolled the page four thousand pixels down to it - which is
   exactly what it looked like from the outside.
   -------------------------------------------------------------------------- */
.modal-overlay {
  position: fixed; inset: 0; z-index: 600;
  display: flex; align-items: flex-end; justify-content: center;
  background: var(--color-overlay);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  opacity: 0;
  transition: opacity var(--duration-base) var(--ease-out);
}
/* The hidden attribute has to WIN.

   The dialog is opened and closed purely by toggling `hidden`, but a class-level
   `display: flex` outranks the user agent's `[hidden] { display: none }`. So
   while "closed", the overlay was still a fixed, full-viewport, z-index 600 box
   sitting over the entire page: invisible at opacity 0, and swallowing every
   click that landed on it. Buttons appeared dead, and anything that did get
   through focused a field inside the overlay, which scrolled the page down to
   the bottom where the overlay's content sat.
   
   Belt and braces: display:none stops it participating at all, and
   pointer-events:none covers the moment between the attribute being removed
   and the opening transition starting. */
.modal-overlay[hidden] { display: none !important; pointer-events: none; }
.modal-overlay { pointer-events: none; }
.modal-overlay.is-open { opacity: 1; pointer-events: auto; }
@media (min-width: 640px) { .modal-overlay { align-items: center; } }
.modal {
  position: relative;
  width: 100%; max-width: 480px; max-height: 88dvh; overflow-y: auto;
  padding: var(--sp-6);
  background: var(--color-white);
  border: 1px solid var(--color-ink-300);
  border-bottom: none;
  border-radius: var(--radius-xl) var(--radius-xl) 0 0;
  transform: translateY(24px);
  transition: transform var(--duration-base) var(--ease-snap);
}
@media (min-width: 640px) { .modal { border-radius: var(--radius-xl); border-bottom: 1px solid var(--color-ink-300); } }
.modal-overlay.is-open .modal { transform: translateY(0); }
.modal__close {
  position: absolute; top: var(--sp-4); right: var(--sp-4);
  width: 34px; height: 34px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--color-ink-100);
  transition: background-color var(--duration-fast) var(--ease-out), transform var(--duration-fast) var(--ease-out);
}
.modal__close:hover { background: var(--color-ink-300); transform: rotate(90deg); }

/* The dialog introduces itself. The title and this line are rewritten from the
   CTA that opened it, so the copy answers the button the visitor actually
   pressed rather than greeting every one of them identically. */
.modal .lead-form__title {
  font-family: var(--font-display);
  font-size: var(--fs-xl); font-weight: 700;
  letter-spacing: var(--tracking-snug);
  color: var(--color-ink-900);
  margin: 0 var(--sp-8) var(--sp-2) 0;
}
.modal__subtitle {
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--color-ink-500);
  margin-bottom: var(--sp-5);
  max-width: 42ch;
}
/* A brand rule along the top edge, so the dialog belongs to the site. */
.modal::before {
  content: ''; position: absolute; inset: 0 0 auto 0; height: 3px;
  background: var(--color-primary-500);
  border-radius: var(--radius-xl) var(--radius-xl) 0 0;
}

.back-to-top {
  position: fixed; z-index: 390;
  bottom: calc(var(--sp-5) + env(safe-area-inset-bottom, 0px));
  right: var(--sp-5);
  width: 46px; height: 46px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--color-accent-500); color: var(--color-white);
  border: 1px solid var(--color-primary-600);
  box-shadow: none;
  opacity: 0; visibility: hidden;
  transform: translateY(10px);
  transition: opacity var(--duration-base) var(--ease-out),
              transform var(--duration-base) var(--ease-snap),
              visibility 0s linear var(--duration-base),
              background-color var(--duration-fast) var(--ease-out);
}
.back-to-top.is-visible {
  opacity: 1; visibility: visible; transform: translateY(0);
  transition: opacity var(--duration-base) var(--ease-out),
              transform var(--duration-base) var(--ease-snap),
              visibility 0s;
}
.back-to-top:hover { background: var(--color-primary-600); color: #fff; }
@media (max-width: 1023px) {
  .back-to-top { bottom: calc(var(--sticky-mobile-bar-height) + var(--sp-4) + env(safe-area-inset-bottom, 0px)); }
}
/* --------------------------------------------------------------------------
   18. FLOATING WHATSAPP ACTION
   Rendered only when siteConfig has a WhatsApp number. Sits above the mobile
   bar's reserved space, and expands to show its label on hover so the resting
   state stays a small, quiet target rather than a permanent banner.
   -------------------------------------------------------------------------- */

.wa-float {
  /* Right rail, stacked above back-to-top. On the left it sat on top of the
     body copy, because the content column starts at the container's left edge. */
  position: fixed; z-index: 395;
  right: var(--sp-5);
  bottom: calc(var(--sp-5) + 46px + var(--sp-3) + env(safe-area-inset-bottom, 0px));
  display: inline-flex; align-items: center; gap: var(--sp-2);
  height: 52px; padding: 0 14px;
  border: 1px solid var(--color-ink-300);
  border-radius: var(--radius-full);
  background: #25D366;
  color: #06331A;
  font-weight: 650; font-size: var(--fs-sm);
  box-shadow: none;
  transition: transform var(--duration-base) var(--ease-snap),
              box-shadow var(--duration-base) var(--ease-snap);
}
.wa-float:hover { filter: brightness(0.94); }
.wa-float:active { filter: brightness(0.88); }
.wa-float svg { flex: none; }
/* The label is revealed on hover/focus on pointer devices; on touch the button
   stays a circle so it never covers content. */
.wa-float__label { max-width: 0; overflow: hidden; white-space: nowrap; transition: max-width var(--duration-base) var(--ease-out); }
@media (hover: hover) {
  .wa-float:hover .wa-float__label,
  .wa-float:focus-visible .wa-float__label { max-width: 8rem; }
}
@media (max-width: 1023px) {
  .wa-float { bottom: calc(var(--sticky-mobile-bar-height) + var(--sp-4) + 46px + var(--sp-3) + env(safe-area-inset-bottom, 0px)); }
}
/* The label opens leftwards from the right edge, so it never runs off-screen. */
.wa-float { flex-direction: row-reverse; }
@media print { .wa-float { display: none !important; } }

/* --------------------------------------------------------------------------
   19. TABLES AND RULES IN CONTENT
   Both are written by the admin editor's toolbar. A wide table scrolls inside
   its own container so a phone never has to scroll the whole document sideways
   — which `body { overflow-x: hidden }` would otherwise turn into content you
   simply cannot reach.
   -------------------------------------------------------------------------- */
.table-scroll {
  margin-block: var(--sp-6);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  border: 1.5px solid var(--color-ink-300);
  border-radius: var(--radius-lg);
  background: var(--color-bg-panel);
}
.table-scroll table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
.table-scroll th,
.table-scroll td {
  padding: var(--sp-3) var(--sp-4);
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--color-ink-300);
}
.table-scroll thead th {
  background: var(--color-bg-alt);
  font-family: var(--font-body);
  font-size: var(--fs-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--color-ink-700);
  white-space: nowrap;
}
.table-scroll tbody th { font-weight: 650; color: var(--color-ink-900); }
.table-scroll tbody tr:last-child th,
.table-scroll tbody tr:last-child td { border-bottom: 0; }
.table-scroll tbody tr:hover { background: var(--color-primary-50); }

/* A thematic break, not a divider line: short, centred, and clearly a pause. */
.content-body hr {
  width: 96px;
  height: 3px;
  margin: var(--sp-8) auto;
  border: 0;
  border-radius: 3px;
  background: linear-gradient(90deg, var(--color-primary-500), var(--color-accent-500));
}


/* ---------------------------------------------------------------------------
   SECTION POLISH
   Small, cheap motion that makes the page feel responsive to the cursor without
   any of it becoming the point. Every rule here is switched off wholesale by
   the reduced-motion block at the end of the file.
   --------------------------------------------------------------------------- */

/* Picture cards: the frame holds still and the image moves inside it. */
.card--industry__media,
.card--editorial__media,
.index-card__media { overflow: hidden; }
.card--industry__media img,
.card--editorial__media img,
.index-card__media img {
  transition: transform var(--duration-slow) var(--ease-out);
}
.card--industry:hover .card--industry__media img,
.card--editorial:hover .card--editorial__media img,
.index-card:hover .index-card__media img { transform: scale(1.04); }

/* An in-content figure gets the same treatment on hover, at half the distance. */
.content-image img { transition: transform var(--duration-slow) var(--ease-out); }
.content-image { overflow: hidden; border-radius: var(--radius-md); }
.content-image:hover img { transform: scale(1.02); }

/* Anchored headings should not land under the sticky bar. */
h2[id], h3[id], .block { scroll-margin-top: calc(var(--header-height) + var(--sp-6)); }

/* Social icons: outline until you touch them, then the brand fills in. */
.social-links a { transition: background-color var(--duration-fast) var(--ease-out),
                              color var(--duration-fast) var(--ease-out),
                              transform var(--duration-base) var(--ease-out); }

@media (prefers-reduced-motion: reduce) {
  .card--industry__media img,
  .card--editorial__media img,
  .index-card__media img,
  .content-image img { transition: none; transform: none !important; }
}


/* ===========================================================================
   CARD & HERO POLISH

   One treatment applied across every card variant on the site, so a service
   card, an industry card and a blog card behave identically under the cursor
   instead of each having been styled on a different day.

   The vocabulary is deliberately small: a hairline that takes the brand colour,
   a shadow that appears rather than one that is always on, and a lift of three
   pixels. Depth arrives on interaction and is absent at rest, which is what
   keeps a page of twelve cards calm.
   =========================================================================== */

/* --- Shared behaviour -------------------------------------------------- */
.index-card,
.card--industry,
.card--editorial {
  position: relative;
  border-radius: var(--radius-lg);
  border: 1px solid var(--color-ink-300);
  background: var(--color-bg-panel);
  overflow: hidden;
  transition:
    transform var(--duration-base) var(--ease-out),
    box-shadow var(--duration-base) var(--ease-out),
    border-color var(--duration-base) var(--ease-out);
}
.index-card:hover,
.card--industry:hover,
.card--editorial:hover {
  transform: translateY(-3px);
  border-color: var(--color-primary-300);
  box-shadow: 0 2px 6px rgba(0, 14, 34, .05), 0 18px 40px rgba(0, 14, 34, .10);
}
/* Keyboard users get the same affordance as the cursor. */
.index-card:focus-within,
.card--industry:focus-within,
.card--editorial:focus-within {
  border-color: var(--color-primary-500);
  box-shadow: 0 2px 6px rgba(0, 14, 34, .05), 0 18px 40px rgba(0, 14, 34, .10);
}

/* A brand rule drawn along the top edge as the card is approached. Scaled from
   the left rather than faded in, so it reads as a line being drawn. */
.index-card::before,
.card--industry::before,
.card--editorial::before {
  content: '';
  position: absolute; inset: 0 0 auto 0; height: 3px;
  background: var(--gradient-brand);
  transform: scaleX(0); transform-origin: left;
  transition: transform var(--duration-slow) var(--ease-out);
  z-index: 3;
}
.index-card:hover::before,
.card--industry:hover::before,
.card--editorial:hover::before,
.index-card:focus-within::before,
.card--industry:focus-within::before,
.card--editorial:focus-within::before { transform: scaleX(1); }

/* --- The index numeral ------------------------------------------------- */
.index-card__num {
  background: var(--gradient-brand);
  color: var(--color-white);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  box-shadow: 0 2px 8px rgba(0, 81, 252, .28);
}

/* --- The arrow ---------------------------------------------------------- */
.index-card__arrow,
.card--sub__more::after {
  transition: transform var(--duration-base) var(--ease-out);
}
.index-card:hover .index-card__arrow { transform: translateX(4px); }

/* --- Ruled cards (the service rail) ------------------------------------- */
/* These are columns under a rule, not boxes, so they lift by weight instead:
   the rule thickens into the brand gradient and the column tints very faintly. */
.card--sub {
  padding-inline: var(--sp-3);
  margin-inline: calc(var(--sp-3) * -1);
  border-radius: var(--radius-md);
  transition: background-color var(--duration-base) var(--ease-out);
}
.card--sub::after { background: var(--gradient-brand); height: 3px; }
.card--sub:hover { background: color-mix(in srgb, var(--color-primary-50) 70%, transparent); }

/* --- FAQ ---------------------------------------------------------------- */
.faq-item { transition: border-color var(--duration-base) var(--ease-out); }
.faq-item:hover { border-color: var(--color-primary-300); }

/* --- The hero ----------------------------------------------------------- */
/* The picture is given a real frame: a hairline, a soft shadow that grounds it
   against the band, and a slow lift. Not the offset outline and floating dot
   that were there before - those were decoration standing in for art direction. */
.hero__media img {
  border-radius: var(--radius-lg);
  border: 1px solid var(--color-ink-300);
  box-shadow: 0 2px 8px rgba(0, 14, 34, .06), 0 24px 56px rgba(0, 14, 34, .10);
  transition: transform var(--duration-slow) var(--ease-out),
              box-shadow var(--duration-slow) var(--ease-out);
}
.hero__media-frame:hover img {
  transform: translateY(-4px);
  box-shadow: 0 4px 12px rgba(0, 14, 34, .08), 0 32px 72px rgba(0, 14, 34, .14);
}

/* The headline is the loudest thing on the page and is set accordingly. */
.hero h1 { letter-spacing: -0.04em; }

/* The trust line is separated by a gradient rule that fades out, rather than a
   hairline that stops dead halfway across the column. */
.hero__trust {
  border-top: 0;
  position: relative;
  padding-top: var(--sp-6);
}
.hero__trust::before {
  content: ''; position: absolute; inset: 0 0 auto 0; height: 1px;
  background: linear-gradient(90deg, var(--color-primary-300), transparent 78%);
}

@media (prefers-reduced-motion: reduce) {
  .index-card, .card--industry, .card--editorial,
  .index-card::before, .card--industry::before, .card--editorial::before,
  .hero__media img, .index-card__arrow {
    transition: none !important;
    transform: none !important;
  }
}


/* ===========================================================================
   FORMS
   The form is where the whole site is trying to get someone to, so it is worth
   the same care as the hero rather than being the one component left in default
   browser dress.
   =========================================================================== */

/* The panel. A brand rule along the top edge ties it to the site, and the
   shadow lifts it off the band it sits on instead of it being a white rectangle
   drawn on a tinted one. */
.lead-form {
  position: relative;
  overflow: hidden;
  padding: clamp(var(--sp-6), 4vw, var(--sp-8));
  border-radius: var(--radius-xl);
  border: 1px solid var(--color-ink-300);
  box-shadow: 0 2px 8px rgba(0, 14, 34, .05), 0 24px 56px rgba(0, 14, 34, .09);
}
.lead-form::before {
  content: '';
  position: absolute; inset: 0 0 auto 0; height: 4px;
  background: var(--gradient-brand);
}

/* Room to breathe between the promise and the thing being asked of you.
   The two were touching, which made the sentence read like a form caption
   rather than the reason to fill it in. */
.lead-form-section .section-lede,
.lead-form-section > .container > p { margin-bottom: clamp(var(--sp-6), 4vw, var(--sp-8)); }
.lead-form-section .lead-form { margin-top: var(--sp-2); }

/* --- Fields ------------------------------------------------------------- */
.form-field { margin-bottom: var(--sp-5); }
.form-field label {
  font-weight: 600;
  font-size: var(--fs-sm);
  color: var(--color-ink-900);
  margin-bottom: var(--sp-2);
  letter-spacing: var(--tracking-normal);
}
/* "(optional)" is information, not emphasis, so it is set quieter than the
   label it qualifies rather than shouting the same weight. */
.form-field__optional {
  font-weight: 500;
  color: var(--color-ink-500);
}

.form-field input,
.form-field select,
.form-field textarea {
  padding: 0.85em 1.05em;
  /* A hairline, not a 2px cage. The field is defined by its fill and its focus
     ring; a heavy permanent border makes eight of them read as a grid. */
  border: 1px solid var(--color-ink-300);
  border-radius: var(--radius-md);
  background-color: var(--color-bg-alt);
  color: var(--color-ink-900);
  font-size: var(--fs-base);
  line-height: 1.4;
  transition: border-color var(--duration-fast) var(--ease-out),
              box-shadow var(--duration-fast) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out);
}
.form-field input::placeholder,
.form-field textarea::placeholder { color: var(--color-ink-500); opacity: 1; }

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

/* Focus fills the field white and lights a soft brand ring, so the field you
   are in is unmistakable without the layout moving by a pixel.

   background-COLOR, never the `background` shorthand. The shorthand resets
   background-position and background-size to their initial values, and the
   select draws its chevron with two 6x6px diagonal gradients. Reset those and
   the gradients redraw at the full size of the element from its top-left
   corner: the whole control fills solid blue with a white wedge in it, which is
   exactly what opening the dropdown looked like. */
.form-field input:focus,
.form-field select:focus,
.form-field textarea:focus {
  outline: none;
  background-color: var(--color-white);
  border-color: var(--color-primary-500);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-primary-500) 16%, transparent);
}
.form-field textarea { min-height: 120px; }

/* The select gets a drawn chevron; the native arrow is a different shape in
   every browser and none of them match the breadcrumb's. */
.form-field select {
  appearance: none; -webkit-appearance: none;
  padding-right: 2.6em;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--color-ink-700) 50%),
    linear-gradient(135deg, var(--color-ink-700) 50%, transparent 50%);
  background-position: right 1.15em top 55%, right 0.8em top 55%;
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
}
.form-field select:focus {
  background-image:
    linear-gradient(45deg, transparent 50%, var(--color-primary-600) 50%),
    linear-gradient(135deg, var(--color-primary-600) 50%, transparent 50%);
}

/* An error is announced by colour AND by a tinted fill, so it does not rely on
   hue alone to be noticed. */
.form-field.has-error input,
.form-field.has-error select,
.form-field.has-error textarea {
  border-color: var(--color-error);
  background: var(--color-error-bg);
}
.form-field.has-error input:focus,
.form-field.has-error select:focus,
.form-field.has-error textarea:focus {
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-error) 16%, transparent);
}

.lead-form button[type="submit"] { margin-top: var(--sp-2); padding-block: 1.05em; }
.lead-form__note { margin-top: var(--sp-4); }

/* The section's own background: a flat tint, not a radial wash. */
.lead-form-section::before { display: none; }
.lead-form-section { background: var(--gradient-page), var(--color-bg-alt); }


/* The inline form is wider now that two of its fields share a row, so it fills
   the column it sits in instead of leaving a third of it empty. Still capped:
   a text input that runs the full width of a desktop page is harder to use,
   not easier. */
.lead-form { max-width: 44rem; }
.form-row .form-field:last-child { margin-bottom: var(--sp-5); }
.form-row { margin-bottom: 0; }
.modal .lead-form { max-width: none; }


.lead-form--inline { margin-top: var(--sp-6); }


/* Cards need room between them.

   The grids ran at 24px, which on a 1200px page puts three bordered panels
   close enough that the eye groups them as one striped block rather than as
   three things. The gap scales now: comfortable on a phone, generous on a
   desktop, and always larger than the padding inside the cards - which is the
   rule that stops a row of cards reading as a table. */
.grid { gap: clamp(var(--sp-5), 2.4vw, var(--sp-7)); }
.card-link-grid { gap: clamp(var(--sp-4), 2vw, var(--sp-6)); }
.bento { gap: clamp(var(--sp-5), 2.4vw, var(--sp-7)); }
.carousel__viewport { gap: clamp(var(--sp-5), 2.2vw, var(--sp-7)); }

/* Breathing room around a grid, so a card row is not welded to the paragraph
   above it or the heading below. */
.block > .grid,
.block > .card-link-grid,
.block > .bento,
.block > .carousel { margin-top: var(--sp-6); }

/* Cards carry a little more padding to match the extra space around them. */
.index-card__body,
.card--industry__body,
.card--editorial__meta { padding: clamp(var(--sp-5), 2vw, var(--sp-6)); }


/* ===========================================================================
   HEADER & FOOTER, FINISHED
   =========================================================================== */

/* No underline under the navigation at all.

   It has now been a flat rule, then a gradient rule with a glow. Both were the
   same idea in different clothes, and a line that draws itself under a nav item
   is one of the most copied hover effects on the web - which is exactly why it
   reads as generated.

   What replaces it is the thing itself moving: the item gets a soft tinted
   plate behind it, the way a key on a keyboard lights when your finger is over
   it. The current page keeps a quieter plate at rest, so "where I am" and
   "what I am pointing at" stay distinguishable without either being a line. */
@media (min-width: 1024px) {
  .primary-nav__link::after { display: none; }
  .primary-nav__link {
    padding: 0.5em 0.75em;
    border-radius: var(--radius-md);
    transition: background-color var(--duration-fast) var(--ease-out),
                color var(--duration-fast) var(--ease-out);
  }
  .primary-nav__link:hover {
    background: color-mix(in srgb, var(--color-primary-500) 9%, transparent);
    color: var(--color-primary-700);
  }
  .primary-nav__link[aria-current="page"] {
    background: color-mix(in srgb, var(--color-primary-500) 13%, transparent);
    color: var(--color-primary-700);
    font-weight: 600;
  }
  .primary-nav__list { gap: var(--sp-1); }
}

/* The Free Audit button is the site's one free offer, so it stops looking like
   a plain outline next to it. A brand-tinted fill, a brand hairline, and a glow
   that arrives on hover - loud enough to be found, quiet enough that it does
   not fight the solid Contact Us button beside it. */
.header-cta .btn--outline {
  background: color-mix(in srgb, var(--color-primary-500) 8%, transparent);
  border-color: color-mix(in srgb, var(--color-primary-500) 38%, transparent);
  color: var(--color-primary-700);
  font-weight: 600;
  transition: background-color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out),
              box-shadow var(--duration-base) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}
.header-cta .btn--outline:hover {
  background: color-mix(in srgb, var(--color-primary-500) 14%, transparent);
  border-color: var(--color-primary-500);
  color: var(--color-primary-700);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-primary-500) 12%, transparent);
}

/* The footer's column headings get the same lit rule, drawn short and left. */
.footer-col__title {
  position: relative;
  padding-bottom: var(--sp-3);
}
.footer-col__title::after {
  content: '';
  position: absolute; left: 0; bottom: 0;
  width: 28px; height: 2px; border-radius: 2px;
  background: var(--color-primary-400);
  opacity: .55;
}

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


/* The form has to be readable in one look on a small phone.

   At 360x640 it ran 722px against 564px of usable height, so the Send button
   sat below the fold and the whole thing read as longer than it is. Nothing is
   removed - the panel simply stops spending its height on padding. Keyed on
   the viewport's HEIGHT as well as its width, because a short laptop window has
   the same problem as a small phone and neither is helped by a rule that only
   asks how wide the screen is. */
@media (max-width: 430px), (max-height: 720px) {
  .lead-form { padding: var(--sp-4) var(--sp-4) var(--sp-3); }
  .form-field { margin-bottom: var(--sp-3); }
  .form-field label { margin-bottom: 3px; font-size: 0.875rem; }
  .form-field input,
  .form-field select,
  .form-field textarea { padding: 0.6em 0.85em; font-size: var(--fs-sm); }
  .form-field textarea { min-height: 52px; }
  .lead-form button[type="submit"] { padding-block: 0.85em; margin-top: 0; }
  .lead-form__note { margin-top: var(--sp-2); font-size: 0.8125rem; }   /* 13px: never below the 12px floor */
  .form-row { gap: var(--sp-2); }
  /* Two short fields still fit side by side at 360px once the padding comes
     down, and pairing them saves a whole row of height. */
  .form-row--2 { grid-template-columns: 1fr 1fr; }
}

/* ===========================================================================
   THE MARK, LARGER
   The lockup was set at 30px, which on a 76px bar left it looking like a
   footnote to its own navigation. It carries the brand; it can hold the line.
   =========================================================================== */
.logo img, .logo svg { height: 38px; }
.is-scrolled .logo img { height: 32px; }
.footer-brand img { height: 36px; width: auto; }
@media (max-width: 640px) {
  .logo img, .logo svg { height: 32px; }
  .is-scrolled .logo img { height: 28px; }
}


/* The section heading holds its position while its prose scrolls past.

   The two columns already start on the same line - measured, the box tops are
   identical to the pixel. The problem is what happens after: a block with five
   paragraphs leaves its heading marooned at the top with four hundred pixels of
   empty column beneath it, and by the time you are reading the last paragraph
   the heading that frames it is off the screen entirely.

   Sticky fixes both. The heading stays with the argument it introduces, and the
   left column stops reading as empty because something is always in it.
   align-self:start is required - a stretched grid item cannot stick. */
@media (min-width: 1040px) {
  /* NOT sticky. Not any more, and not conditionally.

     A sticky heading only ever made sense in the two-column layout this used to
     have, where the heading sat BESIDE its prose and could hold its place while
     the prose scrolled past. That layout is gone: the heading now sits directly
     ABOVE the prose it introduces, in the same column, so pinning it means it
     scrolls straight down over its own paragraphs.

     Scoping it with :has() only narrowed which blocks it happened on. The
     honest fix is that the feature belonged to a layout that no longer exists.
     A heading that scrolls with its section cannot overlap anything. */
}


/* ===========================================================================
   HERO CALLS TO ACTION
   The hero's job is to get one of two buttons pressed, so the primary is given
   the weight to be found without the pair turning into two competing buttons.
   =========================================================================== */
.hero__ctas { gap: var(--sp-4); margin-top: var(--sp-7); align-items: center; }

.hero__ctas .btn--primary {
  padding: 1.15em 2.2em;
  font-size: var(--fs-base);
  font-weight: 700;
  letter-spacing: var(--tracking-normal);
  /* A glow in the brand colour rather than a grey drop shadow: the button
     looks lit from within instead of stuck on top of the page. */
  box-shadow: 0 4px 14px color-mix(in srgb, var(--color-primary-500) 34%, transparent),
              0 2px 4px color-mix(in srgb, var(--color-primary-700) 22%, transparent);
  transition: transform var(--duration-base) var(--ease-out),
              box-shadow var(--duration-base) var(--ease-out),
              background-color var(--duration-fast) var(--ease-out);
}
.hero__ctas .btn--primary:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 24px color-mix(in srgb, var(--color-primary-500) 44%, transparent),
              0 3px 6px color-mix(in srgb, var(--color-primary-700) 26%, transparent);
}
.hero__ctas .btn--primary:active { transform: translateY(0); }

/* An arrow that leans forward on hover. Generated, not markup, so it never
   reaches a screen reader and never has to be kept in step with the label. */
.hero__ctas .btn--primary::after {
  content: '→';
  margin-left: 0.1em;
  transition: transform var(--duration-base) var(--ease-out);
}
.hero__ctas .btn--primary:hover::after { transform: translateX(4px); }

/* The secondary reads as the quieter of the two: no fill, no glow, and it only
   gains an ink edge on approach. */
.hero__ctas .btn--outline {
  padding: 1.15em 1.9em;
  font-size: var(--fs-base);
  border-color: var(--color-ink-300);
  background: color-mix(in srgb, var(--color-white) 70%, transparent);
}
.hero__ctas .btn--outline:hover { border-color: var(--color-ink-900); background: var(--color-white); }

@media (max-width: 560px) {
  /* Full-width targets on a phone: two buttons side by side at this size are
     two buttons that are both slightly too small to hit confidently. */
  .hero__ctas { flex-direction: column; align-items: stretch; gap: var(--sp-3); }
  .hero__ctas .btn { width: 100%; }
}

@media (prefers-reduced-motion: reduce) {
  .hero__ctas .btn--primary,
  .hero__ctas .btn--primary::after { transition: none; transform: none !important; }
}

/* Four fields in two rows plus a message and a button is as short as an audit
   form can honestly be, so the remaining height is spent on the message box -
   the one field where a cramped box actually discourages an answer. */
@media (min-width: 561px) and (max-height: 780px) {
  .form-field textarea { min-height: 84px; }
  .form-field { margin-bottom: var(--sp-4); }
  .lead-form { padding: var(--sp-5) clamp(var(--sp-5), 3vw, var(--sp-7)); }
}


/* ===========================================================================
   THE SERVICE RAIL, PROPERLY DESIGNED

   These were columns under a hairline: correct, restrained, and on a page of
   otherwise substantial cards they read as unfinished rather than as
   deliberate. They are cards now - but built the way the rest of the site
   builds cards, so the home page and the services page finally show the same
   component instead of two different ideas of one.
   =========================================================================== */
.carousel--sub { counter-reset: eqv-sub; }

.card--sub {
  counter-increment: eqv-sub;
  padding: var(--sp-6) var(--sp-5) var(--sp-5);
  margin-inline: 0;
  border: 1px solid var(--color-ink-300);
  border-top: 1px solid var(--color-ink-300);
  border-radius: var(--radius-lg);
  /* A surface that lifts toward the top, so the card has a light source rather
     than being a flat rectangle with a line on it. */
  background: linear-gradient(170deg, var(--color-primary-50) 0%, var(--color-bg-panel) 46%);
  overflow: hidden;
  transition: transform var(--duration-base) var(--ease-out),
              box-shadow var(--duration-base) var(--ease-out),
              border-color var(--duration-base) var(--ease-out);
}

/* The numeral. Drawn from a counter rather than written into the markup, so it
   renumbers itself when a service is added and can never disagree with the
   order the cards are actually in. */
.card--sub::before {
  content: counter(eqv-sub, decimal-leading-zero);
  position: absolute; top: var(--sp-5); right: var(--sp-5);
  font-size: var(--fs-base); font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
  color: var(--color-primary-500);
  opacity: .38;
  transition: opacity var(--duration-base) var(--ease-out);
}

/* The brand rule sits inside the rounded corners now, not across a square top. */
.card--sub::after {
  inset: 0 0 auto 0;
  height: 3px;
  background: var(--gradient-brand);
}

.card--sub h3 { padding-right: var(--sp-6); }

.card--sub:hover,
.card--sub:focus-within {
  transform: translateY(-3px);
  border-color: var(--color-primary-300);
  box-shadow: 0 2px 6px rgba(0, 14, 34, .05), 0 18px 40px rgba(0, 14, 34, .10);
  background: linear-gradient(170deg, var(--color-primary-100) 0%, var(--color-bg-panel) 52%);
}
.card--sub:hover::before,
.card--sub:focus-within::before { opacity: .8; }
.card--sub:focus-within::after,
.card--sub:hover::after { transform: scaleX(1); }

/* --- The rail's head --------------------------------------------------- */
/* The arrows were floating in space above the cards with nothing to sit
   against. They belong on the heading's line, at the far end of it. */
.carousel__head { margin-bottom: var(--sp-5); }
.carousel__btn { width: 40px; height: 40px; }
.carousel__btn:hover { transform: none; border-color: var(--color-primary-600); }

/* The rail sits closer to the heading that introduces it. */
.block > .carousel { margin-top: var(--sp-4); }
.carousel { margin-block: var(--sp-4) var(--sp-6); }

@media (prefers-reduced-motion: reduce) {
  .card--sub { transition: none; transform: none !important; }
}


/* --------------------------------------------------------------------------
   STICKY MOBILE BAR  (recovered)

   Lost along with the modal rules when a retired section was deleted by slicing
   from its heading to the next comment banner. Without them the bar was
   position:static and display:block at every width, so it rendered in normal
   flow after the footer as a stray "Get Your Free Audit" button on a white
   strip below the page - on desktop, where it is supposed to be hidden
   entirely.
   -------------------------------------------------------------------------- */
.sticky-mobile-bar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 400;
  min-height: var(--sticky-mobile-bar-height);
  display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3) calc(var(--sp-2) + env(safe-area-inset-bottom, 0px));
  background: color-mix(in srgb, var(--color-paper) 92%, transparent);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  border-top: 1px solid var(--color-ink-300);
  transform: translateY(100%);
  visibility: hidden;
  transition: transform var(--duration-base) var(--ease-out),
              visibility 0s linear var(--duration-base);
}
.sticky-mobile-bar.is-visible {
  transform: translateY(0); visibility: visible;
  transition: transform var(--duration-base) var(--ease-out), visibility 0s;
}
.sticky-mobile-bar--single { grid-template-columns: 1fr; }
@media (min-width: 1024px) { .sticky-mobile-bar { display: none !important; } }
.sticky-mobile-bar .btn { width: 100%; height: 100%; font-size: var(--fs-xs); padding-inline: var(--sp-2); box-shadow: none; }
/* Reserve the bar's space so it never covers the footer's legal links. */
@media (max-width: 1023px) {
  body { padding-bottom: calc(var(--sticky-mobile-bar-height) + env(safe-area-inset-bottom, 0px)); }
}
@media (max-width: 1023px) {
  .back-to-top { bottom: calc(var(--sticky-mobile-bar-height) + var(--sp-4) + env(safe-area-inset-bottom, 0px)); }
}

/* ===========================================================================
   FLOATING AUDIT BADGE

   A true circle, not a pill: the icon sits above a two-line label, both centred,
   so the badge says what it does at rest without needing a hover it will never
   get on a touch screen. Sized to fit "Free / Audit" comfortably at a legible
   weight rather than shrunk until the words only just squeeze in.
   =========================================================================== */
.audit-float {
  position: fixed; z-index: 396;
  right: var(--sp-5);
  /* Stacked above the WhatsApp pill, which itself sits above back-to-top. */
  bottom: calc(var(--sp-5) + 46px + var(--sp-3) + 52px + var(--sp-3) + env(safe-area-inset-bottom, 0px));
  width: 118px; height: 118px;
  display: none; flex-direction: column;
  align-items: center; justify-content: center; gap: 3px;
  padding: 0;
  border-radius: 50%;
  background: var(--gradient-brand);
  color: var(--color-white);
  text-align: center;
  font-size: 0.9375rem; font-weight: 700;
  line-height: 1.2;
  letter-spacing: 0.01em;
  box-shadow: 0 6px 20px color-mix(in srgb, var(--color-primary-500) 42%, transparent),
              0 2px 5px color-mix(in srgb, var(--color-primary-700) 28%, transparent);
  opacity: 0; transform: translateY(12px) scale(.88);
  transition: opacity var(--duration-base) var(--ease-out),
              transform var(--duration-base) var(--ease-snap),
              box-shadow var(--duration-base) var(--ease-out);
}
@media (min-width: 1024px) { .audit-float { display: flex; } }
.audit-float.is-visible { opacity: 1; transform: translateY(0) scale(1); }
.audit-float:hover {
  transform: translateY(-3px) scale(1.04);
  box-shadow: 0 10px 30px color-mix(in srgb, var(--color-primary-500) 55%, transparent),
              0 3px 7px color-mix(in srgb, var(--color-primary-700) 32%, transparent);
}
.audit-float:focus-visible { outline: 3px solid var(--color-primary-700); outline-offset: 4px; }

.audit-float__icon { display: grid; place-items: center; }
.audit-float__icon svg { width: 24px; height: 24px; }
.audit-float__label { display: block; }

/* Two rings, offset in time, so the badge is never momentarily still - a single
   ring spends most of its cycle invisible, which reads as an occasional blink
   rather than as something continuously alive. */
.audit-float::after,
.audit-float::before {
  content: '';
  position: absolute; inset: 0;
  border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--color-primary-500) 60%, transparent);
  animation: eqv-badge-pulse 2.6s cubic-bezier(.2, .6, .35, 1) infinite;
  pointer-events: none;
}
.audit-float::before { animation-delay: 1.3s; }
@keyframes eqv-badge-pulse {
  0%   { transform: scale(1);    opacity: .75; }
  70%  { transform: scale(1.42); opacity: 0; }
  100% { transform: scale(1.42); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .audit-float { transition: none; transform: none !important; }
  .audit-float::after,
  .audit-float::before { animation: none; opacity: 0; }
}


/* ===========================================================================
   HERO PRESENCE, AND SECTION BANDS

   Two complaints, one cause: the hero image grew but the hero itself did not,
   and every section below it sat on the same flat white, so a long page had no
   rhythm to it at all - just one continuous sheet with headings on it.
   =========================================================================== */

/* The hero has presence without running past the fold.

   Measured first: at clamp(64px, 6vw, 128px) of padding and a 4.4rem headline
   it needed 941px on a 1440x900 screen and 1071px on a phone, so the call to
   action - the entire point of the section - started below the fold on every
   common viewport. The vertical space is now capped by vh as well as by rem,
   which is the only way a hero can be generous on a tall screen and still fit
   on a short one. */
.hero {
  padding-block: clamp(var(--sp-6), 4.5vh, var(--sp-9)) clamp(var(--sp-6), 4vh, var(--sp-8));
}
.hero h1 { font-size: clamp(2.15rem, 1.1rem + 3.4vw, 3.5rem); }
.hero .container { gap: clamp(var(--sp-6), 3vw, var(--sp-8)); }
.hero p { margin-bottom: 0; }
.hero__ctas { margin-top: clamp(var(--sp-5), 3vh, var(--sp-6)); }
.hero__trust { margin-top: clamp(var(--sp-5), 3vh, var(--sp-6)); padding-top: clamp(var(--sp-4), 2.5vh, var(--sp-5)); }

/* The breadcrumb strip is chrome, and chrome above the fold is height the hero
   does not get to use. */
.breadcrumbs { padding-block: var(--sp-4) var(--sp-1); }

@media (max-width: 959px) {
  /* Stacked, the picture is what pushes the buttons under the fold. It keeps
     its aspect ratio and simply stops being allowed to dominate. */
  .hero__media img { max-height: 30vh; object-fit: cover; }
  .hero .container { gap: var(--sp-6); }
}
/* 16/10, matching the 1600x1000 the manifest actually ships. This briefly said
   4/3, and object-fit:cover dutifully cropped a strip off both sides of every
   hero image on the site - which is how the placeholder text lost its left
   edge. The frame follows the asset; the asset does not get cropped to fit a
   frame somebody picked. */
.hero__media img { aspect-ratio: 16 / 10; }

/* Alternating bands. Odd sections keep the paper; even ones take a very shallow
   tint, so the page reads as a sequence of surfaces rather than one sheet.

   Deliberately shallow: the tint has to stay light enough that muted copy still
   clears AA on it, which is why it is built from --color-primary-75 rather than
   from a stop somebody picked because it looked nice. */
main > .section:nth-of-type(even) {
  background: linear-gradient(180deg, #F7FAFF 0%, var(--color-primary-50) 50%, #F7FAFF 100%);
  border-block: 1px solid color-mix(in srgb, var(--color-primary-300) 34%, transparent);
}
/* Two tinted bands must never end up touching: without this, a section that
   happens to follow another tinted one reads as one very tall band. */
main > .section:nth-of-type(even) + .section { background: var(--color-paper); }

/* The closing lead-capture band keeps its own, slightly warmer surface so the
   end of the page is distinguishable from the middle of it. */
.lead-form-section {
  background: linear-gradient(180deg, var(--color-primary-50), #FFFFFF 60%);
  border-top: 1px solid color-mix(in srgb, var(--color-primary-300) 40%, transparent);
}


/* The heading column had 20rem, which forced "One Team for Your Website, Your
   Software and Your Search Rankings" into four cramped lines beside a column of
   body copy - the section read as congested because the heading was. It gets
   more room, more gap, and its own tighter leading. */
@media (min-width: 1040px) {
  :root { --block-col: 24rem; --block-gap: var(--sp-9); }
  .block > h2 { line-height: 1.12; }
}
@media (min-width: 1280px) {
  :root { --block-col: 26rem; --block-gap: var(--sp-10); }
}

/* Body copy in a block matches the body copy above it. */
.block__body p { font-size: var(--fs-base); line-height: var(--lh-normal); }
.block__body > * + * { margin-top: var(--sp-4); }

/* The cards a four-item grid produces are no longer inside a scroll container,
   so they take the card treatment directly rather than the rail's. */
.grid > .card--sub { height: 100%; }


/* ===========================================================================
   THE DIALOG FITS WITHOUT SCROLLING

   Measured: 780-920px of content against a 563-901px box, so every viewport
   got a scrollbar inside the popup. Two causes, both mine.

   First, a rule forcing .form-row--2 to a single column inside the dialog -
   written when the dialog shared a stylesheet with a 320px sidebar that really
   had no room for two. The sidebar is gone; the rule outlived it and was
   turning four rows into six.

   Second, the dialog was capped at 480px wide, which is narrower than the
   breakpoint the paired rows need. It is wider now, and the pairing is keyed
   to the dialog rather than to the viewport.
   =========================================================================== */
.modal {
  max-width: 34rem;
  padding: clamp(var(--sp-5), 3vh, var(--sp-6));
}
.modal .form-row--2 { grid-template-columns: 1fr 1fr; }
.modal .lead-form {
  border: 0; box-shadow: none; padding: 0; background: none;
}
.modal .lead-form::before { display: none; }   /* the dialog has its own top rule */
/* The dialog pays for the larger type out of its own spacing, not out of the
   type. Raising body copy to 19px pushed the dialog past the viewport at five
   of six sizes, and a popup form that scrolls is the thing this dialog exists
   not to be. */
.modal .form-field { margin-bottom: var(--sp-3); }
/* height, not just min-height. A textarea takes its height from the rows
   attribute, so a min-height below that never applies: the box stayed 141px
   tall while the rule said 56 and the dialog went on overflowing. It still
   resizes vertically, so anyone with more to say can drag it open. */
.modal .form-field textarea { min-height: 0; height: 88px; }
.modal .lead-form__note { margin-top: var(--sp-3); }

@media (max-height: 820px) {
  .modal { padding: var(--sp-4); }
  .modal .form-field { margin-bottom: var(--sp-3); }
  .modal .form-field label { margin-bottom: 3px; }
  .modal .form-field input,
  .modal .form-field select,
  .modal .form-field textarea { padding: 0.62em 0.85em; }
  .modal .form-field textarea { height: 72px; }
  .modal .modal__subtitle { margin-bottom: var(--sp-3); }
  .modal .form-row { gap: var(--sp-3); }
  .modal .lead-form__note { margin-top: var(--sp-2); }
}

/* Shorter still - a 640px-tall phone. The message box gives up the height,
   because it is the one field a visitor can expand themselves. */
@media (max-height: 700px) {
  /* On a short phone the dialog is the only thing on screen, so it can take
     more of it. 88dvh leaves a band of dimmed page above and below that nobody
     is reading; 95 keeps enough to show it is an overlay. */
  .modal { max-height: 95dvh; padding: var(--sp-3) var(--sp-4); }
  .modal .lead-form__title { font-size: var(--fs-lg); }
  .modal .modal__subtitle { margin-bottom: var(--sp-2); font-size: 0.8125rem; }
  .modal .form-field { margin-bottom: 6px; }
  .modal .form-field label { font-size: 0.8125rem; }
  .modal .form-row { gap: var(--sp-2); }
  .modal .form-field textarea { height: 56px; }
  .modal .lead-form__note { margin-top: var(--sp-2); font-size: 0.75rem; }
  .modal .lead-form button[type="submit"] { padding-block: 0.8em; }
}




/* Contact routes under the form, for anyone who would rather not fill one in. */
.lead-form__contact {
  margin-top: var(--sp-3);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--color-ink-300);
  text-align: center;
  font-size: var(--fs-sm);
  color: var(--color-ink-500);
}
.lead-form__contact a { color: var(--color-primary-700); font-weight: 600; }
.lead-form__contact a:hover { text-decoration: underline; }


/* ===========================================================================
   SECTION RHYTHM ON PROSE-ONLY PAGES

   The service pages are almost entirely paragraphs - no lists, no bold-led
   runs, nothing the section detectors can turn into a component - so they read
   as an undifferentiated column of text. Converting that prose into bullets
   would mean rewriting the client's approved copy, which is not on the table.

   What IS available is everything except the words: a numeral that gives the
   page a countable structure, a rule that says where one argument ends, and a
   first paragraph set slightly larger so each section opens with emphasis
   rather than starting flat. Not one character of copy changes.
   =========================================================================== */
.content-body { counter-reset: eqv-block; }

.block > h2 {
  counter-increment: eqv-block;
  position: relative;
  padding-top: var(--sp-5);
}
/* The rule and the numeral share the line above the heading. */
.block > h2::before {
  content: counter(eqv-block, decimal-leading-zero);
  position: absolute; top: 0; left: 0;
  font-size: var(--fs-sm);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
  color: var(--color-primary-500);
}
/* No rule above the heading. The numeral is enough of a marker; a line drawn
   by formula above every heading on the site is the decoration this design
   spent its first week removing. */

/* The opening paragraph of a section is MARKED, never guessed at.
   This was `.block__body > p:first-child`, lifting the first paragraph to
   1.18rem in a darker ink. It matched the first ELEMENT, and a paragraph is
   not one element: renderProse breaks long prose at its own sentence
   boundaries, so a single source paragraph becomes two or three <p>s. The rule
   caught the first fragment only, so the type changed size AND colour
   mid-paragraph, after the first sentence, in the middle of a thought. About a
   third of the body paragraphs on the site rendered that way.

   .section-lede already does this job and does it correctly, because the build
   puts the class on every chunk of a paragraph it splits - so a marked lede
   holds one size however it breaks. Two mechanisms for "the opening
   paragraph", giving two different sizes, was the inconsistency itself. */

/* The counter belongs to sections, not to the closing lead-capture band or the
   footer's call to action, neither of which is part of the argument. */
.lead-form-section .block > h2,
.site-footer .block > h2 { counter-increment: none; padding-top: 0; }
.lead-form-section .block > h2::before,
.site-footer .block > h2::before { content: none; }


/* ===========================================================================
   AUDIT FIXES
   =========================================================================== */

/* The logo must not be squashed when the nav runs out of room.

   .logo is a flex item in the header row. From 1024px the desktop nav appears,
   and between 1024 and 1159 there is not enough width for logo + nav + two
   buttons - so flex shrank the logo. Its height is pinned at 38px and the
   global img{max-width:100%} then clamped the width, which breaks the aspect
   ratio rather than scaling it: 159x38 became 41x38 at 1024px, a 74% horizontal
   squash that turned the wordmark into a smear. Intrinsic width/height
   attributes do not prevent this - flex shrinking overrides them.

   The logo holds its size; the navigation gives up the space instead, which it
   can afford because its gap is already fluid. */
.logo { flex: none; }
.logo img, .logo svg { flex: none; max-width: none; }
/* The band where the full desktop header only just fits. Seven nav links, a
   159px logo and two buttons come to 956px at 1024, and the gaps and the
   container's own padding take it past the viewport - so here the row is given
   its space back out of the spacing between things, not out of the things. */
@media (min-width: 1024px) and (max-width: 1199px) {
  .site-header .container { gap: var(--sp-3); padding-inline: var(--sp-5); }
  .primary-nav__list { gap: 2px; }
  .primary-nav__link { padding-inline: var(--sp-2); font-size: 0.875rem; }
  .header-cta { gap: var(--sp-2); }
  .header-cta .btn { padding-inline: 0.85em; }
}

/* The floating badge must not be a live 118px target while invisible.

   At opacity 0 it kept visibility:visible and pointer-events:auto, so before
   the reveal threshold it sat in the bottom-right corner swallowing clicks on
   whatever was underneath and opening the dialog from what looked like blank
   page. It was also still in the tab order. back-to-top and the mobile bar
   both use visibility for exactly this reason; this one did not. */
.audit-float {
  visibility: hidden;
  pointer-events: none;
  transition: opacity var(--duration-base) var(--ease-out),
              transform var(--duration-base) var(--ease-snap),
              box-shadow var(--duration-base) var(--ease-out),
              visibility 0s linear var(--duration-base);
}
.audit-float.is-visible {
  visibility: visible;
  pointer-events: auto;
  transition: opacity var(--duration-base) var(--ease-out),
              transform var(--duration-base) var(--ease-snap),
              box-shadow var(--duration-base) var(--ease-out),
              visibility 0s;
}

/* ===========================================================================
   A PAIRED ROW ALIGNS ON ITS INPUTS, NOT ON ITS LABELS

   "Phone / WhatsApp (optional)" wraps to two lines in a 135px column and
   "Project type" does not, so at 360px and 390px the two inputs in that row
   started 23px apart and the row read as broken. The label copy is approved,
   so the layout has to survive the wrap rather than avoid it.

   Subgrid puts the label and the control of BOTH fields in the same two parent
   tracks: the label track is as tall as the taller label, and both controls
   therefore begin at the top of the second track whatever the labels do. A
   field showing an error takes a third track for the message - without that,
   the message is clamped into the last track and prints over the input.
   =========================================================================== */
.form-row--2 > .form-field {
  display: grid;
  grid-template-rows: subgrid;
  grid-row: span 2;
  /* A subgrid inherits the parent's gutters, and .form-row sets gap for the
     space BETWEEN fields, not inside one. Left alone it landed 16px between
     every label and its input on desktop. The gutter between the two fields is
     a parent gap no subgrid spans, so it is untouched by this. */
  row-gap: 0;
  /* Not the default stretch. The two fields carry different bottom margins -
     12px on the first, 24px on the one the :last-child rule catches - so a
     stretched control grew to eat the difference and the left-hand input came
     out 56px tall against its partner's 44px. Start pins both controls to the
     top of the track and leaves the slack below, where it cannot be seen. */
  align-items: start;
}
.form-row--2 > .form-field.has-error { grid-row: span 3; }


/* ===========================================================================
   TAP TARGETS REACH 44px WITHOUT GROWING ON SCREEN

   Measured in a browser, not estimated: header CTAs 31px tall, breadcrumb
   links 33px, footer legal links 21px, the dialog's close button 34px square.
   All under the 44x44 of WCAG 2.5.5, and the footer pair under the 24x24 floor
   of 2.5.8. The design is signed off, so the reach is grown with transparent
   overlays and with padding that is taken straight back off as margin. Nothing
   below changes a pixel of what is painted.
   =========================================================================== */

/* The header's buttons are a 31px pill in a 76px bar, so the room is there.
   Vertical only: the two sit side by side, and widening them would put one
   button's hit area inside the other's. */
.header-cta .btn::after {
  content: '';
  position: absolute;
  inset: -7px 0;
}

/* The trail scrolls sideways, which makes its overflow-y computed value auto
   as well, so an overlay hung outside the list is clipped away and buys
   nothing. The list is padded to open the clip box by exactly what the overlay
   needs, and the padding is removed again as negative margin so the band keeps
   the height it was designed with.

   The reach is 12px up and 4px down, not 8 and 8, because that is the band's
   own padding. Reaching past it puts the overlay under the hero section that
   follows, which paints later and wins the hit test - measured: a symmetric
   overlay came back as 42px because its bottom 3px were being answered by the
   hero. Asymmetric, it stays inside the band and measures 48px. */
.breadcrumbs ol { padding-block: 12px 4px; margin-block: -12px -4px; }
.breadcrumbs a::before {
  content: '';
  position: absolute;
  inset: -12px 0 -4px;
}

/* Legal links: reach above and below, never sideways. The two sit 16px apart
   on one line and a sideways overlay would put half of "Terms of Service"
   inside "Privacy Policy". What the 12px above meets is the copyright line,
   which is 12px away and is not a link, so nothing is stolen from it. */
.footer-bottom a { position: relative; }
.footer-bottom a::after {
  content: '';
  position: absolute;
  inset: -12px 0;
}

/* The dialog's close button rotates a quarter turn on hover, and a square
   overlay rotated a quarter turn is the same square. */
.modal__close::after {
  content: '';
  position: absolute;
  inset: -5px;
}

/* A field's hit area is the field, so this is the one target that cannot be
   grown with an overlay. On a small phone the compressed padding left the
   inputs at 41px and Send at 42.5px; they take the missing 3px and nothing
   more, and only where they were short - every field is already 54px from
   768px up, where this floor does nothing at all. */
.form-field input,
.form-field select { min-height: 44px; }
.lead-form button[type="submit"] { min-height: 44px; }

/* ---------------------------------------------------------------------------
   PACKAGE CARDS

   The pricing page used to set its three packages as stacked label-and-body
   rows, so "$800-$8,000" sat mid-line in the same weight as the sentence around
   it and there was nothing to compare across. Prices are the one thing on that
   page a reader scans rather than reads, so they get a column each and the
   amount is lifted out onto its own line.

   The gradient and the shadow are the tokens the rest of the site already uses
   - the same band wash the heroes carry and the soft elevation used wherever
   something genuinely sits above the page. Nothing bespoke: a pricing card that
   invented its own palette would read as a different site.
   ------------------------------------------------------------------------- */
.package-grid {
  display: grid;
  gap: var(--sp-5);
  margin-block: var(--sp-6);
  /* Out of the prose measure.
     .block__body caps its children at 44rem, which is the right width for a
     line of text and the wrong one for three columns of it: the cards came out
     219px wide and 637px tall, a shape that reads as a fault rather than a
     comparison. Cards are not prose, so they take the block's full width and
     each column ends up around the measure of a narrow paragraph. */
  max-width: none;
}

.package-card {
  display: grid;
  align-content: start;
  gap: var(--sp-2);
  padding: var(--sp-6);
  border: 1px solid var(--color-primary-100);
  border-radius: var(--radius-lg);
  background-image: var(--gradient-band);
  box-shadow: var(--shadow-soft);
  transition: transform var(--duration-base) var(--ease-out),
              box-shadow var(--duration-base) var(--ease-out),
              border-color var(--duration-base) var(--ease-out);
}

.package-card > h3 {
  margin: 0;
  font-size: var(--fs-lg);
  line-height: var(--lh-tight);
}

.package-card__price {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--fs-2xl);
  line-height: 1.1;
  color: var(--color-primary-700);
  /* Figures line up column to column, which is the entire reason to set the
     prices side by side. */
  font-variant-numeric: tabular-nums;
  padding-bottom: var(--sp-4);
  /* A hairline, not a filled header bar: the card is already tinted, and a
     second block of colour inside it would fight the gradient. */
  border-bottom: 1px solid var(--color-primary-100);
}

.package-card__body { padding-top: var(--sp-4); }

.package-card__body p {
  margin: 0;
  font-size: var(--fs-sm);
  /* No --measure here. The card is already narrower than a comfortable line,
     so capping it again would strand text against the right edge. */
}

.package-card__body p + p { margin-top: var(--sp-3); }

@media (hover: hover) {
  .package-card:hover {
    transform: translateY(-3px);
    box-shadow: var(--shadow-soft-lg);
    border-color: var(--color-primary-300);
  }
}

@media (prefers-reduced-motion: reduce) {
  .package-card { transition: none; }
  .package-card:hover { transform: none; }
}

/* Side by side only once each column can still hold a readable line. Below
   that the cards stack, because three 190px columns of body copy is worse than
   one column of three. */
/* Side by side, every card lines up part for part.
   Equal card heights are not enough on their own: one name that wraps to two
   lines pushed that card's price and rule below its neighbours', so three cards
   of identical height had three different horizons across them. Subgrid hands
   each card the row track its siblings are using, so names, prices, rules and
   copy all start on the same line however the text wraps. */
.package-grid--2,
.package-grid--3 { grid-template-rows: auto auto 1fr; }

.package-grid--2 > .package-card,
.package-grid--3 > .package-card {
  grid-row: span 3;
  grid-template-rows: subgrid;
}

@media (min-width: 720px) {
  .package-grid--2 { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 960px) {
  .package-grid--3 { grid-template-columns: repeat(3, 1fr); }
}

/* ---------------------------------------------------------------------------
   THANK YOU

   Where every form lands. The job of this page is to confirm that the thing
   the visitor just did worked, and then give them somewhere to go — so the
   confirmation is the loudest element on it and everything else is quiet.
   ------------------------------------------------------------------------- */
.thanks-hero { text-align: center; }
.thanks-hero .section-lede,
.thanks-hero .lead-form__contact { margin-inline: auto; }
.thanks-hero .section-lede { max-width: 34rem; }

/* The tick, drawn once and large.
   A ring rather than a filled disc: a solid block of brand colour at this size
   reads as an alert, and this is the opposite of an alert. */
/* .hero .thanks-mark, not .thanks-mark: the hero sets a colour on every <p>
   inside it at higher specificity, so the tick was drawn in body-text ink on a
   solid blue disc - dark on dark, and all but invisible. */
.hero .thanks-mark {
  display: grid;
  place-items: center;
  width: 72px;
  height: 72px;
  margin: 0 auto var(--sp-5);
  border-radius: var(--radius-full);
  color: #fff;
  background-image: var(--gradient-brand);
  box-shadow: 0 0 0 8px var(--color-primary-50), var(--shadow-soft);
  animation: eqv-thanks-in var(--duration-slow) var(--ease-snap) both;
}
.hero .thanks-mark svg { width: 34px; height: 34px; stroke-width: 2.4; }

@keyframes eqv-thanks-in {
  from { opacity: 0; transform: scale(.72); }
  to   { opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .hero .thanks-mark { animation: none; }
}

/* Three ways onward. A card each, because a row of links at the end of a page
   nobody planned to visit gets skipped. */
.next-grid {
  display: grid;
  gap: var(--sp-5);
  max-width: none;          /* not prose: it takes the block's full width */
}
@media (min-width: 760px) {
  .next-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.next-card {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-5);
  border: 1px solid var(--color-primary-100);
  border-radius: var(--radius-lg);
  background-image: var(--gradient-band);
  box-shadow: var(--shadow-soft);
  color: inherit;
  text-decoration: none;
  transition: transform var(--duration-base) var(--ease-out),
              box-shadow var(--duration-base) var(--ease-out),
              border-color var(--duration-base) var(--ease-out);
}
.next-card h3 { margin: 0; font-size: var(--fs-lg); }
.next-card p {
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--color-ink-700);
}
.next-card__link {
  margin-top: auto;
  padding-top: var(--sp-3);
  font-weight: 600;
  color: var(--color-accent-text);
}
.next-card__link span { display: inline-block; transition: transform var(--duration-fast) var(--ease-out); }

@media (hover: hover) {
  .next-card:hover {
    transform: translateY(-3px);
    box-shadow: var(--shadow-soft-lg);
    border-color: var(--color-primary-300);
  }
  .next-card:hover .next-card__link span { transform: translateX(3px); }
}
@media (prefers-reduced-motion: reduce) {
  .next-card, .next-card__link span { transition: none; }
  .next-card:hover { transform: none; }
}
