﻿/* ==========================================================================
   VDPL Next — Components
   Every component is a single class with a `c-` prefix and its own modifiers.
   No element selectors, no nesting deeper than one level, no raw colours.
   ========================================================================== */

/* ==========================================================================
   Buttons
   Height is fixed at 52px (40px small) — comfortably above the 44px touch
   target WCAG 2.2 asks for, and consistent enough that a row of mixed
   button types never looks ragged.
   ========================================================================== */

.c-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  min-height: var(--control-h);
  padding-inline: var(--s-8);
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  font-size: var(--t-body);
  font-weight: var(--w-semi);
  letter-spacing: -.01em;
  text-align: center;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  isolation: isolate;          /* keeps the ripple behind the label */
  transition: background var(--dur) var(--ease),
              color var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              transform var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease);
}
.c-btn:active { transform: translateY(0) scale(.985); }

.c-btn--primary { background: var(--c-accent); color: var(--c-accent-ink); }
.c-btn--primary:hover {
  background: var(--c-accent-hover);
  transform: translateY(-1px);
  box-shadow: 0 8px 24px var(--c-accent-ring);
}

.c-btn--secondary {
  background: transparent;
  color: var(--c-accent-text);
  border-color: var(--c-accent);
}
.c-btn--secondary:hover {
  background: var(--c-accent-wash);
  transform: translateY(-1px);
}

.c-btn--quiet {
  background: var(--c-surface);
  color: var(--c-ink);
  border-color: var(--c-line);
  box-shadow: var(--e-sm);
}
.c-btn--quiet:hover { border-color: var(--c-ink-3); transform: translateY(-1px); }

/* For use on dark inverted sections, where the accent alone lacks contrast. */
.c-btn--onDark { background: var(--c-invert-ink); color: var(--c-invert); }
.c-btn--onDark:hover { background: #fff; transform: translateY(-1px); }

/* Secondary action on a dark ground. Pairs with --onDark so two buttons on an
   inverted panel read as primary and alternative rather than as equals. */
/* The outline has to be visible, not merely present.

   --c-invert-line is rgba(255,255,255,.12), which is right for a hairline
   *between* two things but not for the only edge a button has: against the
   panel the second action dissolved into the background and read as plain
   text rather than as a control. A faint fill lifts it off the ground and the
   border is strong enough to close the shape. */
.c-btn--ghostDark {
  background: color-mix(in srgb, var(--c-invert-ink) 8%, transparent);
  color: var(--c-invert-ink);
  border-color: color-mix(in srgb, var(--c-invert-ink) 38%, transparent);
}
.c-btn--ghostDark:hover {
  background: color-mix(in srgb, var(--c-invert-ink) 12%, transparent);
  border-color: var(--c-invert-ink);
  transform: translateY(-1px);
}

.c-btn--sm { min-height: var(--control-h-sm); padding-inline: var(--s-5); font-size: var(--t-sm); }
.c-btn--block { width: 100%; }

.c-btn[aria-disabled="true"],
.c-btn:disabled { opacity: .45; pointer-events: none; }

/* Ripple. Injected by motion.js, removed on animation end. Purely decorative,
   so it is skipped entirely under reduced-motion. */
.c-btn__ripple {
  position: absolute;
  z-index: -1;
  border-radius: 50%;
  background: rgba(255, 255, 255, .38);
  transform: scale(0);
  pointer-events: none;
  animation: c-ripple .55s var(--ease-out) forwards;
}
.c-btn--secondary .c-btn__ripple,
.c-btn--quiet .c-btn__ripple { background: var(--c-accent-wash); }
@keyframes c-ripple { to { transform: scale(2.8); opacity: 0; } }

/* Text link with a nudging arrow. The arrow moves, not the text, so the
   line does not reflow on hover. */
.c-link {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  min-height: var(--target-min);
  color: var(--c-accent-text);
  font-weight: var(--w-medium);
}
.c-link__arrow { transition: transform var(--dur) var(--ease); }
.c-link:hover .c-link__arrow { transform: translateX(4px); }

/* ==========================================================================
   Surfaces
   ========================================================================== */

.c-card {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  padding: var(--s-8);
  background: var(--c-surface);
  border: 1px solid var(--c-line);
  border-radius: var(--r-lg);
  transition: transform var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}
.c-card--lift:hover {
  transform: translateY(-4px);
  box-shadow: var(--e-md);
  border-color: transparent;
}
.c-card--flat { background: var(--c-surface-2); border-color: transparent; }

/* ---- Case study card (template-parts/case-card.php) ----------------------
   Moved here 2026-08-13 from page-work.css, where it had lived since this
   part was built. That file only loads on /work/, the single case-study
   template and the case-study archive - but the part itself is rendered by
   eight templates, including capability, industry, solution and hire pages,
   none of which load a page stylesheet at all by design (see
   vdpl_page_style_slug()). On those pages the cover rendered with none of
   this: no aspect-ratio box, no gradient wash, the client initial at default
   text size instead of --t-d2. Found on /industries/healthcare/, where "Work
   delivered in this sector" showed a bare "D" with no card cover.

   This is the same mistake as the honeypot field and the booking-form styles
   before it: a shared component's rules written into the one page stylesheet
   that happened to be open at the time, instead of into the file every
   template actually loads. Third time on this project, now moved rather than
   duplicated again.

   No case study has a featured image in the current data, so the cover is
   typographic - a client initial on a gradient wash - sized by aspect-ratio
   rather than a fixed height so a real screenshot later changes nothing about
   the layout. */

.w-case {
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
  width: 100%;
  padding: 0;
  overflow: hidden;
  position: relative;
}

.w-case__cover {
  display: block;
  aspect-ratio: 16 / 10;
  background: var(--c-surface-2);
  border-bottom: 1px solid var(--c-line-soft);
}
.w-case__cover img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.w-case__cover--type {
  display: grid;
  place-items: center;
  background:
    radial-gradient(120% 100% at 50% 0%, var(--c-accent-wash), transparent 70%),
    var(--c-surface-2);
}
.w-case__initial {
  /* --t-d2, capped below the h1. At its old clamp(3rem, 7vw, 5rem) this hit
     80px — larger than the 64px page title — which made a decorative initial
     the biggest type on the site. */
  font-size: var(--t-d2);
  font-weight: var(--w-heavy);
  letter-spacing: -.05em;
  line-height: 1;
  color: var(--c-accent-text);
  opacity: .55;
}

.w-case__body {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  padding: 0 var(--s-6) var(--s-6);
}

.w-case__meta { color: var(--c-ink-3); }
.w-case__tech { margin-top: auto; padding-top: var(--s-2); }

/* The whole card is the target; the heading link supplies the accessible
   name. Same pattern as the capability cards, same reason. */
.w-case__body h2 a::after,
.w-case__body h3 a::after {
  content: "";
  position: absolute;
  inset: 0;
}

/* Glass. Used sparingly — only over the hero's gradient field, where there is
   something behind it worth seeing. Over a flat background it is just a
   more expensive grey. */
.c-glass {
  background: color-mix(in srgb, var(--c-surface) 68%, transparent);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border: 1px solid var(--c-line);
  border-radius: var(--r-lg);
  box-shadow: var(--e-md);
}

/* ==========================================================================
   Badges and pills
   ========================================================================== */

.c-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  min-height: 34px;
  padding-inline: var(--s-3);
  background: var(--c-surface);
  border: 1px solid var(--c-line);
  border-radius: var(--r-pill);
  box-shadow: var(--e-sm);
  font-size: .84rem;
  letter-spacing: -.008em;
  color: var(--c-ink-2);
}
.c-pill__tag {
  padding: 3px var(--s-2);
  border-radius: var(--r-pill);
  background: var(--c-accent);
  color: var(--c-accent-ink);
  font-size: .69rem;
  font-weight: var(--w-bold);
  letter-spacing: .04em;
  text-transform: uppercase;
}

/* Status uses colour AND a text label — never colour alone, which would fail
   WCAG 1.4.1 Use of Colour for anyone who cannot distinguish the hues. */
.c-status {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-1) var(--s-3);
  border-radius: var(--r-pill);
  font-size: var(--t-xs);
  font-weight: var(--w-semi);
}
.c-status--good { background: var(--c-good-wash); color: var(--c-good); }
.c-status--warn { background: var(--c-warn-wash); color: var(--c-warn); }
.c-status--bad  { background: var(--c-bad-wash);  color: var(--c-bad); }

.c-dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}
.c-dot--live {
  background: var(--c-good);
  animation: c-pulse 2.4s ease-out infinite;
}
@keyframes c-pulse {
  0%   { box-shadow: 0 0 0 0 var(--c-good-wash); }
  70%  { box-shadow: 0 0 0 9px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}

/* ==========================================================================
   Statistics
   Counters animate from 0 via motion.js. The final value is written in the
   markup, so with JS disabled the real number is what renders.
   ========================================================================== */

.c-stat { display: flex; flex-direction: column; gap: var(--s-1); }
/* --t-d3, from the scale, rather than a bespoke clamp.

   This was clamp(2rem, 3.4vw, 3.25rem) — 52px at the top, which now overshoots
   the 44px h2 above it. A figure louder than the heading that introduces it
   inverts the hierarchy. It had also drifted into three different sizes across
   the site (52 here, 34 on the home page, 40 on the technology page) for one
   component; both of those local overrides are gone with this. */
.c-stat__value {
  font-size: var(--t-d3);
  line-height: 1;
  letter-spacing: -.032em;
  font-weight: var(--w-bold);
  font-variant-numeric: tabular-nums;
}
.c-stat__label {
  font-size: var(--t-sm);
  color: var(--c-ink-3);
  letter-spacing: -.005em;
}

/* ==========================================================================
   Forms
   Labels are always visible — placeholder-as-label fails WCAG 3.3.2 and
   destroys completion rates on long B2B forms.
   ========================================================================== */

.c-field { display: flex; flex-direction: column; gap: var(--s-2); }

.c-field__label {
  font-size: var(--t-sm);
  font-weight: var(--w-semi);
  color: var(--c-ink);
  letter-spacing: -.006em;
}
.c-field__req { color: var(--c-bad); margin-inline-start: 2px; }

.c-field__hint { font-size: var(--t-sm); color: var(--c-ink-3); }

.c-input {
  width: 100%;
  min-height: var(--control-h);
  padding: var(--s-3) var(--s-4);
  background: var(--c-surface);
  color: var(--c-ink);
  border: 1px solid var(--c-line);
  border-radius: var(--r-sm);
  transition: border-color var(--dur-fast) var(--ease),
              box-shadow var(--dur-fast) var(--ease);
}
.c-input::placeholder { color: var(--c-ink-3); }
.c-input:hover { border-color: var(--c-ink-3); }
.c-input:focus {
  outline: none;
  border-color: var(--c-accent);
  box-shadow: 0 0 0 4px var(--c-accent-wash);
}
.c-input--area { min-height: 132px; padding-block: var(--s-4); }

/* Invalid state is announced by aria-invalid, mirrored visually here. */
.c-input[aria-invalid="true"] { border-color: var(--c-bad); }
.c-input[aria-invalid="true"]:focus { box-shadow: 0 0 0 4px var(--c-bad-wash); }

.c-field__error {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  font-size: var(--t-sm);
  color: var(--c-bad);
}
/* The `display: flex` above outranks the user-agent's `[hidden] {display:none}`,
   so without this an error hidden by JS stays on screen as an empty red line. */
.c-field__error[hidden] { display: none; }

/* Product availability badge.
   Three states, and they are deliberately NOT a red/amber/green traffic light:
   "in development" is an honest answer, not a failure, and colouring it as a
   warning would push whoever writes these pages towards claiming more than is
   true. Only the strongest state is tinted; the rest read as plain metadata. */
.prod-stage { align-self: flex-start; font-size: var(--t-xs); }
.prod-stage--live {
  color: var(--c-accent-text);
  border-color: color-mix(in srgb, var(--c-accent) 35%, transparent);
  background: var(--c-accent-wash);
}
.prod-stage--building { color: var(--c-ink-3); }

/* Eyebrow, then the badge beside its sentence. The row keeps the pill and the
   text side by side because the pill IS the label for that sentence; stacked,
   it repeats what the eyebrow above already said.

   `align-items: start` rather than baseline: on a baseline the 34px pill lines
   up with the first line only and floats against the middle of a two-line
   paragraph. A small top offset drops it onto the text's optical centre-line
   instead of its box top. */
.prod-stageBar {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}
.prod-stageBar__row {
  display: flex;
  align-items: start;
  gap: var(--s-4);
  flex-wrap: wrap;
}
.prod-stageBar__row .prod-stage { margin-top: 1px; }
.prod-stage__note {
  color: var(--c-ink-2);
  max-width: var(--measure);
  margin: 0;
}

/* Note tone: said when a character was filtered out as it was typed. The field
   is correct by the time this appears, so it takes the hint colour rather than
   the error one — red under a field nobody needs to fix reads as a rejection.
   Matches .c-field__hint exactly, because that is what it now is. */
.c-field__error--note { color: var(--c-ink-3); }

/* ==========================================================================
   Form shell — .e-fields / .e-row / .e-notice
   --------------------------------------------------------------------------
   These live here, NOT in a page stylesheet, and that is the whole point. Four
   template parts use them — the contact and estimate enquiry form, the careers
   application, and the booking-system walkthrough request — but the rules were
   declared three times over in page-contact.css, page-estimate.css and
   page-careers.css. Only ONE page stylesheet ever loads, so the walkthrough
   form, which loads demo.css, got no layout at all: `.e-fields` computed to
   `display: block` and `.e-row` to `grid-template-columns: none`, so every
   field stacked at full width.

   This is the third time this exact mistake has been found on this theme —
   `.e-trap` and `.c-pager` were the first two, and the honeypot one shipped a
   visible "Leave this field empty" input site-wide. If a selector appears in
   more than one template part, it belongs in this file. ========================================================================== */
/* The column that holds a heading and then the form.
   The four templates all render `<h2>Apply</h2>` (or "Tell us about the
   project") followed directly by `form.e-fields`, and nothing separated them:
   the h2 carries margin-bottom 0 and the form's own `gap` only applies between
   its fields, so on /careers/ the "Role" label sat 1px under the word "Apply".
   Sized like the gap between two fields, because that is what the heading and
   the first field are to each other here.

   Lives in this file for the same reason .e-fields does - the page stylesheets
   each declare only their own max-width, and single-vdpl_career.php loads none
   of them at all. */
.e-form {
  display: flex;
  flex-direction: column;
  gap: var(--s-6);
}

.e-fields {
  display: flex;
  flex-direction: column;
  gap: var(--s-6);

  /* Queried by .e-row below. The forms sit in containers of very different
     widths — 720px on contact, 704px on a booking page, and a grid column
     beside a 340px sidebar on estimate — so the viewport says nothing useful
     about how much room the fields actually have. */
  container-type: inline-size;
}
.e-fields fieldset { border: 0; padding: 0; }

/* Flex stretches children to the full column, which turned the submit into a
   776px bar — the only button on the site that is not a pill sized to its own
   label. The closing note under it gets the same treatment for the same
   reason: it is a line of small print, not a paragraph to justify. */
.e-fields > .c-btn { align-self: flex-start; }
.e-fields > .t-sm { max-width: var(--measure); }

/* Single column until the FORM is wide enough, not until the window is. The
   previous `@media (max-width: 640px)` measured the wrong thing: on estimate
   at a 1000px viewport the sidebar is still beside the form, so the row is
   501px and each field was 240px — a select too narrow to comfortably show
   its longest option. Same failure the CTA panel had at 470px, milder. */
.e-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s-5);
}
@container (min-width: 560px) { .e-row { grid-template-columns: 1fr 1fr; } }

/* Submission result, above the form. Colour comes from .c-status; this is
   only the block layout, which is why the two classes are used together. */
.e-notice {
  display: block;
  margin-bottom: var(--s-6);
  padding: var(--s-4) var(--s-5);
  border-radius: var(--r-md);
  font-size: var(--t-body);
  font-weight: var(--w-regular);
  line-height: var(--lh-body);
}

/* Consent checkbox beside its label. Had no rule anywhere in the theme, so
   the WhatsApp opt-in on the walkthrough form rendered as a bare input with
   the text jammed against it and no shared hit area. */
.c-check {
  display: flex;
  align-items: flex-start;
  gap: var(--s-3);
  font-size: var(--t-sm);
  line-height: var(--lh-body);
  color: var(--c-ink-2);
  cursor: pointer;
}
.c-check input[type="checkbox"] {
  flex: 0 0 auto;
  width: 1.125rem;
  height: 1.125rem;
  margin-block-start: 0.125rem;
  accent-color: var(--c-accent);
  cursor: pointer;
}
.c-check input[type="checkbox"]:focus-visible {
  outline: 3px solid var(--c-accent-ring);
  outline-offset: 2px;
}

/* Chip group — used for Budget and Timeline. Radio inputs are visually
   hidden but still focusable, so keyboard and screen-reader users get the
   native radio-group behaviour they expect. */
.c-chips { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.c-chip { position: relative; }
.c-chip__input {
  position: absolute; inset: 0;
  opacity: 0; width: 100%; height: 100%;
  margin: 0; cursor: pointer;
}
.c-chip__face {
  display: block;
  min-height: var(--target-touch);
  padding: var(--s-3) var(--s-5);
  border: 1px solid var(--c-line);
  border-radius: var(--r-pill);
  font-size: var(--t-sm);
  color: var(--c-ink-2);
  transition: border-color var(--dur-fast) var(--ease),
              background var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}
.c-chip__input:hover + .c-chip__face { border-color: var(--c-ink-3); }
.c-chip__input:checked + .c-chip__face {
  background: var(--c-accent);
  border-color: var(--c-accent);
  color: var(--c-accent-ink);
  font-weight: var(--w-medium);
}
.c-chip__input:focus-visible + .c-chip__face {
  outline: 3px solid var(--c-accent);
  outline-offset: 3px;
}

/* ==========================================================================
   Marquee
   Duplicated track, translated by half its own width, so the loop is
   seamless without measuring anything in JS. The copy is aria-hidden so
   screen readers hear the list once.
   ========================================================================== */

.c-marquee {
  overflow: hidden;
  mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.c-marquee__track {
  display: flex;
  width: max-content;
  gap: var(--s-16);
  animation: c-marquee var(--marquee-dur, 46s) linear infinite;
}
.c-marquee:hover .c-marquee__track { animation-play-state: paused; }
@keyframes c-marquee { to { transform: translateX(-50%); } }

@media (prefers-reduced-motion: reduce) {
  .c-marquee__track { animation: none; flex-wrap: wrap; width: 100%; justify-content: center; }
}

/* ==========================================================================
   Placeholder
   Marks a slot awaiting real client-supplied content. Deliberately obvious:
   an unfilled slot must never be mistaken for finished design, and must
   never be filled with an invented client name or statistic.
   ========================================================================== */

.c-placeholder {
  display: grid;
  place-items: center;
  gap: var(--s-2);
  padding: var(--s-6);
  min-height: 64px;
  border: 1px dashed var(--c-line);
  border-radius: var(--r-md);
  background: var(--c-surface-2);
  color: var(--c-ink-3);
  font-size: var(--t-sm);
  text-align: center;
}

/* ==========================================================================
   Breadcrumbs
   The separator is a CSS ::before rather than a character in the markup, so
   screen readers announce the list without reading a slash between every
   item. The list semantics carry the hierarchy on their own.
   ========================================================================== */

.c-crumbs__list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-1) var(--s-2);
  font-size: var(--t-sm);
  color: var(--c-ink-3);
}
.c-crumbs__item { display: flex; align-items: center; gap: var(--s-2); }
.c-crumbs__item + .c-crumbs__item::before {
  content: "";
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
  opacity: .45;
}
.c-crumbs a { color: inherit; }
.c-crumbs a:hover { color: var(--c-ink); }
.c-crumbs [aria-current="page"] { color: var(--c-ink-2); }

/* ==========================================================================
   Inner-page masthead
   ========================================================================== */

/* Wider than a normal section header: this one carries the page's H1, which
   is the largest type on the page and needs room to break where the words
   allow rather than after every second one. Same rem-not-ch reason as .l-head. */
.c-masthead .l-head { max-width: 54rem; }
.c-masthead .l-head .t-lead { max-width: 62ch; }
.c-masthead .l-head--center { max-width: 46rem; }
.c-masthead__actions { margin-top: var(--s-4); }

/* Below ~480px two buttons' default padding does not fit the viewport width
   side by side on every masthead that passes a second action ("Discuss a
   project" + "See our work" runs to ~420px of button) - the row wraps and
   the second button drops onto its own line. Same fix and same reasoning as
   .h-hero__actions on the home page, generalised to every inner page that
   shares this template part. */
@media (max-width: 480px) {
  .c-masthead__actions { flex-wrap: nowrap; gap: var(--s-2); }
  .c-masthead__actions .c-btn {
    flex: 1 1 0;
    padding-inline: var(--s-4);
    font-size: var(--t-sm);
  }
}

/* ==========================================================================
   Prose
   Styles the editor's output, which is the one place the design system does
   not control the markup. Scoped so these element selectors can never leak
   into a component.
   ========================================================================== */

.s-prose { max-width: var(--measure); }
.s-prose > * + * { margin-top: var(--s-5); }
/* Article headings run on their own smaller scale, not the display scale.
   An h2 at --t-d3 renders 44px, which sits right against the 56px page
   title and shouts over the paragraphs it is meant to organise. Section
   headings on a landing page and headings inside prose are different jobs. */
.s-prose h2 {
  font-size: var(--t-d3);
  line-height: 1.2;
  letter-spacing: -.02em;
  font-weight: var(--w-bold);
  margin-top: var(--s-12);
}
.s-prose h3 {
  font-size: var(--t-d4);
  line-height: 1.3;
  letter-spacing: -.015em;
  font-weight: var(--w-semi);
  margin-top: var(--s-10);
}
.s-prose h4 {
  font-size: var(--t-body);
  font-weight: var(--w-bold);
  letter-spacing: -.01em;
  margin-top: var(--s-8);
}

/* Running text in an article wants a touch more air than UI copy. */
.s-prose { line-height: 1.65; }
.s-prose h2 + p,
.s-prose h3 + p { margin-top: var(--s-4); }
.s-prose ul,
.s-prose ol { padding-inline-start: var(--s-6); }
.s-prose ul { list-style: disc; }
.s-prose ol { list-style: decimal; }
.s-prose li + li { margin-top: var(--s-2); }
/* Links in running text.

   The archive averages three links per article and its median density is one
   link per 280 words, which is conservative - so when a paragraph looks busy
   it is the styling, not the count. A full-weight accent underline turns
   three links into three interruptions.

   Colour still carries the meaning; the underline is thinned and dimmed so it
   reads as texture at rest and firms up on hover. Contrast is unchanged, so
   this is not "colour alone" under WCAG 1.4.1 - the underline is still there. */
.s-prose a {
  color: var(--c-accent-text);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: .18em;
  text-decoration-color: color-mix(in srgb, var(--c-accent) 38%, transparent);
  transition: text-decoration-color var(--dur-fast) var(--ease);
}
.s-prose a:hover { text-decoration-color: currentColor; }

/* A link inside a bold run inherited the bold and read as a shout. */
.s-prose strong a, .s-prose b a { font-weight: var(--w-medium); }
.s-prose img { border-radius: var(--r-md); }
.s-prose blockquote {
  padding-inline-start: var(--s-5);
  border-inline-start: 3px solid var(--c-accent);
  font-size: var(--t-lead);
  letter-spacing: var(--t-lead-ls);
  color: var(--c-ink-2);
}
.s-prose code {
  font-family: var(--f-mono);
  font-size: .9em;
  padding: .15em .4em;
  border-radius: var(--r-sm);
  background: var(--c-surface-2);
}
.s-prose pre {
  padding: var(--s-5);
  border-radius: var(--r-md);
  background: var(--c-surface-2);
  overflow-x: auto;
}
.s-prose pre code { padding: 0; background: none; }
.s-prose table { width: 100%; border-collapse: collapse; }
.s-prose th,
.s-prose td { padding: var(--s-3); border-bottom: 1px solid var(--c-line); text-align: start; }

/* ==========================================================================
   Search form
   ========================================================================== */

.c-search { display: flex; flex-direction: column; gap: var(--s-2); }
.c-search__row { align-items: stretch; gap: var(--s-3); }
.c-search__row .c-input { flex: 1 1 240px; }

/* Page-break links from wp_link_pages. */
.c-pagelinks { display: flex; flex-wrap: wrap; gap: var(--s-3); font-size: var(--t-sm); }

/* ==========================================================================
   Closing CTA panel
   Lives here rather than in page-home.css because every inner page ends on
   the same panel via template-parts/cta.php. The h- prefix is kept so the
   front page markup does not have to change.
   ========================================================================== */

.h-cta__panel {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-8);
  /* Was clamp(32px, 5vw, 64px). At 64px the panel stood 430px tall to carry a
     two-line heading, three lines of copy and two buttons, with the actions
     marooned in the middle of the right half. */
  padding: clamp(28px, 3.4vw, 48px);
  border-radius: var(--r-xl);

  /* One step lighter than the footer, not identical to it.

     Both were --c-invert, so the page ended with a dark panel, a 70px band of
     light page, and then a dark footer in exactly the same colour — which
     reads as a seam that failed to close rather than as two elements. Stepping
     the panel up makes the ending deliberate: light page, lighter dark, then
     the darkest block on the page. */
  background: var(--c-invert-2);
  color: var(--c-invert-ink);
  border: 1px solid var(--c-invert-line);

  /* The panel does not need the full container. At 1162px wide it put 299px of
     nothing between the copy and the buttons, so the two actions floated
     unanchored at the far right. Capping the panel pulls them back to the
     copy they belong to. */
  max-width: 1040px;
  margin-inline: auto;
}
.h-cta__copy { display: flex; flex-direction: column; gap: var(--s-3); max-width: 52ch; }
.h-cta__body { color: var(--c-invert-ink-2); }
.h-cta__actions { gap: var(--s-3); }
.h-cta__panel :focus-visible { outline-color: var(--c-invert-ink); }

/* The footer already supplies the dark ground, so the CTA panel above it needs
   no section padding beneath — otherwise the two darks are separated by a
   band of page background that reads as a mistake. */
.h-cta { padding-bottom: 0; }

/* A quieter pill for dense lists — technology chips on a card, where the
   full-size pill competes with the heading. */
.c-pill--sm {
  min-height: 26px;
  padding-inline: var(--s-2);
  font-size: var(--t-xs);
  box-shadow: none;
  background: var(--c-surface-2);
}


/* ==========================================================================
   Capability components
   Named for where they first appeared, but shared: the capability pages, the
   capabilities hub and the industry pages all render these. They started in
   page-capability.css and moved here the moment a second template needed
   them — a page loading another page's stylesheet is the alternative, and it
   breaks silently the day that page is renamed.
   ========================================================================== */

/* ---- Capability card -----------------------------------------------------
   The whole card is clickable via the ::after overlay on the heading link,
   so the hit area is the card while the accessible name stays the heading —
   wrapping the card in an anchor would put the description inside the link
   text and make it unreadable in a screen reader's links list. */

/* A permanent soft shadow rather than one that only appears on :hover -
   the border alone (var(--c-line), 10% black) reads as barely-there on a
   light ground, and touch devices never see the hover state that used to
   be the only thing lifting the card off the page. */
.cap-card { position: relative; box-shadow: var(--e-sm); }

.cap-card h3 a::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
}

/* The icon tile is the one place a category hue is allowed to show.

   It reads --cat, falling back to the accent, so every existing use of this
   component is unchanged until a template opts in by setting --cat. Crucially
   the hue stops here: the card's heading and its "Read more" stay on the
   accent, so colour never becomes a signal about what is clickable. */
.cap-card__icon {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--cat, var(--c-accent)) 12%, transparent);
  color: var(--cat, var(--c-accent-text));
}

/* Solution card summary: clamped at the top so one long lead cannot set the
   height of its row, floored at the bottom so a short one still reserves the
   same block. Without the floor the seven cards ran to two, three and four
   lines in one grid, and a row of cards that disagree about their own depth
   reads as unfinished even when every card is correct. Same device as
   .h-cap__desc on the home page. */
.sol-card__desc {
  /* 4.65em is three line boxes at the 1.55 body line height. Written as a
     plain em value rather than calc(3 * var(--lh-body) * 1em) purely because
     it is easier to read; both work. */
  min-height: 4.65em;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

@media (max-width: 760px) {
  /* The rail becomes a swipe carousel here and the column narrows, so the
     same text needs more lines. Clamping it would put the ellipsis back. */
  .sol-card__desc {
    -webkit-line-clamp: unset;
    line-clamp: unset;
    min-height: 0;
    overflow: visible;
  }
}

.cap-card__more {
  margin-top: auto;
  padding-top: var(--s-2);
  color: var(--c-accent-text);
  font-size: var(--t-sm);
}
.cap-card:hover .c-link__arrow { transform: translateX(3px); }

/* ---- Header image -------------------------------------------------------- */

.cap-head__icon {
  display: grid;
  place-items: center;
  width: 52px;
  height: 52px;
  margin-bottom: var(--s-3);
  border-radius: var(--r-md);
  background: var(--c-accent-wash);
  color: var(--c-accent-text);
}

/* ---- Approach steps ------------------------------------------------------
   Two columns rather than four across: these are paragraphs, not labels, and
   four columns of prose gives a measure nobody reads. */

.cap-steps {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s-10) var(--s-12);
  counter-reset: step;
}
@media (max-width: 800px) { .cap-steps { grid-template-columns: 1fr; gap: var(--s-8); } }

.cap-steps__item {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s-5);
  align-items: start;
  padding-top: var(--s-5);
  border-top: 1px solid var(--c-line);
}

.cap-steps__num {
  font-size: var(--t-sm);
  font-weight: var(--w-bold);
  letter-spacing: var(--t-label-ls);
  color: var(--c-accent-text);
}

.cap-steps__item p { max-width: 46ch; }

/* ---- FAQ -----------------------------------------------------------------
   Native <details>. The marker is removed and replaced with the chevron in
   the summary, so the control is one target with a visible state change and
   no JavaScript at all. */

.cap-faq { border-top: 1px solid var(--c-line); }
.cap-faq__item { border-bottom: 1px solid var(--c-line); }

.cap-faq__q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-5);
  min-height: var(--target-touch);
  padding: var(--s-5) 0;
  font-size: var(--t-lead);
  letter-spacing: var(--t-lead-ls);
  font-weight: var(--w-semi);
  cursor: pointer;
  list-style: none;
}
.cap-faq__q::-webkit-details-marker { display: none; }
.cap-faq__q:hover { color: var(--c-accent-text); }

.cap-faq__chev {
  flex: none;
  color: var(--c-ink-3);
  transition: transform var(--dur) var(--ease);
}
.cap-faq__item[open] .cap-faq__chev { transform: rotate(180deg); }

.cap-faq__a {
  max-width: var(--measure);
  padding-bottom: var(--s-6);
  color: var(--c-ink-2);
}

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

/* Full-bleed figure inside a wrap — case study covers, capability and
   industry header images, article images. One class because they are the
   same thing in four templates. */
.c-figure {
  width: 100%;
  height: auto;
  border-radius: var(--r-lg);
  box-shadow: var(--e-md);
}

/* ---- Solution fit / not-fit ---------------------------------------------
   Two columns side by side rather than the caveat tucked underneath. Saying
   where something does not fit only carries weight if it is as visible as
   the part that sells. */

.sol-fit { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-6); align-items: start; }
@media (max-width: 800px) { .sol-fit { grid-template-columns: 1fr; } }

.sol-fit__col { gap: var(--s-5); }
.sol-fit__col--no { background: var(--c-surface-2); border-color: transparent; }
.sol-fit__warn { color: var(--c-warn); margin-top: 2px; }

/* ==========================================================================
   Build-time slots
   Visible markers for artwork that has not arrived yet. Deliberately obvious
   but not ugly: they hold the exact space the real asset will occupy, so the
   page can be judged for balance before a single image exists. Switched off
   site-wide at launch via the vdpl_next_show_placeholders filter.
   ========================================================================== */

.c-slot {
  display: grid;
  place-items: center;
  gap: var(--s-2);
  border: 1px dashed var(--c-line);
  border-radius: var(--r-md);
  background:
    repeating-linear-gradient(
      -45deg,
      var(--c-surface-2) 0 10px,
      var(--c-surface-3) 10px 20px
    );
  color: var(--c-ink-3);
  text-align: center;
}

.c-slot--icon {
  width: 44px;
  height: 44px;
  border-radius: var(--r-sm);
  flex: none;
}

.c-slot--image {
  width: 100%;
  padding: var(--s-6);
  border-radius: var(--r-lg);
  align-content: center;
}
.c-slot__label { font-size: var(--t-sm); font-weight: var(--w-semi); }
.c-slot__note  { font-size: var(--t-xs); max-width: 40ch; }

/* ==========================================================================
   Section rhythm
   Alternating grounds so a long page reads as distinct sections rather than
   one continuous column. Applied to every other content section, never two
   in a row - the contrast is what does the work, not the colour itself.
   ========================================================================== */

/* Alternation is computed from the rendered page, not written into the
   templates.

   It used to be a hand-applied `--alt` class. That is correct only if every
   section always renders, and on these templates almost none of them do — a
   capability with no case studies, no proof figures and no related solutions
   drops three sections out of the middle. On /capabilities/ai-development/
   that left FOUR untinted sections running together, which is exactly the
   undifferentiated column the alternation exists to prevent.

   `:nth-of-type` counts what the browser actually received, so a section that
   did not render cannot break the rhythm. Every child of .c-main is a
   <section>, which is what makes the count reliable. */
.c-main > section:nth-of-type(even) {
  background: var(--c-surface-2);
  border-block: 1px solid var(--c-line-soft);
}

/* Two sections opt out, because they bring their own ground: the masthead
   belongs to the header above it, and the closing CTA carries a dark panel
   that a tint behind it would only muddy. */
.c-main > section.c-masthead,
.c-main > section.h-cta {
  background: transparent;
  border-block: 0;
}

/* A section header centred above a grid, for the wider full-bleed sections.
   Capped narrower than the grid beneath it so the eye finds the heading
   before it starts scanning cards. */
.l-head--wide { max-width: 58rem; }

/* Proof strip: the four signed-off numbers, on a dark ground, used to break
   up long capability and solution pages. */
.c-proof {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--s-8);
  padding: clamp(28px, 4vw, 48px);
  border-radius: var(--r-xl);
  background: var(--c-invert);
  color: var(--c-invert-ink);
}
.c-proof .c-stat__label { color: var(--c-invert-ink-2); }

/* Two-column split: copy on one side, media or a list on the other. The
   default balance is 5/7 rather than 6/6, because a column of running text
   and a column of image want different widths to look equal. */
.l-split {
  display: grid;
  grid-template-columns: 5fr 7fr;
  gap: var(--s-10) var(--s-16);
  align-items: center;
}
.l-split--reverse > :first-child { order: 2; }
@media (max-width: 900px) {
  .l-split { grid-template-columns: 1fr; gap: var(--s-8); }
  .l-split--reverse > :first-child { order: 0; }
}

/* ==========================================================================
   Spacing corrections
   ========================================================================== */

/* The proof strip is a divider between two sections, not a section of its
   own. Given full section padding it inherited the padding above AND below
   from its neighbours, leaving the panel marooned in whitespace. */
/* Half a section, so the seams either side land on the same rhythm as every
   other same-ground seam rather than reading as a hiccup. */
.c-proofSection { padding-block: calc(var(--section) * .5); }

/* Split rows whose second column is a chip list, not prose. Centring a short
   text column against a tall wrapped chip list leaves a hole above the chips. */
/* The page's own prose with an image beside it. The text column is pinned to
   --measure rather than to a fraction, because readability is the constraint
   here and the image should absorb whatever width is left over — at 1280px
   that is the ~700px that used to be blank. */
.s-introSplit {
  display: grid;
  /* Two columns that fill the row, with only the gutter between them.

     Getting here took three attempts, and the two failures are worth keeping:

       1. Image column as `1fr` with a 4/3 box. It absorbed every spare pixel —
          975px at a 1600px container — and stood 731px tall against ~380px of
          prose.
       2. Both columns capped, pair centred. That pushed the prose 215px inside
          the container, so this one section's text began where no other
          section's did.
       3. Both columns capped, `space-between`. Alignment correct, but the
          leftover 620px became a canyon down the middle of the section.

       4. Image `1fr`, prose capped at `--measure`, height capped instead of
          width. This held for a while, but at the 1600px container it still
          resolves to about 900px of picture against 561px of words, and a
          photograph that outweighs the paragraph it illustrates reads as
          decoration that got out of hand.

     So: equal halves. The prose keeps `--measure` as a cap inside its own half,
     so the line length stays readable on the widest screens and the ~160px left
     over sits at the container's right edge, where it reads as a margin. That
     is the one place spare width is allowed to go - between the columns it was
     a canyon (3), and inside the image it was a billboard (1, 4). `max-height`
     below still stops the image from becoming a tall column. */
  grid-template-columns: 1fr 1fr;
  gap: var(--s-10) var(--s-12);
  align-items: center;
}

/* The same section with no image attached.

   Equal halves are right when one of them holds a picture. With nothing to put
   there, the heading takes the left column and the words keep the right, and
   the leftover width collects under the heading - where a big line of type
   with air beneath it reads as a deliberate opening rather than as a gap.

   The prose column is capped rather than `1fr` so the text lands at the
   container's right edge on any screen: the heading column takes the slack,
   which is the one place it can go without becoming either a canyon between
   the columns or an over-long line. Aligned to the top, not centred - there is
   no image to centre against, and a heading floating at the vertical middle of
   its own paragraph looks like a mistake. */
.s-introSplit--plain {
  grid-template-columns: minmax(0, 1fr) minmax(0, var(--measure));
  align-items: start;
}

.s-introSplit--plain .s-introSplit__head { margin: 0; }

@media (max-width: 900px) {
  /* Stacked, the heading belongs directly above the words it introduces. */
  .s-introSplit--plain { grid-template-columns: 1fr; gap: var(--s-5); }
}

/* The image sits left, opposite the header image above it, so the page does
   not run a single vertical stack of pictures down one edge. Done with `order`
   rather than by reordering the markup: the prose stays first in the DOM, so a
   screen reader gets the heading and then the words. */
.s-introSplit__media { order: -1; }

/* The heading rides with the prose in the right-hand column. `align-items:
   center` on the grid centres the pair against the image as one block, which
   is what keeps the heading from floating above the image's top edge. */
.s-introSplit__body {
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
  min-width: 0;
  /* The half is the layout; the measure is the readability floor inside it.
     Without this, equal columns hand the prose 744px at the 1600px container
     and the lines get too long to track back to. */
  max-width: var(--measure);
}

.s-introSplit__title { margin: 0; }

/* Beats the inline `aspect-ratio` on the placeholder and constrains a real
   photograph the same way, so the image is a wide crop on a large screen
   rather than a tall one. */
.s-introSplit .c-slot--image,
.s-introSplit .c-figure {
  max-height: 24rem;
  border-radius: var(--r-lg);
}
.s-introSplit .c-figure {
  width: 100%;
  height: 24rem;
  object-fit: cover;
}
@media (max-width: 900px) {
  /* Stacked, the words come first — an image above the paragraph that explains
     it is a picture nobody has been told how to read yet. */
  .s-introSplit { grid-template-columns: 1fr; gap: var(--s-8); }
  .s-introSplit__media { order: 0; }
}

.l-split--top { align-items: start; }
.l-split--top .c-chips { align-content: flex-start; }

/* ==========================================================================
   Named clients on a solution page

   A list, not cards. Twenty-three cards would be the largest thing on the
   page and would say "look how many" - which is the number the client asked
   to take out. Set in three columns so the eye reads down a list rather than
   across a grid, and the count is felt without being stated.

   `auto-fill` rather than `auto-fit`: with a short list the empty tracks
   should stay empty, so five names do not stretch into three enormous
   columns. --min is the readable width of a practice name plus its
   speciality.
   ========================================================================== */
.sol-clients {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(15rem, 100%), 1fr));
  gap: var(--s-1) var(--s-8);
}

.sol-clients__item {
  border-bottom: 1px solid var(--c-line-soft);
}

/* Name over speciality, not beside it.
   Side by side they were measured wrapping on all 22 rows in a 303px column -
   "Dr Anshula Tayal Bansal" and "Paediatric pulmonology" do not share a line
   at any column width this grid produces, and a list where every row wraps
   differently reads as damage. Stacked, every row is exactly two lines. */
.sol-clients__link,
.sol-clients__item > .sol-clients__name {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-block: var(--s-3);
  color: inherit;
  text-decoration: none;
}

.sol-clients__link:hover .sol-clients__name {
  color: var(--c-accent);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.sol-clients__name {
  font-weight: var(--w-medium);
}

/* The speciality is context, not a second label competing with the name. */
.sol-clients__note {
  color: var(--c-ink-3);
  font-size: var(--t-sm);
}

/* A section that follows the alternating tint of the one before it should not
   also carry a full section's padding at that seam. */
.l-section--alt + .l-section-sm,
.l-section-sm + .c-proofSection { padding-top: var(--s-8); }

/* ==========================================================================
   FAQ, interactive
   Exclusive accordion via the native `name` attribute on <details>: opening
   one closes the others, with no JavaScript at all. Browsers without support
   simply allow several open, which is a perfectly good fallback.
   ========================================================================== */

.cap-faq__item {
  padding-inline: var(--s-5);
  border-radius: var(--r-md);
  transition: background var(--dur) var(--ease);
}
.cap-faq__item:hover { background: var(--c-surface-2); }
.cap-faq__item[open] {
  background: var(--c-surface);
  box-shadow: var(--e-sm);
}

.cap-faq__q { gap: var(--s-6); }
.cap-faq__q:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 2px; border-radius: var(--r-sm); }

/* The chevron sits in a circle that fills with the accent when open, so the
   open item is obvious at a glance rather than only by its height. */
.cap-faq__chev {
  display: block;
  width: 32px;
  height: 32px;
  padding: 7px;
  border-radius: 50%;
  background: var(--c-surface-2);
  transition: transform var(--dur) var(--ease),
              background var(--dur) var(--ease),
              color var(--dur) var(--ease);
}
.cap-faq__item[open] .cap-faq__chev {
  background: var(--c-accent);
  color: var(--c-accent-ink);
}

/* Answers fade and rise in. Cheap, and it makes the panel feel like it
   opened rather than appeared. */
.cap-faq__item[open] .cap-faq__a {
  animation: cap-faq-in var(--dur) var(--ease-out) both;
}
@keyframes cap-faq-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .cap-faq__item,
  .cap-faq__chev { transition: none; }
  .cap-faq__item[open] .cap-faq__a { animation: none; }
}

/* A card grid with a fixed column count rather than auto-fit.

   auto-fit decides the column count from available width, so eight cards land
   as 3+3+2 at one viewport and 4+4 at another. When the content is authored
   in fours, the grid should be authored in fours too - an orphan row of two
   reads as a mistake, not as a layout. */
/* Four columns, then two, then one — never three.
   Every `.l-rail--4` on this site holds exactly four items, so a three-column
   step leaves the fourth alone on a row of its own with two empty cells beside
   it. That is what it did on About, Careers, Clients, the hire template and
   the engagement models, at every width from 900 to 1200 and on any laptop
   narrower than that. Halving is the only step that keeps a set of four
   balanced. */
.l-rail--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 1200px) { .l-rail--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 600px)  { .l-rail--4 { grid-template-columns: 1fr; } }

/* ==========================================================================
   Type signatures

   Capability, industry and solution pages were rendering the same card grid
   in the same order, so three different kinds of page read as one. They now
   share components but not composition: each type gets one layout that only
   it uses, and that is what a reader recognises.

   No new colours, no new type sizes. The variation is structural, which is
   the only kind that survives a design system.
   ========================================================================== */

/* ---- CAPABILITY: bento offerings ----------------------------------------
   The first offering is the lead one, so it occupies twice the width and
   twice the height. A grid of eight identical tiles states that all eight
   matter equally, which is never true. */

.l-bento {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s-6);
}

/* The lead tile is twice as wide, one row tall. It was 2x2, which at eight
   items left it 665px tall around three lines of text - a tile that large is
   not emphasis, it is a hole. Three columns also make eight items exactly
   nine cells, so the grid ends flush with no trailing gap. */
.l-bento > :first-child { grid-column: span 2; }
.l-bento > :first-child .t-d4 { font-size: var(--t-d3); letter-spacing: var(--t-d3-ls); }
.l-bento > :first-child .cap-card__icon,
.l-bento > :first-child .c-slot--icon { width: 56px; height: 56px; }

@media (max-width: 1100px) {
  .l-bento { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .l-bento > :first-child { grid-column: span 2; grid-row: span 1; }
}
@media (max-width: 640px) {
  .l-bento { grid-template-columns: 1fr; }
  .l-bento > :first-child { grid-column: span 1; }
}

/* ---- CAPABILITY: sticky approach ----------------------------------------
   The heading pins while the four points scroll past it. Costs nothing, and
   it keeps the reader oriented in the one section that is pure prose. */

.l-sticky {
  display: grid;
  grid-template-columns: 22rem minmax(0, 1fr);
  gap: var(--s-10) var(--s-16);
  align-items: start;
}
.l-sticky__aside { position: sticky; top: calc(var(--s-20) + var(--s-4)); }
@media (max-width: 1000px) {
  .l-sticky { grid-template-columns: 1fr; gap: var(--s-8); }
  .l-sticky__aside { position: static; }
}

/* ---- INDUSTRY: constraints on a dark ground -----------------------------
   The sector constraints are the argument an industry page exists to make,
   so they get the page's one inverted block. Large index numerals, no card
   chrome - it should read as a statement, not as a feature list. */

.i-constraints {
  padding: clamp(32px, 5vw, 64px);
  border-radius: var(--r-xl);
  background: var(--c-invert);
  color: var(--c-invert-ink);
}
.i-constraints__head { max-width: 46rem; margin-bottom: var(--s-12); }
.i-constraints__head .t-eyebrow { color: var(--c-accent-text); }

.i-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s-10) var(--s-16);
}
@media (max-width: 860px) { .i-list { grid-template-columns: 1fr; gap: var(--s-8); } }

.i-item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--s-5);
  padding-top: var(--s-6);
  border-top: 1px solid var(--c-invert-line);
}
.i-item__num {
  font-size: var(--t-d4);
  font-weight: var(--w-bold);
  line-height: 1;
  color: var(--c-accent-text);
  font-variant-numeric: tabular-nums;
}
.i-item p { color: var(--c-invert-ink-2); max-width: 44ch; }

/* ---- INDUSTRY: solutions as rows ----------------------------------------
   Deliberately not cards. The industry page already spends its card budget
   on case studies, and a divided list is a different texture at no extra
   cost - which is the whole point. */

.i-rows { display: flex; flex-direction: column; }
.i-row {
  display: grid;
  grid-template-columns: auto minmax(0, 22rem) minmax(0, 1fr);
  gap: var(--s-5) var(--s-8);
  align-items: start;
  padding: var(--s-6) 0;
  border-top: 1px solid var(--c-line);
}
.i-row:last-child { border-bottom: 1px solid var(--c-line); }
.i-row p { max-width: 62ch; }
@media (max-width: 860px) {
  .i-row { grid-template-columns: auto minmax(0, 1fr); }
  .i-row > :last-child { grid-column: 2; }
}

/* ---- SOLUTION: deliverables as a checklist ------------------------------
   A solution page is answering "what exactly am I buying", and a ticked list
   answers that better than eight boxes do. */

.s-includes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 var(--s-16);
}
@media (max-width: 860px) { .s-includes { grid-template-columns: 1fr; } }

.s-include {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--s-4);
  padding: var(--s-5) 0;
  border-bottom: 1px solid var(--c-line);
}
.s-include__tick { color: var(--c-good); margin-top: 3px; flex: none; }
.s-include p { color: var(--c-ink-2); font-size: var(--t-sm); margin-top: var(--s-1); }

/* ---- SOLUTION: horizontal timeline --------------------------------------
   Four stages across, joined by a line. A vertical numbered list is what the
   capability page already uses; this says the same thing with a different
   shape, and reads as a sequence at a glance. */

.s-flow {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--s-8);
  counter-reset: stage;
}
@media (max-width: 1000px) { .s-flow { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 620px)  { .s-flow { grid-template-columns: 1fr; } }

.s-stage { position: relative; display: flex; flex-direction: column; gap: var(--s-3); padding-top: var(--s-10); }

/* The connector is drawn from the dot rightwards, and suppressed on the last
   stage and whenever the grid wraps to fewer columns. */
.s-stage::before {
  content: "";
  position: absolute;
  top: 15px;
  left: 0;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 2px solid var(--c-accent);
  background: var(--c-bg);
}
.s-stage::after {
  content: "";
  position: absolute;
  top: 30px;
  left: 32px;
  right: calc(var(--s-8) * -1);
  height: 2px;
  background: var(--c-line);
}
.s-stage:last-child::after { display: none; }
@media (max-width: 1000px) {
  .s-stage:nth-child(2n)::after { display: none; }
}
@media (max-width: 620px) {
  .s-stage::after { display: none; }
}

.s-stage__num {
  position: absolute;
  top: 22px;
  left: 11px;
  font-size: var(--t-sm);
  font-weight: var(--w-bold);
  color: var(--c-accent-text);
  line-height: 1;
}
.s-stage p { color: var(--c-ink-2); font-size: var(--t-sm); }

/* The step list inside the sticky layout is already in a narrow column, so
   it stacks rather than splitting into two of its own. */
.cap-steps--single { grid-template-columns: 1fr; gap: var(--s-8); }

/* With a 2x2 lead tile in a four-column grid, eight items leave exactly one
   empty cell at the end. Widening the last tile fills it. Scoped to the
   eight-item case on purpose: eight is the authored shape, the same reason
   .l-rail--4 exists, and a rule that guessed for every count would be a rule
   nobody could predict. */

/* ---- Masthead with media ------------------------------------------------
   Copy and image side by side, so the first screen carries the promise and
   the proof at once. Previously the image sat in a band underneath, which
   pushed the buttons below the fold and made the top of every page identical.

   Copy takes the narrower column: a paragraph and an image at equal widths
   do not read as equal weight. */

.c-masthead__grid {
  display: grid;
  /* Even split: the headline and the image carry equal weight. */
  grid-template-columns: 1fr 1fr;
  gap: var(--s-8) var(--s-10);
  align-items: center;

}
@media (max-width: 1000px) {
  .c-masthead__grid { grid-template-columns: 1fr; gap: var(--s-8); }
}

/* The head is already capped for a full-width masthead; inside the split it
   only has half the room, so the cap comes off and the column governs. */
.c-masthead--split .l-head { max-width: none; }
.c-masthead--split .l-head .t-lead { max-width: 46ch; }

.c-masthead__media {
  min-width: 0;
  /* Fills its half of the row exactly. An earlier version capped this and
     the grid, which left the hero ending 400px short of every section below
     it - the page looked broken on a wide monitor rather than compact.
     Height is controlled by the 16:9 ratio the templates pass instead. */
}
@media (max-width: 1000px) { .c-masthead__media { margin-inline: 0; max-width: none; } }
.c-masthead__media .c-figure { box-shadow: var(--e-lg); }

/* Breathing room before the section that follows. A split hero ends on an
   image, and an image butting straight into the next band reads as two
   things stuck together rather than one section ending. */
.c-masthead--split { padding-bottom: var(--section); }

/* ==========================================================================
   Ticked list
   A list where each item is prefixed by an icon. Lives here rather than in a
   page stylesheet because three templates use it - How We Work, Engagement
   Models and the Solution pages - and the third of those loads no page CSS at
   all, so the rules were simply absent there and every row stacked its icon
   above its text.
   ========================================================================== */

.p-out { display: flex; flex-direction: column; gap: var(--s-3); font-size: var(--t-sm); }
.p-out li {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--s-3);
  align-items: start;
}
.p-out__tick { color: var(--c-good); margin-top: 2px; }

/* A 3-column card grid with a fixed count, for content authored in threes or
   sixes. Same reasoning as .l-rail--4. */
.l-rail--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 1000px) { .l-rail--3 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 640px)  { .l-rail--3 { grid-template-columns: 1fr; } }

/* Two columns, for content authored in fours where the cards carry a real
   paragraph each. auto-fit gave 3 + 1 and .l-rail--4 gives four columns too
   narrow to read the description in; 2 x 2 is flush at every width. */
.l-rail--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 700px) { .l-rail--2 { grid-template-columns: 1fr; } }

/* ==========================================================================
   Client reviews
   A masonry-ish column layout rather than a carousel: several short reviews
   are readable at once, and a carousel would hide most of them behind an
   interaction while adding JavaScript neither page otherwise needs.

   Lives here rather than in page-home.css because the Clients page renders
   the same component. Duplicating it into a second page stylesheet is the
   mistake .p-out already taught us once.
   ========================================================================== */

/* A grid, not CSS columns.

   `columns: 3` flows the cards down each column before moving right, so the
   second card a visitor reads is the one *below* the first, not the one beside
   it — the order the eye takes and the order the markup declares disagree, and
   a keyboard or screen reader user gets the markup order. With five reviews it
   also packed 2/2/1 and left a column-height hole at the bottom left, which is
   what made the section look broken rather than deliberately staggered.

   A grid reads left to right, matches the markup, and puts any leftover space
   in the last row where it looks intentional. */
.h-revs {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s-4);
  align-items: stretch;
}
@media (max-width: 1100px) { .h-revs { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 700px)  { .h-revs { grid-template-columns: minmax(0, 1fr); } }

.h-rev { gap: var(--s-4); padding: var(--s-6); }

/* Attribution pinned to the bottom of the card. Quotes differ in length by
   design — they are quoted as written — so without this the avatars sat at
   five different heights across a row and the row lost its baseline. */
.h-rev__who { margin-top: auto; }
.h-rev__quote { max-width: none; font-size: var(--t-body); line-height: var(--lh-body); }
.h-rev__quote::before { content: "\201C"; }
.h-rev__quote::after  { content: "\201D"; }

.h-rev__who { display: flex; align-items: center; gap: var(--s-3); }
.h-rev__meta { display: flex; flex-direction: column; }
.h-rev__name { font-weight: var(--w-semi); letter-spacing: -.012em; }

.h-rev__avatar {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 38px; height: 38px;
  border-radius: 50%;
  background: var(--c-accent-wash);
  color: var(--c-accent-text);
  font-weight: var(--w-bold);
  font-size: .9375rem;
}


/* ==========================================================================
   Enquiry form inside the closing CTA panel
   --------------------------------------------------------------------------
   Three fields and a button on one line at desktop, stacked on a phone. The
   panel is dark, so the inputs get an explicit light treatment rather than
   inheriting the page defaults, which would render dark-on-dark.
   ========================================================================== */

/* ---- Pager ---------------------------------------------------------------
   Shared shape with the WordPress-generated pagination in index.php: every
   target clears the 44px touch minimum, which the default markup does not.

   Here rather than in page-insights.css: Insights, the category archives and
   the search results all paginate, and search.php resolves to no page
   stylesheet at all — it would have rendered bare underlined numbers. */

.c-pager { display: flex; flex-wrap: wrap; gap: var(--s-2); justify-content: center; }
.c-pager .page-numbers {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--target-touch);
  min-height: var(--target-touch);
  padding-inline: var(--s-3);
  border: 1px solid var(--c-line);
  border-radius: var(--r-sm);
  font-size: var(--t-sm);
}
.c-pager .page-numbers:hover { border-color: var(--c-accent); color: var(--c-accent-text); }
.c-pager .page-numbers.current {
  background: var(--c-accent);
  border-color: var(--c-accent);
  color: var(--c-accent-ink);
}
.c-pager .page-numbers.dots { border-color: transparent; }


/* Honeypot. Not `display:none` — some bots skip fields that are obviously
   hidden that way. Off-canvas and zero-size is harder to detect and equally
   invisible; aria-hidden and tabindex keep it away from real users.

   This lives here, not in a page stylesheet. It used to be declared three
   times, in page-contact/careers/estimate.css, which was fine while the
   honeypot only appeared on those three pages. The CTA panel now carries the
   same trap and the panel closes EVERY page, so on all the others the rule
   was simply absent and "Leave this field empty" rendered as a visible
   labelled text input at the top of the form. */
.e-trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* The form sits in one column of a panel that is capped at 1040px, so its own
   width and the viewport's have almost nothing to do with each other: the
   column measures ~470px at 1280px AND at 1680px. The breakpoint below used
   to be a viewport media query, which meant every desktop visitor got the
   four-across layout — `1fr 1fr 1fr auto` — inside 470px: three ~120px fields
   with labels wrapped onto two lines and a select too narrow to show "Choose
   one". A container query measures the thing that actually constrains the
   fields. Four across is not offered at all, because the panel cap means the
   column can never be wide enough to carry it. */
.h-cta__form { flex: 1 1 460px; min-width: 0; container-type: inline-size; }

.h-ctaForm__row {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s-3);
  align-items: end;
}
/* Two across pairs the fields as name+mobile / need+button, which fills the
   grid evenly — no stray empty cell. */
@container (min-width: 420px) { .h-ctaForm__row { grid-template-columns: 1fr 1fr; } }
@container (min-width: 700px) { .h-ctaForm__row { grid-template-columns: 1fr 1fr 1fr auto; } }

.h-ctaForm .c-field__label {
  color: var(--c-invert-ink-2);
  font-size: var(--t-xs);
}

.h-ctaForm .c-input {
  background: rgba(255, 255, 255, .06);
  border-color: var(--c-invert-line);
  color: var(--c-invert-ink);
}
.h-ctaForm .c-input::placeholder { color: var(--c-invert-ink-2); }
.h-ctaForm .c-input:focus-visible {
  border-color: var(--c-accent);
  outline: 3px solid var(--c-accent-ring);
  outline-offset: 1px;
}

/* The native select arrow is drawn in the OS text colour, which disappears on
   a dark field. Drawn here instead. */
.h-ctaForm select.c-input {
  appearance: none;
  padding-inline-end: var(--s-8);
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 18px center, right 12px center;
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
}
.h-ctaForm select.c-input option { color: var(--c-ink); background: var(--c-surface); }

/* Validation messages inside the panel.
   --c-bad is #C2321F, chosen for dark text on a light page. On the panel's own
   dark ground it is close to unreadable, so the field errors use the light
   theme's step — the same colour the dark theme already switches --c-bad to.
   Fixed here rather than by flipping --c-bad, which would also repaint the
   required-field asterisk and the status banner. */
.h-ctaForm .c-field__error { color: #FF7A6B; }
/* And the note tone needs the same treatment for the opposite reason: on the
   panel's dark ground the light-page hint grey is the one that disappears. */
.h-ctaForm .c-field__error--note { color: rgba(255, 255, 255, .68); }

.h-ctaForm__submit .c-btn { width: 100%; }
/* Stacked, the button is just another block in the column and needs the extra
   step of air to read as the end of the form rather than a fourth field. */
.h-ctaForm__submit { margin-top: var(--s-2); }
@container (min-width: 420px) { .h-ctaForm__submit { margin-top: 0; } }

.h-ctaForm__wa {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin-top: var(--s-4);
  color: var(--c-invert-ink-2);
  font-size: var(--t-sm);
  cursor: pointer;
}
.h-ctaForm__wa input { accent-color: var(--c-accent); }

.h-ctaForm__note {
  margin-top: var(--s-3);
  color: var(--c-invert-ink-2);
  font-size: var(--t-sm);
}
.h-ctaForm__note a { color: var(--c-invert-ink); text-decoration: underline; }

.h-ctaForm__done { margin: 0; }

/* ==========================================================================
   Target size

   WCAG 2.2 AA (2.5.8) wants 24x24 CSS pixels on anything you can press. A
   sweep of every page at 375px found six things under it, and every one of
   them is site-wide, because they live in the header, the footer or the CTA
   panel that appears on all 28 pages:

     .c-crumbs__item a     38x22   breadcrumb links
     .c-footer__legal a    83x18   privacy / terms / cookie
     .c-footer__links a   150x18   the footer columns
     .c-footer__social a    9x24   the "X" link - one letter, so 9px WIDE
     .h-ctaForm__wa       278x22   the WhatsApp consent label
     .h-ctaForm__note a   150x18   "Rather give more detail?"
     .co-details a        238x22   the contact page's email and phone
     .e-direct a          115x18   the careers and estimate email links

   Grown with a centred overlay rather than padding. Padding on an inline link
   inside a wrapped list re-flows the list and opens gaps between its rows;
   the overlay changes the hit area and nothing else, so not one pixel of the
   design moves. Each list was measured first - the smallest surrounding gap
   is 4px against a 32px row, and the largest growth needed is 3px a side, so
   no two targets can overlap.

   `transform` rather than `translate`: the shorthand property is newer than
   the browsers this site still has to serve.
   ========================================================================== */

.c-crumbs__item a,
.c-footer__legal a,
.c-footer__links a,
.c-footer__social a,
.h-ctaForm__note a,
.co-details a,
.co-details__map,
.e-direct a { position: relative; }

.c-crumbs__item a::after,
.c-footer__legal a::after,
.c-footer__links a::after,
.c-footer__social a::after,
.h-ctaForm__note a::after,
.co-details a::after,
.co-details__map::after,
.e-direct a::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: max(100%, 24px);
  height: max(100%, 24px);
}

/* The label is the target here - clicking it toggles the box - so the label
   is what has to clear 24px, not the 13px control inside it. The control is
   nudged up too, because a checkbox smaller than the text beside it reads as
   decoration rather than something to press. */
.h-ctaForm__wa {
  min-height: 24px;
  align-items: center;
}

.h-ctaForm__wa input[type="checkbox"] {
  width: 16px;
  height: 16px;
  flex: none;
}

/* ==========================================================================
   Cookie consent

   In components.css rather than its own file because it renders on every
   page: a separate stylesheet would be one more request on every request, and
   this is under 2KB.

   Fixed and outside the flow, so it cannot contribute to layout shift. It sits
   at the bottom on desktop and does not cover the page; a notice that holds
   the content hostage is the pattern that gets clicked through unread, which
   makes the consent it collects worth less.
   ========================================================================== */

.c-consent {
  position: fixed;
  z-index: calc(var(--z-overlay) + 1);
  left: var(--s-3);
  right: var(--s-3);
  bottom: var(--s-3);
  opacity: 0;
  transform: translateY(12px);
  transition: opacity var(--dur-fast) var(--ease), transform var(--dur) var(--ease-out);
}

.c-consent.is-in { opacity: 1; transform: none; }

.c-consent__inner {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  max-width: 980px;
  margin: 0 auto;
  padding: var(--s-4) var(--s-5);
  border: 1px solid var(--c-line);
  border-radius: var(--r-lg);
  background: var(--c-surface);
  box-shadow: var(--e-lg);
}

.c-consent__text { flex: 1 1 auto; }

.c-consent__title {
  margin: 0 0 2px;
  font-size: var(--t-body);
  font-weight: var(--w-semi);
  color: var(--c-ink);
}

.c-consent__body {
  margin: 0;
  font-size: var(--t-sm);
  line-height: var(--lh-body);
  color: var(--c-ink-2);
}

.c-consent__body a { color: var(--c-accent-text); }

/* Both actions are the same size and the same shape, and differ only in
   colour. A reject styled as a quiet link next to a filled accept button is
   not an equal choice, and consent collected that way does not count. */
.c-consent__actions {
  display: flex;
  flex: none;
  gap: var(--s-2);
}

.c-consent__btn { min-width: 9.5rem; justify-content: center; }

/* The permanent way back to the choice, for the footer and the cookie policy.
   Looks like the links beside it rather than like a control, because that is
   what it is doing there. */
.c-consent__reopen {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}

.c-consent__reopen:hover { color: var(--c-accent-text); }

@media (max-width: 720px) {
  .c-consent__inner { flex-direction: column; align-items: stretch; gap: var(--s-4); }
  .c-consent__actions { flex-direction: column-reverse; }
  .c-consent__btn { width: 100%; }

  /* Clear of the sticky action bar and the chat launcher, which both live in
     the same corner on a phone. */
  .has-actionbar .c-consent { bottom: calc(var(--control-h) + var(--s-4) + env(safe-area-inset-bottom, 0px)); }
}

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