/* ==========================================================================
   RevHops design system
   Navy #304157 · Deep #1C2635 · Slate #6B7A93 · Mist #C6D3E0
   Warm #F2C39B (accent only) · Paper #FAFAF8
   Ubuntu headings and navigation · Lato body copy
   12px radius on cards · 10px on buttons · nothing fully pill-shaped
   ========================================================================== */

:root {
  --navy: #304157;
  --deep: #1C2635;
  --slate: #6B7A93;
  --mist: #C6D3E0;
  --warm: #F2C39B;
  /* the doc's Do / Don't annotation colours, used as success and failure */
  --ok:      #5C7A5C;
  --ok-bg:   #F1F6F1;
  --ok-line: #D5E3D5;
  --no:      #96564C;
  --no-bg:   #FBF3F2;
  --no-line: #EDD8D5;
  --paper: #FAFAF8;

  /* Every card surface. Slightly translucent so the gradient circles read
     faintly behind them, on every card, not just the hero's. */
  --card-veil: rgba(255, 255, 255, .92);

  /* Text that sits on a navy surface — the navy sections and the footer.
     These exist as tokens purely so dark mode can flip them: there, the
     navy surface becomes light and this text has to become dark. */
  --on-navy:        #C9D4E2;
  --on-navy-strong: #D3DDEA;
  --on-navy-muted:  #A9B8CB;
  --on-navy-faint:  #8798AE;
  --on-navy-head:   #A9BDD6;
  --on-navy-line:   rgba(255, 255, 255, .12);
  --on-navy-field:  rgba(250, 250, 248, .07);
  --on-navy-edge:   rgba(250, 250, 248, .22);

  --ink: #304157;
  --body: #5A6779;
  --muted: #94A0B2;
  --line: #E6E9EE;
  --line-soft: #F0F2F5;
  --surface: #FFFFFF;
  --surface-alt: #F4F5F7;

  /* --------------------------------------------------------------------
     Tokens the dark theme swaps. Split out of --navy on 7 September.

     --navy was doing two unrelated jobs: it was the dark GROUND under the
     footer, the floating nav bar and the primary button, and it was also
     the strongest INK on the paper — links, arrows, stars, focus rings.
     Those two want opposite things when the page goes dark, so --navy now
     means the ground only and --ink-strong means the ink. Anything that
     paints a dark chip keeps --navy and therefore keeps its appearance in
     both themes, which is exactly what James asked for the footer and the
     floating nav.
     -------------------------------------------------------------------- */
  --ink-strong: #304157;    /* strongest ink on the page ground */
  --ink-hover:  #1C2635;    /* and where it goes on hover */
  --paper-step: #FCFCFC;    /* the midpoint of the drift from paper to white */
  --plate:      #FFFFFF;    /* small fields and chips sitting on the page */
  --plate-line: #DFE4EB;
  --plate-ink:  #63718A;
  --pill-bg:    #EEF2F6;
  --track:      #DFE5EC;    /* the slider's rail */
  --track-ring: #C2CBD8;

  --radius-sm: 7px;       /* chips and small tags */
  --radius-btn: 10px;     /* buttons, never a full pill */
  --radius-card: 12px;    /* cards, panels, framed surfaces */

  --font-head: 'Ubuntu', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-body: 'Lato', system-ui, -apple-system, 'Segoe UI', sans-serif;

  /* THE SECTION RHYTHM, one number for the whole site.

     Every band between two sections is this twice, because every section
     carries it on both sides and nothing overrides it. That is the point:
     the page used to run 116 + 72 here, 92 + 0 there and 84 + 36 somewhere
     else, and a reader with no ruler still saw that the gaps were all
     different. If a section needs more or less air, the answer is this
     token, not a local override. */
  --sec-pad: clamp(56px, 6.4vw, 96px);

  --shell: 1240px;
  --gutter: clamp(20px, 5vw, 64px);
  --nav-h: 86px;
  --nav-float-gap: 20px;              /* floating bar's clearance from the top */
  /* Its width once collapsed. The floor was added on 12 September: the bar
     now carries seven links and a button reading "Schedule a discovery call",
     and at 77.6vw alone that content overflowed the pill on every laptop
     narrower than about 1366px. The floor only binds below that width; above
     it, 77.6vw is still what wins and nothing has changed. */
  --nav-float-w: min(1417px, max(77.6vw, 1060px));
  --nav-float-w-sm: calc(100% - 24px);

  --hero-bleed-tail: 62vw; /* how far hero discs carry into the section below */
  /* How far the clipping box reaches ABOVE the document's top edge, so
     elastic overscroll does not expose a cut line across the orange disc.

     In vw, not pixels, and this matters: disc-a sits at top -46vw, so the
     height that needs covering scales with the window. A flat 300px was
     enough at no width worth having — on a 1440px screen the disc still
     ran 362px past the clip. The rule is H >= 46vw; 52vw leaves margin. */
  --hero-bleed-head: 52vw;

  --mcard-t: .62s;                            /* stage-card hover, kept slow */
  --mcard-ease: cubic-bezier(.33, 0, .2, 1);

  /* The nav collapse gets its own pair. --ease-out is an expo curve that
     covers half the distance in the first tenth of the time, which reads as
     a jolt on something as large as the whole bar resizing. This one eases
     in before it accelerates, over a longer beat. */
  --nav-t: .62s;
  --nav-ease: cubic-bezier(.38, .02, .2, 1);

  --ease: cubic-bezier(.22, .61, .36, 1);
  --ease-out: cubic-bezier(.16, 1, .3, 1);
}

/* ---------- reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body {
  margin: 0;
  background-color: var(--paper);
  color: var(--body);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  /* `clip`, not `hidden`. overflow-x: hidden makes the body a scroll
     container, and on a touch device that container swallows the horizontal
     swipe before the case rail inside it ever sees it — the rail would move
     with its arrows and with scrollLeft, and not with a thumb. `clip` still
     stops the page itself sliding sideways, without creating a scroller.
     <main> has been on clip for the same reason. */
  overflow-x: clip;
}
img, svg { max-width: 100%; display: block; }
button { font: inherit; color: inherit; }
a { color: var(--ink-strong); text-decoration: none; }
a:hover { color: var(--ink-hover); }

h1, h2, h3, h4, h5 {
  font-family: var(--font-head);
  font-weight: 700;
  color: var(--ink);
  margin: 0;
  text-wrap: balance;
}
p { margin: 0; text-wrap: pretty; }
ul { margin: 0; padding: 0; list-style: none; }

/* ---------- type scale — headings are heavy throughout ---------- */
.h1 {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(2.1rem, 1.3rem + 3.2vw, 3.3rem);
  line-height: 1.05;
  letter-spacing: -.03em;
  color: var(--ink);
}
.h2 {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(1.6rem, 1.15rem + 1.9vw, 2.3rem);
  line-height: 1.1;
  letter-spacing: -.026em;
  color: var(--ink);
}
.h3 {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(1.08rem, .98rem + .45vw, 1.26rem);
  line-height: 1.3;
  letter-spacing: -.014em;
  color: var(--ink);
}
.lede {
  font-size: clamp(1.02rem, .96rem + .35vw, 1.18rem);
  line-height: 1.6;
  color: var(--body);
  max-width: 62ch;
}
.small { font-size: .875rem; line-height: 1.6; }
.muted { color: var(--muted); }
/* visible to screen readers only — form labels behind placeholders */
.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;
}

/* ---------- layout ---------- */
.shell {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.shell-wide { max-width: 1380px; }
.section { padding-block: var(--sec-pad); }
/* .section-tight predates the shared rhythm and is still in the markup on a
   few pages. It is the same section now. */
.section-tight { padding-block: var(--sec-pad); }
.stack { display: flex; flex-direction: column; }
.gap-8 { gap: 8px; } .gap-10 { gap: 10px; } .gap-12 { gap: 12px; } .gap-16 { gap: 16px; }
.gap-20 { gap: 20px; } .gap-28 { gap: 28px; } .gap-40 { gap: 40px; }
.surface-white { background: var(--surface); }
.surface-navy { background: var(--navy); color: var(--on-navy); }
.surface-navy h1, .surface-navy h2, .surface-navy h3,
.surface-navy .h1, .surface-navy .h2, .surface-navy .h3 { color: var(--paper); }
.surface-navy .lede { color: var(--on-navy); }

/* ---------- buttons — soft corners, never a pill ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  padding: 13px 24px;
  border-radius: var(--radius-btn);
  border: 1px solid transparent;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: .85rem;
  letter-spacing: .01em;
  cursor: pointer;
  position: relative;
  transition: background .22s var(--ease), color .22s var(--ease),
              border-color .22s var(--ease), transform .22s var(--ease-out),
              box-shadow .22s var(--ease);
  text-align: center;
}
.btn:hover { transform: translateY(-2px); }
.btn:active { transform: translateY(0); }
.btn:focus-visible { outline: 2px solid var(--ink-strong); outline-offset: 3px; }
/* Filled buttons are navy on the light theme and warm on the dark one, and
   hovering either way lands on warm. The hover label is a literal navy
   rather than var(--paper): on the warm fill, paper reads at 1.5:1 and navy
   at 6.5:1, and the literal holds in both themes. */
.btn-primary { background: var(--navy); color: var(--paper); }
.btn-primary:hover {
  background: var(--warm); color: #304157;
  box-shadow: 0 12px 26px -14px rgba(48, 65, 87, .5);
}
.btn-light { background: var(--paper); color: var(--navy); }
.btn-light:hover { background: var(--warm); color: #304157; box-shadow: 0 12px 26px -16px rgba(0,0,0,.45); }
/* The quiet second button. Only used where a page offers two real actions
   rather than one ask and a text link after it — /hubspot's header is the
   first. Navy on the light theme and white on the dark one, both through
   --ink-strong, and warm on hover like every other button.

   The two .btn-outline rules further down are scoped to the puzzle and run
   overlays, which sit on a fixed navy board in both themes. They come later
   in the file and are more specific, so they still win inside those. */
.btn-outline {
  background: transparent;
  color: var(--ink-strong);
  border-color: var(--ink-strong);
}
.btn-outline:hover { background: var(--warm); border-color: var(--warm); color: #304157; }
.btn-row { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }
/* .text-link carries align-self: flex-start of its own, which outranks the
   row's align-items and sat it 14px above the centre of the button beside
   it. Standing alone in a stack that is right; next to a 50px button it is
   two things that were meant to be one row. */
.btn-row > .text-link { align-self: center; }
/* and a little more air than the row's 12px, because a text link ends in an
   arrow and the arrow was pointing at the button rather than at its own
   destination */
.btn-row > .text-link:not(:last-child) { margin-inline-end: 8px; }

/* ---------- chips and cards ---------- */
.chip {
  display: inline-flex;
  align-items: center;
  padding: 5px 10px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--plate-line);
  background: var(--plate);
  color: var(--plate-ink);
  font-size: .74rem;
  font-weight: 700;
  letter-spacing: .01em;
  white-space: nowrap;
  transition: background var(--mcard-t) var(--mcard-ease), border-color var(--mcard-t) var(--mcard-ease), color var(--mcard-t) var(--mcard-ease);
}
.card {
  background: var(--card-veil);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: clamp(24px, 3vw, 34px);
  display: flex;
  flex-direction: column;
  gap: 14px;
  position: relative;
  /* matches the hero cards: same duration, same curve */
  transition: border-color var(--mcard-t) var(--mcard-ease),
              transform .5s var(--ease-out),
              box-shadow var(--mcard-t) var(--mcard-ease);
}
/* One hover for every card on the site — the 4px lift and deep soft shadow
   the hero cards use. Previously .card only shifted its shadow while the
   hero cards lifted, so the two read as different components. */
.card:hover,
.card-link:hover,
.price-teaser-cell:hover {
  transform: translateY(-4px);
  box-shadow: 0 26px 52px -28px rgba(28, 38, 53, .38);
}
.card-link:hover { border-color: #C7D1DE; }
.card-link .arrow { transition: transform .3s var(--ease-out); display: inline-block; }
.card-link:hover .arrow { transform: translateX(5px); }
.grid { display: grid; gap: clamp(16px, 2vw, 24px); }
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.split {
  display: grid;
  gap: clamp(28px, 5vw, 72px);
  grid-template-columns: minmax(0, .95fr) minmax(0, 1.05fr);
  align-items: start;
}

/* ticked lists */
.ticks { display: flex; flex-direction: column; gap: 11px; }
.ticks li {
  position: relative;
  padding-left: 26px;
  font-size: .95rem;
  line-height: 1.6;
  color: var(--body);
}
.ticks li::before {
  content: "";
  position: absolute;
  left: 0;
  top: .62em;
  width: 11px;
  height: 2px;
  border-radius: 2px;
  background: var(--warm);
  transform-origin: left;
}
.surface-navy .ticks li { color: var(--on-navy); }
.reveal .ticks li::before { transform: scaleX(0); }
.reveal.is-in .ticks li::before { transform: scaleX(1); transition: transform .5s var(--ease-out); }
.reveal.is-in .ticks li:nth-child(1)::before { transition-delay: .10s; }
.reveal.is-in .ticks li:nth-child(2)::before { transition-delay: .17s; }
.reveal.is-in .ticks li:nth-child(3)::before { transition-delay: .24s; }
.reveal.is-in .ticks li:nth-child(4)::before { transition-delay: .31s; }
.reveal.is-in .ticks li:nth-child(5)::before { transition-delay: .38s; }
.reveal.is-in .ticks li:nth-child(6)::before { transition-delay: .45s; }
.reveal.is-in .ticks li:nth-child(n+7)::before { transition-delay: .52s; }

/* gradient disc: the one decorative device.

   Layering, site-wide, so a disc can bleed across a section boundary
   without ever landing on top of something readable:

     section        position:relative, NO z-index  — background layer
     .disc          z-index 1                      — above every background
     section .shell z-index 2                      — above every disc

   The sections must not carry a z-index of their own. One that does forms
   a stacking context, which traps its disc at that level and puts the
   whole subtree above the next section's content as well as its
   background — which is exactly the bug this replaced. */
.disc {
  position: absolute;
  border-radius: 50%;
  background: radial-gradient(circle at 32% 28%, #F7D9BE 0%, #F2C39B 38%, #C6D3E0 100%);
  pointer-events: none;
  z-index: 1;
  will-change: transform;
}

main > section { position: relative; }

/* Section content runs to 60px from each screen edge rather than being
   capped at --shell and centred. The hero is excluded because its .shell is
   a grandchild of the section, not a child — it keeps its own wider cap so
   the stage cards stay in proportion. The nav and footer sit outside <main>
   and are untouched.

   Clamped rather than a flat 60px: on a 375px phone, 60 a side would eat a
   third of the screen. It scales down and tops out at the 60 asked for. */
main > section > .shell {
  position: relative;
  z-index: 2;
  /* Runs to 60px from each screen edge rather than being capped at --shell
     and centred — but capped eventually, because on a 2560px display an
     uncapped section gives 2400px measures and a service row whose title
     and description are a foot apart. 1680 is wide enough that a 1440px
     laptop never sees it. */
  max-width: 1680px;
  margin-inline: auto;
  padding-inline: clamp(20px, 5vw, 60px);
}
.disc-cool {
  background: radial-gradient(circle at 34% 30%, #E4EDF6 0%, #C6D3E0 46%, #8FA3BC 100%);
}

/* Depth tiers for background shapes. Pair these with data-parallax:
     far   0.06–0.10   large, faint, slow      — sits well behind content
     mid   0.12–0.18   medium                  — the default
     near  0.20–0.28   smaller, quicker        — hero only, so it stays the star
   At most two shapes per section, and alternate warm/cool down the page. */

/* ==========================================================================
   NAV — flat paper, no border, no shadow, no state change on scroll.
   It should read as part of the hero, not as a separate bar.
   ========================================================================== */
/* Two states.
   At rest it has no surface at all, so the hero's gradient disc runs straight
   through it to the top edge of the screen.
   Once the opening section has scrolled past it collapses into a floating
   navy bar: 65% of the viewport, centred, 20px off the top, 12px corners.
   The element carrying data-nav-clear decides when that happens. */
.nav {
  position: sticky;
  top: 0;
  z-index: 60;
  background: transparent;
  border: 0;
  transition: top var(--nav-t) var(--nav-ease);
}
.nav.is-floating { top: var(--nav-float-gap); }

/* At rest the bar runs the width of the screen, 45px in from each edge —
   the same treatment the sections and footer get, one step tighter. Once it
   collapses it takes --nav-float-w instead; that rule carries more
   specificity, so it wins without needing !important. */
.nav-inner {
  position: relative;
  /* 100%, not none. `none` is not an interpolatable value, so the width
     could not transition — it snapped to full width instantly while the
     navy background was still fading out over .62s, which is the flash of
     a full-width bar you see on the way back up. 100% resolves to the same
     width and animates properly. */
  max-width: 100%;
  padding-inline: clamp(18px, 4vw, 45px);
  height: var(--nav-h);
  display: flex;
  align-items: center;
  gap: 18px;
  border-radius: 0;
  background: transparent;
  /* Explicit zero shadows rather than the implied `none`, for the same
     reason: a keyword cannot be interpolated against a real shadow. Two
     layers, matching the floating state layer for layer — box-shadow lists
     of different lengths do not interpolate cleanly. */
  box-shadow: 0 0 0 0 rgba(28, 38, 53, 0),
              0 0 0 0 rgba(28, 38, 53, 0);
  transition: max-width var(--nav-t) var(--nav-ease),
              padding var(--nav-t) var(--nav-ease),
              height var(--nav-t) var(--nav-ease),
              background var(--nav-t) var(--nav-ease),
              border-radius var(--nav-t) var(--nav-ease),
              box-shadow var(--nav-t) var(--nav-ease);
}
.nav.is-floating .nav-inner {
  max-width: var(--nav-float-w);
  height: 72px;
  padding-inline: 24px;
  background: var(--navy);
  border-radius: var(--radius-card);
  /* Two layers: a soft halo with no offset, which is the blur that softens
     the bar's edges against the page, and the original drop shadow beneath
     it for weight. */
  box-shadow: 0 0 26px 3px rgba(28, 38, 53, .17),
              0 18px 40px -22px rgba(28, 38, 53, .65);
}

/* logo: navy lockup at rest, white one once the bar goes navy */
.brand { position: relative; display: flex; align-items: center; flex: none; }
.brand img { height: 32px; width: auto; transition: opacity var(--nav-t) var(--nav-ease); }
.brand .brand-alt { position: absolute; inset: 0; opacity: 0; }
.nav.is-floating .brand-base { opacity: 0; }
.nav.is-floating .brand-alt { opacity: 1; }

/* links sit hard right, next to the button */
.nav-links { display: flex; align-items: center; gap: 2px; margin-left: auto; }
.nav-links a {
  padding: 9px 13px;
  border-radius: var(--radius-btn);
  font-family: var(--font-head);
  font-size: .875rem;
  font-weight: 500;
  letter-spacing: -.008em;
  color: var(--body);
  white-space: nowrap;
  transition: color var(--nav-t) var(--nav-ease);
}
.nav-links a:hover { color: var(--ink-strong); }
.nav-links a[aria-current="page"] { color: var(--ink-strong); font-weight: 700; }
.nav.is-floating .nav-links a { color: var(--on-navy); }
.nav.is-floating .nav-links a:hover { color: var(--paper); }
.nav.is-floating .nav-links a[aria-current="page"] { color: var(--paper); }

.nav-cta { display: flex; align-items: center; gap: 10px; flex: none; margin-left: 6px; }
/* trimmer than the body button, so it sits lighter in a taller bar */
.nav-cta .btn { padding-block: 9px; padding-inline: 22px; }
.nav.is-floating .nav-cta .btn-primary { background: var(--paper); color: var(--navy); }
.nav.is-floating .nav-cta .btn-primary:hover { background: var(--warm); color: #304157; }

/* ---------- light / dark toggle ----------
   Sits to the left of the call button, which puts it in the same group as
   the other controls rather than adrift among the links. Icon only: the
   sun and the moon are drawn as strokes so they take `color` and follow
   the bar the same way the links and the hamburger do.

   Both icons are in the DOM and CSS picks one. Rendering the right icon in
   JS instead would mean the wrong one is on screen until the script runs,
   which on a slow connection is long enough to see. */
.theme-toggle {
  width: 38px; height: 38px;
  display: inline-flex; align-items: center; justify-content: center;
  flex: none;
  border: 0;
  background: transparent;
  padding: 0;
  cursor: pointer;
  color: var(--ink-strong);
  border-radius: 50%;
  transition: color var(--nav-t) var(--nav-ease), background .3s var(--ease);
}
.theme-toggle svg {
  width: 18px; height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* the moon is a filled crescent rather than an outline: at 18px an outlined
   crescent reads as a smudge */
.theme-toggle .theme-moon { fill: currentColor; stroke: none; }
/* light theme shows the sun's opposite — the thing you would switch TO */
.theme-toggle .theme-sun { display: none; }
:root[data-theme="dark"] .theme-toggle .theme-moon { display: none; }
:root[data-theme="dark"] .theme-toggle .theme-sun { display: block; }
.theme-toggle:hover { background: rgba(48, 65, 87, .07); }
:root[data-theme="dark"] .theme-toggle:hover { background: rgba(255, 255, 255, .12); }
.theme-toggle:focus-visible { outline: 2px solid var(--ink-strong); outline-offset: 2px; }
/* on the floating bar it follows the bar, not the page, in both themes */
.nav.is-floating .theme-toggle { color: var(--on-navy); }
.nav.is-floating .theme-toggle:hover { color: #FAFAF8; background: rgba(255, 255, 255, .12); }
.nav.is-floating .theme-toggle:focus-visible { outline-color: #FAFAF8; }

.nav-toggle {
  display: none;
  width: 42px; height: 42px;
  align-items: center; justify-content: center;
  border: 0;
  background: transparent;
  cursor: pointer;
  /* the bars are 16px wide inside a 42px box, so pull the box out to the
     right to sit the glyph itself on the gutter rather than its padding */
  margin-right: -13px;
}
.nav-toggle span {
  display: block; width: 16px; height: 1.5px; background: var(--ink-strong);
  position: relative;
  transition: background .4s var(--ease);
}
.nav-toggle span::before, .nav-toggle span::after {
  content: ""; position: absolute; left: 0; width: 16px; height: 1.5px; background: var(--ink-strong);
  transition: transform .22s var(--ease), background .4s var(--ease);
}
.nav.is-floating .nav-toggle span,
.nav.is-floating .nav-toggle span::before,
.nav.is-floating .nav-toggle span::after { background: var(--paper); }
.nav-toggle span::before { top: -5px; }
.nav-toggle span::after { top: 5px; }
.nav-toggle[aria-expanded="true"] span { background: transparent; }
.nav.is-floating .nav-toggle[aria-expanded="true"] span { background: transparent; }
.nav-toggle[aria-expanded="true"] span::before { transform: translateY(5px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span::after { transform: translateY(-5px) rotate(-45deg); }

/* 940 until 12 September. Seven links and a longer button need more room
   than a 940px bar has, so they tuck away sooner. */
@media (max-width: 1100px) {
  /* the panel hangs off the bar itself, so it lines up in both states */
  .nav-links {
    position: absolute;
    top: calc(100% + 8px);
    left: 0; right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
    margin-left: 0;
    background: var(--paper);
    border-radius: var(--radius-card);
    padding: 10px 12px 14px;
    box-shadow: 0 20px 44px -24px rgba(48, 65, 87, .55);
    display: none;
  }
  .nav.is-floating .nav-links { background: var(--navy); }
  .nav-links.is-open { display: flex; }
  .nav-links a { padding: 13px 14px; font-size: 1.02rem; }
  .nav-toggle { display: flex; }
  .nav-cta .btn { display: none; }
  /* nav-links goes absolute above, so nothing is left to push the toggle
     over — send it to the right edge itself */
  .nav-cta { margin-left: auto; }
  .nav.is-floating .nav-inner { max-width: var(--nav-float-w-sm); padding-inline: 16px; height: 66px; }
  .nav.is-floating .nav-links { margin-inline: 0; }
  .brand img { height: 28px; }
}

/* ---------- footer ---------- */
.footer { background: var(--navy); color: var(--on-navy-muted); padding-block: clamp(32px, 4vw, 54px) 34px; }
/* same treatment as main > section > .shell: runs to 60px from each screen
   edge instead of being capped and centred */
.footer > .shell { max-width: none; padding-inline: clamp(20px, 5vw, 60px); }
.footer h4 {
  font-family: var(--font-head); font-weight: 700; font-size: .95rem;
  letter-spacing: -.01em; color: var(--on-navy-head); margin-bottom: 14px;
}
.footer a {
  color: var(--on-navy-strong); font-size: .9rem;
  transition: color .2s var(--ease);
}
.footer a:hover { color: var(--paper); }
/* the legal row is the same weight as the copyright it sits beside, not the
   size the nav columns use */
.footer-legal a { font-size: inherit; color: inherit; }
.footer-legal a:hover { color: var(--paper); }
/* white lockup, straight onto the navy footer */
.footer-mark {
  height: 47px;
  width: auto;
  align-self: flex-start;
}
/* Two centred rows from 11 September: the legal links on their own line,
   the copyright under it. Side by side they read as one long sentence that
   happened to have links in it, and the slashes now do the separating that
   26px of gap was failing to do. */
.footer-bottom {
  margin-top: 14px; padding-top: 8px;
  display: flex; flex-direction: column; gap: 8px;
  justify-content: center; align-items: center;
  text-align: center;
  font-size: .8rem; color: var(--on-navy-faint);
}
.footer-legal {
  display: flex; flex-wrap: wrap; gap: 4px 10px;
  justify-content: center; align-items: baseline;
}
/* aria-hidden in the markup: it is punctuation, not a list item, and a
   screen reader reading "slash" between three links helps nobody */
.footer-legal-sep { color: var(--on-navy-faint); opacity: .55; user-select: none; }

/* ---------- reveal on scroll ---------- */
.reveal {
  opacity: 0;
  transform: translateY(28px);
  filter: blur(6px);
}
.reveal.is-in {
  opacity: 1;
  transform: none;
  filter: blur(0);
  transition: opacity .78s var(--ease-out), transform .78s var(--ease-out),
              filter .78s var(--ease-out);
}
.reveal-left { transform: translate3d(-28px, 0, 0); }
.reveal-right { transform: translate3d(28px, 0, 0); }

/* An orphan selector sat here — `.reveal .reveal.is-in` with no block and no
   semicolon — which glued itself to the front of the next rule and turned
   `.rail` into `.reveal .reveal.is-in .rail`. Nothing on the site uses a
   bare `.rail` today, so it broke nothing visible, but it is the kind of
   thing that eats the NEXT rule somebody writes here. Whatever the nested
   reveal was going to say is gone; it can come back with braces on it. */
.rail {
  height: 2px; border-radius: 2px; background: var(--line);
  position: relative; overflow: hidden; margin-bottom: 26px;
}
.rail span {
  position: absolute; inset: 0 auto 0 0; width: 0;
  background: linear-gradient(90deg, var(--mist) 0%, var(--navy) 100%);
  border-radius: 2px;
}
.rail.is-in span { width: 100%; transition: width 1.5s .1s var(--ease-out); }

/* ==========================================================================
   HERO — headline, subhead, slider, four cards. Pinned and scroll-driven.
   ========================================================================== */

/* The hero is a normal section. The visitor chooses their stage and the
   choice sticks, so the page below stays reachable and the cards can be
   read and hovered. */
.hero-pin { position: relative; }

.hero {
  position: relative;
  min-height: calc(100svh - var(--nav-h));
  background: transparent;
  display: flex;
  align-items: center;
  padding-block: clamp(36px, 7vw, 124px) clamp(14px, 1.8vw, 30px);
}
.hero > .shell { position: relative; z-index: 2; width: 100%; }

/* Clipping box for the decorative discs. It reaches up over the nav band so
   a disc bleeds off the top corner of the screen rather than stopping at the
   nav, out to both screen edges, and down past the hero by --hero-bleed-tail
   so the cool disc carries on into the section below and scrolls away behind
   it. That section must stay transparent for this to read. */
.hero-bleed {
  position: absolute;
  top: calc((var(--nav-h) + var(--hero-bleed-head)) * -1);
  left: 0;
  right: 0;
  bottom: calc(var(--hero-bleed-tail) * -1);
  overflow: hidden;
  pointer-events: none;
  z-index: 1;
}
/* Both discs are parked well outside the hero at rest, showing only a
   sliver, and sweep across it as you scroll: orange down from above the
   nav, cool blue up from below the fold. The travel comes from site.js —
   see data-parallax on index.html. Starting them this far out is what
   gives them somewhere to travel from now that the hero is one screen
   tall rather than pinned. */
.hero-bleed .disc-a {
  width: min(86vw, 975px); aspect-ratio: 1;
  right: -25vw; top: calc(-46vw + var(--hero-bleed-head)); opacity: .38;
}
/* Bottom is measured from the extended box, so the tail is added back —
   the remainder is how far below the hero's own bottom edge it starts.
   Half the size it used to be, with left and bottom pulled in to match so
   the same slice of it still shows above the hero's bottom edge. Phones
   get the original, larger geometry back below. */
.hero-bleed .disc-b {
  width: min(38.25vw, 428px); aspect-ratio: 1;
  left: -14.7vw;
  bottom: calc(var(--hero-bleed-tail) - 14vw + 150px);
  opacity: .3;
}

/* first section on inner pages: let its disc run up into the nav too */
.section-bleed { overflow: visible; }

/* The decorative discs are positioned past the screen edges on purpose, so
   without this the page is wider than the phone and scrolls sideways.
   Three deliberate choices here:
   - it goes on `main`, not on html/body. Overflow on the root propagates to
     the viewport, which changes what `position: sticky` measures against and
     kills the floating nav. `main` is a sibling of the nav, so it cannot.
   - `clip`, not `hidden`, so no scroll container is created.
   - x only. `overflow-y` stays visible so .section-bleed's disc can still
     run up into the nav band. */
main { overflow-x: clip; }

/* ==========================================================================
   SECTION EDGES
   No hairlines between sections. Where the surface changes, it dissolves
   instead: the neighbouring colour is painted as a gradient over the first
   or last stretch of the section, so the seam has no line in it.

   The one exception is a full-width navy section sitting between two light
   ones — there the colour change is the separation, and a fade would only
   muddy it. Those keep hard edges by simply not carrying these classes.

   The gradients fade to an explicit transparent white rather than the
   `transparent` keyword, which resolves to transparent BLACK and would
   pull a grey haze through the middle of the ramp. */
/* RETIRED 12 September. Every section sits on the page ground now, so there
   is no surface change left for these to dissolve. They painted a ramp of
   white over the top or bottom of a section, which on the dark theme was a
   ramp of white over navy — the "weird gradients" James was seeing. The
   classes stay in the markup and paint nothing. */
.fade-top, .fade-bottom, .fade-top.fade-bottom { background-image: none; }

.mat { display: flex; flex-direction: column; gap: 0; }
/* subhead -> timeline, and timeline -> cards. Tuned so the whole module
   fits one screen without scrolling on a typical laptop. */
.mat-track-wrap { margin-top: var(--mat-gap-head, 22px); }
.mcards { margin-top: var(--mat-gap-cards, 14px); }

.mat-head {
  text-align: center;
  max-width: 980px;
  margin-inline: auto;
  display: flex; flex-direction: column; gap: 16px;
}
/* TWO LINES, ALWAYS.

   Both halves are blocks and both are nowrap, so the headline breaks after
   "your" and before "RevOps" at every width and can never take a third
   line. The size is what has to give instead: 8.6vw is the largest the
   longer of the two lines can be set at and still clear the gutters, and
   5.4rem caps it once there is room to spare. Measured on Ubuntu Bold at
   -.042em — the long line runs 9.93em, so changing the words, the tracking
   or the face means re-measuring this number. */
.mat-head .h1-lead,
.mat-head .h1-tail { display: block; white-space: nowrap; }
.mat-head .h1 {
  font-size: min(5.4rem, 8.6vw);
  line-height: .98;
  letter-spacing: -.042em;
  max-width: none;
  margin-inline: auto;
}
.mat-head .lede {
  max-width: 70ch;
  margin-inline: auto;
  font-size: clamp(1rem, .94rem + .38vw, 1.16rem);
}

/* --- the track --- */
.mat-track-wrap {
  position: relative;
  padding-top: var(--mat-track-pad, 26px);
  touch-action: pan-y;
  user-select: none;
  -webkit-user-select: none;
}
.mat-track {
  position: relative;
  height: 4px;
  border-radius: 999px;
  background: var(--track);
  cursor: pointer;
  margin-inline: 36px;
}
.mat-fill {
  position: absolute;
  inset: 0 auto 0 0;
  border-radius: 999px;
  background: var(--warm);
  width: 0%;
  transition: width .42s var(--ease);
}
.mat-track.is-dragging .mat-fill { transition: none; }

.mat-nodes { position: absolute; inset: 0; pointer-events: none; }
.mat-node {
  position: absolute;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 34px; height: 34px;
  display: flex; align-items: center; justify-content: center;
  background: none; border: 0; padding: 0;
  cursor: pointer;
  pointer-events: auto;
}
.mat-node::before {
  content: "";
  width: 11px; height: 11px;
  border-radius: 50%;
  background: var(--paper);
  box-shadow: inset 0 0 0 2px var(--track-ring);
  transition: box-shadow .3s var(--ease), transform .3s var(--ease-out), background .3s var(--ease);
}
.mat-node:hover::before { box-shadow: inset 0 0 0 2px var(--slate); transform: scale(1.15); }
.mat-node.is-passed::before { background: var(--warm); box-shadow: inset 0 0 0 2px var(--warm); }
.mat-node.is-active::before {
  background: var(--paper);
  box-shadow: inset 0 0 0 3px var(--ink-strong), 0 0 0 5px rgba(48, 65, 87, .12);
  transform: scale(1.15);
}
.mat-node:focus-visible { outline: 2px solid var(--ink-strong); outline-offset: 2px; border-radius: 50%; }

/* --- the bunny thumb --- */
.mat-thumb {
  position: absolute;
  top: 50%;
  left: 0;
  width: 38px; height: 38px;
  margin: -19px 0 0 -19px;
  border-radius: 50%;
  background: var(--navy);
  box-shadow: 0 0 0 3px var(--paper), 0 12px 28px -12px rgba(48, 65, 87, .55);
  display: flex; align-items: center; justify-content: center;
  cursor: grab;
  z-index: 3;
  will-change: left;
  transition: left .42s var(--ease), box-shadow .25s var(--ease);
  touch-action: none;
}
.mat-thumb:active, .mat-thumb.is-dragging { cursor: grabbing; }
.mat-thumb.is-dragging { transition: box-shadow .25s var(--ease); }
.mat-thumb:hover { box-shadow: 0 0 0 3px var(--paper), 0 16px 34px -12px rgba(48, 65, 87, .7); }
.mat-thumb:focus-visible { outline: 2px solid var(--ink-strong); outline-offset: 4px; }
.mat-thumb img, .mat-thumb svg { width: 20px; height: 20px; pointer-events: none; }
.mat-thumb-inner {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%;
}
.mat-thumb.hop .mat-thumb-inner { animation: hop .46s var(--ease-out); }
@keyframes hop {
  0%   { transform: translateY(0) rotate(0deg) scale(1); }
  30%  { transform: translateY(-6px) rotate(-4deg) scale(1.03); }
  60%  { transform: translateY(1px) rotate(1.5deg) scale(.99); }
  100% { transform: translateY(0) rotate(0deg) scale(1); }
}

/* --- stage labels --- */
/* Each label is placed on its own node's percentage, so the name sits under
   its marker. At rest they tuck close to the track, just clear of the bunny's
   bottom edge. The selected one slides further down, leaving the marker free
   for the bunny to occupy. First and last justify to the ends of the track. */
.mat-labels {
  position: relative;
  height: var(--mat-label-h, 50px);
  margin-top: var(--mat-label-top, 24px);
  margin-inline: 36px;
}
.mat-label {
  position: absolute;
  top: 0;
  left: var(--mat-x, 0%);
  background: none; border: 0; padding: 0; cursor: pointer;
  white-space: nowrap;
  font-family: var(--font-head);
  font-weight: 500;
  font-size: .92rem;
  letter-spacing: -.008em;
  line-height: 1.3;
  color: var(--muted);
  transform: translate(-50%, 0);
  transition: color .34s var(--ease), transform .46s var(--ease-out);
}
.mat-label:first-child { transform: translate(0, 0); text-align: left; }
.mat-label:last-child  { transform: translate(-100%, 0); text-align: right; }
.mat-label:hover { color: var(--slate); }

.mat-label.is-active { color: var(--ink-strong); font-weight: 700; }
.mat-label.is-active               { transform: translate(-50%, var(--mat-label-drop, 24px)) scale(1.12); }
.mat-label.is-active:first-child   { transform: translate(0, var(--mat-label-drop, 24px)) scale(1.12); transform-origin: left top; }
.mat-label.is-active:last-child    { transform: translate(-100%, var(--mat-label-drop, 24px)) scale(1.12); transform-origin: right top; }

.mat-label:focus-visible { outline: 2px solid var(--ink-strong); outline-offset: 3px; border-radius: 6px; }

/* --- the four cards --- */
.mcards {
  --mcards-gap: clamp(14px, 1.5vw, 22px);
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--mcards-gap);
  align-items: stretch;
}
/* last column: case study on top, signup beneath, the pair matching the
   height of the single cards next to it */
.mcard-col {
  flex-direction: column;
  gap: var(--mcards-gap);
}
.mcard-col > .mcard { flex: 1; }
.mcard {
  --mcard-pad: clamp(16px, 1.7vw, 24px);
  background: var(--card-veil);
  /* frosted rather than merely translucent: the discs behind the hero read
     as a soft wash through the card instead of showing through it sharply */
  -webkit-backdrop-filter: blur(16px) saturate(1.15);
          backdrop-filter: blur(16px) saturate(1.15);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  --mcard-gap: 9px;
  padding: var(--mcard-pad);
  display: flex; flex-direction: column; gap: var(--mcard-gap);
  transition: background var(--mcard-t) var(--mcard-ease),
              border-color var(--mcard-t) var(--mcard-ease),
              transform .5s var(--ease-out),
              box-shadow var(--mcard-t) var(--mcard-ease);
}
/* hover lifts, nothing more — the surface stays put */
.mcard:hover {
  transform: translateY(-4px);
  box-shadow: 0 26px 52px -28px rgba(28, 38, 53, .38);
}
/* The case study card carries no surface of its own — same white as the
   two beside it, inherited from .mcard */
.mcard-title {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(.94rem, .88rem + .2vw, 1.08rem);
  line-height: 1.24;
  letter-spacing: -.016em;
  color: var(--ink);
  transition: color var(--mcard-t) var(--mcard-ease);
}
.mcard-body { --mcard-body-gap: 10px;
  display: flex; flex-direction: column; gap: var(--mcard-body-gap); flex: 1; }

.mcard-list { display: flex; flex-direction: column; gap: 7px; }
.mcard-list li {
  position: relative;
  padding-left: 18px;
  font-size: .855rem;
  line-height: 1.42;
  color: var(--body);
  transition: color var(--mcard-t) var(--mcard-ease);
}
.mcard-list li::before {
  content: "";
  position: absolute; left: 0; top: .6em;
  width: 9px; height: 2px; border-radius: 2px;
  background: var(--warm);
}

/* A second section inside a card, set off from the bullets above it. The
   padding is the whole separation — there is no rule — so it has to be big
   enough to read as a break. The cards have slack at the foot of them
   (the CTA is pushed down by margin-top:auto), so this eats slack rather
   than making the block taller. */
.mcard-split {
  display: flex; flex-direction: column; gap: 9px;
  padding-top: 20px;
}
/* body copy under a card subhead */
.mcard-note {
  font-size: .855rem;
  line-height: 1.42;
  color: var(--body);
  transition: color var(--mcard-t) var(--mcard-ease);
}

/* a second heading inside a card, a step down from the card title */
.mcard-subhead {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: .92rem;
  line-height: 1.25;
  letter-spacing: -.015em;
  color: var(--ink);
  transition: color var(--mcard-t) var(--mcard-ease);
}

.mcard-chips { display: flex; flex-wrap: wrap; gap: 5px; }
.mcard-chips .chip { font-size: .715rem; padding: 3px 9px; }
/* what they have vs what they are missing. The chips keep their own light
   surfaces through the card's hover so they stay readable against navy. */
.chip-have    { background: var(--ok-bg); border-color: var(--ok-line); color: var(--ok); }
.chip-missing { background: var(--no-bg); border-color: var(--no-line); color: var(--no); }
.chip i {
  font-style: normal;
  font-size: .82em;
  line-height: 1;
  margin-right: 5px;
  opacity: .85;
}

/* copy on the left, square thumbnail on the right */
.mcard-case-row {
  display: flex;
  align-items: flex-start;
  gap: clamp(12px, 1.4vw, 18px);
}
.mcard-case-text {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 7px;
}
/* The client's own mark, and nothing around it: no plate, no border, no
   radius, no shadow. `contain`, because these are wordmarks of every
   proportion and cropping one to a square would cut it in half. Set back to
   match the marquee above, and brought up to full strength on hover with
   the rest of the card. */
.mcard-shot {
  flex: none;
  width: clamp(84px, 8.6vw, 124px);
  aspect-ratio: 1;
  object-fit: contain;
  object-position: center;
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  opacity: .72;
  mix-blend-mode: multiply;
  transition: opacity var(--mcard-t) var(--mcard-ease);
}
.mcard:hover .mcard-shot { opacity: 1; }
/* The exception the marquee makes too: a mark that is a light disc with the
   words knocked out of it cannot be multiplied onto the card — the disc goes
   and the words go with it. */
.mcard-shot[data-solid] { mix-blend-mode: normal; opacity: .9; }
/* A photograph rather than a client mark. None of the marquee treatment
   applies: it fills its square, keeps its own colour in both themes, and
   takes the card radius so it does not read as a stray image. */
.mcard-shot.is-photo {
  object-fit: cover;
  mix-blend-mode: normal;
  opacity: 1;
  border-radius: var(--radius-card);
}
/* This card absorbs whatever height the signup card below it does not use,
   so the pair matches the two single cards beside them. The story link
   drops to the bottom-left corner. */
.mcard-case .mcard-body { --mcard-body-gap: 11px; }
/* The name, line and mark sit in the middle of the space the card has left
   rather than hanging from the top of it. Auto margins on the row rather
   than justify-content on the body: the link below it carries margin-top
   auto, and an auto margin beats justify-content, so the two would fight.
   With the row taking the free space on both sides the link ends up at the
   foot of the card anyway, which is where it was. */
.mcard-case .mcard-case-row { margin-block: auto; }
.mcard-case .mcard-link { align-self: flex-start; margin-top: 0; }
/* The whole case study card is the link, not just "Read the story" (16
   September). A stretched link rather than wrapping the card in an <a>: the
   card body is rebuilt on every stage change and the link stays the one
   focusable, announced element. Its ::after covers the card, so a click on
   the title, the copy or the photo lands on the link. */
.mcard-case { position: relative; cursor: pointer; }
.mcard-case .mcard-link::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
}
.mcard-case:hover .mcard-link { color: var(--ink-hover); }
.mcard-case:hover .mcard-link .arrow { transform: translateX(4px); }

.mcard-case-who {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: .95rem;
  line-height: 1.28;
  letter-spacing: -.014em;
  color: var(--ink);
  transition: color var(--mcard-t) var(--mcard-ease);
}
.mcard-case-line {
  font-size: .855rem; line-height: 1.42; color: var(--body);
  transition: color var(--mcard-t) var(--mcard-ease);
}
.mcard-link {
  align-self: flex-end;
  margin-top: auto;
  padding-top: 4px;
  font-family: var(--font-body);
  font-weight: 700; font-size: .82rem;
  color: var(--ink-strong);
  transition: color var(--mcard-t) var(--mcard-ease);
}
.mcard-link .arrow { display: inline-block; transition: transform .3s var(--ease-out); }
.mcard-link:hover { color: var(--ink-hover); }
.mcard-link:hover .arrow { transform: translateX(4px); }

/* ---- the capture card, sitting under the case study ----
   Same anatomy as the three cards above it: title, rule, body — but on
   navy, so the rule goes white rather than warm. */
/* Same surface, border, radius and hover lift as the three cards above,
   so the column reads as four of one thing rather than three plus an odd
   one out. Everything inside it is set on light accordingly. */
.stage-form {
  flex: none;
  background: var(--card-veil);
  -webkit-backdrop-filter: blur(16px) saturate(1.15);
          backdrop-filter: blur(16px) saturate(1.15);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: clamp(16px, 1.7vw, 22px);
  display: flex;
  flex-direction: column;
  gap: 10px;
  transition: transform .5s var(--ease-out),
              box-shadow var(--mcard-t) var(--mcard-ease);
}
.stage-form:hover {
  transform: translateY(-4px);
  box-shadow: 0 26px 52px -28px rgba(28, 38, 53, .38);
}
.stage-form-title {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(.94rem, .88rem + .2vw, 1.08rem);
  line-height: 1.24;
  letter-spacing: -.016em;
  color: var(--ink);
}
.stage-form-fields {
  display: flex;
  align-items: center;
  gap: 12px;
}
.stage-form input[type="email"] {
  flex: 1 1 auto;
  min-width: 0;
  padding: 11px 14px;
  border-radius: var(--radius-btn);
  border: 1px solid var(--plate-line);
  background: rgba(255, 255, 255, .42);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: .875rem;
  transition: border-color .22s var(--ease), background .22s var(--ease);
}
/* iOS Safari zooms the page in on focus for any field set below 16px, and
   there is no way to opt out of that short of disabling pinch-zoom for the
   whole page, which is worse. So on touch devices the field goes to 16px
   and the zoom never triggers. Pointer devices keep the smaller text. */
@media (pointer: coarse) {
  .stage-form input[type="email"],
  input[type="text"], input[type="tel"], input[type="number"],
  input[type="url"], input[type="password"], select, textarea {
    font-size: 16px;
  }
}
.stage-form input::placeholder { color: var(--muted); opacity: 1; }
.stage-form input:hover { border-color: #B9C3D0; }
.stage-form input:focus {
  outline: 2px solid var(--ink-strong);
  outline-offset: 1px;
  border-color: transparent;
  background: var(--plate);
}
/* the submit reads as a text link, exactly like the CTAs in the cards
   above. It sits to the right of the field, centred on it. */
.stage-form-send {
  flex: none;
  align-self: center;
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
  white-space: nowrap;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: .82rem;
  line-height: 1;
  color: var(--ink-strong);
  transition: color .3s var(--ease);
}
.stage-form-send .arrow { display: inline-block; transition: transform .3s var(--ease-out); }
.stage-form-send:hover { color: var(--ink-hover); }
.stage-form-send:hover .arrow { transform: translateX(4px); }
.stage-form-send:focus-visible { outline: 2px solid var(--ink-strong); outline-offset: 3px; border-radius: 4px; }

/* Five forms, one per stage (see index.html). Only the current stage's
   shows; the display: flex above would otherwise outrank [hidden]. Before
   the slider script has moved them into place they sit in the mount as a
   plain stack, which should never paint. */
.stage-form[hidden],
.stage-form-fields[hidden],
.stage-form-done[hidden] { display: none; }
[data-revhops-maturity] > [data-stage-forms] { display: none; }
/* The hidden iframes each form submits into. */
.stage-sink { display: none; }

/* The confirmation takes the fields' place at the fields' height, so the
   column does not jump when it swaps in. */
.stage-form-done {
  min-height: 41px;
  display: flex;
  align-items: center;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: .875rem;
  line-height: 1.3;
  color: var(--ink-strong);
}

/* ---- stage change: the four cards slide out, then the new set slides in ----
   The slot owns the slide so it never fights the card's own hover lift.
   Timings here must stay in step with OUT_MS / IN_MS in maturity-slider.js. */
.mcard-slot {
  display: flex;
  min-width: 0;
  will-change: transform, opacity;
  transition: transform .26s var(--ease-out), opacity .2s var(--ease);
}
.mcard-slot > .mcard { flex: 1; min-width: 0; }
.mcard-slot:nth-child(2) { transition-delay: .03s; }
.mcard-slot:nth-child(3) { transition-delay: .06s; }

/* outgoing: fade with the smallest of drops */
.mcards.is-out .mcard-slot { opacity: 0; transform: translate3d(0, 5px, 0); }

/* incoming: parked just below with no transition, then released */
.mcards.is-enter .mcard-slot { transition: none; opacity: 0; transform: translate3d(0, -5px, 0); }

/* short viewports: compress so the pinned hero always fits */
@media (min-width: 1000px) and (max-height: 940px) {
  .mat { --mat-gap-head: 18px; --mat-gap-cards: 14px; --mat-track-pad: 22px; }
  .mat-head { gap: 10px; }
  .mat-head .h1 { font-size: min(3.6rem, 1rem + 3.4vw, 8.6vw); }
  .mat-head .lede { font-size: .95rem; }
  .mat-labels { --mat-label-top: 21px; --mat-label-drop: 22px; --mat-label-h: 48px; }
  .mcard { --mcard-pad: 18px; --mcard-gap: 10px; padding: 16px 18px; }
  .mcard-body { --mcard-body-gap: 11px; }
  .mcard-list { gap: 7px; }
  .mcard-list li { font-size: .8rem; }
  .mcard-shot { width: clamp(76px, 7.6vw, 108px); }
  .stage-form { padding: 16px 18px; }
}
@media (min-width: 1000px) and (max-height: 820px) {
  .mat { --mat-gap-head: 14px; --mat-gap-cards: 11px; --mat-track-pad: 18px; }
  .mat-head .h1 { font-size: min(2.6rem, .9rem + 2.4vw, 8.6vw); }
  .mat-head .lede { font-size: .875rem; }
  .mat-thumb { width: 38px; height: 38px; margin: -19px 0 0 -19px; }
  .mat-thumb img, .mat-thumb svg { width: 20px; height: 20px; }
  .mat-labels { --mat-label-top: 19px; --mat-label-drop: 20px; --mat-label-h: 44px; }
  .mat-label { font-size: .84rem; }
  .mcard-title { font-size: .875rem; }
  .mcard-subhead { font-size: .85rem; }
  .mcard-list li { font-size: .775rem; line-height: 1.42; }
  .mcard-case-line { font-size: .775rem; line-height: 1.42; }
  .mcard-case-who { font-size: .8rem; }
  .mcard-shot { width: clamp(66px, 6.4vw, 92px); }
  .stage-form { padding: 15px 16px; gap: 8px; }
  .stage-form-title { font-size: .875rem; }
}

/* below this width the hero un-pins and behaves as a normal section */
@media (max-width: 999px) {
  .hero { min-height: 0; padding-block: 48px 60px; }
}
/* Phones only — tablets keep the desktop disc geometry.
   The hero is far shorter here, so -46vw put almost all of the orange disc
   above the clip box and only a sliver showed behind the nav. Bring it down
   and in, then let the parallax carry it down across the cards. The cool
   disc keeps its original, larger size at this width: halved, it would be
   too small to register on a narrow screen. */
@media (max-width: 760px) {
  .hero-bleed .disc-a {
    width: min(108vw, 620px);
    right: calc(-32vw - 50px);
    top: calc(-12vw + var(--hero-bleed-head));
    opacity: .42;
  }
  .hero-bleed .disc-b {
    width: min(51vw, 570px);
    left: -28.7vw;
    bottom: calc(var(--hero-bleed-tail) - 36vw);
  }
}
/* Phones: five labels will not fit side by side, so the row collapses to
   just the current stage, set large and centred under the bunny. The
   margin below the track is set past the thumb's overhang so it can never
   touch the first card. */
@media (max-width: 760px) {
  .mat-track { margin-inline: 28px; }
  .mat-thumb { width: 36px; height: 36px; margin: -18px 0 0 -18px; }
  .mat-thumb img, .mat-thumb svg { width: 19px; height: 19px; }

  /* Only the current stage shows — five names will not fit side by side —
     but it stays pinned under its own marker rather than being centred, so
     it still reads as a position on the timeline. First and last justify to
     the ends of the track so neither runs off the screen. The transforms
     are inherited from the desktop rules; only the size changes here. */
  .mat-labels { --mat-label-h: 30px; --mat-label-drop: 0px; margin-top: 20px; margin-inline: 28px; }
  .mat-label { display: none; }
  .mat-label.is-active {
    display: block;
    width: max-content;
    max-width: 100%;
    font-size: 1.02rem;
    font-weight: 700;
    letter-spacing: -.018em;
    color: var(--ink-strong);
    animation: stageIn .38s var(--ease-out);
  }
  /* opacity only — the transform is doing the positioning */
  @keyframes stageIn {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
}
/* THE HEADLINE WRAPS ON A PHONE.

   Both halves are nowrap blocks so the break lands after "your" at every
   width, and the size is a vw so the longer of the two just clears the
   gutters. That holds on a wide screen. On a phone it does not: the line
   runs past the shell, and because it is nowrap the BLOCK grows with it
   rather than wrapping — so .mat-head ends up wider than the page, the
   centred lede centres inside that wider box, and the whole header sits
   off to the right with the title clipped.

   Here the two halves go inline and the headline wraps like ordinary type.
   Three lines on a phone is fine; a header hanging off the screen is not.
   The markup has a space between the two spans, so they join cleanly. */
@media (max-width: 760px) {
  .mat-head { max-width: 100%; }
  .mat-head .h1-lead,
  .mat-head .h1-tail { display: inline; white-space: normal; }
  .mat-head .h1 {
    font-size: clamp(1.95rem, 8.4vw, 2.7rem);
    line-height: 1.04;
    letter-spacing: -.03em;
  }
}

/* the band runs faster and tighter on phones, where less of it is on
   screen at once and the default pace reads as almost stationary */
@media (max-width: 760px) {
  .logo-rail { animation-duration: 30s; }
  .logo-run { gap: 30px; padding-right: 30px; padding-block: 8px; }
  /* THE HEIGHT IS NOT SET HERE. This block sits ABOVE `.logo-run img`'s own
     rule in the file and ties with it on specificity, so a height written
     here loses on order and never applied — which is why the marks stayed at
     the desktop clamp's 61px floor on a phone whatever this said. It is in
     the phone block at the end of the file instead. */
  .logo-run img { opacity: .7; }
  .logo-band {
    -webkit-mask-image: linear-gradient(to right, transparent, #000 6%, #000 94%, transparent);
            mask-image: linear-gradient(to right, transparent, #000 6%, #000 94%, transparent);
  }
}

/* three across holds down to here; below it they stack rather than
   leaving an orphan on a second row */
@media (max-width: 840px) {
  .mcards { grid-template-columns: minmax(0, 1fr); }
  /* stacked, and the hero is no longer pinned, so the shot can breathe */
  .mcard-shot { width: clamp(92px, 20vw, 140px); }
}

/* ---------- generic responsive ---------- */
@media (max-width: 1000px) {
  .split { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 760px) {
  .grid-2 { grid-template-columns: minmax(0, 1fr); }
}
/* the media query that used to open here also carried .footer-grid, which
   went with the old footer */
@media (max-width: 520px) {
  .btn { width: 100%; }
  .btn-row { flex-direction: column; align-items: stretch; }
}

/* ---------- page-specific bits ---------- */

/* ---------- client logo band ---------- */
.logo-band {
  padding-top: var(--proof-lead, 100px);
  padding-bottom: var(--sec-pad);
  position: relative;
  z-index: 2;
  /* feather both ends so logos dissolve into the paper rather than being
     chopped off at the edge of the screen */
  -webkit-mask-image: linear-gradient(to right, transparent, #000 9%, #000 91%, transparent);
          mask-image: linear-gradient(to right, transparent, #000 9%, #000 91%, transparent);
}
.logo-rail {
  display: flex;
  width: max-content;
  animation: logoRail 48s linear infinite;
}
.logo-rail:hover { animation-play-state: paused; }
.logo-run {
  display: flex;
  align-items: center;
  flex: none;
  gap: clamp(38px, 5vw, 82px);
  /* Vertical slack for the hover magnification. A mark at the row's full
     height grows about 5px past it on each side when it scales, and with
     nothing to grow into it gets cut at the edge of the line.
     padding-block only — padding-right is load-bearing for the seam, since
     it has to equal the gap for the loop to stay continuous. */
  padding-block: 12px;
  padding-right: clamp(38px, 5vw, 82px);
}
.logo-run li { flex: none; display: flex; align-items: center; }
.logo-run img {
  display: block;
  height: clamp(61px, 6.1vw, 90px);
  width: auto;
  max-width: 100%;
  object-fit: contain;
  opacity: .62;
  /* the files are already greyscale; this just settles them onto the paper */
  mix-blend-mode: multiply;
  transition: opacity .3s var(--ease), transform .35s var(--ease-out);
}
/* Hovering the rail already pauses it, so the mark holds still long enough
   to be worth magnifying. Scaling the image rather than the li keeps the
   rail's own -50% travel untouched — the marquee reads that transform. */
.logo-run img:hover { opacity: 1; transform: scale(1.12); }

/* two identical runs sit side by side, so travelling exactly half the
   rail's width lands the copy precisely where the original started */
@keyframes logoRail {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}
/* underlined so it still reads as a link at this contrast */
@media (max-width: 620px) {
}

/* ---------- scroll-linked entrances ---------- */
/* The element trails the page until it reaches its settle line, then locks
   into its natural position. See [data-enter] in site.js. */
[data-enter] { will-change: transform, opacity; }
@media (prefers-reduced-motion: reduce) {
  [data-enter] { transform: none !important; opacity: 1 !important; }
}

/* ---------- motion preferences ---------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  .reveal { opacity: 1; transform: none; filter: none; }
  .reveal .ticks li::before { transform: none; }
  .rail span { width: 100%; }
  .disc { transform: none !important; }
  /* the band stops moving and becomes a row you can swipe instead. The
     duplicate run is hidden so no logo appears twice. */
  .logo-rail { animation: none; transform: none; width: 100%; }
  .logo-band { overflow-x: auto; }
  .logo-rail .logo-run[aria-hidden="true"] { display: none; }
}

/* ==========================================================================
   HOMEPAGE — PROOF, QUALIFICATION AND PRICE
   Sections added when the homepage was restructured around a
   problem → solution → method → proof → fit → price → CTA arc.
   ========================================================================== */

/* ---------- featured case study ---------- */
/* the numbers that carry the result */

/* ---------- pricing teaser ---------- */
.price-teaser {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(16px, 2vw, 26px);
}
.price-teaser-cell {
  display: flex; flex-direction: column; gap: 7px;
  padding: clamp(22px, 2.4vw, 32px);
  background: var(--card-veil);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  transition: border-color .3s var(--ease), transform .5s var(--ease-out);
}
.price-teaser-cell:hover { border-color: var(--mist); transform: translateY(-3px); }
.price-teaser-label { font-size: .82rem; font-weight: 700; color: var(--slate); }
.price-teaser-fig {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(1.5rem, 1.1rem + 1.5vw, 2.15rem);
  line-height: 1.05;
  letter-spacing: -.032em;
  color: var(--ink);
}
.price-teaser-fig small {
  display: block;
  font-size: .42em;
  font-weight: 400;
  letter-spacing: 0;
  color: var(--slate);
  margin-top: 4px;
}
/* A UNIT, NOT A CAPTION. "Fixed price" is a line about the number and
   belongs under it; "/mo" is part of the number and belongs on it. Stacked,
   it read as a stray fragment — which "a month" never did, because a phrase
   under a figure is a caption and a slash is not. */
.price-teaser-fig small.is-unit {
  display: inline;
  font-size: .44em;
  margin-top: 0;
}
.price-teaser-note { font-size: .82rem; line-height: 1.45; color: var(--body); }

@media (max-width: 940px) {
  .price-teaser { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 700px) {
  .price-teaser { grid-template-columns: minmax(0, 1fr); }
}

/* ---------- newsletter opt-in (/newsletter) ---------- */
/* One narrow centred column. The HubSpot form sets its own field widths and
   will run the full shell if nothing caps it, which on a 1240px page puts
   an email field a metre wide in front of somebody. */
.optin-wrap {
  max-width: 34rem;
  margin-inline: auto;
  width: 100%;
}

/* ==========================================================================
   CARD HOVER — defined last, deliberately.

   .reveal.is-in sets `transform: none` at the same specificity as
   .card:hover, and sits later in this file, so it was silently cancelling
   the lift on every card that animates in. Since every card on the site
   carries .reveal, no card outside the hero was lifting at all. Redefining
   the hover here, after that rule, is what makes it stick.
   ========================================================================== */
.card:hover,
.card-link:hover,
.price-teaser-cell:hover,
.pcard:hover,
.svc-row:hover {
  transform: translateY(-4px);
  box-shadow: 0 26px 52px -28px rgba(28, 38, 53, .38);
  transition: transform .5s var(--ease-out),
              box-shadow var(--mcard-t) var(--mcard-ease),
              border-color var(--mcard-t) var(--mcard-ease);
}
.svc-row:hover { box-shadow: none; }

/* ---------- small label, replacing the old uppercase eyebrow ---------- */
.label {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: .85rem;
  line-height: 1.3;
  letter-spacing: -.012em;
  color: var(--ink);
  margin: 0;
}
.surface-navy .label { color: var(--paper); }

/* ==========================================================================
   SERVICES LIST — editorial rows, not cards.
   Title on the left, description on the right, separated by hairlines.
   Unnumbered on purpose: numbering implied the four run in sequence, which
   they do not. Hover warms the rule and slides the arrow.
   ========================================================================== */
.svc-list { border-top: 1px solid var(--line); }
.svc-row {
  display: grid;
  /* name, timing, description, link */
  grid-template-columns: minmax(0, .74fr) auto minmax(0, 1.3fr) auto;
  gap: clamp(16px, 2.4vw, 40px);
  /* name, copy and link all sit on the row's middle line; the padding is
     symmetric, so whichever column is tallest has equal air above and below */
  align-items: center;
  padding: clamp(24px, 3vw, 40px) 0;
  border-bottom: 1px solid var(--line);
  position: relative;
  text-decoration: none;
  /* how far the hover fill runs past the text on each side */
  --svc-bleed: clamp(12px, 1.6vw, 24px);
  transition: border-color var(--mcard-t) var(--mcard-ease);
}
/* The warm fill. A pseudo-element rather than a background on the row
   itself, so it can bleed past the text on both sides and give the block
   some breathing room without moving the content. z-index -1 puts it behind
   the row's own text, which is unpositioned and therefore paints above it. */
.svc-row::before {
  content: "";
  position: absolute;
  inset: 0 calc(var(--svc-bleed) * -1);
  z-index: -1;
  border-radius: var(--radius-card);
  /* A lighter tint of Mist, at full opacity, rather than Mist at reduced
     opacity. Lowering the opacity would blend the fill with whatever sits
     behind it — on the dark theme that is the navy page, which drags the
     navy text on top down to 2.8:1. Held opaque, the fill is just as faint
     in light mode and stays a solid ground in both. Literal, not a token,
     because the text on it is literal navy and must not invert. */
  background: #EFF3F8;
  opacity: 0;
  transition: opacity .4s var(--ease);
}
.svc-row:hover::before { opacity: 1; }
/* the divider would otherwise cut across the bottom of the fill */
.svc-row:hover { border-bottom-color: transparent; }

/* Text on the fill is set in literal navy, not var(--navy): that token
   inverts to near-white in dark mode, which on the light fill would be
   unreadable. The copy takes a lighter navy so the two keep their
   hierarchy; on this fill they measure 9.3:1 and 6.9:1. */
.svc-row:hover .svc-copy { color: #42546F; }
.svc-row:hover .svc-go   { color: #304157; }
.svc-title {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(1.15rem, 1rem + .75vw, 1.62rem);
  line-height: 1.16;
  letter-spacing: -.028em;
  color: var(--ink);
  transition: color var(--mcard-t) var(--mcard-ease),
              transform .45s var(--ease-out);
}
/* steps in toward the copy on hover, and returns on its own when the
   pointer moves to another row */
.svc-row:hover .svc-title { color: #304157; transform: translateX(15.4px); }
.svc-copy { font-size: .935rem; line-height: 1.6; color: var(--body);
  transition: color var(--mcard-t) var(--mcard-ease); }
/* the timing column. --body rather than --muted: muted only reaches 2.5:1
   on the paper, and this is content, not a hint. */
.svc-time {
  align-self: center;
  flex: none;
  /* a floor, so the description column starts at the same x on every row
     rather than jogging with the length of each estimate */
  min-width: 6.5em;
  font-size: .84rem;
  line-height: 1.3;
  color: var(--body);
  white-space: nowrap;
  transition: color var(--mcard-t) var(--mcard-ease);
}
.svc-row:hover .svc-time { color: #42546F; }

.svc-go {
  flex: none;
  font-weight: 700;
  font-size: .84rem;
  color: var(--ink-strong);
  white-space: nowrap;
  transition: color var(--mcard-t) var(--mcard-ease);
}
.svc-go .arrow { display: inline-block; transition: transform .3s var(--ease-out); }
.svc-row:hover .svc-go .arrow { transform: translateX(5px); }

@media (max-width: 820px) {
  .svc-row { grid-template-columns: minmax(0, 1fr); gap: 10px; align-items: start; }
  .svc-go, .svc-time { align-self: start; }
}

/* ==========================================================================
   TEXT LINK
   The default call to action everywhere. The boxed .btn is reserved for
   booking actions only — book a call, schedule, request the audit — so a
   framed button on the page always means the same thing.
   ========================================================================== */
.text-link {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  align-self: flex-start;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: .86rem;
  color: var(--ink-strong);
  transition: color .3s var(--ease);
}
.text-link .arrow { display: inline-block; transition: transform .3s var(--ease-out); }
.text-link:hover { color: var(--ink-hover); }
.text-link:hover .arrow { transform: translateX(5px); }
.text-link:focus-visible { outline: 2px solid var(--ink-strong); outline-offset: 4px; border-radius: 4px; }

/* reversed, for the navy and mist surfaces */
.surface-navy .text-link { color: var(--paper); }
.surface-navy .text-link:hover { color: var(--warm); }
.surface-navy .text-link:focus-visible { outline-color: var(--warm); }

/* ==========================================================================
   CASE STUDY RAIL
   A row of poster cards you scroll sideways. The image fills the card, a
   gradient sits over its lower half so white type stays readable whatever
   the photo does, and the title, figures and arrow sit on top of that.
   ========================================================================== */
.case-rail-wrap { position: relative; }

/* Spacer, centred heading, arrows. The empty first column is what keeps the
   heading optically centred in the section rather than centred in the space
   left over beside the buttons. */

.case-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 16px;
  margin-bottom: clamp(24px, 3vw, 40px);
}
/* the old centring spacer, wherever it is still in the markup */
.case-head > span { display: none; }
.case-head .sec-head { text-align: left; }
.case-head .case-nav { justify-self: end; }

.case-nav { display: flex; gap: 2px; }
/* The glyph on its own, no box. Kept at 42px square so the tap target is
   still a proper size even though nothing is drawn around it. */
.case-nav button {
  width: 42px;
  height: 42px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  background: none;
  color: var(--ink-strong);
  cursor: pointer;
  font-size: 1.2rem;
  line-height: 1;
  transition: color .3s var(--ease), transform .3s var(--ease-out), opacity .3s var(--ease);
}
.case-nav button:hover:not([disabled]) { color: var(--ink-hover); }
.case-nav [data-case-prev]:hover:not([disabled]) { transform: translateX(-3px); }
.case-nav [data-case-next]:hover:not([disabled]) { transform: translateX(3px); }
.case-nav button[disabled] { opacity: .32; cursor: default; }
.case-nav button:focus-visible { outline: 2px solid var(--ink-strong); outline-offset: 3px; }

/* no pointer, no need for the buttons — the swipe is right there, and
   site.js drives it by hand on touch so it is there on every browser */
@media (hover: none) { .case-nav { display: none; } }
@media (max-width: 700px) {
  .case-head { grid-template-columns: minmax(0, 1fr); }
  .case-head > span { display: none; }
  /* Right, not left. The heading is left-aligned above them, so a pair of
     arrows tucked under its first word read as part of the sentence; over
     on the right they read as a control, and they sit under the thumb that
     is about to swipe the rail. */
  .case-head .case-nav { justify-self: end; }
}

.case-rail {
  display: flex;
  gap: clamp(14px, 1.6vw, 22px);
  overflow-x: auto;
  /* PROXIMITY, NOT MANDATORY, AND NO CSS scroll-behavior.

     Both of those fight a finger on WebKit. `mandatory` re-snaps mid-drag,
     and `scroll-behavior: smooth` on a snap container makes every touch
     scroll a smooth animation the drag then has to interrupt — between
     them the rail would take the arrows and scrollLeft but sit still under
     a thumb. The arrows still animate: site.js passes behavior 'smooth' to
     scrollBy itself, which is where that belonged. */
  scroll-snap-type: x proximity;
  /* legacy iOS: momentum scrolling inside an overflow container. Ignored
     everywhere modern, and harmless where it is not. */
  -webkit-overflow-scrolling: touch;
  /* pan-y hands every horizontal touch gesture to the page's own handler in
     site.js, and leaves the vertical one to the browser. Native horizontal
     panning inside this box could not be relied on — on iOS it took the
     arrows and scrollLeft and sat still under a thumb — so the rail stopped
     asking for it. Trackpads and wheels are untouched: touch-action only
     governs touch. */
  touch-action: pan-y;
  /* Room for the hover shadow. overflow-x: auto forces overflow-y to auto
     too, so this box clips vertically whether or not it is asked to — the
     shadow was being cut off at the rail's own bottom edge, not by the
     section. The padding gives the shadow somewhere to live and the equal
     negative margin takes the space back out of the layout. */
  padding: 30px 8px 48px;
  margin: -30px -8px -48px;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.case-rail::-webkit-scrollbar { display: none; }

.case-card {
  position: relative;
  flex: none;
  /* 20% down on 17 September, from clamp(335px, 32.5vw, 429px), then 15%
     back up later the same day. Net effect against the original: about 8%
     smaller. Only the rails on the homepage and /services read this — the
     /case-studies grid and the hub pages set their own width — so this one
     number is both of the pages James asks about and neither of the ones he
     does not. */
  width: clamp(308px, 29.9vw, 394px);
  aspect-ratio: 3 / 4;
  scroll-snap-align: start;
  border-radius: var(--radius-card);
  overflow: hidden;
  background: var(--mist);
  text-decoration: none;
  transition: transform .5s var(--ease-out), box-shadow var(--mcard-t) var(--mcard-ease);
}
.case-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 26px 52px -28px rgba(28, 38, 53, .45);
}
.case-card img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform .7s var(--ease-out);
}
.case-card:hover img { transform: scale(1.045); }

/* THE SCRIM IS NOT DECORATION — it is what makes the card's text legible.

   It came off when the cards were artwork with a dark foot built into it,
   and went back on 15 September once a card could be any photograph: this
   one is an aerial shot whose bottom third is white concrete and pale
   asphalt, on which white type measures about 1.1:1 and disappears.

   Deep navy rather than black, and it clears the top 42% so the picture
   still reads as a picture. Measured in Pillow against the Ike aerial, the
   worst pixel under the text band carries the white title at 7.8:1 and the
   warm service line, which is small text and therefore needs 4.5:1, at
   4.8:1. Re-measure if the stops move or a lighter photograph arrives. */
.case-card::after {
  content: '';
  position: absolute;
  inset: 42% 0 0 0;
  z-index: 0;
  pointer-events: none;
  background: linear-gradient(to top,
    rgba(28, 38, 53, .96) 0%,
    rgba(28, 38, 53, .92) 20%,
    rgba(28, 38, 53, .78) 40%,
    rgba(28, 38, 53, .40) 65%,
    rgba(28, 38, 53, 0) 100%);
}

.case-body {
  position: absolute;
  inset: auto 0 0 0;
  z-index: 1;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 14px;
  padding: clamp(18px, 2vw, 24px);
}
.case-text { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.case-title {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(1.05rem, .95rem + .4vw, 1.25rem);
  line-height: 1.2;
  letter-spacing: -.022em;
  color: #FFFFFF;
  /* The scrim below does the legibility work now. This is left as a tight
     edge so the title still sits off the picture rather than in it. */
  text-shadow: 0 1px 2px rgba(12, 18, 26, .5);
}
/* The services the case is tagged with, under the title. It replaced two
   figures on 15 September: a card is the door to the story, and a number
   with no page around it is a claim the visitor cannot check yet.

   Set like the eyebrows elsewhere, in the warm accent, one per line and
   two at most: a third turns the foot of the card into a list, and a card
   is a door rather than an index. */
.case-svc {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  min-width: 0;
  font-family: var(--font-head);
  /* The title is the bold thing on a card. The services are a label under
     it, so they keep the size, the tracking and the caps and give up the
     weight. */
  font-weight: 400;
  font-size: .68rem;
  line-height: 1.3;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--warm);
  text-shadow: 0 1px 2px rgba(12, 18, 26, .5);
}


.case-go {
  flex: none;
  color: #FFFFFF;
  font-size: 1.35rem;
  line-height: 1;
  text-shadow: 0 1px 2px rgba(12, 18, 26, .5);
  transition: color .3s var(--ease), transform .35s var(--ease-out);
}
.case-card:hover .case-go { color: var(--warm); transform: translateX(4px); }

/* the two nudge buttons; hidden where there is no pointer to use them */

/* Three columns with an empty first one, so the heading centres on the
   section rather than on the space left over beside the buttons — which is
   what a flex row with space-between would have given. */

/* ==========================================================================
   SERVICES — the closing prompt under the list
   ========================================================================== */
/* centred under the list rather than hanging off its left edge */
.svc-cta {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: clamp(14px, 2vw, 26px);
  margin-top: clamp(26px, 3vw, 40px);
  text-align: center;
}
/* body face, normal weight: it is a prompt, not a heading */
.svc-cta p {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: clamp(.95rem, .92rem + .2vw, 1.05rem);
  color: var(--body);
}

/* ==========================================================================
   SECTION HEADS

   The old pattern was title, short warm rule, paragraph — stacked, every
   time. That triple is the thing that reads as a template, so the rule goes
   and the accent moves into the type instead: one phrase in the headline
   carries a warm underline, drawn behind the text rather than under the box.

   The paragraph is set smaller and on a tight measure, and offset from the
   headline's left edge, so hierarchy comes from size and position rather
   than from a divider.
   ========================================================================== */
/* Centred, and uncapped so a heading takes one line wherever there is room
   for one. text-wrap: balance only matters for the two that are long enough
   to wrap on a narrow screen — it splits them evenly instead of leaving one
   word stranded. */
.sec-head {
  max-width: none;
  margin-inline: auto;
  text-align: center;
}
/* Left is the default everywhere the content under the head is left-aligned,
   which by 12 September is every section on the site that has a head. */
.sec-head-left { text-align: left; }
.sec-head .h2 {
  font-size: clamp(1.7rem, 1.1rem + 2.2vw, 2.9rem);
  line-height: 1.06;
  letter-spacing: -.034em;
  text-wrap: balance;
}

/* ==========================================================================
   THE HIGHLIGHT
   Warm marker behind one phrase of a section head. Removed on 3 September
   while James rethought emphasis, asked for again on the 6th.

   Drawn as a background-image sized to 80% of the line box and sat low in
   it, not a full-height background: the headline runs tight, and a
   full-height fill collides with the line above wherever a highlighted
   phrase wraps.

   The type is a literal #304157 rather than var(--ink). That mattered when
   the token inverted for a dark theme, and it still holds now: the warm is
   fixed, so the type on it has to be too.

   box-decoration-break: clone gives every line of a wrapped phrase its own
   fill; the default slices one background across the fragments and leaves
   the second line bare.

   Not italic this time. The 3 September version was, and the italic was
   half of what made it read as a template.
   ========================================================================== */
/* RETIRED 12 September, at every width and on every page. The markup stays
   — .hl still wraps the phrases it always did, so the copy and the smoke
   test's checks by name are undisturbed — but it paints nothing now.
   Inside a testimonial it bolds instead; see .testi .hl below. */
.hl {
  background-image: none;
  background-repeat: no-repeat;
  background-size: 100% 80%;
  background-position: 0 78%;
  color: inherit;
  padding-inline: 0;
  margin-inline: 0;
  -webkit-box-decoration-break: clone;
          box-decoration-break: clone;
}
/* sweeps in when the block arrives */
.reveal .hl { background-size: 0% 80%; }
.reveal.is-in .hl {
  background-size: 100% 80%;
  transition: background-size .7s var(--ease-out) .15s;
}
@media (prefers-reduced-motion: reduce) {
  .reveal .hl, .reveal.is-in .hl { background-size: 100% 80%; transition: none; }
}


/* ==========================================================================
   BACK TO TOP
   Appears once the tool section has come into view, and stays for the rest
   of the page. Fixed to the bottom right on a translucent scrim so it reads
   over whatever is behind it — the arc at the foot of the page included.
   ========================================================================== */
.to-top {
  position: fixed;
  right: clamp(16px, 2.4vw, 30px);
  bottom: clamp(16px, 2.4vw, 30px);
  z-index: 70;
  width: 48px;
  height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, .16);
  border-radius: 50%;
  background: rgba(38, 52, 71, .62);
  -webkit-backdrop-filter: blur(9px);
          backdrop-filter: blur(9px);
  color: #FFFFFF;
  cursor: pointer;
  box-shadow: 0 14px 34px -16px rgba(12, 18, 26, .6);
  /* out of the way until it is wanted: no fade-in on first paint, and
     nothing to tab to while it is hidden */
  opacity: 0;
  visibility: hidden;
  transform: translateY(10px);
  transition: opacity .4s var(--ease), transform .4s var(--ease-out),
              visibility 0s linear .4s, background .3s var(--ease);
}
.to-top.is-on {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  transition: opacity .4s var(--ease), transform .4s var(--ease-out),
              visibility 0s, background .3s var(--ease);
}
.to-top:hover { background: var(--warm); color: #304157; border-color: transparent; }
.to-top:focus-visible { outline: 2px solid var(--warm); outline-offset: 3px; }
.to-top svg { width: 19px; height: 19px; fill: none; stroke: currentColor; stroke-width: 2.2; }

/* on the dark page the scrim inverts, so the glyph goes dark with it */

@media (prefers-reduced-motion: reduce) {
  .to-top { transition: opacity .001ms, visibility 0s; transform: none; }
  .to-top.is-on { transform: none; }
}


/* ==========================================================================
   GLOBAL PAGE HEADER
   The standard opening band on every page except the homepage. Title and
   one paragraph across two thirds of the width, left aligned; the last
   third is left empty, or holds a mark.

   Two thirds is a literal `2fr 1fr`, not a max-width on the text, so the
   empty third stays the same width whether or not anything is in it and the
   headline breaks at the same point on every page.

   The band reaches up behind the nav — a negative margin equal to the nav's
   height, added back as padding — so the circle runs to the top edge of the
   screen the way the hero's discs do. The nav is transparent at rest and
   sits at z-index 60, so it lies on the circle rather than cutting a paper
   strip above it.

   Nothing clips the circle. It is meant to run out of the bottom of the
   band and sit behind the copy in the section below, which it can do
   because sections carry no z-index: the disc at z-index 1 paints over the
   next section's background and under its .shell at z-index 2. `main` clips
   on x only, so the horizontal overhang is hidden and the vertical bleed is
   not. An earlier version clipped the band and needed a ramp to hide the
   cut; both are gone.
   ========================================================================== */
.page-head {
  /* One height for every page that uses this header, so two of them opened
     side by side line up. Content is centred in the box, which is also what
     makes the vertical rhythm inside the band identical whether the page
     has a title and one line or a title, a paragraph, buttons and a mark.

     It is a token because it is the one number worth tuning: it sets the
     band height AND the ceiling on anything in the media column. */
  --page-head-h: clamp(270px, 32vw, 470px);

  margin-top: calc(var(--nav-h) * -1);
  padding-top: calc(var(--nav-h) + clamp(56px, 6vw, 100px));
  padding-bottom: clamp(64px, 7.5vw, 116px);
}
.page-head .disc {
  --ph-disc: min(78vw, 900px);
  width: var(--ph-disc);
  aspect-ratio: 1;
  /* Leaves the same slice of screen covered at any width. A fixed negative
     offset instead lets the circle swallow most of a narrow screen. */
  right: calc(29vw - var(--ph-disc));
  /* Flush with the top of the band, which sits a nav's height above the top
     of the screen, so the circle hugs the very top corner the way the
     hero's warm disc does. It still reaches past the foot of the band, and
     the parallax carries it further down from there. */
  top: 0;
  opacity: .45;
}
.page-head-inner {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: clamp(26px, 4vw, 64px);
  align-items: center;
  min-height: var(--page-head-h);
  text-align: left;
}
.page-head-text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: clamp(13px, 1.5vw, 20px);
}
/* A step under .mat-head .h1 on the homepage. The homepage headline gets a
   full-width centred column; this one has two thirds of the width and a
   left edge, so the same clamp overran to three lines.

   Two lines is the ceiling and one is preferred, which is a copy
   constraint as much as a type one: if a title wraps to three, shorten the
   title rather than shrinking the type. */
.page-head .h1 {
  font-size: clamp(2.05rem, 1.2rem + 3.2vw, 3.5rem);
  line-height: 1.02;
  letter-spacing: -.038em;
}
.page-head .lede { max-width: 58ch; }
.page-head .btn-row { margin-top: 4px; }

/* The empty third, when a page puts something in it. The column exists
   either way, so adding a mark never moves the headline or changes the
   height of the band. */
.page-head-media {
  grid-column: 2;
  justify-self: center;
  align-self: center;
  width: 100%;
}
/* Height-capped rather than width-capped. The badge is portrait, so height
   is what would otherwise push the band taller than every other page's, and
   capping it at the band height is also what lets it grow to fill the
   column on a wide screen instead of stopping at a fixed pixel width. */
.page-head-media img,
.page-head-badge {
  display: block;
  width: auto;
  max-width: 100%;
  max-height: var(--page-head-h);
  height: auto;
  margin-inline: auto;
}

@media (max-width: 860px) {
  /* the empty third is dead space on a phone, so the grid collapses and a
     mark, if there is one, sits under the copy at a size that suits it */
  .page-head-inner { grid-template-columns: minmax(0, 1fr); min-height: 0; }
  .page-head-media { grid-column: 1; justify-self: start; width: auto; }
  .page-head-media img,
  .page-head-badge { max-width: clamp(160px, 46vw, 260px); max-height: none; margin-inline: 0; }
}

/* Zero-height sentinel at the foot of the band, and the element the nav
   watches. It cannot be the headline: the band's negative top margin puts
   the headline within the nav's collapse threshold at scroll zero, so the
   bar floated on load and the 90px hysteresis then stopped it ever
   expanding again on the way back up. Marking the bottom of the band means
   the bar is transparent for exactly as long as it sits over the circle,
   and expands again the moment the band is back on screen. Ships inside the
   component, so every page that uses the header gets it. */
.page-head-end { height: 0; }

/* The gradient circles are tuned for a light page; the global dark rule
   knocks every .disc back to .22, and this one is large enough that it
   needs to come down further still or it lights the whole band. */

/* ==========================================================================
   TOOL CLUMP
   A pile of marks, not a grid. Two rows, never more, spreading sideways
   rather than stacking: the rows are `nowrap`, so adding a mark makes the
   block wider, not taller.

   The block is full-bleed — `width: 100vw` with a negative margin that
   cancels the shell's inset — so "wider" has somewhere to go. `main` clips
   horizontally, so nothing can produce a scrollbar.

   The fade at each end is a mask on the container rather than opacity on
   individual marks. That way it depends on where a mark actually lands, not
   on its position in the source order, and it keeps working when the count
   changes or a row wraps.

   Each mark is nudged off its line by a few pixels and a fraction of a
   degree from nth-child. Those offsets are static and hand-tuned: do not
   replace them with random values, or the arrangement changes on every
   reload.
   ========================================================================== */
.tool-clump {
  width: 100vw;
  margin-inline: calc(50% - 50vw);
  margin-block: clamp(22px, 3vw, 40px) 0;
  /* `mask-clip` defaults to `border-box`, so a mask CROPS anything outside
     the element's own box. The nth-child offsets lift marks up to 9px above
     the first row, and the tallest lockup is 16px taller than a wordmark, so
     Google Workspace was losing the top of its G. This padding keeps every
     mark inside the box the mask is clipped to. Do not remove it. */
  padding-block: clamp(18px, 2vw, 30px);
  display: flex;
  flex-direction: column;
  gap: clamp(16px, 2.2vw, 34px);
  /* fades the marks out as they approach either edge of the screen */
  -webkit-mask-image: linear-gradient(to right,
    transparent 0, rgba(0, 0, 0, .35) 6%, #000 20%, #000 80%, rgba(0, 0, 0, .35) 94%, transparent 100%);
          mask-image: linear-gradient(to right,
    transparent 0, rgba(0, 0, 0, .35) 6%, #000 20%, #000 80%, rgba(0, 0, 0, .35) 94%, transparent 100%);
}
/* space-evenly rather than centred: the block is full-bleed, so the marks
   spread to fill the width instead of huddling in the middle of it. The gap
   becomes a floor rather than the actual spacing. */
.tool-row {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  justify-content: space-evenly;
  gap: clamp(14px, 2.2vw, 40px);
  padding-block: 6px;
  padding-inline: clamp(10px, 2.4vw, 44px);
}
/* the second row sits slightly off the first, so the two do not read as
   the rows of a table */
.tool-row + .tool-row { transform: translateX(clamp(-38px, -2.4vw, -14px)); }

.tool-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  transition: transform .4s var(--ease-out);
}
.tool-mark img {
  height: clamp(22px, 2.3vw, 37px);
  width: auto;
  max-width: 187px;
  object-fit: contain;
  filter: grayscale(1);
  opacity: .5;
  transition: filter .35s var(--ease), opacity .35s var(--ease);
}
/* Stacked lockups are roughly 2:1 where a wordmark is 4:1, so matching them
   on height alone makes them read at twice the weight of everything else. */
.tool-mark img[data-stack] { height: clamp(33px, 3.5vw, 55px); }
/* a tier between the default and HubSpot, for the two platforms nearest the
   middle that carry the most weight after it */
.tool-mark img[data-mid] { height: clamp(27px, 2.8vw, 45px); opacity: .58; }
.tool-mark:hover img[data-mid] { opacity: 1; }
/* Salesforce runs 30% over the tier it shares with Pipedrive: the cloud
   lockup is a squat shape and matching it on height alone under-sets it. */
.tool-mark img[data-big] { height: clamp(35px, 3.64vw, 59px); opacity: .58; }
.tool-mark:hover img[data-big] { opacity: 1; }

/* HubSpot is the one that has to land. It sits fourth of seven in the top
   row, which is the dead centre of the block and therefore the one position
   the edge mask can never touch, and it runs a size up on everything else. */
.tool-mark img[data-hero] { height: clamp(31px, 3.3vw, 53px); opacity: .62; }
.tool-mark:hover img[data-hero] { opacity: 1; }

.tool-mark:hover img { filter: none; opacity: 1; }

/* offsets are per row, so each row has its own 1..8 */
.tool-row .tool-mark:nth-child(1) { transform: translateY(-9px)  rotate(-2.5deg); }
.tool-row .tool-mark:nth-child(2) { transform: translateY(7px)   rotate(1.5deg); }
.tool-row .tool-mark:nth-child(3) { transform: translateY(-4px)  rotate(3deg); }
.tool-row .tool-mark:nth-child(4) { transform: translateY(11px)  rotate(-1.5deg); }
.tool-row .tool-mark:nth-child(5) { transform: translateY(-8px)  rotate(2deg); }
.tool-row .tool-mark:nth-child(6) { transform: translateY(5px)   rotate(-3deg); }
.tool-row .tool-mark:nth-child(7) { transform: translateY(-6px)  rotate(1deg); }
.tool-row .tool-mark:nth-child(8) { transform: translateY(9px)   rotate(-2deg); }

/* The magnification. Last in the file and matched on specificity, so it
   beats every offset above — both set `transform`, so source order decides.
   Do not tidy this back to a bare `:hover`. */
.tool-row .tool-mark:nth-child(n):hover {
  transform: translateY(-4px) rotate(0deg) scale(1.16);
}

@media (max-width: 760px) {
  /* Nowrap at fifteen marks is illegible on a phone. Letting each ROW wrap
     on its own gave 4 then 3 then 4 then 4, because the seven-mark row
     breaks before the eight-mark one does — a short row in the middle reads
     as a mistake.

     `display: contents` dissolves both rows so all fifteen marks flow as
     one list, and only the last row comes up short, which is what a wrapped
     list is supposed to look like. */
  .tool-clump { flex-direction: row; flex-wrap: wrap; justify-content: center; }
  .tool-row { display: contents; }
  .tool-row .tool-mark:nth-child(n) { transform: none; }
  .tool-row .tool-mark:nth-child(n):hover { transform: scale(1.16); }
  .tool-mark img { height: clamp(22px, 4.8vw, 33px); }
  .tool-mark img[data-stack] { height: clamp(33px, 7vw, 48px); }
  .tool-mark img[data-hero] { height: clamp(31px, 6.6vw, 46px); }
  .tool-mark img[data-mid] { height: clamp(27px, 5.8vw, 40px); }
  .tool-mark img[data-big] { height: clamp(35px, 7.5vw, 52px); }
}

/* ==========================================================================
   START PANEL
   One rounded panel: the ask on the left, the two ways to take it stacked
   down the right and split by a single hairline.

   That hairline is internal to a component, which is why it does not break
   the no-dividers-between-sections rule. It is a border on the second
   option rather than on the container, so it disappears by itself when the
   panel stacks on a narrow screen.
   ========================================================================== */
/* ==========================================================================
   START SECTION — the closing ask
   Type and one button, centred, nothing else. It was a left/right split
   while a supporting paragraph sat on the right; with that gone there was
   nothing to balance the shout against and the button read as an
   afterthought in an empty column.

   The shout runs larger than the page header now that it is alone in the
   section, which is what keeps it using the full width without a second
   column to fill.
   ========================================================================== */
.start-wide {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(26px, 3.4vw, 46px);
  text-align: center;
  /* the same inset the full-bleed sections use, so it lines up with
     everything above even though it is not in a .shell */
  padding-inline: clamp(20px, 5vw, 60px);
}
.start-shout {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(2.8rem, 1rem + 7.4vw, 7rem);
  line-height: .94;
  letter-spacing: -.048em;
  color: var(--ink);
  margin: 0;
}
/* the closing button carries more weight than the ones up the page */
.btn-lg { padding-block: 15px; padding-inline: 34px; font-size: .95rem; }

@media (max-width: 900px) {
  /* the forced break is wrong once the column is narrow */
  .start-shout br { display: none; }
}

/* ==========================================================================
   PAPER TO WHITE
   The About section and the tool clump sit on paper, the start panel's
   section sits on white, and the step between them was a visible line
   across the page. These two carry the change instead, half each, so the
   ground arrives at white by the time the panel does.

   Ordinary backgrounds rather than .fade-top / .fade-bottom: those ramp a
   colour over a fixed distance at one edge, which is right for a seam
   between two surfaces and wrong for a drift across two whole sections.
   ========================================================================== */
/* RETIRED 12 September, with the white testimonial band and the photograph
   behind the close that the drift existed to deliver the page into. Same as
   the fades: the classes stay in the markup and paint nothing. */
.to-white-1, .to-white-2 { background-image: none; }

/* ==========================================================================
   START SECTION BACKGROUND
   The artwork is back, and it now spans two sections rather than one: full
   strength at the foot of the close, fading out as it rises, gone by about
   halfway up the testimonial above it.

   `--start-bleed` pulls the section up over the testimonial and gives the
   same distance back as padding, so the background travels while the layout
   does not. The testimonial's own content stays on top of it because
   sections carry no z-index and `main > section > .shell` is z-index 2, the
   same layering that keeps the discs off the text.

   The zoom is 200%, up from the 160% it used when this covered one section.
   The file has a steel-blue corner where navy type measures 1.7:1, and a
   taller frame pulls more of it into view: at 160% a 1050px-tall section put
   4% of the copy area under the 4.5:1 floor. At 200% the worst pixel is
   4.9:1 at that height and 5.9:1 at 900px. Re-measure if the artwork or the
   section heights change.
   ========================================================================== */
/* NO ARTWORK BEHIND IT, since 12 September, and no bleed up over the
   section above it either. The photograph was the reason this section pulled
   itself up 430px, the reason the two sections above it drifted to white,
   and the reason the shout and its button had to be pinned to light-theme
   colours in the dark theme. All four of those went with it.

   It also sits tighter than a section: two thirds of the shared rhythm above
   and below, so the shout reads as the page's full stop rather than as one
   more band. */
.start-section {
  --start-bleed: 0px;
  margin-top: 0;
  padding-block: calc(var(--sec-pad) * .66);
  background: none;
}


/* ==========================================================================
   FOOTER
   Brand and the single published address on the left, two link columns on
   the right, one bottom bar. The address is a display link rather than a
   list item: it is the only contact route on the site, and it was wasted
   set at the same size as "Pricing".
   ========================================================================== */
.footer-top {
  display: grid;
  /* three link columns since 11 September. The brand column gives up room to
     pay for the third one: at the old 1.1fr the longest service names —
     "HubSpot support retainers", "Lead to cash mapping" — each wrapped to two
     lines, which turned a five-item column into a nine-line one. */
  grid-template-columns: minmax(0, .82fr) minmax(0, 1.75fr) auto;
  gap: clamp(28px, 4vw, 68px);
  /* tightened on 4 September: there was more air above the copyright line
     than under the columns it separates */
  padding-bottom: clamp(12px, 1.4vw, 20px);
}
.footer-brand { display: flex; flex-direction: column; align-items: flex-start; gap: 14px; }
.footer-pitch {
  font-size: .9rem;
  line-height: 1.55;
  color: var(--on-navy-muted);
  max-width: 34ch;
}
.footer-mail {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(1.25rem, 1rem + 1.1vw, 1.85rem);
  letter-spacing: -.028em;
  line-height: 1.1;
  color: var(--paper);
  margin-top: 4px;
  /* the underline is the affordance at this size, where the colour alone
     would just read as a heading */
  text-decoration: underline;
  text-decoration-thickness: 1.5px;
  text-underline-offset: 5px;
  text-decoration-color: rgba(250, 250, 248, .35);
  transition: color .25s var(--ease), text-decoration-color .25s var(--ease);
}
.footer-mail:hover { color: var(--warm); text-decoration-color: var(--warm); }
.footer-place { font-size: .82rem; color: var(--on-navy-faint); }

/* Services and Resources are links to their own pages; Company has no page
   of its own, so it stays plain text. The anchor inherits the h4 rather than
   carrying its own size, so a linked heading and an unlinked one set
   identically. */
.footer h4 a { color: inherit; font-size: inherit; }
.footer h4 a:hover { color: var(--paper); }

/* Four columns since 17 September (Platforms joined). A flex row with
   space-between rather than equal grid tracks: the columns hold text of very
   different widths, so equal tracks left uneven air between them. This way
   the gaps between the four are equal and the row fills its grid cell. The
   phone rules further down switch it back to a single-column grid. */
.footer-links {
  display: flex;
  justify-content: space-between;
  gap: clamp(18px, 2.4vw, 36px);
  width: 100%;
}

/* TWO REVERSED PARTNER MARKS, STACKED.

   Both are the vendors' own white artwork — HubSpot ships the reversed
   Platinum badge, and the Pipedrive mark is their green Authorized Partner
   badge with the green box dropped, which is why neither needs the paper
   chip or the filter the colour versions did. 0% of either mark's ink
   measures under 3:1 on the navy; about 20% of the colour badge did.

   A Pipedrive badge was pulled from this slot once for reading as a logo
   wall beside the Platinum badge. What fixed that is the single column
   width below: both marks are set to --badge-w and centred, so the pair
   reads as one lockup with two lines rather than two competing objects. */
.footer-badges {
  --badge-w: clamp(104px, 9.5vw, 138px);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(16px, 1.8vw, 24px);
  justify-self: end;
}
.footer-badges a {
  display: flex;
  align-items: center;
  justify-content: center;
  transition: opacity .25s var(--ease), transform .35s var(--ease-out);
}
.footer-badges a:hover { opacity: .82; transform: translateY(-2px); }
.footer-badges a:focus-visible { outline: 2px solid var(--warm); outline-offset: 6px; border-radius: 6px; }
.footer-badges img {
  display: block;
  width: var(--badge-w);
  height: auto;
  max-width: 100%;
  max-height: 150px;
  object-fit: contain;
}
/* the Pipedrive lockup is a wordmark over a rule over two words, so it
   carries less ink per pixel of width than the badge above it and needs a
   little more of it to sit at the same visual weight */
.footer-badge-pipedrive img { width: calc(var(--badge-w) * 1.14); }

@media (max-width: 1040px) {
  /* the badges leave the row here, so the two that remain split it — but not
     evenly: three link columns need most of the width or every service name
     wraps, and the brand block is a lockup, two lines and an address */
  .footer-top { grid-template-columns: minmax(0, .68fr) minmax(0, 2fr); }
  /* the badges drop to their own full-width row here, so the pair goes back
     side by side: stacked, they would leave a column of air beside them */
  .footer-badges { grid-column: 1 / -1; flex-direction: row; align-items: center; justify-self: start; }
}
@media (max-width: 820px) {
  .footer-top { grid-template-columns: minmax(0, 1fr); }
  .footer-links { max-width: none; }
}
@media (max-width: 460px) {
  .footer-links { gap: 18px 14px; }
}


/* ==========================================================================
   ABOUT — the offset pair
   Two photographs at different sizes, the smaller overlapping the larger,
   both turned a couple of degrees. Same loose hand as the tool clump, and
   deliberately not a card: this section sits under five 3:4 poster cards,
   and one more rectangle squared to the grid read as more of the same.

   The container carries padding on the two sides the front image hangs
   over, so the overhang is inside the box and nothing has to be clipped or
   pulled out of flow to make room.

   Both images tilt AWAY from the pointer, and the front one responds about
   twice as hard as the back. That difference is the whole effect: moved by
   the same amount they read as one flat sheet. site.js only writes --tx and
   --ty in the range -1 to 1; the angles, the resting rotation and the
   easing are all decided here.
   ========================================================================== */
/* 40/60, image then copy, the ratio James set on 5 September */
.about-split { grid-template-columns: minmax(0, 2fr) minmax(0, 3fr); }
@media (max-width: 1000px) { .about-split { grid-template-columns: minmax(0, 1fr); } }

.about-stack {
  position: relative;
  perspective: 1200px;
  padding: 0 7% 12% 0;
  /* capped rather than filling its column: at full width the pair was the
     heaviest thing in the section, and the copy beside it is the argument */
  max-width: 400px;
  justify-self: center;
  --tx: 0;
  --ty: 0;
}
.about-img {
  display: block;
  border-radius: var(--radius-card);
  object-fit: cover;
  background: var(--mist);
  transition: transform .45s var(--ease-out), box-shadow .45s var(--ease);
  will-change: transform;
}
.about-img-back {
  width: 100%;
  aspect-ratio: 4 / 5;
  transform:
    perspective(1200px)
    rotate(-2.2deg)
    rotateY(calc(var(--tx) * 3deg))
    rotateX(calc(var(--ty) * -3deg))
    scale(var(--tilt-scale, 1));
  box-shadow: 0 26px 54px -30px rgba(28, 38, 53, .42);
}
/* Sits over the lower right of the one behind it. The HubSpot Platinum
   badge, on its own transparent artwork: no plate, no border, no shadow —
   just the mark, kept at the same loose angle the photograph beside it has. */
.about-img-front {
  position: absolute;
  right: 0;
  bottom: 0;
  /* 56% until 12 September; a fifth larger, at James' ask */
  width: 67%;
  height: auto;
  background: none;
  border: 0;
  border-radius: 0;
  object-fit: contain;
  transform:
    perspective(1200px)
    rotate(3.2deg)
    rotateY(calc(var(--tx) * 6deg))
    rotateX(calc(var(--ty) * -6deg))
    scale(var(--tilt-scale, 1));
  box-shadow: none;
}
.about-stack:hover .about-img {
  --tilt-scale: 1.02;
  /* the pointer moves in small steps, so the tilt must not lag behind it
     the way the settle does */
  transition: transform .12s linear, box-shadow .45s var(--ease);
}

@media (max-width: 1000px) {
  /* the pair keeps its shape when the split collapses, just narrower, so it
     does not become the tallest thing on a phone */
  .about-stack { max-width: 340px; justify-self: start; }
}
@media (prefers-reduced-motion: reduce) {
  .about-img, .about-stack:hover .about-img { transition: none; }
  .about-img-back { transform: rotate(-2.2deg); }
  .about-img-front { transform: rotate(3.2deg); }
}


/* ==========================================================================
   MOBILE FOOTER
   Everything centred, and the two link columns kept side by side at half
   each rather than stacking: they are four short items apiece and stacking
   them made the footer twice as long for no gain.
   ========================================================================== */
@media (max-width: 820px) {
  .footer-top { justify-items: center; text-align: center; }
  .footer-brand { align-items: center; }
  .footer-pitch { max-width: 40ch; }
  /* three columns, two abreast: Resources wraps under Services and Company
     rather than squeezing three columns of link text onto a phone. The third
     column then spans the row, because a lone column sitting in the left half
     of a footer where everything else is centred reads as a mistake. */
  .footer-links {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    justify-self: center;
    max-width: 26rem;
  }
  .footer-links > div:nth-child(3) { grid-column: 1 / -1; }
  .footer-badges { justify-self: center; }
}
/* the 460px rule below used to drop the link columns to one; they stay at
   two all the way down, which is the point of the block above */

/* ==========================================================================
   TESTIMONIAL
   One quote at two thirds, the sticker at one third. White ground, so the
   tool clump's drift to white lands on it and the artwork rising from the
   close below fades into it rather than meeting an edge.
   ========================================================================== */
/* No ground of its own since 12 September. It used to be a white band that
   the sections either side drifted into; the drifts are gone, so it sits on
   the page the way every other section does — paper in the light theme,
   navy in the dark one — and keeps the shared rhythm. */
.testi-section { background: transparent; }
/* Three quotes, three columns, Deborah in the left one.

   `align-items: center`, so each quote sits in the middle of its own column
   rather than hanging from the top of the row. The longest quote sets the
   row height and the two shorter ones float in the middle of theirs, which
   means the three titles land on three different lines. That is deliberate:
   James asked for it after seeing the topped-out version, where the ragged
   bottoms were doing all the talking. Centred, the block reads as three
   quotes of different lengths rather than three columns that ran out.

   No cards. The page already has poster cards, service rows and the logo
   clump; space does the separating here, same as it did when this was one
   quote in the middle two of four columns, and before that a boxed pair. */
.testi {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(28px, 4vw, 62px);
  align-items: center;
}
.testi-col { display: flex; flex-direction: column; gap: clamp(12px, 1.6vw, 18px); }

/* The marker inside a quote is slate rather than the warm. Same drawing as
   .hl, only the two colours change — and the type on it goes white, because
   navy on slate is a smudge. Fixed literals for the same reason .hl uses
   them: the pair must never invert apart. */
/* The slate marker is gone too. The phrases James picked out are still the
   ones worth picking out, so they take weight rather than a background. */
.testi .hl {
  background-image: none;
  color: var(--ink);
  font-weight: 700;
}
/* the quote marks around the title carry the warm, which is why the big
   floating quote glyph that used to sit under it came out: two of them was
   one too many. The marks stay upright while the words lean, because an
   italic quote mark next to an italic word reads as a smudge. */
/* A third of the width takes a smaller title than one quote across the page
   did. Its own size rather than .h2, which is why it is an h3 in the markup
   under an sr-only h2. */
.testi-title {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(1.1rem, 1rem + .5vw, 1.4rem);
  line-height: 1.2;
  letter-spacing: -.02em;
  color: var(--ink);
  font-style: italic;
  margin: 0;
}
.testi-title .q { color: var(--warm); font-style: normal; }

/* stars and attribution on one line */
.quote-by {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 14px;
  margin-top: 4px;
}
/* the divider is lighter than the words either side of it, so the name and
   the role read as two things rather than one run-on string */
.quote-by .sep { color: var(--slate); font-weight: 400; padding-inline: 3px; }
/* One line: name, divider, role.

   `display: block` is load-bearing. This was a flex column back when the
   cite held a name above a company; the rule outlived that content and
   stacked "Deborah", "|" and "COO" as three rows. There were also two
   `.quote-by cite` rules for a while, which is worse than it sounds: the
   page was right because the later one won, while the smoke test read the
   earlier one and reported a bug that was not there. One rule now. */
.quote-by cite {
  display: block;
  font-style: normal;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: .88rem;
  color: var(--ink);
}


/* five navy stars, one path repeated */
.stars { display: flex; gap: 4px; }
.stars svg { width: 17px; height: 17px; fill: var(--ink-strong); }

@media (max-width: 900px) {
  /* two up, then one: three columns of quote are unreadably narrow before
     they are unreadably short */
  .testi { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: clamp(26px, 5vw, 40px); }
}

/* the quote, wherever it is used */
.quote { display: flex; flex-direction: column; gap: 12px; margin: 0; }
.quote-mark {
  font-family: var(--font-head); font-weight: 700;
  font-size: 2.4rem; line-height: .6; color: var(--warm);
  /* decoration, not content: keep it out of the reading order */
  user-select: none;
}
.quote p {
  font-size: clamp(.95rem, .92rem + .2vw, 1.05rem);
  line-height: 1.6; color: var(--ink);
  font-style: italic;
}
/* the attribution stays upright: it is a name, not part of the quote */


@media (max-width: 640px) {
  .testi { grid-template-columns: minmax(0, 1fr); gap: clamp(28px, 7vw, 44px); }
}


/* ==========================================================================
   PAPER TO WHITE — one section doing it alone

   .to-white-1 and .to-white-2 split the drift across two sections, which is
   what the homepage needs. Every other page has one section between the last
   piece of content and the close, so it has to cover the whole distance
   itself. Same two colours, same direction, one step instead of two.

   It exists because the close pulls up over whatever is above it by
   --start-bleed and paints white across that bleed. Landing that white on
   paper leaves a visible edge where the ramp ends; landing it on a ground
   that has already arrived at white leaves nothing to see.
   ========================================================================== */
.to-white { background-image: none; }

/* ==========================================================================
   PROSE — terms, privacy, and anything else that is just words

   A single measure with headings and paragraphs, no components. Capped at
   72ch rather than the full section width: legal copy is the one place on
   the site where someone reads every line, and a 1200px measure is unreadable
   for that.
   ========================================================================== */
.prose { max-width: 72ch; display: flex; flex-direction: column; gap: 18px; }

/* ---------- /privacy and /terms only ----------
   Two departures from the inner-page defaults.

   THE HEADER SITS CLOSER TO THE COPY. It used to be the shared hero's
   bottom padding plus a full .section top, up to 188px of nothing between
   the lede and the first heading. On a page that is only headings and
   paragraphs that read as a hero that failed to load rather than as
   breathing room. This is roughly a third of it, and the title and lede
   tighten to match.

   THE COPY RUNS THE FULL SHELL. A deliberate trade against the 72ch
   measure, which is the right call for something read start to finish and
   the wrong one here: nobody reads a privacy policy through, they scan for
   a heading and read the one clause under it. Full width keeps a clause on
   fewer lines and the page shorter to scroll to the heading you want.

   It is a long line. main > section > .shell runs to 60px off each edge and
   caps at 1680, so a paragraph here is near 170 characters on a 1440 laptop
   and near 195 on a wide display, well past a comfortable measure. That was
   the ask. If it ever reads as too long, put a max-width back on
   .legal-section .prose (90ch to 100ch is the usual landing spot) rather
   than reaching for the font size.

   The hero rule needs both classes: .page-hero is defined further down this
   file, so a single .page-hero-legal loses the padding-bottom on order and
   the gap silently stays at the shared 72px. */
.page-hero.page-hero-legal { padding-bottom: 0; }
.page-hero-legal .page-hero-text { gap: clamp(8px, .9vw, 12px); }
.legal-section { padding-block: clamp(22px, 2.4vw, 32px) clamp(64px, 8vw, 116px); }
.legal-section .prose { max-width: none; }
.prose h2 {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(1.15rem, 1.02rem + .55vw, 1.4rem);
  line-height: 1.25;
  letter-spacing: -.02em;
  color: var(--ink);
  margin-top: 22px;
}
.prose h2:first-child { margin-top: 0; }
.prose p, .prose li { font-size: .95rem; line-height: 1.7; color: var(--body); }
.prose ul { display: flex; flex-direction: column; gap: 8px; padding-left: 20px; list-style: disc; }
.prose li::marker { color: var(--mist); }
.prose a { text-decoration: underline; text-underline-offset: 3px; }
.prose-meta { font-size: .82rem; color: var(--muted); }

/* ==========================================================================
   FORM — labels above fields, no card around it

   Deliberately not a .card. The contact page has one job and the form is it;
   putting a frame round the only thing on the page reads as a widget dropped
   into a layout rather than as the layout. Fields carry the plate tokens, so
   they invert with the theme without a dark rule of their own.
   ========================================================================== */
.form { display: flex; flex-direction: column; gap: clamp(14px, 1.8vw, 20px); }
.form-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: clamp(14px, 1.8vw, 20px); }
@media (max-width: 640px) { .form-row { grid-template-columns: minmax(0, 1fr); } }
.field { display: flex; flex-direction: column; gap: 7px; }
.field > label {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: .82rem;
  letter-spacing: -.01em;
  color: var(--ink);
}
.field input,
.field select,
.field textarea {
  font-family: var(--font-body);
  font-size: .95rem;
  line-height: 1.5;
  color: var(--ink);
  background: var(--plate);
  border: 1px solid var(--plate-line);
  border-radius: var(--radius-btn);
  padding: 12px 14px;
  width: 100%;
  transition: border-color .25s var(--ease), background .25s var(--ease),
              box-shadow .25s var(--ease);
}
.field textarea { resize: vertical; min-height: 148px; }
.field input::placeholder,
.field textarea::placeholder { color: var(--muted); }
.field input:hover,
.field select:hover,
.field textarea:hover { border-color: var(--mist); }
.field input:focus,
.field select:focus,
.field textarea:focus {
  outline: 0;
  border-color: var(--ink-strong);
  box-shadow: 0 0 0 3px rgba(48, 65, 87, .12);
}
/* the arrow on a select, drawn rather than left to the platform, so it
   matches the arrows everywhere else on the site */
.field select {
  appearance: none;
  -webkit-appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--slate) 50%),
                    linear-gradient(135deg, var(--slate) 50%, transparent 50%);
  background-position: calc(100% - 19px) calc(50% + 1px), calc(100% - 14px) calc(50% + 1px);
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: 38px;
}
.form-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 16px; }
.form-note { font-size: .82rem; line-height: 1.5; color: var(--muted); }

/* ==========================================================================
   FIGURE BAND — the three numbers on a case study

   Type on a navy band, nothing else: no cards, no borders, no plates. The
   figures are the only thing on the site set larger than an H1, which is the
   point of the band. `data-count` on the number animates it on entry.
   ========================================================================== */
.figs {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(24px, 4vw, 64px);
}
@media (max-width: 760px) { .figs { grid-template-columns: minmax(0, 1fr); gap: 28px; } }
.fig { display: flex; flex-direction: column; gap: 4px; }
.fig > b {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(2.4rem, 1.5rem + 3.6vw, 4rem);
  line-height: 1;
  letter-spacing: -.04em;
  color: var(--paper);
}
/* Direct child only. `.fig span` also matched the counter's own span inside
   the <b>, which reset the 64px figure to 14.4px and made the whole navy
   band read as three lines of small print. */
.fig > span { font-size: .9rem; line-height: 1.5; color: var(--on-navy); }

/* ==========================================================================
   ROW LIST — the services list, reused

   Three columns rather than four: name, description, link. Everything else
   about it — the fill that bleeds past the text, the title stepping in, the
   arrow sliding — is inherited from .svc-row, because it is the same
   component doing the same job on a different list.
   ========================================================================== */
.svc-list-plain .svc-row { grid-template-columns: minmax(0, .82fr) minmax(0, 1.5fr) auto; }
@media (max-width: 820px) {
  .svc-list-plain .svc-row { grid-template-columns: minmax(0, 1fr); }
}

/* three across, for values and certifications — text blocks, not cards */
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 900px) { .grid-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 620px) { .grid-3 { grid-template-columns: minmax(0, 1fr); } }
/* the ticked list, in two columns, for anything that runs long */
.ticks-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 11px clamp(24px, 4vw, 56px); }
@media (max-width: 700px) { .ticks-2 { grid-template-columns: minmax(0, 1fr); } }

/* ==========================================================================
   MEETINGS EMBED
   HubSpot draws its own chrome inside the iframe, so this adds none: no
   border, no plate, no shadow. All it does is give the widget a measure and
   centre it.
   ========================================================================== */
.meet-wrap { max-width: 960px; margin-inline: auto; }
.meetings-iframe-container { width: 100%; }
.meetings-iframe-container iframe { border: 0; width: 100%; display: block; }

/* ==========================================================================
   PAGE HERO — the opening band on every page below the homepage

   Replaces .page-head, which was a headline in the left two thirds with a
   900px gradient circle filling the right and 741px of band height to hold
   it. On a wide display that was a screen and a half before anything
   happened. This is roughly half the height and the right-hand third does
   real work.

   Type left, artwork right, running off the edge of the screen. The media
   is absolutely positioned inside .shell — which is full-bleed on every
   section (see `main > section > .shell`) — so `right: 0` is the screen
   edge, while .page-hero-inner reserves a matching column so the copy
   never runs under it.

   It starts BELOW the nav rather than pulling up behind it the way the
   header used to. The nav is transparent at rest with navy links sitting
   hard right, which is exactly where the artwork is; running the image up
   behind them made the links unreadable on load.

   No gradient circle, here or anywhere else below the homepage. The disc
   is the homepage's device now.
   ========================================================================== */
.page-hero {
  position: relative;
  /* pulled up under the nav so the artwork runs to the top of the screen,
     the way the homepage's disc does. The copy column keeps clear of the bar
     with its own padding. */
  margin-top: calc(var(--nav-h) * -1);
  padding-top: calc(var(--nav-h) + clamp(26px, 3vw, 48px));
  /* THE HEADER GIVES NO BOTTOM GAP. The section under it carries --sec-pad
     and that is the whole distance, on every page. Put a bottom padding back
     here and this one page opens wider than the rest of the site. */
  padding-bottom: 0;
}
.page-hero-inner {
  display: grid;
  /* the second column is a spacer the artwork sits over, so the two stay
     the same width and the headline breaks at the same point on every page */
  grid-template-columns: minmax(0, 1fr) var(--hero-media-w);
  --hero-media-w: min(38vw, 600px);
  gap: clamp(28px, 4vw, 60px);
  align-items: center;
  /* 480 put the band back at 742px tall, which is where it was when James
     first said it pushed the page down. */
  min-height: clamp(280px, 25vw, 396px);
}
.page-hero-text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: clamp(14px, 1.5vw, 20px);
}
/* Bigger than .page-head's headline was, and on a shorter band: the size
   is what carries the top of the page now that the circle is gone. */
/* Bigger than it was. The old size left the headline floating in the middle
   of its own column with nothing holding the top of the page down. */
.page-hero .h1 {
  /* 4.6rem was too big for a column this wide: 'Five ways in and one system
     underneath' broke to three lines, and three is the one thing the type
     rules do not allow. This size holds it to two. */
  font-size: clamp(2.3rem, 1.2rem + 3.4vw, 3.7rem);
  line-height: 1;
  letter-spacing: -.04em;
  /* 15ch forced 'Five ways in and one system underneath' to three lines, and
     three is the one thing the type rules do not allow. */
  max-width: 20ch;
}
.page-hero .lede { max-width: 42ch; font-size: clamp(1.05rem, .98rem + .4vw, 1.24rem); }


/* The meta line — timing, price, whatever the page's two facts are. Set as
   type on a rule rather than as chips: chips here read as tags on the
   headline, and these are facts about the service. */
.hero-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 6px clamp(20px, 2.6vw, 38px);
  padding-top: clamp(12px, 1.4vw, 18px);
  width: 100%;
  max-width: 46ch;
  margin-top: 4px;
}
.hero-meta div { display: flex; flex-direction: column; gap: 1px; }
.hero-meta dt, .hero-meta-label {
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}
.hero-meta dd, .hero-meta-val {
  margin: 0;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(1rem, .94rem + .3vw, 1.15rem);
  letter-spacing: -.02em;
  color: var(--ink);
}

.page-hero-media {
  position: absolute;
  /* 0, not --nav-h. The band's own padding-top already clears the bar; adding
     the nav height on top of it opened an 86px gap between the two. */
  top: 0;
  bottom: 0;
  right: 0;
  width: var(--hero-media-w, min(40vw, 620px));
  overflow: hidden;
  /* square to the screen edge, rounded where it meets the copy */
  border-radius: var(--radius-card) 0 0 var(--radius-card);
  background: var(--mist);
}
.page-hero-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* A mark rather than a photograph — the Platinum badge on the HubSpot page.
   Contained and padded instead of cropped, and on the page ground rather
   than a plate, because the artwork carries its own. */
.page-hero-media.is-mark {
  background: none;
  border-radius: 0;
  display: grid;
  place-items: center;
  padding: clamp(10px, 1.6vw, 26px);
}
/* `width: auto` matters. .page-hero-media img sets width: 100% for the
   photographs, and a portrait badge given the full column width overflows
   the band's height, where the column's own overflow: hidden crops it —
   which on 11 September took the bottom off SOLUTIONS PARTNER as soon as
   the page lost the meta row that had been propping the band taller. Sized
   by height instead, it fits whatever the band is. */
.page-hero-media.is-mark img {
  object-fit: contain;
  width: auto;
  max-width: 100%;
  height: auto;
  max-height: 100%;
  /* AND `min-height: 0`, which is the half that actually does it. A grid
     item's automatic minimum size is its content — for an image, the height
     its intrinsic ratio gives at the width max-width allows — and that
     minimum outranks max-height. Without this the badge computes 584px tall
     inside a 308px box and the overflow: hidden above takes the bottom off
     it, which is exactly what it looks like: artwork cropped rather than
     scaled. */
  min-height: 0;
  min-width: 0;
}

/* ---------- a header whose mark is bigger than its band ----------

   /hubspot only. Two things this fixes, and they are the same thing:

   THE TITLE SAT 44px LOWER THAN EVERY OTHER PAGE'S. The shared header
   centres its two columns on each other, which is right when the artwork is
   a photograph running the height of the band. With a badge in that column
   it meant the title floated in the middle while /services, /resources and
   the rest all start theirs at the same height. Top-aligned here, with
   .page-hero-nomedia's extra 30px, so the two pages open on the same line.

   THE BADGE RAN OUT OF THE BOTTOM OF THE SECTION. It is a third taller than
   the band by design — see .is-big — and centred, so it overhangs by about
   28px at each end. At the top that is clearance under the nav; at the
   bottom it was crossing into the section below. The bottom padding here is
   that overhang and nothing more: the header still gives no gap of its own,
   it just stops the artwork leaving the room. Re-measure if .is-big's
   percentage changes.

   The badge does not move down so much as stay put while the copy rises,
   which is the same picture and one fewer number to keep in step. */
.page-hero-markhead {
  padding-top: calc(var(--nav-h) + clamp(26px, 3vw, 48px) + 30px);
  padding-bottom: clamp(30px, 2.8vw, 42px);
}
.page-hero-markhead .page-hero-inner { align-items: start; }
/* AND THE SECTION UNDER IT GIVES THE PADDING BACK. Every other header on
   the site has no bottom padding, so the whole gap is the next section's
   --sec-pad and every page opens the same. This one has to carry the
   badge's overhang, and left alone that overhang was simply added to the
   gap: the header ran 40px longer than anywhere else before the first
   heading. Subtracting it here keeps the distance the site's, and the badge
   stands in the space rather than pushing it open. */
.page-hero-markhead + .section {
  padding-top: calc(var(--sec-pad) - clamp(30px, 2.8vw, 42px));
}

/* A THIRD BIGGER THAN THE BAND. The badge is height-bound — max-height: 100%
   of the media column — so the only way to grow it is to let it past that,
   and the only way to let it past that is to stop clipping. .is-mark never
   needed the clip: it is contained artwork on the page ground with no plate
   and no radius, so there is nothing for overflow: hidden to protect.

   It is centred in the column, so the extra height falls half above and half
   below. At 1440 that is 29px each way: clear of the nav at the top, and at
   the bottom it lands inside the next section's own top padding rather than
   on anything in it. Re-check both if the band's height changes. */
.page-hero-media.is-mark.is-big { overflow: visible; }
.page-hero-media.is-mark.is-big img { max-height: 133%; }

/* The whole mark is the link. No chrome on it — the badge is finished
   artwork and a border round it would read as a second frame — so hover is
   the arrow-less version of what every card does, and the ring sits outside
   it where it is not fighting the badge's own edge. */
.page-hero-mark-link {
  display: grid;
  place-items: center;
  /* it has to FILL the column, not shrink to the badge: the badge's
     max-height is a percentage, and a percentage of a box that is sized by
     its own content resolves to nothing */
  width: 100%;
  height: 100%;
  min-height: 0;
  min-width: 0;
  transition: transform .4s var(--ease-out);
}
.page-hero-mark-link:hover { transform: translateY(-3px); }
.page-hero-mark-link:focus-visible { outline: 2px solid var(--ink-strong); outline-offset: 6px; border-radius: 6px; }
@media (prefers-reduced-motion: reduce) { .page-hero-mark-link:hover { transform: none; } }

@media (max-width: 900px) {
  /* the artwork comes back into the flow and runs the width of the column,
     so a phone gets a proper 16:9 band rather than a sliver down one side */
  .page-hero-inner { grid-template-columns: minmax(0, 1fr); min-height: 0; }
  .page-hero-media {
    position: static;
    width: auto;
    aspect-ratio: 16 / 9;
    border-radius: var(--radius-card);
    margin-top: clamp(24px, 5vw, 36px);
  }
  .page-hero-media.is-mark { aspect-ratio: auto; margin-inline: auto; max-width: 300px; }
  .page-hero .lede, .hero-meta { max-width: none; }
}

/* Legal pages and anything else with no artwork: same band, one column,
   and the headline steps down because there is nothing beside it. */
.page-hero-plain .page-hero-inner { grid-template-columns: minmax(0, 1fr); min-height: 0; }
.page-hero-plain .h1 { font-size: clamp(2rem, 1.3rem + 2.6vw, 3.1rem); }

/* ==========================================================================
   PANEL CARDS — the frosted card with chips, borrowed off the hero

   These replace the dash-bullet lists that were carrying most of the
   content on every inner page. Same surface as the stage cards on the
   homepage — the --card-veil ground, the blur, one hairline — so the inner
   pages are built out of the homepage's own parts rather than a second
   vocabulary.

   auto-fit rather than a fixed column count: three cards on a laptop, four
   or five across a wide display, one on a phone, and no media query per
   layout. The minmax floor is what stops a card getting narrower than its
   chips can wrap in.
   ========================================================================== */
.pcards {
  display: grid;
  gap: clamp(16px, 1.8vw, 24px);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 264px), 1fr));
}
/* when a page wants exactly two, side by side, and not three-up on a wide
   screen — in scope / out of scope, and the like */
.pcards-2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr)); }
/* when a page wants exactly three, because the count divides by three and
   auto-fit does not know that: six hub cards fit four-then-two on a wide
   display, and the orphan row reads as a card that failed to load */
.pcards-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 980px) { .pcards-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 620px) { .pcards-3 { grid-template-columns: minmax(0, 1fr); } }
.pcard {
  background: var(--card-veil);
  -webkit-backdrop-filter: blur(16px) saturate(1.15);
          backdrop-filter: blur(16px) saturate(1.15);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: clamp(20px, 2.1vw, 30px);
  display: flex;
  flex-direction: column;
  gap: 12px;
  transition: border-color var(--mcard-t) var(--mcard-ease),
              transform .5s var(--ease-out),
              box-shadow var(--mcard-t) var(--mcard-ease);
}
/* the number, where a card is one of a sequence. Faint and large, behind
   the title rather than beside it, so it reads as a marker and not as data.
   On the value cards it is a phrase rather than a number and takes the warm
   ink; the numbered cards on /newsletter stay muted. */
.pcard-n {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: .78rem;
  letter-spacing: .1em;
  /* Caps since 17 September, when the numbers on the value cards became
     phrases. The numbered cards on /newsletter are unaffected. */
  text-transform: uppercase;
  color: var(--muted);
}
/* The accent itself, at James' call on 17 September. It is pale on a light
   card — #F2C39B measures 1.6:1 on white — and a darker mix of the same hue
   was tried first and rejected. Same on the hub pages' label. */
.pcards-values .pcard-n { color: var(--warm); }

.pcard-title {
  text-wrap: balance;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(1.02rem, .95rem + .38vw, 1.24rem);
  line-height: 1.22;
  letter-spacing: -.02em;
  color: var(--ink);
  margin: 0;
}
.pcard-copy { font-size: .9rem; line-height: 1.6; color: var(--body); }
.pcard-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: auto; padding-top: 4px; }
.pcard-chips .chip { font-size: .715rem; padding: 4px 10px; }
/* a card that is a link gets the arrow the rest of the site uses */
a.pcard { text-decoration: none; }
a.pcard .arrow { display: inline-block; transition: transform .3s var(--ease-out); }
a.pcard:hover .arrow { transform: translateX(5px); }

/* ==========================================================================
   BIG STATEMENT — three or four short lines, set large, no list

   For the one moment per page where the point is the sentence rather than
   the detail. Deliberately rare: it stops working the second there are two
   on a page.
   ========================================================================== */
.statement {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(1.5rem, 1rem + 2.2vw, 2.6rem);
  line-height: 1.14;
  letter-spacing: -.032em;
  color: var(--ink);
  max-width: 22ch;
}
.statement-wide { max-width: 34ch; }
.statement + .statement-note {
  margin-top: clamp(16px, 1.8vw, 24px);
  font-size: .95rem;
  line-height: 1.65;
  color: var(--body);
  max-width: 56ch;
}
/* copy in two columns beside a statement, so a long explanation does not
   become one 100ch line on a wide screen */
/* `2 17rem` is a ceiling and a floor: at most two columns, and never one
   narrower than 272px. Inside a .split column at 1440 that resolves to a
   single column, which is what it should have been — two columns of 215px
   is a newspaper, not a paragraph. */
.cols-2 {
  columns: 2 17rem;
  column-gap: clamp(24px, 3vw, 56px);
  font-size: .95rem;
  line-height: 1.65;
  color: var(--body);
}
.cols-2 p { break-inside: avoid; margin-bottom: 14px; }
@media (max-width: 760px) { .cols-2 { columns: 1; } }

/* ==========================================================================
   SECTION HEADS, LEFT

   .sec-head centres, which is right on the homepage where the content under
   it is centred too. On the inner pages it sits over a left-aligned grid of
   cards, and a centred title over a left-aligned grid is the thing that
   made those pages read as a stack of unrelated blocks.
   ========================================================================== */
.sec-head-left { text-align: left; margin-inline: 0; }
.sec-head-left .h2 { margin-inline: 0; }
.sec-head-left .sec-sub { max-width: 62ch; }

/* ==========================================================================
   THE GAP BETWEEN TWO SECTIONS

   Two .section siblings each carry up to 116px of block padding, so the band
   between them was up to 232px of nothing — which on the inner pages was
   most of what made them feel empty. The second one drops its top padding
   and the first one's bottom padding does the separating.

   Exceptions, all of them surfaces rather than plain sections: a navy band,
   the white testimonial band and the logo rail all need their own padding on
   both sides, and the close pulls itself up with a negative margin.
   ========================================================================== */
/* No collapse. Every section carries its own padding, the way the homepage
   does — that space is the thing James likes about it, and removing it here
   is what made these pages feel crowded. It was also reaching the homepage,
   since the selector never excluded it. */


/* ==========================================================================
   FULL-BLEED NAVY BAND
   One per page at most. It is the loudest thing available and stops working
   the moment there are two.
   ========================================================================== */
.band { background: var(--navy); color: var(--on-navy); }
.band .h2, .band h2 { color: var(--paper); }
.band .fit li { color: var(--on-navy); }
.band .fit li b { color: var(--paper); }
.band .lede { color: var(--on-navy); }

/* ==========================================================================
   FIT STATEMENTS — who this is for, and who it is not

   Two sections back to back, one on paper and one on navy. The cut between
   them is the design: no columns, no ticks and crosses, no cards. Each line
   leads with the phrase that carries it, set in the heading face, and the
   rest follows in body.
   ========================================================================== */
.fit { display: flex; flex-direction: column; gap: clamp(20px, 2.2vw, 32px); max-width: 62ch; margin: 0; }
.fit li {
  font-size: clamp(1.02rem, .96rem + .34vw, 1.2rem);
  line-height: 1.55;
  color: var(--body);
}
.fit li b {
  color: var(--ink);
  font-family: var(--font-head);
  font-weight: 700;
  letter-spacing: -.016em;
}

/* ==========================================================================
   PULL QUOTE — a client, set large, with nothing around it

   Used where a page needs proof and has none of its own to show yet. A real
   quotation from a real person beats a card full of [square brackets], which
   is what was there before.
   ========================================================================== */
.pull { max-width: 34ch; margin: 0; }
.pull p {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(1.45rem, 1rem + 2vw, 2.4rem);
  line-height: 1.18;
  letter-spacing: -.032em;
  color: var(--ink);
  font-style: normal;
}
.pull .quote-by { margin-top: clamp(18px, 2vw, 28px); }
.pull-aside { display: flex; flex-direction: column; gap: 14px; align-self: end; }
/* the quote leads at two thirds, the note follows. Not a 50/50. */
.g-quote { display: grid; grid-template-columns: minmax(0, 8fr) minmax(0, 4fr); gap: clamp(28px, 4vw, 76px); align-items: end; }
@media (max-width: 900px) { .g-quote { grid-template-columns: minmax(0, 1fr); } .pull-aside { align-self: start; } }

/* ==========================================================================
   THE REVOPS PUZZLE — /puzzle

   A 3x3 sliding puzzle. Eight tiles and a gap on a navy board, a timer and a
   move count beside it, and a win dialog. Added 10 September. Everything
   here is scoped to .pz-* so none of it can reach another page, and its dark
   rules live with it rather than in the dark block, for the same reason.

   How a tile is drawn: every tile carries the WHOLE picture as its
   background, sized to three tiles across (300%), and shows the third that
   belongs to its home cell via background-position (0 / 50% / 100%). Where
   it sits on the board is a separate thing, set with --r and --c. The gap
   between tiles is not part of the picture, so when --pz-gap animates to
   zero on a win the eight pieces close up into one seamless image.
   ========================================================================== */

/* Registered so it can transition. Without @property the gap would snap
   shut on a win instead of closing; everything else still works. */
@property --pz-gap {
  syntax: '<length>';
  inherits: true;
  initial-value: 8px;
}

/* room under the header's lede before the game starts, asked for 16 September */
.pz-section { padding-top: clamp(32px, 4vw, 60px); }

/* The board and its panel are one group, centred on the page: the panel
   column is sized to its content rather than taking the rest of the row,
   which left the pair hard against the left edge with a third of the
   screen empty beside it. */
.pz {
  display: grid;
  grid-template-columns: minmax(0, 540px) auto;
  justify-content: center;
  gap: clamp(28px, 5vw, 80px);
  align-items: center;
}

/* ---------- the board ---------- */
.pz-board {
  --pz-gap: 8px;
  --pz-pad: clamp(8px, 1.2vw, 12px);
  position: relative;
  width: 100%;
  aspect-ratio: 1 / 1;
  padding: var(--pz-pad);
  border-radius: var(--radius-card);
  background: var(--navy);
  box-shadow: 0 30px 60px -34px rgba(28, 38, 53, .55);
  transition: --pz-gap .5s var(--ease-out), box-shadow .5s var(--ease);
  user-select: none;
  -webkit-user-select: none;
  touch-action: manipulation;
}
.pz-grid { position: relative; width: 100%; height: 100%; }

/* the nine empty slots, faintly, so the gap reads as a hole in a tray */
.pz-slot {
  position: absolute;
  width: calc((100% - 2 * var(--pz-gap)) / 3);
  height: calc((100% - 2 * var(--pz-gap)) / 3);
  transform: translate(calc(var(--c) * (100% + var(--pz-gap))), calc(var(--r) * (100% + var(--pz-gap))));
  border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, .06);
  box-shadow: inset 0 2px 6px rgba(0, 0, 0, .25);
}

.pz-tile {
  position: absolute;
  top: 0;
  left: 0;
  width: calc((100% - 2 * var(--pz-gap)) / 3);
  height: calc((100% - 2 * var(--pz-gap)) / 3);
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background-color: var(--mist);
  background-image: var(--pz-pic);
  background-size: 300% 300%;
  background-repeat: no-repeat;
  transform: translate(calc(var(--c) * (100% + var(--pz-gap))), calc(var(--r) * (100% + var(--pz-gap))));
  transition: transform .16s var(--ease), box-shadow .2s var(--ease), border-radius .5s var(--ease);
  box-shadow: 0 6px 14px -8px rgba(0, 0, 0, .55);
  cursor: default;
  -webkit-tap-highlight-color: transparent;
}
.pz-tile.is-movable { cursor: pointer; }
.pz-tile.is-movable:hover { box-shadow: 0 0 0 3px var(--warm), 0 10px 18px -8px rgba(0, 0, 0, .55); }
.pz-tile:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--warm), 0 0 0 6px rgba(242, 195, 155, .35); }

/* the ninth piece: only ever shown once the other eight are home */
.pz-tile.is-last { opacity: 0; pointer-events: none; transition: opacity .5s var(--ease) .15s, border-radius .5s var(--ease); }

/* the shuffle, when the clock starts: tiles take the long way round */
.pz-board.is-shuffling .pz-tile { transition-duration: .55s; transition-timing-function: var(--ease-out); }

/* ---------- solved ---------- */
/* no warm ring round the finished picture: removed 16 September, asked for */
.pz-board.is-solved { --pz-gap: 0px; box-shadow: 0 30px 60px -30px rgba(28, 38, 53, .6); }
.pz-board.is-solved .pz-tile { border-radius: 0; box-shadow: none; cursor: default; }
.pz-board.is-solved .pz-tile.is-last { opacity: 1; }
.pz-board.is-solved .pz-slot { opacity: 0; }

/* ---------- the start cover ---------- */
.pz-cover {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: grid;
  place-items: center;
  align-content: center;
  gap: 14px;
  padding: 24px;
  border-radius: var(--radius-card);
  background: rgba(28, 38, 53, .52);
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
  text-align: center;
  transition: opacity .3s var(--ease), visibility .3s;
}
.pz-cover.is-off { opacity: 0; visibility: hidden; pointer-events: none; }
.pz-cover p { color: #FAFAF8; font-weight: 700; font-size: .92rem; text-shadow: 0 1px 8px rgba(0, 0, 0, .35); }
/* on the navy veil, the start button is paper; hover lands on warm like every button */
.pz-cover .btn { background: #FAFAF8; color: #304157; }
.pz-cover .btn:hover { background: var(--warm); color: #304157; }

/* ---------- the side panel ---------- */
.pz-panel { display: flex; flex-direction: column; gap: clamp(22px, 2.4vw, 32px); align-items: flex-start; }

.pz-stats {
  display: grid;
  grid-template-columns: repeat(3, auto);
  gap: 6px clamp(22px, 3vw, 44px);
  margin: 0;
}
.pz-stats div { display: flex; flex-direction: column; gap: 2px; }
.pz-stats dt {
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}
.pz-stats dd {
  margin: 0;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(1.4rem, 1.1rem + 1vw, 2rem);
  line-height: 1.1;
  letter-spacing: -.02em;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
/* the clock is the point of the game, so it picks up a warm underline while
   it runs. An underline rather than a marker behind the figures: in dark the
   figures are near-white, and near-white on warm cannot be read. */
.pz-stats .pz-time { min-width: 4.6ch; }
.pz.is-playing .pz-stats .pz-time {
  text-decoration: underline;
  text-decoration-color: var(--warm);
  text-decoration-thickness: .14em;
  text-underline-offset: .14em;
}

.pz-preview { display: flex; align-items: center; gap: 16px; }
.pz-preview img {
  width: clamp(88px, 9vw, 116px);
  height: auto;
  aspect-ratio: 1 / 1;
  border-radius: var(--radius-sm);
  box-shadow: 0 10px 22px -14px rgba(28, 38, 53, .6);
}
.pz-preview .label { font-size: .95rem; }
.pz-preview .small { color: var(--muted); }

.pz-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 14px 22px; }
.pz-actions .text-link { align-self: center; background: none; border: 0; padding: 0; cursor: pointer; }
.pz-help { color: var(--muted); max-width: 40ch; }
/* the instructions read with the stats above them, so they sit closer to
   those than the panel's gap would put them */
.pz-stats + .pz-help { margin-top: calc(-1 * clamp(8px, 1vw, 14px)); }

/* ---------- the win dialog ---------- */
.pz-win {
  width: min(460px, calc(100vw - 32px));
  max-width: none;
  padding: 0;
  border: 0;
  background: transparent;
  overflow: visible;
}
.pz-win::backdrop { background: rgba(28, 38, 53, .55); -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px); }
.pz-win:focus { outline: none; }
/* The card, not the dialog, carries the surface and the pop. A transform on
   the dialog itself would make it the containing block for the confetti
   canvas inside it, and the confetti would fall inside the card instead of
   down the whole screen. */
.pz-win-card {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  padding: clamp(34px, 5vw, 44px) clamp(22px, 4vw, 36px) clamp(26px, 4vw, 34px);
  border-radius: var(--radius-card);
  background: var(--surface);
  color: var(--body);
  box-shadow: 0 40px 80px -30px rgba(28, 38, 53, .6);
  text-align: center;
}
.pz-win[open] .pz-win-card { animation: pz-pop .55s cubic-bezier(.34, 1.56, .64, 1); }
@keyframes pz-pop {
  from { transform: scale(.7) rotate(-3deg); opacity: 0; }
  to   { transform: none; opacity: 1; }
}
/* the burst behind the headline: warm, a little too much, on purpose */
.pz-burst {
  width: 84px;
  height: 84px;
  margin-top: -76px;
  margin-bottom: 2px;
  filter: drop-shadow(0 10px 16px rgba(28, 38, 53, .3));
}
/* only the rays turn; a turning tick reads as a clock hand */
.pz-burst-rays { transform-origin: 50px 50px; animation: pz-spin 9s linear infinite; }
@keyframes pz-spin { to { transform: rotate(360deg); } }
.pz-win h2 { font-size: clamp(1.9rem, 1.5rem + 1.6vw, 2.4rem); }
.pz-win p { max-width: 32ch; font-size: 1.05rem; }
.pz-win-stats {
  display: flex;
  gap: 10px 26px;
  margin: 6px 0 2px;
  padding: 12px 22px;
  border-radius: var(--radius-btn);
  background: var(--surface-alt);
  font-family: var(--font-head);
  font-weight: 700;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.pz-win-stats span { display: flex; flex-direction: column; font-size: 1.35rem; line-height: 1.15; }
.pz-win-stats small { font-family: var(--font-body); font-size: .7rem; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
.pz-record {
  padding: 5px 12px;
  border-radius: var(--radius-sm);
  background: var(--warm);
  color: #304157;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: .85rem;
}
.pz-win-actions { display: flex; flex-direction: column; align-items: center; margin-top: 8px; }
/* the line before the call: sets the second button apart from Play again */
.pz-win .pz-win-ask {
  max-width: 34ch;
  margin: 26px 0 12px;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 1rem;
  color: var(--ink);
}
/* Play again takes focus when the dialog opens, and the ring it drew read as
   a second navy border round a navy button. Removed 16 September, asked for. */
.pz-win .btn-primary:focus-visible { outline: none; }
/* the second button is an outline, so the pair reads as one choice, not two
   equal shouts */
.pz-win .btn-outline { background: transparent; color: var(--ink-strong); border-color: var(--ink-strong); }
.pz-win .btn-outline:hover { background: var(--warm); border-color: var(--warm); color: #304157; }
.pz-close {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--muted);
  cursor: pointer;
}
.pz-close:hover { color: var(--ink-strong); background: var(--surface-alt); }
.pz-close svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; }

.pz-confetti { position: fixed; inset: 0; width: 100vw; height: 100vh; pointer-events: none; z-index: 0; }

/* ---------- narrow screens ---------- */
@media (max-width: 900px) {
  /* One column. The panel dissolves (display: contents) so its parts can be
     ordered around the board: the clock and moves go ABOVE it, so they stay
     on screen while a thumb is on the tiles, and the picture, buttons and
     help go below. */
  .pz { display: flex; flex-direction: column; gap: 20px; align-items: stretch; }
  .pz-panel { display: contents; }
  .pz-stats { order: -2; }
  .pz-help { order: -1; }
  .pz-stats + .pz-help { margin-top: -8px; }
  .pz-board { max-width: 540px; margin-inline: auto; }
  /* the picture and the link under it centre on the screen */
  .pz-preview { order: 1; align-self: center; }
  .pz-actions { order: 2; justify-content: center; }
  .pz-preview img { width: 72px; }
}
@media (max-width: 560px) {
  .pz-board { --pz-gap: 5px; }
  .pz-actions .btn { width: auto; }
  .pz-win-actions { align-items: stretch; }
  .pz-win .btn { width: 100%; }
}

/* ---------- dark ----------
   The board is a navy chip; on a navy page it would disappear, so it steps
   down to the deep navy and picks up an edge. */
:root[data-theme="dark"] .pz-board { background: #1C2635; box-shadow: 0 0 0 1px rgba(255, 255, 255, .14), 0 30px 60px -30px rgba(0, 0, 0, .6); }
:root[data-theme="dark"] .pz-board.is-solved { box-shadow: 0 0 0 1px rgba(255, 255, 255, .14), 0 30px 60px -30px rgba(0, 0, 0, .6); }
:root[data-theme="dark"] .pz-win-card { background: #3A4E68; }
:root[data-theme="dark"] .pz-win-stats { background: rgba(255, 255, 255, .08); }
:root[data-theme="dark"] .pz-win .btn-primary { background: #F2F6FA; color: #304157; }
:root[data-theme="dark"] .pz-win .btn-primary:hover { background: var(--warm); color: #304157; }
:root[data-theme="dark"] .pz-actions .btn-primary { background: #F2F6FA; color: #304157; }

/* .btn sets display:inline-flex, which beats the browser's own [hidden] */
.pz [hidden], .pz-win [hidden] { display: none !important; }
.pz-stats dd.is-empty { font-size: clamp(1rem, .9rem + .4vw, 1.2rem); color: var(--muted); padding-top: .35em; }

/* ==========================================================================
   THE REVOPS RUN — /hop

   A side-scrolling runner on a navy board, with the same furniture as the
   puzzle: the stats row, the start cover and the end-of-run dialog. Added
   10 September. Scoped to .hp-* for the same reason the puzzle is scoped to
   .pz-*, and its dark rules live with it.

   The game is one canvas and draws its own world in fixed colours that sit
   on either navy, so the only thing the theme changes is the board itself.
   ========================================================================== */

.hp-section { padding-top: clamp(32px, 4vw, 60px); }

.hp {
  display: flex;
  flex-direction: column;
  gap: clamp(18px, 2vw, 26px);
  max-width: 1080px;
  margin-inline: auto;
}

/* the stats, then the instructions under them, then the board */
.hp-top {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
}

/* the same stats as the puzzle, word for word in the CSS */
.hp-stats {
  display: grid;
  grid-template-columns: repeat(3, auto);
  gap: 6px clamp(22px, 3vw, 44px);
  margin: 0;
}
.hp-stats div { display: flex; flex-direction: column; gap: 2px; }
.hp-stats dt {
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}
.hp-stats dd {
  margin: 0;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(1.4rem, 1.1rem + 1vw, 2rem);
  line-height: 1.1;
  letter-spacing: -.02em;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.hp-stats .hp-score { min-width: 3.5ch; }
.hp.is-playing .hp-stats .hp-score {
  text-decoration: underline;
  text-decoration-color: var(--warm);
  text-decoration-thickness: .14em;
  text-underline-offset: .14em;
}
.hp-stats dd.is-empty { font-size: clamp(1rem, .9rem + .4vw, 1.2rem); color: var(--muted); padding-top: .35em; }
/* every hundred points the score blinks, the way the original does */
.hp-stats dd.is-flash { animation: hp-blink .9s steps(1) 1; }
@keyframes hp-blink {
  0%, 34%, 67% { opacity: 1; }
  17%, 50%, 84% { opacity: .15; }
}

.hp-help { color: var(--muted); max-width: 46ch; }
.hp-help-touch { display: none; }

/* ---------- the board ---------- */
.hp-stage {
  position: relative;
  width: 100%;
  aspect-ratio: 3 / 1;
  border-radius: var(--radius-card);
  background: var(--navy);
  box-shadow: 0 30px 60px -34px rgba(28, 38, 53, .55);
  overflow: hidden;
  outline: none;
  user-select: none;
  -webkit-user-select: none;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}
.hp-stage:focus-visible { box-shadow: 0 0 0 3px var(--warm), 0 30px 60px -34px rgba(28, 38, 53, .55); }
/* mid-run a swipe on the board is a jump, never a page scroll */
.hp.is-playing .hp-stage { touch-action: none; cursor: pointer; }
.hp-canvas { display: block; width: 100%; height: 100%; }

.hp-stage.is-hit { animation: hp-shake .4s var(--ease); }
@keyframes hp-shake {
  0%, 100% { transform: none; }
  20% { transform: translateX(-7px); }
  40% { transform: translateX(6px); }
  60% { transform: translateX(-4px); }
  80% { transform: translateX(2px); }
}

/* ---------- the start cover, the puzzle's ---------- */
.hp-cover {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: grid;
  place-items: center;
  align-content: center;
  gap: 14px;
  padding: 24px;
  background: rgba(28, 38, 53, .52);
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
  text-align: center;
  transition: opacity .3s var(--ease), visibility .3s;
}
.hp-cover.is-off { opacity: 0; visibility: hidden; pointer-events: none; }
.hp-cover p { color: #FAFAF8; font-weight: 700; font-size: .92rem; text-shadow: 0 1px 8px rgba(0, 0, 0, .35); }
.hp-cover .btn { background: #FAFAF8; color: #304157; }
.hp-cover .btn:hover { background: var(--warm); color: #304157; }

/* ---------- touch pads ----------
   Only where there is no keyboard to speak of. Duck on the left, Jump on
   the right, under the thumb that does most of the work. */
.hp-pads { display: none; }
.hp-pad {
  height: 64px;
  border: 0;
  border-radius: var(--radius-card);
  background: var(--surface-alt);
  box-shadow: inset 0 0 0 1.5px rgba(107, 122, 147, .3);
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 1.05rem;
  color: var(--ink-strong);
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
  transition: background .12s var(--ease), transform .12s var(--ease);
}
.hp-pad.is-down { background: var(--warm); color: #304157; transform: scale(.98); }

@media (hover: none) and (pointer: coarse) {
  .hp-pads { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
  .hp-help-keys { display: none; }
  .hp-help-touch { display: inline; }
}

/* ---------- the end-of-run dialog, the puzzle's win dialog ---------- */
.hp-over {
  width: min(480px, calc(100vw - 32px));
  max-width: none;
  padding: 0;
  border: 0;
  background: transparent;
  overflow: visible;
}
.hp-over::backdrop { background: rgba(28, 38, 53, .55); -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px); }
.hp-over:focus { outline: none; }
.hp-over-card {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  padding: clamp(34px, 5vw, 44px) clamp(22px, 4vw, 36px) clamp(26px, 4vw, 34px);
  border-radius: var(--radius-card);
  background: var(--surface);
  color: var(--body);
  box-shadow: 0 40px 80px -30px rgba(28, 38, 53, .6);
  text-align: center;
}
.hp-over[open] .hp-over-card { animation: hp-pop .55s cubic-bezier(.34, 1.56, .64, 1); }
@keyframes hp-pop {
  from { transform: scale(.7) rotate(-3deg); opacity: 0; }
  to   { transform: none; opacity: 1; }
}
.hp-burst {
  width: 84px;
  height: 84px;
  margin-top: -76px;
  margin-bottom: 2px;
  filter: drop-shadow(0 10px 16px rgba(28, 38, 53, .3));
}
.hp-burst-rays { transform-origin: 50px 50px; animation: hp-spin 9s linear infinite; }
@keyframes hp-spin { to { transform: rotate(360deg); } }
.hp-over h2 { font-size: clamp(1.8rem, 1.45rem + 1.5vw, 2.3rem); }
.hp-over p { max-width: 34ch; font-size: 1.05rem; }
.hp-over-stats {
  display: flex;
  gap: 10px 26px;
  margin: 6px 0 2px;
  padding: 12px 22px;
  border-radius: var(--radius-btn);
  background: var(--surface-alt);
  font-family: var(--font-head);
  font-weight: 700;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.hp-over-stats span { display: flex; flex-direction: column; font-size: 1.35rem; line-height: 1.15; }
.hp-over-stats small { font-family: var(--font-body); font-size: .7rem; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
.hp-record {
  padding: 5px 12px;
  border-radius: var(--radius-sm);
  background: var(--warm);
  color: #304157;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: .85rem;
}
.hp-over-actions { display: flex; flex-direction: column; align-items: center; margin-top: 8px; }
.hp-over .hp-over-ask {
  max-width: 34ch;
  margin: 26px 0 12px;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 1rem;
  color: var(--ink);
}
.hp-over .btn-primary:focus-visible { outline: none; }
.hp-over .btn-outline { background: transparent; color: var(--ink-strong); border-color: var(--ink-strong); }
.hp-over .btn-outline:hover { background: var(--warm); border-color: var(--warm); color: #304157; }
.hp-close {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--muted);
  cursor: pointer;
}
.hp-close:hover { color: var(--ink-strong); background: var(--surface-alt); }
.hp-close svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; }

.hp-confetti { position: fixed; inset: 0; width: 100vw; height: 100vh; pointer-events: none; z-index: 0; }

/* ---------- narrow screens ---------- */
@media (max-width: 700px) {
  /* a 3:1 board on a phone is a letterbox; 2:1 gives the rabbit room */
  .hp-stage { aspect-ratio: 2 / 1; }
}
@media (max-width: 560px) {
  .hp-over-actions { align-items: stretch; }
  .hp-over .btn { width: 100%; }
  .hp-over-stats { gap: 10px 18px; padding: 12px 16px; }
}

/* ---------- dark ----------
   As with the puzzle board: a navy chip on a navy page steps down to the
   deep navy and picks up an edge. */
:root[data-theme="dark"] .hp-stage { background: #1C2635; box-shadow: 0 0 0 1px rgba(255, 255, 255, .14), 0 30px 60px -30px rgba(0, 0, 0, .6); }
:root[data-theme="dark"] .hp-stage:focus-visible { box-shadow: 0 0 0 3px var(--warm), 0 30px 60px -30px rgba(0, 0, 0, .6); }
:root[data-theme="dark"] .hp-over-card { background: #3A4E68; }
:root[data-theme="dark"] .hp-over-stats { background: rgba(255, 255, 255, .08); }
:root[data-theme="dark"] .hp-over .btn-primary { background: #F2F6FA; color: #304157; }
:root[data-theme="dark"] .hp-over .btn-primary:hover { background: var(--warm); color: #304157; }
:root[data-theme="dark"] .hp-pad { background: rgba(255, 255, 255, .08); box-shadow: inset 0 0 0 1.5px rgba(255, 255, 255, .16); }
:root[data-theme="dark"] .hp-pad.is-down { background: var(--warm); color: #304157; }

/* .btn sets display:inline-flex, which beats the browser's own [hidden] */
.hp [hidden], .hp-over [hidden] { display: none !important; }


/* ==========================================================================
   PHONE LAYOUT — asked for on 12 September

   Everything in this block is a phone-width override of something that is
   right on a wide screen and wrong on a narrow one. It sits at the end of
   the stylesheet on purpose: these have to win over the component blocks
   above, and several of them override rules that are themselves in media
   queries.
   ========================================================================== */
@media (max-width: 760px) {

  /* Section heads align left. Centred headings over left-aligned content
     is the thing that made the phone layout read as a stack of unrelated
     blocks. The hero is untouched — it uses .mat-head, not .sec-head.

     SCOPED TO .sec-head-left, NOT EVERY HEAD. The rule used to catch all of
     them, including the nine on the site that are deliberately centred
     because the thing under them is centred too — the tool clump, the
     booking widget, the audit form. Those came out left over centred
     content, which is the same mismatch this rule exists to prevent, just
     the other way round. `secHead(..., 'centred')` is what omits the class,
     so the two stay in step by construction. */
  .sec-head-left,
  .sec-head-left .h2,
  .sec-head-left .sec-sub,
  .sec-sub-left { text-align: left; margin-inline: 0; }

  /* The timing column comes out of the services list. On a phone the row is
     already one column, so the estimate was a stray line of grey between the
     service name and what it is. */
  .svc-time { display: none; }
}

/* The portrait and the badge sit in the middle of the screen once the split
   collapses, rather than hard against the left edge. */
@media (max-width: 1000px) {
  .about-stack { justify-self: center; margin-inline: auto; }
}

/* ==========================================================================
   TESTIMONIALS ON A PHONE — one at a time, with dots

   Three quotes stacked vertically was most of the length of the bottom of
   the page. They become a snap-scrolling row instead: one quote per screen,
   swipeable, with three dots under them and a ten-second auto-advance. See
   the testimonial carousel block in site.js.

   The dots are built by script and hidden without it, so with JS off this
   degrades to a row you can still swipe.
   ========================================================================== */
.testi-dots { display: none; }

@media (max-width: 760px) {
  .testi {
    display: flex;
    grid-template-columns: none;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    /* No gap. A gap puts dead space between the slide boxes, so the offset
       the dots scroll to and the position the snap picks are a gutter apart
       — press a dot and the quote lands flush against the edge again. The
       boxes are contiguous instead and each one carries its own right-hand
       padding, so the separation is inside the slide where the snap cannot
       see it. */
    gap: 0;
    scrollbar-width: none;
    -ms-overflow-style: none;
    /* NO bleed, and no padding of its own. The rail used to run to both
       screen edges on a negative margin and pad itself back, which looked
       right until it snapped: scroll-snap-align resolves against the
       SNAPPORT — the scrollport reduced by scroll-padding, which defaults to
       zero — so the slide aligned to the container's padding box and scrolled
       that 20px of padding out of view. The quote ended up flush against the
       edge of the screen.

       Sitting inside .shell instead, the left inset is the same
       clamp(20px, 5vw, 60px) every other section on the site uses, and there
       is no padding for the snap to eat. */
  }
  .testi::-webkit-scrollbar { display: none; }
  .testi-col {
    flex: 0 0 100%;
    /* start, not center. The dots scroll the rail with
       `scrollLeft = slide.offsetLeft`, which is a start-aligned position; a
       centre snap point pulls against that and the rail creeps by half a
       gutter every time a dot is pressed. */
    scroll-snap-align: start;
    /* the breathing room between two quotes, held inside the box */
    padding-right: var(--gutter);
    box-sizing: border-box;
  }

  .testi-dots {
    display: flex;
    justify-content: center;
    gap: 10px;
    margin-top: clamp(20px, 5vw, 30px);
  }
  .testi-dot {
    appearance: none;
    width: 8px; height: 8px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--mist);
    cursor: pointer;
    transition: background .3s var(--ease), transform .3s var(--ease);
  }
  .testi-dot[aria-current="true"] { background: var(--ink-strong); transform: scale(1.25); }
  .testi-dot:focus-visible { outline: 2px solid var(--ink-strong); outline-offset: 3px; }
}

/* ==========================================================================
   FOOTER ON A PHONE

   Reordered on 12 September. The lockup centres, the link columns centre,
   Company moves below Resources, and the address pair drops to the very
   bottom under the partner badges — where someone scrolling to the end of a
   page expects to find it.

   `display: contents` on .footer-brand is what makes that possible: its four
   children become items of .footer-top directly, so each one can take its
   own order without touching the markup. That matters because this footer is
   hand-written in index.html and generated by footer() in
   tools/build-pages.js, and a CSS-only change cannot drift between them.
   ========================================================================== */
@media (max-width: 820px) {
  .footer-top {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: clamp(26px, 6vw, 40px);
  }

  .footer-brand { display: contents; }
  .footer-mark  { order: 1; margin-inline: auto; }
  .footer-pitch { order: 2; max-width: 40ch; }
  .footer-links { order: 3; }
  .footer-badges { order: 4; }
  .footer-mail  { order: 5; }
  .footer-place { order: 6; margin-top: -8px; }

  /* Services and Resources side by side, Company across the row under them.
     The span used to be on Resources, which was the third child; the order
     swap below moves Resources up, so the span moves with Company. */
  /* One column, not two abreast. Services, then Resources, then Company —
     the DOM order is Services, Company, Resources, so only the last two
     need moving. */
  .footer-links {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: clamp(26px, 7vw, 38px);
    justify-self: center;
    max-width: 26rem;
    text-align: center;
  }
  .footer-links > div:nth-child(2) { order: 3; grid-column: auto; }  /* Company   */
  .footer-links > div:nth-child(3) { order: 2; grid-column: auto; }  /* Resources */
  .footer-links > div:nth-child(4) { order: 4; grid-column: auto; }  /* Platforms */
  .footer-links ul { align-items: center; }

  .footer-badges { justify-self: center; }
}

/* ==========================================================================
   DARK THEME

   Rebuilt 7 September, after the whole of the previous one was deleted on
   the 4th. Not a revert — none of that CSS survived — so this is a second
   attempt, and a simpler one.

   The brief: anything paper-coloured goes navy, anything navy goes white,
   and the footer and the floating navigation bar stay exactly as they are.

   It is a token swap, plus a short list of rules the token model cannot
   express. Two things make the swap possible:

     · --navy now means "a dark chip" and nothing else. The footer, the
       floating bar and the primary button paint with it, so they come out
       the same colour in both themes with no dark rule at all.
     · --ink-strong means "the strongest ink on the page ground", which is
       navy on paper and near-white on navy.

   The rules below are the exceptions: places that read a light token to
   paint ON a dark chip, and so would end up navy-on-navy; artwork whose
   colours are fixed; and the two logo sets, which are images and cannot be
   recoloured by a variable.

   `:root[data-theme="dark"]` rather than a prefers-color-scheme media
   query. The toggle in the nav is the only thing that sets it, so the theme
   is always the visitor's explicit choice and never changes under them.
   ========================================================================== */
:root[data-theme="dark"] {
  /* Grounds. Paper becomes navy, as asked. The drift that used to run
     paper -> white now runs navy -> a lifted navy, so the tool clump and
     the testimonials still read as a step up out of the page rather than
     as a slab dropped on it. */
  --paper: #304157;
  --paper-step: #35485F;
  --surface: #3A4E68;
  --surface-alt: #42587A;

  /* Ink, measured on the navy ground rather than picked by eye:
       --ink   #F2F6FA  9.58:1   headings and quote bodies
       --body  #D5DFEB  7.72:1
       --muted #B6C3D3  5.81:1 on the ground, 4.75:1 on the lifted surface
                                — the surface is the tighter of the two and
                                  is what this value is set by
       --ink-strong #FFFFFF 10.40:1  links, arrows, stars, focus rings
     Headings stop short of pure white on purpose: #FFFFFF over a whole
     paragraph on this navy glares. Full white is kept for the small, sparse
     things where it reads as emphasis rather than as a wall. */
  --ink: #F2F6FA;
  --ink-strong: #FFFFFF;
  --ink-hover: #FFFFFF;
  --body: #D5DFEB;
  --muted: #B6C3D3;
  --slate: #B3C0D1;

  /* Hairlines are alpha now rather than a fixed grey, so they sit correctly
     on both the page ground and the lifted surface above it. */
  --line: rgba(255, 255, 255, .15);
  --line-soft: rgba(255, 255, 255, .08);

  /* Small plates: chips, the email field, the slider rail. Lifted off the
     ground rather than white, so they read as recessed hardware and not as
     holes punched in the page. */
  --plate: rgba(255, 255, 255, .1);
  --plate-line: rgba(255, 255, 255, .2);
  --plate-ink: #D5DFEB;
  --pill-bg: rgba(255, 255, 255, .12);
  --track: rgba(255, 255, 255, .18);
  --track-ring: rgba(255, 255, 255, .34);

  /* the veil the hero cards sit under: white in light, a lift in dark */
  --card-veil: rgba(255, 255, 255, .16);

  /* The have/missing chips. Same hues, brought up off the dark ground —
     the light-mode tints are near-white and would blaze. */
  --ok:      #B2D4B2;
  --ok-bg:   rgba(140, 190, 140, .16);
  --ok-line: rgba(140, 190, 140, .34);
  --no:      #EAB8AE;
  --no-bg:   rgba(200, 120, 105, .16);
  --no-line: rgba(200, 120, 105, .34);
}

/* ---- the two bars that stay the same ----
   The floating nav and the footer are navy chips in both themes, so their
   own backgrounds need nothing. What they DO need is for the light tokens
   they paint with to stop resolving to navy: --paper is the page ground
   now, and a paper-coloured hamburger on a navy bar would disappear.
   Literals here, deliberately. These are on a fixed navy and must not
   follow the theme. */
:root[data-theme="dark"] .nav.is-floating .nav-cta .btn-primary {
  background: #FAFAF8;
  color: #304157;
}
:root[data-theme="dark"] .nav.is-floating .nav-toggle span,
:root[data-theme="dark"] .nav.is-floating .nav-toggle span::before,
:root[data-theme="dark"] .nav.is-floating .nav-toggle span::after { background: #FAFAF8; }
:root[data-theme="dark"] .nav.is-floating .nav-links a:hover,
:root[data-theme="dark"] .nav.is-floating .nav-links a[aria-current="page"] { color: #FAFAF8; }
:root[data-theme="dark"] .footer a:hover,
:root[data-theme="dark"] .footer-legal a:hover { color: #FAFAF8; }
/* The bar floats over a navy page now, so its own navy no longer separates
   it from the ground and its shadow is a dark shadow on a dark surface.
   One hairline of light does the work the contrast used to. */
:root[data-theme="dark"] .nav.is-floating .nav-inner {
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .42),
              0 18px 40px -22px rgba(12, 18, 26, .8);
}

/* ---- buttons ----
   .btn-primary is a navy chip with paper type. In dark, paper resolves to
   navy, so the type would vanish into the button. Inverted instead: the
   button becomes the light thing on a dark page, which is also the right
   emphasis when everything around it is dark. */
:root[data-theme="dark"] .btn-primary { background: #F2F6FA; color: #304157; }
:root[data-theme="dark"] .btn-primary:hover { background: var(--warm); color: #304157; }
:root[data-theme="dark"] .btn-light { background: rgba(255, 255, 255, .12); color: #F2F6FA; }
:root[data-theme="dark"] .btn-light:hover { background: var(--warm); color: #304157; }

/* ---- the lockup ----
   Two files, one navy and one white, cross-faded by opacity. The floating
   bar already swaps them; on a dark page the white one has to be showing at
   rest as well. */
:root[data-theme="dark"] .brand-base { opacity: 0; }
:root[data-theme="dark"] .brand-alt  { opacity: 1; }

/* ---- the two logo sets ----
   Images, so no variable can recolour them; they need a filter or nothing.

   The client marks are already greyscale artwork shown at reduced opacity.
   Left alone on navy the mid-grey ones drop to about 1.9:1 — worse than
   they are on paper. Flattened to white they all land together, and the
   Ignite mark, which is near-white artwork that has always been close to
   invisible on paper, finally reads. */
:root[data-theme="dark"] .logo-run img { filter: brightness(0) invert(1); opacity: .55; }
/* Except Ignite, which is not line art on a transparent ground: it is a
   near-white disc with the wordmark knocked out of it. Flattened to white
   the disc and the words become one blank circle. It keeps its own artwork,
   which already reads as a light mark on the navy. */
:root[data-theme="dark"] .logo-run img[data-solid] {
  filter: none;
  opacity: .92;
  mix-blend-mode: normal;
}

/* The tool marks are brand artwork with fixed colours. In the light theme
   they are greyed back at rest and come up in colour on hover; on the dark
   theme they are in colour the whole time and there is nothing behind them.

   That is James' call, 12 September, and it replaces the white-silhouette
   treatment that shipped with the theme. The silhouettes measured better —
   in colour on this navy, HubSpot's orange lands at 1.16:1 — but the plate
   that had to appear behind a mark on hover to make its colours legible was
   the paperplate he asked to be rid of, and a clump of white ghosts that
   lit up one at a time was not what the section is for. Nothing is drawn
   behind a mark in either theme now. */
:root[data-theme="dark"] .tool-mark { position: relative; }
:root[data-theme="dark"] .tool-mark img { filter: none; opacity: .92; }
:root[data-theme="dark"] .tool-mark:hover img { filter: none; opacity: 1; }

/* The client's mark inside a stage card is the marquee's artwork: flat grey
   on a transparent ground, which on navy is all but invisible. Same fix the
   marquee gets — flattened to white — and `multiply` has to go with it or
   the mark disappears into the card. */
:root[data-theme="dark"] .mcard-shot {
  filter: brightness(0) invert(1);
  mix-blend-mode: normal;
  opacity: .8;
}
:root[data-theme="dark"] .mcard:hover .mcard-shot { opacity: 1; }
/* and flattening that one to white would leave a blank white disc */
:root[data-theme="dark"] .mcard-shot[data-solid] { filter: none; opacity: .95; }
/* and a photograph is not artwork to flatten at all */
:root[data-theme="dark"] .mcard-shot.is-photo { filter: none; opacity: 1; }

/* ---- back to top ----
   A navy chip on a navy page. Same fix as the floating bar: a light edge
   instead of a contrast it no longer has. */
:root[data-theme="dark"] .to-top {
  background: rgba(255, 255, 255, .12);
  border-color: rgba(255, 255, 255, .38);
  color: #F2F6FA;
}
:root[data-theme="dark"] .to-top:hover { background: var(--warm); color: #304157; border-color: transparent; }

/* ---- the service row hover fill ----
   The fill is a literal near-white and the type on it is literal navy, both
   deliberately fixed so the pair can never invert apart. That works in dark
   too: the row lights up as a pale plate. All that needs saying is that the
   hairline under the row follows the theme, which it already does. */

/* ---- the hero slider ----
   The thumb is a navy disc carrying a white icon. Navy on navy, so it gets
   the same light edge everything else on the ground does. */
:root[data-theme="dark"] .mat-thumb {
  box-shadow: 0 0 0 3px var(--paper), inset 0 0 0 1px rgba(255, 255, 255, .3),
              0 12px 28px -12px rgba(12, 18, 26, .7);
}
:root[data-theme="dark"] .mat-thumb:hover {
  box-shadow: 0 0 0 3px var(--paper), inset 0 0 0 1px rgba(255, 255, 255, .45),
              0 16px 34px -12px rgba(12, 18, 26, .8);
}
/* the rail's gradient ends in the ground colour, which is now the ground */
:root[data-theme="dark"] .rail span {
  background: linear-gradient(90deg, rgba(198, 211, 224, .35) 0%, #FFFFFF 100%);
}
/* the email field's hover edge was a fixed grey */
:root[data-theme="dark"] .stage-form input:hover { border-color: rgba(255, 255, 255, .34); }
:root[data-theme="dark"] .stage-form input:focus { background: rgba(255, 255, 255, .16); }
:root[data-theme="dark"] .card-link:hover { border-color: rgba(255, 255, 255, .28); }

/* ---- shadows ----
   Every shadow on the page is a navy at low alpha, tuned to darken paper.
   On a dark ground a navy shadow is invisible, so the cards lose their lift
   entirely. Deepened rather than removed: the depth is part of how the
   poster cards and the service rows read. */
:root[data-theme="dark"] .mcard:hover,
:root[data-theme="dark"] .svc-row:hover,
:root[data-theme="dark"] .case-card:hover {
  box-shadow: 0 26px 52px -28px rgba(6, 10, 16, .85);
}

/* The mobile link panel is a paper card, so in dark it is a navy card on a
   navy page — the same light edge the floating bar gets.

   Scoped to the breakpoint where the panel actually exists. Unscoped, the
   inset ring painted a box around the four desktop links, which are not a
   panel at all: they are four inline links in the bar. */
/* 940 until 12 September. Seven links and a longer button need more room
   than a 940px bar has, so they tuck away sooner. */
@media (max-width: 1100px) {
  :root[data-theme="dark"] .nav-links {
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .28),
                0 20px 44px -24px rgba(6, 10, 16, .9);
  }
}

/* ---- the close, and the testimonials above it ----

   BOTH USED TO BE PINNED TO LIGHT. The close was set over a photograph and
   the testimonials sat on a white band, so in the dark theme the type in
   each had to be forced back to navy, and the sections either side had to
   drift to white to deliver the page into them without a visible edge.

   All of that went on 12 September, along with the photograph and the white
   band. Both sections are ordinary page ground in both themes now, which is
   why every rule that used to be here — four background-images and six type
   colours — is gone rather than rewritten. The tokens already say what these
   sections should look like on navy; the whole job was to stop overriding
   them.

   The one thing worth keeping is the emphasis inside a quote. `.testi .hl`
   paints no background and takes --ink for its colour, which is the right
   near-white here, so the bolded phrases still read as bolded. */


/* ==========================================================================
   SERVICES INDEX — the simplified page
   Header, the list, the filtered case rail, the close. Nothing else, and
   nothing behind any of it: this page is flat paper end to end.

   The rhythm is set once here rather than per section, so the bands are
   separated by the same distance and the page reads as one column rather
   than a stack of unrelated blocks.
   ========================================================================== */

/* No artwork column, so the grid drops to one — but the headline keeps the
   full .page-hero size rather than stepping down the way .page-hero-plain
   does. With the image gone the type is the only thing holding the top of
   the page down, and shrinking it as well left nothing there. */
.page-hero-nomedia .page-hero-inner {
  grid-template-columns: minmax(0, 1fr);
  min-height: 0;
}
/* 30px more between the nav and the title than the shared header gives. The
   base padding already clears the bar (--nav-h) plus its own beat; this adds
   to that rather than replacing it, so the bar's height stays accounted for.

   The header is two lines of type and nothing else, so the gap below it is
   the list section's to give — the header carries none of its own. */
.page-hero-nomedia {
  padding-top: calc(var(--nav-h) + clamp(26px, 3vw, 48px) + 30px);
  padding-bottom: 0;
}
.page-hero-nomedia .lede { max-width: 54ch; }

/* .svc-cta already centres the prompt and the button under the list; it
   only wants a longer beat above it here, where it is closing a page rather
   than a section. */
.svc-index .svc-cta { margin-top: clamp(40px, 4.6vw, 66px); }

@media (max-width: 700px) {
  .case-head { grid-template-columns: minmax(0, 1fr); }
}

/* ==========================================================================
   CASE FILTERS
   Five checkboxes, not five tabs: any combination is valid and all five are
   on when the page loads, so the default state shows everything and the
   control only ever takes work away.

   Real buttons with role="checkbox" rather than input + label — the tick is
   drawn, the whole chip is the target, and the checked state is one
   attribute that both the stylesheet and the script read.
   ========================================================================== */
.cs-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: clamp(24px, 2.8vw, 38px);
}
/* Navy on, plate off. The Do / Don't pair was tried here and read as a
   verdict on each service rather than as the state of a checkbox — the page
   is a menu, and nothing on it is the wrong answer. */
.cs-filter {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 9px 15px 9px 11px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--plate-line);
  background: var(--plate);
  color: var(--plate-ink);
  font-family: var(--font-body);
  font-weight: 700;
  font-size: .82rem;
  line-height: 1.2;
  cursor: pointer;
  transition: background .22s var(--ease), color .22s var(--ease),
              border-color .22s var(--ease), transform .22s var(--ease-out);
}
.cs-filter:hover { transform: translateY(-1px); border-color: var(--track-ring); }
.cs-filter:focus-visible { outline: 2px solid var(--ink-strong); outline-offset: 3px; }

/* the box: an empty square when off, filled with the tick when on */
.cs-box {
  width: 17px;
  height: 17px;
  flex: none;
  border-radius: 4px;
  border: 1.5px solid var(--plate-line);
  display: grid;
  place-items: center;
  transition: background .22s var(--ease), border-color .22s var(--ease);
}
.cs-box svg {
  width: 11px;
  height: 11px;
  fill: none;
  stroke: var(--paper);
  stroke-width: 2.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0;
  transform: scale(.6);
  transition: opacity .18s var(--ease), transform .22s var(--ease-out);
}
.cs-filter[aria-checked="true"] {
  border-color: var(--ink-strong);
  background: var(--pill-bg);
  color: var(--ink-strong);
}
.cs-filter[aria-checked="true"] .cs-box {
  background: var(--ink-strong);
  border-color: var(--ink-strong);
}
.cs-filter[aria-checked="true"] .cs-box svg { opacity: 1; transform: scale(1); }

/* The last one still on cannot be turned off — an empty rail is not a state
   worth being able to reach. It stays in its checked colours and stops
   offering the interaction rather than going grey, which would read as
   broken rather than as held. */
.cs-filter[data-locked] { cursor: default; }
.cs-filter[data-locked]:hover { transform: none; }

/* ==========================================================================
   THE CLOSE AND THE RAIL ABOVE IT

   The close pulls itself up over the section above by --start-bleed and
   paints its plate across that distance. On the homepage the thing above is
   a short centred testimonial, so 430px of bleed never reaches its content.
   A rail of 3:4 posters is a different shape: at the homepage's bleed the
   bottom third of every card sat over the plate.

   So the bleed is shorter here and the rail's bottom padding matches it
   exactly — the plate starts where the rail's content stops, and the
   artwork still fades in rather than arriving at a line. The rail keeps
   .to-white so the ground has already reached white by then.

   100px, flat, on both: the two have to be the same number or the plate
   either eats the cards or leaves a gap the artwork does not cover, so
   changing one means changing the other.
   ========================================================================== */


/* ==========================================================================
   THE SERVICE PAGE — header, quadrant, scheduler

   A leaf page reached from a list of five names. It answers four questions
   and then gets out of the way, so it is built out of three parts and no
   card grids: the header, one quadrant, one booking widget.
   ========================================================================== */

/* ---------- the eyebrow back to /services ----------
   Above the H1 rather than beside it, and set at label size so it reads as
   a position in the site rather than as a second headline. The arrow points
   backwards and slides backwards on hover, which is the .text-link
   behaviour reversed and the only reason it does not just use that class. */
.hero-back {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-head);
  font-size: .8rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
  text-decoration: none;
  transition: color .3s var(--ease-out);
}
.hero-back .arrow {
  display: inline-block;
  transition: transform .3s var(--ease-out);
}
.hero-back:hover { color: var(--ink-hover); }
.hero-back:hover .arrow { transform: translateX(-5px); }
.hero-back:focus-visible {
  outline: 2px solid var(--ink-strong);
  outline-offset: 4px;
  border-radius: 4px;
}

/* ---------- the header ----------
   .page-hero-nomedia gives it the /services header's spacing: same clearance
   under the nav, same full-width single column, no artwork. That rule ends
   at padding-bottom: 0 because the services index hands the gap to its list.
   This page cannot — the meta row is the last thing in the band and needs a
   beat under it — so the bottom comes back here.

   The H1 also gets its measure back. --hero-media-w no longer reserves a
   column, so a 20ch cap would break two-word service names oddly early. */
.page-hero-svc { padding-bottom: 0; }
.page-hero-svc .h1 { max-width: 24ch; }
.page-hero-svc .lede { max-width: 62ch; }
/* the meta row runs the width of the column here rather than stopping at
   46ch: with three facts instead of two, the third was wrapping alone */
.page-hero-svc .hero-meta { max-width: none; gap: 18px clamp(38px, 5vw, 76px); }

/* ---------- the quadrant ----------
   Four cells, two hairlines. Drawn with a gap the parent's background shows
   through rather than with borders on the cells: borders on a 2x2 grid
   double up on the inside edges and need a nth-child rule per side to
   suppress, and the doubled line is visible at these weights.

   The padding is inside the cells, so the hairlines run edge to edge of the
   shell and the text sits off them. --quad-gutter is that inset, and it is
   what makes the cross read as a division of one block rather than as space
   between four blocks. */
.quad {
  --quad-gutter: clamp(22px, 3vw, 46px);
  display: grid;
  grid-template-columns: 1fr 1fr;

  /* Pulled in from the section shell, hairlines and all.

     The shell runs to 1680px on a wide display, and half of that is a 700px
     column holding two short paragraphs — so the copy stopped at its own
     46ch measure and left a slab of empty page between the left column and
     the vertical rule. Capping the whole block at --shell instead lets the
     copy fill its cell edge to edge, which is what closes that gap: the
     measure is now set by the cell rather than by a max-width fighting it. */
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
}
.quad-cell { padding: var(--quad-gutter); }

/* Two lines, drawn as borders on three cells rather than as a grid gap over
   a coloured parent: the ground under this section drifts from paper to
   white down the page, so anything that paints its own background behind
   the cells shows up as a rectangle. Borders inherit nothing and follow
   --line into the dark theme on their own. */
.quad-cell:nth-child(1) { border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.quad-cell:nth-child(2) { border-bottom: 1px solid var(--line); }
.quad-cell:nth-child(3) { border-right: 1px solid var(--line); }
/* the outer edges carry no line, so the padding on the four outside faces
   is doing nothing but pushing the type off the shell margin */
.quad-cell:nth-child(odd)  { padding-left: 0; }
.quad-cell:nth-child(even) { padding-right: 0; }
.quad-cell:nth-child(1),
.quad-cell:nth-child(2)    { padding-top: 0; }
.quad-cell:nth-child(3),
.quad-cell:nth-child(4)    { padding-bottom: 0; }

.quad-title {
  margin: 0 0 clamp(12px, 1.3vw, 18px);
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(1.15rem, 1rem + .55vw, 1.5rem);
  letter-spacing: -.024em;
  line-height: 1.15;
  color: var(--ink);
}
.quad-copy {
  margin: 0 0 .85em;
  /* no max-width: the cell is the measure now. See the note on .quad. */
  color: var(--body);
  font-size: clamp(.95rem, .92rem + .12vw, 1.02rem);
  line-height: 1.62;
}
.quad-copy:last-child { margin-bottom: 0; }

/* one hairline, one column, on a phone: a 2x2 grid at that width gives each
   cell about 20 characters, and the vertical rule stops meaning anything */
@media (max-width: 720px) {
  .quad { grid-template-columns: minmax(0, 1fr); }

  /* `.quad >` AND NOT `.quad-cell`. A media query adds no specificity, so a
     bare `.quad-cell` here (0,1,0) lost to `.quad-cell:nth-child(3)` (0,2,0)
     above it — which left cells 1 and 3 holding the vertical rule they were
     given for the 2x2, as an orphan hairline down the right of two of the
     four stacked cells. The zeroed paddings meant for the grid's outer faces
     survived the same way, so cell 2 had no space above it and cell 3 none
     below. This selector ties on specificity and wins on order. */
  .quad > .quad-cell {
    padding: clamp(24px, 6vw, 34px) 0;
    border-right: 0;
    border-bottom: 1px solid var(--line);
  }
  .quad > .quad-cell:first-child { padding-top: 0; }
  .quad > .quad-cell:last-child { padding-bottom: 0; border-bottom: 0; }
}

/* ---------- the scheduler, and /audit's form ----------
   The head is centred because the thing under it is, and these are the few
   places on the site where that is true. Both a HubSpot widget and a
   HubSpot form paint their own frame, so they want a longer beat under the
   head than a card grid does — a tight gap reads as the head sitting on top
   of the iframe. Shared by both, which is also what keeps the two heads
   identical rather than merely similar. */
.svc-book .sec-head { margin-bottom: clamp(26px, 3vw, 42px); }

/* ==========================================================================
   CASE STUDIES INDEX — the header plate and the filterable shelf

   The page is two objects: a gradient plate carrying the title, and a
   20 / 80 split with the filter column on the left and a grid of the same
   poster cards the homepage uses on the right. The rail, the problem cards
   and the testimonials that used to sit here are gone — this is the page
   you arrive at to FIND a case study, and everything else was in the way.
   ========================================================================== */

/* ---------- the header plate ----------

   100px below the bar, flat, no negative margin: unlike .page-hero this
   header does not run up behind the nav, because the plate has a hard top
   edge and the transparent bar's navy links would sit on the artwork.

   No border, per the brief. The plate's own edge is the radius and the
   contrast step against paper. */
.cs-head {
  padding-top: 74px;
  padding-bottom: 0;
}

/* THE SCRIM IS NOT DECORATION — it is what makes the title legible.

   The artwork is a light gradient that darkens towards the bottom left and
   towards the middle, and navy type measures 2.5:1 on the worst of it and
   8:1 on the white corner — so the crop alone cannot be trusted. `center
   20%` keeps the light band in view, and the wash sits under the middle
   third where the centred title lands, carrying the worst pixel under it to
   5.9:1 at 335px wide and 8.5:1 at 1112px. It clears by 78% of the width,
   so the warm right-hand edge and the white left one are both untouched.

   Re-measure if the artwork, the crop or the title's alignment changes.
   tools/ has no checker for this; it was measured in Pillow. */
.cs-head-panel {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  min-height: clamp(178px, 20vw, 268px);
  padding: clamp(26px, 3.4vw, 54px);
  border-radius: var(--radius-card);
  border: 0;
  overflow: hidden;
  background-color: var(--mist);
  background-image:
    radial-gradient(ellipse 72% 102% at 50% 50%,
      rgba(250, 250, 248, .74) 0%,
      rgba(250, 250, 248, .34) 55%,
      rgba(250, 250, 248, 0) 100%),
    url("../img/start-panel-bg.webp");
  background-size: auto, cover;
  background-position: center, center 20%;
  background-repeat: no-repeat, no-repeat;
}
/* Navy in both themes. The plate brings its own light ground with it, so it
   does not follow the page when the page goes dark — the same reason the
   close panel's headline is pinned to #304157 further up this file. */
.cs-head-panel .h1 {
  position: relative;
  margin: 0;
  color: #304157;
  /* the same ramp .page-hero .h1 uses, so "Case Studies" here and "Services"
     on /services are the same size at every width */
  font-size: clamp(2.3rem, 1.2rem + 3.4vw, 3.7rem);
  line-height: 1;
  letter-spacing: -.04em;
}

/* ---------- the split ----------
   20 / 80 exactly, as asked. The gap comes out of the right column, so the
   filters stay at a fifth of the shell at every width above the breakpoint. */
.cs-layout {
  display: grid;
  grid-template-columns: 20% minmax(0, 1fr);
  gap: clamp(22px, 2.8vw, 44px);
  align-items: start;
}

/* The column travels with the grid. Sticky rather than fixed so it stops at
   the section's own end, and top clears the floating bar plus its gap. */
.cs-side {
  position: sticky;
  top: calc(var(--nav-float-gap) + 74px);
  max-height: calc(100vh - var(--nav-float-gap) - 96px);
  overflow-y: auto;
  scrollbar-width: thin;
  padding-right: 2px;
}
.cs-side-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--line);
}
.cs-side-title {
  margin: 0;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: .78rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink);
}
/* A text button, not a chip: it is an escape hatch rather than a sixth
   filter, and a chip here reads as one more thing to switch on. */
.cs-clear {
  border: 0;
  background: none;
  padding: 0;
  font-family: var(--font-body);
  font-size: .76rem;
  font-weight: 700;
  color: var(--muted);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: transparent;
  transition: color .25s var(--ease), text-decoration-color .25s var(--ease);
}
.cs-clear:hover { color: var(--ink-strong); text-decoration-color: currentColor; }
.cs-clear:focus-visible { outline: 2px solid var(--ink-strong); outline-offset: 3px; border-radius: 3px; }
/* nothing is on but the five services, so there is nothing to clear */
.cs-clear[data-idle] { opacity: .45; pointer-events: none; }

.cs-group { padding-top: clamp(16px, 1.8vw, 22px); }
.cs-group-title {
  margin: 0 0 9px;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: .82rem;
  letter-spacing: -.01em;
  color: var(--ink);
}
.cs-opts { display: flex; flex-direction: column; gap: 2px; }

/* A row, not a chip. At a fifth of the shell the chips from /services wrap
   into a ragged block that is hard to read down; a stacked row keeps the
   boxes in one column and lets a long service name wrap under itself. */
.cs-opt {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  width: 100%;
  padding: 6px 8px 6px 6px;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--body);
  font-family: var(--font-body);
  font-size: .82rem;
  font-weight: 400;
  line-height: 1.35;
  text-align: left;
  cursor: pointer;
  transition: background .2s var(--ease), color .2s var(--ease);
}
.cs-opt:hover { background: var(--pill-bg); color: var(--ink); }
.cs-opt:focus-visible { outline: 2px solid var(--ink-strong); outline-offset: 1px; }
.cs-opt .cs-box { margin-top: 1px; }
.cs-opt[aria-checked="true"] { color: var(--ink); font-weight: 700; }
.cs-opt[aria-checked="true"] .cs-box {
  background: var(--ink-strong);
  border-color: var(--ink-strong);
}
.cs-opt[aria-checked="true"] .cs-box svg { opacity: 1; transform: scale(1); }
/* the last service still on is held — an empty shelf is not a state worth
   being able to reach, and the same rule runs the rail on /services */
.cs-opt[data-locked] { cursor: default; }
.cs-opt[data-locked]:hover { background: none; }

/* ---------- the grid ----------
   Three fixed tracks, not auto-fill. auto-fill was letting a wide display
   put four and five posters across the 80% column, and at that width the
   cards are smaller than the thing they are advertising. Three is the cap,
   and the cards take whatever width that leaves them. */
.cs-results { min-width: 0; }
.cs-count {
  margin: 0 0 clamp(14px, 1.6vw, 20px);
  font-size: .8rem;
  color: var(--muted);
}
.cs-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(14px, 1.8vw, 24px);
}
/* the rail sizes its own cards; in a grid the track does it */
.cs-grid .case-card { width: auto; flex: none; }
.cs-grid .case-card[hidden] { display: none; }

/* Two, then one. The breakpoints are the card's, not the page's: a 3:4
   poster stops being readable somewhere under 200px wide, and three tracks
   reach that before the layout itself changes shape. */
@media (max-width: 860px) {
  .cs-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 540px) {
  .cs-grid { grid-template-columns: minmax(0, 1fr); }
}

/* Nothing stands in for an empty grid. The count line above it already says
   "Showing 0 of 5", and Clear filters is eight inches up the same screen —
   a panel here repeated both and pushed the filters off the fold. */

/* The close bleeds up over whatever is above it. Same pairing as the
   services index: the bleed and the section's bottom padding are the same
   number, so the plate starts exactly where the last row of cards stops.
   Change one and change the other. */
.cs-index .start-section { --start-bleed: 100px; }
.cs-index .cs-section { padding-bottom: 100px; }

@media (max-width: 1000px) {
  /* The column stops being a fifth of anything useful below this. It goes
     full width above the grid and the options lay out as wrapping chips
     again, which is the shape they had room for on /services. */
  .cs-layout { grid-template-columns: minmax(0, 1fr); }
  .cs-side {
    position: static;
    max-height: none;
    overflow: visible;
    padding-bottom: 4px;
    border-bottom: 1px solid var(--line);
  }
  /* ...except under the filters. Stacked above the grid on a narrow screen,
     the chips already read as their own block, and a rule under them draws
     a line across the page before the visitor has seen a single card. The
     meta column on a case study page keeps its rule: there the line is what
     separates the facts from the story beside them. */
  .cs-side[data-cs-filters] { border-bottom: 0; padding-bottom: 0; }
  .cs-opts { flex-direction: row; flex-wrap: wrap; gap: 6px; }
  .cs-opt {
    width: auto;
    align-items: center;
    padding: 7px 12px 7px 9px;
    border: 1px solid var(--plate-line);
    background: var(--plate);
  }
  .cs-opt[aria-checked="true"] { border-color: var(--ink-strong); background: var(--pill-bg); }
  .cs-opt[data-locked]:hover { background: var(--pill-bg); }
  .cs-opt .cs-box { margin-top: 0; }
}

/* ==========================================================================
   ONE CASE STUDY — the detail template

   The same two objects as /case-studies, reused rather than reinvented: the
   header plate from .cs-head, and the 20/80 .cs-layout under it. The plate
   goes left-aligned and carries a description; the filter column becomes a
   meta column; the card grid becomes three sections of story.

   Every case study page is generated from this. Change it once in
   tools/build-pages.js and all of them move together — which is the point
   of it being a template rather than five pages that resemble each other.
   ========================================================================== */

/* ---------- the header plate, left-aligned variant ----------

   .cs-head-panel is centred flex because /case-studies has one word in it.
   Here it holds a title and a paragraph in a two-thirds measure. It stays
   flex and stays vertically centred — the copy sits in the middle of the
   plate however tall the description runs — and only the horizontal
   alignment changes, so the block is left-justified rather than centred.

   THE INK IS PINNED, same as the index. The plate brings its own light ground
   in both themes, so navy that follows the page would turn pale-on-pale the
   moment someone switches to dark. The lede is #44536A rather than --body:
   the scrim under it is what makes it legible, and --body would drop it under
   4.5:1 on the darker pixels of the artwork. */
.cs-head-panel.is-detail {
  justify-content: flex-start;
  text-align: left;
}
.cs-head-copy {
  position: relative;
  width: 66.667%;
}
.cs-head-lede {
  margin: clamp(12px, 1.5vw, 18px) 0 0;
  font-size: clamp(.92rem, .89rem + .2vw, 1rem);
  line-height: 1.6;
  color: #44536A;
}
@media (max-width: 760px) {
  /* two thirds of a phone is a column of two-word lines */
  .cs-head-copy { width: 100%; }
}

/* ---------- back to the index ----------
   The same eyebrow the service pages carry, in the same place relative to
   the title: above it, at label size. It sits above the PLATE rather than
   inside it, because the plate is a picture with a scrim tuned for the
   title and the lede, and a third line of type on it would need its own
   measurement. Outside, it is ordinary ink on the page ground. */
.cs-back {
  margin-bottom: clamp(12px, 1.4vw, 18px);
}

/* ---------- the case study's picture ----------
   Sits above the meta at the top of the left column, and it is the same file
   the card carries, cropped the other way. It held the client's wordmark in
   a dashed well until 15 September: a mark named the client a second time,
   since the title above it already does, and said nothing about the work.

   3:4 and `cover`, against the card's 3:4 landscape crop of the same
   picture, so the page and the shelf read as two views of one thing. */
.cs-shot {
  aspect-ratio: 3 / 4;
  margin: 0 0 clamp(20px, 2.3vw, 30px);
  border-radius: var(--radius-card);
  background: var(--plate);
  overflow: hidden;
}
.cs-shot > img { display: block; width: 100%; height: 100%; object-fit: cover; }
@media (max-width: 1000px) {
  /* the column goes full width here, and a 3:4 picture across the whole
     shell is a poster. Capped, and it keeps its own line above the meta. */
  .cs-shot { width: min(200px, 38vw); }
}
@media (max-width: 620px) {
  /* On a phone it stops being a thumbnail beside the meta and becomes the
     picture at the top of the story: the full width of the shell, inside
     the same gutters as the plate above it, on the card radius, and the
     whole frame rather than a crop of it, so the aspect ratio is the
     file's own. */
  .cs-shot {
    width: 100%;
    margin-inline: 0;
    margin-bottom: clamp(20px, 5vw, 28px);
    aspect-ratio: auto;
    border-radius: var(--radius-card);
    background: none;
  }
  .cs-shot > img { height: auto; object-fit: contain; }
  /* .cs-side keeps 2px of padding for the sticky column's scrollbar, which
     it does not have on a phone, and which left the picture 2px narrower
     than the plate above it. */
  .cs-side { padding-right: 0; }
}

/* ---------- the meta column ----------
   .cs-side already handles the sticky, the scroll cap and the collapse to a
   full-width block under 1000px, so this only has to describe the rows.
   Label light and unbolded, value bold underneath it. */
.cs-meta {
  display: flex;
  flex-direction: column;
  gap: clamp(16px, 1.9vw, 24px);
  margin: 0;
}
.cs-meta-row { display: flex; flex-direction: column; gap: 3px; }
.cs-meta-label {
  margin: 0;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: .8rem;
  line-height: 1.35;
  color: var(--muted);
}
.cs-meta-val {
  margin: 0;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: .9rem;
  line-height: 1.35;
  letter-spacing: -.01em;
  color: var(--ink);
}
/* The services are links to their own pages. Same device as .cs-clear: the
   underline is there from the start and only takes a colour on hover, so
   the column reads as a list of facts rather than a row of links. */
/* each service on its own line */
.cs-meta-val > span { display: block; }
.cs-meta-val > span + span { margin-top: 3px; }
.cs-meta-val a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: transparent;
  transition: color .25s var(--ease), text-decoration-color .25s var(--ease);
}
.cs-meta-val a:hover { color: var(--ink-hover); text-decoration-color: currentColor; }
.cs-meta-val a:focus-visible { outline: 2px solid var(--ink-strong); outline-offset: 3px; border-radius: 3px; }
@media (max-width: 1000px) {
  /* the filter column lays its options out as a wrapping row at this width;
     the meta does the same, so the two pages collapse the same way */
  .cs-meta { flex-direction: row; flex-wrap: wrap; gap: 14px clamp(24px, 5vw, 56px); }
}

/* ---------- the three sections ----------

   THE EYEBROWS ARE AN EXCEPTION James asked for, not a change of policy. The
   rest of the site has none — see README, "No eyebrows" — because a tracked
   uppercase label above every heading turned into wallpaper. Here there are
   exactly three on the page and they are the same three on every case study,
   so they read as structure rather than decoration: you can tell where you
   are in the story without reading the heading.

   They carry --warm, which is a light peach. On paper that is roughly 1.5:1
   and would fail as body copy. It is allowed here because the eyebrow is a
   repeated landmark rather than content — the H2 underneath says the same
   thing in navy at 12:1 — and because James chose the brand accent over
   introducing a darker orange. If an eyebrow ever carries information the
   heading does not, this decision has to be revisited. */
.cs-body { min-width: 0; }
.cs-block + .cs-block { margin-top: clamp(48px, 5.6vw, 88px); }
.cs-eyebrow {
  display: block;
  margin: 0 0 clamp(8px, 1vw, 12px);
  font-family: var(--font-head);
  font-weight: 700;
  font-size: .78rem;
  line-height: 1.2;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--warm);
}
.cs-block .h2 { margin: 0; }
/* No measure cap. The headings run the full width of the column and a
   paragraph stopping short of them read as a second, narrower column that
   nothing else on the page agreed to. James' call, 15 September. */
.cs-block p { margin: clamp(14px, 1.6vw, 20px) 0 0; }

/* ---------- the solution's asset slots ----------
   A dashed well rather than a grey box: it has to read as a hole waiting for
   a photo, not as a component that shipped empty. The first one spans the
   row, because the lead image of a build is never a thumbnail. */
.cs-assets {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(14px, 1.7vw, 22px);
  margin-top: clamp(24px, 2.8vw, 36px);
}
.cs-asset {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 16 / 10;
  padding: 16px;
  border: 1px dashed var(--plate-line);
  border-radius: var(--radius-card);
  background: var(--plate);
  color: var(--muted);
  font-size: .78rem;
  line-height: 1.4;
  text-align: center;
}
.cs-asset.is-wide { grid-column: 1 / -1; aspect-ratio: 16 / 7; }
/* a real asset dropped in later fills the slot and loses the dashes */
.cs-asset > img,
.cs-asset > video,
.cs-asset > iframe {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: calc(var(--radius-card) - 1px);
}
.cs-asset:has(> img),
.cs-asset:has(> video),
.cs-asset:has(> iframe) { border-style: solid; padding: 0; overflow: hidden; }
@media (max-width: 620px) {
  .cs-assets { grid-template-columns: minmax(0, 1fr); }
  .cs-asset.is-wide { aspect-ratio: 16 / 10; }
}

/* ---------- the three key figures ----------

   Navy cards, no border. They are the loudest thing in the 80% column and
   they should be: the figures are what a visitor scrolls to this section
   for. Cards rather than the full-bleed navy .figs band the old template
   used, because this section sits inside a grid track and a bleed cannot.

   THE INK IS FIXED, not tokenised. --navy is the one ground colour that does
   not flip with the theme — it is the same navy under the footer and the
   floating bar in both — so --paper on it would resolve to navy on navy the
   moment someone switches to dark. Literals, for the same reason the close
   panel and the floating bar's button use them. */
.cs-stats {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(12px, 1.6vw, 20px);
  margin-top: clamp(24px, 2.8vw, 36px);
}
.cs-stat {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: clamp(18px, 2.1vw, 28px);
  border: 0;
  border-radius: var(--radius-card);
  background: var(--navy);
}
/* On a dark page the card's navy IS the page ground, so it stops being a
   card. It steps down to Deep and takes a hairline of light, the same fix
   the floating bar uses when it ends up navy on navy. */
:root[data-theme="dark"] .cs-stat {
  background: var(--deep);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .14);
}
.cs-stat > b {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(1.7rem, 1.2rem + 1.5vw, 2.4rem);
  line-height: 1;
  letter-spacing: -.035em;
  color: #FAFAF8;
}
.cs-stat > span {
  font-size: .82rem;
  line-height: 1.45;
  color: #C9D4E2;
}

/* The direction arrow. Warm, which is the accent doing its declared job —
   "a rule, a figure" — and 6.4:1 on the navy. Both directions are the same
   colour on purpose: up is not always the good news (time to close, cost per
   lead), so the arrow says which way the number moved and the label
   underneath says whether that was a win. If you ever want green/red here,
   they have to be measured on the navy first; --ok and --no are tuned for
   paper and both fail on this ground. */
.cs-stat-arrow {
  flex: none;
  width: .62em;
  height: .62em;
  color: var(--warm);
}
.cs-stat-arrow svg { display: block; width: 100%; height: 100%; fill: currentColor; }
.cs-stat-arrow.is-down svg { transform: rotate(180deg); }

@media (max-width: 760px) {
  .cs-stats { grid-template-columns: minmax(0, 1fr); }
}

/* ---------- the client testimonial ----------

   No card and no warm edge. Both were framing a quote that does not need
   framing — the section already has three navy cards in it, and a fourth
   bordered box under them read as a continuation of the row rather than as
   a person talking.

   A round photo on the left, the words to the right of it, the attribution
   under the words. The photo is a circle because it is a face; everything
   else on this page is on the 12px card radius. */
/* The photo centres on the whole testimonial: the quotation and the name
   line under it together. */
.cs-testi {
  display: flex;
  align-items: center;
  gap: clamp(18px, 2.2vw, 30px);
  margin: clamp(30px, 3.4vw, 48px) 0 0;
}
/* The body copy's paragraph spacing gives the quotation a 20px top margin,
   which would sit the block's centre 10px below the words' own. Only while
   the photo sits beside it: stacked on a phone, it is the space above the
   quotation and stays. */
@media (min-width: 621px) {
  .cs-testi-quote > p { margin-block: 0; }
}
.cs-testi-photo {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: clamp(76px, 7.4vw, 104px);
  aspect-ratio: 1;
  border: 1px dashed var(--plate-line);
  border-radius: 50%;
  background: var(--plate);
  color: var(--muted);
  font-size: .66rem;
  line-height: 1.3;
  text-align: center;
  overflow: hidden;
}
.cs-testi-photo > img { width: 100%; height: 100%; object-fit: cover; }
/* Not every quotation comes with a face. A client's mark in the same well is
   line art on a transparent ground, so it is contained inside the circle
   rather than cropped to fill it, and flattened to white on the dark theme
   the way the marquee flattens the same artwork. */
.cs-testi-photo > img.is-mark {
  width: 100%; height: 100%;
  object-fit: contain;
  padding: 16%;
  mix-blend-mode: multiply;
}
:root[data-theme="dark"] .cs-testi-photo > img.is-mark {
  filter: brightness(0) invert(1);
  mix-blend-mode: normal;
  opacity: .85;
}
.cs-testi-photo:has(> img) { border-style: solid; }
/* .quote supplies the italic body and the 12px stack; this only has to stop
   the blockquote from being a flex item that refuses to shrink */
.cs-testi-quote { min-width: 0; margin: 0; }
@media (max-width: 620px) {
  /* Stacked and centred. The face moves under the attribution rather than
     above the quotation: on a narrow column a photograph at the top is a
     bump the eye has to get past before it reaches the words, and the words
     are the point. */
  .cs-testi {
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: clamp(14px, 4vw, 20px);
  }
  .cs-testi-photo { order: 2; }
  .cs-testi-quote { order: 1; align-items: center; }
  .cs-testi .quote-by { justify-content: center; text-align: center; }
}
/* No measure cap: the quotation runs the full width of the column, the
   way the body copy beside it does. */
.cs-testi-quote p { max-width: none; }
@media (max-width: 560px) {
  .cs-testi { flex-direction: column; gap: 16px; }
}

/* Same pairing as the index: the close bleeds up by --start-bleed and the
   section above it stops exactly that far short. Change one, change both. */
.cs-detail .start-section { --start-bleed: 100px; }
.cs-detail .cs-body-section { padding-bottom: 100px; }

/* ==========================================================================
   /call — THE BOOKING PAGE

   No page header. Every 'Schedule a call' button on the site lands here, and
   the visitor arrived having already read the pitch — a full-width band
   restating it is one more screen between them and the calendar.

   So the page is the split: a 30/70 grid with the reassurance on the left
   and the scheduler on the right, both starting at the same y. The left
   column answers "what happens on this call" while the right one is being
   filled in, which is the only question anyone has at this point.
   ========================================================================== */

/* 30 / 70 exactly. Same construction as .cs-layout: the gap comes out of the
   right column, so the panel stays at 30% of the shell at every width above
   the breakpoint.

   `align-items: center` centres the panel against the scheduler rather than
   hanging it from the top. Note what sets the row height: the HubSpot iframe,
   which arrives short and grows to its real height once the widget has
   loaded. So the panel settles downward on load — once, in the first moment,
   before anyone has read anything. If that ever reads as a jump, the fix is
   a min-height on .call-embed at the widget's settled height, not going back
   to top-alignment. */
.call-layout {
  display: grid;
  grid-template-columns: 30% minmax(0, 1fr);
  gap: clamp(22px, 2.8vw, 44px);
  align-items: center;
}

/* ---------- the panel ----------

   The /case-studies header artwork, portrait. Its scrim is doing the same
   job it does there and is not decoration: navy on the raw image measures
   2.5:1 at the bottom left, and this panel carries a paragraph rather than
   four words, so the whole surface has to hold up rather than one band.

   A flat wash would work (.52 clears 4.86:1) but flattens the artwork where
   it is already light. This one follows the image instead — .46 at the top,
   where the title sits on the pale corner, to .70 at the bottom, where the
   gradient goes dark. Worst pixel: 6.37:1 across panel shapes from 300x760
   to 700x420. Re-measure in Pillow if the artwork or the ramp changes. */
.call-panel {
  position: relative;
  overflow: hidden;
  padding: clamp(26px, 2.4vw, 38px);
  border-radius: var(--radius-card);
  background-color: var(--mist);
  background-image:
    linear-gradient(180deg,
      rgba(250, 250, 248, .46) 0%,
      rgba(250, 250, 248, .70) 100%),
    url("../img/start-panel-bg.webp");
  background-size: auto, cover;
  background-position: center, center;
  background-repeat: no-repeat, no-repeat;
}

/* Navy in both themes, like .cs-head-panel: the panel brings its own light
   ground with it and does not follow the page into dark. */
.call-panel .h1,
.call-panel .call-sub {
  position: relative;
  color: #304157;
}
.call-panel .h1 {
  margin: 0;
  font-size: clamp(2rem, 1.1rem + 2.4vw, 2.9rem);
  line-height: 1.02;
  letter-spacing: -.035em;
}
/* Paragraph spacing inside the panel is tighter than the beat under the
   title: the two paragraphs are one thought in two halves, and giving them
   the same gap the h1 gets reads as two separate blocks of copy. */
.call-sub {
  margin: clamp(10px, 1vw, 14px) 0 0;
  font-size: clamp(.94rem, .9rem + .12vw, 1.02rem);
  line-height: 1.6;
}
.call-panel .h1 + .call-sub { margin-top: clamp(14px, 1.4vw, 20px); }

/* ---------- the video ----------

   16:9, on the 12px card radius like everything else. The placeholder is a
   dashed frame on the panel's own ground rather than a grey block, so an
   empty slot reads as "not filled in yet" rather than as a broken embed.

   Swapping in the real one: replace the whole .call-video-ph block with the
   YouTube <iframe> and nothing else changes — the wrapper supplies the
   ratio, the radius and the clipping, and `.call-video > *` sizes whatever
   is inside it. */
.call-video {
  position: relative;
  margin-top: clamp(20px, 2.2vw, 30px);
  aspect-ratio: 16 / 9;
  border-radius: 12px;
  overflow: hidden;
  /* Safari clips an iframe out of a rounded parent only with its own layer */
  isolation: isolate;
}
.call-video > * {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}
.call-video-ph {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  border: 1px dashed rgba(48, 65, 87, .38);
  border-radius: 12px;
  background: rgba(250, 250, 248, .58);
  color: #304157;
}
.call-video-play {
  display: flex;
  align-items: center;
  justify-content: center;
  width: clamp(38px, 3.4vw, 48px);
  aspect-ratio: 1;
  border-radius: 50%;
  background: rgba(48, 65, 87, .10);
}
.call-video-play svg { width: 42%; fill: currentColor; display: block; }
.call-video-note {
  font-family: var(--font-body);
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
}

/* ---------- the scheduler column ----------
   No max-width and no auto margins: .meet-wrap centres itself at 960px,
   which inside a 70% column leaves the widget floating away from the panel
   at wide sizes. Here the column IS the measure. */
.call-embed { width: 100%; min-width: 0; }

/* One column below 900px, and the panel first — the scheduler is tall on a
   phone, and putting it above the reassurance means nobody reads the
   reassurance. Same order as the source, so nothing to reorder.

   `align-items: start` comes back with the single column: centring is a
   two-column idea, and stacked it does nothing except give a short panel
   slack it does not need. */
@media (max-width: 900px) {
  .call-layout {
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
  }
}

/* ---------- proof under the split ----------

   The marquee and one quote, in that order. Both are borrowed objects: the
   logo band off the homepage (via logoBand() in the builder) and the .cs-testi
   figure off the case study Results section. Nothing new is defined for
   either — what is below is only the spacing that puts them on this page.

   The split's own bottom padding is cut back, because the marquee brings its
   own lead in --proof-lead and the two together left a dead screen. */
.call-split { padding-bottom: clamp(40px, 4.4vw, 64px); }

/* The quote alone in a section, rather than at the end of a Results column.
   Capped and centred: this page's .shell runs to 1680px, and a 76px circle
   at the far left of that with the words a hand's width away is two objects,
   not one. The cap is the measure the quote already wants. */
.call-testi-section .call-testi {
  margin: 0 auto;
  max-width: 860px;
}


/* ==========================================================================
   ABOUT — portrait on its own

   The homepage runs the portrait with the Platinum badge overhanging its
   lower right, and .about-stack carries padding on two sides to make room
   for that overhang. /about runs the photograph alone, so that padding is
   dead space that pushes the picture off centre in its column. This strips
   it and lets the image run a little wider, keeping the same tilt.
   ========================================================================== */
.about-stack-solo { padding: 0; max-width: 440px; }
@media (max-width: 1000px) { .about-stack-solo { max-width: 360px; } }


/* ==========================================================================
   MEET THE TEAM

   One card today and more later, which is the whole constraint. auto-FILL
   rather than auto-fit: auto-fit collapses the empty tracks and stretches
   the single card to the full 1240px shell, which reads as a banner with a
   headshot in it. auto-fill keeps the empty tracks, so one person sits in a
   card the same width the third person's card will be, and nothing has to
   change here when the second and third arrive.

   Capped at 380px so three across fills the shell and a lone card does not
   look like a placeholder waiting for content.
   ========================================================================== */
.team-grid {
  display: grid;
  gap: clamp(16px, 1.8vw, 24px);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 380px));
  justify-content: start;
  margin-top: clamp(22px, 2.6vw, 34px);
}
/* /about, 17 September: the card beside the copy off the homepage, so one
   person does not leave two thirds of the shell empty. The left column is
   the grid above, so more people fill it down the page and the copy column
   stays put. */
.team-split {
  display: grid;
  gap: clamp(26px, 4vw, 64px);
  grid-template-columns: minmax(0, 420px) minmax(0, 1fr);
  align-items: start;
  margin-top: clamp(22px, 2.6vw, 34px);
}
.team-split .team-grid { margin-top: 0; }
/* The copy sits optically level with the top of the card rather than with
   its own first line box. */
.team-copy { padding-top: 4px; }
@media (max-width: 900px) {
  .team-split { grid-template-columns: minmax(0, 1fr); }
}

.team-card {
  background: var(--card-veil);
  -webkit-backdrop-filter: blur(16px) saturate(1.15);
          backdrop-filter: blur(16px) saturate(1.15);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  padding: clamp(20px, 2.1vw, 28px);
  display: flex;
  flex-direction: column;
  gap: 12px;
  transition: border-color var(--mcard-t) var(--mcard-ease),
              box-shadow var(--mcard-t) var(--mcard-ease);
}
.team-card:hover {
  border-color: var(--mist);
  box-shadow: 0 26px 54px -34px rgba(28, 38, 53, .38);
}

/* Square, not 4/5. The card is a person and a paragraph; a portrait crop at
   this width makes the photograph the whole card and the words the caption. */
.team-photo {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  object-position: center 22%;
  border-radius: var(--radius-card);
  background: var(--mist);
  margin-bottom: 4px;
}
.team-name {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(1.06rem, .98rem + .4vw, 1.28rem);
  line-height: 1.2;
  letter-spacing: -.02em;
  color: var(--ink);
  margin: 0;
}
/* Sits directly under the name, so it takes negative margin off the flex gap
   rather than a rule of its own. */
.team-role {
  margin: -8px 0 0;
  font-family: var(--font-head);
  font-weight: 500;
  font-size: .74rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
}
.team-bio { margin: 0; font-size: .9rem; line-height: 1.6; color: var(--body); }

/* The LinkedIn link is pinned to the bottom so cards with bios of different
   lengths still line their links up across the row. */
.team-social {
  margin-top: auto;
  padding-top: 4px;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  align-self: flex-start;
  font-family: var(--font-head);
  font-weight: 500;
  font-size: .82rem;
  color: var(--ink-strong);
  text-decoration: none;
}
.team-social svg {
  width: 17px;
  height: 17px;
  fill: currentColor;
  transition: transform .3s var(--ease-out);
}
.team-social:hover { color: var(--ink-hover); }
.team-social:hover svg { transform: translateY(-2px); }
.team-social:focus-visible {
  outline: 2px solid var(--ink-strong);
  outline-offset: 4px;
  border-radius: 4px;
}

/* ==========================================================================
   RESOURCES — the shelf of shelves

   /resources is one object repeated: a 16:9 card in a four-across grid. The
   five sections differ only in what is in the data — a type with a See-all
   destination shows one row and a link, a type without one shows two rows
   and pages through the rest. Nothing here is per-section styling.

   Everything below is generated by tools/build-pages.js from RESOURCES and
   RESOURCE_TYPES. Adding a resource is one object there and no CSS.
   ========================================================================== */

/* The close bleeds up over the last shelf by --start-bleed. 100px, the same
   as /services and /case-studies: the full bleed is tuned to the homepage's
   testimonial block and swallows a row of cards here. */
.res-index .start-section { --start-bleed: 100px; }

/* ---------- the featured slider ----------

   THREE resources on a fifteen second rotation, on THE PLATE OFF THE CASE
   STUDY PAGES rather than the warm ramp this card used to carry. /resources
   and a case study now open on the same object, which is the point: the
   card most often points at one. Same three layers .cs-head-panel uses —
   mist under the artwork under a soft white scrim — and the same
   measurement holds, because it is the same artwork at the same crop:
   #304157 is 5.9:1 on the worst pixel under the title and 8.5:1 on the
   best. Re-measure if the crop or the scrim changes.

   THE INK IS PINNED, like .cs-head-panel and .call-panel. The card brings
   its own light ground with it, so navy that followed the page would turn
   pale-on-pale the moment someone switched to dark.

   NOTHING IN HERE MAY TAKE overflow: hidden. The drawing is anchored to the
   bottom edge of the card and is taller than the card, so it climbs out
   over the top of it and into the header above. That overhang is the whole
   effect and a clip anywhere up the chain removes it silently.
   ========================================================================== */
.res-feature-section {
  /* room above the card for the part of the drawing that sits outside it */
  padding-top: clamp(46px, 5.4vw, 84px);
  padding-bottom: clamp(18px, 2.2vw, 30px);
}
/* 15% NARROWER THAN THE SHELL, and CENTRED IN IT. At the full width the
   card was a band across the page with its copy running to a 60% measure of
   something very wide. It was left-aligned first, on the reasoning that
   every heading and shelf below it starts on the shell's left edge — which
   read as a card that had slipped, because the gap it left was all on one
   side. Centred, the 15% comes off both edges and the eyebrow, the card and
   the dots are one block.

   The drawing is positioned off the CARD, so it comes in with the right edge
   and the overhang is unaffected. */
.res-feature-wrap { position: relative; max-width: 85%; margin-inline: auto; }
@media (max-width: 900px) {
  /* below the breakpoint the shell is already the narrow measure and taking
     another 15% off it leaves a card with two words to a line */
  .res-feature-wrap { max-width: none; }
}

/* SAYS WHAT THE CARD IS. Three cards rotating at the top of a page of cards
   needed something to mark them as a set rather than as the first shelf, and
   the dots alone were not saying it. Slate rather than the warm .cs-eyebrow
   takes: this one labels a section, it does not open a story. */
.res-feature-eyebrow {
  margin: 0 0 clamp(10px, 1.2vw, 15px);
  font-family: var(--font-head);
  font-weight: 700;
  font-size: .78rem;
  line-height: 1.2;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--slate);
}

/* THE THREE SLIDES SHARE ONE GRID CELL. Stacked rather than laid out in a
   row, so the card is as tall as the tallest of the three and does not
   resize under the reader when it rotates. The hidden two keep their space
   — that is what holds the height still — and site.js takes their links out
   of the tab order. */
.res-feature-stack { display: grid; }
.res-feature {
  grid-area: 1 / 1;
  position: relative;
  display: flex;
  align-items: flex-end;
  text-decoration: none;
  color: inherit;
  min-height: clamp(210px, 22vw, 300px);
  padding: clamp(26px, 3.4vw, 54px);
  border-radius: var(--radius-card);
  background-color: var(--mist);
  background-image:
    radial-gradient(ellipse 72% 102% at 50% 50%,
      rgba(250, 250, 248, .74) 0%,
      rgba(250, 250, 248, .34) 55%,
      rgba(250, 250, 248, 0) 100%),
    url("../img/start-panel-bg.webp");
  background-size: auto, cover;
  background-position: center, center 20%;
  background-repeat: no-repeat, no-repeat;

  /* the crossfade. visibility rather than display so the box keeps its
     space, and delayed on the way out so it is not pulled before the fade
     has finished. */
  opacity: 0;
  visibility: hidden;
  transition: opacity .6s var(--ease-out), visibility 0s linear .6s;
}
.res-feature.is-on {
  opacity: 1;
  visibility: visible;
  z-index: 1;
  transition: opacity .6s var(--ease-out), visibility 0s;
}
@media (prefers-reduced-motion: reduce) {
  .res-feature { transition: none; }
}

/* 60% and not a column: the drawing is absolutely placed against the card,
   so nothing in the flow is holding the copy off it. */
.res-feature-text {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  max-width: min(62ch, 60%);
}

/* WHAT KIND OF THING THIS IS. The shelves below say it with a heading over
   a grid; up here there is no heading, so the card says it itself.

   WARM, as of 17 September, so it pops off the mist plate; it was a pale
   chip before. Solid warm with literal navy type, the site's navy-on-warm
   rule, and pinned in both themes because the plate is. */
.res-feature-kind {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  margin-bottom: clamp(10px, 1.2vw, 15px);
  padding: 4px 10px;
  border-radius: var(--radius-sm);
  border: 1px solid #E9AD7B;
  background: #F2C39B;
  color: #304157;
  font-size: .68rem;
  font-weight: 700;
  line-height: 1.5;
  letter-spacing: .1em;
  text-transform: uppercase;
}
.res-feature-title {
  display: block;
  margin: 0;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(1.6rem, 1.1rem + 1.9vw, 2.4rem);
  line-height: 1.06;
  letter-spacing: -.035em;
  color: #304157;
  text-wrap: balance;
}
.res-feature-copy {
  display: block;
  margin: clamp(12px, 1.4vw, 18px) 0 0;
  font-size: clamp(.94rem, .9rem + .14vw, 1.04rem);
  line-height: 1.6;
  color: #3C4B60;
}
/* Looks like the text link it used to be and is not one: the whole card is
   the link now, so this is a span and the hover and the focus ring belong to
   the card. A boxed button would be wrong here anyway — on this site those
   mean booking or requesting. Pinned navy, same as the title. */
.res-feature-go {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  gap: 8px;
  margin-top: clamp(16px, 1.8vw, 24px);
  font-family: var(--font-body);
  font-weight: 700;
  font-size: .95rem;
  color: #304157;
}
.res-feature-go .arrow { display: inline-block; transition: transform .3s var(--ease-out); }
.res-feature:hover .res-feature-go .arrow { transform: translateX(5px); }
/* The ring goes round the whole card, and outside it, because the card's own
   edge is a light plate on a light page and a ring drawn inside it would be
   reading against the artwork. */
.res-feature:focus-visible { outline: 2px solid var(--ink-strong); outline-offset: 4px; }
/* No lift: it is 1300px wide and moving it moves the drawing hanging off the
   top of it. The shadow alone says the whole thing is one target. */
.res-feature:hover { box-shadow: 0 26px 52px -28px rgba(28, 38, 53, .38); }

/* THE OVERHANG. Pinned to the card's bottom edge and taller than the card
   by a fixed beat, so it rises out of the top. HEIGHT drives it and the
   width falls out of the drawing's own 240x260 box, which is how it stays
   near a third of the card at every width with only one number to keep. */
.res-feature-art {
  position: absolute;
  right: clamp(22px, 4vw, 64px);
  bottom: 0;
  height: calc(100% + clamp(38px, 4.4vw, 76px));
  z-index: 1;
  color: #304157;
  pointer-events: none;
}
.res-feature-art .rh-art { position: relative; display: block; height: 100%; width: auto; }

/* THE OUTLINE, AND IT ONLY EXISTS ON THE DARK THEME.

   The drawing's ink is pinned navy, because most of it is sitting on the
   card's own light plate. The part that climbs out over the top edge is
   not: on the dark theme it leaves the plate and lands on a navy page,
   where navy on navy is nothing at all and the overhang simply vanished —
   the drawing read as cropped to the card rather than rising out of it.

   Two tight drop shadows in paper give every stroke a light edge. They hug
   the line rather than pooling behind the drawing, which is the difference
   between an outline and a smudge, and over the light half they fall on
   paper-coloured ground and cannot be seen at all. One filter, no second
   copy of the artwork, and nothing to keep in step.

   drop-shadow and not a wash behind it, because a wash has to end
   somewhere and wherever it ends is a line across the card. */
:root[data-theme="dark"] .res-feature-art .rh-art {
  filter: drop-shadow(0 0 1.6px #F2F6FA)
          drop-shadow(0 0 3.4px rgba(242, 246, 250, .85));
}

/* ---------- the dots ---------- */
.res-feature-dots {
  display: flex;
  justify-content: center;
  gap: 10px;
  margin-top: clamp(16px, 1.8vw, 22px);
}
.res-feature-dot {
  width: 9px;
  height: 9px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--track-ring);
  cursor: pointer;
  transition: background .3s var(--ease), transform .3s var(--ease-out);
}
.res-feature-dot:hover { background: var(--slate); }
.res-feature-dot[aria-current="true"] { background: var(--ink-strong); transform: scale(1.28); }
.res-feature-dot:focus-visible { outline: 2px solid var(--ink-strong); outline-offset: 3px; }

@media (max-width: 760px) {
  /* the drawing drops under the copy and centres: at phone width a third of
     the card beside the type is a drawing an inch wide beside a column of
     two-word lines */
  .res-feature { display: block; padding-bottom: clamp(168px, 42vw, 220px); }
  .res-feature-text { max-width: none; }
  .res-feature-art {
    right: 50%;
    transform: translateX(50%);
    height: clamp(176px, 44vw, 232px);
  }
}

/* ---------- the type filter ----------

   One row of choices, single selection, All by default. It hides whole
   sections rather than individual cards — see the note in build-pages.js.
   Not sticky: a bar that follows you down needs its own opaque ground, and
   this page's ground drifts from paper to white on the way past. */
.res-filter-section { padding-bottom: clamp(6px, 1vw, 14px); }
.res-filter {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.res-tab {
  padding: 9px 16px;
  border-radius: var(--radius-btn);
  border: 1px solid var(--plate-line);
  background: var(--plate);
  color: var(--plate-ink);
  font-family: var(--font-body);
  font-weight: 700;
  font-size: .8rem;
  cursor: pointer;
  transition: background var(--mcard-t) var(--mcard-ease),
              border-color var(--mcard-t) var(--mcard-ease),
              color var(--mcard-t) var(--mcard-ease);
}
.res-tab:hover { border-color: var(--ink-strong); color: var(--ink-strong); }
.res-tab:focus-visible { outline: 2px solid var(--ink-strong); outline-offset: 3px; }
/* THE CHOSEN ONE IS A LITERAL, IN BOTH DIRECTIONS.

   Written with --navy and --paper it disappeared on a dark page: --navy is
   the page ground there and --paper resolves to the same navy, so the
   pressed tab was navy type on a navy chip. It inverts instead, exactly the
   way .btn-primary does — the dark thing on a light page, the light thing
   on a dark one — and both halves are literals so neither can follow the
   theme somewhere it should not go. */
.res-tab[aria-pressed="true"] {
  background: #304157;
  border-color: #304157;
  color: #FAFAF8;
}
:root[data-theme="dark"] .res-tab[aria-pressed="true"] {
  background: #FAFAF8;
  border-color: #FAFAF8;
  color: #304157;
}

/* ---------- the search box, 17 September ----------

   The pills on the left, the search hard right on the same line. It is
   dressed as a pill that takes typing — the same plate, line, radius, type
   and padding as .res-tab — so the two read as one row of controls and sit
   at the same height without a number tying them together.

   Under 860px it takes its own full-width line beneath the pills: squeezed
   beside them it wraps anyway, and a half-width box on a line of its own
   reads as a mistake. On a phone the type goes to 16px, because iOS zooms
   the page into any field set smaller than that. */
.res-filter-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px 24px;
}
.res-filter-bar .res-filter { flex: 1 1 auto; min-width: 0; }
.res-search {
  position: relative;
  flex: 0 1 300px;
  min-width: 220px;
}
.res-search-icon {
  position: absolute;
  left: 13px;
  top: 50%;
  width: 15px;
  height: 15px;
  transform: translateY(-50%);
  fill: none;
  stroke: var(--plate-ink);
  stroke-width: 2.4;
  stroke-linecap: round;
  pointer-events: none;
}
.res-search-input {
  -webkit-appearance: none;
  appearance: none;
  box-sizing: border-box;
  width: 100%;
  margin: 0;
  padding: 9px 14px 9px 36px;
  border-radius: var(--radius-btn);
  border: 1px solid var(--plate-line);
  background: var(--plate);
  color: var(--ink-strong);
  font-family: var(--font-body);
  font-weight: 700;
  font-size: .8rem;
  line-height: inherit;   /* buttons take the body's 1.65 through font: inherit; a field does not */
  transition: border-color var(--mcard-t) var(--mcard-ease);
}
.res-search-input::placeholder { color: var(--plate-ink); opacity: 1; }
.res-search-input:hover,
.res-search-input:focus { border-color: var(--ink-strong); }
/* A field is focus-visible on a mouse click too, so the pills' offset
   outline would ring it every time. A second pixel of border instead. */
.res-search-input:focus { outline: none; box-shadow: 0 0 0 1px var(--ink-strong); }
.res-search-input::-webkit-search-decoration { -webkit-appearance: none; }
.res-search-input::-webkit-search-cancel-button { cursor: pointer; }

/* What the search found. The count is for screen readers only; the
   no-match line is for everybody. */
.res-search-status {
  margin: 18px 0 0;
  color: var(--body);
  font-size: .95rem;
}
.res-search-status[hidden] { display: none; }
.res-search-status.is-quiet {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}
.res-search-status .text-link {
  display: inline;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  font-size: inherit;
}
.res-pager[hidden] { display: none; }

@media (max-width: 860px) {
  .res-search { flex-basis: 100%; min-width: 0; }
}
@media (max-width: 700px) {
  .res-search-input { font-size: 16px; }
}

/* ---------- a shelf ----------
   The heading is left aligned with its subheading under it, which is the
   .sec-head-left arrangement without the wrapper, since a shelf head never
   sits beside anything. */
.res-shelf { padding-top: clamp(34px, 3.8vw, 58px); padding-bottom: clamp(34px, 3.8vw, 58px); }
.res-shelf[hidden] { display: none; }
.res-shelf-head { margin-bottom: clamp(20px, 2.4vw, 32px); }
.res-shelf-head .h2 { margin: 0; text-align: left; margin-inline: 0; }
.res-shelf-head .sec-sub { margin-top: 10px; max-width: 62ch; }

/* Four across, and the count is fixed rather than auto-fit: the shelves that
   paginate show exactly 4x2 and a row that reflowed to three would leave a
   page with two orphans. */
.res-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(16px, 1.8vw, 24px);
}
@media (max-width: 1120px) { .res-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 860px)  { .res-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 560px)  { .res-grid { grid-template-columns: minmax(0, 1fr); } }

/* ---------- one card ----------
   A link, a button or a div depending on what the resource is, so the reset
   has to cover the button case: inherited font, left alignment, no chrome. */
.res-card {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: clamp(14px, 1.3vw, 18px);
  text-align: left;
  text-decoration: none;
  font: inherit;
  color: inherit;
  background: var(--card-veil);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  transition: border-color var(--mcard-t) var(--mcard-ease),
              transform .5s var(--ease-out),
              box-shadow var(--mcard-t) var(--mcard-ease);
}
.res-card[hidden] { display: none; }
a.res-card, button.res-card { cursor: pointer; }
/* THE SAME HOVER AS /pricing, and for the same reason the card hovers were
   unified in the first place: a navy border at full strength read as an
   outline snapping on rather than a card lifting. Mist edge, the 4px lift
   and the deep soft shadow — .price-teaser-cell, .card and .pcard all do
   exactly this. */
a.res-card:hover, button.res-card:hover {
  border-color: var(--mist);
  transform: translateY(-4px);
  box-shadow: 0 26px 52px -28px rgba(28, 38, 53, .38);
}
a.res-card:focus-visible, button.res-card:focus-visible {
  outline: 2px solid var(--ink-strong); outline-offset: 3px;
}
/* A placeholder card is inert and says so. Faded rather than hidden: the
   shelf is easier to judge full than empty, and a card that looks clickable
   and goes nowhere is worse than one that plainly is not filled in yet. */
div.res-card { opacity: .72; }

.res-thumb {
  position: relative;
  display: block;
  aspect-ratio: 16 / 9;
  border-radius: 10px;
  overflow: hidden;
  background: var(--surface-alt);
}
.res-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* A DRAWING, NOT A PICTURE. The ink is navy and the card's own surface goes
   dark with the page, so the art brings its own light plate — the mist end
   of the featured card's ramp, flattened — and the drawing is contained
   rather than cropped, which is what a 240x260 box in a 16:9 frame wants. */
.res-thumb.is-art { background: linear-gradient(158deg, #EDF1F6 0%, #FAFAF8 60%, #E4EBF3 100%); }
.res-thumb.is-art .rh-art {
  display: block;
  width: 100%;
  height: 100%;
  padding: 9px 0;
  color: #304157;
}
/* the dashed well, same idea as the case study logo slot: an empty frame
   reads as "no artwork yet", a grey rectangle reads as a broken image */
.res-thumb-ph {
  position: absolute;
  inset: 0;
  border: 1px dashed rgba(48, 65, 87, .26);
  border-radius: 10px;
  background: repeating-linear-gradient(135deg,
    rgba(198, 211, 224, .18) 0 10px, rgba(198, 211, 224, 0) 10px 20px);
}
.res-play {
  position: absolute;
  inset: 0;
  margin: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  background: rgba(250, 250, 248, .92);
  box-shadow: 0 8px 20px -10px rgba(28, 38, 53, .6);
  transition: transform .3s var(--ease-out);
}
.res-play svg { width: 44%; fill: #304157; display: block; }
.res-card:hover .res-play { transform: scale(1.08); }

.res-title {
  margin: 0;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(1rem, .94rem + .24vw, 1.14rem);
  line-height: 1.24;
  letter-spacing: -.02em;
  color: var(--ink);
  text-wrap: balance;
}
.res-copy { margin: 0; font-size: .86rem; line-height: 1.55; color: var(--body); }

/* ---------- meta on a case study card ----------
   The case study page's own meta column, shrunk to a card: a light label,
   the value in bold under it, one service per line. No boxes. Sizes step
   down from .cs-meta-label / .cs-meta-val so four rows under a title still
   leave the title as the loudest thing on the card. */
.res-meta { display: flex; flex-direction: column; gap: 10px; }
.res-meta-row { display: flex; flex-direction: column; gap: 2px; }
.res-meta-label {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: .74rem;
  line-height: 1.35;
  color: var(--muted);
}
.res-meta-val {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: .82rem;
  line-height: 1.35;
  letter-spacing: -.01em;
  color: var(--ink);
}
.res-meta-val > span { display: block; }
.res-meta-val > span + span { margin-top: 2px; }
.res-copy b { color: var(--ink); font-weight: 700; }

/* pushed to the bottom so the link sits on one line across a row of cards
   whose copy runs to different lengths */
.res-foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-top: auto;
  padding-top: 4px;
}
.res-go {
  font-weight: 700;
  font-size: .84rem;
  color: var(--ink-strong);
  white-space: nowrap;
}
.res-go .arrow { display: inline-block; transition: transform .3s var(--ease-out); }
.res-card:hover .res-go .arrow { transform: translateX(5px); }
div.res-card .res-go { color: var(--muted); }
.res-fact { font-size: .76rem; color: var(--muted); text-align: right; }

/* ---------- see-all, and the pager ---------- */
.res-all { margin-top: clamp(18px, 2vw, 26px); }
.res-pager {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: clamp(18px, 2vw, 26px);
}
.res-page-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border-radius: var(--radius-btn);
  border: 1px solid var(--plate-line);
  background: var(--plate);
  color: var(--ink-strong);
  font-size: .95rem;
  cursor: pointer;
  transition: border-color var(--mcard-t) var(--mcard-ease),
              background var(--mcard-t) var(--mcard-ease);
}
.res-page-btn:hover { border-color: var(--ink-strong); }
.res-page-btn:focus-visible { outline: 2px solid var(--ink-strong); outline-offset: 3px; }
/* Disabled rather than hidden at the ends of the run: a control that
   disappears moves the one beside it, and the pager is two buttons wide. */
.res-page-btn[disabled] { opacity: .38; cursor: default; }
.res-page-btn[disabled]:hover { border-color: var(--plate-line); }
.res-page-count { font-size: .8rem; font-weight: 700; color: var(--muted); }

/* ---------- the video lightbox ----------

   One <dialog> serves every video card on the page. It sits in the top
   layer, so where it is in the document does not matter and it cannot be
   clipped by a section's overflow.

   site.js writes the iframe on open and REMOVES it on close. Leaving the
   src in place keeps YouTube playing behind a closed dialog, which is
   audible and was the first thing to get wrong here. */
.res-lightbox {
  width: min(1100px, 92vw);
  max-width: none;
  padding: 0;
  border: 0;
  border-radius: var(--radius-card);
  background: transparent;
  overflow: visible;
}
.res-lightbox::backdrop { background: rgba(28, 38, 53, .78); }
.res-lightbox-frame {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-card);
  overflow: hidden;
  background: #1C2635;
  /* Safari clips an iframe out of a rounded parent only with its own layer */
  isolation: isolate;
}
.res-lightbox-frame iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}
/* Above the frame rather than over it: at 92vw on a phone a close button
   inside the video covers the picture, and outside the frame it has the
   backdrop to sit on. */
.res-lightbox-close {
  position: absolute;
  top: -46px;
  right: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: var(--radius-btn);
  border: 1px solid rgba(255, 255, 255, .3);
  background: transparent;
  color: #FAFAF8;
  cursor: pointer;
}
.res-lightbox-close svg { width: 18px; stroke: currentColor; stroke-width: 2; fill: none; }
.res-lightbox-close:hover { background: rgba(255, 255, 255, .14); }
.res-lightbox-close:focus-visible { outline: 2px solid #FAFAF8; outline-offset: 3px; }

/* ---------- a gated resource's own page ----------
   The same two-column split /call uses, and for the same reason: the
   reassurance has to sit beside the thing you are asking someone to fill
   in, not above it where it scrolls away on a phone. */
.res-gate {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(24px, 3vw, 48px);
  align-items: start;
}
@media (max-width: 900px) { .res-gate { grid-template-columns: minmax(0, 1fr); } }

/* ---------- a picture of the thing, where the headline was ----------

   A screenshot of the resource in place of the one-line statement, with the
   paragraph under it. Somebody who has landed on a form wants to know what
   they are filling it in for, and a picture of the spreadsheet answers that
   faster than a sentence about the spreadsheet does.

   Same treatment as the partner directory shot on /hubspot: the artwork
   brings its own white ground, so it takes a hairline and a radius and sits
   on the page as an object rather than as a rectangle that fell out of a
   browser. No hover — this one is not a link.

   THE PARAGRAPH'S STYLING FOLLOWED THE STATEMENT. `.statement-note` is only
   ever styled as `.statement + .statement-note`, so swapping the statement
   out took the paragraph's type with it. It is repeated here rather than
   loosened there: `.statement-note` on its own is used elsewhere on the
   site and is not mine to change from under those pages. */
.res-gate-shot {
  margin: 0;
  border-radius: var(--radius-card);
  overflow: hidden;
  background: #FFFFFF;
  box-shadow: 0 0 0 1px rgba(28, 38, 53, .1), 0 22px 44px -30px rgba(28, 38, 53, .5);
}
.res-gate-shot img { display: block; width: 100%; height: auto; }
.res-gate-shot + .statement-note {
  margin-top: clamp(18px, 2vw, 26px);
  font-size: .95rem;
  line-height: 1.65;
  color: var(--body);
  max-width: 56ch;
}
.res-gate-form {
  padding: clamp(22px, 2.4vw, 34px);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--card-veil);
}
.res-gate-title {
  margin: 0 0 clamp(14px, 1.6vw, 20px);
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(1.1rem, 1rem + .4vw, 1.35rem);
  letter-spacing: -.02em;
  color: var(--ink);
}
/* the dashed well again, so an unfinished form reads as unfinished */
.res-form-ph {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: clamp(20px, 2.2vw, 30px);
  border: 1px dashed rgba(48, 65, 87, .34);
  border-radius: 10px;
  color: var(--body);
  font-size: .88rem;
  line-height: 1.55;
}
.res-form-ph p { margin: 0; }
.res-form-note {
  font-family: var(--font-body);
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}

/* ---------- dark ----------
   The tokens carry almost all of it. What is left is the three surfaces
   that paint a literal colour: the dashed wells, which are drawn in navy
   and vanish on a navy page, and the play badge, which is a light plate by
   design and stays one. */
:root[data-theme="dark"] .res-thumb-ph,
:root[data-theme="dark"] .res-form-ph { border-color: rgba(255, 255, 255, .3); }
:root[data-theme="dark"] .res-thumb-ph {
  background: repeating-linear-gradient(135deg,
    rgba(255, 255, 255, .07) 0 10px, rgba(255, 255, 255, 0) 10px 20px);
}

/* ==========================================================================
   /hubspot — WHAT IT IS, and THE ONE REVIEW

   Replaced the sticky-title / scrolling-reviews block on 13 September. That
   column was the tallest thing on the page and it put twelve hundred words
   of praise between the reader and the four things the page wants them to
   click. One review, centred, and the rest one link away on the partner
   directory — which is the version a sceptical reader can verify anyway.
   ========================================================================== */

/* ---------- the one review ----------

   TWO COLUMNS: the directory listing on the left, the review on the right.
   The screenshot is the argument — 5.0 from ten ratings, all of them five
   stars, and the Platinum chip under our own name. That is what the old
   "Every review is five stars" heading was claiming in words, made instead
   by the thing itself, and both the picture and the link under it go to the
   page where a reader can check it.

   TIGHTER THAN THE PAGE'S RHYTHM on purpose. One quote in two columns does
   not need the air a full-width block does, and at the standard padding the
   section read as a gap with a quote in it.
   ========================================================================== */
.hs-testi-section { padding-block: clamp(38px, 4.2vw, 60px); }
/* THE QUOTE LEADS, the proof sits beside it. The screenshot went second on
   13 September: it is the evidence for the review, and evidence reads better
   after the claim than before it.

   The proof column is 20% narrower than it was, and the shot fills it, so
   the picture is sized by the grid rather than by a width on the image —
   one number, and the link under it stays flush with its left edge. */
.hs-testi {
  display: grid;
  grid-template-columns: minmax(0, 1.26fr) minmax(0, .74fr);
  align-items: center;
  gap: clamp(28px, 4.4vw, 64px);
}

/* ---------- the proof ---------- */
.hs-testi-proof {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: clamp(14px, 1.6vw, 20px);
}
/* A screenshot, so it keeps its own light ground in both themes and takes a
   hairline and a radius to sit on the page as an object rather than as a
   rectangle that fell out of a browser. Decorative: the caption under it and
   the quote beside it both say what it is, and reading a whole directory
   listing out is not a service to anybody. */
.hs-testi-shot {
  display: block;
  width: 100%;
  border-radius: var(--radius-card);
  overflow: hidden;
  background: #FFFFFF;
  box-shadow: 0 0 0 1px rgba(28, 38, 53, .1), 0 22px 44px -30px rgba(28, 38, 53, .5);
  transition: transform .4s var(--ease-out), box-shadow var(--mcard-t) var(--mcard-ease);
}
.hs-testi-shot img { display: block; width: 100%; height: auto; }
.hs-testi-shot:hover {
  transform: translateY(-3px);
  box-shadow: 0 0 0 1px rgba(28, 38, 53, .14), 0 28px 54px -28px rgba(28, 38, 53, .45);
}
@media (prefers-reduced-motion: reduce) { .hs-testi-shot:hover { transform: none; } }
/* the shot carries tabindex="-1": one target, one tab stop, and this is it */
.hs-testi-link { font-size: .92rem; }

/* ---------- the review ---------- */
.hs-testi-body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: clamp(12px, 1.5vw, 18px);
  margin: 0;
}
/* the quote marks carry the warm and stay upright while the words lean,
   exactly as .testi-title does — an italic quote mark beside an italic word
   reads as a smudge */
.hs-testi-title {
  margin: 0;
  font-family: var(--font-head);
  font-weight: 700;
  font-style: italic;
  font-size: clamp(1.3rem, 1.05rem + 1.1vw, 1.85rem);
  line-height: 1.16;
  letter-spacing: -.03em;
  color: var(--ink);
  text-wrap: balance;
}
.hs-testi-title .q { color: var(--warm); font-style: normal; }

.hs-testi-quote { margin: 0; }
.hs-testi-quote p {
  margin: 0;
  font-size: clamp(.98rem, .94rem + .34vw, 1.14rem);
  line-height: 1.62;
  color: var(--body);
}
/* weight rather than a marker, the same call the homepage quotes make */
.hs-testi-quote .hl { background-image: none; color: var(--ink); font-weight: 700; }

.hs-testi-by {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px 12px;
  margin-top: clamp(4px, .6vw, 8px);
  font-size: .9rem;
  color: var(--slate);
}
.hs-testi-by cite { font-style: normal; font-weight: 700; color: var(--ink); }
.hs-testi-by .sep { color: var(--mist); }

/* UNDER THE ATTRIBUTION, not over the quote. Above it they read as
   decoration on the heading; here they read as Amy's rating, which is what
   they are. */
.hs-testi-stars svg { width: 19px; height: 19px; }

@media (max-width: 900px) {
  /* the proof goes above the quote: a screenshot of a two-column web page
     shrunk into half a phone is not readable as anything */
  .hs-testi { grid-template-columns: minmax(0, 1fr); gap: clamp(24px, 6vw, 34px); }
}

/* ==========================================================================
   PRICING — /pricing

   Two things that are not on any other page: the 50/50 split between
   projects and the retainer, and the margin above a price grid.
   Everything else on the page is .price-teaser, .sec-head-left and .ticks,
   already here and already dark-safe.

   No dark block at the foot of this one, on purpose. Every colour below is
   a token that already inverts — --card-veil, --line, --ink, --body,
   --mist. The moment one of them becomes a literal, this needs one.
   ========================================================================== */

/* The page rhythm, matching /services. */
.pricing-index .section { padding-block: clamp(56px, 6.4vw, 92px); }

/* The header carries no bottom padding of its own (.page-hero-nomedia), so
   the air under the Schedule a call link is this section's to give. */
.pricing-index .pr-split-section { padding-top: clamp(46px, 5.4vw, 78px); }

/* The close bleeds up over the retainer section. 100px, the same as
   /services and /case-studies — the homepage's full bleed is tuned to the
   testimonial block and would swallow the included strip here. */
.pricing-index .start-section { --start-bleed: 100px; }

/* An anchored column must clear the floating bar, which is a pill 20px
   down from the top of the screen. Nothing on the page links to the two ids
   any more; they stay so an old /pricing#retainers link still lands. */
.pricing-index .pr-col[id] { scroll-margin-top: calc(var(--nav-float-gap) + 66px); }

/* ---------- projects | retainer ----------

   50/50 on desktop, cards stacked one per row inside each column. Each
   column is a subgrid over the split's two rows, so the two section heads
   share a row and the first card on each side starts on the same line even
   when one subheading wraps further than the other.

   At 940px and under the columns stack and each price grid goes back to its
   own responsive columns (two across, then one under 700px), because a
   single card stretched across a tablet is a very long thin box. */
.pr-split {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-template-rows: auto auto;
  column-gap: clamp(32px, 4.4vw, 64px);
  align-items: start;
}
.pr-col {
  display: grid;
  grid-row: span 2;
  grid-template-rows: subgrid;
  row-gap: 0;
  min-width: 0;
}
@media (min-width: 941px) {
  .pr-split .price-teaser { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 940px) {
  .pr-split { grid-template-columns: minmax(0, 1fr); row-gap: clamp(56px, 6.4vw, 92px); }
  .pr-col { display: block; grid-row: auto; }
}

/* ---------- the grids under a section head ---------- */
/* Was an inline style on the old page. Same number, in the stylesheet. */
.pr-grid { margin-top: clamp(22px, 2.6vw, 34px); }

/* A price cell that is a link gets the site's arrow, pinned to the bottom so
   three cells with different amounts of copy still line their links up. */
.pr-cell-go { margin-top: auto; padding-top: 10px; }
.price-teaser-cell:hover .pr-cell-go .arrow { transform: translateX(5px); }

/* A price cell that is not a link does not lift. The hover on
   .price-teaser-cell is there to answer a cursor that is about to click, and
   the three retainer cells have nowhere to go — a card that moves under the
   pointer promises a click that is not there. */
.pr-static:hover { transform: none; border-color: var(--line); }

/* ==========================================================================
   /contact

   One column, two panels, a switch above them: send a message, or book a
   call. Rebuilt 17 September. It was two embeds side by side, which stood a
   long qualification form next to a full-height calendar and left whichever
   one the visitor did not want taking half the page.

   The form is the site's own markup on the .form / .field components rather
   than a HubSpot embed. The newer embed renders inside an iframe and no rule
   in this file can reach a single field of it, which is fine for three
   fields and not fine for nine. assets/js/contact.js is where the
   submission goes.
   ========================================================================== */

/* Both panels share one measure, so the switch above them lines up with
   either. 960px is the scheduler's number: its month grid drops the date
   picker onto a second row below about that, and a two-column form is still
   comfortable at it.

   LEFT, not centred, and it was centred first. The page hero above is left
   aligned at the shell edge, and a centred 960 column started the form a
   couple of hundred pixels inboard of the headline, which read as two
   unrelated blocks rather than one page. Everything on this site is left
   aligned against the shell; this is no exception. */
.contact-wrap { max-width: 960px; }

/* THE SWITCH, and it is deliberately the same object as .res-tab on
   /resources: same plate, line, radius, type and pressed treatment. A site
   with two different row-of-choices controls has neither. It is written out
   again rather than sharing the selector so that a change to one page's
   filter cannot silently restyle the other page's tabs.

   Not .btn. A boxed button on this site means booking or requesting, and
   these two only change which panel is on screen. */
.c-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: clamp(22px, 2.6vw, 32px);
}
.c-tab {
  padding: 9px 16px;
  border-radius: var(--radius-btn);
  border: 1px solid var(--plate-line);
  background: var(--plate);
  color: var(--plate-ink);
  font-family: var(--font-body);
  font-weight: 700;
  font-size: .8rem;
  cursor: pointer;
  transition: background var(--mcard-t) var(--mcard-ease),
              border-color var(--mcard-t) var(--mcard-ease),
              color var(--mcard-t) var(--mcard-ease);
}
.c-tab:hover { border-color: var(--ink-strong); color: var(--ink-strong); }
.c-tab:focus-visible { outline: 2px solid var(--ink-strong); outline-offset: 3px; }
/* The chosen one is a literal in both directions, for the same reason
   .res-tab's is: written with --navy and --paper it comes out navy on navy
   on a dark page. */
.c-tab[aria-selected="true"] {
  background: #304157;
  border-color: #304157;
  color: #FAFAF8;
}
:root[data-theme="dark"] .c-tab[aria-selected="true"] {
  background: #FAFAF8;
  border-color: #FAFAF8;
  color: #304157;
}

/* The line above a panel. Small, and not an .h2 — the page title has
   already said what the page is, and a second heading over each panel
   competes with it. */
.c-panel-note {
  margin: 0 0 clamp(18px, 2.2vw, 26px);
  max-width: 64ch;
  font-size: .92rem;
  line-height: 1.6;
  color: var(--muted);
}

/* The hidden panel, the sent form and the error box are all switched with
   the `hidden` attribute, and three of them carry a `display` of their own
   that outranks it. Scoped rather than a global [hidden] rule, so nothing
   else on the site changes behaviour. */
.c-panel[hidden],
.form[hidden],
.form-ok[hidden],
.form-err[hidden] { display: none !important; }

/* The failure message, in the form's foot where the note was. Warm on the
   leading edge because it is the one place on the site that has to be found
   rather than read. */
.form-err {
  margin: 0;
  font-size: .92rem;
  line-height: 1.6;
  color: var(--ink);
  background: var(--plate);
  border: 1px solid var(--plate-line);
  border-left: 3px solid var(--warm);
  border-radius: var(--radius-btn);
  padding: 12px 14px;
}
.form-err a { font-weight: 700; text-decoration: underline; text-underline-offset: 3px; }

/* THE SENT STATE. It replaces the form rather than sitting above it: a form
   still on screen after it has gone invites a second copy of the same
   enquiry. Type only, no card, for the same reason the form is not in one. */
.form-ok { max-width: 64ch; }
.form-ok h2 {
  font-family: var(--font-head);
  font-size: clamp(1.3rem, 2vw, 1.7rem);
  line-height: 1.25;
  letter-spacing: -.01em;
  color: var(--ink);
  margin: 0 0 10px;
}
.form-ok h2:focus-visible { outline: 2px solid var(--ink-strong); outline-offset: 4px; }
.form-ok p { margin: 0 0 clamp(14px, 1.6vw, 20px); color: var(--body); line-height: 1.7; }
.form-ok p:last-child { margin-bottom: 0; }

/* .meetings-iframe-container is width:100% globally. Inside a panel it needs
   a floor as well: below about 320px the widget's own layout breaks before
   its column does. */
.c-panel .meetings-iframe-container { min-height: 620px; }

/* ---------- kept for /newsletter ----------
   The centred label over the newsletter opt-in, and the guards that stop
   HubSpot's own embed running wider than its column. /contact used both
   until it stopped using an embed at all; /newsletter still does. */
.contact-col-head {
  margin-bottom: clamp(16px, 1.8vw, 24px);
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.contact-col-head h2 {
  font-family: var(--font-head);
  font-size: clamp(1.22rem, 1.6vw, 1.5rem);
  line-height: 1.25;
  color: var(--ink);
  margin: 0;
}
.contact-col-head p {
  font-size: .92rem;
  line-height: 1.6;
  color: var(--muted);
  margin: 0;
}
.hs-form-frame { width: 100%; min-width: 0; }
.hs-form-frame iframe { width: 100% !important; max-width: 100% !important; }

/* ==========================================================================
   PARTNER ARTWORK  (/pipedrive)

   One piece of vendor artwork, the full width of the shell, on the card
   radius. The file is exported at 2000px for a slot that is never wider than
   about 1400, so it never scales up.

   The caption is the affiliate disclosure and it is deliberately attached to
   the picture rather than tucked into the footer: the trial button is in the
   page header, and a disclosure you have to scroll to find is not one.
   ========================================================================== */
.partner-shot { margin: 0; }
.partner-shot img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-card);
}
.partner-shot figcaption,
.partner-note {
  margin: clamp(10px, 1.2vw, 14px) 0 0;
  max-width: 72ch;
  font-size: .78rem;
  line-height: 1.65;
  color: var(--muted);
  text-wrap: pretty;
}


/* ==========================================================================
   THE PLACEHOLDER WELL ON A PANEL CARD

   The 'What you get' cards carry a picture of the deliverable. Until those
   pictures exist they carry .res-thumb + .res-thumb-ph, which is the same
   empty-artwork well the resource shelves use — the same object, not a
   second one that looks like it, so when the real images land both places
   are already the same 16:9 frame.

   The only thing that changes here is the gap under it. .pcard is a flex
   column on a 12px gap, which is the right beat between a title and a
   paragraph and too tight between a picture and a title. */
.pcard-thumb { margin-bottom: 4px; }

/* ==========================================================================
   /AUDIT'S ASIDE — the two other-CRM rows, and the example page beside them

   'Not on HubSpot?' is the lightest section on that page: a heading and two
   rows. That made the column next to it the one place with room for a
   picture of what the audit actually produces, which is the thing the page
   is selling and had nowhere to show.

   THE IMAGE CANNOT SET THE ROW'S HEIGHT. It is a portrait document in a row
   whose left column is two short rows, so uncapped it would stretch the
   section to the height of a page-shaped image. The cap is a viewport clamp
   and the figure centres inside its column, so the section stays as tall as
   the rows beside it on anything but a phone.
   ========================================================================== */
/* 65/35 rather than .split's near-even columns. The left side is two rows
   of name-plus-description and the right side is one picture, so an even
   split starved the rows: 'Something else entirely' broke to three lines
   and the descriptions beside it wrapped every few words. */
.audit-aside { grid-template-columns: minmax(0, 65fr) minmax(0, 35fr); }
.audit-aside .sec-head { margin-bottom: 0; }

/* THE TWO ROWS SHARE THEIR COLUMNS.

   .svc-row is its own grid, so on a plain list each row sizes its tracks
   independently and the `auto` link column takes the width of that row's
   own label. 'See more' and 'Tell us what you've got' are different
   lengths, so the two descriptions started and ended at different x — which
   is what read as the rows not lining up.

   Subgrid fixes it properly: the list owns the tracks and both rows inherit
   them, so the link column is one width (the longer label's) on both and
   the descriptions share an edge. The plain template above it is the
   fallback — a browser without subgrid drops the invalid declaration and
   keeps the per-row grid, which is what the page did before.

   The title floor is what holds 'Something else entirely' to two lines. It
   has to clear the width of 'Something else' at the largest the title gets,
   and .8fr alone does not until about 1500px. */
.audit-aside .svc-list-plain {
  display: grid;
  grid-template-columns: minmax(12.5em, .8fr) minmax(0, 1.45fr) max-content;
}
.audit-aside .svc-list-plain .svc-row {
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
}

/* Justified, as asked. Both descriptions run two lines at every width the
   two-column layout survives, so this sets the first line of each flush to
   both edges and leaves the second ragged — if the word spacing ever opens
   up oddly, this one declaration is the thing to drop.

   TIGHTER LEADING THAN THE LIST'S 1.6. These two sit in a narrower column
   than the rows on /hubspot and run to three lines rather than two, and at
   1.6 a justified three-line block reads as three separate lines rather
   than a paragraph. Only here: the same class on a wider row does not have
   the problem. */
.audit-aside .svc-copy {
  text-align: justify;
  text-wrap: pretty;
  line-height: 1.45;
}

/* THE LINK COLUMN IS ONE WIDTH FOR BOTH ROWS, because the list is a subgrid
   and the track is max-content — the longer of the two labels. That is what
   lines the descriptions up, and it is also why "See more" was starting at
   the same x as "Tell us what you've got" and stopping well short of the
   row's edge. Right-aligned inside the track, both CTAs end flush with it.

   Not needed on /hubspot: those rows are not a subgrid, so each one's auto
   track is exactly its own label's width and the labels already end on the
   row's right edge. */
.audit-aside .svc-go { text-align: right; }

.audit-shot-frame {
  margin: 0;
  display: flex;
  justify-content: center;
}
/* NO FRAME AND NO LETTERBOXING. The box is the image and nothing else.

   Both of those came from forcing width: 100%. A fixed width with a height
   cap means the element stays column-width while the picture shrinks inside
   it to obey the cap, and the leftover band above and below it — painted by
   the plate and outlined by the border — was the letterbox.

   width and height both auto is what removes it: the two maximums scale the
   image down in proportion and the element box shrinks to whatever comes
   out, so there is no space left over to show. The cap still holds, so a
   tall screenshot cannot stretch the row past the rows beside it.

   The plate, the border and the shadow are gone with it. The placeholder
   already draws its own white card and hairline, so they were a second
   frame around the first one; a real screenshot brings its own edges too. */
.audit-shot-frame img {
  display: block;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: clamp(260px, 30vw, 420px);
}

/* .split collapses at 1000px, but this one needs the room sooner: below
   about 1120 the 65% column cannot hold a two-line title, a description and
   a link side by side without the description wrapping every few words. */
@media (max-width: 1120px) {
  .audit-aside { grid-template-columns: minmax(0, 1fr); }
}

/* stacked by now, and shorter, because the picture is below the rows rather
   than beside them and nothing is waiting on its height */
@media (max-width: 860px) {
  .audit-shot-frame img { max-height: clamp(220px, 52vw, 320px); }
}

/* .svc-row goes to one column at 820 and the subgrid template has to go
   with it, or the stacked row keeps three tracks and the copy sits in a
   sliver. */
@media (max-width: 820px) {
  .audit-aside .svc-list-plain { display: block; }
  .audit-aside .svc-copy { text-align: left; }
  /* one column now, so there is no track to be flush with and the link goes
     back under the copy on the left with everything else */
  .audit-aside .svc-go { text-align: left; }
}

/* ==========================================================================
   PHONE LAYOUT, 13 September — James' pass on a real handset

   Same rule as the block above: everything here is a phone-width override of
   something that is right on a wide screen and wrong on a narrow one, and it
   sits at the end of the file so it wins.
   ========================================================================== */

/* ---------- the mark's caption ----------
   Hidden by default. On a wide screen the partner profile link sits beside
   the review further down /hubspot; at phone width that whole block comes
   out (see below) and the link moves up here, under the badge, which is the
   only other place on the page that is making the same claim. */
.page-hero-mark-note { display: none; }

@media (max-width: 900px) {
  /* BLOCK, NOT inline-flex. As a flex container the text became one
     anonymous item taking whatever width was left after the arrow, so it
     wrapped to two lines with the arrow parked out on the right of them.
     As a block with the arrow inline it wraps like a sentence and stays
     centred under the badge. */
  .page-hero-mark-note {
    display: block;
    max-width: 26ch;
    margin: clamp(14px, 3.4vw, 20px) auto 0;
    text-align: center;
    font-weight: 700;
    font-size: .88rem;
    line-height: 1.4;
    text-decoration: none;
    color: var(--ink-strong);
  }
  .page-hero-mark-note .arrow {
    display: inline-block;
    margin-left: 5px;
    transition: transform .3s var(--ease-out);
  }
  .page-hero-mark-note:hover .arrow { transform: translateX(5px); }

  /* THE BADGE SITS BETWEEN THE TWO SECTIONS, not against the one below it.
     In the flow at this width it is its own block, so "centred between them"
     is a real measurement: the space above it is --sec-pad, and the space
     below it is the header's own bottom padding plus the next section's top
     padding, which are each half of --sec-pad. */
  .page-hero-markhead .page-hero-media { margin-top: var(--sec-pad); }
  .page-hero-markhead { padding-bottom: calc(var(--sec-pad) / 2); }
  .page-hero-markhead + .section { padding-top: calc(var(--sec-pad) / 2); }
}

@media (max-width: 760px) {

  /* ---------- the proof heading breaks after "with" ----------
     The highlighted half goes to its own line, and both halves are nowrap so
     it can never take a third. Measured at 320px, where the head is at its
     1.7rem floor: the longer line runs about 240px inside a 280px column. */
  .case-head .sec-head .h2 { white-space: nowrap; }
  .case-head .sec-head .h2 .hl { display: block; }

  /* ---------- no line art on the featured card ----------
     A third of the card is a third of a phone screen, and the drawing was
     taking a block of height under the copy to say what the copy already
     said. The overhang was the reason it earned its space, and there is no
     overhang once it is stacked under the text. */
  .res-feature-art { display: none; }
  .res-feature { padding-bottom: clamp(26px, 3.4vw, 54px); }

  /* ---------- /hubspot: the quote only ----------
     The screenshot is a two-column web page photographed; at phone width it
     is a grey smudge with a logo on it, and the link under it duplicates the
     one now sitting under the badge at the top of the page. */
  .hs-testi-proof { display: none; }

  /* ---------- a case study's meta ----------
     The picture stays on a phone now that it is a photograph of the work
     rather than a dashed well waiting for a wordmark.

     The rule under the meta row is .cs-side's bottom border, and the first
     eyebrow was sitting straight on it. */
  .cs-side { margin-bottom: clamp(22px, 5vw, 32px); }

  /* ---------- the back link sits where every other page's does ----------
     .cs-head does not run up under the nav the way .page-hero does — the
     plate has a hard top edge — so its 74px is measured from the bar rather
     than from the top of the screen, and "All case studies" was landing 21px
     lower than "All services". 56px matches what .page-hero-nomedia's
     padding leaves once the bar's own height is taken out of it. Measured
     at 390: back link top 142 on both. */
  .cs-head { padding-top: 53px; }

  /* ---------- the mark's caption stays on one line ----------
     26ch wrapped it, and the media column's own 300px cap is what it was
     wrapping inside. The column goes full width here and the BADGE takes
     the cap instead, so the caption has the whole shell to sit on — about
     225px of text in a 280px column at the narrowest phone. */
  .page-hero-markhead .page-hero-media.is-mark { max-width: none; }
  .page-hero-markhead .page-hero-media.is-mark img { max-width: min(300px, 78%); }
  .page-hero-mark-note {
    max-width: none;
    white-space: nowrap;
    margin-top: clamp(6px, 1.6vw, 10px);
  }

  /* ---------- the header's meta row ----------
     It lays out as a wrapping row at this width, and a fourth item leaves
     one orphan on a second line under three. The service pages' fourth is
     Type, which repeats what the page you came from already said; /audit's
     is Commitment: None, which is the least load-bearing of its four. Both
     are the last child, so one rule covers them.

     `.meta-long` is the other half of the same idea, inside a value: the
     spans it wraps are the words a value can afford to lose when the row is
     tight. Today that is "business" in /audit's 2-3 business days. */
  .page-hero-svc .hero-meta > div:nth-child(4) { display: none; }
  .meta-long { display: none; }

  /* ---------- the client marquee ----------
     Fewer marks fit on a phone screen at once, so each one can afford to be
     larger than the desktop row's. One rule, so every band on the site —
     the homepage, /services, /call — matches.

     IT HAS TO BE HERE rather than in the phone block further up: that one
     sits above `.logo-run img`'s own rule and ties with it on specificity,
     so a height written there lost on order and the marks stayed at the
     desktop clamp's 61px floor. */
  .logo-run img { height: 96px; }
}

/* ==========================================================================
   HUB PAGES: THE FEATURED CASE STUDY, added 16 September
   The poster card from /case-studies with nothing on it, beside the client's
   name, the lede from its page and a link. Built by hubCase() in
   tools/build-pages.js. Tokens only, so no dark rules.
   ========================================================================== */
/* The card and its copy sit as one group centred in the shell, with equal
   space either side (16 September, second pass). The copy column has a fixed
   ceiling rather than 1fr, and the lede drops its 62ch cap so the text runs
   to the edge of that column: otherwise the column is wider than the words
   in it and the group only looks centred on paper. */
.hub-case {
  display: grid;
  grid-template-columns: clamp(260px, 30vw, 400px) minmax(0, 600px);
  justify-content: center;
  gap: clamp(28px, 5vw, 80px);
  align-items: center;
}
.hub-case .hub-case-card { width: 100%; }
/* the scrim only exists to carry the card's type, and this card has none */
.hub-case .hub-case-card::after { content: none; }
/* The label over the client's name, 17 September. Caps in the stylesheet
   rather than in the copy, and the warm accent, same as the value card
   labels. Pale on a light ground, and James' call. */
.hub-case-tag {
  display: block;
  margin-bottom: 10px;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: .78rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--warm);
}
.hub-case-copy .lede { margin-top: clamp(12px, 1.4vw, 18px); max-width: none; }
.hub-case-copy .text-link { margin-top: clamp(18px, 2vw, 26px); }
@media (max-width: 760px) {
  .hub-case { grid-template-columns: minmax(0, 1fr); }
  .hub-case .hub-case-card { max-width: 360px; }
}


/* ==========================================================================
   HOMEPAGE — ONE GAP BETWEEN EVERY SECTION (16 September)

   Every homepage section already carries --sec-pad top and bottom, but the
   space you actually see between two sections was 146px in one place and
   215px in another at a 1470 laptop. The difference was all slack inside
   components: the logo rail's hover room, the tool clump's mask padding,
   and the close sitting at two thirds of the rhythm. Each of those is
   subtracted here, so the distance from the last thing you can see in one
   section to the first thing you can see in the next is 2 x --sec-pad all
   the way down. James asked for it uniform; no section gets a small gap
   before it and a big one after.

   The one exception is the hero to the logo rail, which is still set by
   --proof-lead (100px under the stage cards) so the logos stay above the
   fold.

   Scoped to body.home because .start-section, .logo-band and the rest are
   shared with inner pages that have their own rhythm.
   ========================================================================== */
/* the rail's 12px of hover room, plus the clear margin inside the logo
   files, sits under the marks */
.home .logo-band { padding-bottom: calc(var(--sec-pad) - 17px); }
/* the clump's mask padding sits under the last row of marks (the 5px back
   is the row nudges, which lift the lowest mark off the box) */
.home .to-white-2 { padding-bottom: calc(var(--sec-pad) - clamp(18px, 2vw, 30px) + 5px); }
/* The close takes the full rhythm, not two thirds of it. 5px off the top:
   at 7rem the shout's capitals sit that much further under their box than
   a section heading's do. */
.home .start-section {
  padding-top: calc(var(--sec-pad) - 5px);
  padding-bottom: var(--sec-pad);
}

/* ABOUT: the copy centres on the picture, not on the picture's padding.
   The pair used to carry 12% bottom padding as room for the badge, and the
   grid centred the copy on that taller box, so the copy sat about 30px low
   against what you can see. The padding goes and the badge is pushed down
   by its own height instead: its solid artwork ends at 83% of the file (the
   rest is soft shadow), so 21% lands it just past the photograph's foot,
   where it was before. Collapsed, the pair sits above the copy and needs its
   room back. */
.home .about-stack { padding-bottom: 0; }
.home .about-img-front { translate: 0 21%; }
/* A heading's first ink sits a few pixels under its box and a turned
   photograph's corner sits a few pixels over it, so this section starts
   10px lower than the rest to land on the same line as a heading would. The
   badge's point hangs just past the box, so the foot gets 5px back too. */
.home .to-white-1 {
  padding-top: calc(var(--sec-pad) + 10px);
  padding-bottom: calc(var(--sec-pad) + 5px);
}
@media (max-width: 1000px) {
  .home .about-stack { padding-bottom: 12%; }
  .home .about-img-front { translate: none; }
  /* collapsed, the copy is the last thing in the section */
  .home .to-white-1 { padding-bottom: var(--sec-pad); }
}

/* ==========================================================================
   /PIPEDRIVE, rebuilt 16 September on /hubspot's layout
   ========================================================================== */

/* ---------- a mark with a file per theme ----------
   The partner badge has no colour of its own: navy on the light theme,
   white on the dark. Both files are in the markup and this picks one. They
   share one grid cell so the link's box does not change size between them. */
.page-hero-mark-link .mark-light,
.page-hero-mark-link .mark-dark { grid-area: 1 / 1; }
.page-hero-mark-link .mark-dark { display: none; }
:root[data-theme="dark"] .page-hero-mark-link .mark-light { display: none; }
:root[data-theme="dark"] .page-hero-mark-link .mark-dark  { display: block; }

/* ---------- the trial ----------
   Copy first, the co-branded banner beside it, in the grid /hubspot's review
   uses. The banner keeps Pipedrive's purple in both themes: it is their
   artwork, so it takes the same hairline and shadow the profile screenshot
   does rather than any theme rule. */
.pd-trial {
  display: grid;
  grid-template-columns: minmax(0, .92fr) minmax(0, 1.08fr);
  align-items: center;
  gap: clamp(28px, 4.4vw, 64px);
}
.pd-trial-copy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: clamp(12px, 1.4vw, 18px);
}
.pd-trial-copy .h2, .pd-trial-copy p { margin: 0; }
.pd-trial-copy .btn-row { margin-top: clamp(4px, .8vw, 10px); }
/* the partner claim, in words, beside the artwork that makes it */
.pd-trial-status { font-weight: 700; color: var(--ink); line-height: 1.5; }
.pd-trial-shot {
  display: block;
  border-radius: var(--radius-card);
  overflow: hidden;
  box-shadow: 0 0 0 1px rgba(28, 38, 53, .1), 0 22px 44px -30px rgba(28, 38, 53, .5);
  transition: transform .4s var(--ease-out), box-shadow var(--mcard-t) var(--mcard-ease);
}
.pd-trial-shot img { display: block; width: 100%; height: auto; }
.pd-trial-shot:hover {
  transform: translateY(-3px);
  box-shadow: 0 0 0 1px rgba(28, 38, 53, .14), 0 28px 54px -28px rgba(28, 38, 53, .45);
}
@media (prefers-reduced-motion: reduce) { .pd-trial-shot:hover { transform: none; } }
@media (max-width: 900px) {
  .pd-trial { grid-template-columns: minmax(0, 1fr); gap: clamp(24px, 6vw, 34px); }
}
/* SMALLER THAN THE COLUMN. The badge is a wordmark rather than a shield, and
   at the column's full width "pipedrive" set larger than the page's own H1.
   Written at this specificity to beat .page-hero-media.is-mark img. The phone
   rule further up still wins below 900px, where it is already small. */
@media (min-width: 901px) {
  .page-hero-media.is-mark .page-hero-mark-link img.mark-light,
  .page-hero-media.is-mark .page-hero-mark-link img.mark-dark { max-width: min(340px, 78%); }
}
