/* Sphinx Solutions — site styles.
   Loaded from include/header.php; keep all custom CSS here, never inline. */
/* Smooth scrolling for in-page jumps — the nav's anchors, the FAQ, the
   calculator stepper, "back to top". Native, not a JS scroll-hijack:
   see the note at the foot of this file before reaching for one.

   ⚠ It has to be conditional. An unconditional scroll-behavior:smooth
   animates every programmatic jump, including the ones assistive
   technology makes to move a reader through a page, and for some people
   that motion is nauseating. This is the one motion rule on the site
   that was not already honouring the preference. */
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

/* While the wheel-smoothing loop in footer.php is running it drives the
   real scroll position frame by frame, so the native smoothing above has
   to stand down or the two animate against each other and the page
   crawls. Higher specificity than the bare html rule, so no !important.
   The class is added only for the length of the glide. */
html.is-lerping { scroll-behavior: auto; }

/* ── Sticky header ────────────────────────────────────────────────
   position:fixed rather than sticky: the header lives inside .field,
   which has overflow:hidden, and that disables position:sticky
   outright. Fixed escapes it and keeps the nav present for the whole
   page rather than only while the hero shell is on screen. */
:root { --header-h: 100px; }

.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 50;
}
/* The header is out of flow now, so the hero shell reserves its height
   or the hero would slide up underneath it. */
.field { padding-top: var(--header-h); }

/* ── Fluid cursor ──────────────────────────────────────────────────────
   The WebGL overlay on the home hero, rendered by header.php and driven
   by assets/js/fluid-cursor.js.

   `screen` is what keeps it from covering the hero: the dye buffer is
   black where nothing has been painted, and screening black leaves the
   video and the scrim exactly as they were. Where there IS dye it adds
   light, so the headline over the top stays legible without the canvas
   needing to dodge it. .field carries `isolate`, so the blend resolves
   against the hero and does not reach the rest of the page.

   Hidden until the script has a first frame up, because a cold WebGL
   init is a few hundred milliseconds and an empty canvas snapping to
   full opacity reads as a flash. */
.fluid-cursor {
  position: absolute;
  inset: 0;
  z-index: 2;
  width: 100%;
  height: 100%;
  pointer-events: none;
  mix-blend-mode: screen;
  opacity: 0;
  transition: opacity .6s ease;
}
.fluid-cursor.is-ready { opacity: 1; }

/* Below lg the hero is a single narrow column with the video at 30%
   opacity behind it, and the pointer is a finger. The script bails out
   at the same width; this makes sure the element is gone even if it is
   ever rendered on a page that does not run it. */
@media (max-width: 1023px) {
  .fluid-cursor { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .fluid-cursor { display: none; }
}

/* ── Docking header ────────────────────────────────────────────────
   Two states, toggled by `is-scrolled` from footer.php.

   AT REST the header is transparent and the nav inside it is a
   floating pill: inset from the viewport edges, rounded, hairlined,
   and only 40% black so the hero reads through it.

   DOCKED, once the page has moved, the bar itself goes solid black and
   full bleed while the nav keeps its 1240px container, so the black
   spans the window and the logo and links still line up with the
   sections below. The pill dissolves into it: background, border and
   radius all go, and the nav takes over the page container's own
   horizontal padding so its content sits exactly where a section's
   content does.

   Scrolling back to the top reverses it.

   THE FULL-BLEED SURFACE IS ON THE HEADER, NOT THE NAV, and that is
   load-bearing. .mega__drop is positioned against the nav (the only
   positioned ancestor), so stretching the nav to the window would drag
   every mega panel out to the window with it. Leaving the nav at
   1240px keeps the panels where they are and costs nothing: the header
   behind it is already edge to edge.

   The nav's border goes to zero width when docked rather than merely
   turning transparent: a transparent 1px border still occupies 1px, and
   that pushed the logo one pixel off the column every section below it
   lines up on. Width is a length, so it tweens with everything else. */
.site-header {
  transition: padding .25s ease, background-color .25s ease, box-shadow .25s ease;
}
.site-header nav {
  transition: background-color .25s ease, border-color .25s ease,
              border-width .25s ease, border-radius .25s ease,
              padding .25s ease;
}

/* The hairline is the first shadow rather than a border-bottom: a
   border would add its pixel to the header at rest too, where the bar
   is invisible and --header-h has already reserved the hero's padding
   for the height without it. */
.site-header.is-scrolled {
  padding: 0;
  background: #000;
  box-shadow: 0 1px 0 rgba(255, 255, 255, .09),
              0 12px 34px -18px rgba(0, 0, 0, .9);
}
.site-header.is-scrolled nav {
  background: transparent;
  border-color: transparent;
  border-width: 0;
  border-radius: 0;
  padding-left: 1rem;
  padding-right: 1rem;
}
/* Matches the px-6 every section container carries above 640px. */
@media (min-width: 640px) {
  .site-header.is-scrolled nav { padding-left: 1.5rem; padding-right: 1.5rem; }
}

@media (prefers-reduced-motion: reduce) {
  .site-header,
  .site-header nav { transition: none; }
}

/* Anchor jumps land below the header instead of under it. Covers every
   #target on the site in one line. */
html { scroll-padding-top: calc(var(--header-h) + 12px); }
  body { background-color: #08090d; }

  /* ── Rotating conic glow rim ──────────────────────────────────────
     A conic gradient sweeps a single lit arc around the element. The
     ::before is masked to a 1px ring so only the rim shows; the ::after
     is the same gradient, blurred, as the outer bloom.               */
  @property --glow-angle {
    syntax: '<angle>';
    initial-value: 0deg;
    inherits: false;
  }

  .glow-rim {
    position: relative;
    isolation: isolate;
    --glow-color: #80C241;
    --glow-base: rgba(255,255,255,.11);   /* resting edge, under the arc */
    --glow-speed: 7s;
  }
  .glow-rim::before,
  .glow-rim::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
    background:
      conic-gradient(
        from var(--glow-angle),
        transparent 296deg,
        var(--glow-color) 334deg,
        transparent 354deg
      ),
      /* base edge, so the shape still reads when the arc is on the far side */
      linear-gradient(var(--glow-base), var(--glow-base));
    animation: glow-spin var(--glow-speed) linear infinite;
  }
  /* The rim: masked down to a 1px ring. No negative z-index — it must
     paint ON TOP of the element's own background, not behind it. */
  .glow-rim::before {
    padding: 1px;
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
            mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
            mask-composite: exclude;
    z-index: 1;
  }
  /* The bloom spilling outward, behind everything */
  .glow-rim::after {
    filter: blur(16px);
    opacity: .5;
    z-index: -1;
  }
  @keyframes glow-spin { to { --glow-angle: 360deg; } }

  /* Fallback where @property is unsupported: a static lit edge, no spin */
  @supports not (background: conic-gradient(from var(--glow-angle), red, blue)) {
    .glow-rim::before,
    .glow-rim::after { animation: none; background: linear-gradient(120deg, var(--glow-color), transparent 60%); }
  }

  /* ── Hero pill ────────────────────────────────────────────────────
     The capsule label above an H1. Pair it with .glow-rim, which
     supplies the arc that travels round the edge — the pill itself is
     just the surface the arc runs on. Used by the home hero and the
     portfolio hero, so it lives here rather than as a line of utilities
     copied into each. */
  .hero-pill {
    display: inline-flex;
    height: 34px;
    align-items: center;
    padding: 0 16px;
    border-radius: 999px;
    background: rgba(255, 255, 255, .04);
    backdrop-filter: blur(4px);
    font-size: 11px;
    font-weight: 500;
    letter-spacing: 1.4px;
    text-transform: uppercase;
    color: rgba(255, 255, 255, .9);
  }

  /* ── Ambient gradient field ───────────────────────────────────────
     Stands in for the reference's raster background plate, rebuilt in
     Sphinx's palette: green low-left, blue right, black elsewhere.   */
  .field::before {
    content: '';
    position: absolute;
    inset: -20% -10% -30%;
    pointer-events: none;
    background:
      radial-gradient(58% 62% at 88% 42%,  rgba(13,110,253,.55) 0%, rgba(13,110,253,0) 62%),
      radial-gradient(46% 52% at 4%  86%,  rgba(128,194,65,.34) 0%, rgba(128,194,65,0) 64%),
      radial-gradient(40% 44% at 20% 12%,  rgba(13,110,253,.16) 0%, rgba(13,110,253,0) 70%);
    filter: blur(40px);
  }

  /* Faint technical grid over the field */
  .field::after {
    content: '';
    position: absolute;
    inset: 0;
    pointer-events: none;
    background-image:
      linear-gradient(to right,  rgba(255,255,255,.035) 1px, transparent 1px),
      linear-gradient(to bottom, rgba(255,255,255,.035) 1px, transparent 1px);
    background-size: 72px 72px;
    mask-image: radial-gradient(ellipse 90% 70% at 50% 34%, #000 35%, transparent 100%);
    -webkit-mask-image: radial-gradient(ellipse 90% 70% at 50% 34%, #000 35%, transparent 100%);
  }

  /* ── Inner-page hero plate ────────────────────────────────────────
     The supplied banner is two soft glows on black — a warm amber one
     top-left and a blue one off the top-right corner — going fully
     black by mid-height. It carries no detail, so it is rebuilt here
     rather than shipped as a ~300KB PNG that would only ever be
     stretched to the viewport width.

     Layer order is bottom-of-stack last: the two blues build the right
     edge, the two ambers the left. */
  .field--banner::before {
    inset: -14% -6% 0;
    background:
      radial-gradient(44% 82% at 100% 10%, rgba( 46, 116, 240, .90) 0%, rgba( 46, 116, 240, 0) 64%),
      radial-gradient(44% 66% at  80%  0%, rgba( 26,  70, 168, .60) 0%, rgba( 26,  70, 168, 0) 70%),
      radial-gradient(30% 62% at  12%  6%, rgba(196, 100,  38, .62) 0%, rgba(196, 100,  38, 0) 66%),
      radial-gradient(42% 50% at  30%  0%, rgba(150,  76,  30, .42) 0%, rgba(150,  76,  30, 0) 72%);
    filter: blur(55px);
  }
  /* The plate resolves to black well before the fold, so the grid is
     masked back to the top third instead of the hero's full height. */
  .field--banner::after {
    mask-image: radial-gradient(ellipse 80% 55% at 50% 6%, #000 20%, transparent 92%);
    -webkit-mask-image: radial-gradient(ellipse 80% 55% at 50% 6%, #000 20%, transparent 92%);
  }

  /* ── Hero still bleed ─────────────────────────────────────────────
     Same job as .video-bleed below, for a photograph, and it needs one
     thing the footage does not: TONE MATCHING.

     The hero is near-black with white copy. The stills these pages are
     given are bright studio renders on white, so dropped in raw one
     would punch a white rectangle through the plate and take the copy
     with it. Three things fix that, and all three are doing work:

       filter   drops the whites to a mid blue-grey and pushes the
                saturation back up, since darkening alone reads as
                muddy grey rather than as part of a blue plate.
       screen   never darkens what is under it, so the plate's blue and
                amber glows still come through the picture, and the
                image's own dark areas fall away into the black instead
                of sitting on top of it as a visible panel edge. .field
                carries `isolate`, so the blend resolves against the
                plate and not against whatever is behind the section.
       mask     the same two-mask intersect as the footage: the left
                edge fades into the canvas so the copy has clean ground,
                the bottom fades so the hero ends without a seam.

     Below lg the art sits behind the copy rather than beside it, so the
     markup drops it to opacity-25 and the scrim carries the rest. */
  .hero-bleed {
    /* Anchored right, because these stills are all composed the same
       way: subject hard against the right edge, dead space on the left.
       A centred cover crop takes ~230px off each side at this box and
       beheads the subject, while the space it spares on the left is
       masked out below anyway. Crop where there is nothing to lose. */
    object-position: right center;
    filter: brightness(.46) contrast(1.12) saturate(1.3);
    mix-blend-mode: screen;
    -webkit-mask-image:
      linear-gradient(to right, transparent 0%, rgba(0,0,0,.42) 32%, #000 64%),
      linear-gradient(to bottom, #000 52%, transparent 97%);
    -webkit-mask-composite: source-in;
            mask-image:
      linear-gradient(to right, transparent 0%, rgba(0,0,0,.42) 32%, #000 64%),
      linear-gradient(to bottom, #000 52%, transparent 97%);
            mask-composite: intersect;
  }

  /* ── Video bleed ──────────────────────────────────────────────────
     The footage is the right-hand background, not a framed card. Two
     masks intersect: one fades its left edge into the dark canvas, one
     fades the bottom, so it dissolves rather than ending on a hard box. */
  .video-bleed {
    -webkit-mask-image:
      linear-gradient(to right, transparent 0%, rgba(0,0,0,.55) 22%, #000 46%),
      linear-gradient(to bottom, #000 55%, transparent 96%);
    -webkit-mask-composite: source-in;
            mask-image:
      linear-gradient(to right, transparent 0%, rgba(0,0,0,.55) 22%, #000 46%),
      linear-gradient(to bottom, #000 55%, transparent 96%);
            mask-composite: intersect;
  }

  /* Below lg the video sits behind the copy, so it fades from the top
     instead of the left and needs a scrim for text legibility. */
  @media (max-width: 1023px) {
    .video-bleed {
      -webkit-mask-image: linear-gradient(to bottom, #000 18%, transparent 72%);
              mask-image: linear-gradient(to bottom, #000 18%, transparent 72%);
      -webkit-mask-composite: source-over;
              mask-composite: add;
    }
  }

  /* ── SPHINX letter reveal ─────────────────────────────────────────
     Each letter fades up in turn once the grid scrolls into view. The
     per-letter --i index drives the stagger.                          */
  .js-anim .pillar-letter {
    opacity: 0;
    transform: translateY(18px);
    will-change: opacity, transform;
  }
  .js-anim .pillars-in .pillar-letter {
    animation: letter-in .6s cubic-bezier(.22,.68,.3,1) forwards;
    animation-delay: calc(var(--i) * 130ms);
  }
  @keyframes letter-in {
    from { opacity: 0; transform: translateY(18px); }
    to   { opacity: 1; transform: translateY(0); }
  }
  @media (prefers-reduced-motion: reduce) {
    .js-anim .pillar-letter { opacity: 1; transform: none; animation: none; }
  }

  /* ── WhatsApp float ───────────────────────────────────────────────
     Soft outward pulse to draw the eye without moving the button
     itself, so it never shifts under the cursor mid-click.          */
  @keyframes wa-pulse {
    0%   { transform: scale(1);   opacity: .55; }
    70%  { transform: scale(1.9); opacity: 0;   }
    100% { transform: scale(1.9); opacity: 0;   }
  }
  .wa-float__pulse { animation: wa-pulse 2.6s cubic-bezier(.25,.6,.4,1) infinite; }
  @media (prefers-reduced-motion: reduce) {
    .wa-float__pulse { animation: none; opacity: 0; }
  }

  /* ── Services accordion ───────────────────────────────────────────
     Buttons + panels rather than <details>, because a closed <details>
     removes its content from the box tree and the open/close height
     transition cannot run. Panels animate via grid-template-rows
     0fr -> 1fr, which needs no fixed height.                          */
  .acc__item { border-top: 1px solid rgba(18,20,29,.13); }
  .acc__item:last-child { border-bottom: 1px solid rgba(18,20,29,.13); }

  .acc__head {
    display: flex;
    align-items: center;
    gap: 20px;
    width: 100%;
    padding: 26px 0;
    background: none;
    border: 0;
    text-align: left;
    cursor: pointer;
  }
  .acc__num {
    flex: none;
    width: 42px;
    font-size: 14px;
    /* limeInk, not the dark-theme lime — 4.9:1 on white */
    color: #4E7D22;
  }
  .acc__title {
    font-size: 22px;
    font-weight: 500;
    letter-spacing: -0.5px;
    color: #12141D;
  }
  @media (min-width: 768px) { .acc__title { font-size: 30px; letter-spacing: -0.9px; } }

  .acc__toggle {
    position: relative;
    flex: none;
    margin-left: auto;
    width: 44px;
    height: 44px;
    border-radius: 9999px;
    border: 1px solid rgba(18,20,29,.22);
    color: #12141D;
    transition: transform .35s ease, background-color .25s, color .25s, border-color .25s;
  }
  /* + rotated 45deg reads as x — one element, no icon swap */
  .acc__toggle::before,
  .acc__toggle::after {
    content: '';
    position: absolute;
    inset: 0;
    margin: auto;
    background: currentColor;
    border-radius: 2px;
  }
  .acc__toggle::before { width: 14px; height: 1.5px; }
  .acc__toggle::after  { width: 1.5px; height: 14px; }

  .acc__item.is-open .acc__toggle {
    transform: rotate(45deg);
    background: #12141D;
    border-color: #12141D;
    color: #fff;
  }

  .acc__body {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows .38s cubic-bezier(.3,.7,.3,1);
  }
  .acc__item.is-open .acc__body { grid-template-rows: 1fr; }
  .acc__inner { overflow: hidden; }
  .acc__content {
    display: flex;
    flex-direction: column;
    gap: 20px;
    padding: 0 0 30px 62px;
  }
  @media (min-width: 900px) {
    .acc__content { flex-direction: row; align-items: flex-end; justify-content: space-between; gap: 48px; }
  }
  .acc__desc {
    max-width: 560px;
    font-size: 16px;
    line-height: 26px;
    color: rgba(18,20,29,.62);
  }
  .acc__tags { display: flex; flex-wrap: wrap; gap: 10px; margin: 0; padding: 0; list-style: none; }
  .acc__tags li {
    padding: 7px 16px;
    border-radius: 9999px;
    background: rgba(128,194,65,.28);
    font-size: 14px;
    color: #12141D;
    white-space: nowrap;
  }

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

  /* ── Dark-band variant of the RULED accordion ─────────────────────
     Not to be confused with .faq--dark, which relights the CARD skin.
     The base .acc__* above is authored light -- ink rules, ink titles,
     limeInk numerals -- because every ruled accordion on the site sat
     on white until the IPA solutions band moved onto #08090d.

     Only colour is restated; every dimension, weight and breakpoint
     above still applies. Brand lime is legal here because the band is
     dark, where the light variant has to use limeInk for the same
     marks. The rule mirrors the light one's weight (.13) rather than
     the --line-dark hairline (.09), which is too faint to separate
     30px headings. */
  .acc--dark .acc__item { border-top-color: rgba(255, 255, 255, .13); }
  .acc--dark .acc__item:last-child { border-bottom-color: rgba(255, 255, 255, .13); }
  .acc--dark .acc__num   { color: #80C241; }
  .acc--dark .acc__title { color: #fff; }
  .acc--dark .acc__desc  { color: rgba(255, 255, 255, .62); }
  .acc--dark .acc__toggle {
    border-color: rgba(255, 255, 255, .22);
    color: #fff;
  }
  /* Open state inverts, as the light variant does in the other
     direction: it fills the disc with ink and goes white. */
  .acc--dark .acc__item.is-open .acc__toggle {
    background: #fff;
    border-color: #fff;
    color: #12141D;
  }
  .acc--dark .acc__tags li {
    background: rgba(128, 194, 65, .22);
    color: #fff;
  }

  /* ── Compliance lattice + badges ──────────────────────────────────
     Honeycomb is generated SVG, faded at the edges so it never ends on
     a hard line. Badges are concentric hexagons with a coloured glow.  */
  .lattice { stroke: rgba(128,194,65,.07); stroke-width: 1; fill: none; }
  .lattice-wrap {
    mask-image: radial-gradient(ellipse 70% 75% at 62% 50%, #000 25%, transparent 82%);
    -webkit-mask-image: radial-gradient(ellipse 70% 75% at 62% 50%, #000 25%, transparent 82%);
  }

  .hexbadge {
    --badge-glow: #676767;
    filter: drop-shadow(0 0 26px var(--badge-glow));
    transition: filter .3s ease;
  }
  .hexbadge .ring-outer {
    stroke: var(--badge-glow);
    stroke-width: 2;
    fill: rgba(0,0,0,.55);
    transition: stroke .3s ease;
  }
  /* Hover lights the rim and bloom in the brand green */
  .hexwrap:hover .hexbadge { --badge-glow: #80C241; }
  @media (prefers-reduced-motion: reduce) {
    .hexbadge, .hexbadge .ring-outer { transition: none; }
  }
  .hexbadge .ring-mid   { stroke: rgba(255,255,255,.16); stroke-width: 1; fill: none; }
  .hexbadge .ring-in    { stroke: rgba(255,255,255,.09); stroke-width: 1; fill: none; }

  /* ── FAQ (accordion, card variant) ────────────────────────────────
     Reuses the .acc__* structure so the existing accordion script drives
     it unchanged; only the skin differs.                              */
  .faq .acc__item {
    border: 0;
    margin-bottom: 14px;
    background: #fff;
    border-radius: var(--r-card);
  }
  .faq .acc__item:last-child { border-bottom: 0; margin-bottom: 0; }
  .faq .acc__head { padding: 22px 26px; gap: 18px; }
  .faq .acc__title { font-size: 17px; font-weight: 600; letter-spacing: -0.2px; }
  @media (min-width: 768px) { .faq .acc__title { font-size: 18px; letter-spacing: -0.2px; } }

  .faq .acc__toggle {
    width: 34px;
    height: 34px;
    border-color: transparent;
    transform: none;
  }
  /* Chevron instead of the plus/cross used by the services list */
  .faq .acc__toggle::after { display: none; }
  .faq .acc__toggle::before {
    width: 8px;
    height: 8px;
    background: none;
    border-right: 1.7px solid currentColor;
    border-bottom: 1.7px solid currentColor;
    border-radius: 1px;
    transform: translateY(-2px) rotate(45deg);
    transition: transform .3s ease;
  }
  .faq .acc__item.is-open .acc__toggle {
    transform: none;
    background: #12141D;
    border-color: #12141D;
    color: #fff;
  }
  .faq .acc__item.is-open .acc__toggle::before { transform: translateY(2px) rotate(225deg); }

  /* align-items has to be restated. The base .acc__content sets
     flex-end at >=900px, which is what puts the tags column on the right
     of a row; this variant is a COLUMN, where flex-end means "shove
     every child against the right edge" instead. Anything narrower than
     the panel — which is every .acc__desc — was being right-aligned. */
  .faq .acc__content { flex-direction: column; align-items: flex-start; padding: 0 26px 26px; gap: 0; }
  .faq .acc__desc { max-width: 640px; }

  /* Full-bleed variant for an accordion that is the section's main
     content rather than a sidebar of questions: the 640px measure is
     right beside a sticky heading column, and adrift in a 1240px band.
     The heads step up with it — these are the section's subject
     headings, not a list of questions. Declared after the .faq rules
     above so it wins at every width; a media query adds no specificity
     of its own, so source order is what decides. */
  .faq--wide .acc__desc  { max-width: none; }

  /* Dark-band variant. .faq skins every .acc__item as a white card, which
     is right on #EDEDEB or #F7F7F5 and wrong on #08090d — six white
     blocks on black read as holes punched in the section. This moves the
     card onto the shared dark surface and relights the type on it.

     Brand lime is legal here because the band is dark; the light variant
     has to use limeInk for the same marks. */
  .faq--dark .acc__item {
    background: var(--card-dark-bg);
    box-shadow: 0 0 0 1px var(--line-dark);
  }
  .faq--dark .acc__title { color: #fff; }
  .faq--dark .acc__desc  { color: rgba(255, 255, 255, .62); }
  /* The chevron is drawn from currentColor, so the toggle only needs its
     colour setting — not the pseudo-element borders. Open state inverts:
     the light variant fills the disc with ink and goes white, so this one
     fills with white and goes ink. */
  .faq--dark .acc__toggle { color: rgba(255, 255, 255, .60); }
  .faq--dark .acc__item.is-open .acc__toggle {
    background: #fff;
    border-color: #fff;
    color: #12141D;
  }
  .faq--dark.faq--numbered .acc__num {
    background: rgba(128, 194, 65, .16);
    color: #80C241;
  }

  /* Numbered variant: a counter badge before each head, for a list whose
     order is part of what it says. The base .acc__num is a bare 42px
     column of text; inside .faq it becomes the circular chip.

     limeInk on a lime wash — this is a light band, where the dark-theme
     lime is about 2:1 and would fail on the number as well as the ring. */
  .faq--numbered .acc__num {
    display: flex;
    width: 38px;
    height: 38px;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: rgba(78, 125, 34, .10);
    font-size: 15px;
    font-weight: 600;
    color: #4E7D22;
  }
  /* The head is flex and .acc__toggle already carries margin-left:auto,
     so the badge simply takes its place at the start of the row and the
     chevron stays pinned right. */
  .faq--numbered .acc__head { gap: 14px; }

  /* The photo beside a .faq--wide accordion — healthcare and automotive
     both run one. Height is capped and the crop is centred: the sources
     are tall portraits, and letting one run its natural height beside a
     column of collapsed panels leaves the column half empty below the
     last of them. */
  .side-shot {
    width: 100%;
    max-height: 620px;
    object-fit: cover;
    object-position: center;
    border-radius: var(--r-panel);
  }
  .faq--wide .acc__title { font-size: 22px; letter-spacing: -0.3px; }

  /* ── Footer wordmark ──────────────────────────────────────────────
     Oversized brand word fading downward into the footer. Painted with
     background-clip:text, so guard it: without the clip the fill would
     be transparent and the word would vanish entirely.               */
  .wordmark {
    font-weight: 700;
    line-height: .78;
    padding-top: .22em;   /* keeps ascenders and the i-dot out of the clip */
    letter-spacing: -0.045em;
    text-align: center;
    white-space: nowrap;
    color: rgba(255,255,255,.55);
  }
  @supports ((-webkit-background-clip: text) or (background-clip: text)) {
    .wordmark {
      background-image: linear-gradient(180deg,
        rgba(255,255,255,.92) 12%,
        rgba(255,255,255,.34) 62%,
        rgba(255,255,255,.04) 96%);
      -webkit-background-clip: text;
              background-clip: text;
      color: transparent;
      -webkit-text-fill-color: transparent;
    }
  }

  /* ── Tech stack tabs ──────────────────────────────────────────────
     Rail + panel. Only the selected panel is in the flow, so the card
     grid does not reserve height for the hidden categories.          */
  .tech-tab {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    padding: 14px 20px;
    border-left: 3px solid transparent;
    background: none;
    text-align: left;
    font-size: 13.5px;
    color: #6B7280;
    cursor: pointer;
    transition: background-color .2s, color .2s, border-color .2s;
  }
  .tech-tab:hover { background: rgba(18,20,29,.03); color: #12141D; }
  .tech-tab[aria-selected="true"] {
    border-left-color: #4E7D22;
    background: #fff;
    color: #12141D;
    font-weight: 600;
  }
  .tech-tab__dot {
    margin-left: auto;
    width: 6px;
    height: 6px;
    border-radius: 3px;
    background: #4E7D22;
    opacity: 0;
  }
  .tech-tab[aria-selected="true"] .tech-tab__dot { opacity: 1; }

  /* Dark-rail variant. OPT-IN VIA .tech-rail--dark ON THE RAIL, not
     scoped off .section--dark like .cmp and .quote are, because the
     section is the wrong thing to key on here: thirty-one pages put a
     WHITE tab card inside a dark band -- see the note in
     include/sections/iot/tech.php, which says so explicitly -- and a
     .section--dark rule painted all of their labels white on #F8FAFF,
     which is to say invisible. The rail's own colour is what decides,
     so the rail is what carries the class.

     Colour only; every dimension above still applies. Brand lime is
     legal on the active border and the dot here, where the light
     variant has to use limeInk. */
  .tech-rail--dark .tech-tab { color: rgba(255, 255, 255, .60); }
  .tech-rail--dark .tech-tab:hover {
    background: rgba(255, 255, 255, .05);
    color: #fff;
  }
  .tech-rail--dark .tech-tab[aria-selected="true"] {
    border-left-color: #80C241;
    background: rgba(255, 255, 255, .07);
    color: #fff;
  }
  .tech-rail--dark .tech-tab__dot { background: #80C241; }

  /* Pill variant of the same tabs, sitting above the panel instead of
     in a left rail — nine categories in a 214px column left the card
     grid narrow and the rail long. Same roles and ARIA, so the tab
     script in footer.php drives either shape.

     The row wraps rather than scrolls: a wrapped second line is easier
     to use on a phone than a horizontal scroller whose overflow is
     invisible until you drag it. */
  .tech-pills {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 10px;
  }
  .tech-pill {
    padding: 11px 20px;
    border: 1px solid var(--line-light);
    border-radius: 999px;
    background: #fff;
    /* 12px, matching .tech-card__name, so the pill and the tile caption
       below it sit at one size. */
    font-size: 12px;
    font-weight: 500;
    line-height: 1;
    color: #4B5563;
    cursor: pointer;
    transition: background-color .2s, color .2s, border-color .2s;
  }
  .tech-pill:hover { border-color: rgba(18,20,29,.22); color: #12141D; }
  /* The CTA fill, so a selected pill reads as the same brand green as
     every "Contact us" button: the .btn--primary gradient with the same
     near-black label. Dark ink on lime clears contrast where white on
     the same lime would not. */
  .tech-pill[aria-selected="true"] {
    border-color: transparent;
    background-image: linear-gradient(to right, #9BD657, #80C241, #5FA22E);
    font-weight: 600;
    color: #12141D;
  }

  .tech-panel[hidden] { display: none; }

  .tech-card {
    overflow: hidden;
    border-radius: var(--r-card);
    background: #fff;
    box-shadow: 0 2px 8px 0 rgba(0,0,0,.06), 0 0 0 1px #EAECF0;
    transition: transform .25s ease, box-shadow .25s ease;
  }
  .tech-card:hover {
    transform: translateY(-3px);
    box-shadow: 0 10px 24px -8px rgba(16,24,40,.16), 0 0 0 1px #E1E4EA;
  }
  .tech-card__bar { height: 4px; }

  /* ── Testimonials ─────────────────────────────────────────────────
     One slide is visible at a time; hidden slides leave the flow so the
     card never reserves height for the tallest quote.

     The portrait and the quote are two cards showing the same person,
     so both are switched at the same index by the slider in footer.php
     — hence the shared rule.                                         */
  /* Every quote sits in the same grid cell, so the card takes the
     height of the tallest and holds it. The media side is already a
     fixed frame, so only the quotes need stacking. */
  .tm-stack { display: grid; }
  .tm-stack > .tm-slide { grid-area: 1 / 1; }

  /* The inactive quote is hidden but still occupies its cell — that is
     the whole point, and it is why display:none cannot be used here.
     visibility takes it out of the accessibility tree and out of tab
     order just as completely, so nothing is announced or focusable
     behind the visible one. */
  .tm-slide[hidden] {
    display: block;
    visibility: hidden;
    pointer-events: none;
  }
  .tm-media[hidden] { display: none; }

  /* Slide transition. The incoming slide travels in from the direction
     of travel and fades up; forward enters from the right, back from
     the left, so the arrows read as movement along a row rather than a
     swap in place.

     Both cards clip (overflow-hidden in the markup), so the 40px offset
     is hidden until it lands rather than spilling over the section. */
  @keyframes tm-in-fwd {
    from { opacity: 0; transform: translate3d(40px, 0, 0); }
    to   { opacity: 1; transform: translate3d(0, 0, 0); }
  }
  @keyframes tm-in-back {
    from { opacity: 0; transform: translate3d(-40px, 0, 0); }
    to   { opacity: 1; transform: translate3d(0, 0, 0); }
  }
  .tm-in--fwd  { animation: tm-in-fwd  .42s cubic-bezier(.22, .61, .36, 1) both; }
  .tm-in--back { animation: tm-in-back .42s cubic-bezier(.22, .61, .36, 1) both; }

  /* The portrait carries more visual weight than the text, so it trails
     the quote slightly. Both still finish well inside half a second. */
  .tm-media.tm-in--fwd,
  .tm-media.tm-in--back { animation-delay: .06s; }

  @media (prefers-reduced-motion: reduce) {
    .tm-in--fwd, .tm-in--back,
    .tm-media.tm-in--fwd, .tm-media.tm-in--back { animation: none; }
  }

  /* The two cards. Both were a pile of arbitrary Tailwind values in the
     markup; the shadow and the lime rim are the same treatment, so they
     are one definition each rather than two strings to keep in step. */
  .tm-media-card {
    border-radius: var(--r-panel);
    box-shadow:
      0 8px 48px 0 rgba(0, 0, 0, .5),
      0 0 0 1px rgba(128, 194, 65, .15);
  }

  /* Each slide is its own containing block, so the scrim, the play
     button and the byline are positioned against the photo rather than
     against the card. Without this they resolve to .tm-media-card,
     which the grid stretches to match the taller quote card — the
     byline then floats to the bottom of that dead space instead of
     sitting on the bottom edge of the image.

     It also fills the card: the grid stretches the media card to the
     height of the taller quote card, so the photo grows with it rather
     than stopping at 431px and leaving a dark strip under the byline. */
  .tm-media {
    position: relative;
    height: 100%;
  }
  .tm-quote-card {
    border-radius: var(--r-panel);
    border: 1px solid rgba(128, 194, 65, .18);
    background: rgba(255, 255, 255, .04);
    box-shadow: 0 8px 48px 0 rgba(0, 0, 0, .35);
  }

  /* Fixed 431px thumbnail. Scoped to .tm-media rather than written as
     `img, video { height: 431px }` — that selector would set a hard
     height on every image and video on the site, squashing the logo,
     the icons and the hero footage along with it.

     object-fit keeps the aspect ratio and crops instead of stretching:
     the sources are 501×334 landscape, so at 431 tall they fill the
     frame from the sides. width/height stay on the <img> so the row
     still reserves space before the file arrives. */
  .tm-media__img {
    display: block;
    width: 100%;
    max-width: 100%;
    height: 100%;
    min-height: 431px;
    object-fit: cover;
  }

  /* Portraits are candid photographs rather than the art-directed shot
     the card was built for, so the scrim is stronger at the foot: the
     byline sits on whatever happened to be behind the subject. */
  .tm-media__scrim {
    background: linear-gradient(
      to top,
      rgba(2, 11, 24, .90) 0%,
      rgba(2, 11, 24, .30) 45%,
      rgba(2, 11, 24, .45) 100%);
  }

  /* Play affordance. An <a> to YouTube, not a <button> — it navigates,
     so it must be keyboard-reachable and open in a new tab like any
     other outbound link. */
  .tm-play {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    display: flex;
    width: 68px;
    height: 68px;
    align-items: center;
    justify-content: center;
    border-radius: 9999px;
    border: 1px solid rgba(255, 255, 255, .35);
    background: rgba(255, 255, 255, .15);
    box-shadow: 0 0 40px 0 rgba(128, 194, 65, .3);
    backdrop-filter: blur(4px);
    transition: transform .25s ease;
  }
  .tm-play:hover,
  .tm-play:focus-visible { transform: translate(-50%, -50%) scale(1.06); }
  @media (prefers-reduced-motion: reduce) {
    .tm-play, .tm-play:hover, .tm-play:focus-visible { transition: none; transform: translate(-50%, -50%); }
  }

  .tm-body {
    font-size: var(--fs-body);
    line-height: 29px;
    font-style: italic;
    color: rgba(255, 255, 255, .85);
  }
  .tm-dot {
    width: 7px;
    height: 7px;
    border-radius: 100px;
    background: rgba(255,255,255,.18);
    transition: width .3s ease, background-color .3s ease;
  }
  .tm-dot[aria-current="true"] {
    width: 22px;
    background: #80C241;
    box-shadow: 0 0 10px rgba(128,194,65,.55);
  }
  @media (prefers-reduced-motion: reduce) { .tm-dot { transition: none; } }

  /* Portrait ring. Two sizes: the byline on the video card and the
     larger one under the quote. */
  .tm-avatar {
    display: block;
    width: 52px;
    height: 52px;
    flex: none;
    overflow: hidden;
    border-radius: 9999px;
    border: 2px solid rgba(128, 194, 65, .5);
    box-shadow: 0 0 18px 0 rgba(128, 194, 65, .22);
  }
  .tm-avatar--sm {
    width: 42px;
    height: 42px;
    border-color: rgba(128, 194, 65, .6);
    box-shadow: 0 0 12px 0 rgba(128, 194, 65, .3);
  }
  .tm-avatar img { height: 100%; width: 100%; object-fit: cover; }

  /* Company pill beside the attribution. */
  .tm-org {
    flex: none;
    padding: 4px 14px;
    border-radius: 9999px;
    border: 1px solid rgba(128, 194, 65, .3);
    background: rgba(128, 194, 65, .12);
    font-size: 11.5px;
    font-weight: 600;
    color: #80C241;
  }

  /* Slide arrows. --primary is the lime one that leads. */
  .tm-nav {
    display: flex;
    width: 40px;
    height: 40px;
    flex: none;
    align-items: center;
    justify-content: center;
    border-radius: 9999px;
    border: 1px solid var(--line-dark);
    background: rgba(255, 255, 255, .06);
    color: #fff;
    transition: background-color .2s ease, transform .2s ease;
  }
  .tm-nav:hover { background: rgba(255, 255, 255, .12); }
  .tm-nav:disabled { cursor: not-allowed; opacity: .35; }
  .tm-nav:disabled:hover { background: rgba(255, 255, 255, .06); transform: none; }

  .tm-nav--primary {
    border-color: transparent;
    background: #80C241;
    color: #12141D;
    box-shadow: 0 4px 10px rgba(128, 194, 65, .35);
  }
  .tm-nav--primary:hover { background: #80C241; transform: scale(1.05); }
  .tm-nav--primary:disabled:hover { background: #80C241; transform: none; }

  @media (prefers-reduced-motion: reduce) {
    .tm-nav { transition: none; }
    .tm-nav--primary:hover { transform: none; }
  }

  /* ── Sector tiles ─────────────────────────────────────────────────
     Wash deepens and the description opens on hover. max-height is
     animated rather than height so the copy can be any length.       */
  .sector__wash {
    background: linear-gradient(180deg,
      rgba(0,0,0,0) 36%,
      rgba(12,26,6,.55) 64%,
      rgba(78,125,34,.92) 100%);
    transition: background .45s ease;
  }
  .sector:hover .sector__wash,
  .sector:focus-visible .sector__wash {
    background: linear-gradient(180deg,
      rgba(12,26,6,.40) 0%,
      rgba(45,74,20,.82) 45%,
      rgba(78,125,34,.96) 100%);
  }

  .sector__desc {
    max-height: 0;
    margin-top: 0;
    opacity: 0;
    overflow: hidden;
    transition: max-height .45s ease, opacity .3s ease, margin-top .45s ease;
  }
  /* 160px cut the client's industry copy off mid-sentence — the RPA
     descriptions run to ~275 characters, which is 200px of wrapped text
     in a 283px tile. The cap only has to exceed the tallest copy; a
     short description still opens to its own height, not to this. */
  .sector:hover .sector__desc,
  .sector:focus-visible .sector__desc {
    max-height: 240px;
    margin-top: 8px;
    opacity: 1;
  }

  @media (prefers-reduced-motion: reduce) {
    .sector__wash, .sector__desc, .sector img { transition: none; }
  }

  /* ── Stacked case studies ─────────────────────────────────────────
     Each card sticks at a slightly lower offset than the one before, so
     the next scrolls up and covers it, leaving a sliver showing. The
     cards must stay opaque or the stack shows through. Disabled below
     lg, where a card is taller than the viewport and sticky would trap
     the reader.                                                       */
  @media (min-width: 1024px) {
    .stack__item {
      position: sticky;
      top: calc(var(--header-h) + 16px + var(--stack-i) * 18px);
    }
  }

  /* ── Motion ───────────────────────────────────────────────────────
     Content is visible by default. The hidden state is only ever added
     by script, and only to elements below the fold, so a stalled script
     can never leave the page blank. The hero uses a CSS keyframe rather
     than a JS-toggled class for the same reason.                      */
  @keyframes rise-in {
    from { opacity: 0; transform: translateY(18px); }
    to   { opacity: 1; transform: none; }
  }
  /* Slowed from .7s. The hero is the first thing on a cold page load,
     and at .7s with a 90ms stagger the last element had settled before
     most readers had focused on the first. */
  .hero-in {
    animation: rise-in .95s cubic-bezier(.22,.68,.3,1) both;
    animation-delay: calc(var(--reveal-delay, 0) * 1ms);
  }

  [data-reveal].is-hidden {
    opacity: 0;
    transform: translateY(22px);
  }
  [data-reveal] {
    transition: opacity .85s cubic-bezier(.22,.68,.3,1),
                transform .85s cubic-bezier(.22,.68,.3,1);
    transition-delay: calc(var(--reveal-delay, 0) * 1ms);
  }

  @media (prefers-reduced-motion: reduce) {
    .hero-in { animation: none; }
    [data-reveal], [data-reveal].is-hidden {
      opacity: 1;
      transform: none;
      transition: none;
    }
  }

  /* ── Section eyebrow ──────────────────────────────────────────────
     Uppercase label flanked by a rule on each side. The rules are
     pseudo-elements, so markup stays a single <p class="eyebrow">.
     Override --eyebrow-color on dark sections.                       */
  .eyebrow {
    --eyebrow-color: #4E7D22;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 20px;
    font-size: 13px;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 3px;
    color: var(--eyebrow-color);
  }
  .eyebrow::before,
  .eyebrow::after {
    content: '';
    height: 1px;
    width: 64px;
    flex: none;
    background: currentColor;
    opacity: .5;
  }
  /* Dark sections light the rule and label instead of overriding
     --eyebrow-color inline on every instance. */
  .eyebrow--dark { --eyebrow-color: #80C241; }

  @media (max-width: 480px) {
    .eyebrow { gap: 12px; letter-spacing: 2px; font-size: 12px; }
    .eyebrow::before, .eyebrow::after { width: 28px; }
  }

  /* ── Section heading + subcopy ────────────────────────────────────
     One definition for every section title on the site, so sizes stay
     in step. The hero <h1> is deliberately larger and not covered.    */
  .section-title {
    font-size: 32px;
    font-weight: 500;
    line-height: 1.12;
    letter-spacing: -1px;
  }
  @media (min-width: 640px)  { .section-title { font-size: 42px; letter-spacing: -1.4px; } }
  @media (min-width: 1024px) { .section-title { font-size: 52px; letter-spacing: -1.8px; } }

  .section-sub {
    font-size: 16px;
    line-height: 26px;
    color: var(--section-sub-color, #64748B);
  }

  /* ── Section header block ─────────────────────────────────────────
     Rendered by include/partials/section-head.php. Centring, widths and
     vertical rhythm live here so every section opens identically; the
     partial only supplies the words.                                  */
  .section-head {
    max-width: 947px;
    margin-inline: auto;
    text-align: center;
  }
  .section-head > .section-title { max-width: 850px; margin: 24px auto 0; }
  .section-head > .eyebrow + .section-title { margin-top: 24px; }
  .section-head > .section-title:first-child { margin-top: 0; }
  .section-head > .section-sub { max-width: 760px; margin: 20px auto 0; }

  /* Full-width variant. BOTH caps have to go: .section-head is capped
     at 947px and the H2 inside it again at 850px, so lifting only the
     inner one leaves the header clamped by its parent and nothing
     appears to change. The subcopy keeps its 760px measure, which is
     a readability limit rather than a fit one. */
  .section-head--wide { max-width: none; }
  .section-head--wide > .section-title { max-width: none; }

  /* Body of a section: same gap under every header, every time. */
  .section-body { margin-top: 56px; }
  @media (min-width: 1024px) { .section-body { margin-top: 64px; } }

  /* Dark sections only need to say so; the subcopy colour follows. */
  .section--dark { --section-sub-color: rgba(255, 255, 255, .55); }

  /* ── Diagonal light beam ──────────────────────────────────────────
     A wide gradient bar, rotated and heavily blurred, so it reads as a
     shaft of light crossing the section. Ends are masked to nothing so
     it never terminates on a visible edge.                            */
  .beam { position: relative; isolation: isolate; }
  /* z-index 0, not -1: a negative value would drop it behind the
     section's own background and it would never be seen. Content above
     it carries z-10. */
  .beam::before {
    content: '';
    position: absolute;
    left: -25%;
    right: -25%;
    top: 50%;
    height: 300px;
    z-index: 0;
    pointer-events: none;
    transform: translateY(-50%) rotate(-22deg);
    background: linear-gradient(90deg,
      rgba(13,110,253,0)   0%,
      rgba(13,110,253,.34) 22%,
      rgba(92,106,226,.30) 44%,
      rgba(214,120,80,.32) 64%,
      rgba(253,156,61,.28) 78%,
      rgba(253,156,61,0)  100%);
    filter: blur(130px);
    /* Two axes: along the shaft AND across its thickness. Without the
       second, the band keeps hard long edges and reads as a wedge. */
    mask-image:
      linear-gradient(90deg,  transparent 0, #000 20%, #000 80%, transparent 100%),
      linear-gradient(180deg, transparent 0, #000 50%, transparent 100%);
    -webkit-mask-image:
      linear-gradient(90deg,  transparent 0, #000 20%, #000 80%, transparent 100%),
      linear-gradient(180deg, transparent 0, #000 50%, transparent 100%);
    mask-composite: intersect;
    -webkit-mask-composite: source-in;
  }

  /* ── Marquee (shared) ─────────────────────────────────────────────
     Track holds two identical groups, so translating it -50% lands
     exactly where it started and the loop is seamless. Each instance
     sets its own gap, speed and edge-fade via the custom properties.  */
  .marquee {
    --marquee-gap: 56px;
    --marquee-speed: 30s;
    --fade-start: 6%;
    --fade-end: 88%;
    overflow: hidden;
    mask-image: linear-gradient(to right, transparent 0, #000 var(--fade-start), #000 var(--fade-end), transparent 100%);
    -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--fade-start), #000 var(--fade-end), transparent 100%);
  }
  .marquee__track {
    display: flex;
    width: max-content;
    animation: marquee-scroll var(--marquee-speed) linear infinite;
  }
  .marquee:hover .marquee__track { animation-play-state: paused; }

  .marquee__group {
    display: flex;
    align-items: stretch;
    gap: var(--marquee-gap);
    padding-right: var(--marquee-gap);   /* keeps spacing even across the seam */
    margin: 0;
    list-style: none;
  }

  /* Logo variant (Trusted By) */
  /* The client-logo variant styles itself — see the <style> block in
     include/sections/trusted-by.php. */

  /* Text-wordmark variant (unused once real logos land) */
  .marquee__group--text { align-items: center; }
  .marquee__group--text li {
    white-space: nowrap;
    font-size: 22px;
    font-weight: 500;
    letter-spacing: -0.3px;
    color: rgba(255,255,255,.42);
    transition: color .2s;
  }
  .marquee__group--text li:hover { color: rgba(255,255,255,.85); }

  @keyframes marquee-scroll { to { transform: translateX(-50%); } }

  /* ── Mega menu ──────────────────────────────────────────────────
     A light panel dropped under the dark nav pill. The wrapper keeps
     its top padding while closed so the pointer never crosses a dead
     gap on its way from the trigger down into the panel.            */
  .mega { position: static; }

  .mega__drop {
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    z-index: 40;
    padding-top: 14px;
    opacity: 0;
    visibility: hidden;
    transform: translateY(8px);
    transition: opacity .18s ease, transform .18s ease, visibility .18s;
  }
  .mega:hover .mega__drop,
  .mega:focus-within .mega__drop,
  .mega.is-open .mega__drop {
    opacity: 1;
    visibility: visible;
    transform: none;
  }

  /* Compact dropdowns centre on their own trigger instead of spanning
     the nav. These come after the open-state rules so the centring
     translate survives both states. */
  .mega--pin { position: relative; }
  .mega--pin .mega__drop {
    left: 50%;
    right: auto;
    transform: translate(-50%, 8px);
  }
  .mega--pin:hover .mega__drop,
  .mega--pin:focus-within .mega__drop,
  .mega--pin.is-open .mega__drop { transform: translateX(-50%); }

  /* Only one category's link grid is mounted at a time. */
  .mega__pane { display: none; }
  .mega__pane.is-active { display: block; }

  /* Category card states live here, not in utilities, so `is-active`
     is the single source of truth the script toggles. */
  .mega__cat {
    position: relative;
    background: transparent;
    box-shadow: none;
    transition: background-color .18s ease, box-shadow .18s ease;
  }
  .mega__cat:hover { background: rgba(255,255,255,.7); }
  .mega__cat.is-active {
    background: #fff;
    box-shadow: 0 10px 30px -14px rgba(18,20,29,.3);
  }

  .mega__cat::after {
    content: '';
    position: absolute;
    top: 50%;
    right: -7px;
    width: 14px;
    height: 14px;
    background: #fff;
    border-top: 1px solid rgba(18,20,29,.06);
    border-right: 1px solid rgba(18,20,29,.06);
    transform: translateY(-50%) rotate(45deg);
    opacity: 0;
    transition: opacity .18s ease;
  }
  .mega__cat.is-active::after { opacity: 1; }

  /* Chevron sits flipped when closed; unflip it to point at the open panel. */
  .mega:hover .mega__chev,
  .mega:focus-within .mega__chev,
  .mega.is-open .mega__chev { transform: none; }

  @media (prefers-reduced-motion: reduce) {
    .glow-rim::before, .glow-rim::after { animation: none; }
    /* No auto-scroll: let the row be swiped/scrolled instead. */
    .marquee { overflow-x: auto; }
    .marquee__track { animation: none; }
    .mega__drop { transition: none; transform: none; }
    .mega--pin .mega__drop { transform: translateX(-50%); }
  }

/* ══════════════════════════════════════════════════════════════════
   DESIGN TOKENS
   The single source of truth for radii, hairlines and elevation.
   Prefer these over ad-hoc arbitrary values in markup.
   ══════════════════════════════════════════════════════════════════ */
:root {
  /* Three radius tiers only. Anything else is drift. */
  --r-sm:    12px;  /* chips, inputs, small tiles, buttons */
  --r-card:  16px;  /* the default card                    */
  --r-panel: 20px;  /* large panels and feature blocks     */

  /* Hairlines. One value per surface, so borders never read
     as two slightly different greys sitting side by side. */
  --line-dark:  rgba(255, 255, 255, .09);  /* on #08090d sections */
  --line-light: rgba(18, 20, 29, .09);     /* on white sections   */

  /* Card surfaces on dark sections. Resting and hover are declared
     once here so no two card grids drift to .04 vs .05 vs .06. */
  --card-dark-bg:          rgba(255, 255, 255, .04);
  --card-dark-bg-hover:    rgba(255, 255, 255, .07);
  --card-dark-line-hover:  rgba(255, 255, 255, .20);

  /* Type scale for card contents. Every card title on the site is
     --fs-card-title and every card description --fs-card-desc, so a
     new section cannot introduce a fourth "roughly 15px" body size. */
  --fs-card-title: 18px;
  --lh-card-title: 1.35;
  --fs-card-desc:  15px;
  --lh-card-desc:  24px;

  /* The rest of the scale, so 12 / 12.5 / 13 / 13.5 / 14 stop competing.
     --fs-label is a name or field label, --fs-meta a byline or date. */
  --fs-body:  16px;
  --lh-body:  26px;
  --fs-label: 14px;
  --fs-meta:  13px;
}

/* ── Buttons ───────────────────────────────────────────────────────
   Every CTA on the site is .btn plus one variant. Size comes from
   .btn (page) or .btn--sm (nav); colour comes from the variant.   */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  height: 52px;
  padding: 0 24px;
  border-radius: var(--r-sm);
  font-size: 14px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .4px;
  white-space: nowrap;
  transition: background-color .2s ease, border-color .2s ease,
              color .2s ease, filter .2s ease, transform .2s ease;
}
/* Nav tier. The inset is tighter than the page tier's 24px because the
   two header controls sit next to a six-item nav and a logo: at 20px
   they were the widest thing in the bar and crowded the menu out at the
   xl breakpoint. gap tightens with it, or the icon drifts away from its
   label as the button narrows. */
.btn--sm { height: 46px; padding: 0 14px; font-size: 13px; gap: 8px; }
/* Smallest tier: pills sitting on top of imagery, where a 46px control
   would cover the picture it is labelling. */
.btn--xs { height: 32px; padding: 0 14px; font-size: 11px; gap: 6px; border-radius: 9999px; }

.btn--primary {
  background-image: linear-gradient(to right, #9BD657, #80C241, #5FA22E);
  color: #12141D;
}
.btn--primary:hover { filter: brightness(1.07); }

.btn--dark { background: #000; color: #FD9C3D; }
.btn--dark:hover { background: #150e05; }

.btn--outline-dark {
  border: 1px solid rgba(255, 255, 255, .2);
  background: rgba(255, 255, 255, .04);
  color: #fff;
  backdrop-filter: blur(4px);
}
.btn--outline-dark:hover {
  border-color: rgba(255, 255, 255, .35);
  background: rgba(255, 255, 255, .09);
}

/* Outlined green CTA. On hover it fills with the SAME lime gradient as
   .btn--primary, so the two green buttons on the site resolve to one
   colour rather than a flat green and a gradient green.

   The fill is a pseudo-element rather than a background-image on the
   button, because background-image cannot transition — assigning it on
   :hover would snap. ::before fades its opacity instead, and sits at
   z-index -1 inside the button's own stacking context so it paints
   under the label (the same trick .glow-rim::after uses).

   The label flips to ink, not white: white on lime is ~2.2:1 and
   unreadable, which is why .btn--primary is dark-labelled too. */
.btn--outline-light {
  position: relative;
  isolation: isolate;
  border: 1px solid #4E7D22;
  color: #4E7D22;
}
.btn--outline-light::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background-image: linear-gradient(to right, #9BD657, #80C241, #5FA22E);
  opacity: 0;
  transition: opacity .25s ease;
}
.btn--outline-light:hover,
.btn--outline-light:focus-visible { border-color: transparent; color: #12141D; }
.btn--outline-light:hover::before,
.btn--outline-light:focus-visible::before { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .btn--outline-light::before { transition: none; }
}

.btn--white {
  background: #fff;
  color: #12141D;
  box-shadow: 0 10px 40px -12px rgba(255, 255, 255, .45);
}
.btn--white:hover { transform: translateY(-2px); }

/* Tinted lime pill for CTAs that sit over photography. */
.btn--lime-soft {
  border: 1px solid rgba(128, 194, 65, .40);
  background: rgba(128, 194, 65, .20);
  color: #80C241;
}
.btn--lime-soft:hover { background: rgba(128, 194, 65, .32); }

/* Arrow glyphs nudge on hover, consistently, in every variant. */
.btn svg { flex-shrink: 0; transition: transform .2s ease; }
.btn:hover svg { transform: translate(2px, -2px); }
/* One size for the shared arrow (partials/icon-arrow.php); the hover
   nudge above already covers every variant, so the markup carries no
   per-button transform utilities. */
.btn__arrow { width: 20px; height: 20px; }
.btn:not(.btn--sm):not(.btn--xs) .btn__arrow { width: 22px; height: 22px; }

@media (prefers-reduced-motion: reduce) {
  .btn, .btn svg { transition: none; }
  .btn--white:hover { transform: none; }
  .btn:hover svg { transform: none; }
}

/* ══════════════════════════════════════════════════════════════════
   CARDS
   Every tile on the page is .card plus a surface variant. Radius,
   hairline, padding and hover all come from here, so "make the cards
   match" is a one-file change rather than a sweep through the markup.
   ══════════════════════════════════════════════════════════════════ */
.card {
  position: relative;
  display: flex;
  flex-direction: column;
  border-radius: var(--r-card);
  border: 1px solid transparent;
  transition: background-color .25s ease, border-color .25s ease;
}
.card--pad   { padding: 24px; }
.card--panel { border-radius: var(--r-panel); }

.card--dark {
  border-color: var(--line-dark);
  background: var(--card-dark-bg);
}
.card--dark:hover {
  border-color: var(--card-dark-line-hover);
  background: var(--card-dark-bg-hover);
}

.card--light {
  border-color: var(--line-light);
  background: #fff;
}

/* Card typography. Titles and descriptions are the same size in every
   grid on the site; only the colour changes with the surface. */
.card-title {
  font-size: var(--fs-card-title);
  font-weight: 600;
  line-height: var(--lh-card-title);
  letter-spacing: -0.2px;
}
.card-desc {
  font-size: var(--fs-card-desc);
  line-height: var(--lh-card-desc);
  color: var(--card-desc-color, rgba(255, 255, 255, .55));
}
.card--light .card-desc,
.section-light .card-desc { --card-desc-color: #64748B; }

/* The framework name under a logo chip (the Java and Node.js pages'
   Frameworks band). Same size as any other small label on the site,
   and the colour follows the surface the way .card-desc does, so the
   band can be light or dark without touching the markup. */
.fw-name {
  font-size: var(--fs-meta);
  font-weight: 500;
  line-height: 1.35;
  color: var(--card-desc-color, rgba(255, 255, 255, .55));
}
.section-light .fw-name { --card-desc-color: #64748B; }

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

/* ── AI use-case icon tiles ───────────────────────────────────────
   One rule; each tile only declares its hue as an RGB triple. */
.uc-icon {
  display: flex;
  width: 50px;
  height: 50px;
  flex: none;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-card);
  border: 1px solid rgba(var(--uc-rgb), .45);
  background: rgba(var(--uc-rgb), .14);
}
.uc-icon--teal   { --uc-rgb:   0, 229, 204; }
.uc-icon--purple { --uc-rgb: 192, 132, 252; }
.uc-icon--sky    { --uc-rgb:  56, 189, 248; }
.uc-icon--orange { --uc-rgb: 251, 146,  60; }
.uc-icon--green  { --uc-rgb:  52, 211, 153; }
.uc-icon--pink   { --uc-rgb: 244, 114, 182; }
.uc-icon--blue   { --uc-rgb:  96, 165, 250; }
.uc-icon--amber  { --uc-rgb: 251, 191,  36; }
.uc-icon--red    { --uc-rgb: 248, 113, 113; }

/* Marquee preset for the use-case card row. */
.marquee--cards {
  --marquee-gap: 24px;
  --marquee-speed: 60s;
  --fade-start: 3%;
  --fade-end: 97%;
}

/* Sector tiles (Flutter industries). Twelve square tiles per group is
   a much longer track than the nine use-case cards, so it needs a
   proportionally longer duration to travel at the same speed. */
.marquee--sectors {
  --marquee-gap: 20px;
  --marquee-speed: 85s;
  --fade-start: 3%;
  --fade-end: 97%;
}

/* ── Mega-menu category icons ─────────────────────────────────────
   The rail's six accent colours, named rather than inlined. */
.mega__cat-icon {
  display: flex;
  width: 44px;
  height: 44px;
  flex: none;
  align-items: center;
  justify-content: center;
  border-radius: 9999px;
  background: #fff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .06);
}
.mega__cat-icon--blue   { color: #0D6EFD; }
.mega__cat-icon--purple { color: #7C3AED; }
.mega__cat-icon--green  { color: #059669; }
.mega__cat-icon--orange { color: #EA580C; }
.mega__cat-icon--pink   { color: #DB2777; }
.mega__cat-icon--cyan   { color: #0891B2; }
.mega__cat-icon--indigo { color: #4F46E5; }

/* Glow-rim presets, so the two brand glows are not re-declared inline
   on every button that uses them. */
.glow-rim--lime  { --glow-color: #80C241; --glow-speed: 5s; }
.glow-rim--amber { --glow-color: #FD9C3D; --glow-base: rgba(253, 156, 61, .42); --glow-speed: 6.5s; }

/* ── Section background plates ────────────────────────────────────
   Raster backdrops referenced by class rather than by an inline
   background-image, so the paths live in one file. */
.bg-plate {
  background-repeat: no-repeat;
  background-size: cover;
}
.bg-plate--testimonials { background-image: url('../img/testimonial-bg.webp'); background-position: center; }
.bg-plate--footer       { background-image: url('../img/footer-bg.webp');      background-position: bottom; }
.bg-plate--form         { background-image: url('../img/form-bg.webp');        background-position: center; }

/* ── Glow plate for a dark section ────────────────────────────────
   CSS, not a raster like the three plates above: this is two soft
   glows on black with no detail in them, so shipping it as a PNG would
   be a few hundred KB stretched to the section width for nothing —
   the same reasoning as .field--banner in the hero.

   Lime low-left, brand blue upper-right, black by the middle. It sits
   in a pseudo-element rather than on the section itself so the copy
   above it needs no stacking context of its own; the section only has
   to be positioned and to clip.

   The section keeps its own #08090d, so with the gradient unsupported
   or the pseudo-element suppressed the band is still dark and the text
   still legible. */
.glow-plate::before {
  content: '';
  position: absolute;
  inset: -20% -10%;
  pointer-events: none;
  background:
    radial-gradient(46% 54% at 88% 12%, rgba( 46, 116, 240, .34) 0%, rgba( 46, 116, 240, 0) 66%),
    radial-gradient(44% 52% at  6% 88%, rgba(128, 194,  65, .26) 0%, rgba(128, 194,  65, 0) 68%),
    radial-gradient(38% 44% at 24% 10%, rgba(253, 156,  61, .12) 0%, rgba(253, 156,  61, 0) 72%);
  filter: blur(60px);
}

/* The faint technical grid, masked to the top so it fades out before
   the copy rather than running under it edge to edge. */
.glow-plate::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image:
    linear-gradient(to right,  rgba(255, 255, 255, .03) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(255, 255, 255, .03) 1px, transparent 1px);
  background-size: 72px 72px;
  mask-image: radial-gradient(ellipse 85% 65% at 50% 0%, #000 25%, transparent 95%);
  -webkit-mask-image: radial-gradient(ellipse 85% 65% at 50% 0%, #000 25%, transparent 95%);
}

/* Everything the section renders sits above both layers. */
.glow-plate > * { position: relative; z-index: 1; }

/* A second arrangement, so two dark bands on one page do not look like
   the same band repeated. Mirrored and warmer: amber low-right, blue
   upper-left, and a lime bloom behind the middle where the poster sits
   — it lifts the artwork off the black without lighting the whole
   band. Only the ::before is redefined; the grid and the stacking rule
   above are inherited. */
.glow-plate--flare::before {
  background:
    radial-gradient(44% 52% at 10% 14%, rgba( 46, 116, 240, .30) 0%, rgba( 46, 116, 240, 0) 68%),
    radial-gradient(48% 56% at 92% 86%, rgba(253, 156,  61, .28) 0%, rgba(253, 156,  61, 0) 66%),
    radial-gradient(52% 60% at 50% 50%, rgba(128, 194,  65, .14) 0%, rgba(128, 194,  65, 0) 70%);
  filter: blur(70px);
}

/* The grid reads as clutter behind a photograph, so this variant drops
   it and keeps only the glows. */
.glow-plate--flare::after { content: none; }

/* ── Stacked case studies: sticky offsets ─────────────────────────
   The index is emitted by the loop in case-studies.php rather than
   derived with :nth-of-type. nth-of-type counts ELEMENTS, not classes,
   so the .section-head div preceding the cards shifted every card by
   one and pushed the whole stack 18px down. An explicit class cannot
   drift when the surrounding markup changes. */
.stack__item--0 { --stack-i: 0; }
.stack__item--1 { --stack-i: 1; }
.stack__item--2 { --stack-i: 2; }
.stack__item--3 { --stack-i: 3; }
.stack__item--4 { --stack-i: 4; }
.stack__item--5 { --stack-i: 5; }
.stack__item--6 { --stack-i: 6; }
.stack__item--7 { --stack-i: 7; }
.stack__item--8 { --stack-i: 8; }
.stack__item--9 { --stack-i: 9; }
.stack__item--10 { --stack-i: 10; }

/* ── SPHINX letter stagger ────────────────────────────────────────
   Same idea: the delay index comes from position in the grid. */
#pillars > *:nth-child(1) .pillar-letter { --i: 0; }
#pillars > *:nth-child(2) .pillar-letter { --i: 1; }
#pillars > *:nth-child(3) .pillar-letter { --i: 2; }
#pillars > *:nth-child(4) .pillar-letter { --i: 3; }
#pillars > *:nth-child(5) .pillar-letter { --i: 4; }
#pillars > *:nth-child(6) .pillar-letter { --i: 5; }

/* Footer wordmark size: fluid, and no longer an inline font-size. */
.wordmark { font-size: clamp(72px, 18.5vw, 290px); }

/* Wordmark rise. Driven directly by scroll position, not a one-shot
   reveal: the word tracks how far the footer has entered the viewport,
   so it keeps moving while you scroll and reverses if you scroll back.

   No transform and no transition are declared here — the script sets
   the transform each frame, and a transition would make it lag behind
   the scroll rather than follow it. With no JS the word simply sits at
   rest, which is why nothing is offset by default. */
.wordmark { will-change: transform; }

@media (prefers-reduced-motion: reduce) {
  /* Belt and braces: the script bails out too. */
  .wordmark { transform: none !important; }
}

/* A wrapping row of .tech-card tiles whose last line centres. Used by
   the case-study tech band, where the count rarely divides evenly.

   Widths are computed from the gap so the tiles sit flush at each
   breakpoint: n across means (100% - (n-1) * gap) / n. Written as a
   percentage minus a fixed remainder because calc cannot divide by a
   percentage. */
/* The list reset is in :where() so it carries no specificity. Written
   as a plain .tech-row rule it beats .section-body — both are one class,
   and this one is later in the file — which silently zeroed the 56/64px
   gap under the section heading. */
:where(.tech-row) { margin: 0; padding: 0; }
.tech-row {
  --tech-gap: 16px;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--tech-gap);
  list-style: none;
}
.tech-row > li { width: calc(50% - var(--tech-gap) / 2); }
@media (min-width: 640px)  { .tech-row > li { width: calc(33.3333% - var(--tech-gap) * 2 / 3); } }
@media (min-width: 1024px) { .tech-row > li { width: calc(20% - var(--tech-gap) * 4 / 5); } }

/* ── Tech stack cards ─────────────────────────────────────────────
   Each card declares one custom property, --tech-rgb, and the bar,
   the icon tile and any lettermark all derive from it. */
.tech-card { --tech-rgb: 107, 114, 128; }
.tech-card__bar {
  background: linear-gradient(90deg,
    rgba(var(--tech-rgb), .33),
    rgba(var(--tech-rgb), .13));
}
/* Wordmark logos — UiPath, Kofax, PEGA — are far wider than they are
   tall. Without object-fit an <img> with equal width/height attributes
   is stretched to fill, so they arrive distorted. */
.tech-card__tile img { object-fit: contain; }

.tech-card__tile {
  display: flex;
  width: 60px;
  height: 60px;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-sm);
  background: rgba(var(--tech-rgb), .06);
}
/* Lettermarks are darkened toward ink before painting. Several brand hues
   (Hugging Face yellow, React cyan, AWS orange) sit near 2:1 on the tile's
   near-white wash; mixing 62% brand with ink keeps the hue recognisable and
   clears 4.5:1. The flat value is the fallback where color-mix is missing. */
.tech-card__mark {
  font-size: 15px;
  font-weight: 700;
  color: rgb(var(--tech-rgb));
  color: color-mix(in srgb, rgb(var(--tech-rgb)) 62%, #101318);
text-transform: uppercase;
}
/* Sprite glyph for the entries with no brand mark of their own. Same
   darkening as the lettermark, for the same reason: several brand hues
   sit near 2:1 on the tile's near-white wash. */
.tech-card__glyph {
  color: rgb(var(--tech-rgb));
  color: color-mix(in srgb, rgb(var(--tech-rgb)) 62%, #101318);
}
.tech-card__name {
  padding-bottom: 4px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .12px;
  text-align: center;
  color: #6B7280;
}

/* Tech stack on a dark band. Scoped to the section rather than changing
   .tech-card, because the home page and the Flutter page run the same
   partial on light surfaces.

   The icon tile deliberately stays light. The brand SVGs in
   assets/img/tech/ are full-colour artwork drawn for a near-white
   ground — MySQL's dolphin and the PHP wordmark are both near-navy, and
   dropping them onto a dark tile erases them. Only the card behind the
   tile goes dark, so every mark reads exactly as its owner drew it and
   .tech-card__mark / __glyph keep their darkened-toward-ink colour. */
.tech-band--dark .tech-card {
  background: var(--card-dark-bg);
  box-shadow: 0 0 0 1px var(--line-dark);
}
.tech-band--dark .tech-card:hover {
  background: var(--card-dark-bg-hover);
  box-shadow: 0 12px 28px -10px rgba(0, 0, 0, .55), 0 0 0 1px rgba(255, 255, 255, .16);
}
.tech-band--dark .tech-card__tile {
  background: #F4F5F7;
  background: color-mix(in srgb, rgb(var(--tech-rgb)) 10%, #fff);
}
.tech-band--dark .tech-card__name { color: rgba(255, 255, 255, .62); }

/* ══════════════════════════════════════════════════════════════════
   TECH-STACK BRAND HUES
   One triple per tool. The bar, the tile wash and the lettermark all
   derive from it, so a logo swap is a one-line change here and no
   colour is ever typed into the markup.
   ══════════════════════════════════════════════════════════════════ */
.tech--html5       { --tech-rgb: 228,  77,  38; }
.tech--css3        { --tech-rgb:  21, 114, 182; }
.tech--js          { --tech-rgb: 247, 223,  30; }
.tech--react       { --tech-rgb:  97, 218, 251; }
.tech--angular     { --tech-rgb: 221,   0,  49; }
.tech--vue         { --tech-rgb:  65, 184, 131; }
.tech--ember       { --tech-rgb: 224,  78,  57; }
.tech--meteor      { --tech-rgb: 223,  85,  57; }
.tech--next        { --tech-rgb:  17,  17,  17; }

.tech--node        { --tech-rgb:  83, 158,  67; }
.tech--python      { --tech-rgb:  55, 118, 171; }
.tech--java        { --tech-rgb: 234,  32,  39; }
.tech--dotnet      { --tech-rgb:  92,  45, 145; }
.tech--php         { --tech-rgb: 119, 123, 180; }
.tech--go          { --tech-rgb:   0, 173, 216; }
.tech--laravel     { --tech-rgb: 255,  45,  32; }
.tech--django      { --tech-rgb:   9,  74,  57; }
.tech--rails       { --tech-rgb: 204,   0,   0; }

.tech--swift       { --tech-rgb: 240,  81,  56; }
.tech--kotlin      { --tech-rgb: 127,  82, 255; }
.tech--flutter     { --tech-rgb:  66, 165, 245; }
.tech--ionic       { --tech-rgb:  56, 128, 255; }

.tech--tensorflow  { --tech-rgb: 255, 111,   0; }
.tech--pytorch     { --tech-rgb: 238,  76,  44; }
.tech--openai      { --tech-rgb:  16, 163, 127; }
.tech--langchain   { --tech-rgb: 127, 200, 255; }
.tech--scikit      { --tech-rgb: 247, 147,  26; }
.tech--huggingface { --tech-rgb: 255, 214,   0; }

.tech--postgres    { --tech-rgb:  51, 103, 145; }
.tech--mysql       { --tech-rgb:   0, 117, 143; }
.tech--mongodb     { --tech-rgb:  71, 162,  72; }
.tech--redis       { --tech-rgb: 217,  55,  43; }
.tech--elastic     { --tech-rgb:   0, 191, 179; }
.tech--firebase    { --tech-rgb: 255, 202,  40; }

.tech--aws         { --tech-rgb: 255, 153,   0; }
.tech--azure       { --tech-rgb:   0, 120, 212; }
/* Blue Ocean has no logo file yet; the hue drives its lettermark. */
.tech--blueocean { --tech-rgb:  41, 128, 185; }
.tech--gcp         { --tech-rgb:  66, 133, 244; }
.tech--docker      { --tech-rgb:  36, 150, 237; }
.tech--kubernetes  { --tech-rgb:  50, 108, 229; }
.tech--vercel      { --tech-rgb:  17,  17,  17; }

/* AI page: models, agent frameworks, vector stores, ML platforms, the
   managed cloud AI services and the MLOps chain. Each hue is sampled
   from the logo it sits under. */
.tech--claude         { --tech-rgb: 217, 119,  87; }
.tech--gemini         { --tech-rgb:  66, 133, 244; }
.tech--llama          { --tech-rgb:   8, 102, 255; }
.tech--mistral        { --tech-rgb: 255, 112,   0; }
.tech--phi            { --tech-rgb:   0, 120, 212; }

.tech--llamaindex     { --tech-rgb: 188, 141, 235; }
.tech--autogen        { --tech-rgb:  17,  17,  17; }
.tech--crewai         { --tech-rgb:  70,  24,  22; }
.tech--langgraph      { --tech-rgb:  28,  60,  60; }
.tech--semantickernel { --tech-rgb: 123,  92, 240; }

.tech--pinecone       { --tech-rgb:  17,  17,  17; }
.tech--weaviate       { --tech-rgb:  96, 189,  88; }
.tech--chromadb       { --tech-rgb: 255, 100,  70; }
.tech--qdrant         { --tech-rgb: 220,  36,  76; }

.tech--sagemaker      { --tech-rgb:   5,  95,  78; }
.tech--azureml        { --tech-rgb:   0, 120, 212; }
.tech--bedrock        { --tech-rgb:  61, 143, 255; }
.tech--azureopenai    { --tech-rgb:   0, 120, 212; }
.tech--vertexai       { --tech-rgb:  66, 133, 244; }

.tech--mlflow         { --tech-rgb:   1, 148, 226; }

/* Generative AI page: the frameworks, serving layer, monitoring,
   fine-tuning and image-generation tools that page adds. */
.tech--haystack       { --tech-rgb:  37,  99, 235; }
.tech--milvus         { --tech-rgb:  79, 196, 249; }
.tech--vllm           { --tech-rgb: 253, 181,  21; }
.tech--triton         { --tech-rgb: 116, 183,  27; }
.tech--ollama         { --tech-rgb:  17,  17,  17; }
.tech--langsmith      { --tech-rgb:  28,  60,  60; }
.tech--arize          { --tech-rgb: 255,   0, 140; }
.tech--wandb          { --tech-rgb: 255, 190,   0; }
.tech--restapi        { --tech-rgb:   0, 150, 214; }
.tech--lora           { --tech-rgb:   0, 159, 227; }
.tech--trl            { --tech-rgb: 236,  72, 153; }
.tech--axolotl        { --tech-rgb:  20,  19,  16; }
.tech--sdxl           { --tech-rgb: 255, 105,  60; }
.tech--dalle          { --tech-rgb:  17,  17,  17; }
.tech--comfyui        { --tech-rgb:  22,  45, 212; }

/* AI chatbot page: NLP toolkits, the speech layer, the messaging
   channels a bot ships on, and the API/cloud pieces under it. */
.tech--spacy          { --tech-rgb:   9, 168, 156; }
.tech--nltk           { --tech-rgb:  55, 118, 171; }
.tech--corenlp        { --tech-rgb: 140,  21,  21; }
.tech--rasa           { --tech-rgb:  90,  23, 238; }
.tech--dialogflow     { --tech-rgb:  66, 133, 244; }
.tech--azurecognitive { --tech-rgb:   0, 120, 212; }
.tech--awstranscribe  { --tech-rgb:   1, 168, 141; }
.tech--elevenlabs     { --tech-rgb:  17,  17,  17; }

.tech--twilio         { --tech-rgb: 242,  47,  70; }
.tech--whatsapp       { --tech-rgb:  37, 211, 102; }
.tech--teams          { --tech-rgb:  98, 100, 167; }
.tech--telegram       { --tech-rgb:  42, 171, 238; }
.tech--discord        { --tech-rgb:  88, 101, 242; }
.tech--intercom       { --tech-rgb:  25, 103, 210; }
.tech--zendesk        { --tech-rgb:  23,  73,  77; }

.tech--fastapi        { --tech-rgb:   5, 153, 139; }
.tech--websocket      { --tech-rgb:  32, 106,  93; }
.tech--graphql        { --tech-rgb: 229,  53, 171; }
.tech--awslambda      { --tech-rgb: 250, 124,  31; }
.tech--awsec2         { --tech-rgb: 255, 153,   0; }

/* Agentic AI page: the two API-layer marks that page adds. */
.tech--webhooks       { --tech-rgb: 199,  58,  99; }
.tech--grpc           { --tech-rgb:  36, 150, 237; }

/* Benefit figure on the RPA page: the number carries the card, so it
   is set oversized in the brand lime with the label beneath it. */
.stat-figure {
  font-family: Outfit, system-ui, sans-serif;
  font-size: 34px;
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -1px;
  color: #80C241;
}

/* RPA page: the automation platforms. */
.tech--uipath         { --tech-rgb: 250,  70,  22; }
.tech--automationany  { --tech-rgb: 232,  66,  46; }
.tech--blueprism      { --tech-rgb:  27,  95, 170; }
.tech--powerautomate  { --tech-rgb:   0, 102, 255; }
.tech--kofax          { --tech-rgb:   0,  94, 184; }
.tech--azureautomation{ --tech-rgb:   0, 120, 212; }
.tech--workfusion     { --tech-rgb: 228,   0,  43; }
.tech--pega           { --tech-rgb:  31,  78, 121; }
.tech--nice           { --tech-rgb:  17,  17,  17; }
.tech--githubactions  { --tech-rgb:  32, 136, 255; }
.tech--prometheus     { --tech-rgb: 218,  78,  49; }
.tech--terraform      { --tech-rgb:  92,  78, 229; }

/* Flutter page: platforms, tooling, design and release targets. The
   handful with no brand of their own (Web, embedded, TV, prototyping,
   APK) take a neutral slate so they read as a set rather than as
   arbitrary colour. */
.tech--dart          { --tech-rgb:   1, 117, 194; }
.tech--android       { --tech-rgb:  61, 220, 132; }
.tech--ios           { --tech-rgb:  17,  17,  17; }
.tech--web           { --tech-rgb:  71, 100, 140; }
.tech--embedded      { --tech-rgb:  71, 100, 140; }
.tech--tv            { --tech-rgb:  71, 100, 140; }

.tech--androidstudio { --tech-rgb:  60, 220, 132; }
.tech--vscode        { --tech-rgb:   0, 122, 204; }
.tech--intellij      { --tech-rgb: 254,  49, 91; }
.tech--git           { --tech-rgb: 240,  80,  50; }
.tech--gitlab        { --tech-rgb: 252,  109, 38; }
.tech--bitbucket     { --tech-rgb:  38, 132, 255; }

.tech--jira          { --tech-rgb:  38, 132, 255; }
.tech--confluence    { --tech-rgb:  38, 132, 255; }
.tech--trello        { --tech-rgb:   0, 121, 191; }
.tech--slack         { --tech-rgb:  74,  21,  75; }
.tech--clickup       { --tech-rgb: 123,  104, 238; }

.tech--objectbox     { --tech-rgb:  23, 165, 170; }
.tech--sqlite        { --tech-rgb:   0, 121, 194; }

.tech--jenkins       { --tech-rgb: 211,  56,  51; }
.tech--codemagic     { --tech-rgb: 249,  74,  33; }
.tech--bitrise       { --tech-rgb: 118,  15, 210; }

.tech--prototype     { --tech-rgb:  71, 100, 140; }
.tech--figma         { --tech-rgb: 162,  89, 255; }
.tech--adobexd       { --tech-rgb: 255,  97, 210; }
.tech--sketch        { --tech-rgb: 250, 179,  26; }
.tech--zeplin        { --tech-rgb: 253, 189,  57; }

.tech--testflight    { --tech-rgb:   0, 122, 255; }
.tech--apk           { --tech-rgb:  71, 100, 140; }
.tech--ganalytics    { --tech-rgb: 232, 113,  10; }

/* Enterprise systems the Automation Anywhere bots integrate with. Every
   Automation Anywhere module — Bot Creator, Bot Runner, IQ Bot and the
   rest — reuses .tech--automationany above, since they are one product
   family and one brand red. */
.tech--sap           { --tech-rgb:   0, 143, 211; }
.tech--oracle        { --tech-rgb: 234,  28,  38; }
.tech--dynamics365   { --tech-rgb:   0, 120, 212; }
.tech--salesforce    { --tech-rgb:   0, 161, 224; }
.tech--servicenow    { --tech-rgb:  98, 214, 154; }
.tech--workday       { --tech-rgb:   8, 117, 225; }
.tech--netsuite      { --tech-rgb:  29,  53,  87; }
.tech--hubspot       { --tech-rgb: 255, 122,  89; }
.tech--sqlserver     { --tech-rgb:   0, 120, 212; }

/* Capabilities with no brand of their own; the glyph carries them. */
.tech--idp           { --tech-rgb: 124,  58, 237; }
.tech--ocr           { --tech-rgb:  13, 148, 136; }
.tech--genai         { --tech-rgb: 219,  39, 119; }
.tech--nlp           { --tech-rgb:  37,  99, 235; }
.tech--cv            { --tech-rgb: 234,  88,  12; }

.tech--soapapi       { --tech-rgb:  14, 165, 233; }
.tech--webservices   { --tech-rgb: 100, 116, 139; }
.tech--apiconnectors { --tech-rgb:  71,  85, 105; }
.tech--rpaconnectors { --tech-rgb:  37,  99, 235; }
.tech--customint     { --tech-rgb: 168,  85, 247; }

.tech--microsoft365  { --tech-rgb:  91,  73, 197; }
.tech--sharepoint    { --tech-rgb:   3, 120, 124; }
.tech--outlook       { --tech-rgb:   0, 114, 198; }
.tech--gworkspace    { --tech-rgb:  66, 133, 244; }

.tech--powerbi       { --tech-rgb: 242, 196,   0; }
.tech--tableau       { --tech-rgb: 224, 119,  42; }
.tech--excelreport   { --tech-rgb:  16, 124,  65; }
.tech--dashboards    { --tech-rgb: 236,  72, 153; }

.tech--activedir     { --tech-rgb:   0, 120, 212; }
.tech--azuread       { --tech-rgb:   0, 120, 212; }
.tech--sso           { --tech-rgb:  45, 110, 215; }
.tech--rbac          { --tech-rgb:  71,  85, 105; }
.tech--mfa           { --tech-rgb:  22, 101, 138; }

.tech--pdf           { --tech-rgb: 220,  53,  69; }
.tech--excel         { --tech-rgb:  16, 124,  65; }
.tech--word          { --tech-rgb:  43,  87, 154; }
.tech--email         { --tech-rgb:  37,  99, 235; }
.tech--scandoc       { --tech-rgb: 100, 116, 139; }
.tech--formproc      { --tech-rgb:  79,  70, 229; }

/* Agentic Process Automation stack. The LLMs, agent frameworks and ML
   tools it shares with the Generative AI page reuse the hues above. */
.tech--azureai       { --tech-rgb:   0, 120, 212; }
.tech--zapier        { --tech-rgb: 255,  74,  30; }
.tech--mulesoft      { --tech-rgb:   0, 163, 224; }
.tech--snowflake     { --tech-rgb:  41, 181, 232; }
.tech--bigquery      { --tech-rgb:  66, 133, 244; }
.tech--databricks    { --tech-rgb: 255,  54,  33; }
.tech--oauth2        { --tech-rgb:  71,  85, 105; }
.tech--auditlog      { --tech-rgb:  13, 148, 136; }
.tech--encryption    { --tech-rgb:  37,  99, 235; }
.tech--soc2          { --tech-rgb:  51,  65,  85; }

/* Intelligent Process Automation stack: document intelligence, process
   mining, BPM and the identity tools. The RPA platforms, clouds, ERPs
   and databases it shares with the pages above reuse those hues. */
.tech--abbyy         { --tech-rgb: 227,  30,  36; }
.tech--azuredoc      { --tech-rgb:   0, 120, 212; }
.tech--googledocai   { --tech-rgb:  66, 133, 244; }
.tech--textract      { --tech-rgb:   1, 150, 137; }
.tech--tesseract     { --tech-rgb:  66, 133, 244; }
.tech--azurelang     { --tech-rgb:   0, 120, 212; }
.tech--googlenl      { --tech-rgb: 168,  85, 247; }
.tech--camunda       { --tech-rgb: 253, 176,  49; }
.tech--pegabpm       { --tech-rgb:  30,  58, 138; }
.tech--nintex        { --tech-rgb: 234,  88,  12; }
.tech--celonis       { --tech-rgb:  87, 217,  85; }
.tech--processadv    { --tech-rgb:   0, 120, 212; }
.tech--signavio      { --tech-rgb:   0, 143, 211; }
.tech--processmaker  { --tech-rgb:  37,  99, 235; }
.tech--looker        { --tech-rgb:  66, 133, 244; }
.tech--qlik          { --tech-rgb:   0, 155,  87; }
.tech--datastudio    { --tech-rgb:  66, 133, 244; }
.tech--okta          { --tech-rgb:   0,  40,  126; }
.tech--defender      { --tech-rgb:   0,  92, 181; }
.tech--awsiam        { --tech-rgb: 118, 155,  63; }
.tech--pegarpa       { --tech-rgb:  30,  58, 138; }

/* Procure-to-Pay stack. The "custom" and deployment-mode entries have
   no brand of their own, so they carry a neutral slate and fall back
   to a lettermark or a sprite glyph in tech-card.php. */
.tech--sage          { --tech-rgb:   0, 220, 130; }
.tech--infor         { --tech-rgb: 210,  35,  42; }
.tech--appian        { --tech-rgb:  32,  70, 149; }
.tech--boomi         { --tech-rgb:   0, 132, 209; }
.tech--powerplatform { --tech-rgb: 116,  43, 164; }
.tech--azureint      { --tech-rgb:   0, 120, 212; }
.tech--customllm     { --tech-rgb: 139,  92, 246; }
.tech--customocr     { --tech-rgb:  13, 148, 136; }
.tech--customwf      { --tech-rgb:  59, 130, 246; }
.tech--onprem        { --tech-rgb: 100, 116, 139; }
.tech--hybrid        { --tech-rgb: 100, 116, 139; }

/* Blockchain stack. Arweave and Slither take a lettermark: v2's file
   for Arweave carries no logo at all, only the wordmark, and Slither's
   is a lockup that would repeat the caption underneath it. */
.tech--solidity      { --tech-rgb:  54,  56,  56; }
.tech--rust          { --tech-rgb:  30,  30,  30; }
.tech--vyper         { --tech-rgb: 120, 122, 128; }
.tech--ethereum      { --tech-rgb:  98, 126, 234; }
.tech--hyperledger   { --tech-rgb:  41,  70,  97; }
.tech--polygon       { --tech-rgb: 130,  71, 229; }
.tech--solana        { --tech-rgb:  20, 241, 149; }
.tech--bsc           { --tech-rgb: 243, 186,  47; }
.tech--avalanche     { --tech-rgb: 232,  65,  66; }
.tech--tron          { --tech-rgb: 235,   0,  41; }
.tech--stellar       { --tech-rgb:  20,  20,  20; }
.tech--corda         { --tech-rgb: 237,  28,  36; }
.tech--web3js        { --tech-rgb: 245, 132,  38; }
.tech--ethersjs      { --tech-rgb:  36,  90, 209; }
.tech--ipfs          { --tech-rgb:  101, 194, 203; }
.tech--arweave       { --tech-rgb:  34,  34,  34; }
.tech--metamask      { --tech-rgb: 246, 133,  27; }
.tech--walletconnect { --tech-rgb:  59, 153, 252; }
.tech--phantom       { --tech-rgb: 171, 159, 242; }
.tech--trustwallet   { --tech-rgb:  51, 117, 187; }
.tech--chainlink     { --tech-rgb:  55,  91, 210; }
.tech--bandprotocol  { --tech-rgb:  81,  108, 246; }
.tech--mythx         { --tech-rgb:  35,  90, 232; }
.tech--slither       { --tech-rgb: 190,  30,  45; }
.tech--echidna       { --tech-rgb: 190,  30,  45; }
.tech--codereview    { --tech-rgb: 100, 116, 139; }
.tech--cicd          { --tech-rgb:  91,  73, 197; }

/* Metaverse stack. */
.tech--unity         { --tech-rgb:  34,  34,  34; }
.tech--unreal        { --tech-rgb:  15,  15,  15; }
.tech--threejs       { --tech-rgb:  20,  20,  20; }
.tech--arkit         { --tech-rgb:  10, 132, 255; }
.tech--spatial       { --tech-rgb:  56, 189, 248; }
.tech--arcore        { --tech-rgb: 124,  58, 237; }
.tech--webxr         { --tech-rgb: 220,  38,  38; }
.tech--openxr        { --tech-rgb: 120,  45, 142; }
.tech--smartcontract { --tech-rgb: 247, 147,  26; }
.tech--webassembly   { --tech-rgb: 101,  79, 240; }
.tech--webgpu        { --tech-rgb:   0,  90, 156; }

/* Mobile app stack. The four Firebase products share the amber of the
   parent brand; their marks differ only by the badge on the corner. */
.tech--maui            { --tech-rgb: 129,  73, 195; }
.tech--firebasertdb    { --tech-rgb: 255, 160,   0; }
.tech--firestore       { --tech-rgb: 255, 160,   0; }
.tech--firebaseappdist { --tech-rgb: 255, 160,   0; }
.tech--firebasetestlab { --tech-rgb: 255, 160,   0; }
.tech--coreml          { --tech-rgb:  10, 132, 255; }
.tech--mlkit           { --tech-rgb:  66, 133, 244; }
.tech--xctest          { --tech-rgb:  52, 168,  83; }
.tech--espresso        { --tech-rgb:  61, 220, 132; }
.tech--appium          { --tech-rgb: 236,  85,  42; }
.tech--browserstack    { --tech-rgb: 255, 102,  16; }
.tech--owaspzap        { --tech-rgb:  18,  36,  70; }
.tech--fastlane        { --tech-rgb: 227,  69,  59; }
.tech--detox           { --tech-rgb:  40,  44,  52; }

/* iOS stack. Apple's first-party frameworks have no brand colour of
   their own, so they take the hue of the icon Apple ships for each. */
.tech--swiftui         { --tech-rgb: 240,  81,  56; }
.tech--objectivec      { --tech-rgb:  67,  86, 112; }
.tech--uikit           { --tech-rgb:  46, 134, 222; }
.tech--combine         { --tech-rgb: 240,  81,  56; }
.tech--xcode           { --tech-rgb:  20, 122, 255; }
.tech--instruments     { --tech-rgb: 122,  92, 199; }
.tech--cocoapods       { --tech-rgb: 231,  56,  53; }
.tech--spm             { --tech-rgb: 240, 129,  56; }
.tech--amplify         { --tech-rgb: 255, 153,   0; }
.tech--azuremobile     { --tech-rgb:   0, 120, 212; }
.tech--coredata        { --tech-rgb:  90, 116, 213; }
.tech--realm           { --tech-rgb: 249,  81, 111; }
.tech--userdefaults    { --tech-rgb: 100, 116, 139; }
.tech--icloud          { --tech-rgb:  62, 168, 240; }
.tech--healthkit       { --tech-rgb: 250,  45,  72; }
.tech--createml        { --tech-rgb:  10, 132, 255; }
.tech--mapkit          { --tech-rgb:  52, 168,  83; }
.tech--passkit         { --tech-rgb:  28,  28,  30; }
.tech--storekit        { --tech-rgb:  10, 132, 255; }
.tech--cloudkit        { --tech-rgb:  62, 168, 240; }
.tech--avfoundation    { --tech-rgb:  88,  86, 214; }
.tech--vision          { --tech-rgb: 236,  72, 153; }
.tech--naturallang     { --tech-rgb: 124,  58, 237; }
.tech--keychain        { --tech-rgb: 245, 158,  11; }
.tech--biometric       { --tech-rgb:  10, 132, 255; }
.tech--certpinning     { --tech-rgb:  22, 163,  74; }
.tech--jwt             { --tech-rgb:  30,  30,  30; }

/* Android stack. The three architecture patterns are concepts rather
   than products, so they carry a sprite glyph and a neutral slate. */
.tech--reactnative     { --tech-rgb:  97, 218, 251; }
.tech--redux           { --tech-rgb: 118,  74, 188; }
.tech--wordpress       { --tech-rgb:  33, 117, 155; }
.tech--elementor       { --tech-rgb: 146,   0,  59; }
.tech--jetpack         { --tech-rgb:  61, 220, 132; }
.tech--xmllayouts      { --tech-rgb: 236, 100,  35; }
.tech--material3       { --tech-rgb: 103,  80, 164; }
.tech--spring          { --tech-rgb: 109, 179,  63; }
.tech--roomdb          { --tech-rgb:  61, 220, 132; }
.tech--androidthings   { --tech-rgb:  61, 220, 132; }
.tech--wearos          { --tech-rgb:  66, 133, 244; }
.tech--ble             { --tech-rgb:   0,  82, 156; }
.tech--mqtt            { --tech-rgb: 102,   0, 153; }
.tech--androidxtest    { --tech-rgb:  61, 220, 132; }
.tech--junit           { --tech-rgb: 223,  73,  55; }
.tech--mockito         { --tech-rgb: 120, 190,  32; }
.tech--mixpanel        { --tech-rgb: 122,  91, 251; }
.tech--amplitude       { --tech-rgb:  27, 160, 253; }
.tech--sslpinning      { --tech-rgb:  22, 163,  74; }
.tech--proguard        { --tech-rgb: 245, 158,  11; }
.tech--owasp           { --tech-rgb:   0,  91, 148; }
.tech--mvvm            { --tech-rgb: 100, 116, 139; }
.tech--cleanarch       { --tech-rgb: 100, 116, 139; }
.tech--mvi             { --tech-rgb: 100, 116, 139; }

/* Cross-platform stack: auth providers and payment gateways. */
.tech--auth0           { --tech-rgb: 235,  87,  36; }
.tech--firebaseauth    { --tech-rgb: 255, 160,   0; }
.tech--azureadb2c      { --tech-rgb:   0, 120, 212; }
.tech--stripe          { --tech-rgb:  99,  91, 255; }
.tech--razorpay        { --tech-rgb:   2, 132, 199; }
.tech--braintree       { --tech-rgb:  47,  74, 122; }
.tech--applepay        { --tech-rgb:  28,  28,  30; }

/* Low-code stack. The remaining tiles (Zapier, HTML5, CSS3, JS, REST,
   webhooks, MySQL, Postgres, Azure, AWS, Tableau) reuse hues above. */
.tech--mendix          { --tech-rgb:   0,  83, 160; }
.tech--kissflow        { --tech-rgb:  27, 117, 187; }
.tech--outsystems      { --tech-rgb: 196,  22,  28; }
.tech--powerapps       { --tech-rgb: 116,  39, 116; }
.tech--power-automate  { --tech-rgb:   0, 102, 255; }
.tech--sql-server      { --tech-rgb: 204,  41,  39; }
.tech--power-bi        { --tech-rgb: 242, 200,  17; }

/* Business Intelligence stack. The thirteen tiles v2 renders as plain
   text plates carry a sprite glyph and a neutral slate hue, so they
   read as a set apart from the branded marks beside them. */
.tech--spark           { --tech-rgb: 230, 82,  47; }
.tech--hadoop          { --tech-rgb: 255, 204,   0; }
.tech--redshift        { --tech-rgb: 143,  73, 217; }
.tech--synapse         { --tech-rgb:   0, 120, 212; }
.tech--datafactory     { --tech-rgb:   0, 120, 212; }
.tech--talend          { --tech-rgb: 255, 106,   0; }
.tech--airflow         { --tech-rgb:   1, 122, 135; }
.tech--ssis            { --tech-rgb: 204,  41,  39; }
.tech--looker-studio   { --tech-rgb:  66, 133, 244; }
.tech--custometl       { --tech-rgb: 100, 116, 139; }
.tech--kpi             { --tech-rgb: 100, 116, 139; }
.tech--customrep       { --tech-rgb: 100, 116, 139; }
.tech--compliance      { --tech-rgb: 100, 116, 139; }

/* DevOps + app-hiring stacks. */
.tech--github          { --tech-rgb:  36,  41,  47; }
.tech--asana           { --tech-rgb: 244,  86,  83; }
.tech--automation-anywhere { --tech-rgb: 250,  70,  22; }

/* ══════════════════════════════════════════════════════════════════
   RECURRING TILES
   Each of these replaced a ~200-character utility string that had been
   pasted between four and forty times. The skin lives here; the markup
   keeps only its layout utilities.
   ══════════════════════════════════════════════════════════════════ */

/* Stat figure + label, shared by the results band and the hex badges,
   so a headline number is one size wherever it appears. */
.stat-figure {
  font-size: 44px;
  font-weight: 700;
  line-height: 1.05;
  color: #80C241;
}
@media (min-width: 1024px) { .stat-figure { font-size: 60px; line-height: 64px; } }

.stat-label {
  margin-top: 8px;
  font-size: var(--fs-meta);
  line-height: 16px;
  letter-spacing: .6px;
  text-align: center;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .55);
}

/* Hex badge contents. Smaller than .stat-figure because the glyph has
   to sit inside the hexagon's inner ring at every breakpoint. */
.hex-figure {
  font-size: 30px;
  font-weight: 700;
  line-height: 1;
  color: #80C241;
  transition: color .3s ease;
}
.hexwrap:hover .hex-figure { color: #fff; }
@media (min-width: 640px)  { .hex-figure { font-size: 40px; } }
@media (min-width: 1024px) { .hex-figure { font-size: 57px; } }

.hex-label {
  margin-top: 8px;
  max-width: 80%;
  font-size: var(--fs-label);
  line-height: 1.35;
  letter-spacing: .2px;
  color: rgba(255, 255, 255, .6);
}
@media (prefers-reduced-motion: reduce) { .hex-figure { transition: none; } }

/* Ratings row and office row: a dark card whose whole body is one link. */
.link-card {
  display: flex;
  align-items: center;
  border-radius: var(--r-panel);
  border: 1px solid var(--line-dark);
  background: var(--card-dark-bg);
  transition: background-color .25s ease, border-color .25s ease;
}
.link-card:hover {
  border-color: var(--card-dark-line-hover);
  background: var(--card-dark-bg-hover);
}

/* White chip that keeps a dark-on-light logo legible on a dark card. */
/* Width grows with the mark: a square rating logo still lands at 50x50,
   while a wide client wordmark keeps its aspect instead of shrinking to
   fit a square. */
.logo-chip {
  display: flex;
  min-width: 50px;
  height: 50px;
  flex: none;
  align-items: center;
  justify-content: center;
  padding: 8px 10px;
  border-radius: var(--r-sm);
  background: #fff;
}
.logo-chip img { height: 100%; width: auto; object-fit: contain; }

/* Light-band variants of the two above. Scoped off .section-light, the
   way .cmp, .quote and .tech-tab are, so the markup adds no class of
   its own and the sixty-odd dark ratings bands cannot be reached.

   The chip inverts rather than staying white: on a light band the card
   is white, and a white chip on a white card has no edge at all. */
.section-light .link-card {
  border-color: var(--line-light);
  background: #fff;
}
.section-light .link-card:hover {
  border-color: rgba(18, 20, 29, .16);
  background: #fff;
}
.section-light .logo-chip { background: #F7F7F5; }

/* Insight card: .card--dark with a bleed image across the top. */
.post-card { overflow: hidden; }
.post-card__media { display: block; overflow: hidden; }
.post-card__media img {
  display: block;
  width: 100%;
  aspect-ratio: 750 / 421;
  object-fit: cover;
  transition: transform .5s ease;
}
.post-card:hover .post-card__media img { transform: scale(1.04); }
.post-card__kicker {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: #80C241;
}
.post-card__link { transition: color .2s ease; }
.post-card__link:hover,
.post-card__link:focus-visible { color: #80C241; }
.post-card__name { font-size: var(--fs-label); font-weight: 500; color: #fff; }
.post-card__date { font-size: var(--fs-meta); color: rgba(255, 255, 255, .45); }

/* SPHINX pillars. The display glyph uses limeMark and the numeral
   amberInk — the dark-theme lime and amber are unreadable on white. */
.pillar-letter {
  display: block;
  font-size: 64px;
  font-weight: 700;
  line-height: 1;
  color: #5F9530;
}
@media (min-width: 1024px) { .pillar-letter { font-size: 72px; } }

.pillar-rule {
  display: block;
  margin-top: 24px;
  width: 1px;
  height: 24px;
  background: rgba(78, 125, 34, .3);
}
.pillar-num {
  margin-top: 20px;
  font-family: Outfit, system-ui, sans-serif;
  font-size: 12px;
  letter-spacing: 1.5px;
  color: #A66A14;
}
.pillar-desc { font-size: var(--fs-meta); line-height: 21px; color: #64748B; }

/* ══════════════════════════════════════════════════════════════════
   PINNED SCROLL SEQUENCE
   Copy on the left, its visual on the right, advancing one step at a
   time while the section stays pinned.

   The DEFAULT here is a plain stacked list — every step readable with
   no JavaScript at all. The script adds .is-pinned only on large
   screens with motion allowed, and only that class switches on the
   sticky stage. A stalled script therefore leaves a long readable
   list, never a blank screen or a scroll trap.
   ══════════════════════════════════════════════════════════════════ */
/* Section backdrop. Same treatment as the plates, at section scale:
   the accent glow sits high-right and the lime low-left, over a base
   that is a touch lighter than the page black.

   --pin-rgb is declared here as well as on the steps so this layer
   resolves; each .pin__step then overrides it for its own plate. */
.pin {
  --pin-rgb: 13, 110, 253;
  background:
    radial-gradient(72% 68% at 70% 18%, rgba(var(--pin-rgb), .30) 0%, rgba(var(--pin-rgb), 0) 70%),
    radial-gradient(60% 60% at 12% 92%, rgba(128, 194, 65, .16) 0%, rgba(128, 194, 65, 0) 72%),
    #0B0D12;
}

/* One accent per step, so consecutive plates never read as identical.
   Same triples as the .uc-icon--* set on the home page. */
.pin__step { --pin-rgb: 13, 110, 253; }
.pin__step--teal   { --pin-rgb:   0, 229, 204; }
.pin__step--purple { --pin-rgb: 192, 132, 252; }
.pin__step--sky    { --pin-rgb:  56, 189, 248; }
.pin__step--orange { --pin-rgb: 251, 146,  60; }
.pin__step--green  { --pin-rgb:  52, 211, 153; }
.pin__step--pink   { --pin-rgb: 244, 114, 182; }
.pin__step--blue   { --pin-rgb:  96, 165, 250; }
.pin__step--amber  { --pin-rgb: 251, 191,  36; }

.pin__step { display: grid; gap: 28px; }
.pin__step + .pin__step { margin-top: 72px; }
@media (min-width: 1024px) {
  .pin__step { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: center; gap: 72px; }
}

.pin__num {
  font-family: Outfit, system-ui, sans-serif;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 2.5px;
  color: #80C241;
}
.pin__title {
  margin-top: 14px;
  font-size: 28px;
  font-weight: 500;
  line-height: 1.12;
  letter-spacing: -0.8px;
  text-transform: capitalize;
}
/* capitalize acts on the first letter of every word, so a brand that
   starts lowercase comes out as IOS, IPad, IPhone or ECommerce. The
   seven titles carrying one wrap it in this, which opts that word
   back out. Needed anywhere a title word keeps its own casing. */
.pin__title .tt-exact { text-transform: none; }
@media (min-width:  768px) { .pin__title { font-size: 36px; letter-spacing: -1.1px; } }
@media (min-width: 1024px) { .pin__title { font-size: 56px; letter-spacing: -1.9px; } }

.pin__desc {
  margin-top: 18px;
  max-width: 520px;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: rgba(255, 255, 255, .58);
}

/* The visual. Falls back to a branded plate built from the step's own
   icon when no photograph has been supplied for it yet. */
.pin__frame {
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 4 / 3;
  margin: 0;
  border-radius: var(--r-panel);
  border: 1px solid var(--line-dark);
  background:
    radial-gradient(72% 68% at 70% 18%, rgba(var(--pin-rgb), .30) 0%, rgba(var(--pin-rgb), 0) 70%),
    radial-gradient(60% 60% at 12% 92%, rgba(128, 194, 65, .16) 0%, rgba(128, 194, 65, 0) 72%),
    #0B0D12;
}
.pin__frame img { width: 100%; height: 100%; object-fit: cover; }

/* Which edge survives the crop. The frame is 4:3 and the photo covers
   it, so a square or portrait image loses its top and bottom to a
   centred crop -- which is how the RPA page's 1:1 art was losing the
   caption baked along one edge. Set $cap_focus on the page rather than
   reaching for these class names directly. */
.pin__frame--focus-top img    { object-position: top; }
.pin__frame--focus-bottom img { object-position: top; }

/* The inline illustrations. Inset rather than bled to the edge, so the
   plate's gradient still reads as a frame around them. */
.ai-art {
  width: 84%;
  height: auto;
  max-height: 78%;
  overflow: visible;
}

.pin__glyph {
  display: flex;
  width: 116px;
  height: 116px;
  align-items: center;
  justify-content: center;
  border-radius: 28px;
  border: 1px solid rgba(var(--pin-rgb), .38);
  background: rgba(var(--pin-rgb), .12);
  backdrop-filter: blur(2px);
}
/* Sprite icons are stroked with currentColor, so the accent is set here
   rather than filtered onto a bitmap. */
.pin__glyph svg { width: 52px; height: 52px; color: #fff; }
.pin__glyph img { width: 52px; height: 52px; object-fit: contain; filter: brightness(0) invert(1); }

/* Watermark numeral, so each plate is distinguishable at a glance. */
.pin__frame::after {
  content: attr(data-num);
  position: absolute;
  right: 22px;
  bottom: 6px;
  font-family: Outfit, system-ui, sans-serif;
  font-size: 108px;
  font-weight: 700;
  line-height: 1;
  color: rgba(255, 255, 255, .05);
}

/* ── Pinned state ─────────────────────────────────────────────────
   Only added by script. --pin-count is set there too, from the number
   of steps actually in the DOM. */
.pin.is-pinned .pin__inner { height: calc(var(--pin-count) * 100vh); }
.pin.is-pinned .pin__stage {
  position: sticky;
  /* Clears the fixed header, and loses that height from the stage so
     the content stays centred in what is actually visible. */
  top: var(--header-h);
  display: flex;
  height: calc(100vh - var(--header-h));
  align-items: center;
}

/* Fill the pinned viewport. A 4:3 plate at column width is only ~47% of
   a 900px viewport, which strands ~240px of dead space above and below
   the content. Pinned, the plate is sized from the viewport instead and
   the media column takes a slightly larger share, so the pair sits at
   roughly three-quarters height with even margins. */
.pin.is-pinned .pin__step {
  grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr);
}
.pin.is-pinned .pin__frame {
  aspect-ratio: auto;
  /* The cap keeps the plate from going extreme-portrait on very tall
     screens; the vh term does the work everywhere else. */
  height: clamp(340px, 55vh, 780px);
}

/* The heading has scrolled away by the time the stage pins, so the gap
   it needed is dead weight — halve it, and trim the section's own
   padding to match. */
.pin.is-pinned .pin__inner { margin-top: 32px; }
.pin.is-pinned { padding-top: 72px; padding-bottom: 72px; }
.pin.is-pinned .pin__steps {
  position: relative;
  width: 100%;
  /* Every child is absolutely positioned, so this would otherwise
     collapse to zero height and the steps would have nothing to
     fill. */
  height: 100%;
}
.pin.is-pinned .pin__step {
  position: absolute;
  inset: 0;
  margin: 0;
  align-content: center;
  opacity: 0;
  visibility: hidden;
  transition: opacity .45s ease, transform .45s ease;
}
.pin.is-pinned .pin__step > * { transform: translateY(14px); transition: transform .45s ease; }
.pin.is-pinned .pin__step.is-active { opacity: 1; visibility: visible; }
.pin.is-pinned .pin__step.is-active > * { transform: none; }

/* Progress rail: one tick per step, filled up to the active one. */
.pin__rail { display: none; }
.pin.is-pinned .pin__rail {
  display: flex;
  gap: 8px;
  margin-top: 40px;
}
.pin__tick {
  height: 3px;
  width: 30px;
  border-radius: 2px;
  background: rgba(255, 255, 255, .14);
  transition: background-color .3s ease;
}
.pin__tick.is-done { background: #80C241; }

@media (prefers-reduced-motion: reduce) {
  .pin.is-pinned .pin__step,
  .pin.is-pinned .pin__step > *,
  .pin__tick { transition: none; }
}

/* ══════════════════════════════════════════════════════════════════
   PROCESS STEPS
   A left rail carrying the step label, and a content column with the
   "what we do" narrative over a two-column "what you get" list.
   Rules between steps rather than boxes, so seven of them in a row
   read as one sequence instead of seven cards.
   ══════════════════════════════════════════════════════════════════ */
/* Section backdrop: the accent glow high-right, lime low-left, over a
   base a shade lighter than the page black. The step cards sitting on
   top stay flat #000, so they read as solid panels against it. */
#how-we-work {
  background:
    radial-gradient(72% 52% at 78% 10%, rgba( 13, 110, 253, .28) 0%, rgba( 13, 110, 253, 0) 70%),
    radial-gradient(62% 48% at  8% 88%, rgba(128, 194,  65, .16) 0%, rgba(128, 194, 65, 0) 72%),
    #0B0D12;
}

/* Requested gap between this section's standfirst and the first card.
   Overrides the site-wide .section-body rhythm for this section only. */
#how-we-work .section-body { margin-top: 70px; }

/* Same again on the Flutter process, at the requested 40px. */
#process .section-body { margin-top: 40px; }

/* Requested heading size, desktop only. Left scoped to this section
   rather than changing .section-title site-wide; the smaller steps of
   the responsive scale still apply below 1024px, or the heading would
   overflow on a phone. */
@media (min-width: 1024px) {
  #how-we-work .section-title { font-size: 55px; letter-spacing: -1.9px; }
}

/* "How To Reduce Mobile Application Cost?" is 37 characters and breaks
   over two lines against .section-title's 820px measure. The measure
   exists to stop long headings running the full 1240px container; this
   one is a single question and reads better unbroken, so it is allowed
   the width it needs.

   nowrap only from xl. Below that the container itself is too narrow,
   and forcing one line would push the heading out of the page rather
   than fix anything — the responsive type scale handles those sizes. */
#reduce-cost .section-head > .section-title { max-width: none; }
@media (min-width: 1280px) {
  #reduce-cost .section-head > .section-title { white-space: nowrap; }
}

/* The list reset is declared here rather than left to Tailwind's
   preflight: this is an <ol>, so without it the browser numbering
   would render alongside the "Step N" labels. */
.proc {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Each step is a card that sticks and is then covered by the next one,
   leaving a sliver of its top edge showing — the same mechanism as the
   case-study stack (.stack__item supplies the sticky offset; the skin
   is here).

   Frosted glass, but deliberately a heavy frost: these cards overlap,
   so a light tint would leave the text of the card underneath legible
   through the one on top. 78% base + a 26px blur reduces whatever is
   behind to an indistinct wash while still reading as glass. The inset
   highlight is the lit top edge. */
.proc__item {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 20px;
  padding: 28px;
  border-radius: var(--r-panel);
  border: 1px solid rgba(255, 255, 255, .11);

  /* Flat black, no gradient and no frost. Being fully opaque also
     removes the reason the glass version needed a heavy blur: the card
     underneath simply cannot show through. Sitting a shade below the
     section's #08090d is what separates the stacked cards, together
     with the hairline and the shadow they cast upward. */
  background: #000;
  box-shadow: 0 -8px 40px -18px rgba(0, 0, 0, .95);
}
.proc__item + .proc__item { margin-top: 26px; }
@media (min-width: 1024px) {
  .proc__item { grid-template-columns: 190px minmax(0, 1fr); gap: 48px; padding: 44px 48px; }
}

.proc__step {
  font-family: Outfit, system-ui, sans-serif;
  font-size: 26px;
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: .5px;
  color: #80C241;
}
@media (min-width: 1024px) { .proc__step { font-size: 30px; } }

/* 23px at every width. It used to step up to 28px at lg, which put the
   stage title close to a section heading and drew the eye past the
   copy under it &mdash; on a rail where the step is the unit, not the
   title. One size keeps the six cards reading as a list. */
.proc__title {
  font-size: 23px;
  font-weight: 600;
  line-height: 1.24;
  letter-spacing: -0.4px;
}

.proc__do {
  margin-top: 16px;
  max-width: 780px;
  font-size: var(--fs-card-desc);
  line-height: var(--lh-card-desc);
  color: rgba(255, 255, 255, .58);
}
/* The "What We Do:" / "What You Get:" lead-ins, one rule for both. */
.proc__lead {
  font-weight: 600;
  color: rgba(255, 255, 255, .92);
}

.proc__gets-label {
  margin-top: 26px;
  font-size: var(--fs-label);
  font-weight: 600;
  color: rgba(255, 255, 255, .92);
}
.proc__gets {
  display: grid;
  gap: 12px 40px;
  margin: 14px 0 0;
  padding: 0;
  list-style: none;
}
@media (min-width: 768px) { .proc__gets { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.proc__gets li {
  position: relative;
  padding-left: 28px;
  font-size: var(--fs-card-desc);
  line-height: 23px;
  color: var(--badge-glow);
}
/* Tick drawn in CSS: it is decorative, so it stays out of the markup
   and out of the accessibility tree. */
.proc__gets li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 3px;
  width: 17px;
  height: 17px;
  border-radius: 50%;
  background-color: rgba(128, 194, 65, .14);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%2380C241' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m2.5 6.2 2.4 2.4 4.6-5'/%3E%3C/svg%3E");
  background-size: 11px 11px;
  background-position: center;
  background-repeat: no-repeat;
  box-shadow: inset 0 0 0 1px rgba(128, 194, 65, .32);
}

/* The same ticked list on a white card. Two things have to change: the
   ink is a pale glow drawn for the dark band and disappears on white,
   and the two-column grid is too tight once it sits inside a card
   rather than across the full process rail. The tick itself steps down
   to limeInk, since the brand lime fails contrast on white. */
.card .proc__gets { grid-template-columns: minmax(0, 1fr); gap: 10px; }
.card--light .proc__gets li { color: #64748B; }
.card--light .proc__gets li::before {
  background-color: rgba(78, 125, 34, .10);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%234E7D22' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m2.5 6.2 2.4 2.4 4.6-5'/%3E%3C/svg%3E");
  box-shadow: inset 0 0 0 1px rgba(78, 125, 34, .24);
}

/* ══════════════════════════════════════════════════════════════════
   COMPARISON TABLE
   Real tabular data, so it stays a <table> — screen readers announce
   the row and column headers, which a grid of divs cannot do. Below
   768px it restacks into one card per row, each cell labelled from
   its data-col attribute, because a three-column table on a phone is
   unreadable either way you slice it.
   ══════════════════════════════════════════════════════════════════ */

/* Header block. Full width rather than two columns: side by side, the
   heading only got 588px and broke across three lines at 52px. */
.cmp__head { display: block; }
.cmp__lede {
  margin-top: 20px;
  /* Capped for readability — across the full 1192px container a 16px
     line runs to ~140 characters, roughly double a comfortable measure. */
  max-width: 920px;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: rgba(255, 255, 255, .58);
}

/* A wide table scrolls inside its own box rather than pushing the
   page sideways. */
.cmp-wrap { overflow-x: auto; }

.cmp {
  width: 100%;
  min-width: 720px;
  border-collapse: collapse;
  text-align: left;
}
.cmp th,
.cmp td {
  padding: 22px 24px;
  vertical-align: top;
  border-top: 1px solid var(--line-dark);
}
.cmp thead th {
  border-top: 0;
  padding-bottom: 18px;
  font-size: var(--fs-label);
  font-weight: 600;
  letter-spacing: .2px;
  color: #fff;
}
/* The two sides get their own accent so the eye can track a column. */
.cmp thead th:nth-child(2) { color: #80C241; }
.cmp thead th:nth-child(3) { color: #6BA6FF; }

.cmp tbody th {
  width: 24%;
  font-size: var(--fs-card-desc);
  font-weight: 600;
  color: #fff;
}
.cmp tbody td {
  font-size: var(--fs-card-desc);
  line-height: 23px;
  color: rgba(255, 255, 255, .60);
}
.cmp tbody tr:hover td,
.cmp tbody tr:hover th { background: rgba(255, 255, 255, .025); }

/* Provenance line under the table. */
.cmp__meta {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 24px;
  justify-content: space-between;
  margin-top: 22px;
  font-size: var(--fs-meta);
  color: rgba(255, 255, 255, .40);
}

/* Source citations under a comparison table. Underlined rather than
   coloured: the meta row sits at 40% white, and a link that only
   differs by hue would not read as a link at that opacity. */
.cmp__src {
  color: rgba(255, 255, 255, .62);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: rgba(255, 255, 255, .3);
  transition: color .2s, text-decoration-color .2s;
}
.cmp__src:hover,
.cmp__src:focus-visible { color: #fff; text-decoration-color: #80C241; }

@media (max-width: 767px) {
  .cmp-wrap { overflow-x: visible; }
  .cmp { min-width: 0; }
  .cmp thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
  .cmp, .cmp tbody, .cmp tr, .cmp th, .cmp td { display: block; width: auto; }
  .cmp tr {
    margin-bottom: 14px;
    border: 1px solid var(--line-dark);
    border-radius: var(--r-card);
    background: var(--card-dark-bg);
  }
  .cmp th, .cmp td { border-top: 0; padding: 16px 18px; }
  .cmp tbody th {
    width: auto;
    border-bottom: 1px solid var(--line-dark);
    font-size: var(--fs-card-title);
  }
  /* The column header the cell belongs to, since thead is hidden. */
  .cmp td::before {
    content: attr(data-col);
    display: block;
    margin-bottom: 6px;
    font-size: 11.5px;
    font-weight: 600;
    letter-spacing: 1.2px;
    text-transform: uppercase;
  }
  .cmp td:nth-of-type(1)::before { color: #80C241; }
  .cmp td:nth-of-type(2)::before { color: #6BA6FF; }
}

/* ── .cmp on a light surface ──────────────────────────────────────
   Every comparison table on the site sat on #08090d, so .cmp above is
   written dark. The Android cost table sits on #F7F7F5. Scoping off
   .section-light, the way .card-desc and .rec__title already do, means
   the markup adds no class of its own and the 16 dark tables cannot be
   reached by these rules.

   Only colour is restated; every dimension, weight and breakpoint
   above still applies. The accents have to move: the brand lime and
   the #6BA6FF blue both fail contrast on a light plate.          */
.section-light .cmp th,
.section-light .cmp td { border-top-color: var(--line-light); }
.section-light .cmp thead th { color: #12141D; }
.section-light .cmp thead th:nth-child(2) { color: #4E7D22; }  /* limeInk */
.section-light .cmp thead th:nth-child(3) { color: #2563EB; }  /* blue-600 */
.section-light .cmp tbody th { color: #12141D; }
.section-light .cmp tbody td { color: #64748B; }
.section-light .cmp tbody tr:hover td,
.section-light .cmp tbody tr:hover th { background: rgba(18, 20, 29, .025); }

.section-light .cmp__lede { color: #64748B; }
.section-light .cmp__meta { color: rgba(18, 20, 29, .45); }
.section-light .cmp__src {
  color: rgba(18, 20, 29, .66);
  text-decoration-color: rgba(18, 20, 29, .28);
}
.section-light .cmp__src:hover,
.section-light .cmp__src:focus-visible { color: #12141D; text-decoration-color: #4E7D22; }

@media (max-width: 767px) {
  .section-light .cmp tr {
    border-color: var(--line-light);
    background: #fff;
  }
  .section-light .cmp tbody th { border-bottom-color: var(--line-light); }
  .section-light .cmp td:nth-of-type(1)::before { color: #4E7D22; }
  .section-light .cmp td:nth-of-type(2)::before { color: #2563EB; }
}

/* A light .cmp sitting on white rather than on #F7F7F5.

   The rules above give each row a #fff card below 768px, which is right
   on the grey plate they were written for and invisible on a white one.
   Only the mobile fill moves; the desktop table has no row background at
   all, so it needs nothing here. */
@media (max-width: 767px) {
  .section-light .cmp--on-white tr { background: #F7F7F5; }
}

/* Left-aligned eyebrow: the centred default flanks the label with a
   rule on each side, which reads oddly when it starts a column. */
.eyebrow--start { justify-content: flex-start; }
.eyebrow--start::before { display: none; }

/* ══════════════════════════════════════════════════════════════════
   PULL QUOTE
   An attributed expert view, sized between body copy and a heading so
   it reads as commentary rather than as another section title.
   ══════════════════════════════════════════════════════════════════ */
.quote { max-width: 880px; margin-inline: auto; text-align: center; }
.quote__mark {
  display: block;
  width: 44px;
  height: 35px;
  margin: 0 auto;
  /* limeInk, not the dark-theme lime: this sits on a light section. */
  color: #4E7D22;
  opacity: .45;
}
.quote__body {
  margin: 26px 0 0;
  font-size: 20px;
  font-weight: 400;
  line-height: 1.55;
  letter-spacing: -0.2px;
  color: #12141D;
}
@media (min-width: 1024px) { .quote__body { font-size: 25px; line-height: 1.5; letter-spacing: -0.4px; } }

.quote__by {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  margin-top: 32px;
}
.quote__avatar {
  display: block;
  width: 54px;
  height: 54px;
  flex: none;
  overflow: hidden;
  border-radius: 50%;
  border: 2px solid rgba(78, 125, 34, .35);
}
.quote__avatar img { width: 100%; height: 100%; object-fit: cover; }
.quote__name { font-size: var(--fs-body); font-weight: 600; color: #12141D; }
.quote__role { margin-top: 2px; font-size: var(--fs-card-desc); color: #64748B; }

/* Dark-band variant. Scoped off .section--dark rather than a class of
   its own, the way .section-light .cmp is, so the markup in
   include/sections/expert-insight.php adds nothing but the band class.
   Only colour is restated; every size and space above still applies.

   Brand lime is legal on the mark here, where the light variant has to
   use limeInk for the same glyph. */
.section--dark .quote__mark { color: #80C241; }
.section--dark .quote__body { color: #fff; }
.section--dark .quote__name { color: #fff; }
.section--dark .quote__role { color: rgba(255, 255, 255, .60); }
.section--dark .quote__avatar { border-color: rgba(128, 194, 65, .40); }

/* ── Quick recommendation cards ───────────────────────────────────
   The takeaway from the comparison table. Each card's accent matches
   the column it summarises — lime for open source, blue for closed —
   so the eye carries the association down from the table. */
.rec { position: relative; overflow: hidden; }
.rec::before {
  content: '';
  position: absolute;
  inset: 0 0 auto;
  height: 3px;
  background: rgb(var(--rec-rgb));
}
.rec__title {
  font-size: var(--fs-card-title);
  font-weight: 600;
  line-height: var(--lh-card-title);
  letter-spacing: -0.2px;
  color: rgb(var(--rec-rgb));
}
.rec--open   { --rec-rgb: 128, 194,  65; }
.rec--closed { --rec-rgb: 107, 166, 255; }
.rec--hybrid { --rec-rgb: 253, 156,  61; }

/* On a white surface the brand lime, sky and amber sit near 2:1 and
   fail contrast as text, so the title takes the ink variants. The 3px
   rule above it keeps the brand colour — decoration, not text. */
.section-light .rec--open   .rec__title { color: #4E7D22; }
.section-light .rec--closed .rec__title { color: #2563EB; }
.section-light .rec--hybrid .rec__title { color: #A66A14; }

/* ── Cost CTA band ────────────────────────────────────────────────
   Copy left, a visual right. Stacks below lg. */
.cost { display: grid; gap: 40px; align-items: center; }
@media (min-width: 1024px) {
  .cost { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 72px; }
}
.cost__lead { margin-top: 24px; font-size: 19px; font-weight: 500; color: #12141D; }
.cost__body {
  margin-top: 14px;
  max-width: 560px;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: #64748B;
}
.cost__panel {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 5 / 4;
  border-radius: var(--r-panel);
  border: 1px solid var(--line-light);
  background:
    radial-gradient(70% 66% at 72% 16%, rgba( 13, 110, 253, .10) 0%, rgba( 13, 110, 253, 0) 70%),
    radial-gradient(58% 58% at 14% 90%, rgba(128, 194,  65, .12) 0%, rgba(128, 194, 65, 0) 72%),
    #FBFBFA;
}
.cost__panel svg { width: 78%; height: auto; }

/* ── Engagement model cards ───────────────────────────────────────
   Numbered, so the three read as options rather than a sequence. */
.eng__num {
  font-family: Outfit, system-ui, sans-serif;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 2.2px;
  color: rgb(var(--eng-rgb));
}
.eng__title { margin-top: 12px; }
.eng { --eng-rgb: 128, 194, 65; }
.eng--a { --eng-rgb: 128, 194,  65; }
.eng--b { --eng-rgb: 107, 166, 255; }
.eng--c { --eng-rgb: 253, 156,  61; }
.eng--d { --eng-rgb: 167, 139, 250; }
.eng--e { --eng-rgb:  45, 212, 191; }

/* ── Video band ───────────────────────────────────────────────────
   The clip sits in a framed panel rather than bleeding edge to edge,
   so it reads as a piece of content instead of a background. */
.vid {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-panel);
  border: 1px solid var(--line-light);
  background: #0B0D12;
  aspect-ratio: 16 / 9;
}
/* Held to 60% of the container from lg up — 40% narrower than full
   bleed. Left full width below that, where 60% of a phone screen would
   leave the clip too small to read. */
@media (min-width: 1024px) {
  .vid { width: 60%; margin-inline: auto; }
}
.vid video { display: block; width: 100%; height: 100%; object-fit: cover; }
.vid__scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(to top, rgba(8, 9, 13, .55), rgba(8, 9, 13, 0) 55%);
}

/* ── Video facade ─────────────────────────────────────────────────
   The poster stands in for the YouTube player until it is clicked.
   It is an anchor rather than a button so the band still goes
   somewhere with no JavaScript; the script upgrades it in place. */
.vid__play {
  position: absolute;
  inset: 0;
  display: block;
  cursor: pointer;
}
.vid__play img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform .5s ease, filter .3s ease;
}
.vid__play:hover img,
.vid__play:focus-visible img { transform: scale(1.03); filter: brightness(.92); }

.vid__badge {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 84px;
  transform: translate(-50%, -50%);
  filter: drop-shadow(0 6px 20px rgba(0, 0, 0, .35));
}
.vid__badge svg { display: block; width: 100%; height: auto; }
.vid__badge-plate { fill: rgba(18, 20, 29, .78); transition: fill .25s ease; }
.vid__badge-arrow { fill: #fff; }
.vid__play:hover .vid__badge-plate,
.vid__play:focus-visible .vid__badge-plate { fill: #FF0000; }

/* Names the film for screen readers and for anyone who lands on the
   band with images off; the badge itself is decorative. */
.vid__label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.vid__frame { display: block; width: 100%; height: 100%; border: 0; }

@media (prefers-reduced-motion: reduce) {
  .vid__play img,
  .vid__badge-plate { transition: none; }
  .vid__play:hover img,
  .vid__play:focus-visible img { transform: none; }
}

/* Sector tile without a photograph yet: a branded plate carrying the
   name, so a missing image reads as deliberate rather than broken. */
.sector--blank {
  background:
    radial-gradient(80% 70% at 70% 18%, rgba( 13, 110, 253, .30) 0%, rgba( 13, 110, 253, 0) 70%),
    radial-gradient(70% 65% at 12% 92%, rgba(128, 194,  65, .22) 0%, rgba(128, 194, 65, 0) 72%),
    #0F1319;
}

/* ══════════════════════════════════════════════════════════════════
   WHY CHOOSE US
   Stat-led. The figure carries the section, so it is set oversized in
   Outfit and painted with a gradient; the copy sits under it. Two
   columns separated by hairlines rather than boxed into cards, so six
   of them read as one wall of evidence.
   ══════════════════════════════════════════════════════════════════ */
.why { display: grid; gap: 0; }
@media (min-width: 900px) { .why { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 72px; } }

.why__item { position: relative; padding: 38px 0; border-top: 1px solid var(--line-light); }
/* The first row on each column needs no rule above it. */
.why__item:first-child { border-top: 0; padding-top: 6px; }
@media (min-width: 900px) {
  .why__item:nth-child(2) { border-top: 0; padding-top: 6px; }
}

/* The figure. background-clip:text needs a guard — without the clip the
   fill would be transparent and the number would vanish entirely. */
.why__fig {
  display: block;
  font-family: Outfit, system-ui, sans-serif;
  font-size: 46px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -1.4px;
  color: #4E7D22;
}
@media (min-width: 1024px) { .why__fig { font-size: 58px; letter-spacing: -2px; } }
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .why__fig {
    background-image: linear-gradient(135deg, #4E7D22 0%, #5F9530 45%, #12141D 100%);
    -webkit-background-clip: text;
            background-clip: text;
    color: transparent;
    -webkit-text-fill-color: transparent;
  }
}

/* Rule under the figure, widening on hover. */
.why__rule {
  display: block;
  width: 46px;
  height: 3px;
  margin-top: 16px;
  border-radius: 2px;
  background: #80C241;
  transition: width .35s cubic-bezier(.22,.68,.3,1);
}
.why__item:hover .why__rule { width: 96px; }

.why__title {
  margin-top: 18px;
  font-size: var(--fs-card-title);
  font-weight: 600;
  line-height: var(--lh-card-title);
  letter-spacing: -0.2px;
  color: #12141D;
}
.why__desc {
  margin-top: 10px;
  max-width: 520px;
  font-size: var(--fs-card-desc);
  line-height: var(--lh-card-desc);
  color: #64748B;
}

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

/* Process rail on a light section: same structure, inverted ink. The
   cards do not stack here, so they can stay flat white. */
.proc--light .proc__item {
  border-color: var(--line-light);
  /* Opaque, like the dark version — the cards stack, so anything
     translucent would show the one underneath through it. */
  background: #fff;
  /* Flat: the hairline alone separates the card from #F7F7F5. */
  box-shadow: none;
}
.proc--light .proc__step  { color: #4E7D22; }
.proc--light .proc__title { color: #12141D; }
.proc--light .proc__do    { color: #64748B; }

/* ── Badged rail ──────────────────────────────────────────────────
   A solid "Step 1" pill beside an icon tile, instead of the plain lime
   numeral. Sits in the same 190px rail column, so nothing about the
   card grid changes; on narrow screens the two just sit above the copy.

   Declared dark-first, matching the rest of the .proc base, with the
   light section overriding the two colours below. On black the pill can
   take the brand lime with near-black text; the reverse (white on lime)
   is the pairing that fails, and that is what .proc--light fixes. */
.proc--badged .proc__rail {
  display: flex;
  align-items: center;
  gap: 12px;
}
.proc--badged .proc__step {
  margin: 0;
  padding: 9px 18px;
  border-radius: 999px;
  background: #80C241;
  font-size: var(--fs-label);
  font-weight: 700;
  line-height: 1;
  letter-spacing: 1.1px;
  text-transform: uppercase;
  color: #08090D;
}
@media (min-width: 1024px) { .proc--badged .proc__step { font-size: var(--fs-label); } }

/* Rimmed tint on black, the same treatment as .uc-icon. */
.proc--badged .proc__ico {
  display: flex;
  width: 42px;
  height: 42px;
  flex: none;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-sm);
  border: 1px solid rgba(128, 194, 65, .38);
  background: rgba(128, 194, 65, .14);
  color: #80C241;
}

/* On white the pill needs limeInk: white on #80C241 is about 2:1,
   while #4E7D22 clears 5:1. The tile becomes a raised white chip
   rather than a tint — the card underneath is already white, so a .14
   wash would all but vanish. */
.proc--light.proc--badged .proc__step { background: #4E7D22; color: #fff; }
.proc--light.proc--badged .proc__ico {
  border-color: var(--line-light);
  background: #fff;
  box-shadow: 0 4px 14px -6px rgba(3, 30, 73, .28);
  color: #4E7D22;
}

/* ── Feature card icon tile (white sections) ──────────────────────
   The dark-section .uc-icon carries a visible rimmed border because it
   sits on near-black. On white that reads as a box, so this variant
   drops the rim and leans on a soft tint of the same hue. Brand hues
   only: the raw lime/amber fail contrast on white, so the -Ink values
   are used for the glyph. */
.feat-icon {
  display: flex;
  width: 56px;
  height: 56px;
  flex: none;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-card);
  background: rgba(var(--feat-rgb), .10);
  color: rgb(var(--feat-rgb));
}
/* ── Tick list ─────────────────────────────────────────────────────
   The bulleted proof list that runs inside cards on the service pages.
   The tick is a CSS background rather than markup: it is decorative and
   belongs out of the accessibility tree, and drawing it once here beats
   an <img> per bullet on a page carrying eighty of them.

   Two surfaces. The default is the white one, where the mark has to be
   limeInk — brand lime sits at ~2:1 on white. .ticks--dark swaps in the
   dark-theme lime and lifts the text off the card. */
.ticks {
  display: grid;
  gap: 10px 32px;   /* column gap only bites when the markup adds columns */
  margin: 0;
  padding: 0;
  list-style: none;
}
.ticks li {
  position: relative;
  padding-left: 26px;
  font-size: var(--fs-card-desc);
  line-height: 23px;
  color: #64748B;
}
.ticks li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 3px;
  width: 17px;
  height: 17px;
  border-radius: 50%;
  background-color: rgba(78, 125, 34, .12);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%234E7D22' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m2.5 6.2 2.4 2.4 4.6-5'/%3E%3C/svg%3E");
  background-size: 11px 11px;
  background-position: center;
  background-repeat: no-repeat;
}
.ticks--dark li { color: rgba(255, 255, 255, .62); }
.ticks--dark li::before {
  background-color: rgba(128, 194, 65, .14);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%2380C241' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m2.5 6.2 2.4 2.4 4.6-5'/%3E%3C/svg%3E");
}

/* ── Long-form guide ───────────────────────────────────────────────
   A sticky chapter list beside one visible chapter. Vertical ARIA tabs
   driven by the shared [data-tabs] script; everything below is skin.

   The whole component lives on a white band, so every accent is the
   light-surface pair — limeInk and amberInk, never the dark-theme lime
   and amber, which sit at ~2:1 here.                                 */
.guide { display: grid; gap: 40px; }
@media (min-width: 1024px) {
  .guide {
    grid-template-columns: minmax(0, 0.62fr) minmax(0, 1.38fr);
    gap: 56px;
    align-items: start;
  }
  /* Sticky only where there is a second column to sit beside. On a
     phone the rail would occupy most of the screen. */
  .guide__nav { position: sticky; top: 120px; }
}

.guide__navlabel {
  font-size: var(--fs-meta);
  font-weight: 600;
  letter-spacing: 2.2px;
  text-transform: uppercase;
  color: rgba(18, 20, 29, .45);
}
.guide__tabs { display: grid; gap: 4px; margin-top: 22px; }

/* Resting state is deliberately flat — no border, no fill. Five bordered
   cards in a column read as five things to do; the selected one being
   the only object on the rail is what makes it the selected one. */
.guide__tab {
  display: grid;
  gap: 8px;
  padding: 16px 18px;
  border-radius: var(--r-card);
  text-align: left;
  background: transparent;
  transition: background-color .2s ease;
}
.guide__tab:hover { background: rgba(18, 20, 29, .04); }
.guide__tab:focus-visible {
  outline: 2px solid #4E7D22;
  outline-offset: 2px;
}
.guide__tab-kicker {
  font-size: var(--fs-meta);
  font-weight: 600;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: #4E7D22;
}
.guide__tab-title {
  font-size: var(--fs-card-desc);
  line-height: 22px;
  color: rgba(18, 20, 29, .78);
}
.guide__tab[aria-current] {
  background: #12141D;
}
/* Brand lime, not amberInk: the selected card is the one dark surface in
   this section, so the dark-theme lime is legal on it where it would
   fail on the white band around it. */
.guide__tab[aria-current] .guide__tab-kicker { color: #80C241; }
.guide__tab[aria-current] .guide__tab-title  { color: #fff; font-weight: 500; }

/* The aside under the rail. Dashed, because it is an offer rather than
   part of the guide — the broken edge says "this is not a chapter". */
.guide__prompt {
  margin-top: 28px;
  padding: 22px;
  border: 1px dashed rgba(18, 20, 29, .22);
  border-radius: var(--r-card);
}
.guide__prompt-title {
  font-size: var(--fs-card-desc);
  font-weight: 600;
  color: #12141D;
}
.guide__prompt-text {
  margin-top: 8px;
  font-size: var(--fs-card-desc);
  line-height: 22px;
  color: #64748B;
}

.guide__title {
  font-size: 26px;
  font-weight: 600;
  line-height: 34px;
  letter-spacing: -.5px;
  color: #12141D;
}
@media (min-width: 768px) { .guide__title { font-size: 32px; line-height: 42px; } }
.guide__intro {
  margin-top: 16px;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: #64748B;
}

/* The rail. One dashed vertical line drawn on the <ol>, with a dot per
   item sitting on it — not a border per item, which would break at
   every gap. The list is ordered because these steps are given in an
   order; the numbering is suppressed because the dots carry it. */
.guide__rail {
  position: relative;
  margin: 36px 0 0;
  padding: 4px 0 0 34px;
  list-style: none;
}
.guide__rail::before {
  content: '';
  position: absolute;
  top: 10px;
  bottom: 10px;
  left: 5px;
  width: 1px;
  background: rgba(18, 20, 29, .16);
}
.guide__item { position: relative; padding-bottom: 32px; }
.guide__item + .guide__item {
  padding-top: 32px;
  border-top: 1px solid var(--line-light);
}
.guide__item:last-child { padding-bottom: 0; }

/* Hollow by default, filled on the first item: the dot marks position on
   the rail, and a column of identical filled dots marks nothing.

   limeInk on the ring, brand lime on the fill. The ring is a hairline on
   white and has to hold contrast on its own; the filled dot is a solid
   shape, where the brighter lime reads as the accent it is. */
.guide__dot {
  position: absolute;
  left: -34px;
  top: 6px;
  width: 11px;
  height: 11px;
  border-radius: 50%;
  border: 2px solid #4E7D22;
  background: #fff;
}
.guide__item:first-child .guide__dot { background: #80C241; border-color: #5F9530; }
.guide__item + .guide__item .guide__dot { top: 38px; }

.guide__item-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
}
.guide__item-title {
  font-size: 20px;
  font-weight: 600;
  line-height: 28px;
  letter-spacing: -.3px;
  color: #12141D;
}
.guide__desc {
  margin-top: 12px;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: #64748B;
}

/* Chapters run one after another. The gap has to be large enough that
   the end of one chapter is not mistaken for another item in its rail,
   which is why it is bigger than any spacing inside a chapter.

   scroll-margin so a jump from the rail clears the sticky header — the
   site-wide scroll-padding-top on <html> covers the header, and this
   adds the breathing room a heading wants above it. */
.guide__panel { scroll-margin-top: 24px; }
.guide__panel + .guide__panel { margin-top: 72px; }
@media (min-width: 1024px) { .guide__panel + .guide__panel { margin-top: 88px; } }

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

/* ── Timeline ──────────────────────────────────────────────────────
   The About page's journey band. Structure is the guide rail — the
   line, the items and the dots are .guide__* — and these two classes
   are the only additions: the year, and the elapsed-time chip beside
   it. A second full timeline component would be drift.

   .tl--dark relights the guide rail's parts for #08090d, which they
   are otherwise authored light for. */
.tl__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 10px;
  margin-bottom: 8px;
}
.tl__year {
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -.3px;
  font-variant-numeric: tabular-nums;
  color: #4E7D22;
}
.tl__age {
  padding: 3px 10px;
  border-radius: 9999px;
  border: 1px solid var(--line-light);
  font-size: var(--fs-meta);
  color: #64748B;
}

/* The rail line is a 1px background, not a border — see .guide__rail. */
.tl--dark::before { background: rgba(255, 255, 255, .18); }
.tl--dark .guide__item + .guide__item { border-top-color: var(--line-dark); }
.tl--dark .guide__dot { background: #08090d; border-color: #80C241; }
.tl--dark .guide__item:first-child .guide__dot { background: #80C241; }
.tl--dark .tl__year { color: #80C241; }
.tl--dark .tl__age {
  border-color: var(--line-dark);
  color: rgba(255, 255, 255, .55);
}
.tl--dark .guide__item-title { color: #fff; }
.tl--dark .guide__desc { color: rgba(255, 255, 255, .62); }

/* ── Hero with a right-hand column ─────────────────────────────────
   Used by service-hero.php when $hero_aside is set.

   The aside is taken OUT OF FLOW, not placed in a second grid column.
   A grid needs the copy wrapped in a column div to hold it together,
   and it cannot be — the motion script staggers `main section > div >
   *`, so every element has to stay a direct child. Left in flow as a
   row-spanning grid item, the aside's 620px is distributed across the
   rows the copy occupies, which prises the breadcrumb, the pill and the
   heading apart down the whole left side.

   Absolute instead: the copy lays out exactly as it does on every other
   hero, and a padding-right reserves the column so long lines cannot
   run under the embed. Centred with flex rather than translateY,
   because .hero-in animates transform and the two would fight.

   Stacked below lg, where a side-by-side split leaves both halves too
   narrow to use. */
/* One variable for the embed height, because two places depend on it:
   the iframe itself, and the min-height that stops an out-of-flow aside
   overflowing a shorter block of copy. Typed twice, they drift.

   769px is the height the Koras widget needs to show its calendar
   without an internal scrollbar. Same value at every width — the
   widget's own layout does not get shorter on a phone. */
.hero-split {
  --hero-embed-h: 769px;
  --hero-aside-w: 540px;
  --hero-aside-gap: 56px;
}
.hero-aside { margin-top: 40px; }

/* Aside is a picture, not the booking embed. The 769px floor exists so
   the iframe has somewhere to sit; an image is happy at its own height,
   and holding the hero open to 769px for one strands ~200px of empty
   black between the hero and the logo strip. The column still centres
   the image against whatever height the copy needs. */
.hero-split--media { --hero-embed-h: 0px; }

/* Proof points on one line beside a picture aside.
   The three points measure ~760px; the left column is only what is
   left after the aside's reserve, which is not enough and breaks them
   over two rows. Three things buy the width back: a narrower aside,
   the row reclaiming the 56px gutter it does not need, and a tighter
   column gap. Only from xl — below that the column is genuinely too
   narrow and wrapping is the right answer, not overflow. */
@media (min-width: 1280px) {
  .hero-split--media { --hero-aside-w: 460px; }
  .hero-split--media .hero-points {
    flex-wrap: nowrap;
    column-gap: 24px;
    max-width: none;
    margin-right: calc(-1 * var(--hero-aside-gap));
  }
  .hero-split--media .hero-point { white-space: nowrap; }
}

/* Anything dropped into the hero's left column under the lead — the
   enquiry form on contact.php. Capped so the form does not stretch to
   the full copy measure and dwarf the heading above it. */
.hero-extra { margin-top: 36px; max-width: 620px; }

/* The address that failed, echoed back on 404.php. A chip rather than
   a run of prose: it is a machine string and the visitor is scanning it
   for the typo, so it wants to look like one. It is rendered into
   .hero-extra, which already sets the space above it.

   overflow-wrap is load-bearing. A 404 URL is arbitrary and can be one
   unbroken 120-character token, which would otherwise push the hero
   wider than the viewport on a phone. */
.nf-url {
  font-size: var(--fs-meta);
  line-height: 1.6;
  color: rgba(255, 255, 255, .5);
}
.nf-url code {
  display: inline-block;
  max-width: 100%;
  padding: 4px 10px;
  border-radius: var(--r-sm);
  border: 1px solid var(--line-dark);
  background: rgba(255, 255, 255, .05);
  color: rgba(255, 255, 255, .82);
  overflow-wrap: anywhere;
}

@media (min-width: 1024px) {
  .hero-split {
    position: relative;
    min-height: var(--hero-embed-h);
    /* Reserved from the same variables the aside is sized with, so the
       column and the space kept clear for it cannot disagree. */
    padding-right: calc(var(--hero-aside-w) + var(--hero-aside-gap));
  }
  .hero-split > .hero-aside {
    position: absolute;
    top: 0;
    bottom: 0;
    right: 0;
    width: var(--hero-aside-w);
    margin-top: 0;
    display: flex;
    align-items: center;
  }
}

/* The booking embed. A white plate behind it because the widget renders
   on white and the hero is near-black — without it the iframe's own
   corners show as hard white squares over the gradient. */
.hero-embed {
  display: block;
  width: 100%;
  height: var(--hero-embed-h, 769px);
  border: 0;
  border-radius: var(--r-panel);
  background: #fff;
  box-shadow: 0 30px 70px -30px rgba(0, 0, 0, .75);
}

/* Office address on the contact page. <address> is italic by browser
   default, which is wrong for a postal address in a card. */
.office-addr {
  font-style: normal;
  font-size: var(--fs-card-desc);
  line-height: 24px;
  color: #64748B;
}

/* ── Office cards (contact page “Find us”) ─────────────────────────
   A tinted plate carries the office mark, then the city, a hairline,
   the address and dial, and a full-width map button pinned to the
   foot. The plate tints are the card's only per-instance value, so
   they are modifier classes rather than an inline custom property. */
.office-card {
  display: flex;
  flex-direction: column;
  padding: 20px;
  border: 1px solid var(--line-light);
  border-radius: var(--r-panel);
  background: #fff;
  box-shadow: 0 1px 2px rgba(18, 20, 29, .04), 0 18px 40px -28px rgba(18, 20, 29, .28);
  transition: box-shadow .25s ease, transform .25s ease;
}
.office-card:hover {
  transform: translateY(-3px);
  box-shadow: 0 1px 2px rgba(18, 20, 29, .05), 0 26px 54px -26px rgba(18, 20, 29, .32);
}

.office-plate {
  display: flex;
  height: 200px;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-card);
  /* Fallback tint; every card carries a modifier that replaces it. */
  background: #F1F2F0;
}
.office-plate--peach  { background: #FBE0D2; }
.office-plate--lilac  { background: #F6DDF7; }
.office-plate--sky    { background: #D6ECFB; }
.office-plate--lime   { background: #E2F1D0; }
.office-plate--sand   { background: #FBEBD1; }

/* The marks are inlined SVG with their white swapped for currentColor
   (see locations.php), so the ink is set here and the glyph sits
   straight on the tint with no plate behind it. */
.office-plate__mark {
  display: flex;
  align-items: center;
  justify-content: center;
  color: #12141D;
}
.office-plate__mark svg {
  width: auto;
  height: 74px;
}

.office-card__title {
  margin-top: 22px;
  font-size: 22px;
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: -.01em;
  color: #12141D;
}
.office-card__rule {
  margin-top: 18px;
  border: 0;
  border-top: 1px solid var(--line-light);
}
.office-card__dial {
  display: inline-block;
  font-size: var(--fs-card-desc);
  line-height: 24px;
  color: #64748B;
  transition: color .2s ease;
}
.office-card__dial:hover,
.office-card__dial:focus-visible { color: #4E7D22; }

/* Full width, and pushed to the foot so the buttons line up across a
   row of cards whose addresses run to different lengths. The arrow
   goes to the right edge rather than trailing the label, so it reads
   as the button's own affordance across a 100%-wide control. */
.office-card__cta {
  margin-top: auto;
  width: 100%;
}
.office-card__cta .btn__arrow { margin-left: auto; }

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

/* Prose column on the About page: the same measure and rhythm the
   case-study story uses, without its dark-band colour scoping. */
.about-body {
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: #64748B;
}
.about-body + .about-body { margin-top: 20px; }
/* Slate grey is a light-surface colour; on the dark bands it sits at
   about 2.5:1 and fails. The dark sections carry their own. */
.section--dark .about-body { color: rgba(255, 255, 255, .68); }

/* A heading that groups cards inside a section, one level below the
   section's own <h2>. Sized between .section-title and .card-title so
   the hierarchy still reads at a glance. */
.group-title {
  font-size: 20px;
  font-weight: 600;
  line-height: 28px;
  letter-spacing: -.2px;
  color: #12141D;
}
.section--dark .group-title { color: #fff; }

/* Big figure and its caption, inside a card. The grid counterpart to
   .why__fig, which is the same idea laid out as a bordered list. */
/* limeInk, not the brand lime. These sit on white cards, where #80C241
   is about 2:1 — under the 3:1 that even large text has to clear, so it
   would fail at any size. limeInk clears 4.9:1 and still reads green. */
.stat-fig {
  font-size: 40px;
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -1.4px;
  font-variant-numeric: tabular-nums;   /* digits must not reflow while counting */
  color: #4E7D22;
}
@media (min-width: 1024px) { .stat-fig { font-size: 48px; } }
.stat-label {
  margin-top: 10px;
  font-size: var(--fs-label);
  line-height: 21px;
  color: #64748B;
}
.section--dark .stat-fig   { color: #fff; }
.section--dark .stat-label { color: rgba(255, 255, 255, .55); }

/* A price range is a much longer string than the two or three
   characters .stat-fig was drawn for ("$90,000 – $150,000+" against
   "97%"), so it gets its own size rather than the 40/48px one. It
   keeps the tabular figures and the weight, so the three cost cards
   still read as one row of numbers. */
.cost-fig {
  font-size: 31px;
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -1px;
  font-variant-numeric: tabular-nums;
  color: #4E7D22;
  /* Two lines' worth, always. The widest range wraps at this size and
     the others do not, and without a floor the card that wraps pushes
     its own title a line below its neighbours'. */
  min-height: 2.3em;
}

.feat-icon--blue  { --feat-rgb:  13, 110, 253; }  /* brand    */
.feat-icon--lime  { --feat-rgb:  78, 125,  34; }  /* limeInk  */
.feat-icon--amber { --feat-rgb: 166, 106,  20; }  /* amberInk */

/* Sector tile caption. Sizes match .card-title / .card-desc so the
   photographic tiles read as the same family as the flat cards. */
.sector__title {
  display: block;
  font-size: var(--fs-card-title);
  font-weight: 600;
  line-height: var(--lh-card-title);
  color: #fff;
}
.sector__desc { font-size: var(--fs-card-desc); line-height: 20px; color: rgba(255, 255, 255, .85); }

/* ── Case studies ─────────────────────────────────────────────────
   One class per study carries its three colours: the wordmark ink, the
   plate showing behind the photo while it loads, and the scrim tint
   under the headline. Same shape as .tech--*: the markup names the
   study, never a hex value. */
.case--vivino { --case-ink: #A11B32; --case-plate: #6B0F1E; --case-scrim: 63,  7, 19; }
.case--energi { --case-ink: #12A550; --case-plate: #152A5C; --case-scrim: 11, 26, 58; }
.case--nsi    { --case-ink: #2E97A3; --case-plate: #4BB3BE; }
.case--wow    { --case-ink: #0E9384; --case-plate: #0B5C54; }
.case--zenyum { --case-ink: #1E88C7; --case-plate: #14496B; }
.case--popmeals { --case-ink: #6D28D9; --case-plate: #3B1580; }
.case--tenx   { --case-ink: #1D4ED8; --case-plate: #10265E; }
.case--ace    { --case-ink: #B45309; --case-plate: #4A2606; }
/* Koras and ProMarketer keep their brand hues but a shade deeper than the
   logo blue/magenta, which sit under 4.5:1 on this white panel. */
.case--koras  { --case-ink: #3259EA; --case-plate: #1B3AA8; }
.case--breathe{ --case-ink: #2F7A34; --case-plate: #17381A; }
/* Amaze Ads is a white laptop mockup on transparency, so its plate is
   light — a dark one swallows the device. */
.case--amaze  { --case-ink: #2457C5; --case-plate: #FFCFD0; }
.case--promkt { --case-ink: #C41F98; --case-plate: #7A1160; }
/* The two RPA studies. Both shots are full-bleed, so the plate only
   shows for the moment before the image paints — it matches the
   dominant colour of each so the swap is invisible. */
.case--etisalat  { --case-ink: #6B2FD6; --case-plate: #7B3FE4; }
.case--telebright{ --case-ink: #C4471A; --case-plate: #121A29; }
.case--amazon    { --case-ink: #B85B00; --case-plate: #232F3E; }

/* The studies the four commerce service pages carry. Those pages set
   their copy beside the photo rather than over it, so no scrim is
   needed: the plate is what shows behind the shot while it loads, and
   behind a contained mockup, and the ink is the wordmark colour on the
   white panel opposite. Each plate is the image's own dominant colour
   darkened, so the swap when the photo paints is invisible. */
.case--yarn-bazaar    { --case-ink: #1F6F84; --case-plate: #12333B; }
.case--pop-meals      { --case-ink: #9A6B04; --case-plate: #4A3406; }
.case--rainman        { --case-ink: #3F6B8A; --case-plate: #1E211E; }
.case--just-breathe   { --case-ink: #2F7A34; --case-plate: #17381A; }
.case--sarwa          { --case-ink: #1E6C8C; --case-plate: #1B2830; }
.case--rekha-metal    { --case-ink: #6D28D9; --case-plate: #260A40; }
.case--jadole-natural { --case-ink: #2E7D45; --case-plate: #1B3020; }
.case--tru-hair       { --case-ink: #8A6A2F; --case-plate: #2A251C; }

.case-plate { background-color: var(--case-plate); }

/* Cut-out mockups — the ones supplied on transparency, with the device
   floating rather than a full-bleed photograph — sit ON the plate
   instead of filling it. object-cover crops their margins away and
   stretches a landscape composition into a portrait frame it was never
   drawn for. */
.case-plate--fit img {
  object-fit: contain;
  padding: 36px;
}
@media (max-width: 1023px) {
  .case-plate--fit img { padding: 24px; }
}
.case-scrim {
  background: linear-gradient(to bottom,
    rgba(var(--case-scrim), .85),
    rgba(var(--case-scrim), .45),
    transparent);
}

/* Case-study panel: the Problem / Challenge / Solution stack. */
.case-brand { font-size: 22px; font-weight: 700; letter-spacing: .5px; color: var(--case-ink); }
.case-step__title { font-size: var(--fs-card-title); font-weight: 600; color: #12141D; }
.case-step__body {
  margin-top: 10px;
  font-size: var(--fs-card-desc);
  line-height: 25px;
  color: #64748B;
}

/* Contact form control. Five inputs previously shared one 200-char string. */
.field-label { display: block; font-size: var(--fs-label); font-weight: 500; color: #fff; }
.field-label .req { color: #80C241; }
.field-input {
  margin-top: 8px;
  width: 100%;
  height: 52px;
  padding: 0 16px;
  border-radius: var(--r-sm);
  border: 1px solid var(--line-dark);
  background: rgba(255, 255, 255, .07);
  font-size: var(--fs-card-desc);
  color: #fff;
  transition: border-color .2s ease, background-color .2s ease;
}
.field-input::placeholder { color: rgba(255, 255, 255, .4); }
.field-input:focus {
  outline: none;
  border-color: #80C241;
  background: rgba(255, 255, 255, .12);
}
textarea.field-input { height: auto; padding: 12px 16px; resize: vertical; }

/* Validation message. Empty in the markup and hidden while it stays
   that way, so the layout does not carry a reserved gap under every
   field for a message most visitors never see. The submit script only
   ever sets textContent. */
.field-error {
  margin-top: 6px;
  font-size: var(--fs-meta);
  line-height: 18px;
  color: #FF8B8B;
}
.field-error:empty { display: none; }
.field-input.is-error { border-color: #FF8B8B; }
.field-input.is-error:focus { border-color: #FF8B8B; }

/* The "how did you hear about us" select. appearance:none because the
   native control paints its own background and arrow, which on Windows
   is a white box on this dark plate. The chevron is a background image
   for the same reason a pseudo-element will not do: <select> cannot
   carry one. */
.field-select {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  padding-right: 42px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1 1.5 6 6.5l5-5' stroke='rgba(255,255,255,.55)' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 16px center;
  cursor: pointer;
}
/* The dropdown list itself is drawn by the OS, which ignores the field
   colours above, so the options are given a light surface of their own
   rather than being left as white-on-white. */
.field-select option { background: #fff; color: #0F172A; }

/* Consent and terms. The box is 20px so it stays a comfortable target
   beside 13px copy, and accent-color tints it lime without replacing
   the native control, which would cost the keyboard behaviour. */
.field-check { display: flex; align-items: flex-start; gap: 12px; }
.field-check input[type="checkbox"] {
  flex: 0 0 auto;
  margin-top: 2px;
  width: 20px;
  height: 20px;
  accent-color: #80C241;
  cursor: pointer;
}
.field-check label {
  font-size: var(--fs-meta);
  line-height: 20px;
  color: rgba(255, 255, 255, .58);
  cursor: pointer;
}
.field-check label strong { color: rgba(255, 255, 255, .78); font-weight: 500; }
.field-check label a { color: #80C241; text-decoration: underline; text-underline-offset: 2px; }
.field-check label a:hover { color: #97D45B; }

/* The reCAPTCHA widget is a fixed 304px iframe. Below that the plate
   would scroll sideways, so it is scaled down instead of clipped: a
   checkbox the visitor cannot reach makes the form unsubmittable. */
.field-captcha { max-width: 100%; }
@media (max-width: 400px) {
  .field-captcha .g-recaptcha {
    transform: scale(.85);
    transform-origin: 0 0;
    height: 66px;
  }
}

.field-note {
  margin-top: 14px;
  text-align: center;
  font-size: var(--fs-meta);
  color: rgba(255, 255, 255, .42);
}

/* ── Phone field ──────────────────────────────────────────────────
   intl-tel-input wraps #txtPhone in a .iti div at runtime and ships its
   own stylesheet (assets/css/intlTelInput.css, vendor, unedited). That
   sheet is built for a light form, so everything below is re-colouring
   for the dark plate. The wrapper is inline-block by default and has to
   be told to fill the column; the 8px gap under the label moves onto it
   at the same time, because the input is no longer the element the
   label sits above. */
.field-tel .iti { display: block; width: 100%; margin-top: 8px; }
.field-tel .iti .field-input { margin-top: 0; }
.field-tel .iti__flag-container:hover .iti__selected-flag,
.field-tel .iti__selected-flag:focus {
  background: rgba(255, 255, 255, .06);
}
.field-tel .iti__arrow { border-top-color: rgba(255, 255, 255, .5); }
.field-tel .iti__arrow--up { border-bottom-color: rgba(255, 255, 255, .5); }
.field-tel .iti__country-list {
  background: #0B0E17;
  border: 1px solid var(--line-dark);
  border-radius: var(--r-sm);
  box-shadow: 0 18px 44px -18px rgba(0, 0, 0, .8);
  color: #fff;
}
.field-tel .iti__country { font-size: var(--fs-meta); }
.field-tel .iti__country.iti__highlight { background: rgba(255, 255, 255, .09); }
.field-tel .iti__dial-code { color: rgba(255, 255, 255, .45); }
.field-tel .iti__divider { border-bottom-color: var(--line-dark); }


/* ── Breadcrumb ───────────────────────────────────────────────────
   Inner-page trail. The separator is a pseudo-element so the markup
   stays a clean <ol> and screen readers never announce the slash. */
.crumbs ol {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--fs-meta);
}
.crumbs li { display: flex; align-items: center; gap: 8px; }
.crumbs li + li::before {
  content: '';
  width: 4px;
  height: 4px;
  flex: none;
  border-radius: 50%;
  background: rgba(255, 255, 255, .3);
}
.crumbs a {
  color: rgba(255, 255, 255, .55);
  transition: color .2s ease;
}
.crumbs a:hover, .crumbs a:focus-visible { color: #80C241; }
.crumbs [aria-current="page"] { color: rgba(255, 255, 255, .9); }

/* Proof points under the inner-page hero CTAs. The tick is decorative,
   so it is drawn in CSS rather than added to the markup. */
.hero-point {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: var(--fs-card-desc);
  color: rgba(255, 255, 255, .72);
}
.hero-point::before {
  content: '';
  width: 18px;
  height: 18px;
  flex: none;
  border-radius: 50%;
  background-color: rgba(128, 194, 65, .16);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%2380C241' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m2.5 6.2 2.4 2.4 4.6-5'/%3E%3C/svg%3E");
  background-size: 12px 12px;
  background-position: center;
  background-repeat: no-repeat;
  box-shadow: inset 0 0 0 1px rgba(128, 194, 65, .35);
}

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

/* Footer column heading — the same uppercase rule in all four columns. */
.footer-head {
  font-size: var(--fs-meta);
  font-weight: 600;
  line-height: 1.5;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: #fff;
}

/* Social icon pill. */
.social-pill {
  display: flex;
  width: 40px;
  height: 40px;
  align-items: center;
  justify-content: center;
  border-radius: 9999px;
  background: rgba(255, 255, 255, .1);
  color: #fff;
  transition: background-color .2s ease, color .2s ease;
}
.social-pill:hover, .social-pill:focus-visible { background: #80C241; color: #12141D; }

/* Footer and nav links: ~40 identical utility strings collapsed to two. */
.link-soft {
  font-size: var(--fs-card-desc);
  color: rgba(255, 255, 255, .75);
  transition: color .2s ease;
}
.link-soft:hover, .link-soft:focus-visible { color: #fff; }

.nav-link {
  font-size: var(--fs-card-desc);
  color: rgba(255, 255, 255, .85);
  transition: color .2s ease;
}
.nav-link:hover, .nav-link:focus-visible { color: #fff; }
/* The page you are on, marked for sighted readers too — aria-current
   alone only reaches assistive tech. */
.nav-link[aria-current="page"] { color: #80C241; }
.link-soft[aria-current="page"] { color: #80C241; }
.mega__link[aria-current="page"] { color: #0D6EFD; font-weight: 500; }

/* Mega-menu rows: the two-column link grids and the compact pinned lists. */
.mega__eyebrow {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 1.3px;
  text-transform: uppercase;
  color: rgba(18, 20, 29, .45);
}
.mega__link {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  font-size: var(--fs-body);
  line-height: 24px;
  color: rgba(18, 20, 29, .85);
  transition: color .2s ease;
}
.mega__link:hover, .mega__link:focus-visible { color: #0D6EFD; }
.mega__link > .ico { margin-top: 1px; flex: none; color: #0D6EFD; }

.mega__row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  border-radius: var(--r-sm);
  font-size: var(--fs-card-desc);
  color: rgba(18, 20, 29, .85);
  transition: background-color .2s ease, color .2s ease;
}
.mega__row:hover, .mega__row:focus-visible { background: #F2F5FB; color: #0D6EFD; }
.mega__row > .ico { flex: none; color: #0D6EFD; }
.mega__row--strong { font-weight: 500; color: #12141D; }
/* Same visible active state .mega__link and .nav-link carry.
   aria-current alone only reaches assistive tech. */
.mega__row[aria-current="page"] { color: #0D6EFD; font-weight: 500; }

@media (prefers-reduced-motion: reduce) {
  .post-card__media img, .link-card, .link-soft,
  .nav-link, .mega__link, .mega__row, .field-input { transition: none; }
}

/* ══════════════════════════════════════════════════════════════════
   PORTFOLIO INDEX
   The grid at /portfolio/all/. Cards are the shared .card--dark, so
   surface, hairline and hover all come from there; only the image
   plate and the filter chips are specific to this page.
   ══════════════════════════════════════════════════════════════════ */

/* The image sits in a fixed-ratio plate so a 989x1100 phone shot and an
   800x600 placeholder occupy the same box and the grid rows stay level.
   Rounded on top only — the card's own radius handles the bottom. */
.pcard__plate {
  position: relative;
  overflow: hidden;
  aspect-ratio: 4 / 3;
  border-radius: calc(var(--r-panel) - 1px) calc(var(--r-panel) - 1px) 0 0;
  background: #101219;
}
.pcard__plate img {
  position: absolute;
  inset: 0;
  height: 100%;
  width: 100%;
  object-fit: cover;
  object-position: center;
  transition: transform .4s ease;
}
.pcard:hover .pcard__plate img { transform: scale(1.04); }

/* The arrow is the affordance, so it only commits colour on hover. */
.pcard__arrow { opacity: .55; transition: opacity .25s ease, transform .25s ease, color .25s ease; }
.pcard:hover .pcard__arrow { opacity: 1; transform: translateX(3px); }

/* Hover and keyboard focus both take the title and arrow to brand lime.
   Safe here because .pcard only ever sits on #08090d — the same lime on
   a white surface would fail contrast, which is why the light sections
   use limeInk instead. Focus is included so the state is not mouse-only. */
.pcard .card-title { transition: color .25s ease; }
.pcard:hover .card-title,
.pcard:focus-visible .card-title,
.pcard:hover .pcard__arrow,
.pcard:focus-visible .pcard__arrow { color: #80C241; }
.pcard:focus-visible .pcard__arrow { opacity: 1; }

/* Keyboard parity: the card is one link, so the ring goes on the card. */
.pcard:focus-visible {
  outline: 2px solid #80C241;
  outline-offset: 3px;
}

/* Filter chips. The dark-surface counterpart to .tech-pill, and
   deliberately the same selected treatment: the .btn--primary lime
   gradient with near-black ink, which clears contrast where white on
   the same lime would not. */
/* Chips are links now that each category has its own URL, so the
   declarations a <button> gave for free are set here: display, and no
   underline. aria-current="page" marks the active one, which is the
   correct attribute for a link that points at the page you are on;
   aria-pressed is a button state and was wrong once these stopped being
   buttons. */
.pchip {
  display: inline-block;
  padding: 11px 20px;
  border: 1px solid var(--line-dark);
  border-radius: 999px;
  background: transparent;
  font-size: var(--fs-label);
  font-weight: 500;
  line-height: 1;
  color: rgba(255, 255, 255, .62);
  text-decoration: none;
  cursor: pointer;
  transition: background-color .2s, color .2s, border-color .2s;
}
.pchip:hover {
  border-color: rgba(255, 255, 255, .22);
  background: rgba(255, 255, 255, .04);
  color: #fff;
}
.pchip[aria-current="page"] {
  border-color: transparent;
  background-image: linear-gradient(to right, #9BD657, #80C241, #5FA22E);
  font-weight: 600;
  color: #12141D;
}
.pchip:focus-visible { outline: 2px solid #80C241; outline-offset: 2px; }

/* Set by the filter script only. Never the default state: with JS off,
   every card stays visible.

   Deliberately NOT .is-hidden. The reveal script in footer.php owns that
   class: it adds it to the children of any `.grid` it observes, and its
   8-second safety-net timeout strips it from everything — which would
   quietly undo the filter. It skips groups larger than nine children, so
   twelve cards happen to miss it today, but trimming the data array
   would walk straight into it. Two systems, two class names. */
.pcard.is-filtered { display: none; }

/* Entrance. The generic [data-reveal] pair fades and lifts 22px, which
   is right for a paragraph and too quiet for a 4:3 image tile — the
   cards were arriving without reading as an arrival. These travel
   further, settle out of a slight shrink so the plate grows into place,
   and run longer. Everything else about the cascade is unchanged: the
   script in footer.php still hides only what is below the fold and still
   deals each row out left to right through --reveal-delay.

   Scoped to .pcard on purpose. Section heads and body copy keep the
   quieter 22px lift; a scale on a line of text just looks unsteady. */
.pcard[data-reveal] {
  transition: opacity .78s cubic-bezier(.22,.68,.3,1),
              transform .78s cubic-bezier(.22,.68,.3,1);
  transition-delay: calc(var(--reveal-delay, 0) * 1ms);
}
.pcard[data-reveal].is-hidden {
  opacity: 0;
  transform: translateY(34px) scale(.955);
}

@media (prefers-reduced-motion: reduce) {
  .pcard__plate img,
  .pcard__arrow,
  .pcard .card-title,
  .pchip { transition: none; }
  .pcard:hover .pcard__plate img { transform: none; }
  .pcard:hover .pcard__arrow { transform: none; }
  /* Restated here because .pcard[data-reveal].is-hidden outranks the
     generic escape hatch up in the motion block. */
  .pcard[data-reveal],
  .pcard[data-reveal].is-hidden {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ══════════════════════════════════════════════════════════════════
   CASE STUDY DETAIL  (/portfolio-item/<slug>/)
   ══════════════════════════════════════════════════════════════════ */

/* Hero meta rail. Small uppercase key, plain value stacked under it. */
/* ⚠ #4CAF50 is not a site colour — the dark-theme green is #80C241. Set
   on request; swap it for the brand lime if the palette wins. It clears
   contrast on #08090d at roughly 7:1, so it is legal, just off-palette. */
.cs-meta__key {
  font-size: var(--fs-meta);
  letter-spacing: .6px;
  text-transform: uppercase;
  color: #4CAF50;
}
.cs-meta__val {
  margin-top: 8px;
  font-size: var(--fs-body);
  line-height: 24px;
  color: #fff;
}
/* Services stacks several values under one key; only the first needs the
   gap to the label. */
.cs-meta__val + .cs-meta__val { margin-top: 4px; }

/* …unless the group is marked inline, where the values run on one line
   separated by a slash. Still several <dd>s under one <dt>, which is
   what they are — the separator is a pseudo-element, so a screen reader
   hears three distinct values rather than one string with slashes in
   it. It also wraps cleanly: at a narrow width the flex row breaks
   between values and the slash stays with the value it precedes. */
.cs-meta--inline {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
}
.cs-meta--inline .cs-meta__key { flex-basis: 100%; }   /* label keeps its own line */
.cs-meta--inline .cs-meta__val { margin-top: 8px; }
.cs-meta--inline .cs-meta__val + .cs-meta__val::before {
  content: '/';
  padding: 0 10px;
  color: rgba(255, 255, 255, .35);
}

/* Gallery strip. A .marquee variant rather than a bespoke scroller, so
   it inherits the seamless -50% loop, the hover pause and the
   reduced-motion fallback from the shared rules above. */
.marquee--shots {
  --marquee-gap: 16px;
  --marquee-speed: 55s;   /* tall tiles read slower than logos */
  --fade-start: 0%;       /* held inside the container, so no edge fade */
  --fade-end: 100%;
  border-radius: var(--r-panel);
}
.marquee__group--shots { align-items: stretch; }
.marquee__group--shots li {
  flex: 0 0 auto;
  width: 340px;
  overflow: hidden;
  border-radius: var(--r-panel);
  background: #101219;
}
/* 4:3, not the 3:4 this started as. The supplied artwork is landscape
   and square; portrait tiles cropped the sides off both. */
.marquee__group--shots img {
  display: block;
  width: 100%;
  height: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}
@media (min-width: 1024px) {
  .marquee--shots { --marquee-gap: 20px; }
  .marquee__group--shots li { width: 440px; }
}

/* A case study with one usable image renders it still rather than
   sliding it past four times over. Same plate, radius and background as
   a marquee tile so the band looks like the same component, but sized
   to the container and left to its own aspect ratio: with nothing
   beside it there is no row to keep square, and cropping a lone image
   to 4:3 would throw away the only artwork the page has. */
.cs-shot {
  overflow: hidden;
  border-radius: var(--r-panel);
  background: #101219;
  max-width: 880px;
  margin-inline: auto;
}
.cs-shot img {
  display: block;
  width: 100%;
  height: auto;
}

/* Story band. */
/* Sized above the body copy it introduces, not level with it. At
   --fs-body these read as bold paragraphs rather than as the three
   headings the section turns on; .group-title's 20px is the step the
   rest of the site uses for a sub-heading inside a section, and this
   takes it plus one at lg. */
.cs-story__head {
  font-size: 22px;
  font-weight: 600;
  line-height: 30px;
  letter-spacing: -.2px;
  color: #12141D;
}
@media (min-width: 1024px) {
  .cs-story__head { font-size: 26px; line-height: 34px; letter-spacing: -.3px; }
}
.cs-story__list {
  margin-top: 16px;
  padding-left: 22px;
  list-style: decimal;
}
.cs-story__list li {
  margin-top: 10px;
  padding-left: 6px;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: #4B5563;
}
.cs-story__body {
  margin-top: 14px;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: #4B5563;
}

/* Stat cards. The light-surface counterpart to .stat-figure, which is
   lime-on-dark and would fail contrast here — these sit on white, so the
   figure is ink and only the source link carries colour (limeInk, 4.9:1). */
.cs-stat {
  flex: 1 1 260px;
  max-width: 380px;
  padding: 40px 32px;
  border-radius: var(--r-panel);
  background: #fff;
  border: 1px solid var(--line-light);
}
.cs-stat__figure {
  font-size: 48px;
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -1.5px;
  color: #12141D;
}
@media (min-width: 1024px) { .cs-stat__figure { font-size: 64px; } }
.cs-stat__label {
  margin-top: 12px;
  font-size: var(--fs-label);
  line-height: 21px;
  color: #64748B;
}
.cs-stat__source {
  margin-left: 6px;
  color: #4E7D22;
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* Page-leave transition, set by the card-click handler in footer.php just
   before it navigates. Deliberately short: this is dead time in front of
   the user, and anything slower than ~250ms reads as lag rather than
   polish. Opacity only — sliding the whole document reflows it. */
.is-leaving body {
  opacity: 0;
  transition: opacity .22s ease;
}
@media (prefers-reduced-motion: reduce) {
  .is-leaving body { opacity: 1; transition: none; }
}

/* Counting figures must not reflow while the digits change: proportional
   digits make the card twitch as 8s replace 1s. */
.cs-stat__figure { font-variant-numeric: tabular-nums; }

/* Pull quote on a case study. Sits on the light story band, so the mark
   is limeInk rather than the dark-theme lime. */
.cs-quote {
  max-width: 760px;
  margin: 56px auto 0;
  padding: 32px 32px 28px;
  border-radius: var(--r-panel);
  border: 1px solid var(--line-light);
  background: #fff;
}
.cs-quote blockquote p {
  font-size: 18px;
  line-height: 30px;
  color: #12141D;
}
.cs-quote blockquote p::before { content: "\201C"; color: #4E7D22; }
.cs-quote blockquote p::after  { content: "\201D"; color: #4E7D22; }
.cs-quote figcaption {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 10px;
  margin-top: 18px;
}
.cs-quote__name { font-size: var(--fs-label); font-weight: 600; color: #12141D; }
.cs-quote__role { font-size: var(--fs-meta); color: #64748B; }

/* Pull quote inside the case-study story. The source page sets these
   lines in caps; this gives them weight without shouting. */
.cs-story__pull {
  margin-top: 20px;
  padding-left: 20px;
  border-left: 3px solid #5F9530;
  font-size: 19px;
  font-weight: 600;
  line-height: 30px;
  letter-spacing: -0.2px;
  color: #12141D;
}

/* Mid-section prompt: a line of copy and a button, side by side on a
   wide screen and stacked on a narrow one. */
/* A line of copy and one button, side by side in a panel: the prompt
   that breaks up a long argument. Was .cs-cta, scoped by name to the
   case study; the healthcare page wanted the same thing, so it is a
   component now. .prompt--dark is the #08090d variant. */
.prompt {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  padding: 26px 28px;
  border-radius: var(--r-panel);
  border: 1px solid var(--line-light);
  background: #fff;
}
.prompt__text {
  max-width: 460px;
  font-size: var(--fs-card-title);
  font-weight: 600;
  line-height: 26px;
  color: #12141D;
}
.prompt__sub {
  margin-top: 6px;
  font-size: var(--fs-card-desc);
  line-height: 23px;
  color: #64748B;
}
.prompt--dark {
  border-color: var(--line-dark);
  background: var(--card-dark-bg);
}
.prompt--dark .prompt__text { color: #fff; }
.prompt--dark .prompt__sub  { color: rgba(255, 255, 255, .55); }

/* Paragraphs inside the story stack with their own rhythm; the first
   after a heading keeps the tighter gap the heading already sets. */
.cs-story__body + .cs-story__body { margin-top: 16px; }

/* The overview's statement plate. The source tints it pink-to-mint; this
   uses the brand pair at low alpha instead, so it belongs to the same
   palette as everything else on the page. Ink stays near-black — the
   tint is a background wash, never a text colour. */
.cs-statement {
  margin-top: 32px;
  padding: 48px 40px;
  border-radius: var(--r-panel);
  background-image: linear-gradient(100deg, rgba(128, 194, 65, .12), rgba(253, 156, 61, .12));
  text-align: center;
  font-size: 20px;
  font-weight: 600;
  line-height: 32px;
  letter-spacing: -0.2px;
  color: #12141D;
}
@media (min-width: 1024px) {
  .cs-statement { padding: 64px 56px; font-size: 24px; line-height: 38px; }
}
.cs-statement + .cs-story__body { margin-top: 32px; }

/* Icon leading a button label, as opposed to .btn__arrow which trails
   it. Inherits the button's colour so it follows every variant and
   hover state without a rule of its own. */
.btn__lead {
  flex: 0 0 auto;
  color: currentColor;
}

/* Crop focal points for gallery tiles. object-fit: cover has to throw
   something away; these say which edge to keep when the subject is not
   in the middle — a caption set hard left, for instance, which a centred
   crop would clip. A class rather than a per-image inline value, so the
   options stay countable. */
.marquee__group--shots img.is-focus-left  { object-position: left center; }
.marquee__group--shots img.is-focus-right { object-position: right center; }

/* ══════════════════════════════════════════════════════════════════
   CASE STUDY STORY — dark band
   Black with the same layered-glow language as .field--banner: soft
   amber to one side, blue to the other, heavily blurred so it reads as
   light in the room rather than as shapes. Everything inside is scoped
   to .cs-story, because detail-overview.php reuses .cs-story__body on a
   white section.
   ══════════════════════════════════════════════════════════════════ */
/* Same plate the .pin steps use, so the two dark bands on the site are
   lit the same way. --pin-rgb is declared here because it is scoped to
   .pin otherwise; this band keeps that component's default blue.

   The background lives here rather than as a bg-[...] utility: it is a
   colour, and colour belongs in this file. */
.cs-story {
  --pin-rgb: 13, 110, 253;
  background:
    radial-gradient(72% 68% at 70% 18%, rgba(var(--pin-rgb), .30) 0%, rgba(var(--pin-rgb), 0) 70%),
    radial-gradient(60% 60% at 12% 92%, rgba(128, 194, 65, .16) 0%, rgba(128, 194, 65, 0) 72%),
    #0B0D12;
}

/* The challenge / The solution / The outcome carry brand lime. Legal
   because this band is dark — the same green on the white overview
   section would fail contrast, which is why the rule is scoped. */
.cs-story .cs-story__head { color: #80C241; }
.cs-story .cs-story__body,
.cs-story .cs-story__list li { color: rgba(255, 255, 255, .68); }
.cs-story .cs-story__list li::marker { color: rgba(255, 255, 255, .40); }

/* Brand lime is legal here — the band is dark. */
.cs-story .cs-story__pull {
  border-left-color: #80C241;
  color: #fff;
}

/* The cards move to the shared dark surface rather than each inventing
   its own. The prompt is not in this list: it carries .prompt--dark and
   dresses itself. */
.cs-story .cs-quote,
.cs-story .cs-stat {
  border-color: var(--line-dark);
  background: var(--card-dark-bg);
}
/* Three cards across a 760px measure leaves ~237px each. The default
   40/32 inset would crowd a 64px figure into what is left, so the inset
   comes in — the cards are narrower here than anywhere else they run. */
.cs-story .cs-stat { padding: 32px 24px; }
.cs-story .cs-quote blockquote p,
.cs-story .cs-stat__figure,
.cs-story .cs-quote__name { color: #fff; }
.cs-story .cs-stat__label,
.cs-story .cs-quote__role { color: rgba(255, 255, 255, .55); }
.cs-story .cs-quote blockquote p::before,
.cs-story .cs-quote blockquote p::after { color: #80C241; }
.cs-story .cs-stat__source { color: #80C241; }

/* The results band that follows it. The section is white, so the cards
   take the tint — .cs-quote and .cs-stat are both authored #fff for the
   grey surfaces they sit on elsewhere, and a white card on a white band
   is only a hairline. Type colours are already ink; nothing here touches
   them. The quote's 56px top margin was spacing it off the outcome copy
   it used to sit under, and the section now supplies that padding. */
.cs-results .cs-quote { margin-top: 0; }

/* ── Client testimonial plate ──────────────────────────────────────
   The client's own words, on a plate in the client's brand colour, so
   the band reads as theirs rather than as ours. Sits directly under the
   Sphinx quote and shares its 760px measure, so the two align.

   The oversized opening mark is a pseudo-content span pinned top-left
   and clipped by the panel — decorative, aria-hidden, and never in the
   flow, so it cannot push the text around at any width.

   --tint is the plate; --tint-mark is the mark, which must stay a
   near-invisible lift off the plate rather than a second colour. */
.cs-testimonial {
  position: relative;
  overflow: hidden;
  max-width: 760px;
  margin: 0 auto;
  padding: 44px 32px 48px;
  border-radius: var(--r-panel);
  background: var(--tint);
  text-align: center;
}
@media (min-width: 768px) { .cs-testimonial { padding: 52px 64px 56px; } }

.cs-testimonial__mark {
  position: absolute;
  top: -46px;
  left: 4px;
  font-size: 200px;
  font-weight: 700;
  line-height: 1;
  color: var(--tint-mark);
  pointer-events: none;
  user-select: none;
}

.cs-testimonial__eyebrow {
  position: relative;
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: 2.4px;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .82);
}
.cs-testimonial__text {
  position: relative;
  margin-top: 18px;
  font-size: 22px;
  font-weight: 700;
  line-height: 32px;
  letter-spacing: -.2px;
  color: #fff;
}
@media (min-width: 768px) {
  .cs-testimonial__text { font-size: 28px; line-height: 40px; }
}

/* One modifier per client. Vivino's wine red; the mark is the same red
   lightened, not white at low alpha, which would grey it. */
.cs-testimonial--wine { --tint: #A62734; --tint-mark: #B23F4A; }
/* Energi Bharat: the deep blue its own product screens are set on. */
.cs-testimonial--volt { --tint: #1E3A8A; --tint-mark: #2F4EA0; }

/* No Stress Impress: the teal from their own mark.
   This one flips the type to ink, unlike the two above. White on
   #59AFB9 measures 2.54:1 — it fails even the 3:1 large-text floor,
   and the quote runs at 22px on a phone. Ink on the same teal is
   7.22:1. The tint is exactly the colour asked for; only the text on
   top of it changes, because the alternative is a quote that some
   readers cannot make out. */
.cs-testimonial--aqua { --tint: #59AFB9; --tint-mark: rgba(18, 20, 29, .13); }
.cs-testimonial--aqua .cs-testimonial__text { color: #12141D; }
/* .78 alpha, not .72: the eyebrow is 14px and uppercase, so it needs
   to clear 4.5:1 rather than the 3:1 large text is allowed. */
.cs-testimonial--aqua .cs-testimonial__eyebrow { color: rgba(18, 20, 29, .78); }

/* The quote takes the brand wash instead of the flat tint — lime into
   amber at .12, the same pairing the dark bands use for their glow
   plate. Both hues are well under the alpha where they would start
   pushing the ink text off 4.5:1. */
.cs-results .cs-quote {
  background-image: linear-gradient(100deg, rgba(128, 194, 65, .12), rgba(253, 156, 61, .12));
}

/* Rotating rim on a portfolio card, the same one the App cost calculator
   button wears — lime rather than amber, and only on hover.

   Three departures from the nav button's version:

   1. The rim is transparent and its animation PAUSED at rest. Twelve
      conic-gradient animations spinning permanently is real compositing
      work for something nobody is looking at; they start when a pointer
      arrives and stop when it leaves.
   2. No resting base edge — .card--dark already draws its own hairline,
      and a second one under it reads as a doubled border.
   3. The outward bloom is dropped. The card sets overflow: hidden so the
      image plate keeps its rounded corners, which clips the bloom
      anyway; keeping it would only cost a blur that never paints. */
.pcard.glow-rim {
  --glow-base: transparent;
}
.pcard.glow-rim::before {
  opacity: 0;
  animation-play-state: paused;
  transition: opacity .28s ease;
}
.pcard.glow-rim::after { display: none; }

.pcard.glow-rim:hover::before,
.pcard.glow-rim:focus-visible::before {
  opacity: 1;
  animation-play-state: running;
}

@media (prefers-reduced-motion: reduce) {
  /* The site-wide rule already stops the spin; this keeps the edge
     lighting up so hover still reads as hover. */
  .pcard.glow-rim:hover::before,
  .pcard.glow-rim:focus-visible::before { opacity: 1; }
}

/* ══════════════════════════════════════════════════════════════════
   APP COST CALCULATOR  (/app-cost-calculator/)
   One question at a time on a light band. The option tiles are real
   radios and checkboxes with the input visually hidden — a styled
   <div> would lose keyboard operation, grouping and the announced
   checked state, all of which come free from the native control.
   ══════════════════════════════════════════════════════════════════ */
.calc-progress { margin-bottom: 28px; }
.calc-progress__label {
  font-size: var(--fs-meta);
  font-weight: 600;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: #64748B;
}
.calc-progress__track {
  margin-top: 10px;
  height: 4px;
  border-radius: 999px;
  background: rgba(18, 20, 29, .10);
  overflow: hidden;
}
.calc-progress__bar {
  display: block;
  height: 100%;
  width: 0;
  border-radius: inherit;
  background: linear-gradient(to right, #9BD657, #80C241, #5FA22E);
  transition: width .35s cubic-bezier(.22,.68,.3,1);
}

.calc {
  padding: 32px 24px 28px;
  border-radius: var(--r-panel);
  border: 1px solid var(--line-light);
  background: #fff;
}
@media (min-width: 768px) { .calc { padding: 40px 40px 32px; } }

/* <fieldset> carries browser default border and padding that no reset
   removes; a stepper needs it to be a plain block. */
.calc-step { border: 0; margin: 0; padding: 0; min-width: 0; }
.calc-step[hidden] { display: none; }
.calc-step__title {
  padding: 0;
  font-size: 22px;
  font-weight: 600;
  line-height: 30px;
  letter-spacing: -.3px;
  color: #12141D;
}
@media (min-width: 768px) { .calc-step__title { font-size: 26px; line-height: 34px; } }
/* The stepper moves focus to this heading on every step so the change
   is announced, but the heading is not a control — a focus ring around
   it reads as a stray border rather than as "you are here". Focus is
   kept, the ring is not: nothing is lost for screen readers, and the
   Next button that was actually clicked keeps its own ring. */
.calc-step__title:focus,
.calc-step__title:focus-visible { outline: none; }
.calc-step__lead {
  margin-top: 8px;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: #64748B;
}

.calc-opts {
  display: grid;
  /* Row gap smaller than column gap: the columns are separated only by
     space, so they need more of it, while the rows already read as
     separate because each tile is bordered. Equal gaps on both axes
     made the grid look like one dense block. */
  gap: 12px 18px;
  margin-top: 22px;
}
@media (min-width: 640px)  { .calc-opts { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* Three columns above lg. The longest label here is "Transport &
   Logistics Management"; at a third of 980px that still sits on one
   line, and it takes twelve options from six rows down to four. */
@media (min-width: 1024px) { .calc-opts { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

.calc-opt {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 16px;
  border-radius: var(--r-sm);
  border: 1px solid var(--line-light);
  background: #fff;
  cursor: pointer;
  transition: border-color .2s ease, background-color .2s ease;
}
.calc-opt:hover { border-color: rgba(18, 20, 29, .22); background: #FAFAF9; }
/* :focus-within, not :focus — the focus is on the hidden input, so the
   ring has to be drawn by the label around it. */
.calc-opt:focus-within { outline: 2px solid #4E7D22; outline-offset: 2px; }

.calc-opt__box {
  position: relative;
  flex: none;
  width: 18px;
  height: 18px;
  border-radius: 6px;
  border: 1.5px solid rgba(18, 20, 29, .28);
  background: #fff;
  transition: border-color .2s ease, background-color .2s ease;
}
.calc-opt input[type="radio"] + .calc-opt__box { border-radius: 50%; }
.calc-opt__box::after {
  content: '';
  position: absolute;
  inset: 0;
  opacity: 0;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%23fff' stroke-width='2.1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m2.5 6.2 2.4 2.4 4.6-5'/%3E%3C/svg%3E") center / 12px 12px no-repeat;
  transition: opacity .15s ease;
}
.calc-opt input:checked + .calc-opt__box {
  border-color: #4E7D22;
  background: #4E7D22;
}
.calc-opt input:checked + .calc-opt__box::after { opacity: 1; }
.calc-opt:has(input:checked) { border-color: #4E7D22; background: rgba(78, 125, 34, .06); }

.calc-opt__label {
  font-size: var(--fs-card-desc);
  line-height: 22px;
  color: #12141D;
}

.calc-other { display: block; margin-top: 18px; }
.calc-other__label,
.calc-field__label {
  display: block;
  margin-bottom: 8px;
  font-size: var(--fs-label);
  font-weight: 500;
  color: #12141D;
}
/* .field-input is authored for the dark form plate — white text on a
   translucent white fill. On the calculator's white card that is white
   on white, so the control is relit here rather than a second input
   class being invented. */
.calc .field-input {
  border-color: var(--line-light);
  background: #fff;
  color: #12141D;
}
.calc .field-input::placeholder { color: #94A3B8; }
.calc .field-input:focus { border-color: #4E7D22; background: #fff; }

/* The phone field carries intl-tel-input's country selector, the same
   widget the enquiry form uses. .field-tel next door relights it for the
   dark plate; this does the same job for the calculator's white card.
   The vendor sheet itself is never edited -- see CLAUDE.md. The .iti
   wrapper is injected at runtime, so the class goes on .calc-field and
   the wrapper is reached through it. */
.calc-field--tel .iti { display: block; width: 100%; }
.calc-field--tel .iti .field-input { margin-top: 0; }
.calc-field--tel .iti__flag-container:hover .iti__selected-flag,
.calc-field--tel .iti__selected-flag:focus {
  background: rgba(18, 20, 29, .04);
  border-radius: var(--r-sm) 0 0 var(--r-sm);
}
.calc-field--tel .iti__arrow { border-top-color: rgba(18, 20, 29, .5); }
.calc-field--tel .iti__arrow--up { border-bottom-color: rgba(18, 20, 29, .5); }
.calc-field--tel .iti__country-list {
  background: #fff;
  border: 1px solid var(--line-light);
  border-radius: var(--r-sm);
  box-shadow: 0 18px 40px -20px rgba(18, 20, 29, .45);
  color: #12141D;
}
.calc-field--tel .iti__country { font-size: var(--fs-meta); }
.calc-field--tel .iti__country.iti__highlight { background: rgba(18, 20, 29, .06); }
.calc-field--tel .iti__dial-code { color: #64748B; }
.calc-field--tel .iti__divider { border-bottom-color: var(--line-light); }

.calc-grid { display: grid; gap: 14px; margin-top: 20px; }
@media (min-width: 640px) { .calc-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.calc-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 14px;
  margin-top: 26px;
  padding-top: 20px;
  border-top: 1px solid var(--line-light);
}
/* Pushes the primary button right whatever else is in the row. */
.calc-nav > [data-calc-next] { margin-left: auto; }
.calc-nav__hint { font-size: var(--fs-card-desc); color: #A66A14; }

.calc-result {
  margin-top: 32px;
  padding: 32px 28px;
  border-radius: var(--r-panel);
  background: #12141D;
  color: #fff;
}
.calc-result[hidden] { display: none; }
/* .calc-result is now only the tail card: the note and the two CTAs
   below the breakdown. Its old __label / __figure / __hours / __rows
   rules went with the standalone estimate card they styled, which the
   breakdown card absorbed. */
.calc-result__note {
  margin-top: 22px;
  font-size: var(--fs-card-desc);
  line-height: 23px;
  color: rgba(255, 255, 255, .55);
}

@media (prefers-reduced-motion: reduce) {
  .calc-progress__bar, .calc-opt, .calc-opt__box, .calc-opt__box::after { transition: none; }
}

/* The step number on a process card. Same limeInk as the calculator's
   own numbering, so the two read as one page. */
.calc-step-num {
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -.4px;
  font-variant-numeric: tabular-nums;
  color: #4E7D22;
}

/* ── Join us / careers band ───────────────────────────────────────
   The panel wears the same plate as the inner-page hero (.field--banner):
   amber glow top-left, blue top-right, black by mid-height, with the
   faint technical grid over it. Rebuilt at panel scale rather than
   reusing .field--banner directly — that rule sizes its glows against a
   full viewport hero, and at 400px tall they would blow past the edges
   and read as two flat washes instead of corner light.

   It is a panel inside the container, so it takes --r-panel (already
   20px) rather than a retyped number. */
.join {
  position: relative;
  border-radius: var(--r-panel);
  background: #08090d;
}

/* The glows. Layer order is bottom-of-stack last, as in .field--banner:
   the blues build the right edge, the ambers the left. */
.join::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: inherit;
  background:
    radial-gradient(52% 92% at 100%  6%, rgba( 46, 116, 240, .85) 0%, rgba( 46, 116, 240, 0) 66%),
    radial-gradient(50% 76% at  82%  0%, rgba( 26,  70, 168, .55) 0%, rgba( 26,  70, 168, 0) 72%),
    radial-gradient(38% 74% at   6%  4%, rgba(196, 100,  38, .58) 0%, rgba(196, 100,  38, 0) 68%),
    radial-gradient(48% 58% at  26%  0%, rgba(150,  76,  30, .40) 0%, rgba(150,  76,  30, 0) 74%);
  filter: blur(45px);
}

/* The grid, masked back to the top third — the plate is black well
   before the bottom of the panel, and a grid on flat black is noise. */
.join::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: inherit;
  background-image:
    linear-gradient(to right,  rgba(255,255,255,.035) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(255,255,255,.035) 1px, transparent 1px);
  background-size: 72px 72px;
  mask-image: radial-gradient(ellipse 80% 62% at 50% 4%, #000 22%, transparent 92%);
  -webkit-mask-image: radial-gradient(ellipse 80% 62% at 50% 4%, #000 22%, transparent 92%);
}

/* The supplied artwork sets this one CTA in caps. It is a label, not
   prose, so the transform is presentational and the markup keeps
   sentence case for screen readers reading the accessible name. */
.join__cta {
  text-transform: uppercase;
  letter-spacing: .6px;
}

/* ── Life at Sphinx ───────────────────────────────────────────────
   The culture bands set their header inside a half-width column, so
   .section-head loses the centring it uses at full width — centred
   text in a narrow column beside a photo reads as a mistake. The gap
   under the header closes up for the same reason: 56px is right under
   a centred full-width heading, far too much in a column. */
.life-split .section-head {
  margin-inline: 0;
  max-width: none;
  text-align: left;
}
.life-split .section-head > .section-title,
.life-split .section-head > .section-sub { margin-inline: 0; max-width: none; }
.life-split .section-body { margin-top: 28px; }

/* The photographs. One radius and one hairline for every image on the
   page, rather than the source's repeated inline border-radius:20px. */
.life-figure {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--r-panel);
  border: 1px solid var(--line-light);
}
/* The light hairline is invisible on #08090d and the dark one is
   invisible on white, so the surface picks its own. */
.section--dark .life-figure { border-color: var(--line-dark); }

/* ── A media column that holds while its copy scrolls ─────────────
   Sticky only from lg, where there is a second column to sit beside.
   Stacked on a phone the two are one above the other, and pinning the
   picture there would park it over its own list. top clears the fixed
   header, matching the guide rail. The grid item around it must be
   left to stretch (no items-center) or there is no travel. */
@media (min-width: 1024px) {
  .sticky-media { position: sticky; top: 120px; }
}

/* A culture card in the sticky stack. Opaque white, because the card
   below scrolls up underneath it and a tint would let its text show
   through. The hairline and the upward shadow are what separate two
   cards sitting on the same colour. */
.life-card {
  padding: 28px;
  border-radius: var(--r-panel);
  border: 1px solid var(--line-light);
  background: #fff;
  box-shadow: 0 -8px 40px -18px rgba(18, 20, 29, .35);
}
.life-card + .life-card { margin-top: 26px; }
@media (min-width: 1024px) { .life-card { padding: 44px 48px; } }

/* A moment card: photo, scrim, play button and caption in one link.
   The whole tile is the target — a play button alone is a small hit
   area, and the caption should be clickable for the same reason. */
.life-shot {
  position: relative;
  display: block;
  overflow: hidden;
  border-radius: var(--r-panel);
  border: 1px solid var(--line-light);
}
.life-shot__img {
  display: block;
  width: 100%;
  height: 232px;
  object-fit: cover;
  transition: transform .4s ease;
}
.life-shot:hover .life-shot__img,
.life-shot:focus-visible .life-shot__img { transform: scale(1.04); }

.life-shot__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(8, 9, 13, .82) 0%, rgba(8, 9, 13, .10) 55%);
}

/* White on a photograph, so it needs the scrim above it to clear 4.5:1
   — it is a label, not decoration. */
.life-shot__cap {
  position: absolute;
  left: 20px;
  right: 20px;
  bottom: 18px;
  font-size: var(--fs-card-desc);
  font-weight: 600;
  color: #fff;
}

@media (prefers-reduced-motion: reduce) {
  .life-shot__img,
  .life-shot:hover .life-shot__img { transition: none; transform: none; }
}

/* ── Cross-document page transitions ──────────────────────────────
   The whole opt-in is the at-rule below: the browser snapshots the
   outgoing page, loads the next one, and animates between the two
   snapshots — ::view-transition-old(root) and -new(root). No router,
   no framework, no JS. This site is multi-page PHP and stays that way.

   Where support is missing (Firefox at the time of writing) the
   navigation simply happens with no transition, which is why this can
   ship without a fallback: the failure mode is the behaviour we have
   today, not a broken page.

   The movement is deliberately small — 24px, not a full screen-width
   swipe. The outgoing page leaves to the left and the incoming one
   arrives from the right, so a forward navigation reads as forward
   motion. Anything larger, or slower than ~350ms, stops feeling like
   polish and starts feeling like waiting. */
@view-transition { navigation: auto; }

@keyframes page-leave {
  to { opacity: 0; transform: translateX(-46px); }
}
@keyframes page-enter {
  from { opacity: 0; transform: translateX(46px); }
}

/* Slower and further than the first pass: 46px of travel over .38s out
   and .62s in, so the movement is legible rather than a flicker.

   The incoming page is deliberately the longer of the two. The reader
   is arriving, not leaving — a slow exit just delays the next page,
   while a slow entrance is the part they actually watch. The old
   snapshot is also gone before the new one finishes, which is what
   keeps the two from crossing over each other mid-travel. */
::view-transition-old(root) {
  animation: page-leave .38s cubic-bezier(.4, 0, 1, 1) both;
}
::view-transition-new(root) {
  animation: page-enter .62s cubic-bezier(.16, .84, .34, 1) both;
}

/* The two snapshots are stacked on top of each other by default, so
   the outgoing page's fade would show the incoming one through it as
   a double image mid-transition. Painting the old one above the new
   one for the length of its own animation keeps the cross-fade clean. */
::view-transition-old(root) { z-index: 1; }
::view-transition-new(root) { z-index: 0; }

/* The API respects this on its own for the default cross-fade, but not
   for animations we name ourselves. */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) { animation: none; }
}

/* The explainer poster is 16:9 and wants to keep that ratio, unlike the
   Life at Sphinx moments row where a fixed 232px band across three
   cards is the point. */
.life-shot__img--video { height: auto; aspect-ratio: 16 / 9; }

/* ── App cost calculator: the breakdown on the estimate page ──────
   One card per question answered. Cards rather than the source's
   dashed-outline flowchart: the flowchart implies a sequence that
   matters, and these are independent line items in a sum. */
.calc-break {
  margin-top: 28px;
  border-radius: var(--r-panel);
  border: 1px solid var(--line-light);
  background: #fff;
  padding: 28px;
}
@media (min-width: 1024px) { .calc-break { padding: 36px 40px; } }

.calc-break__head {
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -.3px;
  color: #12141D;
}

.calc-break__list {
  margin: 22px 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 12px;
}
@media (min-width: 768px) { .calc-break__list { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.calc-break__item {
  border-radius: var(--r-card);
  border: 1px solid var(--line-light);
  background: #F7F7F5;
  padding: 18px 20px;
}
.calc-break__title {
  font-size: var(--fs-label);
  font-weight: 600;
  color: #12141D;
}

.calc-break__opts { margin: 12px 0 0; padding: 0; list-style: none; display: grid; gap: 8px; }
.calc-break__opts li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  font-size: var(--fs-card-desc);
  color: #64748B;
}

/* limeInk on a lime wash: the brand lime behind white text is ~2:1 and
   fails, which is the pairing the source uses for these chips. */
.calc-chip {
  flex: none;
  padding: 3px 10px;
  border-radius: 9999px;
  background: rgba(128, 194, 65, .16);
  font-size: var(--fs-meta);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: #4E7D22;
}
.calc-chip--muted { background: rgba(18, 20, 29, .06); color: #64748B; }

.calc-break__foot {
  margin-top: 22px;
  padding-top: 18px;
  border-top: 1px solid var(--line-light);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  font-size: var(--fs-label);
  font-weight: 600;
  color: #12141D;
}
.calc-break__total {
  font-family: Outfit, system-ui, sans-serif;
  font-size: 26px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: #4E7D22;
}

/* The figure, at the foot of the card that explains it. A dark plate
   inside the white card: the brand lime is ~2:1 on white and fails, so
   it needs a dark ground to be readable at this size. Same treatment
   the old standalone result card used, kept so the estimate still
   carries the visual weight it had before the two cards merged. */
.calc-break__cost {
  position: relative;
  overflow: hidden;
  margin-top: 22px;
  padding: 30px 28px;
  border-radius: var(--r-card);
  /* The flat colour is the floor, not the design: it is what paints if
     the gradients below are unsupported, and it is dark enough on its
     own to keep the lime figure readable. */
  background: #12141D;
  border: 1px solid rgba(128, 194, 65, .22);
  box-shadow: 0 18px 40px -22px rgba(128, 194, 65, .30);
  text-align: center;
}
@media (min-width: 768px) { .calc-break__cost { padding: 36px 32px; } }

/* Layered radial glows on a dark diagonal, the same idiom .field--banner
   and the section glow plate use: lime low, brand blue upper-right.
   CSS rather than a raster for the same reason as those, soft glows
   with no detail in them would be wasted bytes as an image.

   Amber was tried here as a third light and dropped: over a dark ground
   it mixes with the blue into a brown wash rather than reading as its
   own colour. Two lights, well separated, stay clean. */
.calc-break__cost::before {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(58% 92% at 50% 122%, rgba(128, 194, 65, .42) 0%, rgba(128, 194, 65, 0) 62%),
    radial-gradient(46% 92% at 98%  -6%, rgba( 46, 116, 240, .30) 0%, rgba( 46, 116, 240, 0) 66%),
    linear-gradient(152deg, #1C2131 0%, #0E1119 54%, #080A10 100%);
}

/* A hairline of light along the top edge, so the plate reads as lit
   from above rather than as a flat fill with a gradient on it. */
.calc-break__cost::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(255, 255, 255, .07) 0%, rgba(255, 255, 255, 0) 34%);
}

/* Both pseudo-elements are painted before the copy, so the copy is
   lifted above them rather than the plates being pushed behind with a
   negative z-index — no stacking context needed on the card itself. */
.calc-break__cost-label,
.calc-break__cost-figure { position: relative; z-index: 1; }

.calc-break__cost-label {
  font-size: var(--fs-meta);
  font-weight: 600;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .62);
}
.calc-break__cost-figure {
  margin-top: 10px;
  font-family: Outfit, system-ui, sans-serif;
  font-size: 30px;
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -1px;
  font-variant-numeric: tabular-nums;
  color: #80C241;
  /* The glow is decoration on top of a colour that already clears
     contrast on its own, so a browser ignoring text-shadow loses
     nothing that matters. */
  text-shadow: 0 0 34px rgba(128, 194, 65, .38);
  /* An INR estimate is a band of two grouped figures, so it is long.
     Wrapping is fine; breaking a number in half is not. */
  overflow-wrap: break-word;
}
@media (min-width: 768px) { .calc-break__cost-figure { font-size: 42px; } }

/* The CTA block sits below the breakdown, so it drops the top card's
   own heading spacing. */
.calc-result--tail { margin-top: 28px; }

/* Message spans the calculator's contact grid, as it does on the
   enquiry form — a four-row textarea in a half column looks like a
   mistake beside three single-line inputs. */
.calc-field--wide { grid-column: 1 / -1; }

/* The client's mark under a case-study testimonial. Centred and small:
   it identifies the speaker, it is not a brand placement. */
.cs-testimonial__logo {
  display: block;
  width: 88px;
  height: auto;
  margin: 26px auto 0;
}
/* On the two dark tints the mark needs a plate: these logos are set on
   white and carry dark type, which vanishes against a deep blue or
   wine panel. The aqua tint is light enough to take the artwork
   directly, so it is left alone. */
.cs-testimonial--volt .cs-testimonial__logo,
.cs-testimonial--wine .cs-testimonial__logo {
  width: 112px;
  padding: 12px 16px;
  border-radius: var(--r-sm);
  background: #fff;
}

/* ── Custom cursor ────────────────────────────────────────────────
   A pointer drawn by the page instead of the operating system, the
   treatment tile.pt uses. It replaces the arrow with the brand mark and
   swaps state over anything clickable.

   THREE THINGS KEEP IT FROM BEING A PROBLEM:

   • It is opt-in per device. The rules below only apply once the
     script adds .has-cursor to <html>, and the script only does that
     on a fine pointer that can hover — never on a phone or tablet,
     where there is no cursor to replace and hiding it does nothing but
     risk breaking taps.

   • The system cursor is only hidden while the custom one is drawn.
     If the script never runs, the native arrow is untouched.

   • It is off entirely under prefers-reduced-motion. A cursor that
     eases toward the pointer rather than tracking it exactly is
     motion the reader did not ask for, and for some people trailing
     movement is nauseating.

   ⚠ It still overrides an OS-level preference. Someone running an
   enlarged or high-contrast pointer for visibility loses it on this
   site. That is the real cost of the effect — worth weighing against
   how much it adds. */
.cursor {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 9999;
  width: 26px;
  height: 26px;
  margin: -3px 0 0 -3px;   /* the tip, not the box, sits on the point */
  pointer-events: none;
  will-change: transform;
  opacity: 0;
  transition: opacity .25s ease, width .25s ease, height .25s ease;
}
.cursor svg {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}
.cursor__fill { fill: #80C241; }
.cursor__edge { fill: #08090d; }

/* The pointing hand, shown over anything clickable. Brand amber, so the
   hover state reads as a change of colour as well as of shape — a lime
   hand replacing a lime arrow is a difference you have to look for.
   #FD9C3D is the same amber the .btn--dark label and the glow rims
   use. */
.cursor__hand-fill { fill: #FD9C3D; stroke: #08090d; }

/* Only one of the two is ever visible, and the swap is instant: a
   cross-fade would leave both half-drawn over the thing being pointed
   at, which reads as a rendering fault rather than a transition. */
.cursor__hand  { opacity: 0; }
.cursor.is-active .cursor__arrow { opacity: 0; }
.cursor.is-active .cursor__hand  { opacity: 1; }

/* Shown once the pointer has moved at least once — otherwise it sits in
   the top-left corner until first movement. */
.cursor.is-live { opacity: 1; }

/* Over a link or a button: the arrow becomes a pointing hand. The box
   grows and its offset changes with it — the hand's contact point is
   the fingertip, which sits further into the box than the arrow's tip
   does, so without this the pointer would drift as it swapped. */
.cursor.is-active {
  width: 34px;
  height: 34px;
  margin: -4px 0 0 -11px;
}

/* On a light surface the near-black edge reads; on the dark bands it
   disappears, so the mark inverts. Set per section by the script from
   the same [data-cursor-dark] hook the markup carries. */
.cursor.is-inverted .cursor__edge { fill: #ffffff; }
.cursor.is-inverted .cursor__hand-fill { stroke: #ffffff; }

.has-cursor,
.has-cursor a,
.has-cursor button,
.has-cursor [role="button"],
.has-cursor input,
.has-cursor select,
.has-cursor textarea,
.has-cursor label { cursor: none; }

/* Text inputs keep a caret: replacing an I-beam with an arrow makes a
   field look unfocusable. */
.has-cursor input[type="text"],
.has-cursor input[type="email"],
.has-cursor input[type="tel"],
.has-cursor textarea { cursor: text; }

@media (prefers-reduced-motion: reduce) {
  .cursor { display: none; }
  .has-cursor, .has-cursor * { cursor: auto; }
}


/* ── A note on "smooth scrolling" ─────────────────────────────────
   In-page jumps are smoothed above, natively.

   What is deliberately NOT here is the other kind: a library such as
   Lenis or Locomotive that takes the wheel event, cancels it, and
   animates a transform on a wrapper so the whole page glides with
   inertia. On this site that would break three things that already
   work:

     • position: sticky — the case-study stack, the process cards and
       the calculator all rely on it, and it stops working the moment
       the page is moved by transform instead of by scrolling.
     • The scroll-linked footer wordmark, which reads its own position
       every frame against a scroll offset that would no longer move.
     • Cross-document view transitions, which snapshot the real
       scroll position.

   It also takes the scrollbar away from the reader: their trackpad
   momentum, their scroll speed setting, Page Down, and find-in-page
   all stop behaving the way they do everywhere else. If the effect is
   wanted anyway, it is a rebuild of those three systems, not a script
   tag. */

/* ── Heading word reveal ──────────────────────────────────────────
   Each word rises out of its own mask, a fraction behind the one
   before it — the treatment phenomenonstudio.com uses on its
   headings. Theirs is the same construction: a span per word with
   overflow hidden, an inner span translated below it, and an
   incrementing transition-delay.

   No library. It is two transforms and a delay; GSAP or SplitType
   would be ~40KB to do exactly this.

   The mask is on the outer span and the movement on the inner one, so
   nothing ever paints outside the line box — translate the word
   itself and descenders from the line above clip through. */
.w {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  /* Room for descenders: a 'g' or 'y' sits below the baseline, and a
     mask cut exactly to the line box shears it off. */
  padding-bottom: .12em;
  margin-bottom: -.12em;
  /* The same problem sideways, for italics. An italic glyph leans past
     the inline-box it is measured in, so the mask sheared the right of
     the last letter -- visible on every heading that ends in an accent
     phrase, "developers <i>who ship?</i>" and the seventeen others like
     it. Padding widens the clip box; the matching negative margin takes
     the width back, so the advance width and the word spacing either
     side are unchanged. Horizontal only: padding on the vertical axis
     would open the mask the reveal rises out of.

     .2em is measured, not guessed. Inter's italic leans about 10deg, so
     a full-height glyph overhangs by asc x tan(10deg) -- 6.9px on the
     52px title -- and .section-title also carries letter-spacing -1.8px,
     which shortens the last glyph's advance and takes the box in by that
     much again. 8.7px of overhang is .167em; .2em leaves headroom. The
     ratio barely moves at the 42px and 32px steps, so one em value
     covers all three. */
  padding-left: .2em;
  margin-left: -.2em;
  padding-right: .2em;
  margin-right: -.2em;
}
.w__in {
  display: inline-block;
  transform: translateY(110%);
  transition: transform 1.25s cubic-bezier(.16, .84, .3, 1);
  /* The gap between words carries as much of the effect as the travel
     itself: at 70ms a long heading finished almost together, which
     reads as one block moving rather than words arriving in turn. */
  transition-delay: calc(var(--w, 0) * 115ms);
}
[data-words].is-shown .w__in { transform: none; }

/* Visible by default, the same invariant the rest of the motion on
   this site follows: the masked state is only ever applied by script,
   and only to headings the reader has not reached. */
@media (prefers-reduced-motion: reduce) {
  .w__in { transform: none; transition: none; }
}

/* ── Industries: pinned horizontal scroll ────────────────────────
   Heading and tiles are held together while the row travels right to
   left; the section releases once the last tile has arrived.

   Everything is behind .is-h, added by the script above 1024px when
   prefers-reduced-motion is unset. Without it these are inert wrappers
   and the <ul> is the grid the markup ships — so JS off, a phone, or a
   reader who wants less motion get a readable list, never a section
   that will not scroll past.

   Padding sits on the section rather than the stage: the stage is
   sticky, and padding on a sticky box is inside the sticky area, which
   pushes the content down as it pins. */
.hscroll { padding-block: 5rem; }
@media (min-width: 1024px) { .hscroll { padding-block: 7rem; } }

.hscroll.is-h {
  /* Height is set in pixels by the script: one viewport plus exactly
     the distance the track has to travel, so a pixel of scroll is a
     pixel of movement. */
  padding-block: 0;
}
.hscroll.is-h .hscroll__stage {
  position: sticky;
  top: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* Full height on purpose. The wrapper beneath a pinned stage is the
     scroll runway and it is empty; a stage shorter than the window
     lets that emptiness show under the tiles. */
  height: 100vh;
  overflow: hidden;
}

.hscroll.is-h .hscroll__vp { overflow: hidden; }

.hscroll.is-h .hscroll__track {
  display: flex;
  gap: 20px;
  width: max-content;
  will-change: transform;
}
/* Width comes from --tile, set in pixels by the script. A percentage
   cannot be used: the track is a flex item with no definite width of
   its own — it is sized by these children — so the percentage would
   resolve against the box it is meant to be sizing, and the tiles
   collapse to slivers. */
.hscroll.is-h .hscroll__track > li {
  flex: 0 0 var(--tile, 280px);
}
.hscroll.is-h .hscroll__track > li .sector {
  aspect-ratio: auto;
  height: min(46vh, 380px);
}

/* A card rail rather than a photo rail (the PHP page's features band).
   Two differences from the sector tiles above: the card has to fill
   the tile so eight of them line up along one baseline rather than
   ragging, and it takes a floor rather than a fixed height, because
   the copy in them is not the same length. Both apply on the phone
   swipe rail too, so they sit outside .is-h.

   MUST stay behind the --cards modifier. Making the list item a flex
   container unscoped turns the sector tiles into flex items, and a
   .sector is an aspect-square box whose contents are absolutely
   positioned &mdash; as a flex item it has nothing to take its width from
   and collapses to nothing, emptying every photo rail on the site. */
.hscroll__track--cards > li { display: flex; }
.hscroll__track--cards > li > .card {
  width: 100%;
  height: 100%;
}
@media (min-width: 1024px) {
  .hscroll.is-h .hscroll__track--cards > li > .card { min-height: min(46vh, 380px); }
}

/* ── Phone: swipe the tiles rather than stack them ────────────────
   Eight square tiles in one column is a very long scroll on a phone,
   so below sm the same grid becomes a snapping swipe rail. It is CSS
   only — no script, no library — because the markup is already a list
   of equal tiles; overflow plus scroll-snap is the whole carousel.

   The rail is pulled out to the screen edges (the container's own 16px
   inset is added back as padding) so a tile can sit flush left while
   the next one peeks in at the right, which is what tells a reader
   there is more to swipe. scroll-padding keeps a snapped tile off the
   edge it aligns to.

   This stops at 639px, well clear of the 1024px where the script adds
   .is-h, so the two treatments can never both apply. */
@media (max-width: 639px) {
  .hscroll__vp {
    margin-inline: -16px;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: 16px;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .hscroll__vp::-webkit-scrollbar { display: none; }

  .hscroll__vp .hscroll__track {
    display: flex;
    width: max-content;
    padding-inline: 16px;
  }
  .hscroll__vp .hscroll__track > li {
    flex: 0 0 78vw;
    max-width: 320px;
    scroll-snap-align: start;
  }
}

/* ── Hero accent: flowing brand-green gradient ────────────────────
   The two accented words in the home hero — "AI-First" and
   "Solutions" — carry a slow gradient built around the brand green
   rather than a flat fill.

   #80C241 IS THE DOMINANT TONE, not one stop among several. It sits at
   0%, 50% and 100% of the ramp, so the eye returns to the exact brand
   colour twice per cycle and the two shades either side of it read as
   light moving across the word rather than as a colour change. Those
   shades are the site's own limeMark (#5F9530) and a matching tint
   (#A8DF6B) — no hue outside the brand enters the animation.

   All three clear AA against the hero black by a wide margin — 5.5,
   9.2 and 12.7 to 1 — so the word stays legible at every point in the
   cycle, which a gradient that ran to a pale or saturated extreme
   would not.

   18s and an ease-in-out: slow enough to register as ambient rather
   than as something demanding attention.

   Scoped to the hero H1. .text-lime is used across the whole site and
   is a flat colour everywhere else. */
@supports ((background-clip: text) or (-webkit-background-clip: text)) {
  main section > div > h1 .text-lime {
    background-image: linear-gradient(
      100deg,
      #80C241 0%,
      #5F9530 18%,
      #80C241 38%,
      #A8DF6B 58%,
      #80C241 78%,
      #5F9530 100%
    );
    background-size: 260% 100%;
    -webkit-background-clip: text;
            background-clip: text;
    color: transparent;
    animation: lime-flow 18s ease-in-out infinite;
  }
}

@keyframes lime-flow {
  0%, 100% { background-position:   0% 50%; }
  50%      { background-position: 100% 50%; }
}

/* Reduced motion keeps the gradient — it is part of the design — and
   simply stops it moving. */
@media (prefers-reduced-motion: reduce) {
  main section > div > h1 .text-lime { animation: none; }
}



/* ── Mobile navigation drawer ─────────────────────────────────────
   The panel behind the hamburger, for every viewport below xl where
   the nav list itself is hidden.

   The markup ships empty and footer.php clones the desktop nav's own
   links into it on first open, so there is one copy of the navigation
   in header.php rather than two that drift apart.

   z-index 60: above the header's 50, because the drawer covers it.
   Below the drawn cursor's 9999, which should stay on top of
   everything. */
.mnav {
  position: fixed;
  inset: 0;
  z-index: 60;
}
.mnav[hidden] { display: none; }

.mnav__scrim {
  position: absolute;
  inset: 0;
  background: rgba(4, 5, 8, .72);
  backdrop-filter: blur(2px);
  opacity: 0;
  transition: opacity .28s ease;
}
.mnav.is-open .mnav__scrim { opacity: 1; }

/* A panel from the right rather than a full-screen takeover: the
   reader keeps a strip of the page in view, so it reads as a layer
   over what they were reading and not as a new page. */
.mnav__panel {
  position: absolute;
  inset: 0 0 0 auto;
  display: flex;
  width: min(420px, 92vw);
  flex-direction: column;
  border-left: 1px solid var(--line-dark);
  background: #0B0C11;
  box-shadow: -30px 0 80px -30px rgba(0, 0, 0, .8);
  transform: translateX(100%);
  transition: transform .32s cubic-bezier(.22, .61, .36, 1);
}
.mnav.is-open .mnav__panel { transform: none; }

.mnav__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex: none;
  padding: 22px 20px;
  border-bottom: 1px solid var(--line-dark);
}
.mnav__title {
  font-size: var(--fs-label);
  font-weight: 600;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .5);
}
.mnav__close {
  display: flex;
  width: 38px;
  height: 38px;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--line-dark);
  border-radius: var(--r-sm);
  background: rgba(255, 255, 255, .04);
  color: #fff;
  transition: background-color .2s ease;
}
.mnav__close:hover { background: rgba(255, 255, 255, .09); }

/* The scrolling middle. overscroll-behavior stops a flick at the end
   of the list scrolling the page behind the drawer. */
.mnav__body {
  flex: 1 1 auto;
  padding: 8px 20px 20px;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.mnav__foot {
  display: grid;
  flex: none;
  gap: 10px;
  padding: 16px 20px calc(16px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--line-dark);
}

/* One row per top-level nav item. A button where the desktop item
   opens a mega menu, a plain link where it does not. */
.mnav__row {
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 15px 2px;
  border-bottom: 1px solid var(--line-dark);
  font-size: 17px;
  font-weight: 500;
  text-align: left;
  color: #fff;
}
.mnav__row[aria-current="page"] { color: #80C241; }

.mnav__chev {
  flex: none;
  color: rgba(255, 255, 255, .45);
  transition: transform .25s ease;
}
.mnav__row[aria-expanded="true"] .mnav__chev { transform: rotate(180deg); }

/* Buttons plus panels rather than <details>, for the reason the
   accordion documents: a closed <details> cannot animate its height. */
.mnav__panelbox {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows .3s ease;
}
.mnav__panelbox.is-open { grid-template-rows: 1fr; }
.mnav__panelbox > div { overflow: hidden; }

.mnav__group {
  padding: 14px 0 4px;
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .38);
}
.mnav__link {
  display: block;
  padding: 10px 0 10px 14px;
  border-left: 1px solid var(--line-dark);
  font-size: 15px;
  line-height: 22px;
  color: rgba(255, 255, 255, .72);
  transition: color .2s ease, border-color .2s ease;
}
.mnav__link:hover,
.mnav__link:focus-visible { color: #fff; border-color: #80C241; }
.mnav__link[aria-current="page"] { color: #80C241; border-color: #80C241; }

/* The bars become a cross while the drawer is open. */
.mnav-btn[aria-expanded="true"] .mnav-btn__bars { background: transparent; }
.mnav-btn[aria-expanded="true"] .mnav-btn__bars::before { transform: translateY(6px) rotate(45deg); }
.mnav-btn[aria-expanded="true"] .mnav-btn__bars::after  { transform: translateY(-6px) rotate(-45deg); }
.mnav-btn__bars::before,
.mnav-btn__bars::after { transition: transform .25s ease; }

/* Nothing here is load-bearing: without the transitions the drawer
   still opens and closes, it simply arrives rather than slides. */
@media (prefers-reduced-motion: reduce) {
  .mnav__scrim,
  .mnav__panel,
  .mnav__panelbox,
  .mnav__chev,
  .mnav-btn__bars::before,
  .mnav-btn__bars::after { transition: none; }
}

/* ══════════════════════════════════════════════════════════════════
   THREE BAND TREATMENTS FOR THE IoT PAGE
   The page ran Benefits, Offerings and Services one after another and
   all three were the same card grid, so the eye read one long block of
   boxes. Same content, three different shapes: a numbered list, a
   ruled matrix, and a row card. Nothing here is page-specific in its
   naming, so any band with the same shape of content can use them.
   ══════════════════════════════════════════════════════════════════ */

/* ── Numbered list ────────────────────────────────────────────────
   For a short set of ideas that want room to breathe. No card, no
   border: a big lime numeral, a hairline, and the copy. */
.numlist {
  display: grid;
  gap: 0 56px;
  grid-template-columns: 1fr;
}
@media (min-width: 768px) { .numlist { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.numlist__item {
  padding: 30px 0;
  border-top: 1px solid var(--line-light);
}
.numlist__num {
  display: block;
  font-size: 34px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -1px;
  font-variant-numeric: tabular-nums;
  color: #4E7D22;
}
.numlist__title {
  margin-top: 14px;
  font-size: var(--fs-card-title);
  font-weight: 600;
  color: #12141D;
}
.numlist__desc {
  margin-top: 8px;
  font-size: var(--fs-card-desc);
  line-height: 24px;
  color: #64748B;
}

/* ── Ruled matrix ─────────────────────────────────────────────────
   For a longer set where the grid itself should read as one object:
   cells divided by hairlines rather than floated as separate cards.
   The outer rounding and clipping keep the rules from running off the
   corners. */
.matrix {
  display: grid;
  overflow: hidden;
  border: 1px solid var(--line-light);
  border-radius: var(--r-panel);
  background: #fff;
  grid-template-columns: 1fr;
}
@media (min-width: 640px) { .matrix { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1024px) { .matrix { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

.matrix__cell {
  padding: 32px 28px;
  border-top: 1px solid var(--line-light);
  border-left: 1px solid var(--line-light);
  transition: background-color .25s ease;
}
.matrix__cell:hover { background: #F7F9F4; }
/* The first row has the panel's own border above it, and the first
   cell in each row has it to the left. Both are re-drawn by the rules
   above, so they are taken back off. */
.matrix__cell:nth-child(1) { border-top: 0; }
@media (min-width: 640px) {
  .matrix__cell:nth-child(2) { border-top: 0; }
  .matrix__cell:nth-child(odd) { border-left: 0; }
}
@media (min-width: 1024px) {
  .matrix__cell:nth-child(3) { border-top: 0; }
  .matrix__cell:nth-child(odd) { border-left: 1px solid var(--line-light); }
  .matrix__cell:nth-child(3n+1) { border-left: 0; }
}
.matrix__title {
  margin-top: 18px;
  font-size: var(--fs-card-title);
  font-weight: 600;
  color: #12141D;
}
.matrix__desc {
  margin-top: 8px;
  font-size: var(--fs-card-desc);
  line-height: 24px;
  color: #64748B;
}

/* ── Row card ─────────────────────────────────────────────────────
   The icon beside the copy rather than above it, so a run of these
   reads as a list of services and not as another grid of tiles. */
.rowcard {
  display: flex;
  gap: 20px;
  padding: 26px 24px;
  border: 1px solid var(--line-dark);
  border-radius: var(--r-card);
  background: var(--card-dark-bg);
  transition: background-color .25s ease, border-color .25s ease;
}
.rowcard:hover { background: var(--card-dark-bg-hover); border-color: rgba(255, 255, 255, .18); }
.rowcard__body { min-width: 0; }
.rowcard__title {
  font-size: var(--fs-card-title);
  font-weight: 600;
  color: #fff;
}
.rowcard__desc {
  margin-top: 6px;
  font-size: var(--fs-card-desc);
  line-height: 24px;
  color: rgba(255, 255, 255, .62);
}

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

/* ── Tech cards carrying a wordmark ───────────────────────────────
   Most tool marks are square and sit in the 60px tile as they are.
   A few are wordmarks (IoT.js, Device.js, LoRaWAN, Eclipse IoT, 4G
   LTE) and a square tile renders them at a fraction of their width.
   .tech-stack--marks widens the tile and lets the image size itself,
   so a wordmark fills the width and a square logo keeps its 44px. */
.tech-stack--marks .tech-card__tile {
  width: 92px;
  height: 60px;
}
.tech-stack--marks .tech-card__tile img {
  width: auto;
  height: auto;
  max-width: 76px;
  max-height: 44px;
}

/* ── Logos with a white ground baked in ───────────────────────────
   Some supplied marks arrive as a logo sitting on a white card, and on
   a tinted band that card shows as a chip behind each one. multiply
   drops the white into the background while leaving the colour, so the
   logo reads as sitting on the page.

   Only ever on a LIGHT band: multiply against a dark surface would
   take the logo with it. */
.logo-blend { mix-blend-mode: multiply; }

/* ── Numbered list on a dark band ─────────────────────────────────
   The light version's ink and hairline vanish on #08090d, so the dark
   skin restates the three colours that carry it: the numeral takes the
   brand lime rather than limeInk (which is drawn for white surfaces
   and sits too dark here), the title goes white, and the rule and the
   copy take their dark-section values.

   .numlist__num is also used on its own inside dark cards (the .NET
   page's four reasons), which is why it carries the lime there too. */
.section--dark .numlist__num { color: #80C241; }
.numlist--dark .numlist__item { border-top-color: var(--line-dark); }
.numlist--dark .numlist__num   { color: #80C241; }
.numlist--dark .numlist__title { color: #fff; }
.numlist--dark .numlist__desc  { color: rgba(255, 255, 255, .62); }

/* ── Brand hues for the commerce, SaaS, PWA and custom-software stacks ──
   One line per tile added by services/web-development.php,
   custom-software-development.php, e-commerce-development.php,
   saas-development-company.php and pwa-development.php. Same contract as
   every other .tech--*: the card reads --tech-rgb for its top bar and its
   hover wash, and nothing else. Values are each product's own brand colour.

   The handful with no brand of their own (service workers, the web app
   manifest, CDN integration, personalisation engines) take the neutral
   default from .tech-card and render a sprite glyph, exactly as the
   Business Intelligence stack does for its unbranded entries. */
.tech--typescript      { --tech-rgb:  49, 120, 198; }
.tech--tailwind        { --tech-rgb:  56, 189, 248; }
.tech--nuxt            { --tech-rgb:   0, 220, 130; }
.tech--pwa             { --tech-rgb:  93,  15, 192; }
.tech--cassandra       { --tech-rgb:  29,  55,  99; }
.tech--ansible         { --tech-rgb:  30,  30,  30; }
.tech--prometheus      { --tech-rgb: 230,  82,  44; }
.tech--grafana         { --tech-rgb: 246, 130,  31; }
.tech--gitlabci        { --tech-rgb: 226,  67,  41; }
.tech--springboot      { --tech-rgb: 109, 179,  63; }

/* Commerce platforms */
.tech--shopify         { --tech-rgb:  95, 142,  62; }
.tech--magento         { --tech-rgb: 238, 103,  47; }
.tech--adobecommerce   { --tech-rgb: 250,  15,   0; }
.tech--woocommerce     { --tech-rgb: 150,  88, 138; }
.tech--bigcommerce     { --tech-rgb:  18,  20,  29; }
.tech--vtex            { --tech-rgb: 249,  32,  75; }

/* Payments */
.tech--paypal          { --tech-rgb:   0,  48, 135; }
.tech--authorizenet    { --tech-rgb:  27, 116, 187; }
.tech--square          { --tech-rgb:  18,  20,  29; }
.tech--klarna          { --tech-rgb: 255, 179, 199; }
.tech--afterpay        { --tech-rgb:  17, 227, 191; }

/* ERP and CRM */
.tech--epicor          { --tech-rgb:   0,  70,  95; }
.tech--odoo            { --tech-rgb: 113,  75, 103; }
.tech--oraclenetsuite  { --tech-rgb:  18,  20,  29; }
.tech--dynamics365     { --tech-rgb:   0, 120, 212; }
.tech--dynamicscrm     { --tech-rgb:   0, 120, 212; }
.tech--zohocrm         { --tech-rgb: 228,  30,  37; }

/* Analytics and search */
.tech--ga4             { --tech-rgb: 232,  113,  10; }
.tech--adobeanalytics  { --tech-rgb:  73,  22, 216; }
.tech--segment         { --tech-rgb:  82, 189, 149; }
.tech--algolia         { --tech-rgb:   0,  70, 245; }
.tech--coveo           { --tech-rgb: 243,  84,  38; }
.tech--constructorio   { --tech-rgb:  18,  20,  29; }
.tech--sendgrid        { --tech-rgb:  27, 130, 224; }

/* ── Design-tool hues for the UI/UX services page ─────────────────
   Same contract as every other .tech--*: the card reads --tech-rgb for
   its top bar and hover wash. Values are each tool's brand colour. */
.tech--balsamiq       { --tech-rgb: 190,  30,  45; }
.tech--whimsical      { --tech-rgb: 111,  44, 224; }
.tech--miro           { --tech-rgb: 255, 208,   2; }
.tech--figjam         { --tech-rgb: 255, 114,  98; }
.tech--invision       { --tech-rgb: 255,  59,  90; }
.tech--maze           { --tech-rgb:  22, 100, 255; }
.tech--lookback       { --tech-rgb: 232,  62,  92; }
.tech--usertesting    { --tech-rgb:  33,  87, 255; }
.tech--figmalibraries { --tech-rgb: 162,  89, 255; }
.tech--storybook      { --tech-rgb: 255,  71, 133; }

/* ── Shopify stack hues ───────────────────────────────────────────
   Same contract as every other .tech--*. */
.tech--shopifyplus    { --tech-rgb:  18,  20,  29; }
.tech--shopifymarkets { --tech-rgb:  95, 142,  62; }
.tech--liquid         { --tech-rgb:  67,  56, 202; }
.tech--hydrogen       { --tech-rgb: 124,  58, 237; }
.tech--paymentgw      { --tech-rgb:  13, 110, 253; }
.tech--inventory      { --tech-rgb:  14, 116, 144; }
.tech--crmplat        { --tech-rgb:  30,  64, 175; }
.tech--erpsol         { --tech-rgb:   2, 132, 199; }
.tech--speedopt       { --tech-rgb: 147,  51, 234; }
.tech--seoopt         { --tech-rgb:  22, 163,  74; }
.tech--mobileopt      { --tech-rgb:  71,  85, 105; }
.tech--convopt        { --tech-rgb: 217, 119,   6; }
/* Shopify commerce tiles that ship as sprite glyphs rather than logo art:
   v2's own files for these three have their captions baked into the
   artwork, and its "Analytics Platforms" SVG actually holds the
   Marketing Automation mark. */
.tech--shipping       { --tech-rgb:  14, 116, 144; }
.tech--taxtools       { --tech-rgb: 100, 116, 139; }
.tech--analytics      { --tech-rgb:  79,  70, 229; }

/* Digital-marketing stack. Same brand-hue scheme, one line per tool. */
.tech--gsc            { --tech-rgb:  66, 133, 244; }
.tech--ga4            { --tech-rgb: 227, 116,   0; }
.tech--keywordplanner { --tech-rgb:  52, 168,  83; }
.tech--semrush        { --tech-rgb: 255, 100,  42; }
.tech--ahrefs         { --tech-rgb:   5, 101, 255; }
.tech--screamingfrog  { --tech-rgb:   0, 150,  57; }
.tech--yoast          { --tech-rgb: 164,  40, 106; }
.tech--googleads      { --tech-rgb:  66, 133, 244; }
.tech--microsoftads   { --tech-rgb:   0, 120, 212; }
.tech--linkedinads    { --tech-rgb:  10, 102, 194; }
.tech--youtubeads     { --tech-rgb: 255,   0,   0; }
.tech--gdn            { --tech-rgb:  52, 168,  83; }
.tech--metasuite      { --tech-rgb:  24, 119, 242; }
.tech--linkedin       { --tech-rgb:  10, 102, 194; }
.tech--instagram      { --tech-rgb: 225,  48, 108; }
.tech--facebook       { --tech-rgb:  24, 119, 242; }
.tech--xtwitter       { --tech-rgb:  24,  26,  31; }
.tech--hootsuite      { --tech-rgb: 255,  76,  70; }
.tech--buffer         { --tech-rgb:  24,  26,  31; }
.tech--mailchimp      { --tech-rgb: 200, 175,   0; }
.tech--hubspot        { --tech-rgb: 255, 122,  89; }
.tech--brevo          { --tech-rgb:  11, 153, 110; }
.tech--canva          { --tech-rgb:   0, 196, 204; }
.tech--grammarly      { --tech-rgb:  21, 193, 109; }
.tech--chatgpt        { --tech-rgb:  16, 163, 127; }
.tech--gtm            { --tech-rgb:  36, 111, 219; }
.tech--looker         { --tech-rgb:  25, 103, 210; }
.tech--hotjar         { --tech-rgb: 255,  61,   0; }
.tech--clarity        { --tech-rgb:   0, 120, 212; }
.tech--optimizely     { --tech-rgb: 134,  50, 255; }
.tech--vwo            { --tech-rgb: 168,  26,  60; }
.tech--trello         { --tech-rgb:   0, 101, 255; }
.tech--asana          { --tech-rgb: 244,  88,  86; }
.tech--jira           { --tech-rgb:  38, 132, 255; }
.tech--slack          { --tech-rgb:  74,  21,  75; }
.tech--msteams        { --tech-rgb:  98, 100, 167; }
.tech--xamarin        { --tech-rgb:  59, 153, 216; }
.tech--automationanywhere { --tech-rgb: 250,  70,  22; }
.tech--nist           { --tech-rgb:  17,  24,  39; }
.tech--cis            { --tech-rgb:  27, 108, 168; }

/* Cyber-security practices and standards. Most of these are disciplines
   rather than products, so the hue is chosen for legibility in the tile
   rather than borrowed from a brand. */
.tech--securesdlc     { --tech-rgb:  99, 102, 241; }
.tech--apisec         { --tech-rgb:  20, 184, 166; }
.tech--authz          { --tech-rgb: 139,  92, 246; }
.tech--cloudiam       { --tech-rgb:  14, 165, 233; }
.tech--secureconf     { --tech-rgb:  71,  85, 105; }
.tech--iac            { --tech-rgb:  34, 197,  94; }
.tech--oidc           { --tech-rgb: 245, 158,  11; }
.tech--configrev      { --tech-rgb:   6, 182, 212; }
.tech--depscan        { --tech-rgb: 192,  70, 200; }
.tech--owasptop10     { --tech-rgb:   0, 105, 146; }
.tech--bestprax       { --tech-rgb:  16, 185, 129; }
.tech--riskgov        { --tech-rgb: 220,  38,  38; }
.tech--agenticai      { --tech-rgb: 124,  58, 237; }
.tech--aiintegration  { --tech-rgb:  13, 148, 136; }

/* ══════════════════════════════════════════════════════════════════
   AI-first home page (index-ai.php)

   Everything below belongs to the four sections under
   include/sections/ai-home/. All class names are new, so nothing here
   can reach index.php or any existing page. If the AI-first home page
   ever replaces index.php, this block stays as it is.
   ═════════════════════════════════════════════════════════════════ */

/* ── Philosophy: the AI / human Venn ─────────────────────────────
   Two bordered circles on one layer, three text columns on the layer
   above. The geometry is one number: --venn-d, the circle diameter as
   a fraction of the figure width. Everything else derives from it, so
   the middle column and the intersection cannot drift apart.

     circle diameter   62% of the figure
     overlap           2 x 62% - 100% = 24%
     outer columns     (100% - 24%) / 2 = 38% each

   At 62% the intersection is wide enough for "Sphinx delivery" on two
   lines without the lobes swallowing each other. Change --venn-d and
   the grid follows; do not hard-code the columns.
   ───────────────────────────────────────────────────────────────── */
.venn {
  --venn-d: 62%;
  --venn-overlap: calc(2 * var(--venn-d) - 100%);
  --venn-side: calc((100% - var(--venn-overlap)) / 2);

  position: relative;
  width: 100%;
  max-width: 680px;
  margin: 0 auto;
  aspect-ratio: 680 / 420;
}

.venn__shapes { position: absolute; inset: 0; }

.venn__lobe {
  position: absolute;
  top: 50%;
  width: var(--venn-d);
  aspect-ratio: 1;
  transform: translateY(-50%);
  border: 1px solid;
  border-radius: 50%;
}
.venn__lobe--ai {
  left: 0;
  border-color: rgba(192, 132, 252, .45);
  background: radial-gradient(circle at 40% 40%, rgba(192, 132, 252, .10), transparent 70%);
}
.venn__lobe--human {
  right: 0;
  border-color: rgba(128, 194, 65, .45);
  background: radial-gradient(circle at 60% 40%, rgba(128, 194, 65, .10), transparent 70%);
}

/* The label layer. Middle column is the intersection, to the pixel. */
.venn__labels {
  position: relative;
  display: grid;
  grid-template-columns: var(--venn-side) var(--venn-overlap) var(--venn-side);
  align-items: center;
  height: 100%;
}
.venn__label {
  padding: 0 4px;
  text-align: center;
}
.venn__title {
  font-size: clamp(13px, 2.1vw, 16px);
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: .5px;
  text-transform: uppercase;
  color: #fff;
}
.venn__meta {
  margin-top: 8px;
  font-size: clamp(11px, 1.6vw, 13px);
  line-height: 1.45;
  color: rgba(255, 255, 255, .5);
}
.venn__label--core .venn__title { color: #80C241; }

/* Below sm the intersection is roughly 80px wide and "Sphinx delivery"
   cannot sit in it legibly at any size. The figure becomes a stacked
   list: the same three labels, in the same order, without the circles.
   Nothing is lost, because the geometry was never carrying meaning the
   words do not. */
@media (max-width: 639px) {
  .venn {
    aspect-ratio: auto;
    max-width: 340px;
  }
  .venn__shapes { display: none; }
  .venn__labels {
    grid-template-columns: 1fr;
    gap: 22px;
  }
  .venn__label {
    border-left: 2px solid rgba(255, 255, 255, .14);
    padding: 2px 0 2px 16px;
    text-align: left;
  }
  .venn__label--core { border-left-color: #80C241; }
}

/* ── RAG numbered steps ────────────────────────────────────────────
   Was shared with the How we work band until that moved to the ruled
   grid below; the RAG section still renders it. */
.method-step {
  border-top: 1px solid var(--line-light);
  padding-top: 20px;
}
.method-step__num {
  font-family: 'Outfit', sans-serif;
  font-size: var(--fs-meta);
  font-weight: 700;
  letter-spacing: 1px;
  color: #4E7D22;   /* limeInk: the brand lime fails contrast on white */
}
/* .card-desc defaults to a white-on-dark colour; these steps sit on a
   white band without a .card--light wrapper to relight them. */
.method-step .card-desc { --card-desc-color: #64748B; }

/* ── RAG diagram ───────────────────────────────────────────────── */
.rag-figure {
  border: 1px solid var(--line-light);
  border-radius: var(--r-panel);
  background: #F6F6F4;
  padding: 28px 20px;
  overflow-x: auto;
}
.rag-figure__svg {
  width: 100%;
  min-width: 720px;   /* below this the four boxes crush; scroll instead */
  height: auto;
}
.rag-box {
  fill: #fff;
  stroke: rgba(18, 20, 29, .12);
  stroke-width: 1;
}
.rag-box__label {
  fill: #12141D;
  font-size: 15px;
  font-weight: 600;
}
.rag-box__meta {
  fill: #64748B;
  font-size: 12px;
}
.rag-arrow {
  stroke: #94A3B8;
  stroke-width: 1.6;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.rag-guard {
  fill: rgba(78, 125, 34, .08);
  stroke: rgba(78, 125, 34, .28);
  stroke-width: 1;
}
.rag-guard__label {
  fill: #4E7D22;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 1.2px;
}

/* ── Governance icon tile ──────────────────────────────────────── */
.gov-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: var(--r-sm);
  border: 1px solid rgba(192, 132, 252, .28);
  background: rgba(192, 132, 252, .12);
  color: #C084FC;
}

/* ── AI-first hero figure ─────────────────────────────────────────
   The contour head in the right-hand column of index-ai.php's hero,
   and the 3D stage it sits on.

   FOUR LAYERS, one transform each. A single element cannot hold two
   transforms, and the cursor tilt, the scroll parallax and the idle
   float all run on different clocks:

     .hero-figure            perspective + glow. Never transformed.
     .hero-figure__parallax  scroll: translateY + scale.
     .hero-figure__stage     cursor: rotateX/rotateY + translate.
     .hero-figure__img       the idle float, the sizing and the mask.

   EVERY CUSTOM PROPERTY BELOW HAS A RESTING DEFAULT. The script in
   footer.php writes them; if it never runs, or the browser is under
   prefers-reduced-motion, the figure renders centred, level and full
   size. Nothing here can leave the hero blank or scaled down.
   ───────────────────────────────────────────────────────────────── */
.hero-figure {
  position: relative;
  display: flex;
  justify-content: center;

  /* The depth of the 3D box. Lower is a wider-angle lens and a more
     violent tilt; 1100px reads as a sculpture turning rather than a
     card flipping. */
  perspective: 1100px;

  /* Opacity only, and pure CSS: the fade must not depend on the script,
     and animating opacity here leaves the perspective root untouched. */
  animation: hero-figure-in .9s .1s cubic-bezier(.22, .61, .36, 1) both;
}

/* The glow. Sized off the figure rather than fixed, so it tracks the
   image at every breakpoint. Sits behind the whole stage, so it does
   not tilt with the face -- a light source that swings with the object
   it lights is the tell that a 3D effect is fake. */
.hero-figure::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 92%;
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(circle, rgba(37, 99, 235, .28) 0%, rgba(37, 99, 235, .10) 45%, transparent 70%);
  filter: blur(18px);
  pointer-events: none;
}

/* Scroll layer. */
.hero-figure__parallax {
  transform: translate3d(0, var(--fig-shift, 0px), 0) scale(var(--fig-scale, 1));
  will-change: transform;
}

/* Cursor layer. translate3d rather than translate so the whole subtree
   stays on the compositor -- a 2D translate here drops it back to
   painting on every frame, which is where the jitter comes from. */
.hero-figure__stage {
  transform-style: preserve-3d;
  transform:
    rotateX(var(--fig-rx, 0deg))
    rotateY(var(--fig-ry, 0deg))
    translate3d(var(--fig-tx, 0px), var(--fig-ty, 0px), 0);
  will-change: transform;

  /* Keeps the rotated bitmap crisp instead of letting the browser
     re-sample it at a lower quality mid-rotation. */
  backface-visibility: hidden;
}

.hero-figure__img {
  position: relative;
  width: 640px;
  height: 640px;

  /* The source is 920x890, so a literal 640x640 would squash it by 3%.
     object-fit keeps the aspect inside the box instead. The letterboxing
     that leaves is invisible: the image is cut out on transparency and
     the box has no background of its own. */
  object-fit: contain;

  /* Below 1400px the figure column is narrower than 640 and a fixed
     width would push the page sideways, so it falls back to the column.
     Above it there is enough gutter beside the 1240 container for the
     art to sit at a true 640 and bleed a little, which is what hero
     artwork is supposed to do. */
  max-width: 100%;

  /* The lower edge is a hard crop across the shoulders. Fading the
     bottom 18% stops that straight line reading as a seam. */
  -webkit-mask-image: linear-gradient(to bottom, #000 78%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 78%, transparent 100%);
}

@media (min-width: 1400px) {
  .hero-figure__img { max-width: none; }
}

@keyframes hero-figure-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* The idle float, on its own layer so it composes with the two the
   script drives instead of overwriting them. Long and small: enough to
   stop the figure looking pasted on, not enough to pull the eye off
   the h1. */
@media (prefers-reduced-motion: no-preference) {
  .hero-figure__img { animation: hero-figure-float 9s ease-in-out infinite; }
}
@keyframes hero-figure-float {
  0%, 100% { transform: translate3d(0, 0, 0); }
  50%      { transform: translate3d(0, -10px, 0); }
}

/* Reduced motion: no fade, no float, and the script returns before it
   writes a single property, so every default above stands. */
@media (prefers-reduced-motion: reduce) {
  .hero-figure { animation: none; }
  .hero-figure__parallax,
  .hero-figure__stage { will-change: auto; }
}

/* ── Why choose us: the spreading discs ──────────────────────────
   Three circles that start on top of one another and open out as the
   band scrolls in. The row is the same argument the philosophy band
   draws as a Venn: what the machine brings, what the people bring, and
   the thing they resolve into. Here it is dealt rather than diagrammed,
   and the middle disc is the one that holds still while the other two
   come out from under it.

   THEY OVERLAP WHEN OPEN, by design. Each disc is wider than the grid
   column it sits in and pulled back by half the difference, so
   neighbours intersect the way two sets do. THE COLUMN PITCH IS
   UNCHANGED by that trick, which matters: the script measures the
   pitch to work out how far to slide, and a disc that overflowed its
   column without keeping its centre would stack off-target.

   --spread DEFAULTS TO 0 HERE, and 0 is the open layout. The band ships
   correct and the script only ever animates towards a position the
   reader would otherwise have seen immediately. No script, no rAF,
   reduced motion, below lg: three discs in a row, fully legible,
   nothing hidden behind anything.
   ───────────────────────────────────────────────────────────────── */
.spread {
  --spread: 0px;

  /* NARROWER THAN THE CONTAINER. The row is capped and centred rather
     than filling the 1240 band, which is the only lever that changes
     the disc size: everything else here is a percentage of the column.
     The script measures row.clientWidth, so the stacking distance
     follows this number automatically and stays exact. */
  max-width: 1040px;
  margin-inline: auto;

  /* GAP MUST BE 0. The overlap is produced by the discs being wider
     than their columns, not by a negative gap (which CSS has no such
     thing as). The script reads column-gap when it measures the pitch,
     so a gap here would be counted twice. */
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0;
  align-items: center;
}

.spread__card {
  position: relative;

  /* 1.16 columns wide, pulled back 8% each side: the disc keeps the
     column's centre, so the pitch the script measures still lands one
     disc exactly on the next. */
  width: 116%;
  margin-inline: -8%;

  aspect-ratio: 1;
  border-radius: 50%;
  display: flex;
  overflow: hidden;
}

/* THE MIDDLE DISC SITS ON TOP AND NEVER MOVES. That is what lets the
   other two start completely hidden underneath it: the script sets
   --spread to exactly one column pitch, which puts all three on the
   same centre, and this z-index decides which one you see. */
.spread__card--c { z-index: 2; }

/* The travelling pair. translate3d, not translateX: they carry a
   gradient each, and keeping them on the compositor is the difference
   between a smooth open and a repaint on every scroll frame.

   --spread-p runs 0 (stacked) to 1 (open), so the pair also grows the
   last few per cent as it emerges. Slightly under the middle disc
   while stacked, which both hides them completely and gives the deck
   some depth as it opens. */
.spread__card--l,
.spread__card--r {
  z-index: 1;
  transform:
    translate3d(calc(var(--spread) * var(--dir, 0)), 0, 0)
    scale(calc(0.94 + 0.06 * var(--spread-p, 1)));
  will-change: transform;
}

/* SIGNS ARE TOWARDS THE CENTRE, not away from it. --spread is the
   distance still to be travelled, so at the start the left disc has to
   be pushed RIGHT to sit on the middle one, and the right disc LEFT.
   Flipping these sends the pair off the sides of the screen instead of
   under the middle disc, which still looks deliberate and is not the
   effect. */
.spread__card--l { --dir:  1; }
.spread__card--r { --dir: -1; }

/* The three skins. A violet field, the near-black one and a pale one,
   so the row reads as three things rather than as a repeated shape.
   All CSS: these are soft gradients with no detail in them, and
   shipping that as artwork would be a few hundred KB stretched to disc
   width. The hairline ring is what keeps a disc legible where it
   overlaps its neighbour. */
.spread__card--violet {
  background:
    radial-gradient(90% 90% at 30% 25%, rgba(88, 60, 190, .55) 0%, transparent 70%),
    linear-gradient(160deg, #2E1D6B 0%, #1B1145 60%, #120C30 100%);
  box-shadow: inset 0 0 0 1px rgba(192, 132, 252, .35);
  color: #fff;
}
.spread__card--ink {
  background:
    radial-gradient(90% 90% at 50% 20%, rgba(255, 255, 255, .07) 0%, transparent 70%),
    linear-gradient(160deg, #16181F 0%, #08090d 70%);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .14);
  color: #fff;
}
.spread__card--light {
  background:
    radial-gradient(90% 90% at 70% 25%, rgba(128, 194, 65, .16) 0%, transparent 70%),
    linear-gradient(160deg, #FFFFFF 0%, #F4F5F2 100%);
  /* A shade stronger than the other two rings. The pale disc is
     near-white on a light band, so it is the only one whose edge needs
     the ring to be readable rather than decorative. */
  box-shadow: inset 0 0 0 1px rgba(18, 20, 29, .18);
  color: #12141D;
}

/* ── The row on a #08090d band ───────────────────────────────────
   THE DIAGRAM TREATMENT. A thin coloured ring over an almost
   transparent tint, which is what the Venn these discs replaced looked
   like. It is the ring, not the fill, that makes a circle read as a
   set: give the fill any real weight and three sets become three
   tiles that happen to be round.

   The skins above are the light-band versions and still apply if this
   row is ever rendered on one; everything here overrides them.

   THE MIDDLE DISC IS THE ONE EXCEPTION AND IT MUST STAY OPAQUE. It is
   what hides the other two while they are stacked behind it. Made
   translucent to match, they show straight through and the open reads
   as three ghosts sliding apart. It is set at the page black with a
   neutral ring, so it passes for the dark lens where the two sets
   cross rather than looking like a third, heavier object. */
.spread--dark .spread__card--violet {
  background: radial-gradient(80% 80% at 40% 30%, rgba(124, 92, 235, .13) 0%, rgba(88, 60, 190, .05) 55%, transparent 80%);
  box-shadow: inset 0 0 0 1px rgba(167, 139, 250, .55);
}
.spread--dark .spread__card--ink {
  background: linear-gradient(160deg, #0C0E14 0%, #08090d 70%);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .13);
}
.spread--dark .spread__card--light {
  background: radial-gradient(80% 80% at 60% 30%, rgba(128, 194, 65, .12) 0%, rgba(128, 194, 65, .04) 55%, transparent 80%);
  box-shadow: inset 0 0 0 1px rgba(128, 194, 65, .50);
  color: #fff;
}
.spread--dark .spread__card--light .spread__desc { color: rgba(255, 255, 255, .55); }

/* The middle term in lime, the way the Venn set it. It is the thing
   the other two resolve into, and the only colour in the row that is
   not carried by a ring. */
.spread--dark .spread__card--ink .spread__label { color: #80C241; }

/* ── A disc with a photograph in it ──────────────────────────────
   The image fills the disc and a scrim sits between it and the copy.
   Two separate layers on purpose: the photograph keeps its own
   opacity, and the scrim keeps the skin's colour, so a disc still
   reads violet or lime rather than becoming a grey circle with a
   picture in it.
   ───────────────────────────────────────────────────────────────── */
.spread__photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: .38;
}

/* The scrim. Weakest at the edge and strongest in the middle, which is
   the opposite of the usual bottom-up gradient: the copy on these
   discs is centred, not stacked at the foot, so that is where the
   contrast has to be. */
.spread__card--photo::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at 50% 50%,
    rgba(8, 9, 13, .86) 0%, rgba(8, 9, 13, .70) 55%, rgba(8, 9, 13, .42) 100%);
  pointer-events: none;
}

/* Each skin keeps its hue over the photograph. Without this the two
   photographed discs converge on the same grey and the violet/lime
   pairing the band is built on disappears. */
.spread--dark .spread__card--photo.spread__card--violet::after {
  background:
    radial-gradient(circle at 50% 50%, rgba(26, 16, 62, .84) 0%, rgba(18, 12, 48, .68) 55%, rgba(18, 12, 48, .40) 100%);
}
.spread--dark .spread__card--photo.spread__card--light::after {
  background:
    radial-gradient(circle at 50% 50%, rgba(10, 22, 8, .84) 0%, rgba(12, 24, 10, .68) 55%, rgba(12, 24, 10, .40) 100%);
}

/* Copy is centred in the disc, both axes, and kept well inside the
   curve: a line that runs to the edge of a circle reads as clipped
   even when it is not, and the outer thirds are where the discs
   overlap.

   z-index lifts it over the photograph and its scrim; without it the
   scrim paints on top of the words. */
.spread__body {
  position: relative;
  z-index: 1;
  display: flex;
  flex: 1;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 0 16%;
  text-align: center;
}

.spread__mark {
  width: 132px;
  height: auto;
  margin-bottom: 18px;
}

.spread__figure {
  font-family: 'Outfit', sans-serif;
  font-size: 44px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -1.5px;
}

.spread__label {
  margin-top: 10px;
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -0.3px;
}

/* A disc with no figure. The heading takes the figure's weight so it
   anchors the circle instead of floating in it.

   INTER, NOT OUTFIT. Only two families are loaded on this site, and
   they are not interchangeable: Outfit is the numeral face, used for
   stat figures, hex badges and the hero counters, and Inter sets every
   heading including the .section-title directly above this band. These
   three are statements, not numbers, so they belong in the heading
   face. Left in Outfit they read as three numbers that happen to be
   spelled out. Inherited rather than declared, so the family follows
   the Tailwind sans stack if it is ever changed. */
.spread__card--titled .spread__label {
  margin-top: 0;
  font-size: 30px;
  font-weight: 600;
  line-height: 1.16;
  letter-spacing: -0.9px;
}
@media (min-width: 1280px) {
  .spread__card--titled .spread__label { font-size: 36px; letter-spacing: -1.2px; }
}

/* The second heading and the paragraph, on a disc that carries them.
   Separated from the caption above by a hairline rather than by space
   alone: inside a circle there is not enough room to open a gap wide
   enough to read as a division. */
.spread__lead {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid rgba(255, 255, 255, .16);
  font-size: 16px;
  font-weight: 600;
  letter-spacing: -0.2px;
}

.spread__copy {
  margin-top: 8px;
  font-size: 13.5px;
  line-height: 20px;
  color: rgba(255, 255, 255, .60);
}

/* A disc carrying the full block. The type steps down across the board
   because a circle gives up width fast above and below its middle, and
   copy set at the row's normal size runs into the curve. */
.spread__card--full .spread__label { font-size: 26px; letter-spacing: -0.7px; }
@media (min-width: 1280px) {
  .spread__card--full .spread__label { font-size: 30px; letter-spacing: -0.9px; }
}
.spread__card--full .spread__body { padding: 0 13%; }

/* The middle disc carries the logo as well as the full block, which is
   more than the other two hold. The mark gives up the width rather
   than the words. */
/* The middle disc holds the logo on top of the full block, so it is
   always the tightest of the three against the curve. The mark gives
   up the room rather than the words. */
.spread__card--full .spread__mark { width: 88px; margin-bottom: 10px; }
.spread__card--full .spread__desc { font-size: 13.5px; }

.spread__desc {
  margin-top: 12px;
  font-size: var(--fs-card-desc);
  line-height: var(--lh-card-desc);
  color: rgba(255, 255, 255, .62);
}
.spread__card--light .spread__desc { color: #64748B; }

/* Below lg the discs stack vertically, so there is no centre to open
   out from and nothing to travel. --dir is zeroed rather than the
   script being gated on width alone, so a resize past the breakpoint
   cannot leave a disc parked off to one side. A circle also wastes a
   phone's width, so they become rounded panels again. */
@media (max-width: 1023px) {
  .spread { grid-template-columns: minmax(0, 1fr); gap: 18px; }
  .spread__card {
    width: 100%;
    margin-inline: 0;
    aspect-ratio: auto;
    min-height: 300px;
    border-radius: var(--r-panel);
    --dir: 0;
  }
  .spread__card--l,
  .spread__card--r { transform: none; }
  .spread__body { padding: 32px 28px; }
}

@media (prefers-reduced-motion: reduce) {
  .spread__card--l,
  .spread__card--r { will-change: auto; transform: none; }
}

/* ── How we work: the ruled feature grid ─────────────────────────
   Four gates in a 2x2 beside the section's copy, after the pattern in
   the reference: icon tile, number, title, description.

   THE RULES ARE BORDERS ON THE CELLS. Each cell carries a top and a
   left hairline, which produces the line down the left of the grid
   (separating it from the copy column), the cross through the middle
   and the line along the top, all from two declarations. The grid
   closes itself with a bottom border rather than the cells carrying
   one and doubling up in the middle.
   ───────────────────────────────────────────────────────────────── */
.mfeat {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  border-bottom: 1px solid var(--line-light);
}

.mfeat__item {
  padding: 34px 30px;
  border-top: 1px solid var(--line-light);
  border-left: 1px solid var(--line-light);
}

/* The icon plate. Light on light, so it needs its own hairline to read
   as a tile rather than as a stain; limeInk because the brand lime
   sits at about 2:1 on white and fails contrast. */
.mfeat__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  border-radius: var(--r-sm);
  border: 1px solid var(--line-light);
  background: #F4F6F1;
  color: #4E7D22;
}

.mfeat__num {
  margin-top: 22px;
  font-family: 'Outfit', sans-serif;
  font-size: var(--fs-meta);
  font-weight: 700;
  letter-spacing: 1px;
  color: #4E7D22;
}

.mfeat__title {
  margin-top: 8px;
  font-size: var(--fs-card-title);
  font-weight: 600;
  line-height: var(--lh-card-title);
  letter-spacing: -0.2px;
}

.mfeat__desc {
  margin-top: 10px;
  font-size: var(--fs-card-desc);
  line-height: var(--lh-card-desc);
  color: #64748B;
}

/* One column below lg. The left border goes with the second column, so
   the stack is ruled top to bottom and not down one side. */
@media (max-width: 1023px) {
  .mfeat { grid-template-columns: minmax(0, 1fr); }
  .mfeat__item { padding: 28px 0; border-left: 0; }
}

/* ── Hero robot ───────────────────────────────────────────────────
   A cursor-tracked head turn beside the hero copy. The markup is in
   include/sections/hero.php and the scrubbing is in footer.php; this
   is only how it sits and how it blends.

   From lg the copy is capped so the two never overlap. Below lg the
   robot is not rendered at all: the hero already carries a pill, an
   H1, a lead, two buttons and four stats on a narrow screen, and a
   head that cannot be pointed at earns none of that room. */
.hero-copy { max-width: 100%; }

.hero-bot { display: none; }

@media (min-width: 1024px) {
  .hero-copy { max-width: 58%; }

  .hero-bot {
    display: block;
    position: absolute;
    top: 50%;
    right: 0;
    /* Optical centring, not geometric: the head sits in the upper half
       of every frame, so a true 50% leaves it riding high. */
    transform: translateY(-46%);
    width: clamp(320px, 33vw, 470px);
    pointer-events: none;   /* the hero's own links stay clickable */
  }
}

@media (min-width: 1280px) { .hero-bot { right: -12px; } }

.hero-bot__stage {
  position: relative;
  aspect-ratio: 1 / 1;
  /* The cut-out is baked into the sheet as alpha, not done here with
     mix-blend-mode. Blending was the first attempt and it cannot work
     in this position: the hero <main> is a positioned, z-indexed
     stacking context, so a blend inside it has nothing behind it to
     blend with and the frame paints as a black box. See the note in
     tools/build-robot-sheet.sh. */
  /* The shoulders run to the bottom edge of every frame, so that edge
     is faded rather than cut. */
  -webkit-mask-image: linear-gradient(to bottom, #000 78%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 78%, transparent 100%);
}

.hero-bot__idle,
.hero-bot__film {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.hero-bot__idle { object-fit: cover; }

/* Width is 31 cells wide (the strip), height is one cell. The script
   sets background-image and steps background-position-x. */
.hero-bot__film {
  background-repeat: no-repeat;
  background-size: 3100% 100%;   /* 31 cells */
  background-position: 0 0;
}

/* The swap from still to strip, once the strip has decoded. Opacity
   only, so nothing reflows. */
.hero-bot__idle { transition: opacity .35s ease; }
.hero-bot.is-live .hero-bot__idle { opacity: 0; }

/* A slow float, so the head is not dead still while the cursor is
   elsewhere. Deliberately tiny: 6px over 7s. */
@media (min-width: 1024px) {
  .hero-bot__stage { animation: bot-float 7s ease-in-out infinite; }
}
@keyframes bot-float {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-6px); }
}

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