/* ============================================================
   RSQUARE TECHNOLOGIES — Design System v3 "Clean Dynamic"
   Deep-space navy base · Electric azure · Signature red
   Tokenized motion/radius/elevation · no decorative glow
   ============================================================ */

:root {
  /* Brand core (evolved from legacy #2D85C6 / #1C2B3C / #CD1227) */
  --ink-950: #060B14;      /* near-black navy — page dark base */
  --ink-900: #0A1220;
  --ink-800: #101B2E;
  --ink-700: #1C2B3C;      /* legacy navy, kept as mid tone */
  --ink-600: #2C3E54;

  --azure-600: #1E6FD9;
  --azure-500: #2D85C6;    /* legacy azure, anchor */
  --azure-400: #38A4F0;
  --azure-300: #6EC1F7;
  --cyan-400: #22D3EE;

  --red-500: #E5122B;      /* signature R red, brightened */
  --red-400: #FF3B52;

  --paper: #F7F9FC;
  --white: #FFFFFF;
  --mist: #A8B6C8;         /* muted text on dark — 9.1:1 on --ink-900 */
  /* Was #56677D, which measured 3.24:1 on --ink-900 — under the 4.5:1 WCAG
     AA floor for body text, and this token carries every footer link, the
     marquee label and the carousel hint. #74869C clears AA on all three
     dark surfaces (5.03 / 5.29 / 4.62 on ink-900 / 950 / 800) while staying
     a full step below --mist, so the two-tier muted hierarchy is preserved.
     Financial-sector procurement almost always runs an accessibility scan. */
  --slate: #74869C;        /* secondary muted text — 5.03:1 on --ink-900 */

  /* hairline + surface tokens (one border language everywhere) */
  --line: rgba(148, 170, 197, 0.11);
  --line-strong: rgba(148, 170, 197, 0.22);
  --surface: var(--ink-900);
  --surface-raised: var(--ink-800);

  /* single elevation scale — neutral black, no colored glows */
  --shadow-1: 0 2px 8px -2px rgba(0, 0, 0, 0.35);
  --shadow-2: 0 16px 40px -16px rgba(0, 0, 0, 0.55);

  --grad-hero: linear-gradient(135deg, #38A4F0 0%, #2D85C6 40%, #22D3EE 100%);
  --grad-text: linear-gradient(100deg, #6EC1F7, #22D3EE 55%, #38A4F0);

  /* Typeface — Poppins (Latin) + IBM Plex Sans Arabic (Arabic).
     Poppins is fixed by the wider brand system: the pitch decks and other
     client-facing materials are all set in it, so the site matches them
     rather than the other way round.
     The Arabic face is a separate matter. Poppins has no Arabic coverage at
     all, so the site's AR/RTL mode — which ships a complete translation of
     every page — was falling back to whatever unstyled face the visitor's OS
     happened to supply. IBM Plex Sans Arabic gives Arabic a real, branded
     typeface at matching weights and vertical proportion, so EN and AR read
     as one system instead of one designed page and one default one. */
  --font: "Poppins", "Segoe UI", -apple-system, system-ui, sans-serif;
  --font-ar: "IBM Plex Sans Arabic", "Poppins", "Segoe UI", system-ui, sans-serif;
  --font-num: "Poppins", "Segoe UI", -apple-system, system-ui, sans-serif;

  /* radius scale */
  --r-sm: 8px;
  --r-md: 12px;
  --r-lg: 16px;
  --r-xl: 24px;
  /* Controls (buttons, the language toggle, the burger) sat on a hardcoded
     10px in five separate places, with the traveling-border ring hardcoding
     10.5px/9.5px to match its 0.5px insets. One token keeps them locked
     together — the ring's two values are now derived from it. */
  --r-ctl: 10px;

  /* motion tokens — one rhythm across the whole site */
  --dur-1: 160ms;   /* micro: hover, press */
  --dur-2: 280ms;   /* standard: menus, toggles */
  --dur-3: 520ms;   /* large: reveals, slide swaps */
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);

  --nav-h: 76px;

  /* Tells the engine this is a dark UI, so natively-rendered chrome —
     scrollbars, form controls, spinners, the flash before first paint —
     comes through dark instead of the OS light default. On Windows the
     light scrollbar against this palette was the single most obvious
     "unfinished" tell on an otherwise very dark site. */
  color-scheme: dark;
}

/* Reserve the scrollbar's width at all times so pages that do and don't
   overflow don't shift horizontally relative to each other during
   navigation — noticeable when clicking between the short sub-pages and
   the long product pages. */
html { scrollbar-gutter: stable; }

/* ---------- cross-document view transitions ----------
   This is a 28-page static site, so every nav click is a full document load
   and, by default, a hard white-to-dark flash. Opting into cross-document
   view transitions lets supporting browsers cross-fade between pages
   instead, which is what makes a multi-page marketing site feel like the
   single application the product actually is. Progressive enhancement:
   browsers without support simply navigate as before, and the whole thing
   is skipped for users who asked for reduced motion. */
@view-transition { navigation: auto; }
::view-transition-old(root), ::view-transition-new(root) {
  animation-duration: 260ms;
  animation-timing-function: var(--ease-out);
}
/* The nav and footer are identical on every page — tagging them keeps them
   visually anchored while only the page body cross-fades underneath. */
.nav { view-transition-name: site-nav; }
footer { view-transition-name: site-footer; }
@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
}

* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
/* Lenis overrides browser smooth-scroll to prevent double-smoothing */
html.lenis, html.lenis body { height: auto; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-stopped { overflow: hidden; }
body {
  font-family: var(--font);
  background: var(--ink-950);
  color: var(--white);
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  /* Every stat on this site counts up digit-by-digit, so any width
     difference between figures makes the number visibly jitter mid-count.
     Harmless no-op if the active face has no tabular set. */
  font-variant-numeric: tabular-nums;
}
/* Arabic swaps the Latin face for its Arabic counterpart at the root, so
   every descendant inherits it — including the nav/footer markup shared by
   all 28 pages and the runtime-built mobile drawer. */
html[lang="ar"] body { font-family: var(--font-ar); }
img { max-width: 100%; display: block; }

/* ---------- RTL (Arabic) ----------
   direction:rtl on its own mirrors every flex/grid layout on the site
   (nav order, card grids, footer columns, etc.) since the browser's own
   flex/grid algorithms are direction-aware — the one thing that does NOT
   auto-flip is anything using a hardcoded physical left/right (not a
   logical property), like the nav underline's absolute positioning below.
   Known gap for a later pass: a few purely decorative bits (progress bar
   fill direction, marquee scroll direction) still visually read
   left-to-right in RTL; not a correctness issue, just not mirrored yet. */
html[dir="rtl"] body { direction: rtl; }
html[dir="rtl"] .nav-links > li > a::after { left: auto; right: 0; }
a { color: inherit; text-decoration: none; }
.container { width: min(1180px, 92vw); margin: 0 auto; }

::selection { background: var(--azure-500); color: #fff; }

/* ---------- focus system ----------
   The site previously had no focus styling of any kind, so keyboard users
   got only the UA default — which is invisible against this palette on the
   many elements that set their own background. :focus-visible (not :focus)
   means pointer users never see a ring, so this costs the visual design
   nothing while making every control reachable and legible via keyboard.
   One ring token, used everywhere, so focus reads as a single language the
   way the border and elevation scales already do. */
:root { --focus-ring: 0 0 0 2px var(--ink-950), 0 0 0 4px var(--azure-400); }
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
  border-radius: var(--r-sm);
  /* Keep the ring clear of the element's own edge without shifting layout. */
  position: relative; z-index: 5;
}
/* Controls that already own a box-shadow or a non-default radius need the
   ring composed onto their existing treatment rather than replacing it. */
.btn:focus-visible { border-radius: var(--r-ctl); box-shadow: var(--focus-ring); }
.testi2-btn:focus-visible { border-radius: 50%; }
.dd-card:focus-visible { border-radius: var(--r-md); }
.prod-item:focus-visible, .sol-card .learn:focus-visible { border-radius: var(--r-lg); }

/* Skip link — first tab stop on every page, visually hidden until focused. */
.skip-link {
  position: fixed; top: 10px; left: 10px; z-index: 1200;
  padding: 12px 20px; border-radius: var(--r-sm);
  background: var(--azure-600); color: #fff;
  font-size: 14px; font-weight: 500;
  transform: translateY(-160%);
  transition: transform var(--dur-2) var(--ease-out);
}
.skip-link:focus-visible { transform: none; box-shadow: var(--focus-ring); }
html[dir="rtl"] .skip-link { left: auto; right: 10px; }

/* ---------- scroll progress bar ---------- */
#progress {
  position: fixed; top: 0; left: 0; height: 2px; width: 100%;
  background: var(--grad-hero); z-index: 1000;
  transform: scaleX(0); transform-origin: left center; will-change: transform;
}

/* ---------- nav ---------- */
.nav {
  position: fixed; top: 0; left: 0; right: 0; z-index: 900;
  height: var(--nav-h);
  display: flex; align-items: center;
  transition: background-color var(--dur-3) var(--ease-out), box-shadow var(--dur-3) var(--ease-out), height var(--dur-2);
}
.nav.scrolled {
  height: 62px;
  background-color: rgba(6, 11, 20, 0.94);
  box-shadow: 0 1px 0 var(--line);
}
.nav-inner { display: flex; align-items: center; justify-content: space-between; width: min(1280px, 94vw); margin: 0 auto; }
.nav-logo img { height: 30px; filter: brightness(0) invert(1); opacity: 0.95; }
.nav-links { position: relative; display: flex; gap: 34px; list-style: none; font-size: 14.5px; font-weight: 500; color: var(--mist); }
.nav-links > li > a {
  position: relative; padding: 6px 0;
  transition: color var(--dur-1); display: inline-flex; align-items: center; gap: 6px;
}
.nav-links > li > a:hover { color: #fff; }
.nav-links > li > a::after {
  content: ""; position: absolute; left: 0; bottom: 0; height: 1.5px; width: 0;
  background: var(--azure-400); transition: width var(--dur-2) var(--ease-out);
}
.nav-links > li > a:hover::after { width: 100%; }

/* ---------- nav dropdowns ----------
   Full-width panel attached flush under the nav bar, holding a grid of
   rounded cards (icon + name + one-line description). Panels anchor to
   .nav (fixed), not the <li>, so they span the bar; <li>s are full nav
   height so the hover never breaks between trigger and panel. */
.nav-inner { height: 100%; }
.nav-links { height: 100%; }
.nav-links > li { height: 100%; display: flex; align-items: center; }
.has-dd { position: static; }
.has-dd .chev { color: var(--red-500); transition: transform var(--dur-2) var(--ease-out); flex-shrink: 0; }
.has-dd.active .chev, .has-dd:hover .chev { transform: rotate(180deg); }

/* solidify the bar whenever a menu is open so panel + nav read as one module */
.nav:has(.has-dd:hover), .nav.dd-open {
  background-color: rgba(6, 11, 20, 0.97);
  box-shadow: 0 1px 0 var(--line);
}

/* ---------- shared morphing popover ----------
   One popover box under the bar, shared by every menu — JS (see "nav
   dropdowns" in main.js) measures the active pane and the hovered trigger,
   then sets this box's width/height/translateX; the CSS transitions below
   are what produce the "panel morphs between sections" effect the
   reference component got from framer-motion's layoutId. Content panes
   (.dd-pane, one per menu, all children of the popover) cross-fade while
   the box resizes around them. Sized to content (width: max-content on
   panes) instead of the old full-viewport-width panel — which is also what
   fixes the cut-off content: the old left:0/right:0 panel clipped any pane
   content wider than the bar's inner column. */
.dd-popover {
  position: absolute; top: 100%; left: 0;
  border-radius: var(--r-lg);
  border: 1px solid var(--line);
  background: rgba(6, 11, 20, 0.97);
  box-shadow: var(--shadow-2);
  overflow: hidden;
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: width var(--dur-3) var(--ease-out), height var(--dur-3) var(--ease-out),
              transform var(--dur-3) var(--ease-out),
              opacity 200ms var(--ease-out), visibility 200ms;
  z-index: 50;
}
.dd-popover.open { opacity: 1; visibility: visible; pointer-events: auto; }
/* geometry set instantly on first open (else the box would slide in from
   its stale last-closed position); only the fade animates */
.dd-popover.no-anim { transition: opacity 200ms var(--ease-out), visibility 200ms; }
.dd-pane {
  position: absolute; top: 0; left: 0;
  width: max-content; margin: 0; padding: 22px 24px;
  opacity: 0; pointer-events: none;
  transition: opacity 180ms var(--ease-out);
}
.dd-pane.active { opacity: 1; pointer-events: auto; }

/* card grid */
.dd-cards { display: grid; gap: 14px; }
.dd-cards.cols-2 { grid-template-columns: repeat(2, 1fr); }
.dd-cards.cols-3 { grid-template-columns: repeat(3, 1fr); }
.dd-cards.cols-4 { grid-template-columns: repeat(4, 1fr); }
/* Row layout (icon box left, name+desc stacked right) — ported from a
   mega-menu reference component, adapted to brand: our icons are colorful
   isometric illustrations (not single-color glyphs), so the reference's
   "invert to solid white bg + dark icon" hover doesn't translate; instead
   the icon box border brightens and a soft azure glow fades in behind it,
   consistent with the glow language used elsewhere on the site. No HTML
   changes needed — dd-ico/dd-name/dd-desc are already siblings in this
   order, grid placement alone reflows them into a row. */
.dd-card {
  display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto;
  column-gap: 12px; row-gap: 2px; align-items: start; align-content: start;
  padding: 10px 8px; border-radius: var(--r-md);
  background: transparent; border: 1px solid transparent;
  transition: background var(--dur-1), transform var(--dur-2) var(--ease-out);
}
.dd-card:hover { background: rgba(255,255,255,0.04); }
.dd-ico {
  grid-row: 1 / 3; grid-column: 1;
  width: 42px; height: 42px; display: grid; place-items: center;
  border-radius: var(--r-sm); border: 1px solid var(--line-strong);
  background: rgba(255,255,255,0.03);
  transition: border-color var(--dur-2) var(--ease-out), background var(--dur-2) var(--ease-out), box-shadow var(--dur-2) var(--ease-out);
}
.dd-card:hover .dd-ico {
  border-color: rgba(56,164,240,0.55);
  background: rgba(56,164,240,0.14);
  box-shadow: 0 0 0 4px rgba(56,164,240,0.1);
}
.dd-ico svg { width: 16px; height: 16px; color: var(--azure-300); }
.dd-ico img { width: 42px; height: 42px; object-fit: contain; transition: transform var(--dur-2) var(--ease-out); }
.dd-card:hover .dd-ico img { transform: scale(1.08); }
.dd-ico.dd-ico-lg img { width: 63px !important; height: 63px !important; }
.dd-name { grid-row: 1; grid-column: 2; width: 190px; font-size: 14.5px; font-weight: 500; color: #fff; letter-spacing: -0.01em; }
/* Fixed width (not just a cap) forces the description to wrap during
   width:max-content measurement too — without it, "max-content" measures
   each description as one unwrapped line, and a long line like "LLM
   integrations, RAG, and automated workflows." balloons the whole card
   (and therefore the whole popover, and therefore how much room there is
   to center it) out to 280-300px+ instead of the intended compact width. */
.dd-desc { grid-row: 2; grid-column: 2; width: 190px; font-size: 12.5px; color: var(--mist); line-height: 1.45; transition: color var(--dur-1); }
.dd-card:hover .dd-desc { color: var(--mist); }

/* products mega: 4 labeled columns of compact cards */
.dd-mega-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px; }
.dd-group { display: flex; flex-direction: column; gap: 10px; }
.dd-group h4 {
  font-size: 11.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--azure-300);
  font-weight: 500; padding: 0 2px 2px;
}
.dd-card.sm { padding: 8px 6px; column-gap: 10px; row-gap: 1px; }
/* Solutions dropdown only — bumped to match the row-gap already used by
   the base (non-.sm) card variant elsewhere in the dropdown system. */
.dd-popover .dd-pane:first-child .dd-card.sm { row-gap: 2px; }
.dd-card.sm .dd-ico { width: 34px; height: 34px; }
.dd-card.sm .dd-ico svg { width: 13px; height: 13px; }
.dd-card.sm .dd-ico img { width: 34px; height: 34px; }
/* dd-ico-lg's 63px override (below) was sized for the larger 42px-icon
   context (Solutions/Products/Industries "regular" cards) — inside a
   compact .sm card (34px icon box) that blows the icon past the box and
   it gets clipped by the popover's overflow:hidden. This scoped override
   gives .sm cards a smaller, proportionate boost instead. */
.dd-card.sm .dd-ico.dd-ico-lg img { width: 40px !important; height: 40px !important; transform: translateY(-4px); }
.dd-card.sm .dd-name { width: 150px; font-size: 13.5px; }
.dd-card.sm .dd-desc { width: 150px; font-size: 11.5px; }

.dd-all {
  display: inline-flex; align-items: center; gap: 8px;
  margin-top: 18px; padding: 10px 18px; border-radius: var(--r-sm);
  font-size: 13.5px; font-weight: 500; color: var(--azure-300);
  background: rgba(56,164,240,0.06); border: 1px solid rgba(56,164,240,0.18);
  transition: background var(--dur-1), color var(--dur-1);
}
.dd-all:hover { color: #fff; background: rgba(56,164,240,0.14); }

/* ---------- buttons — flat, rectangular, one elevation ---------- */
.btn {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 13px 26px; border-radius: var(--r-ctl);
  font-weight: 300; font-size: 15px; border: none; cursor: pointer;
  transition: background var(--dur-1), border-color var(--dur-1), transform var(--dur-1) var(--ease-out), box-shadow var(--dur-1);
}
.btn:active { transform: scale(0.98); }
.btn-primary {
  background: var(--azure-600); color: #fff;
  box-shadow: var(--shadow-1);
}
.btn-primary:hover { background: var(--azure-500); transform: translateY(-1px); box-shadow: var(--shadow-2); }
.btn-ghost {
  background: transparent; color: #fff;
  border: 1px solid var(--line-strong);
}
.btn-ghost:hover { border-color: var(--azure-400); background: rgba(56,164,240,0.06); transform: translateY(-1px); }
.nav .btn { padding: 10px 22px; font-size: 14px; }

/* ── Traveling border button ── */
@property --travel-angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}
.btn-travel {
  position: relative;
  background: transparent;
  color: #fff;
  border: none;
  isolation: isolate;
}
.btn-travel::before {
  content: '';
  position: absolute;
  inset: -0.5px;
  border-radius: calc(var(--r-ctl) + 0.5px);
  background: conic-gradient(
    from var(--travel-angle),
    #0a1628 0deg,
    #0a1628 215deg,
    #1a5fa8 230deg,
    #22D3EE 245deg,
    #1a5fa8 260deg,
    #0a1628 275deg,
    #0a1628 360deg
  );
  animation: btn-border-travel 6s linear infinite;
  z-index: -1;
}
.btn-travel::after {
  content: '';
  position: absolute;
  inset: 0.5px;
  border-radius: calc(var(--r-ctl) - 0.5px);
  background: #060B14;
  z-index: -1;
}
.btn-travel:hover { transform: translateY(-1px); }
.btn-travel:hover::before { animation-duration: 1.5s; }
/* Pill variant of the traveling border — same trace, but a 999px radius
   on both the conic-gradient ring and its inner mask so it wraps a
   fully-rounded badge (not a 10px-radius button) without squaring off
   the ends. Reuses --travel-angle and the keyframe below as-is. */
.badge-travel { position: relative; isolation: isolate; border-color: transparent !important; }
.badge-travel::before {
  content: ''; position: absolute; inset: -0.5px; border-radius: 999px;
  background: conic-gradient(
    from var(--travel-angle),
    #0a1628 0deg,
    #0a1628 215deg,
    #1a5fa8 230deg,
    #22D3EE 245deg,
    #1a5fa8 260deg,
    #0a1628 275deg,
    #0a1628 360deg
  );
  animation: btn-border-travel 6s linear infinite;
  z-index: -1;
}
.badge-travel::after {
  content: ''; position: absolute; inset: 0.5px; border-radius: 999px;
  background: var(--surface-raised);
  z-index: -1;
}
@keyframes btn-border-travel {
  to { --travel-angle: 360deg; }
}
/* Animating a conic-gradient means repainting the element every frame — a
   registered custom property can't be composited the way a transform can.
   That is affordable for a button-sized box, but there is no reason to pay
   it while the button is off-screen, which is most of the time on the long
   product pages. main.js observes these and toggles the class below. */
.anim-paused, .anim-paused::before, .anim-paused::after { animation-play-state: paused !important; }

/* ---------- language switch ---------- */
.lang-switch {
  /* Stays LTR regardless of page direction: its own content is language
     codes ("EN"/"AR"), not translated text, and the thumb's position is a
     hardcoded translateX — under RTL, flex would visually reorder the two
     buttons but the thumb wouldn't know to follow, landing under the
     wrong one. Keeping this one control LTR-ordered avoids that entirely. */
  direction: ltr;
  position: relative; display: inline-flex; align-items: center;
  width: 84px; height: 36px; padding: 3px; border-radius: var(--r-ctl);
  background: rgba(255,255,255,0.05); border: 1px solid var(--line-strong);
}
.lang-thumb {
  position: absolute; top: 3px; left: 3px; width: 39px; height: 28px;
  border-radius: 7px; background: var(--azure-600);
  transition: transform var(--dur-2) var(--ease-out);
  pointer-events: none;
}
.lang-switch.is-ar .lang-thumb { transform: translateX(39px); }
.lang-opt {
  position: relative; z-index: 1; flex: 1; height: 30px;
  border: none; background: transparent; cursor: pointer;
  font-family: var(--font); font-size: 12.5px; font-weight: 500; letter-spacing: 0.03em;
  color: var(--mist); transition: color var(--dur-2);
  display: flex; align-items: center; justify-content: center;
}
.lang-opt.active { color: #fff; }

/* ---------- hero ---------- */
.hero {
  position: relative; min-height: 100svh;
  display: flex; flex-direction: column; justify-content: center;
  overflow: hidden; padding-top: var(--nav-h);
}
.hero-bg { position: absolute; inset: 0; z-index: 0; }
.orb { position: absolute; border-radius: 50%; }
.orb-1 { width: 80vw; height: 80vw; background: radial-gradient(circle, rgba(30,111,217,0.28) 0%, rgba(30,111,217,0.14) 30%, rgba(30,111,217,0.05) 55%, transparent 72%); top: -30vw; right: -22vw; }
.orb-2 { width: 58vw; height: 58vw; background: radial-gradient(circle, rgba(34,211,238,0.15) 0%, rgba(34,211,238,0.07) 35%, rgba(34,211,238,0.02) 58%, transparent 75%); bottom: -18vw; left: -16vw; }
.orb-3 { display: none; }

/* ---------- floating paths (hero background) ----------
   Port of the "Background Paths" component, rendered on <canvas>
   instead of 72 animated SVG/CSS elements. Two prior CSS/SVG passes
   both failed on real (large, high-DPI) browser windows: capping paint
   size via a transform:scale wrapper only reduced RASTER cost, but the
   dominant cost turned out to be STYLE RECALCULATION — the browser
   re-evaluating stroke-dasharray/dashoffset/opacity across 72 separate
   DOM elements every frame, independent of screen size. That pipeline
   got so backed up that repaints stalled entirely until a click/mousemove
   forced a flush (which is why content only "appeared" after interacting).
   Canvas has no per-element style system to invalidate: one JS loop
   issues 72 stroke() calls into a single bitmap every frame, so cost is
   fully self-controlled rather than left to the browser's CSS engine.
   The canvas's internal pixel buffer is pinned at a small fixed
   resolution (independent of real screen size) and stretched to its
   display size via the browser's native bitmap upscaling — the same
   "cap raster cost" principle as before, but this time actually
   addressing the whole cost, not just paint. */
.hero-paths-canvas {
  position: absolute; pointer-events: none;
  transform: translateZ(0); /* pin a stable compositor layer up front — avoid mid-scroll layer promotion churn */
  will-change: transform;
  /* size/layout/style/paint containment — explicit width/height are set in JS
     and this element has no children, so it's safe to tell the browser its
     box can never affect (or be affected by) anything outside it. Keeps the
     rest of the hero's paint/layout work from ever needing to consider this
     layer, and vice versa. */
  contain: strict;
  /* The center-hole + bottom fade used to be a CSS mask-image (mask-composite:
     intersect). That forced the compositor to re-evaluate a dual-mask shader
     pass on a ~viewport-size layer, seemingly on every vsync regardless of
     whether the canvas texture had actually changed — the dominant cost
     behind hero scroll jank, worse than the ribbon drawing itself. The fade
     is now baked directly into the canvas alpha channel by the drawing code
     (see ribbons-worker.js / the fallback loop in main.js), so the browser
     composites this element as a plain alpha-blended texture — its cheapest
     path — with no CSS mask on it at all. */
}

/* hero-content transform/opacity are scroll-scrubbed from main.js */
.hero-content { position: relative; z-index: 2; text-align: center; will-change: transform, opacity; }
.hero-eyebrow {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 8px 18px; border-radius: 100px; font-size: 13px; font-weight: 500;
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--azure-300);
  background: rgba(56, 164, 240, 0.07); border: 1px solid rgba(56, 164, 240, 0.2);
  margin-bottom: 30px;
}
.hero-eyebrow .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--cyan-400); animation: pulse 2.4s infinite; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }
.hero h1 {
  /* Slope chosen so the max is only reached on genuinely wide screens
     (~1680px+) — vw-only sizing made 16:10 laptops (1440x900, 1280x800)
     render near-monitor type sizes into a much shorter viewport. */
  font-size: clamp(38px, 5vw, 84px); line-height: 1.02;
  font-weight: 400; letter-spacing: -0.035em; margin-bottom: 26px;
  /* Evens out the line lengths instead of leaving a single orphan word on
     the last line — the difference between a headline that looks typeset
     and one that looks like it just wrapped wherever the box ended. */
  text-wrap: balance;
}
/* Aurora text — brand palette (azure/cyan) version of the MagicUI
   "Aurora Text" effect: an oversized gradient drifting behind the text
   via background-position, clipped to the glyphs.
   main.js's word-reveal walker skips recursing into .grad (so "runs the
   enterprise." stays one unit, not split word-by-word) but still tags it
   with .w for the shared rise-in animation — so .grad always carries both
   classes in the DOM. Both rules set the `animation` shorthand on the same
   element with equal specificity; whichever is declared later in the
   cascade wins outright and drops the other entirely (not a merge). Target
   .grad.w directly and list both animations in one rule so they compose
   instead of one silently overwriting the other. */
.hero h1 .grad.w {
  position: relative;
  display: inline-block;
  background-image: linear-gradient(135deg,
    var(--azure-300), var(--cyan-400), var(--azure-400), var(--azure-500), var(--azure-300));
  background-size: 200% auto;
  -webkit-background-clip: text; background-clip: text; color: transparent;
  animation: wordIn 0.7s var(--ease-out) forwards, auroraText 8s ease-in-out infinite alternate;
}
/* background-position (unlike transform/opacity) forces a repaint of the
   text-clip mask every frame it changes — it can't be made compositor-only.
   main.js toggles .is-scrolling on <html> for the duration of active
   scrolling (plus a short settle window) so that repaint cost never
   competes with scroll's own frame budget; the second animation-play-state
   value targets only auroraText, leaving the one-shot entrance untouched. */
.is-scrolling .hero h1 .grad.w { animation-play-state: running, paused; }
@media (prefers-reduced-motion: reduce) {
  .hero h1 .grad.w { animation: none; background: var(--grad-text); -webkit-background-clip: text; background-clip: text; color: transparent; }
}
/* Reusable version of the same effect for headings outside the hero (no
   word-reveal entrance to compose with — just the gradient drift). Reuses
   .is-scrolling from main.js so it pauses during active scroll exactly like
   the hero's, for the same reason (background-position can't be made
   compositor-only). */
.aurora-text {
  background-image: linear-gradient(135deg,
    var(--azure-300), var(--cyan-400), var(--azure-400), var(--azure-500), var(--azure-300));
  background-size: 200% auto;
  -webkit-background-clip: text; background-clip: text; color: transparent;
  animation: auroraText 8s ease-in-out infinite alternate;
}
.is-scrolling .aurora-text { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) {
  .aurora-text { animation: none; background: var(--grad-text); -webkit-background-clip: text; background-clip: text; color: transparent; }
}
@keyframes auroraText {
  0%   { background-position: 0% 50%;   }
  25%  { background-position: 50% 100%; }
  50%  { background-position: 100% 50%; }
  75%  { background-position: 50% 0%;   }
  100% { background-position: 0% 50%;   }
}
.hero p.lede {
  font-size: clamp(17px, 2vw, 21px); color: var(--mist);
  max-width: 640px; margin: 0 auto 42px; line-height: 1.6; text-wrap: pretty;
}
.hero-ctas { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; }

/* word-by-word hero reveal — clean rise, no rotation */
.hero h1 .w { display: inline-block; opacity: 0; transform: translateY(0.55em); animation: wordIn 0.7s var(--ease-out) forwards; }
@keyframes wordIn { to { opacity: 1; transform: none; } }

/* Sub-page heroes carry their h1 size as an inline style (28 static
   pages); stylesheet !important is the one thing that outranks an inline
   declaration, so laptop-width retuning happens here rather than by
   editing every page. */
.hero-content h1[style] { font-size: clamp(32px, 3.6vw, 54px) !important; }

/* hero stat strip (scroll-scrubbed alongside hero-content) */
.hero-stats {
  position: relative; z-index: 2;
  display: flex; justify-content: center; gap: clamp(28px, 6vw, 90px);
  /* svh-aware top margin + a bottom band tall enough to hold the scroll
     cue: on 900/800px-tall laptops the hero column used to overflow
     100svh and push the stats into the absolutely-positioned cue. */
  margin-top: clamp(40px, 6svh, 72px); padding-bottom: 96px; flex-wrap: wrap;
  will-change: transform, opacity;
}
.hstat { text-align: center; }
.hstat .num { font-family: var(--font-num); font-size: clamp(26px, 2.5vw, 40px); font-weight: 500; letter-spacing: -0.02em; }
.hstat .num span.plus { color: var(--cyan-400); }
.hstat .lbl { font-size: 13px; color: var(--mist); margin-top: 4px; letter-spacing: 0.04em; text-transform: uppercase; }

.scroll-cue {
  position: absolute; bottom: 26px; left: 50%; transform: translateX(-50%);
  z-index: 2; color: var(--mist); font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase;
  display: flex; flex-direction: column; align-items: center; gap: 8px; opacity: 0.7;
}
.scroll-cue .line { width: 1px; height: 42px; background: linear-gradient(var(--red-500), transparent); animation: cue 1.8s infinite; }
/* On short viewports there is no clean band left for the cue — drop it
   rather than let it collide with the stat strip. */
@media (max-height: 760px) {
  .scroll-cue { display: none; }
  .hero-stats { padding-bottom: 48px; }
}
@keyframes cue { 0% { transform: scaleY(0); transform-origin: top; } 50% { transform: scaleY(1); transform-origin: top; } 51% { transform-origin: bottom; } 100% { transform: scaleY(0); transform-origin: bottom; } }

/* ---------- logo marquee (JS-driven; CSS animation is no-JS fallback) ---------- */
.marquee-section {
  padding: 54px 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
  /* Soft azure/cyan wash at the very top, echoing the hero's faded-out
     line color so the seam between sections reads as a continuation
     rather than an abrupt background-color change. */
  background:
    linear-gradient(to bottom, rgba(56,164,240,0.06), transparent 45%),
    var(--ink-900);
}
.marquee-label { text-align: center; font-size: 12.5px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--slate); margin-bottom: 30px; }
/* Edge fade used to be a CSS mask-image on this element — the same class of
   bug as the old hero canvas mask: a masked layer whose content (marquee-
   track) gets a transform write every visible frame forces the compositor
   to re-evaluate the mask shader every frame too, right at the section
   boundary immediately after the hero. Replaced with two static gradient
   overlays painted on top instead of a mask on the moving content — these
   never change, so they cost nothing per frame regardless of what's
   scrolling underneath. var(--ink-900) matches this section's background
   by the point the marquee starts (the azure wash above it has already
   fully faded out by then). */
.marquee { overflow: hidden; position: relative; }
.marquee::before, .marquee::after {
  content: ""; position: absolute; top: 0; bottom: 0; width: 90px; z-index: 2; pointer-events: none;
}
.marquee::before { left: 0; background: linear-gradient(to right, var(--ink-900), transparent); }
.marquee::after { right: 0; background: linear-gradient(to left, var(--ink-900), transparent); }
.marquee-track { display: flex; gap: 72px; width: max-content; will-change: transform; }
@keyframes marquee { to { transform: translateX(-50%); } } /* kept as no-JS fallback only */
/* Was rgba(168,182,200,0.55), which composites to roughly #616C7C on this
   section's background — about 3.6:1, under the AA floor. These are named
   client institutions (Central Bank of Bahrain, EY, DHL); they are the
   page's strongest trust signal, so they should not be the hardest thing
   on it to read. Using the --slate token instead of a bespoke alpha also
   pulls them into the same muted-text tier as the rest of the system. */
.mq-item { font-size: 19px; font-weight: 500; color: var(--slate); white-space: nowrap; letter-spacing: 0.02em; transition: color var(--dur-2); }
.mq-item:hover { color: var(--azure-300); }

/* ---------- generic section scaffolding ---------- */
.section { padding: clamp(90px, 12vw, 150px) 0; position: relative; contain: layout style; content-visibility: auto; contain-intrinsic-size: auto 600px; }
/* UniRecon page only — the product page packs in more sections back to
   back than most, so the default padding (tuned for sparser pages)
   stacked into a lot of dead space between them. */
.page-unirecon .section { padding: clamp(56px, 7vw, 96px) 0; }
.section.no-pad { padding: 0; }
.section-head { max-width: 720px; margin-bottom: 64px; }
/* That 64px gutter exists to separate the heading from the content grid
   beneath it. When the head IS the section's only content there is nothing
   to clear, and the margin just stacks onto the section padding. */
.section-head:last-child { margin-bottom: 0; }
/* A lead-in section — heading and standfirst that introduce the section
   immediately after it. The two read as one unit, so collapse the gap
   between them instead of paying the full section rhythm twice. */
.section-lead { padding-bottom: 0; }
.section-lead + .section { padding-top: clamp(40px, 5vw, 64px); }
.section-head.center { margin-left: auto; margin-right: auto; text-align: center; }
.kicker {
  display: inline-block; font-size: 13px; font-weight: 500; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--azure-400); margin-bottom: 16px;
}
.kicker::before { content: ""; }
.section-head h2 { font-size: clamp(28px, 3vw, 48px); font-weight: 400; letter-spacing: -0.03em; line-height: 1.08; text-wrap: balance; }
/* `pretty` rather than `balance` for running copy: it only reflows the last
   couple of lines to kill orphans, leaving the paragraph's overall rag
   intact, whereas `balance` would even out every line of a multi-line
   paragraph and read oddly. */
.section-head p { margin-top: 18px; color: var(--mist); font-size: 17.5px; line-height: 1.65; text-wrap: pretty; }

/* reveal-on-scroll */
.reveal { opacity: 0; transform: translateY(28px); transition: opacity var(--dur-3) var(--ease-out), transform var(--dur-3) var(--ease-out); }
.reveal.in { opacity: 1; transform: none; }
.reveal[data-delay="1"] { transition-delay: 0.08s; }
.reveal[data-delay="2"] { transition-delay: 0.16s; }
.reveal[data-delay="3"] { transition-delay: 0.24s; }
.reveal[data-delay="4"] { transition-delay: 0.32s; }

/* ---------- solutions grid — flat surfaces, hairline borders ---------- */
.sol-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 22px; }
@media (max-width: 980px) { .sol-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 560px) { .sol-grid { grid-template-columns: 1fr; } }
/* Card art sits behind the text (transparent-background isometric SVG,
   desaturated + dimmed at rest) and both fully saturate plus a color wash
   fades in on hover — the same "shadowy until touched" treatment as the
   Arc reference. Fixed height (not min-height) is what actually guarantees
   all four cards match regardless of copy length differences. */
.sol-card {
  /* min-height (not fixed height): the grid's default stretch alignment
     keeps all cards in a row equal anyway, and a hard height clipped card
     copy at narrow laptop widths where text wraps to more lines. */
  position: relative; min-height: 483px; padding: 32px 28px; border-radius: var(--r-lg);
  background: var(--surface);
  border: 1px solid var(--line);
  transition: transform var(--dur-3) var(--ease-out), border-color var(--dur-3) var(--ease-out), box-shadow var(--dur-3) var(--ease-out);
  overflow: hidden;
  display: flex; flex-direction: column;
}
.sol-card:hover { transform: translateY(-4px); border-color: rgba(56,164,240,0.32); box-shadow: var(--shadow-2); }
/* Compact variant — for wide 2-up text-only cards (no art) where the
   default 483px min-height (tuned for narrow 4-up art-backed cards)
   would leave a huge block of empty space below a couple of sentences. */
.sol-card.compact { min-height: 0; }
.sol-art {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background-repeat: no-repeat; background-position: center 78%; background-size: 78%;
  opacity: 0.4; filter: saturate(0.25) brightness(0.75);
  transition: opacity var(--dur-3) var(--ease-out), filter var(--dur-3) var(--ease-out), transform var(--dur-3) var(--ease-out);
}
.sol-card:hover .sol-art { opacity: 0.95; filter: saturate(1) brightness(1); transform: scale(1.05); }
.sol-card::after {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: radial-gradient(120% 100% at 75% 100%, rgba(56,164,240,0.28), transparent 60%);
  opacity: 0; transition: opacity var(--dur-3) var(--ease-out);
}
.sol-card:hover::after { opacity: 1; }
.sol-body { position: relative; z-index: 1; }
.sol-card h3 { font-size: 19px; font-weight: 400; line-height: 1.3; min-height: 50px; margin-bottom: 10px; letter-spacing: -0.01em; }
.sol-card p { font-size: 14.5px; color: var(--mist); line-height: 1.6; margin-top: 19.3px; }
.sol-card .learn {
  position: relative; z-index: 1; margin-top: auto; align-self: flex-start; padding-bottom: 2px;
  display: inline-flex; align-items: center; font-size: 14px; font-weight: 500; color: var(--azure-300);
}
.sol-card .learn::after {
  content: ""; position: absolute; left: 0; bottom: 0; height: 1.5px; width: 0;
  background: var(--azure-400); transition: width var(--dur-2) var(--ease-out);
}
.sol-card:hover .learn::after { width: 100%; }

/* ---------- value proposition band ----------
   Five benefit statements answering "why us" before the page starts listing
   what we sell. Deliberately lighter than .sol-card: no art, no elevation,
   no hover lift — this section's job is to be scanned in a few seconds, so
   it stays a flat list with a single accent mark per row. auto-fit means the
   five items sit in one row on wide screens and reflow to 3+2 / 2+2+1 / 1
   without needing per-breakpoint column counts. */
.value-section { background: var(--ink-900); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.value-grid {
  list-style: none;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 34px 28px;
}
/* Centred column: the icon sits centred above a centred heading and body,
   which also keeps each item symmetric under the centred section head above
   and mirrors correctly under RTL without any extra rules. */
.value-item { display: flex; flex-direction: column; align-items: center; text-align: center; }
/* Holds the isometric line-art icons (same illustration family as the
   Expertise card art and the nav icons), not a glyph — so no circular chip,
   no border and no background: the artwork carries its own visual weight and
   a container would fight it. Sized generously enough for the linework to
   stay legible; the source PNGs are 256px so this stays sharp on high-DPI. */
.value-ico {
  width: 72px; height: 72px; flex-shrink: 0; margin-bottom: 14px;
  display: block;
}
.value-ico img {
  width: 100%; height: 100%; object-fit: contain;
  /* The art is drawn for a light ground; on this near-black surface the
     palest fills read slightly hot, so pull them back a touch. Matches the
     restraint used on .sol-art / .ind-art at rest. */
  opacity: 0.92;
  transition: opacity var(--dur-2) var(--ease-out), transform var(--dur-2) var(--ease-out);
}
.value-item:hover .value-ico img { opacity: 1; transform: translateY(-2px); }
.value-item h3 {
  font-size: 16px; font-weight: 500; line-height: 1.35; letter-spacing: -0.01em;
  margin-bottom: 8px; text-wrap: balance;
}
/* `balance` rather than `pretty` now that these are centred: a ragged rag is
   far more visible on centred text, and these are short 2-3 line blocks —
   exactly the case balance is meant for. */
.value-item p { font-size: 14px; color: var(--mist); line-height: 1.6; text-wrap: balance; }
@media (max-width: 560px) {
  .value-grid { gap: 26px; }
}

/* ---------- proof chips ----------
   Short claims that back up a differentiator block. Pill-shaped, one cyan
   dot each, deliberately lighter than a button so they read as evidence
   rather than something to click. */
.proof-chips {
  list-style: none; display: flex; flex-wrap: wrap; gap: 12px; justify-content: center;
}
.proof-chips li {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 11px 20px; border-radius: 999px;
  font-size: 14px; color: var(--mist);
  background: rgba(255, 255, 255, 0.03); border: 1px solid var(--line-strong);
}
.proof-chips li::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: var(--cyan-400); flex-shrink: 0;
}

/* ---------- product link row ----------
   Anchors are interactive; plain <span>s are used for products that have no
   page yet, so the row never carries a dead link. The two share a shape and
   differ only in affordance — the span has no hover and no pointer. */
.product-links { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; }
.product-links a, .product-links span {
  display: inline-flex; align-items: center;
  padding: 11px 22px; border-radius: var(--r-sm); font-size: 14.5px; font-weight: 500;
  border: 1px solid rgba(56, 164, 240, 0.18);
  background: rgba(56, 164, 240, 0.06); color: var(--azure-300);
  transition: background var(--dur-1), color var(--dur-1), border-color var(--dur-1);
}
.product-links a:hover { color: #fff; background: rgba(56, 164, 240, 0.14); border-color: rgba(56, 164, 240, 0.4); }
.product-links span { color: var(--slate); background: rgba(255, 255, 255, 0.02); border-color: var(--line); }

/* ---------- technology strip ----------
   A long flat list of tool names. Kept visually quiet — this is a
   credibility footnote, not a feature grid, so it should read as texture
   until someone goes looking for a specific name. */
.tech-strip { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
.tech-tag {
  padding: 8px 15px; border-radius: var(--r-sm);
  font-size: 13px; color: var(--slate); white-space: nowrap;
  background: rgba(255, 255, 255, 0.02); border: 1px solid var(--line);
  transition: color var(--dur-1), border-color var(--dur-1);
}
.tech-tag:hover { color: var(--azure-300); border-color: rgba(56, 164, 240, 0.28); }

/* ---------- products — dual-row card marquee ----------
   Same architecture as the client-logo marquee: JS drives translate3d
   drift at a fixed px/s (see tickProdMarquee in main.js), content
   duplicated once per row so each wraps seamlessly at -50%, edge fade is
   two static gradient overlays rather than a CSS mask (a mask on a layer
   that's translated every frame forces the compositor to re-evaluate the
   mask shader every frame too — the same bug fixed earlier on the hero
   canvas and the logo marquee). Cards are real links to each product page. */
.showcase { background: var(--ink-900); padding-bottom: clamp(90px, 12vw, 150px); }
.prod-marquee { overflow: hidden; position: relative; margin-top: 54px; display: flex; flex-direction: column; gap: 20px; }
.prod-marquee::before, .prod-marquee::after {
  content: ""; position: absolute; top: 0; bottom: 0; width: 90px; z-index: 2; pointer-events: none;
}
.prod-marquee::before { left: 0; background: linear-gradient(to right, var(--ink-900), transparent); }
.prod-marquee::after { right: 0; background: linear-gradient(to left, var(--ink-900), transparent); }
.prod-track { display: flex; gap: 16px; width: max-content; will-change: transform; padding: 10px 0; }
.prod-item {
  flex: 0 0 314px;
  border-radius: var(--r-lg); padding: 16px 18px; height: 128px; box-sizing: border-box; overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--line);
  display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center;
  color: inherit; text-decoration: none;
  transition: transform var(--dur-2) var(--ease-out), border-color var(--dur-2);
}
.prod-item:hover { transform: translateY(-4px); border-color: rgba(56,164,240,0.35); }
.prod-item .ico { display: block; }
.prod-item .ico svg { width: 20px; height: 20px; color: var(--cyan-400); }
.prod-item h3 {
  font-size: 15px; font-weight: 400; margin-top: 10px; line-height: 1.25;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.prod-item p {
  font-size: 12.5px; color: var(--mist); margin-top: 4px; line-height: 1.4;
  display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden;
}

/* ---------- big stats band — flat surface, solid numerals ---------- */
.stats-band {
  position: relative;
  background:
    radial-gradient(900px 400px at 80% 0%, rgba(30,111,217,0.1), transparent 60%),
    var(--ink-800);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  /* Noticeably shorter than the inherited .section padding (which is
     tuned for full content sections) — a stat band is just 4 numbers,
     so the generous 90-150px padding left it mostly empty space. Stays
     symmetric top/bottom (see the stage-grid centering fix earlier). */
  padding: clamp(48px, 6vw, 80px) 0;
}
/* Same traveling-highlight technique as .btn-travel, adapted from a
   rotating conic-gradient ring (works on a closed rectangle) to a sweeping
   linear-gradient (needed on an open horizontal edge) — a comet-shaped
   bright segment drifts across each edge on a loop, same color stops and
   duration as the hero button so both traces read as one family. */
/* Rebuilt as a compositor-only animation. The previous version animated
   `background-position` on a full-bleed element AND ran a drop-shadow()
   filter over it — background-position can't be promoted to the compositor,
   so every frame repainted the whole band width and then re-ran a filter
   pass on the result, twice (::before and ::after), forever.
   Now the trace is a fixed-width element that simply translateX()es across,
   which the compositor handles with no repaint at all, and the glow that
   drop-shadow() was producing is baked into a second background layer.
   The element travels beyond both edges of the band; the band is full-bleed
   so the viewport (body has overflow-x: hidden) does the clipping. */
.stats-band::before, .stats-band::after {
  content: ""; position: absolute; left: 0; width: 40%; height: 14px;
  background:
    /* soft halo — replaces the drop-shadow() filter pass */
    radial-gradient(ellipse 55% 55% at 50% 50%, rgba(34, 211, 238, 0.45), transparent 70%),
    /* bright 2px core, vertically centred in the 14px glow box */
    linear-gradient(90deg, transparent, #1a5fa8 35%, #22D3EE 50%, #1a5fa8 65%, transparent)
      center / 100% 2px no-repeat;
  animation: edge-trace 6s linear infinite;
  pointer-events: none;
  will-change: transform;
}
.stats-band::before { top: -7px; }
.stats-band::after { bottom: -7px; }
@keyframes edge-trace {
  /* -100% of a 40%-wide box = fully clear of the left edge;
     250% x 40% = 100% of the band, so fully clear of the right edge. */
  from { transform: translateX(-100%); }
  to   { transform: translateX(250%); }
}
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 40px; text-align: center; }
/* Flex column with the number bottom-anchored in a reserved 2-line box:
   a longer stat value (e.g. "RAG + RPA" vs "24/7") wraps to two lines at
   this font size while its neighbors stay on one, which used to push
   just that label down and break the row's alignment. Reserving room for
   the worst case and bottom-aligning the number keeps every label
   sitting at the same y position regardless of how any one value wraps. */
.bstat { display: flex; flex-direction: column; align-items: center; }
.bstat .num {
  font-family: var(--font-num); font-size: clamp(38px, 4.2vw, 64px); font-weight: 400;
  letter-spacing: -0.04em; color: #fff; line-height: 1.15;
  display: flex; align-items: center; justify-content: center;
  /* px, not em: an em-based min-height recalculates against each
     element's OWN font-size, so a stat with a smaller inline font-size
     override (long text sized down to fit one line) got a shorter
     reserved box than its full-size neighbors and drifted out of
     alignment again. Sized for one line of the largest clamp value —
     the one stat that used to need 2 lines (AI page's "RAG + RPA") now
     has its own nowrap override, so nothing on the site wraps anymore;
     the old 150px (2-line) reservation just left a big gap before the
     label below. */
  min-height: 78px;
}
.bstat .num span.plus { color: var(--cyan-400); }
.bstat .lbl { color: var(--mist); font-size: 15px; margin-top: 8px; }

/* ---------- industries — card art (ported from .sol-card) ----------
   Same treatment as the Solutions cards: a transparent-background
   isometric illustration sits behind the text, dimmed/desaturated at
   rest, fully saturating plus a color wash fading in on hover. 3-column
   grid (was auto-fit/min-height) sized to give the art real room, same
   breakpoints as .sol-grid. */
/* ---------- industries — 3D drag carousel ----------
   Ported from a React/framer-motion "cylinder" carousel: faces arranged in
   a ring via rotateY + translateZ, dragged around a shared Y axis, with
   momentum after release. No framer-motion here, so the drag/inertia loop
   is plain pointer-event math (see the "industries carousel" section in
   main.js) instead of a spring — same rotate-and-coast feel. The reference
   also opened a full-screen photo viewer on card click; skipped here since
   these are content cards (hover already reveals the art/glow), not
   photos to zoom into. */
.ind-carousel-wrap { perspective: 1400px; }
/* Width matches the ring's actual visual diameter (set by JS as
   --ind-ring-w, based on the rotation radius) instead of the full section
   width — the drag hit-box lives on THIS element (not the 3D-rotated
   track inside it), so it needs to track where the ring is actually
   visible, not the much wider container. Without this, dragging worked in
   the empty margins beside the ring, and failed over cards that projected
   near the ring's own edge at some rotation angles. */
.ind-carousel {
  /* left:50% + translateX(-50%) instead of margin:0 auto — auto-margins
     silently collapse to 0 (not negative) once the ring is wider than its
     container, which happens on every phone width; that made the ring
     render flush against the left edge instead of centered with
     symmetric overflow. Transform-centering has no such width ceiling. */
  position: relative; left: 50%; transform: translateX(-50%);
  height: 500px; width: var(--ind-ring-w, 900px);
  display: flex; align-items: center; justify-content: center;
  cursor: grab; touch-action: pan-y;
  user-select: none; -webkit-user-select: none;
}
.ind-carousel:active { cursor: grabbing; }
.ind-carousel-track {
  position: relative; height: 462px; width: 100%;
  transform-style: preserve-3d; will-change: transform;
}
.ind-carousel-hint { text-align: center; margin-top: 14px; font-size: 13px; color: var(--slate); }
.ind-carousel .ind-card {
  position: absolute; top: 0; left: 50%;
  width: var(--ind-face-w, 340px); margin-left: calc(var(--ind-face-w, 340px) / -2);
  backface-visibility: visible;
}
/* Card box itself (background/border) stays visible from the back so the
   ring reads as solid rotating shapes with no gaps — only the content
   inside (art + text) hides when facing away. */
.ind-carousel .ind-card .ind-art,
.ind-carousel .ind-card .ind-body {
  backface-visibility: hidden;
}
.ind-card {
  position: relative; height: 462px; padding: 32px 28px; border-radius: var(--r-lg);
  background: var(--surface);
  border: 1px solid var(--line);
  overflow: hidden;
  display: flex; flex-direction: column;
  transition: transform var(--dur-2) var(--ease-out), border-color var(--dur-2) var(--ease-out), box-shadow var(--dur-2) var(--ease-out);
}
.ind-card:hover { transform: translateY(-4px); border-color: rgba(56,164,240,0.35); box-shadow: var(--shadow-2); }
.ind-art {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background-repeat: no-repeat; background-position: center 75%; background-size: 78%;
  opacity: 0.4; filter: saturate(0.25) brightness(0.75);
  transition: opacity var(--dur-3) var(--ease-out), filter var(--dur-3) var(--ease-out), transform var(--dur-3) var(--ease-out);
}
.ind-card:hover .ind-art { opacity: 0.95; filter: saturate(1) brightness(1); transform: scale(1.05); }
.ind-card::after {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: radial-gradient(120% 100% at 75% 100%, rgba(56,164,240,0.28), transparent 60%);
  opacity: 0; transition: opacity var(--dur-3) var(--ease-out);
}
.ind-card:hover::after { opacity: 1; }
.ind-body { position: relative; z-index: 1; }
.ind-card h3 { font-size: 20px; font-weight: 400; }
.ind-card p { font-size: 14px; color: var(--mist); margin-top: 8px; line-height: 1.55; }

/* ---------- hub-page card icons (products/industries index) ----------
   .sol-icon normally holds an inline 24x24 line-icon SVG with no width/
   height attrs, which stretches to fill the flex column (~220px) — fine
   for a single big decorative glyph. Hub listing cards instead use the
   small colored nav-icon SVG files via <img>, which carry their own
   intrinsic size, so they need an explicit cap here. */
.sol-card .sol-icon img { width: 56px; height: 56px; object-fit: contain; }
/* Inline-SVG icons ship with no width/height attribute, so as a flex item in
   the card column they stretched to fill it — rendering as ~200px pure-white
   glyphs that dominated the card and read as placeholder art. Constrain them
   to the same 56px box as the <img> icons and give them the brand accent
   instead of inheriting the body's white, matching the .dd-ico treatment in
   the nav. This is the floor for every card; cards mapped to real library
   artwork override it with an <img>. */
.sol-card .sol-icon { flex: 0 0 auto; display: block; width: 56px; height: 56px; }
.sol-card .sol-icon svg { width: 56px; height: 56px; color: var(--azure-300); stroke-width: 1.5; }

/* Icon-led cards are centred: artwork over a centred heading and body, the
   same composition as the homepage value band. Scoped with :has(.sol-icon)
   so it applies only to these — the homepage Solutions cards use a
   full-bleed .sol-art background positioned against a left-aligned text
   column, and centring those would fight the artwork rather than sit with
   it. Mirrors correctly under RTL with no extra rules. */
/* The base grid is four columns, so a group of two or three cards left-aligns
   and leaves dead tracks on the right — visible on the products index, where
   the groups run 3 / 3 / 3 / 2. Matching the track count to the card count
   fills the row instead, which centres the group and gives each card more
   width (the cramped copy was a symptom of the same thing). Two cards would
   be far too wide at full container width, so that case is capped and
   centred at the same card width as a row of three. */
/* Desktop only. These selectors compute to a much higher specificity than the
   plain `.sol-grid` rules in the responsive block below — :has() takes the
   specificity of its argument — so left unscoped they would win against the
   tablet and phone breakpoints and pin narrow screens to three columns. */
@media (min-width: 981px) {
  .sol-grid:has(> .sol-card:nth-child(3):last-child) { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .sol-grid:has(> .sol-card:nth-child(2):last-child) {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    max-width: 764px; margin-inline: auto;
  }
  /* Six is the most common count on the site (15 grids) and was rendering
     4+2, leaving two dead tracks on the second row. Three columns gives two
     full rows and the same card width as a row of three. */
  .sol-grid:has(> .sol-card:nth-child(6):last-child) { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  /* A lone card would otherwise stretch the full container width. */
  .sol-grid:has(> .sol-card:nth-child(1):last-child) {
    grid-template-columns: minmax(0, 380px); justify-content: center;
  }
}

.sol-card:has(.sol-icon) {
  align-items: center; text-align: center;
  /* Roomier than the base 32px — these are centred cards where the copy sits
     between the artwork and the link, and the tighter padding made that
     column feel squeezed. */
  padding: 38px 30px 32px;
  /* Height follows content. The base rule's 483px floor is sized for the
     homepage's art-backed cards, where the illustration needs the room; on
     an icon card carrying three lines of copy it left a large empty well
     below the text. Grid stretch still equalises every card in a row, so
     rows stay tidy without reserving space nothing uses. */
  min-height: 0;
}
/* Was flush against the heading. */
.sol-card:has(.sol-icon) .sol-icon { margin-bottom: 20px; }
/* The base rule reserves min-height:50px here so body copy lines up across
   a row of mixed one- and two-line headings. Combined with the paragraph's
   own 19.3px top margin, a single-line heading opened a ~55px hole between
   the title and its text. Tighten to a normal heading/body relationship and
   let each card's copy sit directly under its own title. */
.sol-card:has(.sol-icon) h3 { min-height: 0; margin-bottom: 12px; text-wrap: balance; }
.sol-card:has(.sol-icon) p { margin-top: 0; text-wrap: pretty; }
/* Bottom-left, matching the homepage Solutions cards. margin-top:auto on the
   base rule still pins it to the card's bottom edge so it lines up across a
   row regardless of how much copy sits above it.
   padding-top guarantees a gap even when the copy is long enough that
   margin-top:auto resolves to zero — otherwise the link crowds the last
   line of text, which is what made these cards look cramped. */
.sol-card:has(.sol-icon) .learn { align-self: flex-start; padding-top: 22px; }
/* A floor only for cards that carry a link: margin-top:auto needs slack in
   the card to push against, and with none the link sits flush under the
   copy. Cards with no link stay content-height so they gain no dead space. */
.sol-card:has(.sol-icon):has(.learn) { min-height: 316px; }
/* .compact cards are wide 2-up text blocks (no art), where the "big
   decorative glyph" stretch above would blow the icon up to 300px+ —
   cap it back down to a normal small icon for this variant only. */
.sol-card.compact .sol-icon { align-self: flex-start; width: 44px; height: 44px; }
.sol-card.compact .sol-icon svg { width: 24px; height: 24px; }

/* ---------- comparison table ---------- */
.comp-table-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--r-xl); background: var(--surface-raised); }
.comp-table { width: 100%; border-collapse: collapse; min-width: 640px; }
.comp-table th, .comp-table td { padding: 18px 26px; text-align: left; font-size: 14.5px; white-space: nowrap; }
.comp-table th { color: var(--mist); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; font-weight: 600; border-bottom: 1px solid var(--line); }
.comp-table td { border-bottom: 1px solid var(--line); color: var(--mist); }
.comp-table tr:last-child td { border-bottom: none; }
.comp-table td { white-space: normal; }
.comp-table td:first-child { color: #fff; font-weight: 500; }
.comp-table td.yes { color: var(--azure-300); font-weight: 600; }
.comp-table td.limited { color: var(--slate); }
@media (max-width: 700px) { .comp-table th, .comp-table td { padding: 14px 18px; } }

/* ---------- testimonial ---------- */
.quote-wrap { max-width: 860px; margin: 0 auto; text-align: center; }
.quote-mark { font-size: 90px; line-height: 0.6; color: var(--red-500); font-family: Georgia, serif; }
blockquote { font-size: clamp(22px, 3vw, 32px); font-weight: 500; line-height: 1.4; letter-spacing: -0.015em; margin: 30px 0; }
.quote-attr { color: var(--mist); font-size: 15px; }
.quote-attr b { color: #fff; font-weight: 500; }

/* ---------- testimonials v2 (ported from a pasted React/framer-motion
   component) — rotating quote carousel: oversized parallax index number,
   vertical rail progress, word-by-word quote reveal, faint scrolling
   company ticker. Framer's spring-driven parallax is approximated with a
   CSS transition; AnimatePresence enter/exit swaps are approximated by
   replacing DOM content on each change so fresh nodes replay their CSS
   entrance animation on their own — see render()/restartAnim() in main.js
   for the two nodes (badge, author) that persist and need an explicit
   reflow-restart rather than being freshly created. */
.testi2 { position: relative; padding: 20px 0 10px; }
.testi2-index {
  position: absolute; left: clamp(-40px, -4vw, -10px); top: 46%; transform: translateY(-50%);
  font-family: var(--font-num); font-size: clamp(140px, 19vw, 320px); font-weight: 700;
  color: rgba(56, 164, 240, 0.16); line-height: 1; letter-spacing: -0.04em;
  pointer-events: none; user-select: none; z-index: 0;
}
.testi2-index-inner { display: block; animation: testi2NumIn 0.6s var(--ease-out); }
@keyframes testi2NumIn {
  from { opacity: 0; transform: scale(0.85); filter: blur(10px); }
  to { opacity: 1; transform: scale(1); filter: blur(0); }
}
.testi2-body { position: relative; z-index: 1; display: flex; gap: clamp(32px, 5vw, 64px); }
.testi2-main { flex: 1; min-width: 0; padding: 8px 0 8px clamp(48px, 6vw, 100px); }
.testi2-top { display: flex; align-items: center; gap: 20px; margin-bottom: 30px; }
.testi2-label {
  font-size: clamp(28px, 3vw, 48px); font-weight: 500; letter-spacing: -0.03em;
  white-space: nowrap;
}
.testi2-progress { position: relative; width: 100%; height: 2px; background: var(--line); }
.testi2-progress span { position: absolute; top: 0; left: 0; height: 100%; background: #fff; transition: width 0.5s var(--ease-out); }
.testi2-quote {
  /* Fixed height (not min-height) — the longest testimonials run to 4
     lines and the shortest to 2; a variable height here shifts the
     oversized index number below (positioned via top: 46% of the whole
     .testi2 block) between slides. Same fix as .sol-card elsewhere. */
  font-size: clamp(22px, 2.6vw, 38px); font-weight: 300; line-height: 1.2; letter-spacing: -0.01em;
  color: #fff; height: 4.8em; margin-bottom: 40px;
  transform: translateY(15%);
}
.testi2-quote .w {
  display: inline-block; opacity: 0; transform: translateY(20px) rotateX(70deg); transform-origin: bottom;
  animation: testi2WordIn 0.5s var(--ease-out) forwards;
}
@keyframes testi2WordIn { to { opacity: 1; transform: none; } }
.testi2-foot { display: flex; align-items: flex-end; justify-content: space-between; gap: 24px; flex-wrap: wrap; }
.testi2-author { display: flex; align-items: center; gap: 16px; transform: translateY(19.8%); animation: testi2FadeUp 0.5s var(--ease-out) 0.1s backwards; }
.testi2-rule { width: 32px; height: 2px; background: var(--red-500); flex-shrink: 0; }
.testi2-name { font-size: 15.5px; font-weight: 500; color: #fff; }
.testi2-role { font-size: 13.5px; color: var(--mist); margin-top: 2px; }
@keyframes testi2FadeUp { from { opacity: 0; transform: translateY(calc(19.8% + 16px)); } to { opacity: 1; transform: translateY(19.8%); } }
.testi2-nav-wrap { display: flex; flex-direction: column; align-items: center; gap: 14px; transform: translateY(19.55px); }
.testi2-nav { display: flex; gap: 12px; }
.testi2-btn {
  position: relative; width: 46px; height: 46px; border-radius: 50%; border: 1px solid var(--line);
  background: none; color: #fff; display: flex; align-items: center; justify-content: center;
  cursor: pointer; overflow: hidden; flex-shrink: 0; padding: 0;
}
.testi2-btn::before { content: ""; position: absolute; inset: 0; background: #fff; transform: scaleX(0); transition: transform var(--dur-2) var(--ease-out); z-index: 0; }
.testi2-btn.prev::before { transform-origin: left; }
.testi2-btn.next::before { transform-origin: right; }
.testi2-btn:hover::before { transform: scaleX(1); }
.testi2-btn svg { position: relative; z-index: 1; transition: color var(--dur-1); }
.testi2-btn:hover svg { color: var(--ink-950); }
.testi2-btn:active { transform: scale(0.94); }
.testi2-ticker { overflow: hidden; opacity: 0.06; pointer-events: none; margin-top: 48px; }
.testi2-ticker-track { display: inline-flex; white-space: nowrap; font-size: 46px; font-weight: 700; letter-spacing: -0.02em; color: var(--azure-300); }
@media (max-width: 760px) {
  .testi2-main { padding-left: 0; }
  .testi2-top { flex-wrap: wrap; gap: 12px; }
  .testi2-index { display: none; }
  .testi2-ticker-track { font-size: 28px; }
  /* Quotes wrap to far more lines at phone widths — the desktop fixed
     height would overlap the author row. The oversized index number is
     hidden here, so nothing depends on a constant section height. */
  .testi2-quote { height: auto; min-height: 3.5em; transform: none; margin-bottom: 28px; }
  .testi2-author { transform: none; animation: none; }
  .testi2-nav-wrap { transform: none; align-self: center; }
  .testi2-foot { flex-direction: column; align-items: flex-start; gap: 28px; }
  .testi2-progress { width: 140px; }
}

/* ---------- numbered stage list (static, no carousel/animation —
   same oversized faint index number as .testi2, just laid out as a
   plain 3-up row instead of a rotating single quote). ---------- */
.stage-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(32px, 4vw, 56px); }
.stage-item { position: relative; display: flex; flex-direction: column; align-items: flex-start; }
.stage-index {
  font-family: var(--font-num); font-size: clamp(130px, 13vw, 195px); font-weight: 700;
  color: rgba(56, 164, 240, 0.16); line-height: 1; letter-spacing: -0.04em;
  pointer-events: none; user-select: none;
  transform: translateX(-15%);
}
.stage-body { position: relative; z-index: 1; padding-left: clamp(36.4px, 4.42vw, 62.4px); margin-top: -40px; }
.stage-body h3 { font-size: 19px; font-weight: 400; line-height: 1.3; margin-bottom: 10px; letter-spacing: -0.01em; }
.stage-body p { font-size: 14.5px; color: var(--mist); line-height: 1.6; }
@media (max-width: 760px) {
  .stage-grid { grid-template-columns: 1fr; gap: 40px; }
}

/* ---------- CTA ---------- */
.cta {
  position: relative; overflow: hidden; text-align: center;
  border-radius: var(--r-xl); padding: clamp(64px, 9vw, 110px) 30px;
  background:
    radial-gradient(700px 320px at 50% -10%, rgba(56,164,240,0.14), transparent 70%),
    var(--surface-raised);
  border: 1px solid var(--line);
}
.cta h2 { font-size: clamp(28px, 3.1vw, 48px); font-weight: 400; letter-spacing: -0.03em; margin-bottom: 18px; text-wrap: balance; }
.cta p { color: var(--mist); font-size: 17px; max-width: 520px; margin: 0 auto 38px; line-height: 1.6; text-wrap: pretty; }

/* ---------- CTA beams background (ported from a React/framer-motion
   canvas component) — soft colored light beams rise and pulse behind the
   card content. Sized to the card itself here (the reference covered the
   full viewport for a standalone hero); driven by main.js's tickCtaBeams,
   gated by the same pageHidden/IntersectionObserver/reduceMotion pattern
   as the hero canvas and marquees. The reference's breathing overlay
   (framer-motion animating opacity 0.05→0.15→0.05 over 10s) is plain CSS
   here since it's a simple periodic value with no interaction driving it. */
/* This region used to stack THREE separate blurs over the same pixels, all
   re-evaluated on every frame the canvas repainted:
     1. ctx.filter = blur(35px)      (canvas-side, per beam draw)
     2. .cta-beams { filter: blur(15px) }        (CSS filter on the element)
     3. .cta-overlay { backdrop-filter: blur(50px) }   (compositor backdrop)
   (3) was the structurally expensive one: a backdrop-filter has to re-sample
   and re-blur everything behind it whenever that backdrop changes, and its
   backdrop here is a canvas being redrawn every single frame — so the most
   expensive compositor operation on the page was tied to the highest-frequency
   repaint on the page. It was also nearly invisible: blurring content that
   two earlier passes had already blurred adds almost nothing perceptually.
   Now a single canvas-side blur does the work (raised 35px -> 38px, since
   sequential Gaussian blurs add in quadrature: sqrt(35^2 + 15^2) ~= 38, so
   the softness is visually unchanged), and the overlay is a plain tint. */
.cta-beams {
  position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none;
  /* Stable compositor layer, and the canvas can neither affect nor be
     affected by anything outside its own box. */
  transform: translateZ(0);
  contain: strict;
}
.cta-overlay {
  position: absolute; inset: 0; background: rgba(6, 11, 20, 0.55);
  pointer-events: none;
  animation: ctaOverlayBreathe 10s ease-in-out infinite;
}
/* Opacity-only, so this is a compositor animation with no repaint. */
@keyframes ctaOverlayBreathe { 0%, 100% { opacity: 0.35; } 50% { opacity: 0.6; } }
.cta-content { position: relative; z-index: 1; }

/* ---------- enquiry form ----------
   Lives once, on the homepage, inside the CTA card. Every other page's CTA
   already links to /#contact, so there is one form to maintain and one
   delivery endpoint to configure rather than eleven copies. */
.cta-form { max-width: 620px; margin: 0 auto; text-align: left; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 560px) { .form-row { grid-template-columns: 1fr; } }
.field { display: block; margin-bottom: 16px; }
.field > span {
  display: block; margin-bottom: 7px;
  font-size: 13px; font-weight: 500; color: var(--mist); letter-spacing: 0.01em;
}
.field > span em { font-style: normal; color: var(--slate); font-weight: 400; }
.field input, .field select, .field textarea {
  width: 100%; padding: 12px 14px;
  font-family: var(--font); font-size: 15px; color: #fff;
  background: rgba(6, 11, 20, 0.55);
  border: 1px solid var(--line-strong); border-radius: var(--r-ctl);
  transition: border-color var(--dur-1), background var(--dur-1);
}
.field textarea { resize: vertical; min-height: 96px; }
.field input::placeholder, .field textarea::placeholder { color: var(--slate); }
.field input:hover, .field select:hover, .field textarea:hover { border-color: rgba(148, 170, 197, 0.34); }
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none; border-color: var(--azure-400);
  background: rgba(6, 11, 20, 0.8);
  box-shadow: var(--focus-ring);
}
/* Native select arrow is unstyleable and renders light on some platforms;
   colour-scheme: dark on :root already darkens the popup, this just keeps
   the closed control consistent with the text inputs. */
.field select { appearance: none; -webkit-appearance: none; cursor: pointer;
  background-image: linear-gradient(45deg, transparent 50%, var(--mist) 50%), linear-gradient(135deg, var(--mist) 50%, transparent 50%);
  background-position: calc(100% - 19px) center, calc(100% - 14px) center;
  background-size: 5px 5px, 5px 5px; background-repeat: no-repeat;
  padding-right: 38px;
}
html[dir="rtl"] .field select {
  background-position: 14px center, 19px center;
  padding-right: 14px; padding-left: 38px;
}
/* Flags an invalid field only after a submit attempt, never while typing. */
.cta-form.submitted :invalid { border-color: var(--red-400); }
/* Deliberate gap above the button: it separates "filling in the form" from
   "committing to send it", so the last field doesn't run straight into the
   action. Larger than the 16px rhythm between fields for that reason. */
.cta-form-submit { margin-top: 34px; width: 100%; justify-content: center; }
.cta-form-submit[disabled] { opacity: 0.6; cursor: progress; }
/* Honeypot — off-screen rather than display:none, which some bots detect. */
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
/* Scoped under .cta-form on purpose. These are <p> elements inside the CTA
   card, so the earlier `.cta p` rule (0,1,1) outranks a bare `.form-note`
   class (0,1,0) and was overriding the size, colour and margins with the
   CTA's own 17px centred-paragraph treatment. Two classes beats one class
   plus a type. */
.cta-form .form-note {
  margin: 14px 0 0; max-width: none;
  font-size: 12.5px; color: var(--slate); text-align: center; line-height: 1.5;
}
.cta-form .form-status {
  margin: 10px 0 0; max-width: none;
  font-size: 14px; min-height: 20px; text-align: center; color: var(--mist);
}
.form-status.ok { color: var(--cyan-400); }
.form-status.err { color: var(--red-400); }

/* ---------- footer ---------- */
footer { padding: 70px 0 40px; border-top: 1px solid var(--line); background: var(--ink-900); }
.foot-grid { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 40px; margin-bottom: 54px; }
.foot-grid img { height: 26px; filter: brightness(0) invert(1); opacity: 0.9; margin-bottom: 18px; }
.foot-grid p { color: var(--slate); font-size: 14px; line-height: 1.6; max-width: 300px; }
.foot-col h4 { font-size: 13px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--mist); margin-bottom: 18px; }
.foot-col ul { list-style: none; display: grid; gap: 12px; }
.foot-col a { color: var(--slate); font-size: 14.5px; transition: color var(--dur-1); }
.foot-col a:hover { color: var(--azure-300); }
.foot-col li > span { color: var(--slate); font-size: 14.5px; }
.foot-base { display: flex; justify-content: space-between; align-items: center; padding-top: 28px; border-top: 1px solid var(--line); color: var(--slate); font-size: 13px; flex-wrap: wrap; gap: 14px; }

/* ---------- mobile nav (burger + drawer injected by main.js) ----------
   The bar keeps logo + language switch + burger on small screens; the
   drawer is built at runtime by cloning the existing .nav-links structure
   (see "mobile nav" in main.js), so the 29 static pages need no per-page
   markup for it. */
.nav-burger {
  display: none; width: 42px; height: 42px; flex-shrink: 0;
  border: 1px solid var(--line-strong); border-radius: var(--r-ctl);
  background: none; cursor: pointer; padding: 0;
  flex-direction: column; align-items: center; justify-content: center; gap: 5px;
}
.nav-burger span { display: block; width: 18px; height: 2px; background: #fff; border-radius: 2px; transition: transform var(--dur-2) var(--ease-out), opacity var(--dur-2); }
.nav-burger.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav-burger.open span:nth-child(2) { opacity: 0; }
.nav-burger.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
.nav.mm-open { background-color: rgba(6, 11, 20, 0.98); box-shadow: 0 1px 0 var(--line); }

.mobile-menu {
  position: fixed; inset: 0; z-index: 890;
  padding: calc(var(--nav-h) + 14px) 6vw 44px;
  background: rgba(6, 11, 20, 0.98);
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  opacity: 0; visibility: hidden; transform: translateY(-10px);
  transition: opacity var(--dur-2) var(--ease-out), transform var(--dur-2) var(--ease-out), visibility 0s linear var(--dur-2);
}
.mobile-menu.open { opacity: 1; visibility: visible; transform: none; transition-delay: 0s; }
.mm-group { border-bottom: 1px solid var(--line); }
.mm-head {
  width: 100%; display: flex; align-items: center; justify-content: space-between;
  padding: 18px 2px; background: none; border: none; cursor: pointer; text-align: left;
  font-family: var(--font); font-size: 17px; font-weight: 500; color: #fff;
}
.mm-head .chev { color: var(--red-500); flex-shrink: 0; transition: transform var(--dur-2) var(--ease-out); }
.mm-group.open .mm-head .chev { transform: rotate(180deg); }
.mm-list { list-style: none; overflow: hidden; max-height: 0; transition: max-height var(--dur-3) var(--ease-out); }
.mm-group.open .mm-list { max-height: 620px; }
.mm-list a { display: block; padding: 11px 2px 11px 16px; font-size: 15px; color: var(--mist); }
.mm-list a:hover, .mm-list a:active { color: #fff; }
.mm-list li:last-child a { padding-bottom: 20px; }
.mm-cta { display: flex; justify-content: center; margin-top: 28px; }
body.mm-locked { overflow: hidden; }

/* ---------- responsive ---------- */
/* Laptop widths: tighten the bar so five items + controls never crowd. */
@media (max-width: 1200px) {
  .nav-links { gap: 22px; font-size: 13.5px; }
}
@media (max-width: 900px) {
  .nav-links { display: none; }
  .nav .btn-primary { display: none; } /* Book a Demo lives in the drawer */
  .nav-burger { display: flex; }
  .foot-grid { grid-template-columns: 1fr 1fr; }
}
/* Several pages set explicit 2/3-column counts inline on feature grids;
   those must still collapse below tablet, and inline styles outrank any
   normal rule — hence the attribute selector + !important. */
@media (max-width: 980px) {
  .sol-grid[style*="grid-template-columns"] { grid-template-columns: repeat(2, 1fr) !important; }
}
@media (max-width: 560px) {
  .sol-grid[style*="grid-template-columns"] { grid-template-columns: 1fr !important; }
  .sol-card { min-height: 340px; }
  /* Cards with background art (only the homepage Solutions section) need
     more headroom at this width: the art sits at a fixed 78%-down
     position, so a short card puts it directly behind the last lines of
     body copy instead of below it, killing contrast. Taller minimum
     pushes the art's vertical center safely past the text block. */
  .sol-card:has(.sol-art) { min-height: 460px; }
  /* Belt-and-suspenders alongside the taller min-height above: smaller and
     lower so its top edge clears the text even on the longest copy, and
     dimmer since mobile has no hover state to fade it back in behind. */
  .sol-art { background-position: center 92%; background-size: 60%; opacity: 0.28; }
  .foot-grid { grid-template-columns: 1fr; }
  .prod-item { flex: 0 0 270px; height: 120px; }
  .hero-stats { gap: 26px 40px; margin-top: 48px; }
  .stats-grid { gap: 30px; }
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
