/* ============================================================
   GTI Mobilise 2026 — Base styles & shared components
   ============================================================ */

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

html {
  scroll-behavior: smooth;
  overflow-x: clip;
  /* The header is fixed, so in-page anchors must clear it. */
  scroll-padding-top: calc(var(--header-h-scrolled) + 24px);
  /* Lime thumb on a white track. White reads cleanest against the page, at the
     cost of contrast: lime on white is 1.06:1, so the thumb is found by its
     edges rather than its colour. Cool-gray was 1.51:1 if it ever needs to be
     easier to see; sage, the obvious pairing, was 1.01:1 and unusable.
     `scrollbar-color` inherits, so this reaches the mobile menu and any other
     scroll container — the card rails are unaffected, they hide theirs outright. */
  scrollbar-color: var(--color-electric-lime) #FFFFFF;
}

/* `scrollbar-color` is a single pair with no state, so the browser derives the
   dragged thumb itself — darkening lime, which lands on a dark green. Where the
   pseudo-elements exist they take that decision back, but only while
   `scrollbar-color` is auto: set to anything else it wins outright and these
   rules are ignored (measured — the declared 12px width had no effect until the
   property was handed back). So Chrome and Safari get `auto` and style through
   the pseudo-elements instead; Firefox has no pseudo-elements and keeps the
   standard property above, including its own derived drag colour.

   Unprefixed, not `html::-webkit-scrollbar`, because `scrollbar-color` inherited
   and this has to reach the same nested scroll containers. The card rails still
   hide theirs outright — `.m-rail::-webkit-scrollbar` out-specifies this.

   15px is what Chrome already reserves here, so taking the scrollbar over costs
   no reflow. macOS overlay scrollbars ignore all of it and keep their own look. */
@supports selector(::-webkit-scrollbar) {
  html { scrollbar-color: auto; }

  ::-webkit-scrollbar { width: 15px; height: 15px; }
  ::-webkit-scrollbar-track { background: #FFFFFF; }
  ::-webkit-scrollbar-thumb {
    background: var(--color-electric-lime);
    border-radius: 8px;
  }
  /* Held while the thumb is being dragged. Cool Gray taken down to a dark
     grey — the palette has no dark neutral between #121212 and #B9BFCA, and
     the brand black reads as black rather than grey at this size. */
  ::-webkit-scrollbar-thumb:active { background: #4A4F58; }
}

/* Safari before 18.2 and Chrome before 121 take neither the standard property
   nor the selector() test above, so they need the same rules stated plainly. */
@supports not (selector(::-webkit-scrollbar)) {
  @supports not (scrollbar-color: auto) {
    html::-webkit-scrollbar { width: 15px; height: 15px; }
    html::-webkit-scrollbar-track { background: #FFFFFF; }
    html::-webkit-scrollbar-thumb {
      background: var(--color-electric-lime);
      border-radius: 8px;
    }
    html::-webkit-scrollbar-thumb:active { background: #4A4F58; }
  }
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  margin: 0;
  background: var(--bg-page);
  color: var(--text-body-dark);
  font-family: var(--font-sans);
  font-size: var(--text-body-size);
  line-height: var(--text-body-lh);
  letter-spacing: var(--tracking-body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img, svg { display: block; max-width: 100%; }
h1, h2, h3, h4, p, figure, blockquote { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
button { font: inherit; letter-spacing: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
/* !important because this is an invariant, not a default: a hidden tab panel
   must never render. Without it the rule sits at specificity (0,2,0), which a
   scoped `.panel-class[data-astro-cid-…]` ties and then wins on source order —
   so scoping a tab section would silently reveal every panel at once. */
[role="tabpanel"][hidden] { display: none !important; }

a { color: var(--color-emerald); text-decoration: none; }
a:hover { color: var(--color-emerald-deep); text-decoration: underline; text-underline-offset: 3px; }

:focus-visible {
  outline: 2px solid var(--color-electric-lime);
  outline-offset: 3px;
  border-radius: 2px;
}

::selection { background: var(--color-electric-lime); color: var(--color-primary-black); }

/* ---------- Layout ---------- */
.container {
  width: 100%;
  max-width: calc(var(--page-max) + 2 * var(--page-gutter));
  margin-inline: auto;
  padding-inline: var(--page-gutter);
}

.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

/* ---------- Type ---------- */
.h-display {
  font-size: var(--text-display-size);
  line-height: var(--text-display-lh);
  font-weight: 300;
  letter-spacing: var(--tracking-display);
}
.h-section {
  font-size: var(--text-section-size);
  line-height: var(--text-section-lh);
  font-weight: 300;
  letter-spacing: var(--tracking-display);
}
.h-title-lg {
  font-size: var(--text-title-lg-size);
  line-height: var(--text-title-lg-lh);
  font-weight: 400;
  letter-spacing: var(--tracking-body);
}
.h-card {
  font-size: var(--text-card-size);
  line-height: var(--text-card-lh);
  font-weight: 500;
  letter-spacing: var(--tracking-body);
}
.text-lede { font-size: var(--text-lede-size); line-height: var(--text-lede-lh); }
.text-small { font-size: var(--text-small-size); line-height: var(--text-small-lh); }
.mono {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--mono-size);
  line-height: var(--mono-lh);
  letter-spacing: var(--tracking-mono);
  text-transform: uppercase;
}
.tab-btn { text-transform: uppercase; }
/* Keep proper-noun org names out of the uppercase treatment */
.convo-card-org.mono { text-transform: none; }

/* ---------- Buttons & pills ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--spacing-2);
  padding: 12px 28px;
  min-height: 48px;
  border-radius: var(--radius-pill);
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--mono-lg-size);
  line-height: 1.5;
  letter-spacing: var(--tracking-mono);
  text-transform: uppercase;
  color: var(--color-primary-black);
  white-space: nowrap;
  text-decoration: none;
  transition: filter 120ms ease, transform 120ms ease;
}
.btn:hover { color: var(--color-primary-black); text-decoration: none; filter: brightness(1.05); }
.btn:active { transform: translateY(1px); }
/* `display: inline-flex` above outranks the UA's [hidden]{display:none}, so a
   button hidden by script kept its box and its space. Any .btn that gets hidden
   — the newsletter submit swapping for its reply — needs this to actually go. */
.btn[hidden] { display: none; }
.btn--lime  { background: var(--color-electric-lime); }
.btn--white { background: #FFFFFF; }
.btn--nav   { min-height: 36px; padding: 10px 24px; font-size: var(--mono-size); line-height: var(--mono-lh); }
.btn .btn-icon { flex: 0 0 auto; }
.btn .btn-icon--down { transform: rotate(90deg); }

/* Small mono label pill (section eyebrows, stat labels) */
.label-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--spacing-1);
  padding: 4px 20px;
  min-height: 24px;
  border-radius: var(--radius-pill);
  background: #FFFFFF;
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--mono-size);
  line-height: var(--mono-lh);
  letter-spacing: var(--tracking-mono);
  color: var(--color-primary-black);
  white-space: nowrap;
  text-transform: uppercase;
  /* Sit above the SectionTransition glow. That glow lives in a sibling element
     at z-index 2, so without this it washes over a white pill on a dark
     section. Must clear 2 — z-index 1 still loses to it. */
  position: relative;
  z-index: 3;
}
.label-pill--dark { background: var(--color-primary-black); color: #FFFFFF; }
.label-pill--legislation {
  align-self: flex-start;
  width: fit-content;
  max-width: 100%;
}

/* Tiny chips on cards */
.chip {
  display: inline-flex;
  align-items: center;
  padding: 4px 20px;
  min-height: 24px;
  border-radius: var(--radius-pill);
  background: #FFFFFF;
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: var(--chip-size);
  line-height: 1.6;
  letter-spacing: var(--tracking-mono);
  color: var(--color-primary-black);
  white-space: nowrap;
  text-transform: uppercase;
}

/* Bordered tab (sector / problem switchers) */
.tab-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--spacing-2);
  padding: 8px 16px 5px;
  min-height: 40px;
  border: 1px solid currentColor;
  border-bottom-width: 2px;
  background: transparent;
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--mono-lg-size);
  line-height: 1.5;
  letter-spacing: var(--tracking-mono);
  white-space: nowrap;
  transition: background-color 120ms ease, color 120ms ease;
}

/* ---------- Rules & dividers ---------- */
.dashed-rule { border: 0; border-top: 1px dashed var(--color-cool-gray); margin: 0; }
.dashed-rule--lime { border-top-color: var(--color-electric-lime); }
.dashed-rule--white { border-top-color: var(--color-alpha-24); }

.title-rule { width: 80px; border: 0; border-top: 2px solid var(--color-primary-black); margin: 0; }

/* Keep repeated service-page rows fluid until four full cards fit. */
.svc-quote .svc-quote-text {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 940px;
}

@media (min-width: 640px) and (max-width: 1535px) {
  .am-line-grid {
    grid-template-columns: 1fr 1fr !important;
  }

  .svc-quote .svc-quote-text { white-space: normal !important; }
}

@media (max-width: 479px) {
  .label-pill {
    max-width: 100%;
    height: auto;
    white-space: normal;
    text-align: center;
  }

  /* Keeps the 16px mono label (--mono-lg-size) — a narrow viewport wraps the
     label, it does not shrink it. */
  .btn {
    max-width: 100%;
    padding-inline: 16px;
    white-space: normal;
    text-align: center;
  }

  .orbit-statement__content,
  .orbit-statement__cta { width: 100%; }

  .footer-certs {
    flex-wrap: wrap;
    gap: 12px;
  }

  .footer-cta-btn { width: 100%; }
}

/* Stepped "tab" section divider — svg/tab-divider.svg inlined per section.
   Color = the colour of the section the tab belongs to. */
.tab-divider { display: block; width: 100%; height: var(--tab-divider-h); }
.tab-divider svg {
  display: block;
  width: 100%;
  /* Only the middle tab is painted (the path closes along y=0, so the left and
     right thirds are transparent), which hangs the tab down from the box's top
     edge with its tip on the bottom.

     The pair below is one mechanism: grow the paint by 2px, then lift it by the
     same 2px, so the fill spans -2px..100% of the box. Whichever way the box is
     rotated, that surplus lands on the edge the divider abuts — down dividers
     overlap the section above them, --up dividers (rotated 180deg) overlap the
     section below — so the fill can never stop a sub-pixel short of it and let
     the page background through. It is why --up needs no offset of its own.

     Measured, because this is the third attempt at it: 16 sub-pixel offsets x
     both directions x device-scale 0.75/0.9/1.1/1.25/2. Before, --up leaked
     cream on 6-9 of the 16 offsets at every fractional scale; after, both
     directions are clean at all five. Do NOT stack a further margin on top of
     this: -3px of lift against +2px of height falls 1px short at the far edge,
     which is the regression in tasks/lessons.md. */
  height: calc(100% + 2px);
  margin-top: -2px;
}
.tab-divider--up {
  transform: rotate(180deg);
  /* Sits a pixel into the section below.

     `translate`, not `position: relative` + `top`. A divider that positions
     itself — .line-tab-top is absolute at `inset: 0 0 auto` — out-specifies both
     of those from its own component and loses the seal entirely, which is how one
     divider in 119 ended up with a visible seam while the rest were sealed. The
     individual `translate` property is applied before `transform`, so it composes
     with the rotation above instead of competing with a consumer's `inset`, and
     it cannot be cancelled by one.

     On the wrapper, NOT on the svg: the box is rotated, so an offset on the svg
     is mirrored onto the abutting edge and cuts its 2px overshoot to 1px.
     Measured — that leaks on all 16 sub-pixel offsets at 0.75/0.9/1.1/1.25,
     while shifting the whole box keeps the overshoot intact (0 at every scale). */
  translate: 0 1px;
}
/* The mirror of it for a bottom divider, which abuts the section above instead:
   pulled 2px up into it rather than 1px down. The svg's own overshoot covers the
   tab's fill, but the box edge still meets the section on a fractional boundary
   and a 1px row of page background showed through there — measured on the centre
   column as single cream rows between two runs of the section's colour.

   `translate` rather than a margin: several of these carry spacing of their own
   (.tech-tab-bottom 52px, .why-tab-bottom 96px), and a transform moves the paint
   without touching the layout that spacing sets up — and unlike `top` it applies
   whatever position a component gives the divider. */
.tab-divider:not(.tab-divider--up) {
  translate: 0 -2px;
}

/* Mobile redraw of the same tab.
   The inlined path is a 1440x18 viewBox carrying preserveAspectRatio="none", so
   at 390 the x axis is squashed 3.7x while the 18px height is untouched. Its
   shoulders are only ~7px wide in that viewBox, which lands at ~2px on a phone:
   the curves collapse into hard corners and the tab reads far narrower than the
   mobile export draws it. Both are the same defect — a fixed viewBox stretched
   to a box it was not drawn for.

   Redrawn here at the proportions the phone actually renders (390x18, so the
   curve keeps its aspect ratio across the whole mobile band) and to the export's
   own measure: shoulders from 14.1% to 85.9%, flat bottom 22.2% to 78.1%. Each
   shoulder is one symmetric cubic with horizontal tangents at both ends, so it
   meets the flat top and the flat bottom without the straight diagonal segment
   the desktop path steps through.

   Applied as a mask on .tab-divider rather than by editing the path, because
   the path is inlined into 24 files and only this class is shared. The size
   repeats the svg rule's 2px overshoot so the tip still clears the seam. */
@media (max-width: 639px) {
  .tab-divider {
    background: currentColor;
    -webkit-mask: var(--tab-divider-mask) top center / 100% calc(100% + 2px) no-repeat;
    mask: var(--tab-divider-mask) top center / 100% calc(100% + 2px) no-repeat;
  }
  .tab-divider > svg { display: none; }
}


/* ---------- Decorative dot grid: wide, sparse, diagonal fade ----------
   Used by two sections on the careers page. It lived in CareersWhy's `is:global`
   block and CareersRoles borrowed it by leakage — the comment there even said
   "shared with Open roles". Declared once here so both get it on purpose, and so
   scoping either component cannot break the other. */
.dot-grid {
  justify-self: end;
  /* The site's one lattice: 3px dot on a 16px pitch, both axes. This field used
     to run 2.6px on a stretched 24x18 grid — the only place with its own dot
     size and the only non-square pitch. Width and height are whole multiples of
     16 so no column or row is clipped. */
  --dot-grid-width: round(down, min(624px, 100%), 16px);
  --dot-grid-height: 112px;   /* 7 whole rows at the 16px pitch */
  /* Stone at 0.55, like every other field on a light section. The near-black at
     0.32 this replaces was the only dark lattice on a light background. */
  --dot-grid-color: var(--color-stone);
  --dot-grid-opacity: 0.55;
  --dot-fade-angle: 215deg;
  --dot-fade-start: 30%;
  --dot-fade-end: 85%;
}

@media (max-width: 899px) {
  .dot-grid { display: none; }
}

/* ---------- Section pull-quote and accents ----------
   Used by sector sections, the "three lives" cross-sell and several heroes.
   These were previously declared identically inside seven separate hero
   components, so any page whose hero happened not to declare them would have
   rendered the quote unstyled. Declared once here instead. */
.svc-accent,
.press-accent { color: var(--color-emerald); }
.svc-lime   { color: var(--color-electric-lime); }

.svc-quote {
  display: flex;
  align-items: center;
  gap: 24px;
}
.svc-quote-line { flex: 1 1 auto; height: 6px; color: var(--color-emerald); opacity: 0.6; }
.svc-quote-line svg { width: 100%; height: 6px; }
.svc-quote-line--lime { color: var(--color-electric-lime); }
.svc-quote-line--white { color: #FFFFFF; opacity: 0.7; }
.svc-quote-text {
  flex: 0 0 auto;
  font-size: var(--text-card-size);
  line-height: var(--text-card-lh);
  text-align: center;
}

@media (max-width: 1099px) {
  .svc-quote { flex-direction: column; }
  .svc-quote-line { width: 100%; }
  .svc-quote-text { white-space: normal !important; }
}

/* Shared quote connectors: diamond at the outer edge, dotted line fading inward. */
:where([class$="-quote-line"], [class*="-quote-line "]) {
  position: relative;
  height: 5px !important;
  opacity: 1 !important;
}

:where([class$="-quote-line"], [class*="-quote-line "]) svg {
  display: none !important;
}

:where([class$="-quote-line"], [class*="-quote-line "])::before {
  content: "";
  position: absolute;
  z-index: 1;
  left: 0;
  top: 50%;
  width: 5px;
  height: 5px;
  background: currentColor;
  transform: translateY(-50%) rotate(45deg);
}

:where([class$="-quote-line"], [class*="-quote-line "])::after {
  content: "";
  position: absolute;
  inset: 1.5px 0 1.5px 7px;
  background: repeating-linear-gradient(90deg, currentColor 0 2px, transparent 2px 9px);
  -webkit-mask-image: linear-gradient(90deg, #000 12%, transparent 100%);
  mask-image: linear-gradient(90deg, #000 12%, transparent 100%);
}

:where([class$="-quote-line"], [class*="-quote-line "]):last-child::before {
  left: auto;
  right: 0;
}

:where([class$="-quote-line"], [class*="-quote-line "]):last-child::after {
  inset-inline: 0 7px;
  -webkit-mask-image: linear-gradient(270deg, #000 12%, transparent 100%);
  mask-image: linear-gradient(270deg, #000 12%, transparent 100%);
}

/* ---------- Chamfered (cut-corner) cards — geometry from the Figma vectors.
   Applied to every card surface site-wide (brand rule). ---------- */
.chamfer,
.card,
.stat-card,
.value-card,
.about-stat,
.forces-card,
.ip-stat-card,
.tech-card,
.client-quote,
.footer-cta-card,
.blog-card-media,
.convo-card-media {
  clip-path: var(--chamfer-clip);
}

/* ---------- Lime glow behind dark sections ----------
   One appearance for every section glow: colour, size, softness, strength.
   Each section supplies only its own PLACEMENT (top/left/transform), since
   that depends on what the glow sits behind. */
.section-glow {
  position: absolute;
  width: 461px;
  height: 358px;
  border-radius: 50%;
  background: rgba(183, 254, 2, 0.74);
  opacity: 0.2;
  filter: blur(90px);
  pointer-events: none;
}

/* ---------- Light dot-grid decoration, anchored top-right ----------
   One class for the shared 772x116 dot-grid SVG. Keeps the SVG's intrinsic
   aspect ratio (height: auto) and fades it out to the left so it never
   collides with section copy. Per-section vertical offset: --dots-bottom. */
/* Shared top-right decorative dot grid. Positioning only — the grid itself is
   <GradientDotGrid>, so tone, geometry and fade are set as its custom
   properties. --dots-bottom is the per-usage knob for vertical placement. */
.light-dots-right {
  position: absolute;
  /* Bottom-anchored, so the field's last row sits on the bottom of the head
     text however the heading wraps. This used to be top-anchored with a magic
     offset, which needed hand-correcting at eight call sites and still drifted
     the moment a headline ran to two lines. --dots-bottom is the escape hatch
     for a section that genuinely wants to sit off the baseline. */
  bottom: var(--dots-bottom, 0);
  right: 0;
  pointer-events: none;
  /* Whole columns at the 16px pitch: 800 / 16 = 50. */
  --dot-grid-width: 800px;
  /* The site's one dot field: 16px pitch, 3px dot, eight whole rows. A 24px
     pitch lived here for a while, but every section opened after it landed
     overrode it back to 16 inline — nine call sites against seven, plus a
     .light-dots-dense modifier whose whole purpose was to undo it. The default
     is now what everyone was already asking for, so those overrides come out. */
  --dot-grid-height: 128px;
  --dot-gap: 16px;
  --dot-size: 3px;
  --dot-grid-color: var(--color-stone);
  --dot-grid-opacity: 0.55;
  --dot-fade-angle: 270deg;
  --dot-fade-start: 0%;
  --dot-fade-end: 50%;
}

/* Narrow modifier: the same lattice over a shorter field, held at full strength
   across the whole width instead of fading out halfway. Pair it with
   .light-dots-right, which still supplies the positioning and the geometry —
   the pitch it used to restate is now the shared default. */
.light-dots-dense {
  --dot-grid-width: 304px;   /* 19 whole columns */
  --dot-grid-opacity: 0.72;
  --dot-fade-end: 100%;
}

@media (max-width: 899px) {
  .light-dots-right { display: none; }
}


/* ---------- Cards ---------- */
.card { position: relative; border-radius: var(--radius-card); }
.card--sage { background: var(--color-sage); }
.card--emerald { background: var(--color-emerald); color: #FFFFFF; }
.card--white { background: #FFFFFF; }

/* Chrome paints autofilled fields with its own pale blue and ignores
   background-color, so any field went white the moment a saved value went in.
   The inset shadow repaints it in the surface the field sits on, which callers
   set through --autofill-bg; the page background is the right default, and the
   footer card overrides it with its own sage. The long transition delay stops
   the highlight being repainted on focus. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
select:-webkit-autofill,
select:-webkit-autofill:hover,
select:-webkit-autofill:focus,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--color-primary-black);
  -webkit-box-shadow: 0 0 0 1000px var(--autofill-bg, var(--bg-page)) inset;
  transition: background-color 100000s ease-in-out 0s;
}

/* ---------- Hero (shared page-top pattern: treated image, tab divider) ---------- */
.hero {
  position: relative;
  /* Capped to the viewport so the hero always fits above the fold on laptops.
     svh (not vh) stays stable when mobile browser chrome collapses. The tab
     divider is a sibling AFTER this section, so its height plus a little
     breathing room must be reserved or it lands below the fold on 13"/14".

     Note the 840px cap is reached at 1440px wide (58.3vw), so past that the
     hero keeps widening but stops growing taller. That deepens the vertical
     crop `cover` applies to .hero-bg, and heroes anchored low (90%) lose the
     top of the frame. Those heroes re-anchor at min-width:1440px rather than
     raising this cap, which would make every hero taller on wide screens. */
  min-height: min(
    clamp(640px, 58.3vw, 840px),
    calc(100svh - var(--tab-divider-h) - 24px)
  );
  display: flex;
  align-items: stretch;
  color: #FFFFFF;
  overflow: hidden;
}
.hero-bg {
  position: absolute;
  inset: 0 0 -18px;
}
/* The hero art is an <Image /> filling .hero-bg rather than a background-image,
   so Astro can emit a srcset for it — a CSS url() gets one fixed file at every
   viewport. Each hero sets its own `object-position` (the old
   `background-position`) under its `-hero-bg` prefix. */
.hero-bg-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.hero-fold {
  display: none;
}
.hero-inner {
  position: relative;
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  /* Single source of hero spacing for every hero on the site. Fluid on viewport
     HEIGHT: computes to the approved 180/64/48 on tall displays and only
     compresses where it must. The 150px floor still clears the 112px header. */
  gap: clamp(40px, 5svh, 48px);
  padding-top: clamp(150px, 20svh, 180px);
  padding-bottom: clamp(56px, 7svh, 64px);
}
.hero-head { display: flex; flex-direction: column; align-items: flex-start; gap: 24px; }
.hero-title { max-width: 12em; }
.hero-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 24px;
}
.hero-rule {
  flex: 1 1 auto;
  height: 2px;
  margin-bottom: 24px;
  border: 0;
  background: radial-gradient(circle, var(--color-electric-lime) 1px, transparent 1.25px) left center / 8px 2px repeat-x;
}
@media (prefers-reduced-motion: no-preference) {
  .js .hero-rule { clip-path: inset(0 100% 0 0); }
  .js .hero-foot.is-visible .hero-rule { animation: hero-rule-reveal 2.4s steps(96, end) 400ms infinite; }
}
@keyframes hero-rule-reveal { to { clip-path: inset(0); } }
.hero-buttons {
  flex: 0 0 auto;
  display: flex;
  gap: 8px;
  align-items: center;
}
.hero-tab { color: #0B0D09; margin-top: -1px; }

@media (max-width: 899px) {
  .hero { min-height: 0; }
  .hero-inner { gap: 40px; }
  .hero-title { font-size: 2.75rem; line-height: 1.05; }
  .hero-foot { flex-direction: column; align-items: stretch; }
  /* Nine heroes set `align-items: flex-end` on their own `-hero-foot` class for
     the desktop row. Astro scopes that to `.x-hero-foot[data-astro-cid-…]`
     (0,2,0), which beats the `.hero-foot` reset above (0,1,0) — so on the two
     that never reset it (tech, disassembly) the stacked column stayed
     right-aligned and the CTAs shrank to a 193px pillar against the right
     gutter. `align-self` on the children is not in that fight at all: no hero
     sets it, so this wins everywhere and fixes the whole family at once. */
  .hero-foot > * { align-self: stretch; }
  .hero-rule { display: none; }
  .hero-buttons { flex-direction: column; align-items: stretch; }
  .hero-buttons .btn { width: 100%; }
  .hero-fold { right: -300px; }
}

/* Keep the full desktop navigation inside 13-inch laptop viewports site-wide. */
@media (max-width: 1284px) {
  .main-nav, .header-cta { display: none !important; }
  .nav-toggle { display: inline-flex !important; align-items: center; justify-content: center; position: relative; z-index: 70; }
  .header-bar { min-height: 104px; }
  body.menu-open .site-header { color: var(--color-primary-black); }
  body.menu-open { overflow: hidden; }
}

/* ---------- Value cards (Retention / Regeneration / Reinvention — shared) ---------- */
.value-card {
  position: relative;
  min-height: 296px;
  border-radius: var(--radius-card);
  padding: var(--card-pad);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.value-card--retention { background: var(--color-lime-leaf); color: var(--color-primary-black); }
.value-card--regeneration { background: var(--color-moss); color: #FFFFFF; }
.value-card--reinvention { background: var(--color-forest); color: #FFFFFF; }
.value-card--emerald { background: var(--color-emerald); color: #FFFFFF; }

/* ---------- Corner language (Tweaks: body[data-corners]) ---------- */
body[data-corners="soft"] .chamfer,
body[data-corners="soft"] .card,
body[data-corners="soft"] .value-card,
body[data-corners="soft"] .stat-card,
body[data-corners="soft"] .about-stat,
body[data-corners="soft"] .forces-card,
body[data-corners="soft"] .ip-stat-card,
body[data-corners="soft"] .tech-card,
body[data-corners="soft"] .client-quote,
body[data-corners="soft"] .footer-cta-card,
body[data-corners="soft"] .blog-card-media,
body[data-corners="soft"] .convo-card-media { clip-path: none; border-radius: 20px; }
body[data-corners="sharp"] .chamfer,
body[data-corners="sharp"] .card,
body[data-corners="sharp"] .value-card,
body[data-corners="sharp"] .stat-card,
body[data-corners="sharp"] .about-stat,
body[data-corners="sharp"] .forces-card,
body[data-corners="sharp"] .ip-stat-card,
body[data-corners="sharp"] .tech-card,
body[data-corners="sharp"] .client-quote,
body[data-corners="sharp"] .footer-cta-card,
body[data-corners="sharp"] .blog-card-media,
body[data-corners="sharp"] .convo-card-media { clip-path: none; border-radius: 0; }

/* ---------- Scroll reveal (disabled under prefers-reduced-motion) ---------- */
.reveal { opacity: 1; transform: none; }
@media (prefers-reduced-motion: no-preference) {
  .js .reveal {
    opacity: 0;
    transform: translateY(24px);
    transition: opacity 600ms cubic-bezier(0.22, 1, 0.36, 1), transform 600ms cubic-bezier(0.22, 1, 0.36, 1);
  }
  .js .reveal.is-visible { opacity: 1; transform: none; }
}

/* ---------- Rhombus-capped dashed rule ----------
   The flanking line used either side of a centred statement. Geometry lived
   only inside QuoteRule; the press engagements CTA hand-rolled a plain dashed
   border instead and so lost the rhombus caps. One definition, `currentColor`
   for tone, mirrored automatically when it is the last child. */
.rule-line {
  position: relative;
  flex: 1 1 auto;
  min-width: 48px;
  height: 5px;
}
.rule-line::before {
  content: "";
  position: absolute;
  z-index: 1;
  top: 50%;
  left: 0;
  width: 5px;
  height: 5px;
  background: currentColor;
  transform: translateY(-50%) rotate(45deg);
}
.rule-line::after {
  content: "";
  position: absolute;
  inset: 1.5px 0 1.5px 7px;
  background: repeating-linear-gradient(90deg, currentColor 0 2px, transparent 2px 9px);
  -webkit-mask-image: linear-gradient(90deg, #000 12%, transparent 100%);
  mask-image: linear-gradient(90deg, #000 12%, transparent 100%);
}
.rule-line:last-child::before { right: 0; left: auto; }
.rule-line:last-child::after {
  inset-inline: 0 7px;
  -webkit-mask-image: linear-gradient(270deg, #000 12%, transparent 100%);
  mask-image: linear-gradient(270deg, #000 12%, transparent 100%);
}

/* Authored line break inside a heading or paragraph. Block on desktop so the
   copy breaks where the design breaks it, inline below 768px where a forced
   break would strand a short line. Used from `set:html` copy — a <br> cannot do
   this, because a break is unconditional.

   `SectorServices` and `MissionValues` predate this and declare their own
   prefixed equivalents at the same 767px threshold; converge them when either is
   next touched. */
.line-break { display: block; }

@media (max-width: 767px) {
  .line-break { display: inline; }
}

/* ============================================================
   Mobile layer (<= 639px) — the Figma mobile exports.
   Kept last in the file so it wins the source-order tie against the
   earlier max-width blocks (notably the 899px hero rules).
   Anything expressible as a token lives in tokens.css instead.
   ============================================================ */
@media (max-width: 639px) {
  /* Every CTA drops to 12px with a 20x13 arrow.

     !important on both, which base.css otherwise reserves for invariants: about
     twenty components set their own .btn-icon size — and BlogIndex its own .btn
     font-size — inside scoped blocks. Those selectors carry Astro's scope
     attribute, so they outrank an unscoped rule here and would each keep the
     desktop figure. The alternative is a mobile override in twenty files.

     Only .btn is targeted. The arrowed read-more links (.coverage-read,
     .engage-read, .cloc-link) are text links rather than CTAs and keep theirs. */
  .btn { font-size: 12px !important; }
  .btn .btn-icon { width: 20px !important; height: 13px !important; }

  /* Hero copy hard-codes its line breaks with <br> so the desktop headline
     lands where the export draws it. At phone widths the measure is roughly a
     third as wide, so every one of those breaks fires early and leaves a short
     orphan line mid-thought. Drop them and let the text wrap to its own box.
     `.contact-hero` is named separately because it is the one hero section that
     does not carry `.hero`. */
  .hero br,
  .contact-hero br { display: none; }

  /* 42/42, replacing the 44/46.2 set in the 899px hero block above. */
  .hero-title {
    font-size: var(--text-display-size);
    line-height: var(--text-display-lh);
  }

  /* 20/24. Not a token override: .h-card shares --text-card-size with
     .svc-quote-text, which stays at 24 on mobile. */
  .h-card {
    font-size: 1.25rem;
    line-height: 1.2;
  }

  .label-pill {
    padding: 4px 16px;
    font-size: 0.6875rem;   /* 11px mono */
  }

  /* --text-body-size drops to 14/20 for CARD copy; the export sets hero prose
     two steps larger, at 18/24. Set on .hero-foot, not on each hero's own copy
     wrapper (.hero-copy, .tech-hero-copy, .svc-hero-copy …): every hero's
     paragraphs live inside .hero-foot and none of them declare a font-size, so
     one inherited value covers the whole family and no hero can drift from it.
     .label-pill sits in .hero-head, and .text-lede on the tagline still wins
     its own 18px. */
  .hero-foot {
    font-size: 1.125rem;
    line-height: 1.3333;
  }

  /* Section ledes — the same story one step down. Full-width section intros
     inherited the CARD 14/20 too, so the ramp ran 18px hero prose straight to
     14px section lede with nothing between; the export sets these at 16/24.
     The lede is structurally "the paragraph right after the section heading":
     that selector matches 41 elements across the 15 pages, every one of them
     358px wide section intro copy and not one of them card copy, so it fixes
     the whole family at once and cannot reach inside a card. 16/24 is the
     desktop --text-body-size, restored here rather than re-tokenised, because
     the token still has to mean 14/20 for everything a card inherits. */
  .h-section + p {
    font-size: 1rem;
    line-height: 1.5;
  }

  /* The orbit ring is deliberately full-bleed (Figma runs it to ~x5 of a 390
     frame) and its four triangular markers ride the ring's edge, so ~10px of a
     decorative marker sits past the viewport at the ring's widest point. That
     was always invisible — html { overflow-x: clip } hides it — but it still
     counted toward documentElement.scrollWidth, and it was the whole of the 1px
     horizontal scroll on /, /about and /mission-values. Clipped at the section
     that owns the decoration, so the ring keeps its measured width; NOT on body,
     which would also kill every .m-rail below. */
  .orbit-statement { overflow: clip; }

  /* 44px tall pill (was 48): padding-block drives the height, min-height only
     backstops it. 44 is the accessible tap-target floor. .btn--nav is excluded
     — it is the compact 36px variant and this block loads after it. */
  .btn:not(.btn--nav) {
    min-height: 44px;
    padding-block: 10px;
  }

  /* Fixed rather than fluid on viewport height: clamp(150px, 20svh, 180px)
     moves the whole hero block by up to 30px depending on how much browser
     chrome is showing. The export puts the eyebrow's top edge at y=144. */
  .hero-inner { padding-top: 144px; }

  /* ---------- .m-rail — one-card-wide horizontal snap rail ----------
     Figma lays every multi-card grid out this way on mobile. Measured off the
     390px export: first card left edge x=16, card 318 wide, 8px gap, the next
     card starting at x=342 and clipped by the viewport for a deliberate 48px
     peek that advertises the scroll.

     The rail cancels the .container gutter with a negative margin and re-adds
     it as padding, so the scroll track spans the full viewport while the first
     card still starts on the page gutter. Both are needed: without the padding
     the first card sits flush against the viewport edge, and without the
     negative margin the track ends at the gutter and the peeking card is cut
     off 16px early. `scroll-padding-inline` repeats the gutter so a snapped
     card lands on x=16 rather than x=0. */
  .m-rail {
    /* Declared here, not in :root — they are this utility's own geometry, and
       custom properties inherit down to the cards that need them. */
    --m-rail-gap: 8px;
    --m-rail-peek: 48px;

    display: flex;
    flex-wrap: nowrap;
    gap: var(--m-rail-gap);
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: var(--page-gutter);
    padding-inline: var(--page-gutter);
    margin-inline: calc(var(--page-gutter) * -1);
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .m-rail::-webkit-scrollbar { display: none; }

  /* Card width is derived, not the literal 318, so the peek stays at the
     measured 48px across the whole mobile band instead of only at 390 — a
     fixed 318 leaves an 18px peek at 360 and a 297px one at 639, and at 320
     the card overhangs the viewport entirely.

     The percentage resolves against the rail's CONTENT box, which the
     padding-inline above has already reduced by two gutters, so the subtraction
     only has to remove what sits to the RIGHT of the card:

       card = (viewport - 2*gutter) - gap - peek + gutter
            = viewport - gutter - gap - peek
       390  -   16    -  8  -  48  = 318   ✓ the Figma measurement

     Percentages, not 100vw: on a narrow desktop window with a classic
     scrollbar 100vw counts the scrollbar and the peek grows by its width. */
  .m-rail > * {
    flex: 0 0 calc(100% - var(--m-rail-gap) - var(--m-rail-peek) + var(--page-gutter));
    min-width: 0;
    scroll-snap-align: start;
  }
}
