/* ==========================================================================
   Bitchief Technology Services — brand tokens and shared primitives
   --------------------------------------------------------------------------
   Single source of truth for colour, type, spacing and the recurring layout
   primitives (hero, section, card, button, badge).

   Before this file existed, :root was declared independently in header.php,
   footer.php, index.php, about.php and certificate-management.php, each with a
   different value for the brand green. Because header.php was included partway
   down the document, its :root won the cascade for the header while the page's
   own :root still governed the body — so a single page rendered two greens.

   The values here are the ones the site already used most: #00a859 brand,
   #00ff9d accent, #0a1628 dark. Nothing is a new colour; the outliers
   (#00B74A, #009639, #1a2332, #10b981) are the ones being retired.

   Load order matters: vendor CSS, then this file, then any page-specific CSS.
   ========================================================================== */

:root {
  /* ---- Brand ---------------------------------------------------------- */
  --bts-green: #00a859;
  --bts-green-dark: #008c4a;
  --bts-green-darker: #00754a;
  --bts-green-light: #00c569;
  --bts-green-accent: #00ff9d;   /* highlight / gradient terminus only */
  --bts-green-soft: #e8f5f0;     /* tinted surface */

  /* Translucent brand, for borders, glows and hover washes. */
  --bts-green-a08: rgba(0, 168, 89, 0.08);
  --bts-green-a12: rgba(0, 168, 89, 0.12);
  --bts-green-a20: rgba(0, 168, 89, 0.20);
  --bts-green-a30: rgba(0, 168, 89, 0.30);
  --bts-accent-a20: rgba(0, 255, 157, 0.20);
  --bts-accent-a30: rgba(0, 255, 157, 0.30);

  /* ---- Surfaces --------------------------------------------------------- */
  /*
   * The site is dark throughout, over the animated field in .bts-backdrop below.
   *
   * Almost nothing here may be opaque, and that is a harder rule than it looks.
   * The backdrop sits behind the content at z-index: -1; any opaque background on
   * a full-width element blankets it for that element's height and leaves a flat
   * band across the page. That is what --bts-bg-deep did as a section and footer
   * background, and what --bts-bg did on the body, where it hid the field
   * entirely. Sections and the footer recede by DARKENING what is behind them --
   * rgba(6, 16, 29, 0.45) and 0.55 -- not by replacing it.
   *
   * The opaque values below are for things that must genuinely occlude: menus and
   * popovers, where letting the page text behind show through makes them
   * unreadable.
   */
  --bts-bg: #0a1628;
  --bts-bg-deep: #06101d;        /* footer, and anything that must sit "under" */
  --bts-bg-gradient: radial-gradient(circle at 20% 40%, #1a3a2e 0%, #0a1628 52%, #0d1e2d 100%);

  --bts-panel: rgba(255, 255, 255, 0.045);
  --bts-panel-hover: rgba(255, 255, 255, 0.075);
  /* For menus and popovers, where translucency would let text show through. */
  --bts-panel-solid: #0e1f30;
  --bts-panel-raised: #132738;

  /*
   * The plate behind vendor logo artwork.
   *
   * Flat white, and it has to be. Of the 47 logo files the walls render, 21 carry
   * a baked-in opaque white background -- they are JPEGs, so that rectangle is
   * part of the image and no CSS removes it. The plate has to match it exactly or
   * each of those 21 shows its own white block inside the tile. An off-white
   * gradient was tried here and did precisely that: a visible seam on nearly half
   * the wall.
   *
   * The same 21 files are why the logos cannot be knocked out to a monochrome
   * white and put on dark tiles, which would suit the theme better. That is not a
   * CSS decision; it needs transparent source artwork. If these are ever
   * re-cut as transparent SVGs, revisit it -- 23 of the rest are dark-on-
   * transparent and would invert cleanly, and 3 are light artwork that would not.
   *
   * The wall is made to sit on the theme by framing it instead: see
   * .bts-logo-grid.
   */
  --bts-plate: #ffffff;

  --bts-dark: #0a1628;           /* retained: the gradient's mid stop */
  --bts-dark-soft: #12233a;
  --bts-dark-light: #1a2838;

  /* ---- Ink -------------------------------------------------------------- */
  /*
   * All three clear WCAG AA for normal-size text at the WORST ground the site can
   * put behind them: the backdrop's brightest broad region, seen through a card
   * inside a panel inside a tinted section.
   *
   * Both soft inks have been lifted twice, for two different reasons, and the
   * history is worth keeping because both mistakes are easy to repeat.
   *
   * First, --bts-ink-subtle was #8098aa and measured 4.14:1 on the form panel.
   * Nesting was the thing missed: each translucent layer lifts the ground a
   * little, so an ink that passes on the bare page can still fail two panels
   * deep. Measure at the deepest nesting, not the shallowest.
   *
   * Then the animated field moved from the home hero to every page, and the
   * brightest ground stopped being flat navy. #9bb0bf fell to 3.70:1. Note that
   * the orbs were not the cause and dimming them does not help: the brightest
   * broad ground is --bts-bg-gradient's own first stop, #1a3a2e, which sits
   * underneath them. The fix was a veil on the backdrop plus these two values.
   *
   * A busier background genuinely demands more contrast from its text, so soft
   * ink simply has less room to be soft here than it did over flat navy.
   */
  --bts-ink: #e8eef4;
  --bts-ink-muted: #c3d3de;
  --bts-ink-subtle: #b6c7d3;

  --bts-text: var(--bts-ink);
  --bts-text-muted: var(--bts-ink-muted);
  --bts-text-subtle: var(--bts-ink-subtle);
  --bts-text-on-dark: var(--bts-ink);
  --bts-text-on-dark-muted: var(--bts-ink-muted);

  /* Literal white. Stays white: it is the text colour on a green button and the
     fill for the logo plate, neither of which inverts. */
  --bts-white: #ffffff;

  --bts-surface: rgba(255, 255, 255, 0.03);
  --bts-surface-alt: rgba(255, 255, 255, 0.055);
  --bts-border: rgba(255, 255, 255, 0.10);
  --bts-border-strong: rgba(255, 255, 255, 0.18);
  --bts-border-on-dark: rgba(255, 255, 255, 0.10);

  /* ---- Status ---------------------------------------------------------- */
  --bts-success: #00a859;
  --bts-warning: #f0a020;
  --bts-danger: #dc3545;
  --bts-info: #0088cc;

  /* ---- Gradients ------------------------------------------------------- */
  /* Named so a page never hand-rolls its own and drifts out of alignment. */
  --bts-gradient-brand: linear-gradient(135deg, var(--bts-green) 0%, var(--bts-green-dark) 100%);
  /* The primary action fill. Bright, carrying dark ink -- see the note on
     .bts-btn--primary for why it is this way round. */
  --bts-gradient-action: linear-gradient(135deg, var(--bts-green-accent) 0%, #00d97d 100%);
  --bts-gradient-accent: linear-gradient(135deg, var(--bts-green-accent) 0%, var(--bts-green) 100%);
  --bts-gradient-hero: linear-gradient(135deg, var(--bts-dark) 0%, #0d2a33 55%, var(--bts-green-dark) 100%);
  --bts-gradient-rule: linear-gradient(90deg, var(--bts-green) 0%, var(--bts-green-accent) 100%);

  /* ---- Typography ------------------------------------------------------ */
  --bts-font: 'Poppins', 'Open Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --bts-font-heading: 'Poppins', 'Open Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;

  /* Fluid scale. Every page-level heading should use one of these rather than
     its own clamp(), which is why headings previously differed page to page. */
  --bts-text-xs: 0.75rem;    /* 12px */
  --bts-text-sm: 0.8125rem;  /* 13px */
  --bts-text-base: 0.9375rem;/* 15px */
  --bts-text-md: 1rem;       /* 16px */
  --bts-text-lg: 1.125rem;   /* 18px */
  --bts-text-xl: clamp(1.25rem, 1.1rem + 0.6vw, 1.5rem);
  --bts-text-2xl: clamp(1.5rem, 1.3rem + 1vw, 1.875rem);
  --bts-text-3xl: clamp(1.75rem, 1.4rem + 1.6vw, 2.25rem);
  --bts-text-4xl: clamp(2rem, 1.5rem + 2.4vw, 3rem);

  --bts-leading-tight: 1.2;
  --bts-leading-snug: 1.4;
  --bts-leading: 1.65;

  --bts-weight-normal: 400;
  --bts-weight-medium: 500;
  --bts-weight-semibold: 600;
  --bts-weight-bold: 700;
  --bts-weight-black: 800;

  /* ---- Spacing --------------------------------------------------------- */
  --bts-space-1: 4px;
  --bts-space-2: 8px;
  --bts-space-3: 12px;
  --bts-space-4: 16px;
  --bts-space-5: 20px;
  --bts-space-6: 24px;
  --bts-space-8: 32px;
  --bts-space-10: 40px;
  --bts-space-12: 48px;
  --bts-space-16: 64px;
  --bts-space-20: 80px;

  /* ---- Layout ---------------------------------------------------------- */
  --bts-container: 1320px;
  --bts-container-narrow: 900px;

  /* The header is fixed, so every first-in-page section must clear it.
     Pages previously used 120px or 100px arbitrarily; deriving hero padding
     from the header height is what keeps the fold consistent site-wide. */
  --bts-header-h: 80px;
  --bts-header-h-mobile: 70px;

  /* Canonical vertical rhythm.
     Was 88/64. At those values a page with three sections spent ~350px on
     section padding alone before reaching its form, which is what put the
     contact form 1676px down the page. */
  --bts-section-y: 60px;         /* standard section top/bottom */
  --bts-section-y-tight: 40px;

  /* Interior page header.
     A breadcrumb bar with the page title, not a banner. It began as a centred
     hero of 435-593px -- half the fold or more, on every page, before any
     content. The padding clears the fixed header and adds a slim band. */
  --bts-hero-pt: calc(var(--bts-header-h) + 12px);
  --bts-hero-pb: 16px;

  --bts-hero-pt-mobile: calc(var(--bts-header-h-mobile) + 10px);
  --bts-hero-pb-mobile: 14px;

  /* ---- Shape & depth --------------------------------------------------- */
  --bts-radius-sm: 6px;
  --bts-radius: 10px;
  --bts-radius-lg: 16px;
  --bts-radius-xl: 24px;
  --bts-radius-pill: 999px;

  --bts-shadow-sm: 0 1px 3px rgba(10, 22, 40, 0.06), 0 1px 2px rgba(10, 22, 40, 0.04);
  --bts-shadow: 0 4px 16px rgba(10, 22, 40, 0.08);
  --bts-shadow-md: 0 8px 28px rgba(10, 22, 40, 0.10);
  --bts-shadow-lg: 0 16px 48px rgba(10, 22, 40, 0.14);
  --bts-shadow-brand: 0 8px 24px var(--bts-green-a20);

  /* ---- Motion ---------------------------------------------------------- */
  --bts-ease: cubic-bezier(0.4, 0, 0.2, 1);
  --bts-duration: 0.28s;
  --bts-duration-slow: 0.45s;
  --bts-transition: all var(--bts-duration) var(--bts-ease);

  --bts-z-header: 1000;
  --bts-z-menu: 999;
  --bts-z-overlay: 998;
  --bts-z-megamenu: 1010;
}

/* --------------------------------------------------------------------------
   Compatibility aliases.

   Page CSS across the site references --primary, --primary-green, --brand and
   --bts-* interchangeably. Mapping the old names onto the new tokens means the
   consolidation does not require touching every rule in every page at once,
   and guarantees the legacy names can no longer resolve to a different green.
   -------------------------------------------------------------------------- */
:root {
  --primary: var(--bts-green);
  --primary-dark: var(--bts-green-dark);
  --primary-light: var(--bts-green-light);
  --primary-glow: var(--bts-green-accent);
  --primary-green: var(--bts-green);
  --primary-green-hover: var(--bts-green-light);
  --brand: var(--bts-green);
  --brand-dark: var(--bts-green-dark);

  --dark: var(--bts-dark);
  --dark-light: var(--bts-dark-light);
  --text-dark: var(--bts-text);
  --text-light: var(--bts-text-on-dark);
  --text-muted: var(--bts-text-muted);

  /* --gray-50 and --bts-light meant "a very light surface" in the legacy page
     CSS; on a dark theme that role is the faint panel lift. --white keeps
     meaning literal white, because the legacy CSS also uses it for button text
     and for the logo plate. */
  --gray-50: var(--bts-surface);
  --white: var(--bts-white);
  --bts-light: var(--bts-surface);

  --footer-bg: var(--bts-dark);
  --footer-bg-dark: #060f1c;
  --border-footer: var(--bts-border-on-dark);

  --header-bg: rgba(255, 255, 255, 0.97);
  --transition: var(--bts-transition);
}

/* ==========================================================================
   Base
   --------------------------------------------------------------------------
   Scoped deliberately. header.php previously injected a bare
   `* { margin:0; padding:0; box-sizing:border-box }` *after* Bootstrap had
   loaded, which stripped Bootstrap's own spacing on every element and is the
   root cause of the inconsistent gutters and list indentation between pages.
   ========================================================================== */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

/* The dark canvas is opt-in, keyed on a class only page_start() emits. Two pages
   own their own light theme and still load this file for its tokens and
   primitives: certificate-verification renders a certificate the visitor
   downloads as an image, and certificate-management is an internal panel. A
   plain `body` selector would paint both dark and the glow below would tint
   them green. The gradient also goes on html so over-scroll paints dark rather
   than flashing white; that needs :has(), and a browser without it only loses
   the over-scroll gutter's colour. */
html:has(> body.bts-theme) {
  background: var(--bts-bg);
}

body {
  position: relative;
  margin: 0;
  font-family: var(--bts-font);
  font-size: var(--bts-text-md);
  font-weight: var(--bts-weight-normal);
  line-height: var(--bts-leading);
  color: var(--bts-text);
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body.bts-theme {
  /*
   * Transparent, and it MUST stay transparent. This is not a style choice.
   *
   * .bts-backdrop below is a child of body at z-index: -1. Body is
   * position: relative with z-index: auto, which does not create a stacking
   * context, so the nearest one is the root element. Within a stacking context
   * the paint order is: the root's background, then negative-z descendants, then
   * the backgrounds of in-flow block descendants -- and body is an in-flow block
   * descendant. So body's own background box paints AFTER the backdrop and, if it
   * is opaque, hides it completely.
   *
   * That is exactly what happened: this was `background: var(--bts-bg)` and the
   * entire field was invisible on every page. It was not obvious because the home
   * hero still had its own copy of the decoration inside it, which is not
   * negative-z and so kept rendering -- the hero looked right while every
   * interior page was flat, and the flatness read as "the field is subtle here"
   * rather than "the field is covered".
   *
   * The dark floor comes from html instead, and the backdrop's own bottom layer
   * is an opaque gradient, so there are two fallbacks before anything flashes
   * white. Do not put a colour back here.
   */
  background: transparent;
}

/* ==========================================================================
   The site-wide animated backdrop
   --------------------------------------------------------------------------
   This whole field used to belong to the home page hero alone, which is why the
   landing page moved while every interior page sat flat behind its content. It
   is one fixed layer behind every page now, rendered by
   includes/partials/backdrop.php.

   Why it is affordable to run everywhere:

     - It is position: fixed, so it does not move relative to the viewport when
       the page scrolls. Scrolling costs the same with it as without -- there is
       no per-scroll-frame work in here at all.
     - Every animation moves transform or opacity and nothing else. Those are
       compositor properties: the GPU replays cached textures instead of the
       browser re-running layout or paint.

   What is deliberately NOT in here: anything animating a layout property
   (width, height, top, bottom) or a paint property (background-position,
   box-shadow, colour). The particles animated `bottom` once, costing eight
   forced layouts a frame; the hex mesh animated background-position and
   repainted the whole viewport every frame. Both are transforms now. Keep it
   that way -- this layer is always on screen, so a mistake here is permanent.

   pointer-events: none is not optional: this covers the whole viewport and would
   otherwise swallow every click on the page.
   ========================================================================== */

.bts-backdrop {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  /* The orbs and cubes are deliberately placed past the edges; this clips them
     rather than letting them widen anything. */
  overflow: hidden;
  /* The base field: brand glow over the gradient. Earlier layers paint on top. */
  background:
    radial-gradient(ellipse 60% 30% at 15% 0%, rgba(0, 168, 89, 0.16), transparent 70%),
    radial-gradient(ellipse 50% 24% at 85% 6%, rgba(0, 255, 157, 0.07), transparent 70%),
    var(--bts-bg-gradient);
}

/*
 * The veil, over every layer below.
 *
 * Text has to stay legible on top of all of this, and the field is bright enough
 * without it that --bts-ink-subtle measured 3.70:1 at the deepest nesting. The
 * instinct is to dim the orbs, and it does not work: the brightest broad ground
 * is --bts-bg-gradient's own first stop, #1a3a2e, and the orbs sit above it. One
 * veil over the whole field bounds every layer at once, in one number, and keeps
 * the pattern -- the grid, the orbs and the cubes all still read through it.
 *
 * 0.20 was solved for, not chosen by eye: with the lifted inks it is the lightest
 * value at which the worst ground the site can produce still clears AA for all
 * three inks (the subtle ink lands at 4.62:1). 0.15 gives 4.46:1 and fails, so
 * there is not much room below this. If you brighten any layer below, re-solve
 * it rather than nudging this number.
 *
 * z-index puts it above the absolutely positioned layers, which all sit at auto.
 */
.bts-backdrop::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background: rgba(6, 16, 29, 0.20);
}

/* ---- The receding perspective grid ------------------------------------- */

/* The layer that gives the page its floor. */
.bts-backdrop__grid {
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(rgba(0, 255, 157, 0.1) 1px, transparent 1px),
    linear-gradient(90deg, rgba(0, 255, 157, 0.1) 1px, transparent 1px);
  background-size: 50px 50px;
  transform: perspective(600px) rotateX(60deg);
  transform-origin: center;
}

/* ---- The drifting hex mesh --------------------------------------------- */

/* Oversized by more than one tile: the drift translates the layer, and the
   overhang keeps the trailing edge off-screen instead of exposing a gap. */
.bts-backdrop__hex {
  position: absolute;
  inset: -60px;
  background-image: url("data:image/svg+xml,%3Csvg width='50' height='50' viewBox='0 0 50 50' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M25 0l21.65 12.5v25L25 50 3.35 37.5v-25z' fill='none' stroke='rgba(0,255,157,0.08)' stroke-width='0.5'/%3E%3C/svg%3E");
}

/* ---- Gradient orbs ----------------------------------------------------- */

.bts-backdrop__orb {
  position: absolute;
  border-radius: 50%;
  filter: blur(90px);
  mix-blend-mode: screen;
  opacity: 0.4;
}

.bts-backdrop__orb--1 {
  width: 450px;
  height: 450px;
  background: radial-gradient(circle, var(--bts-green) 0%, transparent 70%);
  top: -100px;
  right: -100px;
}

.bts-backdrop__orb--2 {
  width: 350px;
  height: 350px;
  background: radial-gradient(circle, var(--bts-green-light) 0%, transparent 70%);
  bottom: -80px;
  left: -80px;
}

.bts-backdrop__orb--3 {
  width: 280px;
  height: 280px;
  background: radial-gradient(circle, var(--bts-green-accent) 0%, transparent 70%);
  top: 50%;
  left: 50%;
}

/* ---- Wireframe cubes --------------------------------------------------- */

/* The perspective lives here rather than on .bts-backdrop, because the cubes are
   the only thing that needs a vanishing point -- and a perspective on the parent
   would pull the veil above into the same 3D rendering context as the cubes'
   preserve-3d faces, where its paint order stops being predictable. */
.bts-backdrop__cubes {
  position: absolute;
  inset: 0;
  perspective: 1000px;
}

.bts-backdrop__cube {
  position: absolute;
  width: 60px;
  height: 60px;
  transform-style: preserve-3d;
}

.bts-backdrop__cube::before,
.bts-backdrop__cube::after {
  content: '';
  position: absolute;
  width: 60px;
  height: 60px;
  border: 1px solid rgba(0, 255, 157, 0.2);
  background: rgba(0, 168, 89, 0.05);
}

.bts-backdrop__cube::before { transform: rotateY(0deg) translateZ(30px); }
.bts-backdrop__cube::after  { transform: rotateY(90deg) translateZ(30px); }

.bts-backdrop__cube:nth-child(1) { top: 15%; left: 10%; }
.bts-backdrop__cube:nth-child(2) { top: 60%; left: 85%; }
.bts-backdrop__cube:nth-child(3) { top: 75%; left: 15%; }

/* ---- Travelling wave lines --------------------------------------------- */

.bts-backdrop__waves {
  position: absolute;
  inset: 0;
  opacity: 0.3;
}

.bts-backdrop__wave {
  position: absolute;
  width: 100%;
  height: 2px;
  background: linear-gradient(90deg, transparent, var(--bts-green-accent), transparent);
  /* Parked off to the left; the animation brings it across. */
  transform: translateX(-100%);
}

.bts-backdrop__wave:nth-child(1) { top: 20%; }
.bts-backdrop__wave:nth-child(2) { top: 40%; }
.bts-backdrop__wave:nth-child(3) { top: 60%; }
.bts-backdrop__wave:nth-child(4) { top: 80%; }

/* ---- Rising particles -------------------------------------------------- */

.bts-backdrop__particles {
  position: absolute;
  inset: 0;
}

/* Parked on the bottom edge and moved with transform. These used to animate
   `bottom` from -10px to 110%, a layout property: eight particles each forcing
   layout and paint on every frame, for as long as the page stayed open. */
.bts-backdrop__particle {
  position: absolute;
  bottom: 0;
  background: var(--bts-green-accent);
  border-radius: 50%;
  box-shadow: 0 0 10px var(--bts-green-accent);
  opacity: 0;
}

.bts-backdrop__particle:nth-child(1) { left: 5%;  width: 3px; height: 3px; }
.bts-backdrop__particle:nth-child(2) { left: 15%; width: 5px; height: 5px; }
.bts-backdrop__particle:nth-child(3) { left: 25%; width: 2px; height: 2px; }
.bts-backdrop__particle:nth-child(4) { left: 40%; width: 4px; height: 4px; }
.bts-backdrop__particle:nth-child(5) { left: 55%; width: 3px; height: 3px; }
.bts-backdrop__particle:nth-child(6) { left: 70%; width: 5px; height: 5px; }
.bts-backdrop__particle:nth-child(7) { left: 85%; width: 4px; height: 4px; }
.bts-backdrop__particle:nth-child(8) { left: 95%; width: 3px; height: 3px; }

/* ---- Motion -------------------------------------------------------------
   All of it behind one query. Without a motion preference the field is still
   there and still layered -- it simply holds its pose, which is why each layer
   above sets its own resting transform rather than relying on frame 0.
   ---------------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  .bts-backdrop__grid {
    animation: bts-grid-recede 20s linear infinite;
  }

  .bts-backdrop__hex {
    animation: bts-hex-drift 30s linear infinite;
    will-change: transform;
  }

  .bts-backdrop__orb {
    animation: bts-orb-morph 12s ease-in-out infinite;
  }

  .bts-backdrop__orb--2 { animation-delay: 4s; }
  .bts-backdrop__orb--3 { animation-delay: 2s; }

  .bts-backdrop__cube {
    animation: bts-cube-rotate 20s linear infinite;
  }

  .bts-backdrop__cube:nth-child(1) { animation-duration: 15s; }
  .bts-backdrop__cube:nth-child(2) { animation-duration: 20s; animation-delay: 2s; }
  .bts-backdrop__cube:nth-child(3) { animation-duration: 18s; animation-delay: 4s; }

  .bts-backdrop__wave {
    animation: bts-wave-flow 8s ease-in-out infinite;
  }

  .bts-backdrop__wave:nth-child(2) { animation-delay: 2s; }
  .bts-backdrop__wave:nth-child(3) { animation-delay: 4s; }
  .bts-backdrop__wave:nth-child(4) { animation-delay: 1s; }

  .bts-backdrop__particle {
    animation: bts-particle-rise 12s linear infinite;
    will-change: transform, opacity;
  }

  .bts-backdrop__particle:nth-child(1) { animation-delay: 0s; animation-duration: 10s; }
  .bts-backdrop__particle:nth-child(2) { animation-delay: 2s; animation-duration: 14s; }
  .bts-backdrop__particle:nth-child(3) { animation-delay: 4s; animation-duration: 9s; }
  .bts-backdrop__particle:nth-child(4) { animation-delay: 1s; animation-duration: 12s; }
  .bts-backdrop__particle:nth-child(5) { animation-delay: 3s; animation-duration: 11s; }
  .bts-backdrop__particle:nth-child(6) { animation-delay: 5s; animation-duration: 13s; }
  .bts-backdrop__particle:nth-child(7) { animation-delay: 6s; animation-duration: 10s; }
  .bts-backdrop__particle:nth-child(8) { animation-delay: 7s; animation-duration: 15s; }
}

/* The grid keeps its perspective in every keyframe: dropping it from one would
   flatten the floor for that frame and snap it back. */
@keyframes bts-grid-recede {
  from { transform: perspective(600px) rotateX(60deg) translateY(0); }
  to   { transform: perspective(600px) rotateX(60deg) translateY(50px); }
}

/* One tile exactly, so the pattern lands back on itself and the loop is seamless. */
@keyframes bts-hex-drift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(50px, 50px, 0); }
}

@keyframes bts-orb-morph {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1);          opacity: 0.4; }
  25%      { transform: translate3d(40px, -30px, 0) scale(1.2); opacity: 0.6; }
  50%      { transform: translate3d(-30px, 40px, 0) scale(0.9); opacity: 0.5; }
  75%      { transform: translate3d(30px, 30px, 0) scale(1.1);  opacity: 0.55; }
}

@keyframes bts-cube-rotate {
  from { transform: rotateX(0deg) rotateY(0deg); }
  to   { transform: rotateX(360deg) rotateY(360deg); }
}

@keyframes bts-wave-flow {
  0%, 100% { transform: translate3d(-100%, 0, 0); }
  50%      { transform: translate3d(100%, -20px, 0); }
}

@keyframes bts-particle-rise {
  0%   { opacity: 0; transform: translate3d(0, 10px, 0) scale(0.5); }
  20%  { opacity: 1; }
  80%  { opacity: 1; }
  100% { opacity: 0; transform: translate3d(80px, -110vh, 0) scale(1.2); }
}

/* Phones get a lighter field. The cubes are the most expensive layer here -- each
   one is a 3D-transformed element with two transformed children -- and they are
   also the least legible at this width, landing on top of the content rather than
   beside it. Halving the particles for the same reason: a phone GPU has less to
   spare, and eight glowing dots on a 375px-wide field reads as clutter.

   This rule used to live in home.css and hide .cube-3d, which only worked
   because the field was the home page's. It belongs with the backdrop now. */
@media (max-width: 768px) {
  .bts-backdrop__cubes,
  .bts-backdrop__particle:nth-child(n + 5) {
    display: none;
  }

  .bts-backdrop__orb {
    /* Scaled to the viewport rather than fixed pixels: a 450px orb on a 375px
       screen is a wash over the whole page, not an accent in a corner. */
    filter: blur(60px);
  }

  .bts-backdrop__orb--1 { width: 260px; height: 260px; }
  .bts-backdrop__orb--2 { width: 220px; height: 220px; }
  .bts-backdrop__orb--3 { width: 180px; height: 180px; }
}

/*
 * Clearance for the fixed site header.
 *
 * Keyed on .bts-has-header, which page_start() emits only when it actually renders
 * that header. It used to sit on bare `body`, which meant certificate-verification
 * and certificate-management -- both of which bring their own, non-fixed topbar --
 * carried 80px of empty space above it.
 *
 * :not(.has-hero) rather than a second rule resetting it: a hero supplies its own
 * clearance through --bts-hero-pt, and the two selectors have equal specificity,
 * so as separate rules the outcome depended on which came last in the file.
 */
body.bts-has-header:not(.has-hero) {
  padding-top: var(--bts-header-h);
}

@media (max-width: 991.98px) {
  body.bts-has-header:not(.has-hero) {
    padding-top: var(--bts-header-h-mobile);
  }
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--bts-font-heading);
  font-weight: var(--bts-weight-bold);
  line-height: var(--bts-leading-tight);
  color: var(--bts-text);
  margin-top: 0;
  margin-bottom: var(--bts-space-4);
}

h1 { font-size: var(--bts-text-4xl); font-weight: var(--bts-weight-black); }
h2 { font-size: var(--bts-text-3xl); }
h3 { font-size: var(--bts-text-2xl); }
h4 { font-size: var(--bts-text-xl); font-weight: var(--bts-weight-semibold); }
h5 { font-size: var(--bts-text-lg); font-weight: var(--bts-weight-semibold); }
h6 { font-size: var(--bts-text-md); font-weight: var(--bts-weight-semibold); }

p {
  margin-top: 0;
  margin-bottom: var(--bts-space-4);
}

a {
  color: var(--bts-green-accent);
  text-decoration: none;
  transition: color var(--bts-duration) var(--bts-ease);
}

a:hover {
  /* Lighter on hover, not darker: darker means less contrast here. */
  color: #7dffc6;
}

img,
svg {
  max-width: 100%;
  height: auto;
}

/* Visible keyboard focus. Several pages set outline:none without a
   replacement, leaving the site unusable by keyboard. */
:focus-visible {
  outline: 3px solid var(--bts-green-accent);
  outline-offset: 2px;
  border-radius: var(--bts-radius-sm);
}

/* Skip link for keyboard and screen-reader users. */
.bts-skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 10000;
  padding: var(--bts-space-3) var(--bts-space-5);
  /* Same inversion as the primary button, for the same reason. */
  background: var(--bts-green-accent);
  color: #06121f;
  font-weight: var(--bts-weight-bold);
  border-radius: 0 0 var(--bts-radius) 0;
}

.bts-skip-link:focus {
  left: 0;
  color: #06121f;
}

.bts-visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ==========================================================================
   Layout primitives
   ========================================================================== */

.bts-container {
  width: 100%;
  max-width: var(--bts-container);
  margin-inline: auto;
  padding-inline: var(--bts-space-5);
}

/* Keep Bootstrap's container on the same measure as ours so the header,
   footer and page body align to one grid. They previously did not: the header
   used a 1320px bespoke container while page content used Bootstrap's 1140px,
   which is why the logo sat left of the content edge. */
.container,
.container-lg,
.container-xl {
  max-width: var(--bts-container);

  /* 24px, not Bootstrap's default 12px.
     A .row applies a negative inline margin of half its --bs-gutter-x, which
     the container's padding is meant to absorb. The gutter utilities set that
     variable on the row only, so a `row g-5` (3rem gutter, -24px margin) against
     a container padded for the default 1.5rem gutter (12px) overhangs by 12px
     each side and puts a horizontal scrollbar on the page. Padding to 24px
     covers gutters up to g-5, which is the largest the site uses. */
  padding-inline: var(--bts-space-6);
}

/* ---- Section ------------------------------------------------------------ */

.bts-section {
  padding-block: var(--bts-section-y);
  position: relative;
}

.bts-section--tight {
  padding-block: var(--bts-section-y-tight);
}

/* A barely-there lift, not a colour change.
   The page background is already dark, so an alternating band only needs to be a
   few percent lighter to read as a separate band; anything stronger looks like a
   box sitting on the page. */
.bts-section--surface {
  background: var(--bts-surface);
}

/* Deeper than the page, for a section that should recede rather than lift.
   Translucent, not --bts-bg-deep itself: an opaque band here blankets the
   backdrop for the height of the section, which is what put a flat panel across
   the middle of the about page. Recede by darkening what is behind, not by
   replacing it. */
.bts-section--dark {
  background: rgba(6, 16, 29, 0.45);
}

@media (max-width: 767.98px) {
  .bts-section {
    padding-block: var(--bts-space-12);
  }

  .bts-section--tight {
    padding-block: var(--bts-space-10);
  }
}

/* ---- Section heading --------------------------------------------------- */
/* One heading treatment site-wide. Pages each had their own eyebrow/rule/
   centring combination, so section headers sat at different heights and
   alignments from page to page. */

.bts-section-head {
  max-width: 780px;
  /* Was --bts-space-12 (48px) between a section's heading and its content. */
  margin: 0 auto var(--bts-space-6);
  text-align: center;
}

.bts-section-head--start {
  margin-inline: 0;
  text-align: left;
}

.bts-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--bts-space-2);
  margin-bottom: var(--bts-space-2);
  padding: 5px 12px;
  border: 1px solid var(--bts-green-a20);
  border-radius: var(--bts-radius-pill);
  background: var(--bts-green-a08);
  color: var(--bts-green-dark);
  font-size: var(--bts-text-xs);
  font-weight: var(--bts-weight-bold);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* The eyebrow is already a green chip on a dark ground everywhere now, so the
   dark-section override only needs to brighten it slightly. */
.bts-eyebrow {
  border-color: var(--bts-accent-a30);
  background: rgba(0, 255, 157, 0.07);
  color: var(--bts-green-accent);
}

.bts-section-title {
  margin-bottom: var(--bts-space-3);
  /* Down from --bts-text-3xl, which capped at 36px. "Positions we are hiring
     for" at 36px pushed the job list itself below the fold. */
  font-size: var(--bts-text-2xl);
  font-weight: var(--bts-weight-black);
  letter-spacing: -0.01em;
}

.bts-section-lead {
  margin-bottom: 0;
  color: var(--bts-text-muted);
  font-size: var(--bts-text-md);
  line-height: var(--bts-leading);
}



/* Gradient text. Includes the standard fallback so the text is never
   invisible where background-clip is unsupported. */
.bts-gradient-text {
  background: var(--bts-gradient-accent);
  -webkit-background-clip: text;
  background-clip: text;
  /* Fallback is the accent, not the base green: on this background the base green
     is too dark to read if background-clip is unsupported. */
  color: var(--bts-green-accent);
}

@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .bts-gradient-text {
    -webkit-text-fill-color: transparent;
    color: transparent;
  }
}

/* ==========================================================================
   Hero
   --------------------------------------------------------------------------
   One hero, used by every interior page. Replaces ~30 near-identical
   per-page hero blocks (.networks-hero, .azure-hero, .waf-hero ...) which had
   drifted to two different paddings and three different heading sizes.
   ========================================================================== */

/* Transparent.
   The page background is now the same dark treatment the hero used to supply on
   its own, so painting a gradient here would show as a visible seam where the
   band ends. What is left is the spacing and a hairline to separate the header
   area from the content. */
.bts-hero {
  position: relative;
  padding-top: var(--bts-hero-pt);
  padding-bottom: var(--bts-hero-pb);
  border-bottom: 1px solid var(--bts-border);
  color: var(--bts-ink);
  overflow: hidden;
  isolation: isolate;
}

/* The body already supplies a glow at the top of the page, so the hero adds
   nothing of its own. */

.bts-hero > .container,
.bts-hero > .bts-container {
  position: relative;
  z-index: 1;
}

/* Title and eyebrow on one row, so the eyebrow labels the page instead of
   stacking above it and adding its own height. Wraps on narrow screens. */
.bts-hero__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--bts-space-3);
}

/* 2xl, not the 4xl display size this started at. A two-line service title alone
   occupied 115px at 48px. The home page keeps the large size; that one genuinely
   is a landing screen. */
.bts-hero__title {
  /* One step down the scale. This is a breadcrumb header, not a banner -- it
     names the page, and at 30px it was competing with the section headings
     below it for the same job. */
  font-size: var(--bts-text-xl);
  margin-bottom: 0;
  color: var(--bts-ink);
  font-weight: var(--bts-weight-bold);
  letter-spacing: -0.01em;
  line-height: var(--bts-leading-tight);
}

/* Only the error page uses this now. render_hero() no longer emits a lead: on
   every interior page it restated the meta description and pushed the real
   content below the fold. */
.bts-hero__lead {
  max-width: 68ch;
  margin: var(--bts-space-3) 0 0;
  color: rgba(255, 255, 255, 0.88);
  font-size: var(--bts-text-md);
  line-height: var(--bts-leading-snug);
}

/* Centred hero variant centres its own lead too, so the two never disagree. */
.bts-hero--center {
  text-align: center;
}

.bts-hero--center .bts-hero__lead {
  margin-inline: auto;
}

.bts-hero--center .bts-hero__head,
.bts-hero--center .bts-breadcrumb {
  justify-content: center;
}

.bts-hero--center .bts-hero__actions,
.bts-hero--center .bts-hero__badges {
  justify-content: center;
}

.bts-hero__badges {
  display: flex;
  flex-wrap: wrap;
  gap: var(--bts-space-2);
  /* Top margin, not bottom: see the note on .bts-hero__lead. */
  margin: var(--bts-space-4) 0 0;
  padding: 0;
  list-style: none;
}

.bts-hero__badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border: 1px solid var(--bts-accent-a30);
  border-radius: var(--bts-radius-pill);
  background: rgba(255, 255, 255, 0.08);
  color: var(--bts-white);
  font-size: var(--bts-text-sm);
  font-weight: var(--bts-weight-medium);
}

.bts-hero__badge i {
  color: var(--bts-green-accent);
}

.bts-hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--bts-space-3);
  /* Top margin so the gap exists only when actions are present. */
  margin-top: var(--bts-space-4);
}

@media (max-width: 991.98px) {
  .bts-hero {
    padding-top: var(--bts-hero-pt-mobile);
    padding-bottom: var(--bts-hero-pb-mobile);
  }
}

@media (max-width: 767.98px) {
  /* One scrolling row instead of wrapping.
     Four badges wrap to three or four rows on a phone, which pushed the service
     page banner to 637px. Scrolling keeps every badge -- they carry real claims,
     so hiding them would be losing content -- while bounding the band to a
     single row. */
  /* Two-class selector on purpose. `.bts-hero--center .bts-hero__badges` sets
     justify-content: center, and a media query adds no specificity of its own, so
     a single-class rule here loses and the centring survives. That matters
     because a centred flex container that overflows pushes its first items out
     past its own start edge, where no amount of scrolling brings them back --
     the first badge sat 170px off-screen and was simply unreachable. */
  .bts-hero .bts-hero__badges,
  .bts-hero--center .bts-hero__badges {
    flex-wrap: nowrap;
    justify-content: flex-start;
    overflow-x: auto;
    scrollbar-width: none;

    /* No negative-margin bleed here: making the element wider than its container
       put horizontal scroll on the page itself. */
    scroll-padding-inline: var(--bts-space-2);
  }

  .bts-hero__badges::-webkit-scrollbar {
    display: none;
  }

  .bts-hero__badge {
    flex: 0 0 auto;
  }
}

/* Breadcrumb inside a hero. */
.bts-breadcrumb {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--bts-space-2);
  margin-bottom: var(--bts-space-2);
  padding: 0;
  list-style: none;
  font-size: var(--bts-text-sm);
}

.bts-breadcrumb li {
  display: inline-flex;
  align-items: center;
  gap: var(--bts-space-2);
  color: rgba(255, 255, 255, 0.65);
}

.bts-breadcrumb li:not(:last-child)::after {
  content: '/';
  color: rgba(255, 255, 255, 0.35);
}

.bts-breadcrumb a {
  color: rgba(255, 255, 255, 0.8);
}

.bts-breadcrumb a:hover {
  color: var(--bts-green-accent);
}

.bts-breadcrumb [aria-current='page'] {
  color: var(--bts-green-accent);
  font-weight: var(--bts-weight-semibold);
}

/* ==========================================================================
   Buttons
   ========================================================================== */

.bts-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--bts-space-2);
  padding: 13px 28px;
  border: 2px solid transparent;
  border-radius: var(--bts-radius-pill);
  font-family: inherit;
  font-size: var(--bts-text-base);
  font-weight: var(--bts-weight-semibold);
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition: var(--bts-transition);
}

.bts-btn:disabled,
.bts-btn[aria-disabled='true'] {
  opacity: 0.6;
  cursor: not-allowed;
  transform: none !important;
}

/*
 * Bright fill, dark label -- the inverse of the light-theme treatment.
 *
 * White on the brand green measures 3.11:1, which fails WCAG AA for text at this
 * size. Darkening the green enough to carry white text (#00754a or below) drops
 * the button's own contrast against the dark page to 3.15:1 and makes it look
 * muddy. Inverting instead gives dark ink on the accent green at 13.6:1, and the
 * fill reads at 13.6:1 against the page, so the button is both more legible and
 * more vivid than it was.
 */
.bts-btn--primary {
  background: var(--bts-gradient-action);
  border-color: transparent;
  color: #06121f;
  font-weight: var(--bts-weight-bold);
  box-shadow: 0 6px 20px rgba(0, 255, 157, 0.22);
}

.bts-btn--primary:hover:not(:disabled) {
  color: #06121f;
  transform: translateY(-2px);
  box-shadow: 0 12px 32px rgba(0, 255, 157, 0.34);
}

/* Accent, not base green: the base green does not have enough contrast against
   this background for a border and label. */
.bts-btn--outline {
  background: transparent;
  border-color: var(--bts-green-a30);
  color: var(--bts-green-accent);
}

.bts-btn--outline:hover:not(:disabled) {
  background: var(--bts-green);
  border-color: var(--bts-green);
  color: var(--bts-white);
  transform: translateY(-2px);
}

/* On a dark background the outline needs white, not brand green, to hold
   contrast. Several pages used the light-background variant on the hero. */
.bts-btn--on-dark {
  background: rgba(255, 255, 255, 0.10);
  border-color: rgba(255, 255, 255, 0.5);
  color: var(--bts-white);
}

.bts-btn--on-dark:hover:not(:disabled) {
  background: var(--bts-white);
  border-color: var(--bts-white);
  color: var(--bts-dark);
  transform: translateY(-2px);
}

.bts-btn--sm {
  padding: 9px 18px;
  font-size: var(--bts-text-sm);
}

.bts-btn--lg {
  padding: 16px 34px;
  font-size: var(--bts-text-md);
}

.bts-btn--block {
  width: 100%;
}

/* ==========================================================================
   Progress and busy states
   --------------------------------------------------------------------------
   Submitting a form used to change the button's label to "Sending..." and
   nothing else. On a fast connection that is a flicker, and on a slow one it is
   indistinguishable from a click that did not register -- which is what made
   pressing "Send message" feel like nothing had happened.

   Two pieces, both driven from JS: a bar across the top of the viewport for any
   in-flight request or navigation, and a spinner inside the button itself so the
   feedback is where the pointer already is.

   Both animate transform and opacity only, per the rule on .bts-backdrop.
   ========================================================================== */

/*
 * The blocker: a scrim that takes the page out of use while a request is in
 * flight, so a second click cannot land on a form that is already submitting.
 *
 * It is shown on a delay, not immediately -- see initProgress in site.js. A
 * local navigation completes in well under 100ms and flashing a blur over the
 * whole page for that long reads as a glitch, so only waits long enough to
 * notice get one.
 *
 * pointer-events: auto is the point of the element: .bts-progress above is inert
 * so it cannot swallow clicks, and this one exists to swallow them.
 */
.bts-blocker {
  position: fixed;
  inset: 0;
  z-index: calc(var(--bts-z-megamenu) + 5);
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: auto;
  background: rgba(6, 16, 29, 0.55);
  backdrop-filter: blur(3px);
  opacity: 0;
  transition: opacity 180ms var(--bts-ease);
}

.bts-blocker[hidden] {
  display: none;
}

.bts-blocker.is-visible {
  opacity: 1;
}

/* The label rides with the spinner so the block is explained rather than just
   imposed. Panel-solid, because page text behind a translucent one would show
   through the blur and make it unreadable. */
.bts-blocker__note {
  display: inline-flex;
  align-items: center;
  gap: var(--bts-space-3);
  padding: 14px 22px;
  border: 1px solid var(--bts-border-strong);
  border-radius: var(--bts-radius-pill);
  background: var(--bts-panel-solid);
  box-shadow: 0 18px 44px rgba(0, 0, 0, 0.5);
  color: var(--bts-ink);
  font-size: var(--bts-text-base);
  font-weight: var(--bts-weight-semibold);
}

/* Larger than the in-button spinner: this one is the only thing on screen. */
.bts-blocker .bts-spinner i {
  width: 4px;
  height: 18px;
}

/* Above the header, which is fixed: the bar has to sit on top of it, not under. */
.bts-progress {
  position: fixed;
  inset-inline: 0;
  top: 0;
  z-index: calc(var(--bts-z-megamenu) + 10);
  height: 3px;
  overflow: hidden;
  pointer-events: none;
  background: rgba(0, 255, 157, 0.10);
}

.bts-progress[hidden] {
  display: none;
}

.bts-progress__bar {
  width: 38%;
  height: 100%;
  background: var(--bts-gradient-action);
  box-shadow: 0 0 14px rgba(0, 255, 157, 0.55);
  /* Its resting pose, so it is off-screen before the animation starts rather
     than flashing at the left edge for one frame. */
  transform: translateX(-110%);
}

@media (prefers-reduced-motion: no-preference) {
  .bts-progress__bar {
    animation: bts-progress-sweep 1.15s ease-in-out infinite;
  }
}

/* Without a motion preference the bar cannot sweep, so it fills the track and
   stays -- still a clear "something is happening", just not a moving one. */
@media (prefers-reduced-motion: reduce) {
  .bts-progress__bar {
    width: 100%;
    transform: none;
    opacity: 0.75;
  }
}

@keyframes bts-progress-sweep {
  from { transform: translateX(-110%); }
  to   { transform: translateX(320%); }
}

/*
 * The button spinner, built from the logo's own motif.
 *
 * The mark is three skewed parallel bars, so the busy state is those three bars
 * pulsing in sequence rather than a generic ring. currentColor means it inherits
 * the button's ink -- dark on the primary button's bright fill, light on the
 * outline ones -- so it never needs a per-variant colour.
 */
.bts-spinner {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  flex-shrink: 0;
}

.bts-spinner i {
  width: 3px;
  height: 14px;
  border-radius: 1px;
  background: currentColor;
  transform: skewX(-18deg);
  opacity: 0.35;
}

@media (prefers-reduced-motion: no-preference) {
  .bts-spinner i {
    animation: bts-spinner-pulse 0.9s ease-in-out infinite;
  }

  .bts-spinner i:nth-child(2) { animation-delay: 0.15s; }
  .bts-spinner i:nth-child(3) { animation-delay: 0.30s; }
}

@keyframes bts-spinner-pulse {
  0%, 100% { opacity: 0.30; transform: skewX(-18deg) scaleY(0.55); }
  50%      { opacity: 1;    transform: skewX(-18deg) scaleY(1); }
}

/* A busy button keeps its size so the layout does not jump when the label
   changes, and stops taking the pointer. */
.bts-btn[aria-busy="true"] {
  cursor: progress;
}

/* ==========================================================================
   Cards
   ========================================================================== */

/* Translucent, so the grid and gradient behind it stay visible through the card.
   No backdrop-filter. It used to carry one, and a blur is a low-pass filter:
   what shows through here is a smooth gradient plus a hex grid at 0.07 alpha,
   both already low-frequency, so the blur was invisible. It was not free
   though -- every card is its own blur region, recomputed each frame as it
   moves across the background, and a page carries four to six of them. The
   translucent fill is what makes this read as glass, not the blur. Only the
   header and the mobile nav overlay keep one, because those genuinely blur page
   content rather than the background. */
.bts-card {
  display: flex;
  flex-direction: column;
  height: 100%;
  /* 24px -> 20px. Small per card, but these come in grids of three to six. */
  padding: var(--bts-space-5);
  border: 1px solid var(--bts-border);
  border-radius: var(--bts-radius-lg);
  background: var(--bts-panel);
  transition: var(--bts-transition);
}

.bts-card:hover {
  border-color: var(--bts-green-a30);
  background: var(--bts-panel-hover);
  transform: translateY(-4px);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.28);
}

/* Accent rule that animates in on hover. */
.bts-card--accent {
  position: relative;
  overflow: hidden;
}

.bts-card--accent::before {
  content: '';
  position: absolute;
  inset-inline: 0;
  top: 0;
  height: 3px;
  background: var(--bts-gradient-rule);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--bts-duration) var(--bts-ease);
}

.bts-card--accent:hover::before {
  transform: scaleX(1);
}

.bts-card__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  margin-bottom: var(--bts-space-4);
  border: 1px solid var(--bts-green-a20);
  border-radius: var(--bts-radius);
  background: var(--bts-green-a12);
  /* Accent green: the base green is too dark to read as an icon here. */
  color: var(--bts-green-accent);
  font-size: 24px;
  flex-shrink: 0;
  transition: var(--bts-transition);
}

.bts-card:hover .bts-card__icon {
  background: var(--bts-gradient-action);
  border-color: transparent;
  color: #06121f;
}

.bts-card__title {
  margin-bottom: var(--bts-space-3);
  font-size: var(--bts-text-lg);
  font-weight: var(--bts-weight-semibold);
}

.bts-card__text {
  margin-bottom: 0;
  color: var(--bts-text-muted);
  font-size: var(--bts-text-base);
}

/* Push trailing content to the bottom so cards in a row align. */
.bts-card__footer {
  margin-top: auto;
  padding-top: var(--bts-space-4);
}

/* ---- Feature list ------------------------------------------------------ */

.bts-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.bts-list li {
  position: relative;
  padding-left: 28px;
  margin-bottom: var(--bts-space-3);
  color: var(--bts-text-muted);
  font-size: var(--bts-text-base);
  line-height: var(--bts-leading-snug);
}

.bts-list li::before {
  content: '\f26e'; /* bootstrap-icons check-circle-fill */
  position: absolute;
  left: 0;
  top: 1px;
  font-family: 'bootstrap-icons';
  font-size: 15px;
  color: var(--bts-green-accent);
  line-height: 1.4;
}

/* ---- Chip / tag -------------------------------------------------------- */

.bts-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 12px;
  border: 1px solid var(--bts-border);
  border-radius: var(--bts-radius-pill);
  background: var(--bts-surface-alt);
  color: var(--bts-ink-muted);
  font-size: var(--bts-text-xs);
  font-weight: var(--bts-weight-medium);
}

.bts-chip--brand {
  border-color: var(--bts-green-a30);
  background: var(--bts-green-a12);
  color: var(--bts-green-accent);
}

/* ==========================================================================
   Call to action band
   ========================================================================== */

/* Deliberately compact.
   This band previously used the full section rhythm (64px top and bottom) with a
   large heading and stacked lead, which came to 306px -- about two thirds of a
   laptop viewport -- on every single page. It is a closing prompt, not a section:
   it should read in one glance and not push the footer off the fold. */
/*
 * A dark panel with a green edge, not a green slab.
 *
 * Filled with the brand gradient, the lead text sat at 3.11:1 against it -- below
 * AA -- and a full-width bright green band is overbearing on a dark page. An
 * elevated dark panel with an accent top edge keeps it as a distinct closing
 * prompt while letting the normal ink colours apply.
 */
.bts-cta {
  position: relative;
  padding-block: var(--bts-space-8);
  border-top: 1px solid var(--bts-green-a30);
  background:
    radial-gradient(ellipse 80% 140% at 12% 0%, rgba(0, 168, 89, 0.20), transparent 70%),
    rgba(0, 0, 0, 0.28);
  color: var(--bts-ink);
  overflow: hidden;
}

/* Title, lead and buttons sit on one line where there is room, and stack only
   when there genuinely is not. */
.bts-cta .container {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--bts-space-5);
}

.bts-cta__text {
  flex: 1 1 26rem;
  min-width: 0;
}

.bts-cta__title {
  margin-bottom: 4px;
  color: var(--bts-ink);
  font-size: var(--bts-text-xl);
  font-weight: var(--bts-weight-bold);
  line-height: var(--bts-leading-snug);
}

.bts-cta__lead {
  margin: 0;
  color: var(--bts-ink-muted);
  font-size: var(--bts-text-base);
  line-height: var(--bts-leading-snug);
}

.bts-cta__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--bts-space-3);
  flex-shrink: 0;
}

@media (max-width: 767.98px) {
  .bts-cta {
    padding-block: var(--bts-space-6);
  }

  .bts-cta .container {
    flex-direction: column;
    align-items: stretch;
    gap: var(--bts-space-4);
  }

  /* The flex basis must be reset when the container switches to a column.
     `flex: 1 1 26rem` applies its basis along the main axis, so once that axis
     is vertical the text block is given a 26rem *height* -- which inflated this
     band to 586px on a phone. */
  .bts-cta__text {
    flex: 0 0 auto;
  }

  .bts-cta__actions {
    flex-direction: column;
  }

  .bts-cta__actions .bts-btn {
    width: 100%;
  }
}

/* ==========================================================================
   Forms
   ========================================================================== */

.bts-field {
  margin-bottom: var(--bts-space-5);
}

.bts-label {
  display: block;
  margin-bottom: var(--bts-space-2);
  color: var(--bts-text);
  font-size: var(--bts-text-base);
  font-weight: var(--bts-weight-semibold);
}

.bts-label.is-required::after {
  content: ' *';
  /* Lightened: #dc3545 on this background is below AA. */
  color: #ff8b95;
}

.bts-input,
.bts-select,
.bts-textarea {
  display: block;
  width: 100%;
  padding: 12px 16px;
  border: 1.5px solid var(--bts-border-strong);
  border-radius: var(--bts-radius);
  /* Darker than the panel it sits in, so a field reads as recessed. */
  background: rgba(0, 0, 0, 0.22);
  color: var(--bts-ink);
  font-family: inherit;
  font-size: var(--bts-text-base);
  line-height: 1.5;
  transition: border-color var(--bts-duration) var(--bts-ease),
              box-shadow var(--bts-duration) var(--bts-ease);
}

.bts-input:focus,
.bts-select:focus,
.bts-textarea:focus {
  border-color: var(--bts-green);
  box-shadow: 0 0 0 4px var(--bts-green-a12);
  outline: none;
}

.bts-input[aria-invalid='true'],
.bts-select[aria-invalid='true'],
.bts-textarea[aria-invalid='true'] {
  border-color: #ff8b95;
}

/* Placeholders need an explicit colour: the browser default is tuned for a light
   field and is close to invisible on a dark one. */
.bts-input::placeholder,
.bts-textarea::placeholder {
  color: var(--bts-ink-subtle);
  opacity: 1;
}

/* Native select dropdowns and date pickers inherit the OS light scheme unless
   told otherwise, which renders black-on-black options. */
.bts-select,
.bts-input {
  color-scheme: dark;
}

.bts-textarea {
  min-height: 140px;
  resize: vertical;
}

.bts-field__error {
  display: block;
  margin-top: 6px;
  color: #ff8b95;
  font-size: var(--bts-text-sm);
  font-weight: var(--bts-weight-medium);
}

.bts-field__hint {
  display: block;
  margin-top: 6px;
  color: var(--bts-ink-subtle);
  font-size: var(--bts-text-sm);
}

/* ---- Alert ------------------------------------------------------------- */

.bts-alert {
  display: flex;
  align-items: flex-start;
  gap: var(--bts-space-3);
  padding: var(--bts-space-4) var(--bts-space-5);
  margin-bottom: var(--bts-space-5);
  border: 1px solid transparent;
  border-radius: var(--bts-radius);
  font-size: var(--bts-text-base);
}

.bts-alert i {
  flex-shrink: 0;
  font-size: 18px;
  line-height: 1.4;
}

.bts-alert--success {
  border-color: var(--bts-green-a30);
  background: rgba(0, 168, 89, 0.12);
  color: #8ff0be;
}

.bts-alert--error {
  border-color: rgba(255, 139, 149, 0.32);
  background: rgba(220, 53, 69, 0.12);
  color: #ffb3ba;
}

.bts-alert--info {
  border-color: rgba(0, 136, 204, 0.32);
  background: rgba(0, 136, 204, 0.12);
  color: #9ad8f5;
}

/* ==========================================================================
   Utilities
   ========================================================================== */

.bts-stack > * + * { margin-top: var(--bts-space-4); }
.bts-stack-sm > * + * { margin-top: var(--bts-space-2); }
.bts-stack-lg > * + * { margin-top: var(--bts-space-6); }

.bts-text-center { text-align: center; }
.bts-text-muted { color: var(--bts-text-muted); }
.bts-text-brand { color: var(--bts-green); }

.bts-mb-0 { margin-bottom: 0 !important; }

/* ---- Card grid ---------------------------------------------------------- */

/* Replaces the Bootstrap row/col approach for card groups.
   A flex row equalises heights only within a single line, so a group of three
   cards in a two-column layout left the third one alone on its own line at its
   natural height -- the "Email" card on the contact page was 176px against its
   siblings' 201px. grid-auto-rows: 1fr makes every row the same height, so all
   cards match regardless of how they wrap. */
/*
 * Flexbox, not grid, and the reason is the last row.
 *
 * This was `grid-template-columns: repeat(auto-fit, minmax(..., 1fr))` with
 * `grid-auto-rows: 1fr`, which matched every card's height across every row. It
 * had one flaw with no CSS grid answer: grid places items into tracks, so an
 * incomplete final row fills tracks from the first one and sits hard left under a
 * full row above it. Six cards in a four-column grid left two adrift on the left
 * with a gap the width of two cards to their right. `justify-content` cannot fix
 * it -- that aligns the whole track set, not one row within it.
 *
 * This is the same reason .bts-logo-grid is flex. Flex lays out lines rather than
 * tracks, so `justify-content: center` centres each line independently, and the
 * short last row sits under the middle of the row above.
 *
 * What is given up: heights now equalise within a line rather than across all of
 * them, because `align-items: stretch` works per line. In practice that reads
 * better here -- `grid-auto-rows: 1fr` matched every card to the tallest card
 * anywhere in the grid, so on the data centre page one seven-bullet card left
 * several inches of dead space inside every card in the row above it.
 *
 * `flex: 0 1 basis` rather than `1 1`: allowed to shrink, never to grow. Growing
 * would stretch a two-card last row to half the container each, which is the
 * opposite of centring them. min-width: 0 lets a long unbroken string shrink
 * rather than force the line wider, and max-width: 100% is what stops a 22rem
 * basis overflowing a 272px container at 320px -- the bug the old min() guard was
 * there for.
 */
.bts-card-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: stretch;
  gap: var(--bts-space-5);
}

.bts-card-grid > * {
  flex: 0 1 var(--bts-card-min, 17rem);
  min-width: 0;
  max-width: 100%;
}

/* Column-count variants, for groups that should not reflow purely on width. */
.bts-card-grid--2 { --bts-card-min: 22rem; }
.bts-card-grid--4 { --bts-card-min: 13rem; }

/* Fills its flex line. Needed on the nested case because align-items: stretch
   only reaches the direct child; a .bts-card inside a wrapper needs telling. */
.bts-card-grid > .bts-card,
.bts-card-grid > * > .bts-card {
  height: 100%;
}

/* Kept for the Bootstrap rows that remain. Equalises within a line only; use
   .bts-card-grid for anything that can wrap. */
.bts-row-cards > [class*='col-'] {
  display: flex;
  margin-bottom: var(--bts-space-6);
}

/* Scrollbar, matched to the brand rather than left at three different
   gradients as it was across pages. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--bts-green) var(--bts-bg-deep);
}

::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}

::-webkit-scrollbar-track {
  background: var(--bts-bg-deep);
}

::-webkit-scrollbar-thumb {
  border-radius: var(--bts-radius-pill);
  background: var(--bts-green);
  border: 2px solid var(--bts-bg-deep);
}

::-webkit-scrollbar-thumb:hover {
  background: var(--bts-green-dark);
}

/* ---- Print ------------------------------------------------------------- */

@media print {
  #header,
  #footer,
  .mobile-menu,
  .nav-overlay,
  .bts-cta,
  .bts-skip-link {
    display: none !important;
  }

  /* Print resets to a light document: the dark theme would waste ink and the
     grid overlay would render as noise. */
  html,
  body {
    padding-top: 0;
    color: #000;
    background: #fff;
  }

  body.bts-theme::before,
  body.bts-theme::after {
    display: none;
  }

  .bts-card,
  .bts-section--surface,
  .bts-section--dark {
    background: #fff;
    border-color: #ccc;
    color: #000;
  }

  a[href^='http']::after {
    content: ' (' attr(href) ')';
    font-size: 11px;
  }
}
