/* =====================================================================
   QA Madness - Service template, New Design
   ---------------------------------------------------------------------
   SCOPING RULE: every selector is prefixed with `body.service-v2`, the class
   added by qm_srv_design_body_class() only when a Service has
   service_design = 'new'. Nothing here can reach a Legacy Service, the
   homepage, or any other template.

   Token values come from the Figma design variables (QA Madness new design),
   so they are authoritative rather than eyeballed. They deliberately match the
   values already used by the homepage / AI / Contact Us redesigns.

   Sections are added one at a time as they are built. Anything not yet
   redesigned still renders its original markup and is styled by the legacy
   stylesheets, which continue to load.
   ===================================================================== */

/* ---------- Fonts (self-hosted, already shipped with the theme) -------- */
@font-face {
  font-family: "GT Sectra Display";
  src: url("../../fonts/GTSectraDisplaySuper.woff2") format("woff2"),
       url("../../fonts/GTSectraDisplaySuper.woff") format("woff");
  font-weight: 400 900;
  font-style: normal;
  font-display: swap;
}

/* Noto Sans is deliberately NOT declared here.

   This file used to declare three @font-face rules pointing at
   ../fonts/NotoSans-{Regular,Medium,Bold}.ttf (1,847 KB of TrueType). They were
   DEAD DECLARATIONS and were never downloaded: the parent theme's
   sass/general/_fonts-include.scss @imports Google Fonts' Noto Sans, and
   redesign-general-styles.scss is enqueued globally and lands AFTER this
   stylesheet, so those faces win the cascade for every weight this page uses.

   Verified by measurement, not assumption: the homepage still carries the same
   three .ttf declarations in css/home.css and fetches zero .ttf files - both
   pages load 78.4 KB of fonts (GT Sectra 43 KB + one 35 KB Google variable font
   covering 300-700). Self-hosting static subsets was tried and reverted: at
   3 x 18.5 KB it is BIGGER than the single variable font Google serves.

   So the rules were removed rather than converted. Two real issues remain, both
   outside this template and both site-wide - see the review:
     - assets/fonts/NotoSans-*.ttf (2.4 MB) are unreferenced once home.css,
       ai-product-testing.css and contact-us.css drop their dead copies too;
     - the Google Fonts dependency arrives via a cross-origin @import inside a
       compiled stylesheet, which is render-blocking, invisible to the preload
       scanner, and a GDPR consideration on a site running a consent banner. */

/* ---------------------------- Design tokens --------------------------- */
:is(body.service-v2, .sv-scope) {
  /* main */
  --sv-purple:      #7D51F1;
  --sv-green:       #00CCA6;
  --sv-white:       #F9F7FF;
  --sv-black:       #211F25;
  /* The design's raised band. Was #110D1A, which was very close to but not the
     value the design actually specifies - and it left .sv-textcard-section, the
     one place #14101D was hard-coded, looking like an exception rather than the
     rule. Changed 2026-08-25: every alternating section now uses the design's
     colour and the text card stops being a special case. */
  --sv-bg-1:        #14101D;
  --sv-bg-2:        #08060D;

  /* secondary */
  --sv-purple-dark: #5336A1;
  --sv-purple-2:    #15111E;
  --sv-lilac:       #CBB9F9;
  --sv-green-light: #68DCC7;
  --sv-green-dark:  #00886F;
  --sv-green-2:     #0D1A18;
  --sv-grey-light:  #CCCBD2;
  --sv-grey-dark:   #575659;

  /* hero accent gradient (blue -> teal, sampled from the design) */
  --sv-accent-from: #3D8FCA;
  --sv-accent-to:   #00CCA6;

  /* type */
  --sv-serif: "GT Sectra Display", Georgia, "Times New Roman", serif;
  --sv-sans:  "Noto Sans", system-ui, -apple-system, "Segoe UI", sans-serif;

  /* layout */
  --sv-container: 1200px;
  --sv-radius:    16px;
  --sv-radius-lg: 24px;
  --sv-shadow:    0 20px 60px rgba(21, 17, 30, 0.5);
}

/* The redesigned page paints its own ground; the legacy stylesheets assume a
   light body on inner pages. */
body.service-v2 {
  background: var(--sv-bg-2);
}
/* Page-level defaults, body only - NOT .sv-scope.
 *
 * A scope is a component borrowed into someone else's template, and the host
 * page's typography should keep governing its own text. When these travelled to
 * .sv-scope they landed on the hub's <main class="shub sv-scope"> at (0,1,1),
 * outranking `.shub` at (0,1,0), so the whole hub took its colour and face from
 * the Service palette. That was invisible - --sv-white and --shub-white are
 * both #f9f7ff, --sv-sans and --shub-font are both Noto Sans - which is exactly
 * what made it a trap: retuning a --shub-* value would have changed nothing and
 * given no clue why.
 *
 * The borrowed components are unaffected. Their own elements set their own
 * colours from --sv-* tokens, which the token block above still puts on
 * .sv-scope; only the inherited default changes, and on a hub page it changes
 * to an identical value from the palette that page actually owns. */
body.service-v2 {
  color: var(--sv-white);
  font-family: var(--sv-sans);
}

/* Same container as the homepage and AI Product Testing redesigns
   (1200px / 19px inline padding), so Services line up with them rather than
   sitting at a slightly different width. */
/* .sv-scope is the hub pages' way in - they render the CTA component but have
   no .service-page--v2 wrapper, so without this the container had no max-width
   and the band ran the full width of the window. `:is()` reports the
   specificity of its most specific argument, so this still weighs (0,3,1),
   exactly what `body.service-v2 .service-page--v2 .sv-container` did. */
:is(body.service-v2 .service-page--v2, .sv-scope) .sv-container {
  width: 100%;
  max-width: var(--sv-container);
  margin: 0 auto;
  padding-inline: 19px;
}

/* ------------------------------- Hero --------------------------------- */
body.service-v2 .sv-hero {
  position: relative;
  overflow: hidden;
  background: var(--sv-bg-2);
  /* One full screen, matching the hub pages. MIN-height, not height: the hero
     stacks a title, a description, actions and - on some Services - a figure
     row, which on a short laptop already exceeds the viewport; a fixed height
     would crop it. 100svh rather than 100vh because `vh` on mobile is the
     LARGE viewport, the one you only get after the address bar retracts, so a
     100vh hero starts partly under the browser chrome. `dvh` also fits but
     re-lays-out as that bar hides and shows, jumping the centred content. */
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* Clears the transparent overlay header.
     Keyed to viewport WIDTH, not height: with vh the hero collapsed on wide but
     short windows (508px tall at 1920x940, against the design's 655 at 1600 -
     aspect 0.27 vs 0.41), which read as a squeezed hero. vw tracks the design,
     which is a fixed-width mock: 13vw = 208px at the 1600 it was drawn at.
     That reasoning still holds and the padding is unchanged: the min-height
     above sets how tall the BAND is, the padding still sets the gaps, and the
     heavier top value keeps the content optically centred below the header. */
  padding: clamp(150px, 13vw, 215px) 0 clamp(80px, 8.5vw, 140px);
  text-align: center;
}

/* Aurora wash: red bleeding into purple top-left, purple/blue right, green
   into the lower right. Values shared with the homepage hero. */
body.service-v2 .sv-hero::before,
body.service-v2 .sv-hero2::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(34% 30% at 2% 2%, rgba(214, 54, 84, 0.55), transparent 60%),
    radial-gradient(42% 44% at 0% 24%, rgba(120, 70, 230, 0.55), transparent 62%),
    radial-gradient(34% 40% at 100% 22%, rgba(126, 84, 250, 0.55), transparent 60%),
    radial-gradient(40% 46% at 102% 52%, rgba(60, 110, 240, 0.45), transparent 62%),
    radial-gradient(46% 50% at 96% 84%, rgba(38, 205, 158, 0.50), transparent 62%);
  filter: blur(32px);
  pointer-events: none;
  z-index: 0;
}

/* Keep the centre dark so the headline stays crisp over the wash. */
body.service-v2 .sv-hero::after,
body.service-v2 .sv-hero2::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(46% 44% at 50% 40%, rgba(8, 6, 13, 0.85) 8%, transparent 66%);
  pointer-events: none;
  z-index: 0;
}

body.service-v2 .sv-hero > .sv-container {
  position: relative;
  z-index: 1;
}

/* Shared site hero image (assets/images/hero-bg.*) - the same asset and the
   same treatment as the homepage hero: the whole frame stretched to the box,
   no blur, and the dark-centre vignette off. It is an abstract gradient, so
   showing all of it matters more than preserving its aspect ratio. */
body.service-v2 .sv-hero--image::before,
body.service-v2 .sv-hero2--image::before {
  background: var(--sv-hero-bg) center / 100% 100% no-repeat;
  background-color: var(--sv-bg-2);
  filter: none;
}
body.service-v2 .sv-hero--image::after,
body.service-v2 .sv-hero2--image::after { display: none; }

/* A per-Service upload is likely real imagery, so keep its aspect ratio and
   keep the vignette to protect the headline. */
body.service-v2 .sv-hero--photo::before {
  background: var(--sv-hero-bg) center / cover no-repeat;
  background-color: var(--sv-bg-2);
  filter: none;
}
body.service-v2 .sv-hero--photo::after {
  background: rgba(8, 6, 13, var(--sv-hero-overlay, 0.55));
}

body.service-v2 .sv-hero__title {
  font-family: var(--sv-serif);
  font-weight: 400; /* GT Sectra "Super" is already heavy */
  font-size: clamp(34px, 5vw, 60px);
  line-height: 1.06;
  letter-spacing: 0;
  color: var(--sv-white);
  margin: 0 0 28px;
  text-wrap: balance;
}

body.service-v2 .sv-hero__title-accent {
  /* blue -> teal gradient clipped to the glyphs */
  background: linear-gradient(90deg, var(--sv-accent-from), var(--sv-accent-to));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
}

body.service-v2 .sv-hero__subtitle {
  font-size: clamp(16px, 1.4vw, 20px);
  line-height: 1.4;
  color: var(--sv-grey-light);
  max-width: 720px;
  margin: 0 auto 36px;
}

body.service-v2 .sv-hero__subtitle p { margin: 0 0 8px; }
body.service-v2 .sv-hero__subtitle p:last-child { margin-bottom: 0; }

/* Explicit, so the theme's default paragraph margin cannot set this gap. */
body.service-v2 .sv-hero__cta { margin: 0 0 48px; }

/* -------------------------- Buttons (shared) --------------------------
   MOTION IS THE HOMEPAGE'S. Reproduced from css/home.css `.home-btn` so a
   Service button animates exactly like a Home page button:
     - the content is anchored left and the gap to the trailing arrow widens on
       hover, so the pill STRETCHES RIGHTWARD instead of the label drifting out
       from the centre;
     - the trailing arrow slides a further 2px as that gap opens;
     - a gradient sheen fades in over the flat fill. It is an overlay whose
       opacity animates - a flat -> gradient background swap cannot transition
       and blinks on un-hover - and it is delayed slightly so it lands just
       after the stretch finishes;
     - filled buttons DO NOT LIFT. The homepage drops the -1px translate on
       every coloured variant (it read as a shake), and every button in this
       template is a coloured variant, so the lift is simply absent here.
   Each variant supplies its own sheen colour through --sv-btn-sheen, an
   "r, g, b" triple carrying the homepage's green / purple / lilac values. */
:is(body.service-v2, .sv-scope) .sv-btn {
  --sv-btn-sheen: 255, 255, 255;
  display: inline-flex;
  align-items: center;
  /* Left-anchored: the hover gap then extends the button rightward rather than
     spreading label and arrow apart from the centre. */
  justify-content: flex-start;
  gap: 10px;
  border-radius: 999px;
  font-family: var(--sv-sans);
  font-weight: 700;
  font-size: 16px;
  line-height: 1;
  padding: 18px 32px;
  text-decoration: none;
  border: 0;
  cursor: pointer;
  position: relative;
  z-index: 0;
  overflow: hidden;
  transition: transform 0.35s ease, box-shadow 0.3s ease, opacity 0.25s ease,
              background 0.35s ease, gap 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
/* z-index:-1 keeps the sheen above the flat fill but behind the label. */
:is(body.service-v2, .sv-scope) .sv-btn::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: linear-gradient(to right,
              rgba(var(--sv-btn-sheen), 0) 35%,
              rgba(var(--sv-btn-sheen), 0.95) 100%);
  opacity: 0;
  transition: opacity 0.5s ease 0.18s;
  pointer-events: none;
}
:is(body.service-v2, .sv-scope) .sv-btn:hover::after { opacity: 1; }
:is(body.service-v2, .sv-scope) .sv-btn svg { flex: 0 0 auto; }
/* Only the TRAILING icon moves; a leading icon stays glued to the label. */
:is(body.service-v2, .sv-scope) .sv-btn svg:last-child { transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1); }
/* +16px of gap - the homepage's 8 -> 24 stretch, from this template's 10px rest. */
:is(body.service-v2, .sv-scope) .sv-btn:hover { gap: 26px; }
:is(body.service-v2, .sv-scope) .sv-btn:hover svg:last-child { transform: translateX(2px); }
:is(body.service-v2, .sv-scope) .sv-btn:focus-visible {
  outline: 2px solid var(--sv-green-light);
  outline-offset: 3px;
}

:is(body.service-v2, .sv-scope) .sv-btn--green {
  background: var(--sv-green);
  color: #05221C;
  --sv-btn-sheen: 69, 230, 186;   /* homepage .home-btn--green overlay */
}

/* Solid purple on white - the hero CTA in the design. Distinct from --lilac,
   which is the pale fill with dark ink used mid-page. */
:is(body.service-v2, .sv-scope) .sv-btn--purple {
  background: var(--sv-purple);
  color: #fff;
  --sv-btn-sheen: 185, 166, 255;  /* homepage .home-btn--purple overlay */
}
:is(body.service-v2, .sv-scope) .sv-btn--purple:focus-visible { outline-color: var(--sv-lilac); }

/* An editor-picked button colour.
   The three custom properties are set inline by qm_srv_btn_color(): the fill,
   an ink colour derived from the fill's luminance, and the sheen triple the
   hover wipe reads. Everything else - radius, padding, the gap stretch, the
   arrow nudge - stays with .sv-btn, so a custom colour changes the hue and
   nothing else. The class is added ALONGSIDE the design's own variant, so
   clearing the colour restores the original look with no other change.

   `.sv-btn.sv-btn--custom` doubles the class DELIBERATELY. Source order cannot
   be relied on here: --lilac is defined ~370 lines below this block and --tone
   ~880 below, so a single-class rule here loses to both. Measured before the
   fix - the button took the custom label and the custom sheen but stayed lilac.
   The extra class outranks every one-class variant wherever it is defined. */
:is(body.service-v2, .sv-scope) .sv-btn.sv-btn--custom {
  background: var(--sv-btn-bg);
  color: var(--sv-btn-ink);
}
/* The focus ring deliberately does NOT follow the custom colour.
   Each built-in variant tints its own ring, but every one of those hues is
   light by construction (green-light, lilac, tone-main). An editor's colour is
   not: `outline-color: var(--sv-btn-bg)` measured 1.79:1 for a navy fill
   against the page ground #08060D, failing WCAG 1.4.11 Non-text Contrast, and
   the ring sits OUTSIDE the button (offset 3px) so it is read against that
   ground, not against the fill. Inheriting the base --sv-green-light ring keeps
   it legible whatever colour is picked. */

/* ---------------------------- Trust badges ---------------------------- */
body.service-v2 .sv-hero__badges {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 18px 34px;
}

body.service-v2 .sv-badge {
  display: inline-flex;
  align-items: center;
  gap: 10px;
}
/* Each badge keeps its own designed geometry: the height attribute sets the
   rendered height per asset and width follows the intrinsic aspect ratio, so
   unlike marks are never squeezed to one global size. */
body.service-v2 .sv-badge img {
  display: block;
  width: auto;
  max-width: 100%;
}
body.service-v2 .sv-badge__text {
  font-size: 12px;
  line-height: 1.25;
  color: var(--sv-grey-light);
  text-align: left;
}

@media (max-width: 640px) {
  body.service-v2 .sv-hero { padding: 120px 0 64px; }
  body.service-v2 .sv-hero__title { margin-bottom: 20px; }
  body.service-v2 .sv-hero__cta { margin-bottom: 34px; }
  body.service-v2 .sv-hero__badges { gap: 14px 22px; }
}

/* ---- WordPress admin bar ----
   The header is position:absolute/fixed with top:0, which resolves against the
   initial containing block. html{margin-top:32px} from the admin bar does NOT
   move that, so for logged-in users the header renders UNDER the bar and the
   logo is clipped. Scoped to this template; the same fix is needed on the
   homepage, AI Product Testing and Contact Us, which share this header. */
body.service-v2.admin-bar .home-header,
body.service-v2.admin-bar .home-header.header-to-top { top: 32px; }

@media screen and (max-width: 782px) {
  /* Below 782px WordPress makes the bar 46px tall and non-fixed, so it scrolls
     away - the stuck header goes back to the viewport top. */
  body.service-v2.admin-bar .home-header { top: 46px; }
  body.service-v2.admin-bar .home-header.header-to-top,
  body.service-v2.admin-bar .home-header.is-stuck { top: 0; }
}

@media (prefers-reduced-motion: reduce) {
  :is(body.service-v2, .sv-scope) .sv-btn,
  :is(body.service-v2, .sv-scope) .sv-btn::after,
  :is(body.service-v2, .sv-scope) .sv-btn svg,
  :is(body.service-v2, .sv-scope) .sv-btn svg:last-child { transition: none; }
  :is(body.service-v2, .sv-scope) .sv-btn:hover { transform: none; gap: 10px; }
  :is(body.service-v2, .sv-scope) .sv-btn:hover svg,
  :is(body.service-v2, .sv-scope) .sv-btn:hover svg:last-child { transform: none; }
  /* The sheen is a colour change rather than motion, so it is kept. */
}

/* ---------------------------- Links (shared) --------------------------
   Two link species, both taken from the homepage:

   1. LINK WITH ARROW (.sv-card__more, .sv-project__more, .sv-panel__link,
      .sv-svc__more) - the homepage `.home-case__more` / `.home-blog__more`
      motion: the gap between label and arrow stretches by 8px on hover while
      the colour tints to the section accent. The arrow is NOT translated; the
      gap does the work, and that is what makes the homepage links read as one
      family. Each component keeps its own resting gap, so nothing shifts at
      rest - only the hover changes.

   2. PLAIN LINK (inline links inside rich text) - the homepage `.home-link`:
      colour only, 0.15s ease, nothing else animates.                      */
body.service-v2 .sv-step__text a,
body.service-v2 .sv-textcard__body a,
body.service-v2 .sv-faq__a a,
:is(body.service-v2, .sv-scope) .sv-check--consent a {
  transition: color var(--ds-dur-fast, 150ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1));
}
/* Dark sections: brighten to white, as the homepage brightens muted -> text. */
body.service-v2 .sv-step__text a:hover,
body.service-v2 .sv-textcard__body a:hover,
body.service-v2 .sv-faq__a a:hover { color: var(--sv-white); }
/* The form card is light, so its consent link tints to the form tone instead.
   An errored consent link keeps its red - that rule is later at equal weight. */
:is(body.service-v2, .sv-scope) .sv-check--consent a:hover { color: var(--sv-form-strong); }

/* ------------------------- Shared section shell ----------------------- */
:is(body.service-v2, .sv-scope) .sv-section {
  background: var(--sv-bg-2);
  padding: clamp(56px, 7vw, 96px) 0;
}

body.service-v2 .sv-section__title {
  font-family: var(--sv-sans);
  font-weight: 700;
  font-size: clamp(28px, 3.4vw, 48px);
  line-height: 1.2;
  color: var(--sv-white);
  text-align: center;
  margin: 0 0 20px;
  text-wrap: balance;
}

/* Accent half of a two-tone heading. FLAT colours, alternating purple / green
   down the page - the tone is decided by the section's POSITION, not by the
   section, and comes from qm_srv_accent_class().

   These were clipped gradients (purple->lilac, blue->teal) to echo the hero's
   headline treatment. Two things were wrong with that. The hero keeps its own
   gradient under a different class (.sv-hero__title-accent), so nothing here has
   to match it; and a gradient across a heading tail reads as two colours rather
   than one, which is exactly what made the two-line ecosystem accent look like a
   mistake. Flat also means an accent is one value an editor can name.

   The custom-colour variants (.sv-accent--faq-tone, --form-tone) were always
   flat, so the whole accent system is now one thing instead of two. An editor's
   Accent colour still overrides via qmh_accent_color_attr(), which sets `color`
   and `-webkit-text-fill-color` together - both still land, because neither is
   being fought by a clipped background any more. */
:is(body.service-v2, .sv-scope) .sv-accent--purple { color: var(--sv-purple); }
:is(body.service-v2, .sv-scope) .sv-accent--green  { color: var(--sv-green); }

body.service-v2 .sv-section__lead {
  font-size: clamp(15px, 1.3vw, 20px);
  line-height: 1.4;
  color: var(--sv-grey-light);
  text-align: center;
  max-width: 900px;
  margin: 0 auto clamp(32px, 4vw, 48px);
}
body.service-v2 .sv-section__lead p { margin: 0 0 10px; }
body.service-v2 .sv-section__lead p:last-child { margin-bottom: 0; }

/* ---------------------------- Card grid ------------------------------- */
/* 1200px container / three 390px columns / 15px gaps, per the design. */
body.service-v2 .sv-cards__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
  align-items: stretch;
}

body.service-v2 .sv-card {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 24px;
  border-radius: var(--sv-radius);
  border: 1px solid var(--sv-tone-line);
  background: var(--sv-tone-bg);
}

/* ------------------------------ Tones --------------------------------- */
/* The design runs one two-tone system across every surface: the DARK secondary
   paints fills, borders and tiles; the LIGHT secondary paints titles and
   numbers; the ground is a near-black tinted toward the tone. Declared once
   here and consumed by cards, steps and anything added later, so a tone change
   is a one-line edit rather than a sweep. */
:is(body.service-v2, .sv-scope) .sv-tone--purple {
  --sv-tone-bg:    #0E0B15;
  --sv-tone-line:  var(--sv-purple-dark);
  --sv-tone-fill:  var(--sv-purple-dark);
  --sv-tone-text:  var(--sv-lilac);
  --sv-tone-main:  var(--sv-purple);
  --sv-tone-ink:   var(--sv-white);
  --sv-tone-sheen: 185, 166, 255;  /* hover sheen for a toned button */
}
:is(body.service-v2, .sv-scope) .sv-tone--green {
  --sv-tone-bg:    #0A1012;
  --sv-tone-line:  var(--sv-green-dark);
  --sv-tone-fill:  var(--sv-green-dark);
  --sv-tone-text:  var(--sv-green-light);
  --sv-tone-main:  var(--sv-green);
  --sv-tone-ink:   #0D1A18;
  --sv-tone-sheen: 69, 230, 186;
}

body.service-v2 .sv-card__head {
  display: flex;
  align-items: center;
  gap: 12px;
}

body.service-v2 .sv-card__tile {
  flex: 0 0 48px;
  width: 48px;
  height: 48px;
  border-radius: 12px;
  background: var(--sv-tone-fill);
}
/* With an editor-picked icon the tile keeps its size and radius but drops from
   the SOLID tone fill to the translucent tint `.sv-svc__ico` already uses at
   rest. The solid fill is near-black purple/green, which would swallow the
   coloured icons this site actually has (the purple service SVGs in the media
   library); the tint keeps the tile reading as a tile while leaving artwork of
   any colour legible. No icon set = the solid block, exactly as before. */
body.service-v2 .sv-card__tile--img {
  display: grid;
  place-items: center;
  overflow: hidden;
}
body.service-v2 .sv-card.sv-tone--purple .sv-card__tile--img { background: rgba(142, 108, 255, 0.16); }
body.service-v2 .sv-card.sv-tone--green  .sv-card__tile--img { background: rgba(0, 204, 166, 0.16); }
/* The icon fills the tile. Artwork made for this slot is already tile-sized -
   the first one uploaded is exactly 48x48 - so insetting it to 28px shrank it
   for no reason. `contain` keeps the aspect ratio of anything squarer or wider,
   and the tile's own overflow clip rounds the corners to match. */
body.service-v2 .sv-card__tile--img img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

body.service-v2 .sv-card__title {
  font-family: var(--sv-sans);
  font-weight: 700;
  font-size: 18px;
  line-height: 1.25;
  color: var(--sv-tone-text);
  margin: 0;
}

body.service-v2 .sv-card__text {
  font-size: 14px;
  line-height: 22px;
  color: var(--sv-white);
}
body.service-v2 .sv-card__text p { margin: 0 0 10px; }
body.service-v2 .sv-card__text p:last-child { margin-bottom: 0; }

/* Pinned to the bottom so cards of unequal length still line up. */
body.service-v2 .sv-card__foot {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: auto;
  padding-top: 4px;
}

/* Shared tag pill. The design uses the same pill on the card grid and on the
   feature cards: dark-secondary outline, MAIN tone for the label. Only the
   padding differs between the two contexts. */
body.service-v2 .sv-tag {
  display: inline-block;
  padding: 6px 14px;
  border: 1px solid var(--sv-tone-line);
  border-radius: 999px;
  font-size: 12px;
  line-height: 1;
  color: var(--sv-tone-main);
}

body.service-v2 .sv-card__more {
  display: inline-flex;
  align-items: center;
  gap: 8px;              /* resting gap between label and arrow */
  margin-left: auto;
  font-size: 14px;
  font-weight: 700;
  color: var(--sv-white);
  text-decoration: none;
  white-space: nowrap;
  transition: gap 0.4s cubic-bezier(0.4, 0, 0.2, 1), color 0.35s ease;
}
body.service-v2 .sv-card__more svg { flex: 0 0 auto; }
/* Hover: stretch the gap so the arrow slides away, and tint to the card tone. */
body.service-v2 .sv-card__more:hover { gap: 16px; color: var(--sv-tone-text); }
body.service-v2 .sv-card__more:focus-visible {
  outline: 2px solid var(--sv-tone-text);
  outline-offset: 3px;
  border-radius: 4px;
}

@media (max-width: 1024px) {
  body.service-v2 .sv-cards__grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 700px) {
  body.service-v2 .sv-cards__grid { grid-template-columns: 1fr; }
}

@media (prefers-reduced-motion: reduce) {
  body.service-v2 .sv-card__more { transition: none; }
  body.service-v2 .sv-card__more:hover { gap: 8px; }
}

/* --------------------------- Numbered steps --------------------------- */
/* Used twice in the design with different tones: "When to Use ..." (green)
   and the testing process (purple). One component, two tone classes. */
body.service-v2 .sv-steps {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

body.service-v2 .sv-step {
  display: flex;
  align-items: stretch;
  gap: 16px;
}

/* The number block matches the panel height rather than sitting as a chip. */
body.service-v2 .sv-step__num {
  flex: 0 0 76px;
  display: grid;
  place-items: center;
  border-radius: 12px;
  background: var(--sv-tone-fill);
  color: var(--sv-tone-text);
  font-size: 20px;
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

body.service-v2 .sv-step__panel {
  flex: 1 1 auto;
  min-width: 0;
  border: 1px solid var(--sv-tone-line);
  border-radius: 12px;
  background: var(--sv-tone-bg);
  padding: 20px 24px;
}

body.service-v2 .sv-step__title {
  font-family: var(--sv-sans);
  font-size: 18px;
  font-weight: 700;
  line-height: 1.3;
  color: var(--sv-tone-text);
  margin: 0 0 8px;
}

body.service-v2 .sv-step__text {
  font-size: 16px;
  line-height: 25px;
  color: var(--sv-white);
}
body.service-v2 .sv-step__text p { margin: 0 0 14px; }
body.service-v2 .sv-step__text p:last-child { margin-bottom: 0; }
body.service-v2 .sv-step__text a { color: var(--sv-tone-text); }

@media (max-width: 640px) {
  body.service-v2 .sv-step { gap: 12px; }
  /* Keep the number beside the panel but let it shrink, so the text column
     stays readable instead of the number eating a third of the row. */
  body.service-v2 .sv-step__num { flex-basis: 56px; font-size: 17px; }
  body.service-v2 .sv-step__panel { padding: 16px 18px; }
  body.service-v2 .sv-step__text { font-size: 15px; line-height: 24px; }
}

/* ------------------------ Lilac button (shared) ----------------------- */
:is(body.service-v2, .sv-scope) .sv-btn--lilac {
  background: var(--sv-lilac);
  color: var(--sv-black);
  --sv-btn-sheen: 227, 219, 255;  /* homepage .home-btn--lilac overlay */
}
:is(body.service-v2, .sv-scope) .sv-btn--lilac:focus-visible { outline-color: var(--sv-lilac); }

/* -------------------- Feature cards (tag + title) --------------------- */
/* "Advantages of ..." (purple, left, with button) and "Why Choose ..."
   (green, centred, no button) are this one component. Sits on the lighter
   band colour, unlike the card grid and steps. */
body.service-v2 .sv-features {
  background: var(--sv-bg-1);
}

body.service-v2 .sv-features__head {
  display: flex;
  align-items: flex-end;
  gap: 32px;
  margin-bottom: clamp(32px, 4vw, 48px);
}
body.service-v2 .sv-features__intro { flex: 1 1 auto; min-width: 0; }
body.service-v2 .sv-features__btn { flex: 0 0 auto; }

/* The head owns the spacing, so the shared title/lead rules stop adding theirs. */
body.service-v2 .sv-features__intro .sv-section__lead { margin-bottom: 0; }

/* Left-aligned variant (Advantages). The centred variant needs no overrides -
   the shared section rules already centre. */
body.service-v2 .sv-features.is-left .sv-section__title,
body.service-v2 .sv-features.is-left .sv-section__lead {
  text-align: left;
  margin-left: 0;
  margin-right: 0;
}
body.service-v2 .sv-features.is-left .sv-section__lead { max-width: 880px; }

/* Centred variant (Why Choose Us).
   It STACKS rather than sitting the button beside the heading. The head is a
   flex ROW, so with a button the row centres the heading-plus-button PAIR and
   the heading itself lands off-centre - measured at 1280px, the intro's centre
   sat 163px left of the container's. That never showed before because this
   variant had no button; it can have one now, so the row has to become a
   column. The <=859px breakpoint below already did exactly this - this makes
   the behaviour the same at every width instead of only on small screens. */
body.service-v2 .sv-features.is-center .sv-features__head {
  justify-content: center;
  flex-direction: column;
  align-items: center;
}

body.service-v2 .sv-features__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
  align-items: stretch;
}

body.service-v2 .sv-feature {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 14px;
  padding: 24px;
  border: 1px solid var(--sv-tone-line);
  border-radius: var(--sv-radius);
  background: var(--sv-tone-bg);
}

body.service-v2 .sv-feature .sv-tag { padding: 9px 16px; }

body.service-v2 .sv-feature__title {
  font-family: var(--sv-sans);
  font-size: 18px;
  font-weight: 700;
  line-height: 1.3;
  color: var(--sv-white);
  margin: 0;
}

body.service-v2 .sv-feature__text {
  font-size: 14px;
  line-height: 22px;
  color: var(--sv-grey-light);
}
body.service-v2 .sv-feature__text p { margin: 0 0 10px; }
body.service-v2 .sv-feature__text p:last-child { margin-bottom: 0; }

@media (max-width: 1024px) {
  body.service-v2 .sv-features__grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 860px) {
  /* Stack the header so the button does not squeeze the heading column. */
  body.service-v2 .sv-features__head { flex-direction: column; align-items: flex-start; gap: 20px; }
  body.service-v2 .sv-features.is-center .sv-features__head { align-items: center; }
}
@media (max-width: 700px) {
  body.service-v2 .sv-features__grid { grid-template-columns: 1fr; }
}

/* ---------------- Page Builder per-section background ------------------
   Each redesigned section paints its own band, which is right by default and
   cannot express "this particular section should be the other one". The
   "Section background" control adds a wrapper around the row; the wrapper
   paints, and the section inside goes transparent so its own colour cannot sit
   on top of it.

   A row with no override emits NO wrapper, so unchanged pages produce byte-
   identical markup.

   background-COLOR, not the `background` shorthand: the shorthand would also
   clear background-image, and the hero and the CTA card carry artwork there. */
body.service-v2 .sv-flexbg--deep   { background-color: var(--sv-bg-2); }
body.service-v2 .sv-flexbg--raised { background-color: var(--sv-bg-1); }
body.service-v2 .sv-flexbg > section { background-color: transparent; }

/* ------------------------- Dark serif text card ----------------------- */
/* "What is ...?" - a rounded near-black card with a purple aurora, sitting on
   its own band. The only section whose heading comes out of editor content
   rather than its own field. */
body.service-v2 .sv-textcard-section {
  /* Same value as --sv-bg-1 since 2026-08-25; kept as the token so there is one
     place to change the raised band. */
  background: var(--sv-bg-1);
}

body.service-v2 .sv-textcard {
  position: relative;
  overflow: hidden;
  border-radius: var(--sv-radius-lg);
  background: #111113;
}

/* Purple aurora: strongest right of centre, with a softer upper-left wash,
   matching the design. Sits behind the copy. */
body.service-v2 .sv-textcard::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(58% 120% at 78% 52%, rgba(125, 81, 241, 0.55), transparent 66%),
    radial-gradient(40% 90% at 18% 22%, rgba(125, 81, 241, 0.22), transparent 70%);
  filter: blur(26px);
  pointer-events: none;
  z-index: 0;
}

/* Homepage intro-card artwork (assets/images/intro-card-bg.*) - the same export
   used behind "A passing test isn't the goal. Confidence is". Home treats that
   file as the ENTIRE card, so the CSS aurora is switched off rather than layered
   under it. */
body.service-v2 .sv-textcard--bg {
  background: var(--sv-textcard-bg) center / cover no-repeat;
  background-color: #111113;
}
body.service-v2 .sv-textcard--bg::before { display: none; }

body.service-v2 .sv-textcard__inner {
  position: relative;
  z-index: 1;
  padding: clamp(36px, 5vw, 64px) clamp(24px, 5vw, 72px);
  text-align: center;
}

body.service-v2 .sv-textcard__title {
  font-family: var(--sv-serif);
  font-weight: 400; /* GT Sectra "Super" is already heavy */
  font-size: clamp(28px, 4vw, 52px);
  line-height: 1.1;
  color: var(--sv-white);
  margin: 0 0 20px;
  text-wrap: balance;
}

body.service-v2 .sv-textcard__body {
  font-size: clamp(15px, 1.2vw, 16px);
  line-height: 25px;
  color: var(--sv-white);
  max-width: 1000px;
  margin: 0 auto;
  /* BODY COPY READS LEFT; the card's heading keeps the centring.

     .sv-textcard__inner is text-align:center, which is right for the title but
     was inherited by the prose, so every paragraph came out centred. Authors had
     been working around it one at a time with inline style="text-align:left" on
     each <p> - the tell that this is where the alignment belonged all along.
     .sv-textcard__title is a SIBLING of this block, not a child, so it is
     untouched and stays centred as designed. Those inline styles are now
     redundant, though harmless. */
  text-align: left;
}
body.service-v2 .sv-textcard__body p { margin: 0 0 14px; }
body.service-v2 .sv-textcard__body p:last-child { margin-bottom: 0; }
body.service-v2 .sv-textcard__body a { color: var(--sv-lilac); }
/* A list is a block of items, not a centred line of prose.

   The alignment now comes from the block above; what still matters here is that
   these lists must NOT be `display: inline-block`. That was the original bug: an
   inline-block shrink-wraps to its content, so the centred parent placed each
   list by its OWN width - a list of long items filled the line and looked
   left-aligned, while a list of short items became a narrow box pushed toward
   the middle. Two lists, two apparent indents, read as two nesting levels. */
body.service-v2 .sv-textcard__body ul,
body.service-v2 .sv-textcard__body ol { margin: 0 0 14px; }

/* Legacy content wraps whole headings in <strong>. Any heading that survives
   in the body copy must not be re-weighted or tinted by that. */
body.service-v2 .sv-textcard__body :is(h2, h3, h4) {
  font-family: var(--sv-serif);
  font-weight: 400;
  color: var(--sv-white);
}
body.service-v2 .sv-textcard__body :is(h2, h3, h4) strong { font-weight: inherit; color: inherit; }

/* -------------------------- Expertise panels -------------------------- */
/* Two outlined panels, each a labelled two-column list. Reuses the tone
   variables wholesale: border = tone-line, label = tone-main, ground =
   tone-bg. The legacy version of this data is an accordion; here the panels
   are always open. */
body.service-v2 .sv-expertise__grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 16px;
  align-items: stretch;
}

body.service-v2 .sv-panel {
  border: 1px solid var(--sv-tone-line);
  border-radius: 12px;
  background: var(--sv-tone-bg);
  padding: 24px;
}

body.service-v2 .sv-panel__label {
  font-family: var(--sv-sans);
  font-size: 18px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--sv-tone-main);
  margin: 0 0 18px;
}

body.service-v2 .sv-panel__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 8px 24px;
}

body.service-v2 .sv-panel__item { margin: 0; }

/* Every item label is bold; a linked one also gets an arrow. Bold used to mark
   "this one is a link", but the arrow already says that, and the two weights
   made one list look like two lists. */
body.service-v2 .sv-panel__link {
  display: inline-flex;
  align-items: center;
  gap: 8px;              /* resting gap between label and arrow */
  font-size: 14px;
  line-height: 18px;
  font-weight: 700;
  color: var(--sv-white);
  text-decoration: none;
  transition: gap 0.4s cubic-bezier(0.4, 0, 0.2, 1), color 0.35s ease;
}
body.service-v2 .sv-panel__link svg { flex: 0 0 auto; }
body.service-v2 .sv-panel__link:hover { gap: 16px; color: var(--sv-tone-main); }
body.service-v2 .sv-panel__link:focus-visible {
  outline: 2px solid var(--sv-tone-main);
  outline-offset: 3px;
  border-radius: 3px;
}

body.service-v2 .sv-panel__plain {
  display: inline-block;
  font-size: 14px;
  line-height: 18px;
  font-weight: 700;
  color: var(--sv-white);
}

@media (max-width: 860px) {
  body.service-v2 .sv-expertise__grid { grid-template-columns: 1fr; }
}
@media (max-width: 480px) {
  /* One list column once two would leave ~120px per item. */
  body.service-v2 .sv-panel__list { grid-template-columns: 1fr; }
}

@media (prefers-reduced-motion: reduce) {
  body.service-v2 .sv-panel__link { transition: none; }
  body.service-v2 .sv-panel__link:hover { gap: 8px; }
}

/* ------------------------------- FAQ ---------------------------------- */
/* Disclosure rows carrying the AI PRODUCT TESTING accordion's motion - the
   "Common Questions About AI Product Testing" component, `.ai-faq` in
   assets/styles/ai-product-testing.css. Four parts, all reproduced here:

     1. the panel opens by animating a grid row from 0fr to 1fr, so the answer's
        own height drives the reveal. The previous max-height cap meant a
        one-line answer and a ten-line one took the same 0.3s over wildly
        different distances (short answers snapped, long ones crawled) and a
        tall answer past the cap would clip. Neither can happen now;
     2. the answer fades in and drifts up 4px as the row opens, so text flows
        out rather than appearing all at once. Its padding is CONSTANT - never
        toggled with .is-open - so opening cannot snap the content box;
     3. hover lifts the toggle icon to the light tone (this is the accordion's
        only hover effect on the AI page too);
     4. an open row tints its background and takes the section colour on the
        question and the icon.

   The one deliberate departure: the icons cross-fade in a shared grid cell
   instead of the AI page's display:none swap. Same two icons, same result, no
   hard cut - and it is what this template already did.

   COLOUR is the per-Service `srv_faq_tone` setting (Green / Purple / Custom).
   The template puts `sv-faq--green|purple|custom` on the section and, for
   Custom, passes the picked hex as --sv-faq-tone. `--sv-faq-accent` is the
   strong step (open question, focus ring) and `--sv-faq-soft` the lighter one
   (icon on hover and when open) - the same two-step the AI page uses with
   #00cca6 for the question and #68dcc7 for the icon. */
body.service-v2 .sv-faq {
  /* Purple is the default: it is the colour these rows already used, so a
     Service that never opens the setting is unchanged. */
  --sv-faq-accent: var(--sv-purple);
  --sv-faq-soft:   var(--sv-lilac);
}
body.service-v2 .sv-faq--green {
  --sv-faq-accent: var(--sv-green);
  --sv-faq-soft:   var(--sv-green-light);
}
/* Custom is one picked colour, so both steps use it - there is no safe lighter
   shade to derive from an arbitrary hex. */
body.service-v2 .sv-faq--custom {
  --sv-faq-accent: var(--sv-faq-tone, var(--sv-purple));
  --sv-faq-soft:   var(--sv-faq-tone, var(--sv-lilac));
}
/* Heading accent for the custom tone; green/purple use the shared classes. */
:is(body.service-v2, .sv-scope) .sv-accent--faq-tone { color: var(--sv-faq-tone, var(--sv-purple)); }

body.service-v2 .sv-faq__list {
  border-top: 1px solid rgba(249, 247, 255, 0.05);
}

body.service-v2 .sv-faq__item {
  border-bottom: 1px solid rgba(249, 247, 255, 0.05);
  border-radius: 12px;
  transition: background var(--ds-dur, 200ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1));
}
body.service-v2 .sv-faq__item.is-open { background: rgba(255, 255, 255, 0.02); }

body.service-v2 .sv-faq__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  width: 100%;
  /* The 24px inset comes with the open-row tint: without it the tint would be a
     full-bleed band with the text flush against its edge, which is not the card
     the AI rows read as. */
  padding: 28px 24px;
  background: none;
  border: 0;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: color var(--ds-dur-slow, 250ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1));
}
body.service-v2 .sv-faq__head:focus-visible {
  outline: 2px solid var(--sv-faq-accent);
  outline-offset: -2px;
  border-radius: 4px;
}

body.service-v2 .sv-faq__q {
  font-family: var(--sv-sans);
  font-size: clamp(15px, 1.4vw, 18px);
  font-weight: 700;
  line-height: 1.35;
  color: var(--sv-white);
  transition: color var(--ds-dur-slow, 250ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1));
}
body.service-v2 .sv-faq__item.is-open .sv-faq__q { color: var(--sv-faq-accent); }
/* Hovering a CLOSED row highlights the question the same way it highlights the
   arrow, so the whole row answers the pointer rather than just its icon. Scoped
   with :not(.is-open) on purpose: an open row's question already carries the
   strong tone, and an unscoped hover rule would out-rank it on source order and
   knock it back down to the lighter one mid-read. That also gives the three
   states a legible ladder - white at rest, soft on hover, accent when open. */
body.service-v2 .sv-faq__item:not(.is-open) .sv-faq__head:hover .sv-faq__q { color: var(--sv-faq-soft); }

body.service-v2 .sv-faq__icon {
  flex: 0 0 18px;
  width: 18px;
  height: 18px;
  display: grid;
  place-items: center;
  color: var(--sv-grey-light);
  transition: color var(--ds-dur, 200ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1));
}
/* Hover raises the icon to the light tone; an open row holds it there. */
body.service-v2 .sv-faq__head:hover .sv-faq__icon,
body.service-v2 .sv-faq__item.is-open .sv-faq__icon { color: var(--sv-faq-soft); }
/* Both icons occupy the same cell so the row never shifts on toggle. */
body.service-v2 .sv-faq__icon svg { grid-area: 1 / 1; transition: opacity var(--ds-dur, 200ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1)); }
body.service-v2 .sv-faq__icon-close { opacity: 0; }
body.service-v2 .sv-faq__item.is-open .sv-faq__icon-arrow { opacity: 0; }
body.service-v2 .sv-faq__item.is-open .sv-faq__icon-close { opacity: 1; }

/* 0fr -> 1fr on the grid row: the answer's natural height is the distance, so
   every row opens at the same speed regardless of how much text it holds. */
body.service-v2 .sv-faq__body {
  display: grid;
  grid-template-rows: 0fr;
  /* `visibility: hidden` is what actually CLOSES the row. Collapsing the grid
     track to 0fr and clipping the overflow hides the answer visually, but it
     stays in the accessibility tree and its links stay in the tab order -
     measured: a link injected into a closed answer took focus with a 42x20 box
     while the row read as 0px tall. That is WCAG 2.4.3 (focus order) and 2.4.11
     (focus not obscured); a screen reader also read every closed answer aloud,
     so the disclosure did nothing for AT users.

     `visibility` is one of the few properties that animates discretely: with a
     transition delay equal to the collapse duration it flips to hidden only
     after the row has finished closing, so the fade still plays. Opening resets
     the delay to 0s so the content is available immediately. */
  visibility: hidden;
  transition: grid-template-rows 0.42s cubic-bezier(0.4, 0, 0.2, 1),
              visibility 0s linear 0.42s;
}
body.service-v2 .sv-faq__item.is-open .sv-faq__body {
  grid-template-rows: 1fr;
  visibility: visible;
  transition: grid-template-rows 0.42s cubic-bezier(0.4, 0, 0.2, 1),
              visibility 0s linear 0s;
}

/* min-height:0 is what lets a grid item shrink below its content, and
   overflow:hidden clips it while it does - without both, the row cannot
   collapse to zero and the answer stays visible when closed.
   NOTE, and a deliberate departure from the AI page: only the item's CONTENT
   is excused from the minimum by those two properties - its own padding still
   counts towards the track's min-content contribution. `.ai-faq__a-inner`
   carries `padding: 0 24px 26px`, so on that page every CLOSED row still
   reserves 26px of empty space below the question (measured: a closed item is
   100.8px for a 74px question). Harmless there because the rows are plain, but
   here it would hold the divider and the open-row tint 28px off the question.
   Vertical breathing room is therefore a ::after spacer - content, which the
   collapse does discard - and only the horizontal inset is padding. */
body.service-v2 .sv-faq__a {
  overflow: hidden;
  min-height: 0;
  padding: 0 24px;
  font-size: 15px;
  line-height: 24px;
  color: var(--sv-grey-light);
  max-width: 900px;
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity 0.3s ease, transform 0.42s cubic-bezier(0.4, 0, 0.2, 1);
}
body.service-v2 .sv-faq__item.is-open .sv-faq__a { opacity: 1; transform: translateY(0); }
body.service-v2 .sv-faq__a::after { content: ""; display: block; height: 28px; }
body.service-v2 .sv-faq__a p { margin: 0 0 12px; }
body.service-v2 .sv-faq__a p:last-child { margin-bottom: 0; }
body.service-v2 .sv-faq__a a { color: var(--sv-lilac); }
body.service-v2 .sv-faq__a :is(ul, ol) { margin: 0 0 12px; padding-left: 20px; }

@media (max-width: 640px) {
  /* The 24px inset costs real reading width on a phone, inside the container's
     own 19px padding. */
  body.service-v2 .sv-faq__head { padding: 22px 16px; }
  body.service-v2 .sv-faq__a { padding: 0 16px; }
  body.service-v2 .sv-faq__a::after { height: 22px; }
}

@media (prefers-reduced-motion: reduce) {
  body.service-v2 .sv-faq__item,
  body.service-v2 .sv-faq__head,
  body.service-v2 .sv-faq__q,
  body.service-v2 .sv-faq__icon,
  body.service-v2 .sv-faq__icon svg,
  body.service-v2 .sv-faq__body,
  body.service-v2 .sv-faq__a { transition: none; }
  /* The row still opens and the answer still appears - it just does not drift. */
  body.service-v2 .sv-faq__a { transform: none; }
  /* With no transition the visibility flip is instant in both directions, which
     is what we want here: closed rows leave the tab order the moment they close,
     open ones are reachable the moment they open. Nothing to add - noted so the
     `transition: none` above is not "simplified" into re-enabling the delay. */
}

/* --------------------------- Aurora CTA card -------------------------- */
/* Appears twice in the design - purple mid-page, green closing - as the same
   card with a different tone. */
:is(body.service-v2, .sv-scope) .sv-ctaband {
  background: var(--sv-bg-1);
}

:is(body.service-v2, .sv-scope) .sv-ctacard {
  position: relative;
  overflow: hidden;
  border-radius: var(--sv-radius-lg);
  background: #111113;
}

/* Purple across the top and upper right, green into the lower right, centre
   left dark so the headline stays legible. Values sampled from the design. */
:is(body.service-v2, .sv-scope) .sv-ctacard::before {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(46% 62% at 6% 0%, rgba(83, 54, 161, 0.90), transparent 66%),
    radial-gradient(52% 74% at 100% 6%, rgba(83, 54, 161, 0.80), transparent 68%),
    radial-gradient(46% 62% at 88% 96%, rgba(0, 136, 111, 0.85), transparent 66%);
  filter: blur(30px);
  pointer-events: none;
  z-index: 0;
}
/* Keeps the middle dark enough for the serif headline. */
:is(body.service-v2, .sv-scope) .sv-ctacard::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(44% 52% at 42% 46%, rgba(17, 17, 19, 0.86) 10%, transparent 70%);
  pointer-events: none;
  z-index: 0;
}

/* Homepage AI-CTA artwork, mirroring that card's treatment exactly:
   the image replaces both gradient layers, and it is scaled ~14% because the
   glow fades toward the file's edges - fitting it flush leaves a dark frame
   inside the rounded card. overflow:hidden on the card clips the overscan. */
:is(body.service-v2, .sv-scope) .sv-ctacard--bg::before {
  background: var(--sv-cta-bg) center / cover no-repeat;
  filter: none;
  transform: scale(1.14);
  transform-origin: center;
}
:is(body.service-v2, .sv-scope) .sv-ctacard--bg::after { display: none; }

@media (max-width: 640px) {
  /* The card is portrait on phones but the desktop artwork is landscape, so
     cropping it is aspect-fragile. Use the dedicated portrait crop at plain
     cover, no overscan - the tall card already crops in enough. */
  :is(body.service-v2, .sv-scope) .sv-ctacard--bg::before {
    background-image: var(--sv-cta-bg-mobile, var(--sv-cta-bg));
    background-size: cover;
    background-position: center;
    transform: none;
  }
}

:is(body.service-v2, .sv-scope) .sv-ctacard__inner {
  position: relative;
  z-index: 1;
  /* Vertical air raised 64 -> 88px at full width (2026-08-25, client): the card
     is the page's closing call and was reading cramped against the redesign's
     rhythm. Still clamped, so narrow screens keep a sane 40px rather than
     inheriting a desktop number. 6.9vw is what puts the curve at 88px by 1280. */
  padding: clamp(40px, 6.9vw, 88px) clamp(24px, 5vw, 56px);
  text-align: center;
}

:is(body.service-v2, .sv-scope) .sv-ctacard__title {
  font-family: var(--sv-serif);
  font-weight: 400; /* GT Sectra "Super" is already heavy */
  font-size: clamp(28px, 4.2vw, 56px);
  line-height: 1.08;
  color: var(--sv-white);
  /* 16 -> 28px: a 56px serif heading needs more under it than a body line. */
  margin: 0 0 clamp(18px, 2.2vw, 28px);
  text-wrap: balance;
}

:is(body.service-v2, .sv-scope) .sv-ctacard__text {
  font-size: clamp(15px, 1.2vw, 16px);
  line-height: 25px;
  color: var(--sv-white);
  max-width: 760px;
  /* 28 -> 40px, so the button reads as its own step rather than trailing the
     sub-line. */
  margin: 0 auto clamp(24px, 3.2vw, 40px);
}
/* Rich text: the block is a div wrapping wpautop()'s paragraphs, so the spacing
   above has to come off the inner <p>s to keep the card's rhythm unchanged. */
:is(body.service-v2, .sv-scope) .sv-ctacard__text p { margin: 0 0 10px; }
:is(body.service-v2, .sv-scope) .sv-ctacard__text p:last-child { margin-bottom: 0; }

/* ------------------- CTA 2: copy beside a contact card -----------------
   The updated design (2026-09-08) for the band that carries a named contact:
   heading and lead move left and the contact - photo, name, position and the
   booking button - becomes a bordered panel beside them.

   ONLY when there is a contact to draw. `.sv-ctacard__body` is always emitted so
   the template has one code path, but it carries no styles of its own: without
   `--split` the card is the centred one it has always been, which is what the
   closing "Start a new project" band renders (its group has no person fields).

   Everything coloured here paints from --sv-tone-main, so the "Card colour"
   control recolours the accent, the panel, the tile and the button together
   with nothing repeated per colour. */
:is(body.service-v2, .sv-scope) .sv-ctacard__inner--split {
  display: grid;
  /* 380px, not the design's 360 - see .sv-ctaperson for the 20px. minmax(0,1fr)
     for the copy, not 1fr: a long unbroken word in the lead would otherwise
     push its column past the track and squeeze the card off its measurement. */
  grid-template-columns: minmax(0, 1fr) 380px;
  gap: clamp(28px, 4vw, 64px);
  align-items: center;
  text-align: left;
}

/* The heading no longer has the full card width to breathe into, so it sits
   closer to its lead than the centred version's 28px. */
:is(body.service-v2, .sv-scope) .sv-ctacard__inner--split .sv-ctacard__title {
  margin-bottom: clamp(14px, 1.8vw, 20px);
}

/* Drops the centred card's `auto` side margins and the space it left for the
   button underneath - the button lives in the panel now. */
:is(body.service-v2, .sv-scope) .sv-ctacard__inner--split .sv-ctacard__text {
  max-width: 52ch;
  margin: 0;
}

/* 380 x 269. The design says 360 wide; the extra 20px is room for the button to
   STRETCH on hover without the label wrapping.
     "Book call with Anastasia" rests at 286 and hovers at 302
     the same with "Anastasiia" rests at 291, hovers at 307
   360 leaves a 310px content box, so those clear by 8px and 3px - and the full
   "Book a call with Anastasiia Letychivska" (326 hovered) does not clear at all.
   380 leaves 330, which fits every one of them with room spare.

   A MINIMUM height, not a fixed one: the position line is editor-typed and wraps
   to two lines at around 34 characters at this width, and a hard 269px would
   clip it. At the design's own content the natural height is 269, so the minimum
   is what you see. align-content centres the stack when the minimum is doing the
   work. */
:is(body.service-v2, .sv-scope) .sv-ctaperson {
  box-sizing: border-box;
  width: 380px;
  max-width: 100%;
  min-height: 269px;
  border: 1px solid var(--sv-tone-main);
  border-radius: 20px;
  /* Over the aurora, not instead of it: a flat panel would hide the glow the
     card is built on, and pure transparency leaves the border floating. */
  background: rgba(8, 6, 13, 0.42);
  padding: 24px;
  text-align: center;
  display: grid;
  justify-items: center;
  align-content: center;
  /* 10px, not a rounder 12: with the 88px photo, the two text line boxes and a
     52px button, three 10px gaps are what land the card on its 269px. */
  gap: 10px;
}

:is(body.service-v2, .sv-scope) .sv-ctaperson__photo {
  width: 88px;
  height: 88px;
  border-radius: 12px;
  /* NO FILL BEHIND THE PHOTO (2026-09-08, client). It used to paint
     --sv-tone-main, on the reading that the design's portraits are cut-outs
     sitting on a coloured tile. They are not: the uploads are ordinary opaque
     photos, so the fill was invisible on those and showed as a purple square
     around the subject on the one transparent PNG - the opposite of the intent.
     With no fill, a cut-out sits on the card itself and a normal photo is
     unchanged. cover still keeps a non-square upload from distorting. */
  object-fit: cover;
  display: block;
}

:is(body.service-v2, .sv-scope) .sv-ctaperson__name {
  margin: 0;
  font-size: clamp(17px, 1.5vw, 20px);
  font-weight: 700;
  line-height: 1.2;
  color: var(--sv-tone-main);
}

:is(body.service-v2, .sv-scope) .sv-ctaperson__role {
  margin: 0;
  font-size: 15px;
  font-weight: 700;
  line-height: 1.35;
  color: var(--sv-white);
}

/* WIDTH FOLLOWS THE LABEL, which carries the contact's name ("Book call with
   Anastasia") and so changes length with the person. The base .sv-btn is an
   inline-flex that already sizes to its content, so this only has to not fight
   it - an earlier full-width rule did, and made every button the same width
   whatever the name.

   max-width is the backstop for a name longer than the card was widened for:
   past 330px the label wraps and the card grows on its min-height rather than
   the button bursting the panel. The arrow never shrinks, so it cannot be
   squeezed into the text. */
:is(body.service-v2, .sv-scope) .sv-ctaperson .sv-btn {
  max-width: 100%;
  /* The base button anchors left so its hover extends the gap rightward; inside
     the card the label is centred, which is what the design shows. */
  justify-content: center;
  text-align: center;
  line-height: 1.25;
}

:is(body.service-v2, .sv-scope) .sv-ctaperson .sv-btn svg { flex-shrink: 0; }

/* A custom "Card colour" sets --sv-tone-main on the section; the two presets
   are the tone classes, which set it themselves. The built-in accent classes
   are flat per-colour rules, so the custom case needs its own. */
:is(body.service-v2, .sv-scope) .sv-accent--tone { color: var(--sv-tone-main); }

@media (max-width: 900px) {
  /* One column, and back to centred: at this width the panel is as wide as the
     copy, so left-aligned text beside nothing reads as a mistake. */
  /* minmax(0, 1fr), not 1fr: the card asks for 360px, which a bare `1fr` takes
     as the track's minimum and grows the track to - overflowing the card on any
     phone narrower than 360 plus the inner padding, with the card's own
     max-width powerless because 100% of an overgrown track is still 360.
     minmax(0, ...) lets the track shrink, and the max-width then bites. */
  :is(body.service-v2, .sv-scope) .sv-ctacard__inner--split {
    grid-template-columns: minmax(0, 1fr);
    gap: clamp(28px, 5vw, 40px);
    text-align: center;
  }
  :is(body.service-v2, .sv-scope) .sv-ctacard__inner--split .sv-ctacard__text {
    max-width: 60ch;
    margin: 0 auto;
  }
  /* Centring only. The card's own `width: 360px; max-width: 100%` already stops
     it stretching to a tablet card's full width and shrinks it on a phone; a
     stacked grid item would otherwise sit against the left edge. */
  :is(body.service-v2, .sv-scope) .sv-ctaperson {
    margin: 0 auto;
  }

  /* The button's 32px sides are what tip "Book call with <name>" onto a second
     line once the card has shrunk below its 380px - by about 7px on a 375px
     phone. 24px buys that back, and a longer name still wraps rather than
     overflowing. */
  :is(body.service-v2, .sv-scope) .sv-ctaperson .sv-btn {
    padding-left: 24px;
    padding-right: 24px;
  }

  /* THE STRETCH IS THE ONE THING THAT CANNOT COME ALONG. Below the stack the
     card is capped by its container rather than by 380px - about 327px on a
     375px phone - so the 20px of slack the stretch needs is not there, and a
     hovered button would wrap exactly as it did before the card was widened.
     The arrow travels on a transform here instead: painted, not laid out, so
     the button cannot widen and there is nothing to wrap. Hover is a pointer
     interaction anyway, and a phone-width window is the least likely place to
     have one. */
  :is(body.service-v2, .sv-scope) .sv-ctaperson .sv-btn--tone:hover svg:last-child {
    margin-left: 0;
    transform: translateX(16px);
  }
}

/* Tone-filled button: the main tone as the ground, with per-tone ink so the
   label keeps contrast on both. */
:is(body.service-v2, .sv-scope) .sv-btn--tone {
  background: var(--sv-tone-main);
  color: var(--sv-tone-ink);
  --sv-btn-sheen: var(--sv-tone-sheen, 185, 166, 255);
}
:is(body.service-v2, .sv-scope) .sv-btn--tone:focus-visible { outline-color: var(--sv-tone-main); }

/* THE BUTTON STRETCHES ON HOVER: the arrow's margin grows and takes the box
   with it, so the whole button widens rather than the arrow sliding about
   inside a fixed one. Layout, deliberately - it is the effect the design asks
   for, and the CTA 2 contact card is sized to leave room for it (see
   .sv-ctaperson) rather than the button being held still to fit the card.

   The gap stays fixed and the stretch comes from the trailing arrow alone, so
   the label does not drift away from the left padding as the button grows.

   (This block used to explain itself by the LEADING calendar icon, which the
   design dropped on 2026-09-08 - the label and arrow are all that is left, so
   holding the gap fixed is now only about where the growth comes from.) */
:is(body.service-v2, .sv-scope) .sv-btn--tone:hover { gap: 10px; }
:is(body.service-v2, .sv-scope) .sv-btn--tone svg:last-child {
  transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1),
              margin-left 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
:is(body.service-v2, .sv-scope) .sv-btn--tone:hover svg:last-child { margin-left: 16px; }

@media (prefers-reduced-motion: reduce) {
  :is(body.service-v2, .sv-scope) .sv-btn--tone svg:last-child { transition: none; }
  :is(body.service-v2, .sv-scope) .sv-btn--tone:hover svg:last-child { margin-left: 0; }
}

/* --------------------------- Client marquee --------------------------- */
/* The homepage client band, reused here. Full-bleed brand strip on its own
   darker band; logos are duplicated in the markup so the track can translate
   -50% and loop with no visible seam.

   Kept in step with `body.home .home-clients` in css/home.css: band height,
   edge fade, the greyscale treatment and both hover behaviours (pause the
   strip, brighten the logo under the cursor) are the same numbers. The two
   pages cannot share a rule - each stylesheet is scoped to its own body class -
   so a change to one belongs in the other.

   ONE DELIBERATE DIFFERENCE: the logos are 29px here against the homepage's
   27px. The band reads smaller on a Service page, where it sits under a dense
   hero rather than under the site's opening headline. */
body.service-v2 .sv-clients {
  text-align: center;
  background: var(--sv-black);
  min-height: 146px;
  padding: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  overflow: hidden;
}

body.service-v2 .sv-clients__label {
  margin: 0 0 16px;
  /* The homepage puts its title in a .container; this band has no wrapper, so
     the inset lives here - it only shows if a longer custom label wraps. */
  padding: 0 20px;
  font-size: 11px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--sv-grey-light);
}

body.service-v2 .sv-clients__marquee {
  position: relative;
  overflow: hidden;
  width: 100%;
  /* Fade the logos out toward both edges. */
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 10%, #000 90%, transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 10%, #000 90%, transparent 100%);
}

body.service-v2 .sv-clients__track {
  display: flex;
  align-items: center;
  margin: 0;
  padding: 0;
  list-style: none;
  width: max-content;
  animation: svClientsScroll 45s linear infinite;
}
/* Hover 1: hold the strip still so a logo can be read (and, with links, aimed
   at). Pointer-only by nature - touch never fires it. */
body.service-v2 .sv-clients__marquee:hover .sv-clients__track { animation-play-state: paused; }

/* Spacing is a per-item margin, NOT a flex gap. With `gap`, the duplicated
   half is one gap narrower than 50% of the track, so translateX(-50%) lands
   just short of the loop point and the strip visibly jumps each cycle. A
   trailing margin on every item makes each half exactly 50%. */
body.service-v2 .sv-clients__item {
  flex: 0 0 auto;
  margin: 0 clamp(38px, 4.8vw, 77px) 0 0;
  display: flex;
  align-items: center;
}
/* The client list mixes white and coloured marks; greyscale + a brightness lift
   normalises them into one quiet row on the dark band. */
body.service-v2 .sv-clients__item img {
  display: block;
  width: auto;
  height: 29px;
  max-width: none;
  opacity: 0.55;
  filter: grayscale(1) brightness(1.7);
  transition: opacity var(--ds-dur, 200ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1));
}
/* Hover 2: lift the one logo under the cursor out of the row. */
body.service-v2 .sv-clients__item img:hover { opacity: 0.9; }

@keyframes svClientsScroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

@media (prefers-reduced-motion: reduce) {
  /* Stop the scroll and let the strip be swiped instead. The homepage wraps the
     track onto several rows here, which shows the duplicate pass as visible
     repeats; swiping keeps each logo appearing once. */
  body.service-v2 .sv-clients__track { animation: none; }
  body.service-v2 .sv-clients__marquee { overflow-x: auto; }
  body.service-v2 .sv-clients__item img { transition: none; }
}

/* ---------------------------- Testimonials ---------------------------- */
body.service-v2 .sv-reviews {
  background: var(--sv-bg-1);
}

body.service-v2 .sv-reviews__grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 16px;
  align-items: stretch;
}

body.service-v2 .sv-review {
  display: flex;
  flex-direction: column;
  gap: 16px;
  margin: 0;
  padding: 24px;
  border: 1px solid var(--sv-tone-line);
  border-radius: var(--sv-radius);
  background: var(--sv-tone-bg);
}

body.service-v2 .sv-review__stars {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  align-self: flex-start;
  padding: 7px 14px;
  border: 1px solid var(--sv-tone-line);
  border-radius: 999px;
  color: var(--sv-tone-main);
  line-height: 0;
}

body.service-v2 .sv-review__quote {
  margin: 0;
  font-size: 15px;
  line-height: 24px;
  color: var(--sv-white);
}
body.service-v2 .sv-review__quote p { margin: 0 0 10px; }
body.service-v2 .sv-review__quote p:last-child { margin-bottom: 0; }

body.service-v2 .sv-review__author {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-top: auto;
}

body.service-v2 .sv-review__avatar {
  flex: 0 0 48px;
  width: 48px;
  height: 48px;
  border-radius: 8px;
  object-fit: cover;
}

body.service-v2 .sv-review__meta {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}
body.service-v2 .sv-review__name {
  font-size: 14px;
  font-weight: 700;
  line-height: 1.3;
  color: var(--sv-white);
}
body.service-v2 .sv-review__role {
  font-size: 13px;
  line-height: 1.3;
  color: var(--sv-grey-light);
}

@media (max-width: 860px) {
  body.service-v2 .sv-reviews__grid { grid-template-columns: 1fr; }
}

/* ------------------------ Featured case studies ----------------------- */
/* Gradient cards, purple with the middle of each row green. The legacy band
   renders these rows as a slick carousel with images; here the gradient does
   the work and there is no image. */
body.service-v2 .sv-projects {
  background: var(--sv-bg-2);
}

body.service-v2 .sv-projects__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 32px;
  margin-bottom: clamp(28px, 3.5vw, 40px);
}
/* The head owns its own alignment, so the shared centred title rule steps back. */
body.service-v2 .sv-projects__head .sv-section__title {
  text-align: left;
  margin: 0;
}
body.service-v2 .sv-projects__btn { flex: 0 0 auto; }

body.service-v2 .sv-projects__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
  align-items: stretch;
}

/* THE HOMEPAGE'S CASE-STUDY CARD, ported. The border, the textured background
   and the tag treatment are the homepage's (css/home.css, .home-case) rather
   than a second interpretation of them - the two sections show the same content
   type and were drifting apart.

   Ported and not shared, because css/home.css scopes every rule to `body.home`
   and so cannot dress this markup. Same situation as the Tools We Master block
   further down this file.

   THE TONE IS FIVE CUSTOM PROPERTIES, not five repeated declarations. Which card
   is green depends on the column count, so it changes at 1024px - and with the
   tone spread across background, border and two tag properties, that switch used
   to mean restating each one. Now the breakpoint swaps the variables and every
   property follows. */
body.service-v2 .sv-project {
  /* Purple set - the default. */
  --sv-proj-tex:    url("../images/purple-back.png?v=1");
  --sv-proj-grad:   linear-gradient(150deg, #2A1A52 0%, #160E2C 70%);
  --sv-proj-edge:   #5336A1;
  --sv-proj-tag:    #7D51F1;
  --sv-proj-tag-bg: linear-gradient(to right, #2E2150 0%, #241A3F 100%);
  --sv-proj-more:   var(--sv-lilac);

  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 24px;
  border-radius: var(--sv-radius);
  border: 1px solid var(--sv-proj-edge);
  /* 110% overscan crops the texture's own baked border and corners off all four
     edges; the card's radius plus overflow:hidden then cut clean corners. Taken
     verbatim from the homepage, including the reason. */
  background: var(--sv-proj-tex) center / 110% 110% no-repeat, var(--sv-proj-grad);
  overflow: hidden;
  /* Sides only (QA, 2026-10-08): the background is already cut to the radius
     without any overflow clipping; what the clip still does is hold a long
     word inside the card. Leaving the bottom unclipped lets the hover strip
     below the card work (see HOVER JITTER FIX). Browsers without `clip` keep
     `hidden` from the line above. */
  overflow: clip visible;
}
/* Green set - every second card in a row of three. */
body.service-v2 .sv-projects__grid > .sv-project:nth-child(3n + 2) {
  --sv-proj-tex:    url("../images/green-back.png?v=1");
  --sv-proj-grad:   linear-gradient(150deg, #0C4A3A 0%, #0A2820 70%);
  --sv-proj-edge:   #00886F;
  --sv-proj-tag:    #00CCA6;
  --sv-proj-tag-bg: linear-gradient(to right, #0F3A31 0%, #0A2D27 100%);
  --sv-proj-more:   var(--sv-green-light);
}

body.service-v2 .sv-project__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
/* The tag reads in its card's own colour, with the border taking that colour via
   currentColor - so one declaration keeps edge and text in step. The grey outline
   this replaces made every tag look the same regardless of the card it sat on. */
body.service-v2 .sv-project__tag {
  min-width: 67px;
  height: 34px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 11px;
  border: 1px solid currentColor;
  border-radius: 999px;
  font-size: 12px;
  line-height: 1;
  /* 500 = Medium, a real face. 600 rounds UP to 700 Bold, because this family
     only ships 300/400/500/700 - which reads as a different, heavier font. The
     homepage carries the same note. */
  font-weight: 500;
  color: var(--sv-proj-tag);
  background: var(--sv-proj-tag-bg);
}

body.service-v2 .sv-project__title {
  font-family: var(--sv-sans);
  font-size: 18px;
  font-weight: 700;
  line-height: 1.3;
  color: var(--sv-white);
  margin: 0;
}

body.service-v2 .sv-project__text {
  font-size: 14px;
  line-height: 22px;
  color: rgba(249, 247, 255, 0.86);
}
body.service-v2 .sv-project__text p { margin: 0 0 10px; }
body.service-v2 .sv-project__text p:last-child { margin-bottom: 0; }

body.service-v2 .sv-project__more {
  display: inline-flex;
  align-items: center;
  gap: 8px;              /* resting gap between label and arrow */
  margin-top: auto;
  margin-left: auto;
  font-size: 14px;
  font-weight: 700;
  color: var(--sv-white);
  text-decoration: none;
  white-space: nowrap;
  transition: gap 0.4s cubic-bezier(0.4, 0, 0.2, 1), color 0.35s ease;
}
body.service-v2 .sv-project__more svg { flex: 0 0 auto; }
/* The tint follows the card's own gradient, which alternates by position - the
   selectors below mirror the background rules exactly so the two cannot drift. */
body.service-v2 .sv-project__more:hover { gap: 16px; color: var(--sv-proj-more); }
body.service-v2 .sv-project__more:focus-visible {
  outline: 2px solid var(--sv-white);
  outline-offset: 3px;
  border-radius: 4px;
}

@media (max-width: 1024px) {
  body.service-v2 .sv-projects__grid { grid-template-columns: repeat(2, 1fr); }
  /* Two columns: keep alternating rather than leaving column 2 always green.
     Only the tone SET is swapped - background, border, tag colour and tag fill
     all follow, where each previously had to be restated. */
  body.service-v2 .sv-projects__grid > .sv-project:nth-child(3n + 2) {
    --sv-proj-tex:    url("../images/purple-back.png?v=1");
    --sv-proj-grad:   linear-gradient(150deg, #2A1A52 0%, #160E2C 70%);
    --sv-proj-edge:   #5336A1;
    --sv-proj-tag:    #7D51F1;
    --sv-proj-tag-bg: linear-gradient(to right, #2E2150 0%, #241A3F 100%);
    --sv-proj-more:   var(--sv-lilac);
  }
  body.service-v2 .sv-projects__grid > .sv-project:nth-child(even) {
    --sv-proj-tex:    url("../images/green-back.png?v=1");
    --sv-proj-grad:   linear-gradient(150deg, #0C4A3A 0%, #0A2820 70%);
    --sv-proj-edge:   #00886F;
    --sv-proj-tag:    #00CCA6;
    --sv-proj-tag-bg: linear-gradient(to right, #0F3A31 0%, #0A2D27 100%);
    --sv-proj-more:   var(--sv-green-light);
  }
}
@media (max-width: 860px) {
  body.service-v2 .sv-projects__head { flex-direction: column; align-items: flex-start; gap: 20px; }
}
@media (max-width: 700px) {
  body.service-v2 .sv-projects__grid { grid-template-columns: 1fr; }
}

@media (prefers-reduced-motion: reduce) {
  body.service-v2 .sv-project__more { transition: none; }
  body.service-v2 .sv-project__more:hover { gap: 8px; }
}

/* --------------------------- Closing form ----------------------------- */
/* A single centred teal-tinted glass card. The form inside is the Contact Us
   form (same markup, field names, endpoint and script), so only the skin lives
   here. */
/* Colour comes from the Section settings radio; section-form.php sets the
   modifier. Purple and green are lifted verbatim from the Accessibility Audit
   page so the three read as one family - see assets/styles/aa-page.css
   `.aa-req` (Request a Full Audit) and `.aa-scan` (Check Your Accessibility
   Coverage Now), including their gradient geometry and opacities. */
:is(body.service-v2, .sv-scope) .sv-formsection {
  position: relative;
  overflow: hidden;
  background: #110D1A;              /* the AA base, not --sv-bg-2 */

  /* Everything inside the card - labels, field edges, checkboxes, the button -
     reads from these, so a tone swap carries the whole form and not just the
     background. Defaults are the green set; the modifiers below override them. */
  --sv-form-ink:       var(--sv-green-light);      /* labels, legends, links, status */
  --sv-form-strong:    var(--sv-green);            /* focus, checked, required mark */
  --sv-form-line:      rgba(104, 220, 199, 0.28);  /* field and pill edges */
  --sv-form-line-2:    rgba(104, 220, 199, 0.55);  /* checkbox edge */
  --sv-form-field:     rgba(8, 22, 20, 0.55);      /* field ground */
  --sv-form-field-2:   rgba(8, 22, 20, 0.8);       /* field ground, focused */
  --sv-form-autofill:  #0D1A18;                    /* opaque twin of --sv-form-field, for autofill masking */
  --sv-form-check-ink: #05221C;                    /* tick drawn on --sv-form-strong */
  --sv-form-btn-ink:   #05221C;                    /* button label on --sv-form-strong */
  --sv-form-sheen:     69, 230, 186;               /* submit button hover sheen */

  /* Validation. Not toned - an error reads the same on every colour. The grounds
     are darker and more opaque than the normal field so an invalid box stands
     out as filled-in-red rather than merely outlined. */
  --sv-form-err:         #FD563F;
  --sv-form-err-field:   rgba(44, 11, 8, 0.78);
  --sv-form-err-field-2: rgba(44, 11, 8, 0.92);
  --sv-form-err-autofill: #2C0B08;                 /* opaque twin, same purpose */
}

:is(body.service-v2, .sv-scope) .sv-formsection--purple {
  --sv-form-ink:       var(--sv-lilac);
  --sv-form-strong:    var(--sv-purple);
  --sv-form-line:      rgba(203, 185, 249, 0.28);
  --sv-form-line-2:    rgba(203, 185, 249, 0.55);
  --sv-form-field:     rgba(16, 12, 26, 0.55);
  --sv-form-field-2:   rgba(16, 12, 26, 0.8);
  --sv-form-autofill:  #15111E;
  --sv-form-check-ink: #FFFFFF;   /* purple is dark - a light tick, not a dark one */
  --sv-form-btn-ink:   #FFFFFF;
  --sv-form-sheen:     185, 166, 255;
}

:is(body.service-v2, .sv-scope) .sv-formsection--custom {
  --sv-form-ink:       var(--sv-form-tone, var(--sv-purple));
  --sv-form-strong:    var(--sv-form-tone, var(--sv-purple));
  --sv-form-line:      rgba(255, 255, 255, 0.18);
  --sv-form-line-2:    rgba(255, 255, 255, 0.34);
  --sv-form-field:     rgba(12, 10, 24, 0.55);
  --sv-form-field-2:   rgba(12, 10, 24, 0.8);
  /* An editor's tone is an arbitrary hex, so there is no principled opaque twin
     to derive - this is the same neutral the hero modal's autofill mask uses,
     which reads correctly under any of them. */
  --sv-form-autofill:  #0C0A18;
  --sv-form-check-ink: #FFFFFF;
  --sv-form-btn-ink:   #FFFFFF;
  /* An editor-picked tone is an arbitrary hex, so it yields no rgb triple for
     the sheen; a white highlight reads correctly over any of them. */
  --sv-form-sheen:     255, 255, 255;
}

/* The submit carries no colour class of its own so it can follow the tone. */
:is(body.service-v2, .sv-scope) .sv-form__btn {
  background: var(--sv-form-strong);
  color: var(--sv-form-btn-ink);
  --sv-btn-sheen: var(--sv-form-sheen, 255, 255, 255);
}
:is(body.service-v2, .sv-scope) .sv-form__btn:focus-visible { outline-color: var(--sv-form-strong); }
:is(body.service-v2, .sv-scope) .sv-formsection::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
}
/* The wash is behind the card, never over it. */
:is(body.service-v2, .sv-scope) .sv-formsection > * { position: relative; z-index: 1; }

/* .aa-req: radial #5336A1 at 35% over the base. */
:is(body.service-v2, .sv-scope) .sv-formsection--purple::before {
  background: radial-gradient(60% 80% at 50% 118%, rgba(83, 54, 161, 0.35), transparent 72%);
}
/* .aa-scan: a tighter, bottom-anchored green at 30%. */
:is(body.service-v2, .sv-scope) .sv-formsection--green::before {
  background: radial-gradient(60% 60% at 50% 100%, #00886F 0%, #110D1A 100%);
  opacity: 0.3;
}
/* Custom keeps the purple geometry and swaps the colour. */
:is(body.service-v2, .sv-scope) .sv-formsection--custom::before {
  background: radial-gradient(60% 80% at 50% 118%, var(--sv-form-tone, var(--sv-purple-dark)), transparent 72%);
  opacity: 0.35;
}

:is(body.service-v2, .sv-scope) .sv-formcard {
  max-width: 800px;
  margin: 0 auto;
  padding: clamp(28px, 4vw, 48px);
  border-radius: var(--sv-radius-lg);
  box-shadow: var(--sv-shadow);
}
/* .aa-req__card, verbatim. */
:is(body.service-v2, .sv-scope) .sv-formsection--purple .sv-formcard {
  border: 1px solid rgba(125, 81, 241, 0.4);
  background: linear-gradient(135deg, rgba(21, 17, 30, 0.5) 0%, rgba(83, 54, 161, 0.5) 100%);
}
:is(body.service-v2, .sv-scope) .sv-formsection--green .sv-formcard {
  border: 1px solid var(--sv-green-dark);
  background:
    linear-gradient(160deg, rgba(0, 136, 111, 0.22), rgba(0, 136, 111, 0.06) 55%, rgba(13, 26, 24, 0.35)),
    #0D1A18;
}
:is(body.service-v2, .sv-scope) .sv-formsection--custom .sv-formcard {
  border: 1px solid var(--sv-form-tone, var(--sv-purple));
  background: linear-gradient(135deg, rgba(21, 17, 30, 0.5) 0%, var(--sv-form-tone, var(--sv-purple-dark)) 100%);
}
:is(:is(body.service-v2, .sv-scope), .sv-scope) .sv-accent--form-tone { color: var(--sv-form-tone, var(--sv-purple)); }

:is(body.service-v2, .sv-scope) .sv-formcard__title {
  font-family: var(--sv-sans);
  font-size: clamp(26px, 3.2vw, 42px);
  font-weight: 700;
  line-height: 1.15;
  color: var(--sv-white);
  margin: 0 0 10px;
  text-wrap: balance;
}
/* The accent sits on its own line, as in the design. */
:is(:is(body.service-v2, .sv-scope), .sv-scope) .sv-formcard__title .sv-accent { display: block; }

:is(body.service-v2, .sv-scope) .sv-formcard__note {
  font-size: 16px;
  line-height: 1.4;
  color: var(--sv-white);
  margin-bottom: 26px;
}

/* Honeypot: the shared script expects it present but unseen. */
:is(body.service-v2, .sv-scope) .cu-form__hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

:is(body.service-v2, .sv-scope) .sv-form__row {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 16px;
}
:is(body.service-v2, .sv-scope) .sv-form__field { margin-bottom: 16px; min-width: 0; }
:is(body.service-v2, .sv-scope) .sv-form__row .sv-form__field { margin-bottom: 0; }
:is(body.service-v2, .sv-scope) .sv-form__row + .sv-form__field,
:is(body.service-v2, .sv-scope) .sv-form__row + .sv-form__row { margin-top: 16px; }

:is(body.service-v2, .sv-scope) .sv-form__field label {
  display: block;
  margin-bottom: 8px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--sv-form-ink);
}
:is(body.service-v2, .sv-scope) .sv-req { color: var(--sv-form-strong); }

:is(body.service-v2, .sv-scope) .sv-form__field input,
:is(body.service-v2, .sv-scope) .sv-form__field textarea {
  width: 100%;
  padding: 14px 16px;
  border: 1px solid var(--sv-form-line);
  border-radius: 10px;
  background: var(--sv-form-field);
  color: var(--sv-white);
  font-family: var(--sv-sans);
  font-size: 15px;
  line-height: 1.4;
  transition: border-color var(--ds-dur, 200ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1)), background-color var(--ds-dur, 200ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1));
}
:is(body.service-v2, .sv-scope) .sv-form__field textarea { resize: vertical; min-height: 110px; }
:is(body.service-v2, .sv-scope) .sv-form__field ::placeholder { color: rgba(249, 247, 255, 0.45); }
:is(body.service-v2, .sv-scope) .sv-form__field input:focus,
:is(body.service-v2, .sv-scope) .sv-form__field textarea:focus {
  outline: none;
  border-color: var(--sv-form-strong);
  background: var(--sv-form-field-2);
}
/* The shared script marks an invalid field by adding .has-error to its wrapper
   (and aria-invalid to the control itself). */
/* An invalid field goes fully red: red edge, red label and asterisk, and a
   darker red-tinted ground so the field itself reads as the problem rather than
   just its outline. Tone-independent - red means the same on green and purple. */
:is(body.service-v2, .sv-scope) .sv-form__field.has-error input,
:is(body.service-v2, .sv-scope) .sv-form__field.has-error textarea {
  border-color: var(--sv-form-err);
  background: var(--sv-form-err-field);
}
:is(body.service-v2, .sv-scope) .sv-form__field.has-error input:focus,
:is(body.service-v2, .sv-scope) .sv-form__field.has-error textarea:focus {
  border-color: var(--sv-form-err);
  background: var(--sv-form-err-field-2);
  box-shadow: 0 0 0 3px rgba(253, 86, 63, 0.22);
}
:is(body.service-v2, .sv-scope) .sv-form__field.has-error ::placeholder { color: rgba(253, 86, 63, 0.62); }
:is(body.service-v2, .sv-scope) .sv-form__field.has-error label { color: var(--sv-form-err); }
:is(body.service-v2, .sv-scope) .sv-form__field.has-error .sv-req { color: var(--sv-form-err); }

/* Consent: the box AND the sentence, including the Privacy Policy link. */
:is(body.service-v2, .sv-scope) .sv-check--consent.has-error .sv-check__box {
  border-color: var(--sv-form-err);
  background: var(--sv-form-err-field);
}
:is(body.service-v2, .sv-scope) .sv-check--consent.has-error,
:is(body.service-v2, .sv-scope) .sv-check--consent.has-error .sv-check__label,
:is(body.service-v2, .sv-scope) .sv-check--consent.has-error a { color: var(--sv-form-err); }

:is(body.service-v2, .sv-scope) .sv-form__services {
  margin: 22px 0 18px;
  padding: 0;
  border: 0;
}
:is(body.service-v2, .sv-scope) .sv-form__services legend {
  padding: 0;
  margin-bottom: 12px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--sv-form-ink);
}
:is(body.service-v2, .sv-scope) .sv-form__hint {
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: rgba(249, 247, 255, 0.7);
}

:is(body.service-v2, .sv-scope) .sv-checks {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

:is(body.service-v2, .sv-scope) .sv-check {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 9px 14px;
  border: 1px solid var(--sv-form-line);
  border-radius: 10px;
  cursor: pointer;
  font-size: 13px;
  line-height: 1.3;
  color: var(--sv-white);
  transition: border-color var(--ds-dur, 200ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1)), background-color var(--ds-dur, 200ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1));
}
/* The service chips (not the consent row, which has no border) get the Contact
   Us treatment: a barely-there ground at rest, filling to the same colour as the
   form's inputs on hover, and lit in the section tone once checked.

   `--sv-form-sheen` is the tone's own "r, g, b" triple - already defined per tone
   for the button hover - so the checked tint follows a green, purple or custom
   form without a second set of colours. */
:is(body.service-v2, .sv-scope) .sv-check:hover { border-color: var(--sv-form-strong); }
:is(body.service-v2, .sv-scope) .sv-checks .sv-check { background: rgba(249, 247, 255, 0.02); }
:is(body.service-v2, .sv-scope) .sv-checks .sv-check:hover { background: var(--sv-form-field); }
:is(body.service-v2, .sv-scope) .sv-checks .sv-check:has(input:checked) {
  border-color: var(--sv-form-strong);
  background: rgba(var(--sv-form-sheen), 0.10);
}
/* The native control stays in the DOM for a11y and is visually replaced. */
:is(body.service-v2, .sv-scope) .sv-check input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}
:is(body.service-v2, .sv-scope) .sv-check__box {
  flex: 0 0 16px;
  width: 16px;
  height: 16px;
  border: 1px solid var(--sv-form-line-2);
  border-radius: 4px;
  background: var(--sv-form-field);
  position: relative;
  transition: border-color var(--ds-dur-fast, 150ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1)), background var(--ds-dur-fast, 150ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1));
}
:is(body.service-v2, .sv-scope) .sv-check input:checked + .sv-check__box {
  background: var(--sv-form-strong);
  border-color: var(--sv-form-strong);
}
/* The tick EXISTS at rest and fades in, rather than being created by the
   :checked rule. Generating a pseudo-element on check gives nothing to
   transition from, so the mark used to snap in while the box behind it
   animated - two halves of one control moving at different speeds. */
:is(body.service-v2, .sv-scope) .sv-check__box::after {
  content: "";
  position: absolute;
  left: 5px;
  top: 1px;
  width: 4px;
  height: 9px;
  border: solid var(--sv-form-check-ink);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
  opacity: 0;
  transition: opacity var(--ds-dur-fast, 150ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1));
}
:is(body.service-v2, .sv-scope) .sv-check input:checked + .sv-check__box::after { opacity: 1; }
:is(body.service-v2, .sv-scope) .sv-check input:focus-visible + .sv-check__box {
  outline: 2px solid var(--sv-form-strong);
  outline-offset: 2px;
}

:is(body.service-v2, .sv-scope) .sv-check--consent {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin: 4px 0 22px;
  padding: 0;
  border: 0;
  font-size: 13px;
  line-height: 1.45;
  color: rgba(249, 247, 255, 0.85);
}
:is(body.service-v2, .sv-scope) .sv-check--consent:hover { border-color: transparent; }
:is(body.service-v2, .sv-scope) .sv-check--consent .sv-check__box { margin-top: 2px; }
:is(body.service-v2, .sv-scope) .sv-check--consent a { color: var(--sv-form-ink); }

:is(body.service-v2, .sv-scope) .sv-form__turnstile { margin-bottom: 18px; }

:is(body.service-v2, .sv-scope) .sv-form__btn {
  width: 100%;
  justify-content: center;
}

:is(body.service-v2, .sv-scope) .sv-form__status {
  margin: 14px 0 0;
  font-size: 14px;
  line-height: 1.45;
  color: var(--sv-form-ink);
}
:is(body.service-v2, .sv-scope) .sv-form__status.is-error { color: #FD563F; }

@media (max-width: 640px) {
  :is(body.service-v2, .sv-scope) .sv-form__row { grid-template-columns: 1fr; }
  :is(body.service-v2, .sv-scope) .sv-form__row .sv-form__field + .sv-form__field { margin-top: 16px; }
}

@media (prefers-reduced-motion: reduce) {
  :is(body.service-v2, .sv-scope) .sv-form__field input,
  :is(body.service-v2, .sv-scope) .sv-form__field textarea,
  :is(body.service-v2, .sv-scope) .sv-check { transition: none; }
}

/* ---- Submit button: in-flight, success and error --------------------------
   The shared script (assets/scripts/contact-us.js) already puts is-loading /
   is-success / is-error on this button and swaps its label to the matching
   data-label-*. These are the rules that make that visible; ported from
   contact-us.css, which solved it first.

   Keyframes are named sv* rather than cu* so that if both stylesheets ever load
   on one page neither redefines the other's animation. */

/* The arrow slide has to be named explicitly now. `.sv-btn:hover svg:last-child`
   used to be the arrow; with three icons in the button the last child is the
   (hidden) cross, so the hover nudge would silently stop happening. */
:is(body.service-v2, .sv-scope) .sv-form__btn .cu-btn__ico--arrow {
  transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
:is(body.service-v2, .sv-scope) .sv-form__btn:hover .cu-btn__ico--arrow { transform: translateX(2px); }

/* One icon at a time. */
:is(body.service-v2, .sv-scope) .sv-form__btn .cu-btn__ico--check,
:is(body.service-v2, .sv-scope) .sv-form__btn .cu-btn__ico--cross { display: none; }
:is(body.service-v2, .sv-scope) .sv-form__btn.is-success .cu-btn__ico--arrow,
:is(body.service-v2, .sv-scope) .sv-form__btn.is-error .cu-btn__ico--arrow { display: none; }
:is(body.service-v2, .sv-scope) .sv-form__btn.is-success .cu-btn__ico--check,
:is(body.service-v2, .sv-scope) .sv-form__btn.is-error .cu-btn__ico--cross { display: inline-block; }

/* In flight. `.sv-btn::after` is the hover sheen, so the spinner uses ::before. */
:is(body.service-v2, .sv-scope) .sv-form__btn.is-loading { cursor: progress; opacity: 0.7; }
:is(body.service-v2, .sv-scope) .sv-form__btn.is-loading .cu-btn__ico { display: none; }
:is(body.service-v2, .sv-scope) .sv-form__btn.is-loading::before {
  content: "";
  /* Sit where the arrow / check / cross sit, not before the label. A ::before is
     the FIRST flex item by default, so without this the spinner lands to the left
     of the label; `order: 1` puts it after every item still on the initial order
     of 0. Contact Us carries the same line for the same reason - dropping it is
     what put the spinner on the wrong side. */
  order: 1;
  width: 18px;
  height: 18px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: svBtnSpin 0.7s linear infinite;
}
@keyframes svBtnSpin { to { transform: rotate(360deg); } }

/* Settled states do not respond to hover - the action is over. */
:is(body.service-v2, .sv-scope) .sv-form__btn.is-loading:hover,
:is(body.service-v2, .sv-scope) .sv-form__btn.is-success:hover,
:is(body.service-v2, .sv-scope) .sv-form__btn.is-error:hover { gap: 10px; }
:is(body.service-v2, .sv-scope) .sv-form__btn.is-loading:hover::after,
:is(body.service-v2, .sv-scope) .sv-form__btn.is-success:hover::after,
:is(body.service-v2, .sv-scope) .sv-form__btn.is-error:hover::after { opacity: 0; }
:is(body.service-v2, .sv-scope) .sv-form__btn.is-success,
:is(body.service-v2, .sv-scope) .sv-form__btn.is-error { cursor: default; opacity: 1; }

/* Success keeps the section tone - the tick is the signal. Error goes red on
   every tone, because an error should not be a brand colour. */
:is(body.service-v2, .sv-scope) .sv-form__btn.is-error {
  background: linear-gradient(135deg, #FF6B6B, #E5484D);
  color: #FFFFFF;
}

/* The strokes draw themselves in, with a small pop of the icon. */
:is(body.service-v2, .sv-scope) .sv-form__btn .cu-btn__ico--check path { stroke-dasharray: 30; stroke-dashoffset: 30; }
:is(body.service-v2, .sv-scope) .sv-form__btn.is-success .cu-btn__ico--check { animation: svIcoPop 0.32s ease; }
:is(body.service-v2, .sv-scope) .sv-form__btn.is-success .cu-btn__ico--check path { animation: svIcoDraw 0.4s ease 0.05s forwards; }
:is(body.service-v2, .sv-scope) .sv-form__btn .cu-btn__ico--cross .cu-x1,
:is(body.service-v2, .sv-scope) .sv-form__btn .cu-btn__ico--cross .cu-x2 { stroke-dasharray: 16; stroke-dashoffset: 16; }
:is(body.service-v2, .sv-scope) .sv-form__btn.is-error .cu-btn__ico--cross { animation: svIcoPop 0.32s ease; }
:is(body.service-v2, .sv-scope) .sv-form__btn.is-error .cu-btn__ico--cross .cu-x1 { animation: svIcoDraw 0.2s ease forwards; }
:is(body.service-v2, .sv-scope) .sv-form__btn.is-error .cu-btn__ico--cross .cu-x2 { animation: svIcoDraw 0.2s ease 0.16s forwards; }
@keyframes svIcoDraw { to { stroke-dashoffset: 0; } }
@keyframes svIcoPop { 0% { transform: scale(0.4); } 60% { transform: scale(1.15); } 100% { transform: scale(1); } }

/* The status line becomes a pill once it says something. It carried a colour for
   errors only, so a success message was an unstyled grey line under a button
   that had just turned into a tick. */
:is(body.service-v2, .sv-scope) .sv-form__status.is-success,
:is(body.service-v2, .sv-scope) .sv-form__status.is-error {
  width: fit-content;
  max-width: 100%;
  margin: 16px auto 0;
  padding: 11px 20px;
  border-radius: 999px;
  font-weight: 600;
  text-align: center;
}
:is(body.service-v2, .sv-scope) .sv-form__status.is-success {
  background: rgba(var(--sv-form-sheen), 0.10);
  border: 1px solid rgba(var(--sv-form-sheen), 0.34);
  color: var(--sv-form-ink);
}
:is(body.service-v2, .sv-scope) .sv-form__status.is-error {
  background: rgba(253, 86, 63, 0.14);
  border: 1px solid rgba(253, 86, 63, 0.5);
  color: #FFB4B4;
}

@media (prefers-reduced-motion: reduce) {
  :is(body.service-v2, .sv-scope) .sv-form__btn.is-loading::before { animation: none; }
  :is(body.service-v2, .sv-scope) .sv-form__btn .cu-btn__ico--check path,
  :is(body.service-v2, .sv-scope) .sv-form__btn .cu-btn__ico--cross .cu-x1,
  :is(body.service-v2, .sv-scope) .sv-form__btn .cu-btn__ico--cross .cu-x2 {
    stroke-dashoffset: 0;
    animation: none;
  }
  :is(body.service-v2, .sv-scope) .sv-form__btn.is-success .cu-btn__ico--check,
  :is(body.service-v2, .sv-scope) .sv-form__btn.is-error .cu-btn__ico--cross { animation: none; }
  :is(body.service-v2, .sv-scope) .sv-form__btn .cu-btn__ico--arrow { transition: none; }
  :is(body.service-v2, .sv-scope) .sv-check__box { transition: none; }
  :is(body.service-v2, .sv-scope) .sv-check__box::after { transition: none; }
}

/* ---- Autofill ----------------------------------------------------------
   WebKit repaints an autofilled field with its own near-white background and
   dark text, wiping the dark styling entirely - which is what made a filled
   form look like a different site. It cannot be overridden with `background`,
   because the UA paints behind it; the established trick is to cover it with a
   1000px inset box-shadow and force the text and caret colours.

   THIS IS THE CONTACT US FIX, ported. That page solved this already
   (assets/styles/contact-us.css) and this form is a reskin of it, so the same
   four-layer structure is used rather than a fresh attempt - including the
   reason those layers are SEPARATE rules: if one Chrome version stops
   recognising an `:-internal-*` pseudo, an invalid selector in a shared rule
   would take the others down with it.

   The difference from Contact Us is the colour. That page has one palette and
   hardcodes #0d1a18; this form has three tones, so the mask reads
   --sv-form-autofill and follows whatever the editor picked.

   The `transition` on background-color is not decoration: it defers the UA's
   own repaint far enough into the future that it never lands.

   `!important` on the last three groups matches the Contact Us fix. The UA
   styling for those states outranks a plain author declaration, and this is the
   one place in this stylesheet where that is true. */
:is(body.service-v2, .sv-scope) .sv-form input:-webkit-autofill,
:is(body.service-v2, .sv-scope) .sv-form textarea:-webkit-autofill,
:is(body.service-v2, .sv-scope) .sv-form input:-webkit-autofill:hover,
:is(body.service-v2, .sv-scope) .sv-form textarea:-webkit-autofill:hover {
  -webkit-text-fill-color: var(--sv-white);
  caret-color: var(--sv-white);
  -webkit-box-shadow: 0 0 0 1000px var(--sv-form-autofill) inset;
  box-shadow: 0 0 0 1000px var(--sv-form-autofill) inset;
  transition: background-color 9999s ease-in-out 0s;
}

/* Focused: keep the mask and restore the tone's focus edge. This form has no
   focus ring beyond the border, so unlike Contact Us there is no second shadow
   to preserve alongside the mask. */
:is(body.service-v2, .sv-scope) .sv-form input:-webkit-autofill:focus,
:is(body.service-v2, .sv-scope) .sv-form textarea:-webkit-autofill:focus {
  border-color: var(--sv-form-strong);
  -webkit-box-shadow: 0 0 0 1000px var(--sv-form-autofill) inset;
  box-shadow: 0 0 0 1000px var(--sv-form-autofill) inset;
}

/* Chrome's autofill PREVIEW - shown while a suggestion is merely hovered in the
   dropdown - uses internal pseudo-classes distinct from :-webkit-autofill. */
:is(body.service-v2, .sv-scope) .sv-form input:-internal-autofill-previewed,
:is(body.service-v2, .sv-scope) .sv-form textarea:-internal-autofill-previewed {
  -webkit-text-fill-color: var(--sv-white) !important;
  box-shadow: 0 0 0 1000px var(--sv-form-autofill) inset !important;
}
:is(body.service-v2, .sv-scope) .sv-form input:-internal-autofill-selected,
:is(body.service-v2, .sv-scope) .sv-form textarea:-internal-autofill-selected {
  -webkit-text-fill-color: var(--sv-white) !important;
  box-shadow: 0 0 0 1000px var(--sv-form-autofill) inset !important;
}

/* The standardised pseudo-class, for browsers that ship it. */
:is(body.service-v2, .sv-scope) .sv-form input:autofill,
:is(body.service-v2, .sv-scope) .sv-form textarea:autofill {
  -webkit-text-fill-color: var(--sv-white) !important;
  box-shadow: 0 0 0 1000px var(--sv-form-autofill) inset !important;
}

/* An autofilled field that is ALSO invalid must still read as the problem.
   Without this the mask paints over the red ground and only the border stays
   red, which is the weakest of the three error signals - and autofill is
   exactly when a wrong value arrives without the visitor having typed it.
   Contact Us does not cover this case; it is the same mechanism, so it is
   covered here. */
:is(body.service-v2, .sv-scope) .sv-form .cu-form__field.has-error input:-webkit-autofill,
:is(body.service-v2, .sv-scope) .sv-form .cu-form__field.has-error textarea:-webkit-autofill,
:is(body.service-v2, .sv-scope) .sv-form .cu-form__field.has-error input:autofill,
:is(body.service-v2, .sv-scope) .sv-form .cu-form__field.has-error textarea:autofill {
  border-color: var(--sv-form-err);
  -webkit-box-shadow: 0 0 0 1000px var(--sv-form-err-autofill) inset !important;
  box-shadow: 0 0 0 1000px var(--sv-form-err-autofill) inset !important;
}

/* ---------------- Services card grid (builder: two-colums-tabs) -------- */
/* The homepage "End-to-End QA Services" grid, values carried across from
   css/home.css `.home-services`: 4 columns at 19px gap, the first two cards
   spanning half a row, cards tinted to their tone. */
body.service-v2 .sv-svcgrid {
  background: var(--sv-bg-2);
}

/* This section's header is left-aligned like the homepage's, so it opts out of
   the shared centred rules. */
body.service-v2 .sv-svcgrid__title,
body.service-v2 .sv-svcgrid__lead {
  text-align: left;
  margin-left: 0;
  margin-right: 0;
}
/* Full container width, so the lead lines up with the card grid below instead
   of stopping short of it. Overrides the shared lead's 900px measure. */
body.service-v2 .sv-svcgrid__lead {
  max-width: none;
  margin-bottom: 35px;
}

body.service-v2 .sv-svcgrid__grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 19px;
}
body.service-v2 .sv-svcgrid__grid > .sv-svc--wide { grid-column: span 2; }

/* Short card sets set their own track count instead of the 2-wide + rest
   pattern, which would strand a half-empty second row at these counts. The part
   emits the matching --cols-N and drops .sv-svc--wide; 4 cards use --cols-2 for
   a filled 2 x 2, since a 4-up line crowds the longer card copy and 3 up leaves
   an orphan. Same specificity as the base rule and declared above the
   breakpoints, so the responsive collapse below still wins. */
body.service-v2 .sv-svcgrid__grid--cols-2 { grid-template-columns: repeat(2, 1fr); }
body.service-v2 .sv-svcgrid__grid--cols-3 { grid-template-columns: repeat(3, 1fr); }

body.service-v2 .sv-svc {
  display: flex;
  flex-direction: column;
  min-width: 0; /* let text wrap rather than widening the track */
  padding: 22px;
  border: 1px solid;
  border-radius: var(--sv-radius);
}
/* Tone tints, matching the homepage cards rather than the darker tone-bg used
   elsewhere in this template. */
body.service-v2 .sv-svc.sv-tone--purple {
  background: #110E19;
  border-color: rgba(142, 108, 255, 0.35);
}
body.service-v2 .sv-svc.sv-tone--green {
  background: #0D1217;
  border-color: rgba(0, 204, 166, 0.35);
}

body.service-v2 .sv-svc__top {
  display: flex;
  align-items: center;
  gap: 11px;
  margin-bottom: 13px;
}

body.service-v2 .sv-svc__ico {
  flex: none;
  width: 38px;
  height: 38px;
  border-radius: 10px;
  display: grid;
  place-items: center;
}
body.service-v2 .sv-svc.sv-tone--purple .sv-svc__ico { background: rgba(142, 108, 255, 0.16); color: #A78BFF; }
body.service-v2 .sv-svc.sv-tone--green  .sv-svc__ico { background: rgba(0, 204, 166, 0.16); color: var(--sv-green); }
/* An uploaded icon is a complete tile, so drop our own tile behind it. */
body.service-v2 .sv-svc__ico--img { background: none; }
body.service-v2 .sv-svc__ico img { width: 100%; height: 100%; object-fit: contain; }

body.service-v2 .sv-svc__title {
  font-family: var(--sv-sans);
  font-size: 18px;
  font-weight: 500;
  line-height: 1.2;
  margin: 0;
}
body.service-v2 .sv-svc.sv-tone--purple .sv-svc__title { color: var(--sv-lilac); }
body.service-v2 .sv-svc.sv-tone--green  .sv-svc__title { color: var(--sv-green-light); }

body.service-v2 .sv-svc__text {
  font-size: 14px;
  line-height: 1.6;
  color: var(--sv-white);
}
body.service-v2 .sv-svc__text p { margin: 0 0 10px; }
body.service-v2 .sv-svc__text p:last-child { margin-bottom: 0; }

body.service-v2 .sv-svc__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 14px 0 0;
  padding: 0;
  list-style: none;
}
/* currentColor border so a pill always matches its own text colour. */
body.service-v2 .sv-svc__tags li {
  padding: 4px 10px;
  border: 1px solid currentColor;
  border-radius: 999px;
  font-size: 10px;
  font-weight: 600;
}
body.service-v2 .sv-svc__tags li::before { content: "#"; }
body.service-v2 .sv-svc.sv-tone--purple .sv-svc__tags li { background: #0E0B15; color: var(--sv-purple-dark); }
body.service-v2 .sv-svc.sv-tone--green  .sv-svc__tags li { background: #0D1217; color: var(--sv-green-dark); }

/* Resting gap 12px, stretching to 20px on hover - the homepage's own motion. */
body.service-v2 .sv-svc__more {
  margin-top: auto;
  padding-top: 18px;
  align-self: flex-end;
  display: inline-flex;
  align-items: center;
  gap: 12px;
  font-size: 12px;
  font-weight: 500;
  color: var(--sv-white);
  text-decoration: none;
  transition: gap 0.4s cubic-bezier(0.4, 0, 0.2, 1), color 0.35s ease;
}
body.service-v2 a.sv-svc__more:hover { gap: 20px; }
body.service-v2 .sv-svc.sv-tone--purple a.sv-svc__more:hover { color: var(--sv-lilac); }
body.service-v2 .sv-svc.sv-tone--green  a.sv-svc__more:hover { color: var(--sv-green-light); }
body.service-v2 .sv-svc__more--static { cursor: default; }
body.service-v2 a.sv-svc__more:focus-visible {
  outline: 2px solid var(--sv-white);
  outline-offset: 3px;
  border-radius: 4px;
}

@media (max-width: 1024px) {
  body.service-v2 .sv-svcgrid__grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 700px) {
  body.service-v2 .sv-svcgrid__grid { grid-template-columns: 1fr; }
  body.service-v2 .sv-svcgrid__grid > .sv-svc--wide { grid-column: auto; }
  body.service-v2 .sv-svc__more { align-self: flex-start; }
}

@media (prefers-reduced-motion: reduce) {
  body.service-v2 .sv-svc__more { transition: none; }
  body.service-v2 a.sv-svc__more:hover { gap: 12px; }
}

/* ------------- Tools We Master (builder: technology-stack) ------------- */
/* The Service renders the HOMEPAGE's section itself - templates/home/tools.php,
   via template-parts/service-v2/flexible/section-technology-stack.php - so the
   markup here is the homepage's `.home-tools` tree, not an `sv-` copy.
   css/home.css scopes all of its rules to `body.home`, so it cannot reach this
   page; these rules carry the same values under `body.service-v2` instead.

   The bridge block below covers the wrappers the homepage part expects
   (.home-section padding, .container, .home-section__head, the eyebrow and the
   accent span), each scoped INSIDE .home-tools so nothing leaks onto the legacy
   header/footer that shares generic class names like .container. */
body.service-v2 .home-tools {
  background: var(--sv-bg-2);
  padding: clamp(56px, 7vw, 96px) 0;   /* .home-section on the homepage */
  position: relative;
}

body.service-v2 .home-tools > .container {
  width: 100%;
  max-width: var(--sv-container);
  margin-inline: auto;
  padding-inline: 19px;
}

body.service-v2 .home-tools .home-section__head {
  max-width: 1000px;
  margin: 0 auto 45px;
  text-align: center;
}
/* Matched to .sv-section__title so this heading sits with the rest of the page
   rather than the homepage's fixed 48px. */
body.service-v2 .home-tools .home-section__head h2 {
  font-family: var(--sv-sans);
  font-weight: 700;
  font-size: clamp(28px, 3.4vw, 48px);
  line-height: 1.2;
  letter-spacing: -0.01em;
  color: var(--sv-white);
  margin: 0;
  text-wrap: balance;
}
body.service-v2 .home-tools .home-section__head p {
  margin: 14px 0 0;
  color: var(--sv-grey-light);
  font-size: clamp(15px, 1.3vw, 20px);
  line-height: 1.4;
}
/* Hidden on the homepage too - kept so the markup stays identical. */
body.service-v2 .home-tools__eyebrow { display: none; }
body.service-v2 .home-tools .home-accent-text-purple { color: var(--sv-purple); }

body.service-v2 .home-tools__rows {
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin: 6px auto 38px;
  max-width: var(--sv-container);
  padding-inline: 19px;
}

/* Fades both ends so chips enter and leave rather than popping. */
body.service-v2 .home-tools__marquee {
  overflow: hidden;
  width: 100%;
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 6%, #000 94%, transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 6%, #000 94%, transparent 100%);
}

body.service-v2 .home-tools__track {
  display: flex;
  width: max-content;
  margin: 0;
  padding: 0;
  list-style: none;
  animation: svToolsScroll 50s linear infinite;
}
body.service-v2 .home-tools__marquee--rev .home-tools__track { animation-direction: reverse; }
body.service-v2 .home-tools__rows:hover .home-tools__track { animation-play-state: paused; }

/* Spacing is a trailing margin, not a flex gap: with `gap` the duplicated half
   is one gap narrower than 50% of the track and the loop visibly jumps. */
body.service-v2 .home-tools__chip {
  flex: none;
  margin-right: 13px;
  padding: 9px 18px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--h-border, rgba(255, 255, 255, 0.09));
  border-radius: 999px;
  font-size: 14px;
  font-weight: 500;
  color: var(--sv-grey-light);
  white-space: nowrap;
  cursor: default;
  transition: color var(--ds-dur, 200ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1)), border-color var(--ds-dur, 200ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1)), background var(--ds-dur, 200ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1));
}
body.service-v2 .home-tools__chip:hover {
  color: #fff;
  background: rgba(255, 255, 255, 0.1);
  border-color: var(--h-border-strong, rgba(255, 255, 255, 0.16));
}

@keyframes svToolsScroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

@media (prefers-reduced-motion: reduce) {
  /* Stop scrolling and let the chips wrap into a static block. */
  body.service-v2 .home-tools__track { animation: none; flex-wrap: wrap; width: auto; }
  body.service-v2 .home-tools__marquee { -webkit-mask-image: none; mask-image: none; }
}

body.service-v2 .home-tools__grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 19px;
}
/* A category with more than four tools takes half a row. */
body.service-v2 .home-tools__grid > .home-tools__cat--wide { grid-column: span 2; }

body.service-v2 .home-tools__cat {
  padding: 21px;
  border: 1px solid var(--h-border, rgba(255, 255, 255, 0.09));
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.03);
}
body.service-v2 .home-tools__cat--purple { border-color: rgba(142, 108, 255, 0.35); }
body.service-v2 .home-tools__cat--green  { border-color: rgba(0, 204, 166, 0.35); background: #0F1419; }

body.service-v2 .home-tools__cat-title {
  font-family: var(--sv-sans);
  font-size: 18px;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  margin: 0 0 16px;
}
body.service-v2 .home-tools__cat--purple .home-tools__cat-title { color: #A78BFF; }
body.service-v2 .home-tools__cat--green  .home-tools__cat-title { color: var(--sv-green); }

body.service-v2 .home-tools__cat-list {
  margin: 16px 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px 16px;
}
body.service-v2 .home-tools__cat-list.is-one-col { grid-template-columns: 1fr; }
body.service-v2 .home-tools__cat-list li {
  color: #fff;
  font-size: 12px;
  font-weight: lighter;
}

@media (max-width: 1024px) {
  body.service-v2 .home-tools__grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 700px) {
  body.service-v2 .home-tools__grid { grid-template-columns: 1fr; }
  body.service-v2 .home-tools__grid > .home-tools__cat--wide { grid-column: auto; }
}

/* ------- Coverage cards (builder: three-box-list) ---------------------- */
/* The AI Product Testing page's "What AI Product Testing Covers" grid: a
   centred head over three columns of outlined cards. Values from
   assets/styles/ai-product-testing.css `.ai-covers`. */
body.service-v2 .sv-covers {
  background: var(--sv-bg-2);
}

body.service-v2 .sv-covers__head {
  max-width: 820px;
  margin: 0 auto 52px;
  text-align: center;
}

body.service-v2 .sv-covers__title {
  font-family: var(--sv-sans);
  font-size: clamp(28px, 3.2vw, 42px);
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: -0.01em;
  color: var(--sv-white);
  margin: 0;
  text-wrap: balance;
}

body.service-v2 .sv-covers__sub {
  margin-top: 16px;
  font-size: 17px;
  line-height: 1.5;
  color: var(--sv-grey-light);
}
body.service-v2 .sv-covers__sub p { margin: 0 0 10px; }
body.service-v2 .sv-covers__sub p:last-child { margin-bottom: 0; }

body.service-v2 .sv-covers__grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
}

body.service-v2 .sv-covers__card {
  padding: 26px 26px 28px;
  border: 1px solid rgba(203, 185, 249, 0.14);
  border-radius: 18px;
  background: rgba(255, 255, 255, 0.028);
}

body.service-v2 .sv-covers__badge {
  display: inline-block;
  margin-bottom: 18px;
  padding: 7px 16px;
  border: 1px solid var(--sv-purple);
  border-radius: 999px;
  background: rgba(21, 17, 30, 0.5);
  color: var(--sv-purple);
  font-size: 12px;
  font-weight: 700;
}

body.service-v2 .sv-covers__card-title {
  font-family: var(--sv-sans);
  font-size: 18px;
  font-weight: 700;
  line-height: 1.3;
  color: #fff;
  margin: 0 0 10px;
}

body.service-v2 .sv-covers__card-text {
  font-size: 14.5px;
  line-height: 1.55;
  color: var(--sv-grey-light);
}
body.service-v2 .sv-covers__card-text p { margin: 0 0 10px; }
body.service-v2 .sv-covers__card-text p:last-child { margin-bottom: 0; }

@media (max-width: 1024px) {
  body.service-v2 .sv-covers__grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 700px) {
  body.service-v2 .sv-covers__grid { grid-template-columns: 1fr; }
}

/* ------------- Two-column hero (builder: hero-2) ---------------------- */
/* Background is the standard hero's (shared ::before/::after rules above);
   layout follows the AI Product Testing hero - copy and ratings on the left,
   a glass panel on the right. */
body.service-v2 .sv-hero2 {
  position: relative;
  overflow: hidden;
  background: var(--sv-bg-2);

  /* One full screen, as on .sv-hero and the hub pages - see the note there for
     why this is min-height and why svh rather than vh/dvh. The padding formula
     below is untouched and still does the optical centring: the extra height
     this adds is split evenly above and below the content box, so the gap
     difference the formula establishes survives intact. */
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;

  /* Vertically centre the content in the band BELOW the overlay header, rather
     than in the section box. The header is absolutely positioned on top of the
     hero, so its height has to be added to the top padding for the visible gap
     above the content to match the one below it:

         gap above content = padding-top - header height
         gap below content = padding-bottom

     Equal gaps therefore mean padding-top = header height + padding-bottom.
     Expressed that way it stays balanced at every width instead of only at the
     one the old fixed clamp happened to suit (it ran 15px heavy at 1280px and
     45px heavy at 1920px, which is what made the hero look bottom-weighted). */
  --sv-hero2-header-h: 74px;
  --sv-hero2-pad-y: clamp(64px, 6vw, 96px);
  padding: calc(var(--sv-hero2-header-h) + var(--sv-hero2-pad-y)) 0 var(--sv-hero2-pad-y);
}

body.service-v2 .sv-hero2__inner {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 432px;
  column-gap: 56px;
  /* The CTA column is taller than the copy, so top-aligning left it hanging
     well below the text. Centring balances the two against each other; the
     hero's top padding below is sized to keep the overhang clear of the
     header. */
  align-items: center;
}

body.service-v2 .sv-hero2__title {
  font-family: var(--sv-serif);
  font-weight: 400; /* GT Sectra "Super" is already heavy */
  font-size: clamp(34px, 4.6vw, 60px);
  line-height: 1.04;
  color: var(--sv-white);
  margin: 0 0 20px;
}
/* The accent half reuses the standard hero's blue-to-teal gradient. */
body.service-v2 .sv-hero2__title .sv-hero__title-accent { display: block; }

body.service-v2 .sv-hero2__text {
  font-size: clamp(16px, 1.3vw, 19px);
  line-height: 1.5;
  color: var(--sv-grey-light);
  max-width: 620px;
  margin: 0 0 32px;
}
body.service-v2 .sv-hero2__text p { margin: 0 0 10px; }
body.service-v2 .sv-hero2__text p:last-child { margin-bottom: 0; }

/* The shared badge row is centred for the standard hero; here it sits left. */
body.service-v2 .sv-hero2__badges {
  justify-content: flex-start;
  gap: 16px 28px;
}


/* ---- Glass panel (AI Product Testing hero) ---- */
/* The column is a plain stack; the card treatment lives on its children, so the
   benefit list and the CTA read as two separate cards (per the design) rather
   than one panel divided by a rule. */
body.service-v2 .sv-hero2__panel {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

body.service-v2 .sv-hero2__card {
  padding: 26px 26px 28px;
  border: 1px solid rgba(142, 108, 255, 0.35);
  border-radius: 26px;
  background: linear-gradient(135deg, rgba(21, 17, 30, 0.5) 0%, rgba(83, 54, 161, 0.5) 100%);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

body.service-v2 .sv-hero2__points {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 14px;
}

body.service-v2 .sv-hero2__point {
  display: flex;
  align-items: flex-start;
  gap: 13px;
}

/* Green tick, no tile behind it - the design marks the benefits rather than
   numbering them. Sized to sit on the first line of its text. */
body.service-v2 .sv-hero2__point-check {
  flex: none;
  display: grid;
  place-items: center;
  width: 14px;
  height: 14px;
  margin-top: 4px;
  color: var(--sv-green);
}

body.service-v2 .sv-hero2__point-text {
  font-size: 14px;
  line-height: 1.45;
  color: var(--sv-white);
}

/* ---- CTA card ---- */
/* Its own card now, so no divider rule or top spacing - the flex gap on
   .sv-hero2__panel supplies the separation. */
body.service-v2 .sv-hero2__cta {
  text-align: center;
}

body.service-v2 .sv-hero2__cta-title {
  margin: 0 0 16px;
  font-size: 16px;
  font-weight: 700;
  line-height: 1.35;
  color: #fff;
}

/* Content-width so the arrow nudge reads, but centred in the panel. */
body.service-v2 .sv-hero2__cta-btn {
  margin: 0 auto;
  width: fit-content;
  padding: 15px 26px;
  font-size: 15px;
}

body.service-v2 .sv-hero2__cta-sub {
  margin: 12px 0 0;
  font-size: 12px;
  line-height: 1.4;
  color: var(--sv-grey-light);
}

@media (max-width: 1024px) {
  /* Panel drops under the copy; it keeps a comfortable reading width. */
  body.service-v2 .sv-hero2__inner { grid-template-columns: minmax(0, 1fr); row-gap: 40px; }
  body.service-v2 .sv-hero2__panel { max-width: 560px; }
}
@media (max-width: 640px) {
  /* Only the breathing space changes; the padding formula above still adds the
     header height on top, so the content stays centred under it. */
  body.service-v2 .sv-hero2 { --sv-hero2-pad-y: 56px; }
  body.service-v2 .sv-hero2__badges { gap: 14px 22px; }
  body.service-v2 .sv-hero2__panel { gap: 16px; }
  body.service-v2 .sv-hero2__card { padding: 20px 18px 22px; }
}

/* ------------------- Hero modal: HubSpot estimate form ---------------------
   The hero CTA opens the HubSpot embed (portal 3076879). The embed, its fields
   and its submission path are untouched - this is presentation only, mirroring
   the AI Product Testing estimate card (assets/styles/ai-product-testing.css
   `.ai-cta__card` / `.ai-cta__field`) so the two forms read as one component.

   assets/scripts/service-v2.js supplies the two things CSS cannot: the labels
   (HubSpot renders empty ones) and `.has-error` on the field wrapper (HubSpot
   only flags the input).

   Everything is scoped to `body.service-v2` so the legacy page-flexible modals
   - /end-to-end-testing/ uses the same markup - keep their current look.
   Several rules below out-specify `!important` declarations in css/flexible.css
   and have to carry `!important` themselves; each is marked. */

/* ---- The card ---- */
body.service-v2 .modal .modal__inner {
  width: min(720px, 92vw);
  max-height: 88vh;
  overflow-y: auto;
  padding: 38px 40px 34px;
  border-radius: 26px;
  border: 1px solid rgba(142, 108, 255, 0.35);
  /* Replaces the legacy popup-overlay.png with the AI card's glass gradient. */
  background-image: none;
  background: linear-gradient(135deg, rgba(21, 17, 30, 0.92) 0%, rgba(83, 54, 161, 0.92) 100%);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}
body.service-v2 .modal { background-color: rgba(8, 6, 13, 0.72); }

/* ---- Two-column field grid: name pair, contact pair, message full width ---- */
body.service-v2 .modal .hbspt-form form {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px 24px;
}
body.service-v2 .modal .hbspt-form form .hs_message,
body.service-v2 .modal .hbspt-form form .hs_submit,
body.service-v2 .modal .hbspt-form form .hs_error_rollup,
body.service-v2 .modal .hbspt-form form .hs-richtext { grid-column: 1 / -1; }

body.service-v2 .modal .hs-form-field {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* ---- Labels (built by service-v2.js) ----
   css/flexible.css hides every label in this form with `display:none !important`,
   so the generated one has to be re-shown just as forcefully. */
body.service-v2 .modal.hbspt-flex-styles .hbspt-form form label.sv-hsf__label {
  display: block !important;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--sv-lilac);
}
body.service-v2 .modal .sv-hsf__req { color: #ff7d95; }

/* ---- Inputs ----
   `background-color: transparent !important` in css/flexible.css forces the
   !important here; the rest simply out-specifies it. */
body.service-v2 .modal.hbspt-flex-styles .hs-input {
  width: 100%;
  padding: 14px 16px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 12px;
  background-color: rgba(10, 8, 20, 0.55) !important;
  color: #fff;
  font: inherit;
  font-size: 15px;
}
body.service-v2 .modal.hbspt-flex-styles textarea.hs-input {
  resize: vertical;
  min-height: 110px;
}
body.service-v2 .modal.hbspt-flex-styles .hs-input::placeholder { color: #8B8A92; }

body.service-v2 .modal.hbspt-flex-styles .hs-input:focus,
body.service-v2 .modal.hbspt-flex-styles .hs-input:focus-visible {
  outline: none;
  border: 1px solid var(--sv-purple);
  background-color: #0C0A18 !important;
  box-shadow: 0 0 0 3px rgba(125, 81, 241, 0.25);
}

/* Autofill: WebKit repaints its own background over ours. Same masking trick the
   AI form uses. */
body.service-v2 .modal.hbspt-flex-styles .hs-input:-webkit-autofill,
body.service-v2 .modal.hbspt-flex-styles .hs-input:-webkit-autofill:hover {
  -webkit-text-fill-color: #fff;
  caret-color: #fff;
  -webkit-box-shadow: 0 0 0 1000px #0C0A18 inset;
  box-shadow: 0 0 0 1000px #0C0A18 inset;
  transition: background-color 100000s ease-in-out 0s;
}

/* ---- Validation (HubSpot's own; restyled) ---- */
/* Matches the shape of the base label rule above, which is (0,5,3) - a shorter
   selector here loses on specificity and the label stays lilac. */
body.service-v2 .modal.hbspt-flex-styles .hbspt-form form .hs-form-field.has-error label.sv-hsf__label {
  color: #FF6B85;
}
body.service-v2 .modal.hbspt-flex-styles .hs-input.invalid,
body.service-v2 .modal.hbspt-flex-styles .hs-input.error {
  border-color: #E62D54;
  background-color: rgba(230, 45, 84, 0.08) !important;
}
body.service-v2 .modal.hbspt-flex-styles .hs-input.invalid:focus,
body.service-v2 .modal.hbspt-flex-styles .hs-input.error:focus {
  border-color: #E62D54;
  box-shadow: 0 0 0 3px rgba(230, 45, 84, 0.25);
}

body.service-v2 .modal .hs-error-msgs {
  margin: 0;
  padding: 0;
  list-style: none;
}
body.service-v2 .modal .hs-error-msgs li { margin: 0; }
body.service-v2 .modal .hs-error-msgs label {
  display: block !important;   /* beats the blanket label hide */
  font-size: 13px;
  line-height: 1.35;
  color: #FF7D95;
}
/* The form-level "Please complete all required fields." rollup. */
body.service-v2 .modal .hs_error_rollup .hs-error-msgs label {
  padding: 11px 16px;
  border: 1px solid rgba(230, 45, 84, 0.55);
  border-radius: 12px;
  background: rgba(230, 45, 84, 0.14);
  text-align: center;
}

/* ---- Submit ----
   Two legacy patterns have to be undone here.

   1. css/flexible.css pins .actions to 50% width with !important inside a modal.

   2. The theme's custom stylesheet (5ed8e1c2....css) makes .actions ITSELF the
      visual button - `display:flex; height:58px; padding:16px 32px; border-radius:6px;
      background:#5622DA` - and lays the real <input> over it with
      `position:absolute; height:100%`. Our button styling lives on the input, so
      leaving that in place traps the input at the height of .actions' padding box
      (32px) while its own 16px padding needs ~55px, which CROPS the label.
      .actions is therefore reduced to a plain wrapper and the input put back in
      flow. */
body.service-v2 .modal.hbspt-flex-styles .hs_submit.hs-submit { display: block; }
/* Only the BOX is neutralised here. Background, radius and overflow are left
   alone so the .home-btn--lilac component below can supply them - overriding
   them here would out-specify the component and kill both the lilac fill and the
   `overflow:hidden` its hover sheen needs in order to clip to the pill. */
body.service-v2 .modal.hbspt-flex-styles .hbspt-form form .actions {
  display: block;
  width: 100% !important;
  height: auto;
  min-height: 0;
  padding: 0;
  margin: 6px 0 0 !important;
}
/* The wrapper carries the header's "Contact Us" component - `home-btn
   home-btn--lilac`, added in service-v2.js - so the pill and its hover sheen are
   the shared ones, not a copy. Only the box is adjusted: the modal wants a
   full-width button, and the padding moves onto the input so the input's own
   line box drives the height (see the crop note above). */
body.service-v2 .modal.hbspt-flex-styles .hbspt-form form .actions.home-btn {
  display: block;
  width: 100% !important;
  padding: 0;
  border-radius: 999px;
}
body.service-v2 .modal.hbspt-flex-styles .hbspt-form form .actions input.hs-button {
  position: static;
  width: 100%;
  height: auto;
  padding: 16px 32px;
  border: 0;
  border-radius: inherit;
  background: none;
  color: inherit;              /* the dark ink from .home-btn--lilac */
  font: inherit;
  font-size: 16px;
  font-weight: 700;
  cursor: pointer;
}
body.service-v2 .modal.hbspt-flex-styles .hbspt-form form .actions input.hs-button:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 3px;
}

/* Post-submit thank-you copy. Also what the "Show a message in place of the
   form" setting renders into, so it carries the card's own type scale. */
body.service-v2 .modal .submitted-message { font-size: 16px; line-height: 1.5; color: var(--sv-white); }
body.service-v2 .modal .submitted-message p { margin: 0 0 12px; }
body.service-v2 .modal .submitted-message p:last-child { margin-bottom: 0; }
body.service-v2 .modal .submitted-message a { color: var(--sv-purple-light, #CBBCFF); }

/* ---- Close button ----
   Sits inside .modal__inner rather than over the backdrop so it stays attached
   to the card at every width. The card's top padding is what keeps it clear of
   the first field's label. */
body.service-v2 .modal .modal__inner { position: relative; padding-top: 52px; }
body.service-v2 .modal .modal__close {
  position: absolute;
  top: 12px;
  right: 12px;
  z-index: 2;
  width: 36px;
  height: 36px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid rgba(203, 188, 255, 0.28);
  border-radius: 50%;
  background: rgba(10, 8, 20, 0.55);
  color: var(--sv-white);
  cursor: pointer;
  transition: background-color var(--ds-dur, 200ms), border-color var(--ds-dur, 200ms), color var(--ds-dur, 200ms);
}
body.service-v2 .modal .modal__close:hover {
  background: rgba(125, 81, 241, 0.35);
  border-color: rgba(203, 188, 255, 0.6);
}
body.service-v2 .modal .modal__close:focus-visible {
  outline: 2px solid var(--sv-purple);
  outline-offset: 3px;
}

@media (max-width: 640px) {
  /* 56px, not the 26px this used to be: the close button occupies 12px to 48px
     from the card's top edge, so anything less puts the first field's label
     under it - measured at 375px, where the single column runs full width and
     there is no clear space beside the button. */
  body.service-v2 .modal .modal__inner { padding: 56px 20px 24px; border-radius: 20px; }
  body.service-v2 .modal .hbspt-form form { grid-template-columns: 1fr; }
}

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

@media (prefers-reduced-motion: reduce) {
  body.service-v2 .modal.hbspt-flex-styles .hs-button.primary { transition: none; }
  body.service-v2 .modal.hbspt-flex-styles .hs-button.primary:hover { transform: none; }
}

/* ---------------------------- Card hover ------------------------------
   One lift for every card on the template. It started as a one-off on the
   "Types of services" grid (.sv-card); the other six card components had no
   hover at all, so a page mixed cards that responded to the pointer with cards
   that did not. Values are that original effect, unchanged.

   WHAT IS NOT A CARD, and so is not here: `.sv-step` (a full-width numbered
   row - lifting the whole width reads as the page twitching, not as a card
   responding), and the three single-block panels `.sv-ctacard`, `.sv-formcard`
   and `.sv-textcard`, which are section-sized surfaces rather than items in a
   grid.

   These components now declare their own `transition`, which is exactly the
   case the scroll-reveal below is built to survive: the reveal animates rather
   than transitions, so a card's transition cannot swallow it, and the reveal
   does not fill forwards, so it cannot block this hover once it has run. */
body.service-v2 .sv-card,
body.service-v2 .sv-feature,
body.service-v2 .sv-panel,
body.service-v2 .sv-project,
body.service-v2 .sv-review,
body.service-v2 .sv-svc,
body.service-v2 .sv-covers__card {
  transition: transform var(--ds-dur-slow, 250ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1)), box-shadow var(--ds-dur-slow, 250ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1));
}
body.service-v2 .sv-card:hover,
body.service-v2 .sv-feature:hover,
body.service-v2 .sv-panel:hover,
body.service-v2 .sv-project:hover,
body.service-v2 .sv-review:hover,
body.service-v2 .sv-svc:hover,
body.service-v2 .sv-covers__card:hover {
  transform: translateY(-3px);
  box-shadow: var(--sv-shadow);
}

@media (prefers-reduced-motion: reduce) {
  body.service-v2 .sv-card,
  body.service-v2 .sv-feature,
  body.service-v2 .sv-panel,
  body.service-v2 .sv-project,
  body.service-v2 .sv-review,
  body.service-v2 .sv-svc,
  body.service-v2 .sv-covers__card { transition: none; }
  body.service-v2 .sv-card:hover,
  body.service-v2 .sv-feature:hover,
  body.service-v2 .sv-panel:hover,
  body.service-v2 .sv-project:hover,
  body.service-v2 .sv-review:hover,
  body.service-v2 .sv-svc:hover,
  body.service-v2 .sv-covers__card:hover { transform: none; }
}

/* HOVER JITTER FIX (QA, 2026-10-08). A card that lifts on its own :hover
   slides its bottom edge 3px up from under a cursor parked there: the hover
   drops, the card falls back under the cursor, hovers again - a loop that
   shakes the card (measured: ~40 up/down reversals tracing one bottom edge).
   While hovered, a transparent 6px strip hangs below the card and fills the gap
   the lift opens, so the cursor stays on it. Absolutely positioned against the
   card itself, which is a containing block only because it is transformed -
   so it exists only with the lift (not under reduced motion, where the card
   does not move and has no transform to anchor to). Nothing is painted and
   nothing in the layout moves. The same fix for the product cards needed a
   wrapper (.home-product-cell): those clip their overflow, which clips this
   strip too. */
@media (prefers-reduced-motion: no-preference) {
  body.service-v2 .sv-card:hover::after,
  body.service-v2 .sv-feature:hover::after,
  body.service-v2 .sv-panel:hover::after,
  body.service-v2 .sv-project:hover::after,
  body.service-v2 .sv-review:hover::after,
  body.service-v2 .sv-svc:hover::after,
  body.service-v2 .sv-covers__card:hover::after,
  .cs-page .cs-related .sv-project:hover::after {
    content: "";
    position: absolute;
    /* The card's border too, so the corners hold (it sits outside the box
       this is positioned in). */
    left: -2px;
    right: -2px;
    bottom: -6px;
    height: 6px;
  }
}

/* ------------------------- In-page anchor targets ----------------------
   The header is fixed once the page scrolls (`.home-header.header-to-top`), so
   a plain anchor jump parks the target underneath it - the form's heading ends
   up hidden behind the bar and the section looks like it starts mid-sentence.
   `scroll-padding-top` insets every scroll-into-view on this page by the header
   height (74px) plus breathing room.

   Set on `html` because that is the scrolling element - it cannot be scoped to
   body.service-v2 - and that is safe here for the same reason it is on the AI
   Product Testing page: this stylesheet is only ever enqueued on this template.

   Smooth behaviour is declared rather than assumed: it currently comes from
   another sheet, which is not a dependency worth relying on. */
html {
  scroll-behavior: smooth;
  scroll-padding-top: 96px;
}
/* The admin bar sits above everything for logged-in users. */
body.admin-bar {
  scroll-padding-top: 128px;
}
@media screen and (max-width: 782px) {
  /* Below 782px the admin bar is 46px and scrolls away with the page. */
  body.admin-bar { scroll-padding-top: 96px; }
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

/* A bare jump target. No box, no space: it exists only to be scrolled to. */
:is(body.service-v2, .sv-scope) .sv-anchor {
  display: block;
  height: 0;
  overflow: hidden;
}

/* --------------------- Rich text: code and quotes ----------------------
   Every section lead and row description is a wysiwyg, so an editor can put a
   <pre> or <code> in one - deliberately, for a snippet, or accidentally, by
   pasting from anywhere that formats its examples as preformatted text.

   Unstyled, `<pre>` is the single most destructive thing that can land in one
   of these boxes: the UA default is `white-space: pre`, so the text refuses to
   wrap and one paragraph became a 2,846px line inside a 1,000px card. It also
   arrives in monospace with a 1em margin, and wpautop() skips <pre> entirely,
   so paragraph breaks inside it are lost too. That reads as "the page is
   broken" long before anyone suspects the content.

   Scoped to the redesigned main so it cannot reach a Legacy Service. */
body.service-v2 .service-page--v2 pre {
  margin: 0 0 12px;
  padding: 12px 14px;
  border-radius: 8px;
  border: 1px solid rgba(249, 247, 255, 0.10);
  background: rgba(249, 247, 255, 0.04);
  /* pre-wrap keeps the author's own line breaks but still wraps long lines;
     the scroller then only appears for something genuinely unbreakable. */
  white-space: pre-wrap;
  overflow-wrap: break-word;
  overflow-x: auto;
  font-family: ui-monospace, "Cascadia Mono", "SF Mono", Consolas, monospace;
  font-size: 0.92em;
  line-height: 1.5;
  color: inherit;
}
body.service-v2 .service-page--v2 pre:last-child { margin-bottom: 0; }

body.service-v2 .service-page--v2 :not(pre) > code {
  padding: 0.1em 0.36em;
  border-radius: 4px;
  border: 1px solid rgba(249, 247, 255, 0.12);
  background: rgba(249, 247, 255, 0.06);
  font-family: ui-monospace, "Cascadia Mono", "SF Mono", Consolas, monospace;
  font-size: 0.9em;
}
/* The closing form card is the one LIGHT surface here, so the white-on-dark
   tints above would be invisible inside it. */
:is(body.service-v2, .sv-scope) .sv-formcard pre,
:is(body.service-v2, .sv-scope) .sv-formcard :not(pre) > code {
  border-color: rgba(12, 10, 24, 0.14);
  background: rgba(12, 10, 24, 0.05);
}

body.service-v2 .service-page--v2 blockquote {
  margin: 0 0 12px;
  padding: 2px 0 2px 16px;
  border-left: 2px solid var(--sv-tone-main, var(--sv-purple));
  color: var(--sv-grey-light);
}

/* --------------------------- Scroll reveal ----------------------------
   Sections fade up as they enter the viewport, driven by the shared module
   assets/scripts/shared/.. (assets/scripts/scroll-reveal.js), enqueued for this
   template in inc/service-design.php. That module adds `.qm-reveal` and then
   `.is-in`, and injects its own hiding CSS - these rules exist because of one
   cascade problem it cannot solve from a <style> tag it appends at runtime.

   THE PROBLEM. The module reveals with a `transition`. Two components here
   declare a transition of their own - `.sv-card` (transform, box-shadow) and
   `.sv-faq__item` (background) - at `body.service-v2 .sv-card` (0,2,1), which
   OUT-SPECIFIES the module's `.qm-reveal.is-in` (0,2,0). The component's
   transition therefore wins the property outright: cards would slide with the
   wrong timing and no fade at all, and FAQ rows would simply pop in.

   THE FIX. Reveal with an `animation` instead. `animation` and `transition` are
   different properties, so nothing can swallow it and no component loses the
   transition it declared for its own hover.

   `backwards` fill covers the stagger delay (the element is held at the `from`
   keyframe while it waits its turn). The animation deliberately does NOT fill
   forwards: once it has run, no transform is left applied, so `.sv-card:hover`
   lifts normally instead of fighting a filled end state.

   Nothing here applies without JavaScript, and the module bails out entirely
   under `prefers-reduced-motion`, so `.qm-reveal` is never added and these
   selectors never match. That is why there is no reduced-motion block below. */
body.service-v2 .qm-reveal:not(.is-in) {
  opacity: 0;
  transform: translateY(26px);
  /* Hide INSTANTLY. The module applies `.qm-reveal` from JS after paint, which
     is a transform change - and `.sv-card` transitions transform for its hover
     lift, so without this the cards visibly sank 26px on every page load before
     they had even been revealed. Dropped the moment `.is-in` lands, so each
     component gets its own transition back for hover. */
  transition: none;
}
body.service-v2 .qm-reveal.is-in {
  opacity: 1;
  /* --qm-rd is the per-sibling stagger the module sets (0.07s steps, capped). */
  animation: svReveal 0.7s cubic-bezier(0.22, 0.61, 0.36, 1) var(--qm-rd, 0s) backwards;
}
@keyframes svReveal {
  from { opacity: 0; transform: translateY(26px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ------------------- Long-word overflow containment --------------------
   A CSS grid track's automatic minimum is its content's min-content width, so
   ONE unbroken token longer than the track - a German compound, a pasted URL,
   a product name like "QAMadnessAccessibilityScanner" - pushes the track wider
   than the container and the whole PAGE scrolls sideways. Measured at 320px
   before this block: a 61-character title in one card grew that card to 674px
   and put 73 elements past the viewport edge.

   Two halves, and both are needed:
     min-width:0        releases the grid item from that automatic minimum
     overflow-wrap      lets the word itself break rather than just overflow

   `anywhere` rather than `break-word` because only `anywhere` also shrinks the
   min-content contribution, which is the half that stops the track expanding.
   Real content is unaffected either way - this only changes what happens past
   the point where the layout used to break.

   The grid list below covers BOTH surfaces a New Design Service can render: the
   standard sections, and the Page-Flexible builder layouts (hero-2,
   three-box-list, technology-stack, ...) that builder-driven Services render
   through template-parts/service-v2/flexible/. The builder layouts were checked
   separately on /services/ai-in-qa-testing/ - hero-2's benefit list, the
   three-box-list cards and the Tools We Master category lists are all grids too
   and needed the same treatment.
   ---------------------------------------------------------------------- */
body.service-v2 :is(.sv-cards__grid,
                    .sv-features__grid,
                    .sv-projects__grid,
                    .sv-svcgrid__grid,
                    .sv-expertise__grid,
                    .sv-reviews__grid,
                    .sv-covers__grid,
                    .sv-hero2__points,
                    .sv-panel__list,
                    .sv-steps,
                    .sv-checks,
                    .home-tools__grid,
                    .home-tools__cat-list) > * {
  min-width: 0;
}
/* One inherited declaration rather than a list of every text class.
   An enumerated list was tried first and was wrong twice: a sweep of every
   leaf text node in <main> found eight more that it missed (heading accent
   spans, project tags, the form hint, the submit label), and it would miss
   the next section anyone adds. `overflow-wrap` inherits, so scoping it to
   the page root covers everything, now and later.

   Safe because `anywhere` only acts when a word cannot otherwise fit -
   verified by measuring the rendered layout with and without it: card, step
   and feature widths and the full document height were byte-identical
   (27584px both ways), and all 26 leaf text types went from 8 overflowing
   to zero. */
body.service-v2 .service-page--v2 {
  overflow-wrap: anywhere;
}

/* ------------------- Marquee pause control (WCAG 2.2.2) -----------------
   Both bands scroll indefinitely, so they need a control anyone can reach -
   the pre-existing `:hover` pause is invisible to keyboard and touch. The
   button is inserted by assets/scripts/service-v2.js; these rules dress it and
   own what "paused" means, so the script only toggles a class.

   Placed inside the band, bottom-right, quiet until focused or hovered. It sits
   above the edge mask, which is why it carries its own solid background rather
   than relying on the band behind it. */
body.service-v2 :is(.sv-clients__marquee, .home-tools__rows) {
  position: relative;
}
body.service-v2 .sv-marquee-toggle {
  position: absolute;
  right: 10px;
  bottom: 8px;
  z-index: 2;
  /* 28px clears the 24x24 WCAG 2.5.8 minimum with room to spare. */
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid rgba(249, 247, 255, 0.16);
  border-radius: 999px;
  background: rgba(12, 10, 24, 0.72);
  color: var(--sv-grey-light);
  cursor: pointer;
  opacity: 0.45;
  transition: opacity var(--ds-dur, 200ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1)), color var(--ds-dur, 200ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1)), border-color var(--ds-dur, 200ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1));
}
body.service-v2 :is(.sv-clients__marquee, .home-tools__rows):hover .sv-marquee-toggle,
body.service-v2 .sv-marquee-toggle:hover,
body.service-v2 .sv-marquee-toggle:focus-visible {
  opacity: 1;
  color: var(--sv-white);
  border-color: rgba(249, 247, 255, 0.4);
}
body.service-v2 .sv-marquee-toggle:focus-visible {
  outline: 2px solid var(--sv-green-light);
  outline-offset: 2px;
}
/* Paused state: the class is the single source of truth, so it also beats the
   existing hover-pause rule rather than fighting it. */
body.service-v2 .sv-clients__marquee.is-paused .sv-clients__track,
body.service-v2 .home-tools__rows.is-paused .home-tools__track {
  animation-play-state: paused;
}
body.service-v2 .sv-marquee-toggle__play { display: none; }
body.service-v2 .is-paused .sv-marquee-toggle__pause { display: none; }
body.service-v2 .is-paused .sv-marquee-toggle__play { display: block; }
/* The control stays visible once paused, so the way back is always on screen. */
body.service-v2 .is-paused .sv-marquee-toggle { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  /* The animation is already off here, so a pause button would control nothing.
     The clients band also becomes horizontally scrollable in this mode and the
     button would sit over the content. */
  body.service-v2 .sv-marquee-toggle { display: none; }
}

/* ------------------ Target size (WCAG 2.2 AA, 2.5.8) -------------------
   The standalone "more" links measured 104x20 and 114x18 at 375px - under the
   24x24 minimum in one dimension. The inline-link exception does not obviously
   apply: these are block-level calls to action in their own row, not links
   inside a sentence.

   min-height plus centred content rather than padding, so the row they sit in
   does not grow and no card changes height. */
body.service-v2 :is(.sv-card__more, .sv-panel__link, .sv-project__more, .sv-svc__more) {
  min-height: 24px;
}
body.service-v2 :is(.sv-card__more, .sv-project__more, .sv-svc__more) {
  align-items: center;
}
/* The expertise list is a column of links; giving each one the minimum height
   also puts real space between adjacent targets. */
body.service-v2 .sv-panel__link {
  display: flex;
  align-items: center;
}

/* ------------- AI-Assisted QA Ecosystem diagram (section: ecosystem) ----
   template-parts/service-v2/section-ecosystem.php, registered by
   inc/service-ecosystem-section.php.

   A diagram built from text, not an exported image: a five-column grid whose
   middle three columns are arrow, core, arrow, then a delivery band and four
   notes underneath. Every colour comes from the tone system already declared
   above (.sv-tone--green / .sv-tone--purple), so the two source cards are the
   same components the rest of the page uses, in the same two tones.

   The connectors are the one piece of pure decoration here, so they are
   aria-hidden in the markup and they change direction rather than disappear at
   narrow widths - see the stacked block at the bottom. */

body.service-v2 .sv-eco__title {
  margin-bottom: 20px;
}

/* The design sets the accented half on its own line, under "The QA Madness".
   A block span keeps that without a <br> in editable copy - an editor who
   rewrites the heading gets one line or two depending on length, not a stray
   break in the middle of a sentence.

   FLAT, not the gradient the other sections use. .sv-accent--green sweeps blue
   into teal across the glyphs, which works on a heading tail of two or three
   words; across a whole second line it reads as "AI-Assisted" being blue and
   "Ecosystem" being green rather than as one coloured line. The class is left
   off in the markup, so there is no gradient to undo here - only a colour to
   set, which an editor's Accent colour still overrides inline. */
body.service-v2 .sv-eco__accent {
  display: block;   /* colour comes from .sv-accent--purple/green, like the rest */
}

body.service-v2 .sv-eco__lead {
  max-width: 940px;
  margin-bottom: clamp(36px, 4.5vw, 56px);
}

body.service-v2 .sv-eco__diagram {
  display: flex;
  flex-direction: column;
  align-items: stretch;
}

/* ---- The flow row: card -> core <- card ---- */
/* Measured off the design as RATIOS of the content width, not the pixel numbers
   on the mock: the mock's container is ~1194 wide where .sv-container gives 1162
   (1200 less its own 19px padding), so copying 394 / 56 / 300 literally would
   have overrun. The design's 0.329 / 0.046 / 0.249 split lands here as
   380 / 56 / 290.

   The gutters matter more than they look - at 40px the cards grow to 410 and the
   arrows sit tight against them, which is what made the first build read as
   three touching panels rather than two sources feeding a hub. */
body.service-v2 .sv-eco__flow {
  display: grid;
  grid-template-columns: 1fr 56px minmax(240px, 290px) 56px 1fr;
  align-items: stretch;
}

body.service-v2 .sv-eco__card {
  padding: 24px;
  border-radius: var(--sv-radius);
  border: 1px solid var(--sv-tone-line);
  background: var(--sv-tone-bg);
}

body.service-v2 .sv-eco__card-title {
  margin: 0 0 12px;
  font-size: clamp(19px, 1.7vw, 23px);
  font-weight: 700;
  line-height: 1.25;
  color: var(--sv-tone-text);
}

body.service-v2 .sv-eco__card-sub {
  margin: 0 0 16px;
  font-size: 15px;
  font-weight: 700;
  line-height: 1.4;
  color: var(--sv-white);
}

body.service-v2 .sv-eco__list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 9px;
}

/* The bullet is drawn rather than inherited so it takes the card's tone and
   stays put when a line wraps - a list-style marker would sit outside the
   padding box and hang into the border. */
body.service-v2 .sv-eco__list-item {
  position: relative;
  padding-left: 16px;
  font-size: 14px;
  line-height: 1.45;
  color: var(--sv-grey-light);
}
body.service-v2 .sv-eco__list-item::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.55em;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--sv-tone-main);
}

/* ---- Connectors ---- */
body.service-v2 .sv-eco__arrow {
  align-self: center;
  display: flex;
  align-items: center;
  justify-content: center;
}
body.service-v2 .sv-eco__arrow svg {
  display: block;
  width: 34px;
  height: 14px;
}
body.service-v2 .sv-eco__arrow--in  { color: var(--sv-green); }
/* One arrow drawn once and mirrored, so both gutters carry the same shape. */
body.service-v2 .sv-eco__arrow--out {
  color: var(--sv-purple);
  transform: scaleX(-1);
}

/* The design leaves ~63px between the flow and the band, with the arrow in the
   top two thirds of it - the gap under the arrowhead is what stops the band
   reading as attached to the right-hand card. */
body.service-v2 .sv-eco__down {
  display: flex;
  justify-content: center;
  padding-bottom: 24px;
  color: var(--sv-white);
}
body.service-v2 .sv-eco__down svg {
  display: block;
  width: 14px;
  height: 40px;
}

/* ---- The QA Team core ---- */
/* A ROUNDED RECTANGLE, not an ellipse. This was `border-radius: 50%` for three
   revisions and it was the wrong primitive: a 50% radius is a true ellipse, so
   the outline curves continuously and the box has no straight edge anywhere.
   The design's shape has flat top, bottom and sides with large rounded corners,
   which is a different silhouette at any size - so no amount of adjusting the
   width or the padding was ever going to match it, and two rounds spent
   flattening the aspect were spent on the wrong axis.

   The radius is a PERCENTAGE so the corners scale with the box: content-driven
   height means the box grows if an editor adds a line, and a fixed pixel radius
   would make the corners look tighter as it did.

   The padding is fixed rather than a vw clamp. Width is pinned by the grid
   track (290px), so vertical padding is the only thing deciding the proportion,
   and a clamp made it drift with the viewport - 1.20 at 1280, 1.30 by 1100,
   flatter still below. A shape that changes proportion as the window narrows
   reads as a rendering bug rather than a design.

   The proportion here is read off a zoomed crop of the mock and is good to a
   few percent, no better. See the note against .sv-eco__core-list. */
body.service-v2 .sv-eco__core {
  align-self: center;
  border-radius: 30%;
  background: linear-gradient(150deg, #4FDFC0 0%, #22CFAE 55%, #17C2A2 100%);
  color: var(--sv-black);
  text-align: center;
  padding: 44px 30px;
  display: grid;
  place-items: center;
}

/* A FLAT size, not a vw clamp, for the same reason the padding above is flat.
   The shape's width is pinned at 290px wherever it is the rounded box, so the
   title has exactly the same room at 1600 as at 481 - a responsive size buys
   nothing there. What it did buy was instability: shrinking 30px to 24px below
   ~1364 took 12px out of the content block, and with the height driven by that
   content the proportion slid by 4%. Below 480 the shape becomes a full-width
   card with more room than this needs, so 30px is safe there too. */
body.service-v2 .sv-eco__core-title {
  margin: 0 0 10px;
  font-size: 30px;
  font-weight: 700;
  line-height: 1.15;
  color: var(--sv-black);
}

/* The row pitch in the mock is ~30px, against the 25px a 14px/1.35 line and a
   6px gap gave. 15px type plus a 10px gap lands on 30 and is what makes the
   block read as spaced rather than stacked.

   MEASURED OFF AN IMAGE, so treat these three numbers - the type size, the gap
   and the core's vertical padding - as good to a few percent and no better. If
   the Figma node is to hand they should be read from it instead. */
body.service-v2 .sv-eco__core-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 10px;
  align-items: center;
}

/* Centred rows with the dot travelling with the text, so the list stays
   optically centred however long each line is. */
/* Not a translucent black: this text sits on a gradient, so its contrast has to
   clear 4.5:1 against the DARKEST stop (#17C2A2), not the average. The
   semi-transparent value this started as measured 4.48:1 there - a fail by two
   hundredths that no eyeball would catch. This flat ink measures 5.45:1 against
   that stop and 7.43:1 against the lightest. */
body.service-v2 .sv-eco__core-item {
  display: inline-flex;
  align-items: baseline;
  gap: 7px;
  font-size: 15px;
  line-height: 1.35;
  color: #1B3A34;
}
body.service-v2 .sv-eco__core-item::before {
  content: "";
  flex: 0 0 5px;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: rgba(24, 46, 42, 0.55);
  transform: translateY(-2px);
}

/* ---- Delivery band ---- */
body.service-v2 .sv-eco__base {
  border: 1px solid rgba(249, 247, 255, 0.14);
  border-radius: var(--sv-radius);
  background: #0C0A12;
  overflow: hidden;
}

body.service-v2 .sv-eco__base-title {
  margin: 0;
  padding: 28px 24px;
  text-align: center;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--sv-white);
  border-bottom: 1px solid rgba(249, 247, 255, 0.14);
}

body.service-v2 .sv-eco__base-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(6, 1fr);
}

/* Dividers are borders on the cells rather than gaps, so the band reads as one
   ruled table the way the design draws it. The nth-child resets below are what
   keep that true once the columns wrap - a plain :first-child rule would leave
   a stray divider at the start of every row after the first. */
body.service-v2 .sv-eco__base-item {
  padding: 28px 12px;
  text-align: center;
  font-size: 14px;
  line-height: 1.35;
  color: var(--sv-grey-light);
  border-left: 1px solid rgba(249, 247, 255, 0.14);
}
body.service-v2 .sv-eco__base-item:nth-child(6n + 1) { border-left: 0; }

/* ---- Footer notes ---- */
body.service-v2 .sv-eco__notes {
  margin: 24px 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 20px;
}

/* No border. These are the one surface in the section the design draws as a
   plain lighter fill - a hairline round them made four more outlined boxes under
   a band that is already outlined, and the row stopped reading as a footnote. */
body.service-v2 .sv-eco__note {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 30px 18px;
  text-align: center;
  border-radius: var(--sv-radius);
  background: #15121F;
}
body.service-v2 .sv-eco__note-title {
  font-size: 16px;
  font-weight: 700;
  line-height: 1.3;
  color: var(--sv-lilac);
}
body.service-v2 .sv-eco__note-text {
  font-size: 14px;
  line-height: 1.35;
  color: var(--sv-grey-light);
}

/* ---- Stacked ---- */
/* Below 1024 the five columns become one, in DOM order: left card, arrow, core,
   arrow, right card. Nothing is reordered by CSS, so the reading order a screen
   reader follows and the order on screen stay the same at every width.

   The arrows still have to mean what they meant across the row. The first one
   turns DOWN (left card -> core) and the second one turns UP (right card ->
   core), so the diagram keeps saying "both sources feed the QA team" instead of
   turning into a chain where the QA team feeds QAM Hub. */
@media (max-width: 1024px) {
  body.service-v2 .sv-eco__flow {
    grid-template-columns: minmax(0, 1fr);
  }

  body.service-v2 .sv-eco__arrow {
    height: 44px;
  }
  /* The mirror is the desktop trick for pointing left; vertically the direction
     comes from the rotation instead. */
  body.service-v2 .sv-eco__arrow--out {
    transform: none;
  }
  body.service-v2 .sv-eco__arrow--in svg  { transform: rotate(90deg); }
  body.service-v2 .sv-eco__arrow--out svg { transform: rotate(-90deg); }

  /* 290px, the same width the desktop grid track gives it, so the oval keeps
     its proportion when it stops being a column and becomes a row of its own.
     At the 360px this used to be, the box stayed ~230 tall and the shape landed
     at a 1.6-1.8 aspect - a flat lens, not the mock's oval. */
  body.service-v2 .sv-eco__core {
    width: min(100%, 290px);
    margin-inline: auto;
  }
}

@media (max-width: 860px) {
  body.service-v2 .sv-eco__base-list {
    grid-template-columns: repeat(3, 1fr);
  }
  body.service-v2 .sv-eco__base-item:nth-child(6n + 1) { border-left: 1px solid rgba(249, 247, 255, 0.14); }
  body.service-v2 .sv-eco__base-item:nth-child(3n + 1) { border-left: 0; }
  body.service-v2 .sv-eco__base-item:nth-child(n + 4)  { border-top: 1px solid rgba(249, 247, 255, 0.14); }

  body.service-v2 .sv-eco__notes {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (max-width: 480px) {
  body.service-v2 .sv-eco__card {
    padding: 20px;
  }

  /* The oval stops paying for itself here: at this width its padding eats more
     room than the shape adds, and long lines start touching the curve. The
     card keeps the tone and the gradient, and loses the ellipse. */
  body.service-v2 .sv-eco__core {
    /* Back to full width: it is a card now, and a 290px cap would leave it
       narrower than the two cards above and below it.

       `margin-inline` has to be reset with it. Auto inline margins on a grid
       item override `justify-self: stretch` - so `width: auto` alone left the
       margins from the 1024 rule in force, the box shrank to its max-content
       207px and centred itself, two thirds the width of the cards it sits
       between. Restoring the margin is what makes `auto` mean "fill the track"
       again. */
    width: auto;
    margin-inline: 0;
    border-radius: var(--sv-radius-lg);
    padding: 24px 20px;
  }

  body.service-v2 .sv-eco__base-list {
    grid-template-columns: repeat(2, 1fr);
  }
  body.service-v2 .sv-eco__base-item:nth-child(3n + 1) { border-left: 1px solid rgba(249, 247, 255, 0.14); }
  body.service-v2 .sv-eco__base-item:nth-child(2n + 1) { border-left: 0; }
  body.service-v2 .sv-eco__base-item:nth-child(n + 3)  { border-top: 1px solid rgba(249, 247, 255, 0.14); }

  body.service-v2 .sv-eco__notes {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* --------------- Schema / diagram image (section: schema) --------------
   template-parts/service-v2/section-schema.php.

   Deliberately almost no styling: the artwork carries its own design, and a
   frame, shadow or panel round it would compete with whatever the diagram
   already draws. The section supplies the shared heading pair, a centred box
   and nothing else. */

body.service-v2 .sv-schema__figure {
  margin: 0;
  /* A wide diagram is the one thing in this section likely to need scrolling on
     a phone when no mobile version was uploaded - see the note on --natural. */
  text-align: center;
}

body.service-v2 .sv-schema__img {
  display: block;
  height: auto;
  margin-inline: auto;
}

/* An SVG with no viewBox reports no intrinsic ratio, so `height: auto` resolves
   to nothing and the box collapses to zero. The template flags that case rather
   than the stylesheet guessing at it.

   The floor used to be a bare `min-height: 40px` on every schema image, which
   turned a missing viewBox into a full-width 40px sliver - indistinguishable
   from a broken layout, and applied to correctly-formed images too while their
   file was still in flight. Now only the ratio-less case gets a box, and it gets
   one big enough to read as "this image needs a viewBox" rather than as damage.
   The field instructions ask for a viewBox for this reason. */
body.service-v2 .sv-schema__figure--noratio .sv-schema__img {
  min-height: clamp(200px, 22vw, 320px);
  object-fit: contain;
}

/* Full width: fills the content column. Right for an SVG, which loses nothing
   at any scale. */
body.service-v2 .sv-schema__figure--full .sv-schema__img {
  width: 100%;
}

/* Natural size: never wider than the file itself, so a small export is not
   stretched soft. --sv-schema-max is the file's own width, printed on the figure
   by the template - for an SVG that is the viewBox width, read out of the file,
   because the browser does not expose it.

   `width: 100%` with the cap, NOT `width: auto`. Auto looks like the obvious way
   to say "its own size" and is unreliable for exactly the format this section is
   built for: an SVG carrying only a viewBox has NO intrinsic width (the 300x130
   that naturalWidth reports is a legacy DOM default, not a layout size), so auto
   falls through to the stretch-fit branch of the default sizing algorithm and
   fills the column anyway. Same rendered result, arrived at by accident. With
   an explicit 100% the cap is the only thing deciding the width, and it behaves
   identically for a raster, whose intrinsic width is real. */
body.service-v2 .sv-schema__figure--natural .sv-schema__img {
  width: 100%;
  max-width: min(100%, var(--sv-schema-max, 100%));
}

/* The mobile variant's shape, applied in the SAME query as the <source> that
   swaps the file. It has to live here rather than as width/height on that
   <source>: measured in Chromium, those attributes are honoured even when the
   source's media query does NOT match, so a portrait mobile file was giving the
   desktop diagram a 420/900 ratio at 1585px wide - a 1162x2490 box for artwork
   that is 1162x504. An author `aspect-ratio` matches exactly when this query
   does, and outranks the ratio the browser derives from the <img> attributes.

   SCOPED TO --swap, which the template adds only when the mobile file's own
   dimensions are known. That scoping is not tidiness: applied to every image in
   this query as `var(--sv-schema-ar-m, auto)`, the `auto` fallback overrode -
   and threw away - the ratio the browser derives from the <img> width/height
   attributes, so every schema WITHOUT a mobile variant collapsed to the
   min-height floor below 700px until its file decoded. */
@media (max-width: 700px) {
  body.service-v2 .sv-schema__figure--swap .sv-schema__img {
    aspect-ratio: var(--sv-schema-ar-m);
  }
}

/* ---- Section background: editor-chosen colour or image ----------------
   Set by qm_srv_section_bg(). The PHP hands over a colour or a URL and nothing
   else; every size, position and opacity is here, so an editor can change the
   backdrop but not the design.

   Written against .sv-section rather than .sv-schema so another section can
   adopt the same four fields without a second copy of these rules. */

/* Beats the flat `background: var(--sv-bg-2)` on .sv-section by one class. */
body.service-v2 .sv-section.sv-bg--color {
  background: var(--sv-bg);
}

/* CHILD selectors, not descendant. These four rules are the only ones in the
   background feature that reach shared classes, and the helper is explicitly
   built to be reused by other sections - the first time it is applied to a
   wrapper rather than to a <section>, a descendant selector would repaint every
   heading inside it. `> .sv-container >` keeps each rule to the section that
   actually carries the background. */

/* The heading and lead follow the picked colour: --sv-bg-ink is derived from it
   in PHP by measuring real contrast ratios, so a pale band flips the text to
   near-black instead of leaving white on white.

   The ACCENT half of a heading keeps its own colour deliberately - it is a brand
   colour, and both the purple and the teal hold up against a light band as well
   as a dark one. Only the plain half needs to move. */
body.service-v2 .sv-bg--color > .sv-container > .sv-section__title,
body.service-v2 .sv-bg--color > .sv-container > .sv-section__lead {
  color: var(--sv-bg-ink, var(--sv-white));
}

body.service-v2 .sv-section.sv-bg--image {
  position: relative;
  background-color: var(--sv-bg-2);   /* shows through if the file 404s */
  background-image: var(--sv-bg-img);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  /* Contains the scrim's negative z-index so it cannot slide behind the page
     background and disappear - without a stacking context of its own, a
     `z-index: -1` child paints behind its nearest positioned ANCESTOR. */
  isolation: isolate;
}

/* The scrim sits between the image and the content: negative z-index puts it
   under the section's children but, thanks to `isolation` above, still over the
   section's own background-image. Doing it this way rather than layering a
   gradient into `background-image` keeps the editor's URL the only thing in that
   property, which is what makes the PHP side a single esc_url(). */
body.service-v2 .sv-section.sv-bg--image::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: rgba(8, 6, 13, var(--sv-bg-scrim, 0.6));
}

/* Over an image the text goes PURE WHITE, both lines. The page's normal lead
   grey (#CCCBD2) is tuned for a near-black band and is too weak over a scrimmed
   photograph: measured against the lightest pixel of a pale background image at
   the Medium scrim it scored 3.9:1, under the 4.5:1 AA floor for body text,
   while white on the same pixel scores 7.04:1. The heading was never the problem
   - it is large text, so it only needs 3:1 - the subtitle was.

   The accent half keeps its own brand colour, as it does on a flat background. */
body.service-v2 .sv-bg--image > .sv-container > .sv-section__title,
body.service-v2 .sv-bg--image > .sv-container > .sv-section__lead {
  color: var(--sv-white);
}

/* Scrim strengths, chosen by measuring the WORST case an editor can produce: a
   near-white background image (#E8E2FF), white text, checked against WCAG AA.
   With a normal dark-brand image every number here is far better.

     none    1.18:1 - fails everything. Only for artwork already dark and even.
     light   3.65:1 - heading yes (large text needs 3:1), subtitle no.
     medium  7.04:1 - both pass. Which is why it is the default.
     strong 13.80:1 - both pass with room to spare.

   Light and None are deliberately still offered: an editor who has drawn a dark
   backdrop should not have to fight a wash they do not need. The field
   instructions carry the same warning, so the choice is informed rather than
   blocked. */
body.service-v2 .sv-bg--scrim-none::before   { --sv-bg-scrim: 0; }
body.service-v2 .sv-bg--scrim-light::before  { --sv-bg-scrim: 0.45; }
body.service-v2 .sv-bg--scrim-medium::before { --sv-bg-scrim: 0.65; }
body.service-v2 .sv-bg--scrim-strong::before { --sv-bg-scrim: 0.85; }

/* ================= Homepage sections reused on Services =================
   These render templates/home/<part>.php. css/home.css is FRONT-PAGE ONLY and
   every rule in it is scoped to the homepage body class, so it can never dress
   them here. The rules are copied across and re-scoped to body.service-v2; the
   homepage keeps its own copy untouched and the two never load together.
   See inc/home-sections.php.

   EXTRACTED WITH A BRACE-WALKING PARSER, not a regex, and that matters:
     - @media context is PRESERVED. An earlier pass flattened 211 media rules
       into unconditional ones and lost `.home-blog .container` (which lives in
       `@media (min-width: 769px)`), which is why the blog button fell below the
       cards instead of sitting beside the heading.
     - at-rules are carried when referenced. `@property --agent-angle` is what
       makes that custom property an <angle> the browser can interpolate; without
       it the agents glow jumps between corners.
     - class matching uses NO trailing  - "_" is a word character, so
       `\.home-acc` silently skips every `.home-acc__child`.
     - relative url() is rebased: `../assets/` is correct from css/ and resolves
       to assets/assets/ from assets/styles/, giving 404s.

   ---- Shared chrome ----
   Every home part wraps itself in `.home-section` and uses the same head,
   accent and button classes. Bridged ONCE here so each section carries only its
   own rules. Values match the `.home-tools` bridge above, which is already tuned
   to the v2 rhythm; that block stays and still wins where the two overlap.

   Scoped inside `.home-section`, so these generic names cannot reach the legacy
   header, footer or any sv- section. */
body.service-v2 .home-section {
  background: var(--sv-bg-2);
  padding: clamp(56px, 7vw, 96px) 0;
  position: relative;
}
body.service-v2 .home-section > .container {
  width: 100%;
  max-width: var(--sv-container);
  margin-inline: auto;
  padding-inline: 19px;
}
body.service-v2 .home-section .home-section__head {
  max-width: 1000px;
  margin: 0 auto 45px;
  text-align: center;
}
body.service-v2 .home-section .home-section__head h2 {
  font-family: var(--sv-sans);
  font-weight: 700;
  font-size: clamp(28px, 3.4vw, 48px);
  line-height: 1.2;
  letter-spacing: -0.01em;
  color: var(--sv-white);
  margin: 0;
  text-wrap: balance;
}
body.service-v2 .home-section .home-section__head p {
  margin: 14px 0 0;
  color: var(--sv-grey-light);
  font-size: clamp(15px, 1.3vw, 20px);
  line-height: 1.4;
}
body.service-v2 .home-section .home-accent-text-purple { color: var(--sv-purple); }
body.service-v2 .home-section .home-accent-text-green  { color: var(--sv-green); }

/* The homepage button, in the two tones these sections use. */
body.service-v2 .home-section .home-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 14px 26px;
  border: 0;
  border-radius: 999px;
  font-family: var(--sv-sans);
  font-weight: 700;
  font-size: 15px;
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
  transition: gap 0.4s cubic-bezier(0.4, 0, 0.2, 1), background 0.3s ease;
}
body.service-v2 .home-section .home-btn svg { flex: none; }
body.service-v2 .home-section .home-btn:hover { gap: 20px; }
body.service-v2 .home-section .home-btn--green  { background: var(--sv-green);  color: #05221C; }
body.service-v2 .home-section .home-btn--green svg path  { stroke: #05221C; }
body.service-v2 .home-section .home-btn--purple { background: var(--sv-purple); color: #fff; }
body.service-v2 .home-section .home-btn--purple svg path { stroke: #fff; }
body.service-v2 .home-section .home-btn:focus-visible {
  outline: 2px solid var(--sv-lilac);
  outline-offset: 3px;
}

/* ---- Results (stat cards) ---- */
body.service-v2 .home-stat { display: inline-flex; align-items: center; gap: 10px; text-align: left; }
body.service-v2 .home-stat__icon { display: grid; place-items: center; flex: none; }
body.service-v2 .home-stat__icon svg { display: block; }
body.service-v2 .home-stat__icon-img { display: block; width: 36px; height: 36px; object-fit: contain; border-radius: 8px; }
body.service-v2 .home-stat__text { display: inline-flex; flex-direction: column; }
body.service-v2 .home-stat__title { font-size: 16px; font-weight: 500; line-height: 1.1; }
body.service-v2 .home-stat__title--green { color: #68DCC7; }
body.service-v2 .home-stat__title--purple { color: #CBB9F9; }
body.service-v2 .home-stat__sub { font-size: 12px; color: #fff; }
body.service-v2 .home-results { background: #110D1A; }
body.service-v2 .home-results__eyebrow { display: inline-flex; align-items: center; justify-content: center; padding: 8px 16px; white-space: nowrap; margin-bottom: 16px; border-radius: var(--h-pill); border: 1px solid #00886F; background: #0F1419; color: #00CCA6; font-size: 12px; font-weight: 700; }
body.service-v2 .home-results__eyebrow { display: none; }
body.service-v2 .home-results__grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 19px; }
body.service-v2 .home-results__item { display: flex; flex-direction: column; align-items: center; text-align: center; padding: 26px 14px; background: var(--h-card); border: 1px solid var(--h-border); border-radius: var(--h-radius); }
body.service-v2 .home-results__item--purple { border-color: rgba(142, 108, 255, 0.35); background: #110E19; }
body.service-v2 .home-results__item--green { border-color: rgba(0, 204, 166, 0.35); background: #0D1217; }
body.service-v2 .home-results__ico { width: 37px; height: 37px; border-radius: 10px; display: grid; place-items: center; margin-bottom: 18px; }
body.service-v2 .home-results__ico--has-img { background: none; }
body.service-v2 .home-results__ico-img { width: 100%; height: 100%; object-fit: contain; }
body.service-v2 .home-results__item--purple .home-results__ico { background: rgba(142, 108, 255, 0.16); color: #a78bff; }
body.service-v2 .home-results__item--green .home-results__ico { background: rgba(0, 204, 166, 0.16); color: #00cca6; }
body.service-v2 .home-results__num { display: block; font-size: clamp(26px, 3.2vw, 37px); font-weight: 700; line-height: 1; }
body.service-v2 .home-results__item--purple .home-results__num { color: #a78bff; }
body.service-v2 .home-results__item--green .home-results__num { color: #00cca6; }
body.service-v2 .home-results__label { display: block; margin-top: 11px; color: #fff; font-size: 18px; font-weight: 500; }
body.service-v2 .home-results__sub { display: block; margin-top: 6px; color: var(--h-muted); font-size: 14px; }
body.service-v2 .home-results__cta { position: relative; margin-top: 35px; display: flex; align-items: center; justify-content: space-between; gap: 19px; padding: 22px clamp(22px, 3.2vw, 38px); border-radius: var(--h-radius-lg); border: 1px solid var(--h-border); overflow: hidden; background: radial-gradient(60% 160% at 42% 50%, rgba(63, 90, 230, 0.45), transparent 60%), radial-gradient(45% 150% at 60% 60%, rgba(0, 200, 150, 0.4), transparent 60%), #0c0c14; }
body.service-v2 .home-results__cta.has-bg { background: var(--results-cta-bg) center / cover no-repeat; }
body.service-v2 .home-results__cta-text { position: relative; font-size: clamp(13px, 1.36vw, 18px); font-weight: 500; color: #fff; max-width: 650px; }
body.service-v2 .home-results__cta-btn { position: relative; flex: none; }
@media (max-width: 900px) {
  body.service-v2 .home-results__grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 560px) {
  body.service-v2 .home-results__grid { grid-template-columns: 1fr; }
}
@media (max-width: 768px) {
  body.service-v2 .home-results__grid { grid-template-columns: 1fr; }
  body.service-v2 .home-results__cta { flex-direction: column; align-items: center; text-align: center; }
}
@media (max-width: 480px) {
  body.service-v2 .home-results__num { font-size: 29px; }
}

/* ---- Intro card ---- */
body.service-v2 .site-homepage .home-intro__text { overflow-wrap: break-word; }
body.service-v2 .home-section--tight { padding: 51px 0; }
body.service-v2 .home-section:not(.home-clients):not(.home-testimonials).home-intro { padding-bottom: 28px; }
body.service-v2 .home-intro .home-accent-purple { background: none; -webkit-text-fill-color: var(--intro-accent, #7d51f1); color: var(--intro-accent, #7d51f1); }
body.service-v2 .home-intro { background: #14101D; }
body.service-v2 .home-intro { text-align: center; }
body.service-v2 .home-intro__card { position: relative; max-width: 1200px; min-height: clamp(208px, 27.2vw, 323px); margin: 0 auto; display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; padding: clamp(29px, 4vw, 51px) clamp(19px, 4vw, 64px); border-radius: var(--h-radius-lg); border: 1px solid var(--h-border); background: radial-gradient(42% 56% at 80% 24%, rgba(110, 64, 240, 0.60), rgba(110, 64, 240, 0) 70%), radial-gradient(46% 52% at 92% 80%, rgba(82, 40, 205, 0.52), rgba(82, 40, 205, 0) 72%), radial-gradient(95% 130% at 102% 52%, rgba(60, 32, 150, 0.30), rgba(60, 32, 150, 0) 70%), #0a0a0f; overflow: hidden; }
body.service-v2 .home-intro__card.has-bg { background: var(--intro-bg) center / cover no-repeat; border: 0; }
body.service-v2 .site-homepage .home-intro__text { font-family: var(--h-serif); font-size: clamp(22px, 3.36vw, 40px); line-height: 1.12; color: #fff; }
body.service-v2 .home-intro__lead, body.service-v2 .home-intro__text .home-accent-purple { display: block; }
body.service-v2 .home-intro__lead { color: #fff; }
body.service-v2 .home-intro__pills { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px 22px; margin: 19px 0 0; padding: 0; list-style: none; }
body.service-v2 .home-intro__pills li { display: inline-flex; align-items: center; gap: 6px; color: var(--h-muted); font-size: 11px; font-weight: 500; margin-bottom: 24px; }
body.service-v2 .home-intro__dot { width: 6px; height: 6px; border-radius: 50%; background: var(--h-purple); flex: none; }
body.service-v2 .home-intro__sub { max-width: 800px; margin: 18px auto 0; color: white; font-size: 16px; line-height: 1.6; font-weight: 300; }
body.service-v2 .home-intro__sub strong { color: #fff; font-weight: 600; }
body.service-v2 .home-intro__sub > :first-child { margin-top: 0; }
body.service-v2 .home-intro__sub > :last-child { margin-bottom: 0; }
@media (max-width: 768px) {
  body.service-v2 .site-homepage .home-intro__text { font-size: 18px; line-height: 1.2; }
  body.service-v2 .site-homepage .home-intro__sub { font-size: 13px; }
  body.service-v2 .home-intro__card { padding-left: 26px; padding-right: 26px; }
  body.service-v2 .home-intro__card.has-bg { background: radial-gradient(42% 56% at 80% 24%, rgba(110, 64, 240, 0.60), rgba(110, 64, 240, 0) 70%), radial-gradient(46% 52% at 92% 80%, rgba(82, 40, 205, 0.52), rgba(82, 40, 205, 0) 72%), radial-gradient(95% 130% at 102% 52%, rgba(60, 32, 150, 0.30), rgba(60, 32, 150, 0) 70%), #0a0a0f; border: 1px solid var(--h-border); }
}
@media (max-width: 480px) {
  body.service-v2 .home-intro__text { font-size: 22px; }
}

/* ---- Problems + comparison ---- */
body.service-v2 .home-section:not(.home-clients):not(.home-testimonials).home-problems { padding-top: 28px; }
body.service-v2 .home-problems { background: #14101D; }
body.service-v2 .home-cmp { max-width: 1200px; margin: 45px auto 0; }
body.service-v2 .home-cmp__row { display: grid; grid-template-columns: 1.5fr 1fr 1fr; }
body.service-v2 .home-cmp__cell { padding: 14px 18px; color: #fff; display: flex; align-items: center; gap: 6px; }
body.service-v2 .home-cmp__ico { flex: none; width: 18px; height: 18px; object-fit: contain; }
body.service-v2 .home-cmp__cell--feature { color: #fff; font-weight: 500; }
body.service-v2 .home-cmp__row:nth-child(3) .home-cmp__cell--feature { border-top: 1px solid #211F25; border-bottom: 1px solid #211F25; }
body.service-v2 .home-cmp__row:nth-child(4) .home-cmp__cell--feature { border-bottom: 1px solid #211F25; }
body.service-v2 .home-cmp__cell--with { background: rgba(63, 227, 182, 0.06); border-inline: 1px solid rgba(63, 227, 182, 0.4); border-top: 1px solid rgba(63, 227, 182, 0.4); }
body.service-v2 .home-cmp__row--head .home-cmp__cell { font-weight: 700; color: #fff; text-transform: uppercase; letter-spacing: 0.06em; font-size: 14px; }
body.service-v2 .home-cmp__row--head .home-cmp__cell--without { color: #ff5470; }
body.service-v2 .home-cmp__row--head .home-cmp__cell--with { color: var(--h-green); border-top: 1px solid rgba(63, 227, 182, 0.4); border-radius: 0 10px 0 0; }
body.service-v2 .home-cmp__row:last-child .home-cmp__cell--with { border-bottom: 1px solid rgba(63, 227, 182, 0.4); border-radius: 0 0 10px 0; }
body.service-v2 .home-cmp__cell--with b { color: #fff; font-weight: 700; }
body.service-v2 .home-cmp__cell--without { background: rgba(255, 255, 255, 0.02); border-inline: 1px solid var(--h-border); border-top: 1px solid var(--h-border); }
body.service-v2 .home-cmp__row--head .home-cmp__cell--without { border-top: 1px solid var(--h-border); border-radius: 10px 0 0 0; }
body.service-v2 .home-cmp__row:last-child .home-cmp__cell--without { border-bottom: 1px solid var(--h-border); border-radius: 0 0 0 10px; }
body.service-v2 .home-cmp__note { color: var(--h-green); font-style: normal; font-size: 10px; }
body.service-v2 .home-problems__notes { max-width: 1200px; margin: 29px auto 0; display: flex; flex-direction: column; gap: 11px; }
@media (max-width: 768px) {
  body.service-v2 .home-cmp { margin-top: 28px; }
  body.service-v2 .home-cmp__cell { padding: 12px 10px; font-size: 12px; font-weight: 400; min-width: 0; }
  body.service-v2 .home-cmp__row--head { grid-template-columns: 1fr 1fr; gap: 8px; }
  body.service-v2 .home-cmp__row--head .home-cmp__cell--feature { display: none; }
  body.service-v2 .home-cmp__row--head .home-cmp__cell { flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: 6px; font-size: 11px; letter-spacing: 0; white-space: normal; }
  body.service-v2 .home-cmp__row:not(.home-cmp__row--head) { grid-template-columns: 1fr 1fr; grid-template-areas: "feat feat" "without with"; gap: 8px; margin-top: 16px; }
  body.service-v2 .home-cmp__cell--feature { grid-area: feat; justify-content: center; text-align: center; color: var(--h-muted); padding: 0 0 2px; border: 0 !important; }
  body.service-v2 .home-cmp__row:not(.home-cmp__row--head) .home-cmp__cell--without { grid-area: without; }
  body.service-v2 .home-cmp__row:not(.home-cmp__row--head) .home-cmp__cell--with { grid-area: with; }
  body.service-v2 .home-cmp__cell--without, body.service-v2 .home-cmp__cell--with { flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: 2px; border-radius: 12px !important; border: 1px solid var(--h-border) !important; }
  body.service-v2 .home-cmp__cell--with { border-color: rgba(63, 227, 182, 0.4) !important; }
  body.service-v2 .home-cmp__note { display: block; }
}

/* ---- Agent cards ---- */
body.service-v2 .home-product__text > :first-child { margin-top: 0; }
body.service-v2 .home-product__text > :last-child { margin-bottom: 0; }
body.service-v2 .home-agents__diagram { display: grid; grid-template-columns: minmax(0, 288px) minmax(0, 592px) minmax(0, 288px); justify-content: center; column-gap: 16px; align-items: center; }
body.service-v2 .home-agents__col { display: flex; flex-direction: column; gap: 16px; }
body.service-v2 .home-agent-card { height: 161px; background: var(--h-card); border: 1px solid var(--h-border); border-radius: var(--h-radius); padding: 18px; text-align: center; display: flex; flex-direction: column; align-items: center; justify-content: center; transition: border-color 1.4s cubic-bezier(0.4, 0, 0.2, 1), background 1.4s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 1.4s cubic-bezier(0.4, 0, 0.2, 1); }
body.service-v2 .home-agent-card.is-active { border-color: #046654; background: rgba(6, 66, 60, 0.12); box-shadow: 0 0 0 1px rgba(6, 66, 60, 0.4), 0 11px 27px rgba(6, 66, 60, 0.22); }
body.service-v2 .home-agent-card__ico { width: 35px; height: 35px; border-radius: 10px; display: grid; place-items: center; overflow: hidden; margin-bottom: 13px; background: #00886F; transition: background 1.4s cubic-bezier(0.4, 0, 0.2, 1); }
body.service-v2 .home-agent-card.is-active .home-agent-card__ico { background: #00CCA6; }
body.service-v2 .home-agent-card__glyph { width: 20px; height: 20px; background-color: #68DCC7; -webkit-mask: var(--glyph) center / contain no-repeat; mask: var(--glyph) center / contain no-repeat; transition: background-color 1.4s cubic-bezier(0.4, 0, 0.2, 1); }
body.service-v2 .home-agent-card.is-active .home-agent-card__glyph { background-color: #00886F; }
body.service-v2 .home-agent-card__ico img { width: 100%; height: 100%; display: block; opacity: 0.5; }
body.service-v2 .home-agent-card.is-active .home-agent-card__ico img { opacity: 1; }
body.service-v2 .home-agent-card__title { font-size: 18px; font-weight: 700; color: #fff; margin-bottom: 3px; }
body.service-v2 .home-agent-card__sub { color: var(--sv-white); font-size: 14px; }
body.service-v2 .home-agents__center { display: flex; }
body.service-v2 .home-agents__node { position: relative; width: 100%; height: 338px; border-radius: 999px; border: 0.5px solid transparent; background: linear-gradient(#08070E, #08070E) padding-box, conic-gradient(from var(--agent-angle, 45deg), #046654 0deg, #0D1A18 180deg, #046654 360deg) border-box; background-origin: border-box; background-clip: padding-box, border-box; transition: --agent-angle 1.4s cubic-bezier(0.4, 0, 0.2, 1); overflow: hidden; display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; padding: 26px; }
body.service-v2 .home-agents__node::before { content: ""; position: absolute; inset: -45%; background: radial-gradient(circle 640px at center, rgba(6, 66, 60, 1) 0%, rgba(6, 66, 60, 0.82) 16%, rgba(6, 66, 60, 0.58) 31%, rgba(6, 66, 60, 0.38) 46%, rgba(6, 66, 60, 0.22) 62%, rgba(6, 66, 60, 0.1) 80%, rgba(6, 66, 60, 0) 100%); filter: blur(60px); z-index: 0; transform-origin: center; transform: rotate(var(--agent-angle, 45deg)) translateY(-250px); will-change: transform; }
body.service-v2 .home-agents__node > * { position: relative; z-index: 1; }
body.service-v2 .home-agents__node-eyebrow { color: #fff; font-size: 16px; font-weight: 700; letter-spacing: 0; text-transform: none; margin-bottom: 8px; }
body.service-v2 .home-agents__node-title { font-size: clamp(21px, 2.4vw, 32px); font-weight: 500; color: #00cca6; line-height: 1.1; }
body.service-v2 .home-agents__node-sub { color: rgba(255, 255, 255, 0.85); font-size: 14px; margin-top: 10px; }
body.service-v2 .home-agents__node-dot { color: var(--h-green); font-size: 0.6em; vertical-align: middle; }
body.service-v2 .home-agents__products { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; margin-top: 35px; }
/* HOVER JITTER FIX (QA, 2026-10-08): the hover is read off .home-product-cell,
   which never moves; the card inside lifts. Hovering the card itself, its 6px
   lift slid the bottom edge out from under a cursor parked there, the hover
   dropped, the card fell back under the cursor - a loop that jittered the card
   and its button. The cell is the grid item and stretches, so the card keeps
   filling it (equal heights as before). */
body.service-v2 .home-product-cell { display: flex; min-width: 0; }
body.service-v2 .home-product-cell > .home-product { flex: 1 1 auto; min-width: 0; }
body.service-v2 .home-product { position: relative; border-radius: var(--h-radius-lg); padding: 22px; overflow: hidden; display: flex; flex-direction: column; transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1), box-shadow 0.6s cubic-bezier(0.22, 1, 0.36, 1), border-color 0.6s ease; }
body.service-v2 .home-product-cell:hover > .home-product { transform: translateY(-6px); box-shadow: 0 18px 40px rgba(0, 0, 0, 0.45); }
body.service-v2 .home-product-cell:hover > .home-product--purple { border-color: rgba(142, 108, 255, 0.6); }
body.service-v2 .home-product-cell:hover > .home-product--green { border-color: rgba(63, 227, 182, 0.6); }
body.service-v2 .home-product--purple { background: url("../images/purple-back.png?v=3") center / 100% 100% no-repeat; border: 1px solid rgba(142, 108, 255, 0.35); }
body.service-v2 .home-product--green { background: url("../images/green-back.png?v=3") center / 100% 100% no-repeat; border: 1px solid rgba(63, 227, 182, 0.35); }
body.service-v2 .home-product__badge { align-self: flex-start; padding: 5px 11px; border-radius: var(--h-pill); font-size: 10px; font-weight: 700; border: 1px solid currentColor; margin-bottom: 24px; }
body.service-v2 .home-product--purple .home-product__badge { color: var(--h-purple); }
body.service-v2 .home-product--green .home-product__badge { color: var(--h-green); }
/* font-weight is DECLARED, not inherited from the <h3> default.

   The card only ever looked bold because the surfaces it shipped on - the
   homepage and Service v2 - do not load dist/styles/main_new.css, so the UA's
   bold <h3> survived. Add the section to a Default-template page or the Page
   Builder, where that sheet IS loaded, and its Bootstrap-style heading rule
   takes the title down to 400 and the card reads unstyled. Measured: 700 on the
   homepage and on service-v2, 400 on page-flexible.

   Stating the weight here makes the component carry its own appearance instead
   of depending on which global sheet a template happens to load. */
body.service-v2 .home-product .home-product__title { font-size: 18px; font-weight: 700; margin: 0 0 24px; }
body.service-v2 .home-product .home-product__text { color: rgba(255, 255, 255, 0.9); font-size: 12px; line-height: 1.6; margin: 0 0 32px; }
body.service-v2 .home-product__foot { margin-top: auto; position: relative; display: flex; align-items: center; justify-content: center; gap: 8px; padding: 11px 14px; border-radius: var(--h-radius); background: rgba(0, 0, 0, 0.28); color: #fff; font-size: 11px; font-weight: normal; text-decoration: none; transition: background 0.6s cubic-bezier(0.22, 1, 0.36, 1), color 0.6s ease; }
body.service-v2 .home-product__foot-ico { display: inline-flex; position: absolute; left: 14px; top: 50%; transform: translateY(-50%); transition: left 0.7s cubic-bezier(0.22, 1, 0.36, 1), color 0.6s ease; }
body.service-v2 .home-product--purple .home-product__foot-ico { color: var(--h-purple); }
body.service-v2 .home-product--green .home-product__foot-ico { color: var(--h-green); }
body.service-v2 .home-product__foot-text { text-align: center; }
body.service-v2 .home-product__foot--static { cursor: default; }
body.service-v2 .home-product-cell:hover > .home-product--purple .home-product__foot { background: var(--h-purple); color: #0c0a18; }
body.service-v2 .home-product-cell:hover > .home-product--green .home-product__foot { background: var(--h-green); color: #07120f; }
body.service-v2 .home-product-cell:hover > .home-product .home-product__foot-ico { left: calc(100% - 32px); color: inherit; }
@media (prefers-reduced-motion: reduce) {
  body.service-v2 .home-agents__node { transition: none; }
}
@media (max-width: 1040px) {
  body.service-v2 .home-agents__diagram { grid-template-columns: 1fr; justify-content: stretch; column-gap: 0; row-gap: 18px; }
  body.service-v2 .home-agents__center { order: -1; }
  body.service-v2 .home-agents__col { display: grid; grid-template-columns: 1fr 1fr; gap: 13px; }
  body.service-v2 .home-agent-card { height: auto; min-height: 120px; }
  body.service-v2 .home-agents__node { width: 100%; height: auto; min-height: 192px; }
  body.service-v2 .home-agents__products { grid-template-columns: 1fr; }
}
@media (max-width: 768px) {
  body.service-v2 .home-agents__diagram { grid-template-columns: 1fr 1fr; column-gap: 13px; row-gap: 13px; }
  body.service-v2 .home-agents__col { display: contents; }
  body.service-v2 .home-agents [data-agent="tl"] { order: 1; }
  body.service-v2 .home-agents [data-agent="tr"] { order: 2; }
  body.service-v2 .home-agents__center { order: 3; grid-column: 1 / -1; }
  body.service-v2 .home-agents [data-agent="bl"] { order: 4; }
  body.service-v2 .home-agents [data-agent="br"] { order: 5; }
}

/* ---- Three ways ---- */
body.service-v2 .home-ways { background: #110d1a; }
/* NOT PORTED: `padding-bottom: 22px`. On the homepage that is one half of a pair
   - the other half is `.home-ctaai { padding-top: 22px }` - and together they
   tighten the gap between the comparison TABLE and the AI CTA banner that
   follows it there. On a Service the editor picks the order and the pair does not
   survive: measured on /services/automated-testing/ and /services/manual-testing/,
   `.home-ways` is followed by `.home-domains`, so the 22px was unpaired and the
   table sat 34px tight against the next section. Both halves are dropped here.
   (`.home-intro` / `.home-problems` below keep theirs - checked, those two really
   are adjacent in the registry order.) */
body.service-v2 .home-ways__matrix { max-width: 1200px; margin: 0 auto; }
body.service-v2 .home-ways__table { border: 1px solid var(--h-border); border-radius: var(--h-radius-lg); background: rgba(255, 255, 255, 0.015); }
body.service-v2 .home-ways__row { display: grid; grid-template-columns: minmax(0, 1fr) 266px 266px 266px; align-items: stretch; border-bottom: 1px solid var(--h-border); }
body.service-v2 .home-ways__row:last-child { border-bottom: 0; }
body.service-v2 .home-ways__cell { padding: 18px; text-align: center; color: #fff; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px; position: relative; }
body.service-v2 .home-ways__cell--feature { align-items: flex-start; text-align: left; color: #fff; font-weight: 600; font-size: 14px; }
body.service-v2 .home-ways__cell:not(.home-ways__cell--feature):not(.is-highlight) { border-left: 1px solid var(--h-border); }
body.service-v2 .home-ways__row:not(.home-ways__row--head) .home-ways__cell { height: 88px; justify-content: flex-start; }
body.service-v2 .home-ways__row:not(.home-ways__row--head) .home-ways__cell:not(.home-ways__cell--note) { justify-content: center; }
body.service-v2 .home-ways__row--head .home-ways__cell { font-weight: 700; color: #fff; text-transform: uppercase; letter-spacing: 0.04em; font-size: 18px; }
body.service-v2 .home-ways__row--head .home-ways__cell--feature { color: #fff; }
body.service-v2 .home-ways__cell.is-highlight { background: rgba(63, 227, 182, 0.05); border-inline: 1px solid rgba(63, 227, 182, 0.4); }
body.service-v2 .home-ways__row--head .home-ways__cell.is-highlight { color: var(--h-green); border-top: 1px solid rgba(63, 227, 182, 0.4); border-radius: 11px 11px 0 0; }
body.service-v2 .home-ways__row:last-child .home-ways__cell.is-highlight { border-bottom: 1px solid rgba(63, 227, 182, 0.4); border-radius: 0 0 11px 11px; }
body.service-v2 .home-ways__mark { display: inline-flex; }
body.service-v2 .home-ways__mark-img { width: 20px; height: 20px; object-fit: contain; }
body.service-v2 .home-ways__note { position: absolute; left: 0; right: 0; bottom: 24px; font-size: 12px; color: var(--h-muted-2); }
body.service-v2 .home-ways__captions { display: grid; grid-template-columns: 1.7fr 1fr 1fr 1fr; margin-top: 24px; }
body.service-v2 .home-ways__caption { padding: 0 13px; text-align: center; color: #fff; font-size: 12px; font-weight: 700; line-height: 1.4; }
body.service-v2 .home-ways__caption.is-highlight { color: var(--h-green); }
@media (max-width: 900px) {
  body.service-v2 .home-ways__matrix { overflow-x: auto; }
  body.service-v2 .home-ways__row, body.service-v2 .home-ways__captions { grid-template-columns: 1.5fr 1fr 1fr 1fr; min-width: 544px; }
}
@media (max-width: 768px) {
  body.service-v2 .home-ways__matrix { overflow-x: visible; }
  body.service-v2 .home-ways__table { border: 0; background: none; border-radius: 0; }
  body.service-v2 .home-ways__row--head .home-ways__cell--feature, body.service-v2 .home-ways__cap-feature { display: none; }
  body.service-v2 .home-ways__row { grid-template-columns: 1fr 1fr 1fr; min-width: 0; border-bottom: 0; margin-bottom: 14px; }
  body.service-v2 .home-ways__captions { grid-template-columns: 1fr 1fr 1fr; min-width: 0; }
  body.service-v2 .home-ways__cell { min-width: 0; padding: 12px 6px; }
  body.service-v2 .home-ways__row:not(.home-ways__row--head) .home-ways__cell--feature { grid-column: 1 / -1; align-items: center; text-align: center; color: var(--h-muted); font-weight: 500; font-size: 13px; height: auto; padding: 4px 8px 12px; }
  body.service-v2 .home-ways__cell:not(.home-ways__cell--feature) { border-top: 1px solid var(--h-border); border-bottom: 1px solid var(--h-border); border-left: 1px solid var(--h-border); }
  body.service-v2 .home-ways__cell--feature + .home-ways__cell { border-radius: 11px 0 0 11px !important; }
  body.service-v2 .home-ways__cell:not(.home-ways__cell--feature):last-child { border-right: 1px solid var(--h-border); border-radius: 0 11px 11px 0 !important; }
  body.service-v2 .home-ways__row .home-ways__cell.is-highlight { border-color: rgba(63, 227, 182, 0.4) !important; }
  body.service-v2 .home-ways__row--head .home-ways__cell { font-size: 11px; letter-spacing: 0; }
  body.service-v2 .home-ways__row:not(.home-ways__row--head) .home-ways__cell { height: auto; min-height: 64px; justify-content: center; }
  body.service-v2 .home-ways__note { position: static; left: auto; right: auto; bottom: auto; font-size: 10px; margin-top: 4px; }
  body.service-v2 .home-ways__caption { font-size: 11px; padding: 0 6px; }
}

/* ---- Industry cards ---- */
body.service-v2 .home-domain__text > :first-child { margin-top: 0; }
body.service-v2 .home-domain__text > :last-child { margin-bottom: 0; }
body.service-v2 .home-domains__grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 19px; }
body.service-v2 .home-domain { background: var(--h-card); border: 1px solid var(--h-border); border-radius: var(--h-radius); padding: 21px; }
body.service-v2 .home-domain--purple { border-color: rgba(142, 108, 255, 0.35); }
body.service-v2 .home-domain--green { border-color: rgba(0, 204, 166, 0.35); background: #0F1419; }
body.service-v2 .home-domain__top { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 16px; }
body.service-v2 .home-domain__ico { flex: none; width: 48px; height: 48px; border-radius: 12px; display: grid; place-items: center; overflow: hidden; }
body.service-v2 .home-domain__ico-img { width: 100%; height: 100%; display: block; }
body.service-v2 .home-domain--purple .home-domain__ico { background: rgba(142, 108, 255, 0.16); color: #a78bff; }
body.service-v2 .home-domain--green .home-domain__ico { background: rgba(0, 204, 166, 0.16); color: #00cca6; }
body.service-v2 .home-domain__badge { padding: 5px 11px; border-radius: var(--h-pill); border: 1px solid; font-size: 14px; font-weight: 500; }
body.service-v2 .home-domain--purple .home-domain__badge { border-color: #5336A1; color: #7D51F1; }
body.service-v2 .home-domain--green .home-domain__badge { border-color: #00886F; color: #00CCA6; }
body.service-v2 .home-domain__title { font-size: 18px; font-weight: 700; margin-bottom: 10px; }
body.service-v2 .home-domain__text { color: #fff; font-size: 14px; line-height: 1.6; font-weight: lighter; }
body.service-v2 .home-domains { background: #110D1A; }
@media (max-width: 900px) {
  body.service-v2 .home-domains__grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 560px) {
  body.service-v2 .home-domains__grid { grid-template-columns: 1fr; }
}
@media (max-width: 768px) {
  body.service-v2 .home-domains__grid { grid-template-columns: 1fr; }
}

/* ---- How we work ---- */
body.service-v2 .home-work__sub > :first-child { margin-top: 0; }
body.service-v2 .home-work__sub > :last-child { margin-bottom: 0; }
body.service-v2 .home-work__grid { display: grid; grid-template-columns: repeat(2, 1fr); align-items: start; gap: 12px; max-width: 1000px; margin: 0 auto; }
body.service-v2 .home-work__item { border-radius: var(--h-radius); background: transparent; transition: background 0.45s cubic-bezier(0.4, 0, 0.2, 1); }
body.service-v2 .home-work__item:hover { background: #2C2A31; }
body.service-v2 .home-work__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; padding: 22px 24px; }
body.service-v2 .home-work__title { font-size: 14px; font-weight: 500; letter-spacing: 0.06em; text-transform: uppercase; color: #fff; transition: color 0.45s cubic-bezier(0.4, 0, 0.2, 1); }
body.service-v2 .home-work__item:hover .home-work__title { color: var(--h-green); }
body.service-v2 .home-work__sub { display: block; margin-top: 10px; color: #fff; font-weight: lighter; font-size: 18px; line-height: 1.6; }
body.service-v2 .home-work__icon { flex: none; color: var(--h-muted); display: inline-flex; transition: color 0.45s cubic-bezier(0.4, 0, 0.2, 1); }
body.service-v2 .home-work__item:hover .home-work__icon { color: var(--h-green); }
body.service-v2 .home-work__icon-close { display: none; }
body.service-v2 .home-work__item:hover .home-work__icon-arrow { display: none; }
body.service-v2 .home-work__item:hover .home-work__icon-close { display: inline-flex; }
body.service-v2 .home-work__body { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.5s cubic-bezier(0.4, 0, 0.2, 1); }
body.service-v2 .home-work__item:hover .home-work__body { grid-template-rows: 1fr; }
body.service-v2 .home-work__body-inner { overflow: hidden; }
body.service-v2 .home-work__body p { color: #fff; font-size: 14px; font-weight: lighter; line-height: 1.6; margin: 0 24px 16px; }
body.service-v2 .home-work__body p:last-child { margin-bottom: 22px; }
body.service-v2 .home-work__item.is-open { background: #2C2A31; }
body.service-v2 .home-work__item.is-open .home-work__title { color: var(--h-green); }
body.service-v2 .home-work__item.is-open .home-work__icon { color: var(--h-green); }
body.service-v2 .home-work__item.is-open .home-work__icon-arrow { display: none; }
body.service-v2 .home-work__item.is-open .home-work__icon-close { display: inline-flex; }
body.service-v2 .home-work__item.is-open .home-work__body { grid-template-rows: 1fr; }
body.service-v2 .home-work { background: #110D1A; }
@media (hover: none) {
  body.service-v2 .home-work__head { cursor: pointer; }
  body.service-v2 .home-work__item:hover { background: transparent; }
  body.service-v2 .home-work__item:hover .home-work__title { color: #fff; }
  body.service-v2 .home-work__item:hover .home-work__icon { color: var(--h-muted); }
  body.service-v2 .home-work__item:hover .home-work__icon-arrow { display: inline-flex; }
  body.service-v2 .home-work__item:hover .home-work__icon-close { display: none; }
  body.service-v2 .home-work__item:hover .home-work__body { grid-template-rows: 0fr; }
}
@media (max-width: 900px) {
  body.service-v2 .home-work__grid { grid-template-columns: 1fr; }
}

/* ---- Reliable partner ---- */
body.service-v2 .home-partner__list { max-width: none; margin: 0 auto; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 13px; }
body.service-v2 .home-partner__row { display: flex; align-items: stretch; gap: 13px; }
body.service-v2 .home-partner__num { flex: none; width: 84px; height: 84px; align-self: flex-start; display: grid; place-items: center; border-radius: 10px; background: #5336A1; color: #CBB9F9; font-weight: 800; font-size: 24px; }
body.service-v2 .home-partner__bar { flex: 1; display: flex; align-items: center; padding: 16px 21px; border-radius: 10px; background: var(--h-card); border: 1px solid #5336A1; color: #fff; font-size: 18px; font-weight: lighter; line-height: 1.5; }
body.service-v2 .home-partner__bar strong { color: #fff; font-weight: 700; }
body.service-v2 .home-partner { background: #08060D; }
@media (max-width: 768px) {
  body.service-v2 .home-partner__num { width: 44px; height: auto; align-self: stretch; font-size: 22px; }
  body.service-v2 .home-partner__bar { padding: 13px 14px; font-size: 12px; }
}

/* ---- Blog cards ---- */
body.service-v2 .home-blog__head { display: flex; align-items: flex-end; justify-content: space-between; gap: 19px; margin-bottom: 35px; }
body.service-v2 .home-blog__head-text { max-width: 512px; }
body.service-v2 .home-blog__eyebrow { display: none; margin-bottom: 16px; padding: 7px 14px; white-space: nowrap; border-radius: var(--h-pill); border: 1px solid #00886F; background: #0F1419; color: #00CCA6; font-size: 11px; font-weight: 700; }
body.service-v2 .home-blog__head h2 { font-size: 48px; letter-spacing: -0.01em; }
body.service-v2 .home-blog__head p { margin-top: 13px; color: var(--h-muted); font-size: 14px; }
body.service-v2 .home-blog__btn { flex: none; }
body.service-v2 .home-blog__grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 19px; }
body.service-v2 .home-blog__card { display: flex; flex-direction: column; background: var(--h-card); border: 1px solid var(--h-border); border-radius: var(--h-radius); overflow: hidden; }
body.service-v2 .home-blog__thumb { position: relative; display: block; aspect-ratio: 16 / 9; overflow: hidden; background: var(--h-card-2); }
body.service-v2 .home-blog__thumb img { width: 100%; height: 100%; object-fit: cover; }
body.service-v2 .home-blog__badge { position: absolute; top: 13px; left: 13px; padding: 5px 11px; border-radius: var(--h-pill); font-size: 10px; font-weight: 700; backdrop-filter: blur(3px); border: 1px solid; }
body.service-v2 .home-blog__card--purple .home-blog__badge { background: #15111E; color: #7D51F1; border-color: #5336A1; }
body.service-v2 .home-blog__card--green .home-blog__badge { background: #0D1A18; color: #00CCA6; border-color: #00886F; }
body.service-v2 .home-blog__body { display: flex; flex-direction: column; flex: 1; padding: 20px 21px 21px; }
body.service-v2 .home-blog__meta { display: flex; align-items: center; gap: 7px; color: var(--h-lilac); font-size: 13px; line-height: 1; }
body.service-v2 .home-blog__meta-item { display: inline-flex; align-items: center; gap: 6px; line-height: 1; }
body.service-v2 .home-blog__meta-item svg { flex: none; display: block; }
body.service-v2 .home-blog__meta-dot { display: inline-flex; align-items: center; color: #fff; opacity: 1; }
body.service-v2 .home-blog__body .home-blog__title { font-size: 19px; line-height: 1.35; margin: 16px 0 8px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
body.service-v2 .home-blog__title a { color: var(--sv-white); text-decoration: none; }
body.service-v2 .home-blog__excerpt { color: var(--sv-white); font-size: 14px; line-height: 1.65; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
body.service-v2 .home-blog__more { margin-top: auto; padding-top: 16px; align-self: flex-end; display: inline-flex; align-items: center; gap: 11px; color: #fff; font-weight: 700; font-size: 13px; text-decoration: none; transition: gap 0.4s cubic-bezier(0.4, 0, 0.2, 1), color 0.35s ease; }
body.service-v2 .home-blog__more svg { flex: none; }
body.service-v2 .home-blog__more:hover { gap: 19px; color: var(--h-green); }
body.service-v2 .home-blog { background: #110D1A; }
@media (min-width: 769px) {
  body.service-v2 .home-blog .container { display: grid; grid-template-columns: 1fr auto; align-items: end; column-gap: 19px; row-gap: 35px; }
  body.service-v2 .home-blog__head { grid-column: 1; grid-row: 1; margin-bottom: 0; }
  body.service-v2 .home-blog__btn { grid-column: 2; grid-row: 1; justify-self: end; }
  body.service-v2 .home-blog__grid { grid-column: 1 / -1; grid-row: 2; }
}
@media (max-width: 900px) {
  body.service-v2 .home-blog__grid { grid-template-columns: 1fr; }
}
@media (max-width: 768px) {
  body.service-v2 .home-blog__head { flex-direction: column; align-items: flex-start; }
  body.service-v2 .home-blog__btn { width: 100%; justify-content: center; margin-top: 22px; }
  body.service-v2 .site-homepage .home-blog__head h2 { font-size: 44px; }
}

/* ---- Accordion (shared) ---- */
body.service-v2 .home-accent-green { font-family: var(--h-serif); font-style: italic; background: var(--h-green-grad); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; color: transparent; }
body.service-v2 .home-accent-purple { font-family: var(--h-serif); font-style: normal; background: var(--h-purple-grad-soft); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; color: transparent; }
body.service-v2 .home-intro .home-accent-purple { background: none; -webkit-text-fill-color: var(--intro-accent, #7d51f1); color: var(--intro-accent, #7d51f1); }
body.service-v2 .home-intro__text .home-accent-purple { display: block; }
body.service-v2 .home-acc { max-width: 704px; margin: 0 auto; border-top: 1px solid var(--h-border); }
body.service-v2 .home-acc__item { border-bottom: 1px solid var(--h-border); }
body.service-v2 .home-acc__head { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 13px; padding: 19px 3px; background: none; border: 0; cursor: pointer; text-align: left; color: #fff; }
body.service-v2 .home-acc__title { font-size: clamp(14px, 1.6vw, 18px); font-weight: 600; }
body.service-v2 .home-acc__icon { position: relative; width: 18px; height: 18px; flex: none; }
body.service-v2 .home-acc__icon::before, body.service-v2 .home-acc__icon::after { content: ""; position: absolute; top: 50%; left: 50%; width: 10px; height: 2px; background: var(--h-green); transform: translate(-50%, -50%); transition: transform var(--ds-dur, 200ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1)); }
body.service-v2 .home-acc__icon::after { transform: translate(-50%, -50%) rotate(90deg); }
body.service-v2 .home-acc__item.is-open .home-acc__icon::after { transform: translate(-50%, -50%) rotate(0deg); }
body.service-v2 .home-acc__body { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.28s ease; }
body.service-v2 .home-acc__body > .home-acc__rich { overflow: hidden; color: var(--h-muted); font-size: 13px; padding-right: 30px; }
body.service-v2 .home-acc__rich > :first-child { margin-top: 0; }
body.service-v2 .home-acc__rich > :last-child { margin-bottom: 0; }
body.service-v2 .home-acc__item.is-open .home-acc__body { grid-template-rows: 1fr; }
body.service-v2 .home-acc__item.is-open .home-acc__body > .home-acc__rich { padding-bottom: 19px; }
body.service-v2 .home-accent-text-purple { color: #7D51F1; }
body.service-v2 .home-accent-text-green { color: var(--h-green); }
body.service-v2 .home-acc--problems { max-width: 1200px; border-top: 0; }
body.service-v2 .home-acc--problems .home-acc__item { border: 0; border-top: 0.5px solid #211F25; background: transparent; transition: background var(--ds-dur, 200ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1)), border-color var(--ds-dur, 200ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1)); }
body.service-v2 .home-acc--problems .home-acc__item:first-child { border-top: 0; }
body.service-v2 .home-acc--problems .home-acc__head { gap: 14px; justify-content: flex-start; padding: 15px 18px; }
body.service-v2 .home-acc--problems .home-acc__item:hover, body.service-v2 .home-acc--problems .home-acc__item.is-open { background: #15111E; border-top-color: transparent; }
body.service-v2 .home-acc--problems .home-acc__item:last-child:hover, body.service-v2 .home-acc--problems .home-acc__item:last-child.is-open { border-radius: 0 0 var(--h-radius) var(--h-radius); }
body.service-v2 .home-acc--problems .home-acc__item:hover + .home-acc__item { border-top-color: #5336A1; }
body.service-v2 .home-acc--problems .home-acc__item.is-open + .home-acc__item:not(:hover) { border-top: 1px solid #5336A1; }
body.service-v2 .home-acc--problems .home-acc__item:last-child { border-bottom: 0.5px solid transparent; }
body.service-v2 .home-acc--problems .home-acc__item:last-child:hover { border-bottom-color: #5336A1; }
body.service-v2 .home-acc--problems .home-acc__item:last-child.is-open { border-bottom: 1px solid #5336A1; }
body.service-v2 .home-acc--problems .home-acc__item:hover .home-acc__title { color: #CBB9F9; }
body.service-v2 .home-acc__ico { flex: none; width: 35px; height: 35px; display: grid; place-items: center; }
body.service-v2 .home-acc__ico img { width: 100%; height: 100%; display: block; }
body.service-v2 .home-acc--problems .home-acc__title { flex: 1 1 auto; font-size: clamp(12px, 1.36vw, 14px); transition: color var(--ds-dur, 200ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1)); }
body.service-v2 .home-acc__badge { flex: none; min-width: 96px; height: 34px; padding: 0 16px; display: inline-flex; align-items: center; justify-content: center; border-radius: var(--h-pill); border: 1px solid #5336A1; color: #7D51F1; font-size: 12px; font-weight: 600; white-space: nowrap; }
body.service-v2 .home-acc__chev { flex: none; color: var(--h-muted); display: inline-flex; }
body.service-v2 .home-acc__chev-close { display: none; }
body.service-v2 .home-acc--problems .home-acc__item.is-open .home-acc__chev-arrow { display: none; }
body.service-v2 .home-acc--problems .home-acc__item.is-open .home-acc__chev-close { display: inline-flex; }
body.service-v2 .home-acc--problems .home-acc__item.is-open .home-acc__chev { color: #fff; }
body.service-v2 .home-acc--problems .home-acc__body > .home-acc__rich { padding-left: 18px; padding-right: 18px; }
body.service-v2 .home-note { padding: 14px 18px; border: 1px solid var(--h-border); border-radius: var(--h-radius); background: rgba(255, 255, 255, 0.02); color: #fff; font-size: 14px; line-height: 1.6; }
body.service-v2 .home-note strong { color: #fff; font-weight: 700; }
body.service-v2 .home-acc--faq { max-width: none; border-top: 0; }
body.service-v2 .home-acc--faq .home-acc__item { border-top: 1px solid var(--h-border); border-bottom: 0; transition: background var(--ds-dur, 200ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1)), border-color var(--ds-dur, 200ms) var(--ds-ease, cubic-bezier(0.4, 0, 0.2, 1)); }
body.service-v2 .home-acc--faq .home-acc__item:hover, body.service-v2 .home-acc--faq .home-acc__item.is-open { background: #2C2A31; border-top-color: transparent; }
body.service-v2 .home-acc--faq .home-acc__head { gap: 16px; padding: 21px 19px; }
body.service-v2 .home-acc--faq .home-acc__q { font-size: clamp(13px, 1.44vw, 15px); font-weight: 700; }
body.service-v2 .home-acc--faq .home-acc__item:hover .home-acc__q, body.service-v2 .home-acc--faq .home-acc__item.is-open .home-acc__q { color: #a78bff; }
body.service-v2 .home-acc--faq .home-acc__item:hover .home-acc__chev, body.service-v2 .home-acc--faq .home-acc__item.is-open .home-acc__chev { color: #a78bff; }
body.service-v2 .home-acc--faq .home-acc__item.is-open .home-acc__chev-arrow { display: none; }
body.service-v2 .home-acc--faq .home-acc__item.is-open .home-acc__chev-close { display: inline-flex; }
body.service-v2 .home-acc--faq .home-acc__body > .home-acc__rich { color: #fff; font-size: 14px; font-weight: lighter; line-height: 1.6; padding: 0 19px; }
body.service-v2 .home-acc--faq .home-acc__item.is-open .home-acc__body > .home-acc__rich { padding-bottom: 21px; }
body.service-v2 .home-final__title .home-accent-purple { display: block; }
@media (max-width: 768px) {
  body.service-v2 .home-acc--problems .home-acc__head { flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: 14px; padding: 26px 18px; }
  body.service-v2 .home-acc__ico { width: 44px; height: 44px; }
  body.service-v2 .home-acc--problems .home-acc__title { flex: 0 0 auto; order: 0; font-size: 15px; line-height: 1.45; max-width: 280px; }
  body.service-v2 .home-acc__badge { order: 0; margin: 0; }
  body.service-v2 .home-acc__chev { order: 0; margin: 0; }
  body.service-v2 .home-acc--problems .home-acc__body > .home-acc__rich { padding-left: 0; padding-right: 0; text-align: center; }
}

/* ---- AI CTA band (templates/home/cta-ai.php) ----
   Ported from css/home.css the same way as the sections above, with the same
   brace-walking extraction: @media context preserved, grouped selectors filtered
   to the .home-ctaai ones only (the shared band rule it sits in also dresses
   .home-ways, which is already carried above and must not be duplicated here).

   `--ctaai-bg` / `--ctaai-bg-mobile` are set inline on the box by the part; the
   mobile artwork is a theme file (assets/images/ctaai-bg-mobile.*), not a field.

   Nothing here restyles the button beyond its size - `.home-btn--green` comes
   from the shared chrome at the top of this block. */
body.service-v2 .home-ctaai__btn {
  height: 57px;
  padding-inline: 32px;
}
body.service-v2 .home-ctaai__desc > :first-child {
  margin-top: 0;
}
body.service-v2 .home-ctaai__desc > :last-child {
  margin-bottom: 0;
}
body.service-v2 .home-ctaai {
  background: #110d1a;
}
/* NOT PORTED: `padding-top: 22px`. On the homepage that rule tightens the gap
   between the three-ways comparison TABLE and this banner, and home.css says so
   in its own comment - it is about one specific pair of neighbours. Carried
   across it applied unconditionally, and on a Service the section above is
   whatever the editor dragged there: measured 22px top against 56px bottom, and
   56px on every sibling, so the block sat visibly high in its own band. The
   section keeps the standard rhythm here instead. The homepage's copy of the
   rule is untouched. */
body.service-v2 .home-ctaai__box {
  position: relative;
  border-radius: var(--h-radius-lg);
  padding: clamp(32px, 4.8vw, 61px) clamp(19px, 4vw, 64px);
  text-align: center;
  overflow: hidden;
  border: 1px solid var(--h-border);
  background:
  radial-gradient(70% 90% at 10% 6%, rgba(120, 80, 255, 0.55), transparent 55%),
  radial-gradient(70% 100% at 92% 96%, rgba(0, 200, 150, 0.5), transparent 55%),
  #0a0a12;
}
body.service-v2 .home-ctaai__box.has-bg {
  background: none;
  border-color: transparent;
  border-radius: 32px;
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.55);
}
body.service-v2 .home-ctaai__box.has-bg::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background: var(--ctaai-bg) center / cover no-repeat;
  transform: scale(1.14);
  transform-origin: center;
  pointer-events: none;
}
body.service-v2 .home-ctaai__box.has-bg > * {
  position: relative;
  z-index: 1;
}
body.service-v2 .home-ctaai__box .home-ctaai__title {
  font-family: var(--h-serif);
  font-weight: 400;
  font-size: clamp(24px, 3.68vw, 54px);
  line-height: 1.08;
  max-width: 800px;
  margin: 0 auto;
  color: #fff;
}
body.service-v2 .home-ctaai__accent {
  font-family: var(--h-serif); color: #00cca6;
}
body.service-v2 .home-ctaai__box .home-ctaai__desc {
  max-width: 800px;
  margin: 18px auto 48px;
  color: var(--h-muted);
  font-size: 18px;
  line-height: 1.6;
  color: #fff;
}
body.service-v2 .home-ctaai__desc strong {
  color: #fff; font-weight: 800;
}
body.service-v2 .home-ctaai__link {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 6px;
  margin-top: 48px;
  color: var(--sv-white);
  font-size: 12px;
  text-decoration: none;
}
body.service-v2 .home-ctaai__link:hover {
  color: #fff;
}
body.service-v2 .home-ctaai__link-path {
  color: inherit;
}
body.service-v2 .home-ctaai__link-img {
  width: 15px; height: 15px; object-fit: contain; flex: none;
}
body.service-v2 .home-ctaai__chips {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 21px 32px;
  margin: 35px 0 0;
  padding: 0;
  list-style: none;
}
body.service-v2 .home-ctaai__chip {
  display: flex; align-items: center; gap: 10px; text-align: left;
}
body.service-v2 .home-ctaai__chip-ico {
  flex: none;
  width: 32px; height: 32px;
  border-radius: 9px;
  display: grid; place-items: center;
}
body.service-v2 .home-ctaai__chip-ico-img {
  width: 20px; height: 20px; object-fit: contain;
}
body.service-v2 .home-ctaai__chip-ico--green {
  background: #00886F; color: #68DCC7;
}
body.service-v2 .home-ctaai__chip-ico--purple {
  background: #5336A1; color: #CBB9F9;
}
body.service-v2 .home-ctaai__chip-text {
  display: flex; flex-direction: column; line-height: 1.3; min-width: 0;
}
body.service-v2 .home-ctaai__chip-title {
  font-weight: 500; color: var(--sv-white); font-size: 14px;
}
body.service-v2 .home-ctaai__chip-ico--green + .home-ctaai__chip-text .home-ctaai__chip-title {
  color: #68DCC7;
}
body.service-v2 .home-ctaai__chip-ico--purple + .home-ctaai__chip-text .home-ctaai__chip-title {
  color: #CBB9F9;
}
body.service-v2 .home-ctaai__chip-sub {
  color: var(--sv-white); font-size: 12px;
}
@media (max-width: 768px) {
  body.service-v2 .home-ctaai__btn {
    width: 100%; justify-content: center;
  }
  body.service-v2 .home-ctaai__box.has-bg::before {
    background-image: var(--ctaai-bg-mobile, var(--ctaai-bg));
    background-size: cover;
    background-position: center;
    transform: none;
  }
  body.service-v2 .home-ctaai__chips {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: start;
    gap: 18px 20px;
    max-width: 340px;
    margin: 35px auto 0;
  }
  body.service-v2 .home-ctaai__chip:last-child:nth-child(odd) {
    grid-column: 1 / -1;
    justify-content: center;
  }
  body.service-v2 .home-ctaai__chip:nth-child(1) {
    order: 1;
  }
  body.service-v2 .home-ctaai__chip:nth-child(2) {
    order: 2;
  }
  body.service-v2 .home-ctaai__chip:nth-child(3) {
    order: 4;
  }
  body.service-v2 .home-ctaai__chip:nth-child(4) {
    order: 3;
  }
  body.service-v2 .home-ctaai__chip:nth-child(5) {
    order: 5;
  }
  body.service-v2 .home-ctaai__link {
    flex-wrap: wrap;
  }
  body.service-v2 .home-ctaai__link > span:first-child {
    white-space: nowrap;
  }
  body.service-v2 .home-ctaai__link-path {
    flex-basis: 100%;
    overflow-wrap: anywhere;
    text-align: center;
    opacity: 0.7;
  }
}
@media (max-width: 480px) {
  body.service-v2 .home-ctaai__box h2 {
    font-size: 19px;
  }
}

/* ---- At-rules the sections depend on ---- */
@property --agent-angle { syntax: "<angle>"; inherits: true; initial-value: 45deg; }

/* ---------- Keyboard focus ring for links + buttons ----------------------
   Mirrors the same rule on the AI Product Testing and Contact Us pages.

   THIS IS THE RULE THE PAGE WAS ALREADY WRITTEN AGAINST. Four button variants
   here declare a focus ring by COLOUR only - .sv-btn--purple, .sv-btn--lilac,
   .sv-btn--tone and .sv-form__btn each set `outline-color` and nothing else.
   `outline-style` initially computes to `none`, so with no base rule to supply
   the style and width those four declarations painted nothing at all and this
   template had no author focus indicator on any button. The shared header CTA
   (.home-btn:focus-visible { outline-color: #fff }) was inert here for the same
   reason. Adding the base makes all five light up as their authors intended.

   Those overrides stay in charge of the colour: each is (0,3,1) against this
   rule's (0,2,2), so they win on outline-color and inherit style and width.

   No `border-radius` here, unlike the two sibling pages: an outline already
   follows the element's own corners, and pinning a radius during focus would
   visibly square off the pill-shaped buttons this template uses. */
body.service-v2 a:focus-visible,
body.service-v2 button:focus-visible {
  outline: 2px solid var(--sv-lilac, #CBB9F9);
  outline-offset: 3px;
}

/* Pressed state - see the note on .home-btn:active in shared/home-header.css. */
:is(body.service-v2, .sv-scope) .sv-btn:active:not(:disabled):not(.is-loading):not(.is-success):not(.is-error),
:is(body.service-v2, .sv-scope) .sv-form__btn:active:not(:disabled):not(.is-loading):not(.is-success):not(.is-error) {
  transform: translateY(0);
  filter: brightness(0.93);
  transition-duration: var(--ds-dur-fast, 150ms);
}

/* Icon slots: finish the object-fit pattern - see the note in
   assets/styles/shared/home-header.css. Same three homepage components this
   template borrows, so they need the same declaration here. */
body.service-v2 .home-agent-card__ico img,
body.service-v2 .home-domain__ico-img,
body.service-v2 .home-acc__ico img {
  object-fit: contain;
}
