/* ═══════════════════════════════════════════════════════════════════════
   GCC Center for the Fourth Industrial Revolution - homepage concept
   Light institutional theme. GOIC blue + GCC gold (MOM §5).
   ═══════════════════════════════════════════════════════════════════ */

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

:root {
  /* ── the palette ───────────────────────────────────────────────────────
     The only colour literals in this file. Everything below is one of these or
     a mix of them, so there is exactly one place to change a brand colour and
     no second opinion about what the brand blue is.

     Four of them are the logo's own blues, taken from the mark rather than
     matched by eye - the client asked for the blue shades to be the logo
     guide's hexes, and these are what assets/logo-colored.svg is drawn in:

       --c-deep    #014368  the logo's darkest blue
       --c-blue    #094A75  the brand blue: buttons, links, the current state
       --c-azure   #1E6195  the logo's mid blue
       --c-sky     #A4DFF5  the logo's light blue, for type and marks on a dark ground

     The other three are not in the mark and are unchanged. The logo has no
     near-black and no hairline grey, and inventing one out of a blue would only
     put a fifth blue in the file that the guide cannot vouch for:

       --c-navy    the dark ground, and the ink
       --c-slate   the mid tone: secondary text, muted marks
       --c-mist    the pale surface and the hairline
       --c-gold    the GCC accent, also straight off the mark
  */
  --c-navy:  #141E26;
  --c-deep:  #014368;
  --c-blue:  #094A75;
  --c-azure: #1E6195;
  --c-slate: #4B7E99;
  --c-sky:   #A4DFF5;
  --c-mist:  #CBD8DF;
  --c-gold:  #DAA520;

  /* Blue scale. Every step the logo defines is now that exact hex rather than a
     mix computed toward it: 800 and 600 used to be interpolated between their
     neighbours, and the guide has a colour for both. */
  --blue-900: var(--c-navy);
  --blue-800: var(--c-deep);
  --blue-700: var(--c-blue);
  --blue-600: var(--c-azure);
  --blue-500: var(--c-slate);
  --blue-300: var(--c-sky);
  --blue-100: var(--c-mist);
  --blue-50:  color-mix(in srgb, var(--c-mist) 40%, #fff);

  /* Gold. The accent is #DAA520 exactly wherever it sits on a dark ground or
     carries no text. 700 is the same gold taken down against the navy so gold
     *type* on white clears 4.5:1 (5.2:1), and 300 is it lifted with white for
     the reverse case. */
  --gold-700: color-mix(in srgb, var(--c-gold) 56%, var(--c-navy));
  --gold-600: color-mix(in srgb, var(--c-gold) 84%, var(--c-navy));
  --gold-500: var(--c-gold);
  --gold-300: color-mix(in srgb, var(--c-gold) 66%, #fff);
  --gold-100: color-mix(in srgb, var(--c-gold) 17%, #fff);

  /* Ink. The two lighter steps are the navy walked toward the slate rather
     than toward grey, so secondary copy stays in the same family: 12.7:1 and
     8.3:1 on white. */
  --ink:   var(--c-navy);
  --ink-2: color-mix(in srgb, var(--c-navy) 76%, var(--c-slate));
  --ink-3: color-mix(in srgb, var(--c-navy) 46%, var(--c-slate));

  --surface:   #ffffff;
  --surface-2: color-mix(in srgb, var(--c-mist) 34%, #fff);
  --surface-3: var(--c-mist);
  --line: color-mix(in srgb, var(--c-navy) 14%, transparent);
  --line-soft: color-mix(in srgb, var(--c-navy) 8%, transparent);

  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --pad: clamp(1.25rem, 5vw, 5.5rem);

  /* ── the corner treatment ──
     Set by the client 2026-08-29: every button and every box is rounded on its
     top-left and bottom-right and square on the other two, so the silhouette
     carries a diagonal instead of being a plain rounded rectangle.

     Tokens rather than forty literal four-value shorthands, because the pairs
     have to stay in step. Half these boxes have a ring, a sheen or a glow drawn
     on a ::before at a small inset, and with a symmetric radius a mismatch of a
     pixel or two between the two never showed. With two square corners it shows
     there first, and it shows as a chipped corner.

     Circles, pills and the round seals are deliberately not in this - a 50%
     radius has no corners to treat, and a pill that is square on two of its
     four ends is not a pill any more. Neither are the hairline bits at 2px that
     are not boxes: a progress bar, a rule, a flag swatch inside a chip.

     Three sizes because the boxes are three sizes. Most of what these replace
     was 2px or 3px, which is to say square: the asymmetry does not read at all
     until the rounded pair is big enough to see, so the radius had to come up
     at the same time. */
  --r-box: 14px;
  --r-btn: 10px;
  --r-chip: 8px;
  --corner-box: var(--r-box) 0 var(--r-box) 0;
  --corner-btn: var(--r-btn) 0 var(--r-btn) 0;
  --corner-chip: var(--r-chip) 0 var(--r-chip) 0;
  --maxw: 1240px;

  /* ── the type scale ────────────────────────────────────────────
     Added 2026-08-29 on the client's instruction: the type is to be consistent
     across the site, and services.php in particular was reading too small.

     There was no scale before this, which is the whole problem. An audit of the
     five stylesheets found 178 literal font-size declarations using more than
     forty distinct values, over thirty of them packed into the 0.62rem-0.98rem
     band - 0.855, 0.86, 0.875, 0.885, 0.9, 0.92, 0.925rem all in use, sometimes
     on things sitting side by side. Nobody chose those differences; they are
     what happens when each component picks its own number. The reader cannot
     see a 0.02rem step as a distinction, only as a slight wrongness, and the
     cumulative effect is a page that looks small rather than one that looks
     hierarchical.

     Six steps, and six is deliberate. Fewer would flatten a page that genuinely
     has micro-labels, table cells and body copy on it at once; more would let
     the old drift back in. The steps are far enough apart to read as different
     sizes rather than as mistakes.

       --fs-micro  11.5px  all-caps labels, eyebrows, pills, list numerals
       --fs-tiny   12.8px  captions, meta, country codes, units
       --fs-xs     14px    dense UI: chips, table cells, pager, rail nav
       --fs-sm     15.2px  secondary body, facts, names
       --fs-base   16.8px  body copy - the default for anything read in
                           sentences
       --fs-md     18.4px  card and panel titles

     Note where the floor sits. The smallest reading size on the site is now
     --fs-sm at 15.2px, not the 0.82rem/13.1px that services.php had under its
     form lists; --fs-micro and --fs-tiny are for labels, which are scanned, not
     read. That single change is most of what the client was asking for.

     --fs-lead is separate and fluid because a lede is set against the width of
     its column rather than against the body text, and both pages' ledes were
     already clamps. It replaces two near-identical ones, clamp(1rem, 1.1vw,
     1.08rem) and clamp(1rem, 1.15vw, 1.14rem), which differed by a third of a
     pixel at the top and not at all at the bottom.

     Headings are not in here. They are fluid clamps tuned per component and the
     client did not raise them; a token set that tried to cover both the 5.6rem
     section numeral and a 14px table cell would be a scale in name only. */
  --fs-micro: 0.75rem;
  --fs-tiny:  0.8rem;
  --fs-xs:    0.875rem;
  --fs-sm:    0.95rem;
  --fs-base:  1.05rem;
  --fs-md:    1.15rem;
  --fs-lead:  clamp(1.08rem, 1.2vw, 1.2rem);
}

/* `clip`, not `hidden`: it contains the horizontal overshoot of the reveal
   animations without establishing a scroll container, so the fixed header and
   the news rail's own horizontal scrolling are unaffected. Without it, an
   element sliding in from the left briefly pushes the page ~10px wide on a
   phone, which reads as horizontal jitter while a section reveals. */
html { overflow-x: clip; }

body {
  margin: 0;
  background: var(--surface);
  color: var(--ink);
  font-family: 'IBM Plex Sans', 'Noto Kufi Arabic', system-ui, sans-serif;
  font-weight: 400;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 { margin: 0; line-height: 1.1; letter-spacing: -0.025em; font-weight: 600; }
p { margin: 0; }
a { color: inherit; }
img, svg { display: block; max-width: 100%; }

.wrap { width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: var(--pad); }

/* ─────────────────────────── shared bits ─────────────────────────── */
/* The eyebrow is gold. Set by the client 2026-08-29, against the previous
   --blue-600, which put the label in the same family as the heading under it
   and so read as a smaller line of the same thought rather than as a tag on
   it. Gold is the only other colour the identity carries and it is already
   what the eyebrow is on every dark ground, so this makes one rule out of two.

   --gold-700 and not --gold-300 because this is the light-ground value:
   --gold-300 is the gold mixed 34% toward white, which is 1.6:1 on --surface.
   The dark grounds keep --gold-300 through .section--dark .eyebrow and the
   hero's --hero-eyebrow, both of which already existed.

   The size is 0.72rem here and nowhere else, which is the second half of the
   instruction. It was not holding: .section__head p is one specificity step
   above .eyebrow and was catching every eyebrow that sits in a section head -
   which is nearly all of them - and sizing them at the lede's 1rem-1.1rem in
   --ink-2. That is why the hero's eyebrows rendered at 11.52px and gold while
   the section ones rendered at 16px and grey. The lede rules now say
   :not(.eyebrow), which is what .section__head--wide, .section__head--center
   and .eco__head had each already worked out locally. */
.eyebrow {
  margin: 0;
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--gold-700);
}
/* Now the same colour as the base rule. Kept rather than deleted because the
   class is on markup in services.php and the reference pages, and a modifier
   that stops existing is a silent change on pages nobody re-checked. */
.eyebrow--gold { color: var(--gold-700); }

/* The section rhythm, deliberately tighter than it was.
   Nine sections at the old 9vh gave ~1170px of padding and ~1450px of heads
   before a word of content, which on a long page reads as scrolling past
   furniture. The vh terms stay - the rhythm should still breathe with the
   viewport - the ceilings just come down, and the heads give up the largest
   step of their type scale, which was competing with the hero for weight
   rather than labelling a section. */
.section { padding-block: clamp(2.5rem, 5.5vh, 4.5rem); }
.section__head { max-width: 62ch; margin-bottom: clamp(1.5rem, 3vh, 2.25rem); }
.section__head h2 { font-size: clamp(1.75rem, 2.7vw, 2.35rem); margin-block: 0.5rem 0.7rem; }
/* :not(.eyebrow) - see the note on .eyebrow above. The eyebrow is a <p> in
   this head too, and without the exclusion this rule sizes and colours it. */
.section__head p:not(.eyebrow) { color: var(--ink-2); font-size: clamp(1rem, 1.15vw, 1.1rem); }

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.72rem 1.35rem;
  border: 1px solid transparent;
  border-radius: var(--corner-btn);
  font: inherit;
  font-size: 0.9rem;
  font-weight: 500;
  text-decoration: none;
  cursor: pointer;
  transition: background 0.3s var(--ease-out), color 0.3s var(--ease-out),
              border-color 0.3s var(--ease-out);
}
.btn--primary { background: var(--blue-700); color: #fff; }
.btn--primary:hover { background: var(--blue-800); }
.btn--ghost { border-color: var(--line); color: var(--ink); background: transparent; }
.btn--ghost:hover { border-color: var(--blue-600); color: var(--blue-700); }
/* The button for a dark band. Added 2026-08-29 with the services CTA.

   There was no variant for a dark ground before this and both existing ones are
   unusable on one: --primary is --blue-700 on white, which is a dark blue on a
   navy band, and --ghost is --ink text inside a --line border, both of which are
   drawn to sit on paper. Inverting is the whole trick - the ground is dark, so
   the button is the light thing on it, which is the same relationship --primary
   has to a white section and therefore reads as the same weight of action.

   Gold on hover rather than a lighter white. The dark sections already carry
   gold as their accent - the rule under the services head is --gold-500 and the
   news rail's CTA is --gold-300 - so the hover state is the section's own colour
   arriving rather than a new one being introduced for it. The text stays navy
   throughout: gold is a mid tone and white on it is the one combination here
   that would actually fail contrast.

   White literally, not var(--surface). --surface was the wrong token to reach
   for: it means "the colour of a plate sitting on this ground", and a dark band
   is entitled to redefine it to a dark one - .svsection--dark in
   css/services.css does exactly that, on purpose, so its cards sit above the
   band rather than below it. That turned all three "Apply for listing" buttons
   in the services Get Listed section into navy text on a navy pill, invisible
   in both languages, while the .btn--ghost download buttons beside them stayed
   legible because they name their own white. The whole point of this variant is
   that it is the light thing on a dark ground, so it has to say so itself -
   .btn--primary names #fff for the same reason. */
.btn--on-dark { background: #fff; color: var(--c-navy); }
.btn--on-dark:hover { background: var(--gold-300); color: var(--c-navy); }
.btn__arrow { transition: transform 0.3s var(--ease-out); }
.btn:hover .btn__arrow { transform: translateX(3px); }

.tag {
  display: inline-block;
  padding: 0.2rem 0.6rem;
  border-radius: var(--corner-chip);
  background: var(--blue-50);
  color: var(--blue-700);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  transition: background 0.3s var(--ease-out), color 0.3s var(--ease-out), transform 0.3s var(--ease-out);
}
.tag--gold { background: var(--gold-100); color: var(--gold-700); }
.tag--live { background: var(--gold-100); color: var(--gold-700); }

/* a small, honest marker on invented figures */
.placeholder-note {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-size: var(--fs-micro);
  color: var(--ink-3);
  letter-spacing: 0.04em;
}
.placeholder-note::before {
  content: '';
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--gold-500);
}

/* ─────────────────────────── header ───────────────────────────
   The bar is transparent while the hero is beneath it and only resolves into
   the solid light bar once the page scrolls into the light sections. Colours
   are tokenised so the same markup works on either ground; `is-over-hero` is
   toggled in js/main.js, and the inversion only applies when the hero is
   actually dark. */
.nav {
  --nav-ink: var(--ink);
  --nav-ink-2: var(--ink-2);
  --nav-ink-3: var(--ink-3);
  --nav-line: var(--line);
  --nav-accent: var(--blue-700);

  position: fixed;
  inset: 0 0 auto 0;
  z-index: 40;
  display: flex;
  align-items: center;
  gap: clamp(1rem, 3vw, 2.5rem);
  padding: 0.85rem var(--pad);
  background: rgba(255, 255, 255, 0.82);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--line-soft);
  transition: background 0.45s var(--ease-out), border-color 0.45s var(--ease-out),
              backdrop-filter 0.45s var(--ease-out),
              transform 0.4s var(--ease-out);
}

/* past the hero the bar retreats on the way down and returns on the way up */
.nav.is-hidden { transform: translateY(-100%); }

.nav.is-over-hero {
  background: transparent;
  backdrop-filter: none;
  border-bottom-color: transparent;
}

body[data-hero-theme="dark"] .nav.is-over-hero {
  --nav-ink: #ffffff;
  --nav-ink-2: rgba(255, 255, 255, 0.84);
  --nav-ink-3: rgba(255, 255, 255, 0.62);
  --nav-line: rgba(255, 255, 255, 0.3);
  --nav-accent: #ffffff;
}

/* the monogram has to survive both grounds */
.bm-ring  { fill: none; stroke: var(--blue-300); stroke-width: 1; }
.bm-nodes { fill: var(--blue-700); }
.bm-hub   { fill: var(--blue-800); }
.bm-core  { fill: var(--gold-500); }
body[data-hero-theme="dark"] .nav.is-over-hero .bm-ring  { stroke: rgba(255, 255, 255, 0.55); }
body[data-hero-theme="dark"] .nav.is-over-hero .bm-nodes { fill: #ffffff; }
body[data-hero-theme="dark"] .nav.is-over-hero .bm-hub   { fill: rgba(255, 255, 255, 0.28); }
body[data-hero-theme="dark"] .nav.is-over-hero .bm-core  { fill: var(--gold-300); }

/* The hero carries its own call to action, so the bar's copy would put two of
   the same control on one screen. The bar hands it over while the hero is
   behind it and takes it back the moment the hero is gone.
   It collapses its own width rather than being hidden with `display` or left
   in place at `opacity: 0` - the first shifts the links beside it 200-odd px
   every time the boundary is crossed, the second strands the language button
   that far from the edge for the whole of the hero. `grid-template-columns` is
   the one width that animates without laying out on every frame, and the child
   needs `min-width: 0` or its own content props the track back open.
   Only where the hero's copy is actually shown - on a viewport too short for
   it the bar keeps the only one. */
.nav__cta {
  display: grid;
  grid-template-columns: 1fr;
  transition: grid-template-columns 0.45s var(--ease-out),
              opacity 0.3s var(--ease-out);
}
/* `min-width: 0` lets the track win over the label, but padding and border are
   a floor it cannot shrink past - a 0fr track still measured 45px until these
   collapsed too. */
.nav__cta > .btn {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  transition: background 0.3s var(--ease-out), color 0.3s var(--ease-out),
              padding 0.45s var(--ease-out), border-width 0.45s var(--ease-out);
}
@media (min-height: 621px) {
  .nav.is-over-hero .nav__cta {
    grid-template-columns: 0fr;
    opacity: 0;
    pointer-events: none;
  }
  .nav.is-over-hero .nav__cta > .btn {
    padding-inline: 0;
    border-inline-width: 0;
  }
}

/* solid navy on near-black has almost no contrast - invert the CTA instead */
body[data-hero-theme="dark"] .nav.is-over-hero .btn--primary {
  background: #ffffff;
  color: var(--blue-900);
}
body[data-hero-theme="dark"] .nav.is-over-hero .btn--primary:hover {
  background: var(--blue-100);
}

.brand { display: flex; align-items: center; gap: 0.7rem; text-decoration: none; flex: none; }
.brand__mark { width: 34px; height: 34px; flex: none; }
.brand__text {
  font-size: 0.76rem;
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: 0.005em;
  color: var(--nav-ink);
}
.brand__text small { display: block; font-weight: 400; color: var(--nav-ink-3); letter-spacing: 0.06em; }

.nav__links { display: flex; gap: clamp(0.9rem, 2vw, 1.9rem); margin-inline-start: auto; }
.nav__links a {
  position: relative;
  font-size: 0.87rem;
  font-weight: 450;
  text-decoration: none;
  color: var(--nav-ink-2);
  padding-block: 0.4rem;
  transition: color 0.25s var(--ease-out);
}
.nav__links a::after {
  content: '';
  position: absolute;
  left: 0; bottom: 0;
  width: 100%; height: 1.5px;
  background: var(--nav-accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.35s var(--ease-out);
}
.nav__links a:hover { color: var(--nav-accent); }
.nav__links a:hover::after { transform: scaleX(1); }

.nav__actions { display: flex; align-items: center; gap: 0.7rem; flex: none; }
.lang,
.nav__login {
  border: 1px solid var(--nav-line);
  background: transparent;
  border-radius: var(--corner-chip);
  padding: 0.5rem 0.7rem;
  font: inherit;
  font-size: 0.8rem;
  color: var(--nav-ink-2);
  cursor: pointer;
  transition: border-color 0.3s var(--ease-out), color 0.3s var(--ease-out);
}
.lang:hover,
.nav__login:hover { border-color: var(--nav-accent); color: var(--nav-accent); }
/* The switcher was a dead <button>; it is now a real link to the other
   language, so it needs the two things a <button> gave it for free. */
a.lang { display: inline-block; text-decoration: none; line-height: 1.2; }
/* The sign-in chip carries a touch more weight than the language control, so
   the pair reads as "language, then action" rather than as two switches.

   Deliberately not `a.nav__login`, though it is only ever an <a>. The element
   selector is what `a.lang` above needs and what this one must not have: an
   (0,1,1) `display: inline-block` here would outrank the (0,1,0)
   `display: none` in the narrow-width block below, and a rule inside a media
   query does not beat a more specific rule outside one. The chip would then
   never hide, at any width. */
.nav__login {
  display: inline-block;
  text-decoration: none;
  line-height: 1.2;
  font-weight: 550;
  color: var(--nav-ink);
  white-space: nowrap;
}

/* The burger carries a word beside its lines - "Menu", "Close" once open -
   after gord.qa's mobile header, on the client's request 2026-09-30. The word
   is CSS content rather than markup so the thirteen templates that print this
   button stay untouched; :lang(ar) swaps it on the Arabic pages. The lines keep
   column 1 and their 5px rows, which the X transforms below are measured on. */
.nav__burger {
  display: none;
  height: 44px;
  grid-template-columns: 18px auto;
  align-content: center;
  column-gap: 0.55rem;
  row-gap: 5px;
  padding-inline: 0.8rem 0.95rem;
  border: 1px solid var(--nav-line);
  border-radius: var(--corner-chip);
  background: transparent;
  color: var(--nav-ink);
  font: inherit;
  cursor: pointer;
}
.nav__burger span { grid-column: 1; display: block; width: 18px; height: 1.5px; background: var(--nav-ink); transition: transform 0.35s var(--ease-out), opacity 0.2s; }
.nav__burger::after {
  content: "Menu";
  grid-column: 2;
  grid-row: 1 / span 3;
  align-self: center;
  font-size: 0.9rem;
  font-weight: 550;
  line-height: 1;
}
.nav__burger[aria-expanded="true"]::after { content: "Close"; }
:lang(ar) .nav__burger::after { content: "القائمة"; }
:lang(ar) .nav__burger[aria-expanded="true"]::after { content: "إغلاق"; }
.nav__burger[aria-expanded="true"] span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
.nav__burger[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav__burger[aria-expanded="true"] span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }

/* The mobile menu: a white panel that drops from under the bar, after gord.qa,
   replacing the full-screen sheet on 2026-09-30. Shown only where the inline
   links are hidden. --menu-top is the bar's bottom edge, written by each
   page's initMenu() as the menu opens; the bar is 79px on the inner pages and
   85px on the homepage, so no single fixed value fits. The reveal is a clip-path
   wipe, which slides the panel open without animating its height; the
   negative bottom inset when open lets the drop shadow out. */
.menu {
  position: fixed;
  inset: var(--menu-top, 80px) 0 auto 0;
  z-index: 39;
  max-height: calc(100dvh - var(--menu-top, 80px));
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--surface);
  display: grid;
  gap: 1.25rem;
  padding-bottom: 1.5rem;
  box-shadow: 0 14px 28px rgba(8, 24, 48, 0.14);
  clip-path: inset(0 0 100% 0);
  transition: clip-path 0.45s var(--ease-out);
}
.menu[hidden] { display: none; }
.menu.is-open { clip-path: inset(0 0 -3rem 0); }
.menu nav { display: grid; }
.menu a:not(.btn) {
  font-size: 1rem;
  font-weight: 450;
  text-decoration: none;
  color: var(--ink);
  padding: 0.85rem var(--pad);
  border-bottom: 1px solid var(--line-soft);
  transition: color 0.2s, background-color 0.2s;
}
.menu a:not(.btn):hover,
.menu a:not(.btn):focus-visible { color: var(--blue-700); background: var(--surface-2, rgba(8, 24, 48, 0.03)); }
.menu a[aria-current="page"] { color: var(--blue-700); font-weight: 550; }
.menu__cta { justify-self: start; margin-inline: var(--pad); }
@media (prefers-reduced-motion: reduce) {
  .menu { transition: none; }
}

/* ── the bar while the menu is open ──────────────────────────────────────
   The drawer is a white sheet at z-index 39 and the bar is fixed above it at
   40, so opening the menu puts a white plane behind a bar that is transparent
   at rest on every page that opens on a dark ground - the homepage over its
   hero, Services and the Knowledge Hub over their mastheads, About, News and
   Contact over theirs. The lockup is assets/logo-for-header.svg, whose wordmark
   is `fill: #fff`, so it went white on white: the bar kept its burger and its
   language chip and lost its name. Reported by the client 2026-09-19.

   The bar takes the same navy it already flips to past the hero, rather than
   the drawer going dark: the drawer's links are set at 2.6rem in --blue-900 on
   white, and inverting a full screen of type is a different design decision
   from giving a 92px bar a ground. This way the open menu reads as the page's
   own bar over a sheet, which is what it is.

   Specificity, because this has to win from the first stylesheet on the page:
   the transparent rules live in css/pages.css and css/services.css, which both
   load after this file, so source order is against us and the selector has to
   carry the win on its own.

   The one to clear is css/pages.css's
   `body:has(main > .pagehead:first-child) .nav:not(.is-dark)`. Count it
   carefully - the argument to :has() contributes a type (`main`) and a
   pseudo-class (`:first-child`) as well as the class, so the rule is (0,4,2),
   not the (0,4,1) it looks like at a glance. This was first written as
   `body:has(.menu.is-open) .nav:not(.is-dark)`, which is exactly (0,4,1), and
   it lost on About, News, Contact and the Arabic page while passing on the
   homepage, Services and Insights - the three templates with no `.pagehead`
   and so no competing rule. Measured on the live site 2026-09-19.

   `[data-menu]` is what buys the extra step, and it is not padding: it is the
   hook every header script already uses to find the drawer
   (`document.querySelector('[data-menu]')`), so the selector says what it
   means - the drawer element, open. That puts this at (0,5,1), which clears
   (0,4,2).

   It still sits under css/pages.css's `...:not(.is-dark).is-scrolled` at
   (0,5,2), and that is correct: once the page has moved, that rule is already
   painting the bar navy.

   `:not(.is-dark)` is the right exclusion as well as a class - past the
   masthead the bar is already navy and can keep its own rule, shadow
   included. */
body:has(.menu[data-menu].is-open) .nav:not(.is-dark) {
  background: var(--blue-900);
  backdrop-filter: none;
  border-bottom-color: rgba(255, 255, 255, 0.12);

  --nav-ink: #ffffff;
  --nav-ink-2: rgba(255, 255, 255, 0.84);
  --nav-ink-3: rgba(255, 255, 255, 0.62);
  --nav-line: rgba(255, 255, 255, 0.26);
  --nav-accent: #ffffff;

  /* The blue lift is drawn for white on white and is a smudge on navy, the
     same reason css/hero-spider.css swaps it on `is-dark`. */
  box-shadow: 0 1px 24px rgba(0, 0, 0, 0.18);
}

/* ─────────────────────────────────────────────────────────────────────────
   THE REQUEST MENU

   "Request an assessment" opens three links to the three registers. The
   wrapper keeps whatever class its call site needs - .nav__cta in the bar,
   .menu__cta in the mobile menu, .svrail__cta on the Insights rail - so the
   button keeps every rule those already carry, including the bar's collapse
   animation, which still matches through `.nav__cta > .btn`.

   The panel is absolutely positioned so it cannot disturb the grid or flex row
   it sits in: .nav__cta is a single-column grid and a second child in the flow
   would open a second row under the bar.
   ───────────────────────────────────────────────────────────────────────── */
.navdrop { position: relative; }

/* Inherits .btn from the call site. Only the chevron is this component's. */
.navdrop__btn { cursor: pointer; }

.navdrop__chev {
  display: inline-block;
  margin-inline-start: 0.4em;
  font-size: 0.85em;
  transition: transform 0.2s var(--ease-out);
}
.navdrop__btn[aria-expanded="true"] .navdrop__chev { transform: rotate(180deg); }

/* inset-inline-start, not left: on the Arabic pages the whole bar mirrors and
   the panel has to hang from the button's own leading edge. */
.navdrop__menu {
  position: absolute;
  z-index: 60;
  inset-block-start: calc(100% + 0.5rem);
  inset-inline-start: 0;
  min-width: max(100%, 15rem);
  display: flex;
  flex-direction: column;
  padding: 0.35rem;
  border: 1px solid var(--line);
  border-radius: var(--radius, 10px);
  background: var(--surface);
  box-shadow: 0 10px 30px -12px rgb(0 0 0 / 0.28), 0 2px 6px -2px rgb(0 0 0 / 0.12);
}

/* The `hidden` attribute needs saying again here, and this is not belt and
   braces. `[hidden] { display: none }` lives in the browser's own stylesheet,
   and the user-agent origin loses to every author rule - so the `display: flex`
   above silently defeated it and the panel rendered open from first paint even
   though the attribute was set correctly. The attribute in the selector puts
   the rule above the class rule it has to beat.

   The lesson generalises: any component that sets `display` on the element it
   also toggles with `hidden` has to restate it. */
.navdrop__menu[hidden] { display: none; }

/* The panel opens over whatever is beneath it, including the dark provider
   plane further down the page, so it carries its own light ground and its own
   ink rather than inheriting the section's. */
.navdrop__item {
  display: block;
  padding: 0.6rem 0.8rem;
  border-radius: calc(var(--radius, 10px) - 4px);
  color: var(--ink);
  font-size: var(--fs-sm, 0.95rem);
  line-height: 1.3;
  text-decoration: none;
  white-space: nowrap;
  transition: background 0.15s var(--ease-out), color 0.15s var(--ease-out);
}
.navdrop__item:hover,
.navdrop__item:focus-visible {
  background: var(--gold-100);
  color: var(--blue-900, var(--ink));
}

/* The menu is taller than the viewport is wide on a phone, so it anchors to
   the trailing edge there and wraps rather than running off the screen. */
@media (max-width: 30rem) {
  .navdrop__menu {
    inset-inline-start: auto;
    inset-inline-end: 0;
    min-width: min(16rem, calc(100vw - 2rem));
  }
  .navdrop__item { white-space: normal; }
}

/* The first few pixels above the burger breakpoint are the tightest bar on the
   site: the lockup, all five links, the language chip, the sign-in chip and a
   primary CTA on one line, with no burger yet to hand anything to.

   Measured on the services page, whose CTA is the longest ("Request an
   assessment"), with the chip forced visible and the webfonts confirmed loaded
   - an earlier pass measured mid-font-swap and every figure it produced was
   wrong by more than the chip is wide. The CTA's right edge sits 3px past the
   viewport at 1081, is back inside it by 1090, and gains room from there on:
   11px clear at 1100, 42px at 1140. So the zone this rule exists for is about
   nine pixels wide, and the chip gives way across it because it is the least
   load-bearing control in the bar. 1100 rather than 1090 as the upper bound,
   for the Arabic labels and for whatever a future CTA is called.

   Worth knowing before widening this: <html> carries overflow-x: clip, so even
   at 1081 the 3px never becomes a scrollbar or a scrollable page. The cost of
   the overflow is a button grazing the edge; the cost of the rule is a missing
   Login chip. That trade is why the band is this narrow.

   :has() so the rule only fires where there is actually a CTA competing for the
   room - the sign-in page's own bar has none, and its chip should stay. */
@media (min-width: 1081px) and (max-width: 1100px) {
  .nav__actions:has(.nav__cta) .nav__login { display: none; }
}

@media (max-width: 1080px) {
  .nav__links { display: none; }
  .nav__actions { margin-inline-start: auto; }
  .nav__burger { display: grid; }
}
@media (max-width: 560px) {
  .brand__text { display: none; }
  .nav .btn--primary { display: none; }
  /* The wrapper too, not just the button inside it. A hidden button left its
     span in the flex row as a zero-width item, and a zero-width item still
     takes a gap - 11px of the narrowest bar on the site, spent on nothing. */
  .nav__cta { display: none; }
  /* Below 560 the bar is the lockup, the language control and the burger, and
     that is already measured to the pixel (see the 375 note in
     hero-spider.css). Sign in moves into the menu, where it is a row like any
     other. */
  .nav__login { display: none; }
}
/* Below 430 the burger's "Menu" word and the language chip no longer both fit
   beside the lockup - 26px over at 360, and the Arabic word is wider still.
   The chip gives way, not the word (client's call, 2026-09-30): the language
   link is already a row in the menu panel as .menu__lang. */
@media (max-width: 430px) {
  .nav__actions > .lang { display: none; }
}

/* ─────────────────────────── hero ─────────────────────────── */
/* Hero colours are tokenised so the band can flip with one attribute:
   `data-theme="dark"` on .hero. The placeholder background art is near-black,
   which forces the dark treatment - drop the attribute and the hero returns to
   the light scheme the rest of the page uses. */
.hero {
  --hero-ink: var(--blue-900);
  --hero-accent: var(--blue-600);
  --hero-body: var(--ink-2);
  --hero-eyebrow: var(--gold-700);
  --hero-meta: var(--ink-3);
  --hero-rule: var(--line);
  /* the flare that travels across the accent word, and the CTA's own glow */
  --hero-flare: var(--blue-500);
  --hero-halo: rgba(45, 103, 137, 0.35);
  --cta-ring: rgba(45, 103, 137, 0.5);
  --cta-halo: rgba(45, 103, 137, 0.3);
  --cta-sheen: rgba(255, 255, 255, 0.55);

  /* How much scroll each act gets. 1 viewport per act reads as roughly a
     full screen of travel to build each element - shorter feels rushed on a
     trackpad, longer and the reader thinks the page has stopped. */
  --hero-acts: 3;

  position: relative;
  /* The track: one viewport to pin plus one per act to scrub through. */
  height: calc(100svh * (var(--hero-acts) + 1));
  outline: none;
  background: var(--surface-2);
}

/* The pinned stage. Everything that was positioned against .hero is positioned
   against this instead, so it keeps the old geometry exactly. */
.hero__viewport {
  position: sticky;
  top: 0;
  height: 100svh;
  min-height: 560px;
  overflow: hidden;
  isolation: isolate;
}

/* Landscape phones are shorter than the 560px floor, which would push the
   headline and body below the fold in a hero the user cannot scroll within.
   Drop the floor and let the hero be exactly the viewport. */
@media (max-height: 620px) {
  .hero__viewport { min-height: 0; }
}

/* A track this tall is only meaningful if there is room to scrub. Below the
   short-viewport breakpoint the acts get less travel so the hero does not
   become a marathon on a landscape phone. */
@media (max-height: 620px) {
  .hero { --hero-acts: 2.2; }
}

.hero[data-theme="dark"] {
  --hero-ink: #ffffff;
  --hero-accent: var(--blue-300);
  --hero-body: rgba(255, 255, 255, 0.76);
  --hero-eyebrow: var(--gold-300);
  --hero-meta: rgba(255, 255, 255, 0.58);
  --hero-rule: rgba(255, 255, 255, 0.24);
  --hero-flare: #ffffff;
  --hero-halo: rgba(154, 197, 211, 0.8);
  --cta-ring: rgba(255, 255, 255, 0.5);
  --cta-halo: rgba(231, 196, 108, 0.5);
  --cta-sheen: rgba(255, 255, 255, 0.85);
  /* matches the art, so there is no pale flash before the textures decode */
  background: #141E26;
}

/* Scrim. The placeholder art is high-contrast and busy, and the headline,
   body copy and progress readout all sit over it. Sits above the canvas
   (both z-index 0, later in DOM order) and below .hero__stage (z-index 1). */
.hero[data-theme="dark"] .hero__viewport::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    linear-gradient(to top,
      rgba(20, 30, 38, 0.88) 0%, rgba(20, 30, 38, 0.5) 26%, rgba(20, 30, 38, 0) 58%),
    linear-gradient(to right,
      rgba(20, 30, 38, 0.7) 0%, rgba(20, 30, 38, 0.12) 48%, rgba(20, 30, 38, 0) 70%);
}

/* Light hero: the generated backdrop already settles toward white at its foot,
   but the Ken Burns push-in drifts that floor around and the washes are lighter
   in some frames than others. The type gets its own ground rather than relying
   on the art - same geometry as the dark scrim, a fraction of the weight. */
.hero:not([data-theme="dark"]) .hero__viewport::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    linear-gradient(to top,
      rgba(255, 255, 255, 0.9) 0%, rgba(255, 255, 255, 0.52) 26%, rgba(255, 255, 255, 0) 58%),
    linear-gradient(to right,
      rgba(255, 255, 255, 0.72) 0%, rgba(255, 255, 255, 0.12) 48%, rgba(255, 255, 255, 0) 70%);
}

/* the network reads as light-on-dark in that mode */
.hero[data-theme="dark"] .net-node { fill: #ffffff; }
.hero[data-theme="dark"] .net-node-core { fill: #141E26; }
.hero[data-theme="dark"] .net-sat { fill: var(--blue-300); }
.hero[data-theme="dark"] .net-spoke { stroke: var(--blue-300); }
.hero[data-theme="dark"] .net-ring { stroke: rgba(255, 255, 255, 0.4); }
.hero[data-theme="dark"] .net-halo { fill: #ffffff; }
.hero:focus-visible { box-shadow: inset 0 0 0 2px var(--blue-600); }

.hero__gl,
.hero__fallback {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  display: block;
}
.hero__fallback { display: none; background-size: cover; background-position: center; }
.hero__fallback.is-active { display: block; transition: opacity 0.7s var(--ease-out); }

.hero__stage {
  position: relative;
  z-index: 1;
  height: 100%;
  /* Same template as .slide. The build is a single instance shared by all three
     acts so it cannot live inside a slide, but it has to land on exactly the
     cell the network planes use - otherwise the mark and the spokes converge on
     two different centers. The slides are absolutely positioned over this, so
     giving the stage a grid does not disturb them. */
  display: grid;
  grid-template-columns: 1fr min(var(--maxw), calc(100% - var(--pad) * 2)) 1fr;
  grid-template-rows: clamp(4.5rem, 11vh, 7rem) 1fr auto auto auto clamp(3.5rem, 10vh, 6.5rem);
  align-items: end;
}

/* ─────────────── scroll-scrubbed hero centerpiece ───────────────
   js/hero-build.js writes --net-back / --net-mid / --net-front / --net-pulse
   on .hero as the reader scrolls act 2. The three network planes already exist
   in the slide markup, so the build reveals those rather than drawing a second
   copy - that keeps the interactive state markers (js/hero-countries.js) as
   the real ones and guarantees the two can never fall out of alignment.

   Defaults are 1, not 0: if the module never runs the network is simply on,
   which is the old appearance. A broken build must not blank the hero. */
.hero {
  --net-back: 1;
  --net-mid: 1;
  --net-front: 1;
  --net-pulse: 1;

  /* the warm core inside the mark's nucleus */
  --hb-glow: var(--gold-300);
}

.hero .layer--visual[data-net="back"]  { opacity: var(--net-back); }
.hero .layer--visual[data-net="mid"]   { opacity: var(--net-mid); }
.hero .layer--visual[data-net="front"] { opacity: var(--net-front); }

/* Reveal is opacity on the outer .layer only. Deliberately not a transform on
   .layer__inner: js/entrance.js owns that element and writes it inline through
   GSAP, so anything set here would be silently overridden. The ownership split
   is the same one documented at the top of entrance.js. */

/* Pulses are expensive and pointless before their spokes exist. */
.hero .net-pulse { opacity: var(--net-pulse); }

/* ── the build mount: same cell and box as the network planes, so the mark
      lands exactly on the center the spokes converge on ── */
.hero__build,
.hero__build > svg {
  grid-area: 2 / 2 / 5 / 3;
  align-self: center;
  justify-self: center;
  width: min(660px, 88vw, 72svh);
  aspect-ratio: 1;
}
.hero__build {
  position: relative;
  z-index: 1;            /* above the network planes, where the sphere was */
  display: grid;
  place-items: center;
  pointer-events: none;
}
.hero__build > svg {
  display: block;
  /* Clipped, unlike the network planes. The nodes travel in from outside the
     hexagon and an unclipped box let them fly across the headline. */
  overflow: hidden;
}

/* ── mark ──
   The gradient stops are properties so one set of SVG defs serves both grounds.
   Each blade is rotated, and a user-space gradient rotates with the element it
   paints, so six blades take six sheen directions from one definition. */
.hero {
  --hb-face-a: var(--blue-500);
  --hb-face-b: var(--blue-800);
  --hb-fold-a: var(--blue-800);
  --hb-fold-b: var(--blue-900);
}
.hero[data-theme="dark"] {
  --hb-face-a: var(--blue-300);
  --hb-face-b: var(--blue-700);
  --hb-fold-a: var(--blue-700);
  --hb-fold-b: var(--blue-900);
}

.hb-blade__face { fill: url(#hb-face); }
.hb-blade__fold { fill: url(#hb-fold); }
.hb-chevron     { fill: var(--gold-500); }
.hb-ball        { fill: var(--gold-500); }
.hb-hex         { fill: none; stroke: var(--gold-500); stroke-linejoin: round; }

.hero[data-theme="dark"] .hb-chevron { fill: var(--gold-300); }
.hero[data-theme="dark"] .hb-ball    { fill: var(--gold-300); }
.hero[data-theme="dark"] .hb-hex     { stroke: var(--gold-300); }

/* ── maturity radar (act 3) ──
   Grid recedes, measured profile is the solid statement, target is the dashed
   aspiration, and the gap between them is the only warm fill in the act - so the
   eye goes to what is missing, which is the point of the slide. */
.hb-ring        { fill: none; stroke: var(--hero-rule); stroke-width: 1; stroke-dasharray: 2 6; opacity: 0.5; }
.hb-ring--last  { stroke-dasharray: none; opacity: 0.8; }
.hb-axis        { stroke: var(--hero-rule); stroke-width: 1; opacity: 0.55; }

.hb-current {
  fill: color-mix(in srgb, var(--hero-accent) 22%, transparent);
  stroke: var(--hero-accent);
  stroke-width: 2;
  stroke-linejoin: round;
}
.hb-target {
  fill: none;
  stroke: var(--gold-300);
  stroke-width: 1.5;
  stroke-dasharray: 6 5;
  stroke-linejoin: round;
}
.hb-gap {
  /* evenodd is what makes one path read as the ring between two profiles */
  fill-rule: evenodd;
  fill: color-mix(in srgb, var(--gold-300) 14%, transparent);
  stroke: none;
}
.hb-point   { fill: var(--gold-300); }
.hb-uplift  { fill: var(--gold-300); }

/* Axis labels. font-size on SVG text resolves in user units, so this is 14 of
   the viewBox's 600 - the labels scale with the graphic instead of drifting off
   their axes when the box is resized. Below 700px the box is small enough that
   this lands under 10px, so they are dropped there and .hero__key-axes names
   the six dimensions in prose instead. */
.hb-label {
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.03em;
  fill: var(--hero-meta);
}

/* The beam. A wide soft body under a bright leading edge: the edge is what the
   eye tracks, the body is what makes it read as a beam rather than a line. */
.hb-sweep__body { fill: var(--hero-accent); opacity: 0.1; }
.hb-sweep__edge { stroke: var(--gold-300); stroke-width: 1.6; opacity: 0.9; }

/* The mark is the hero's centerpiece and the gauge frames it; at the desktop
   ratio both drop to about a third of this on a phone. Give the box more room
   as the box itself shrinks, matching what the sphere used to do. */
@media (max-width: 900px), (max-height: 900px) {
  .hero__build > svg { transform: scale(1.1); }
}
@media (max-width: 620px), (max-height: 620px) {
  .hero__build > svg { transform: scale(1.24); }
}

@media (prefers-reduced-motion: reduce) {
  /* No scrub: the network is simply present and the mark is already built. */
  .hero { --net-back: 1; --net-mid: 1; --net-front: 1; --net-pulse: 1; }
}


.slide {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-columns: 1fr min(var(--maxw), calc(100% - var(--pad) * 2)) 1fr;
  grid-template-rows: clamp(4.5rem, 11vh, 7rem) 1fr auto auto auto clamp(3.5rem, 10vh, 6.5rem);
  align-items: end;
  pointer-events: none;
}
.slide[aria-hidden="true"] { visibility: hidden; }
/* Through a scrubbed crossfade both slides are on screen while only one of them
   is the current one, so visibility and aria-hidden have to be able to
   disagree. js/entrance.js's `createSlideScrub` sets this on any slide that is
   not fully off stage; the triggered path in js/main.js never sets it, so
   index-v1.html is unaffected. Equal specificity, so source order decides. */
.slide[data-live] { visibility: visible; }

.layer { grid-column: 2; will-change: transform; z-index: 2; }

/* the three network layers stack in one cell and share a viewBox */
.layer--visual {
  grid-row: 2 / 5;
  align-self: center;
  justify-self: center;
  display: grid;
  place-items: center;
  z-index: 0;
  width: min(660px, 88vw, 72svh);
  aspect-ratio: 1;
}
.layer--visual,
.layer--visual .layer__inner { grid-area: 2 / 2 / 5 / 3; }
.layer--visual .layer__inner {
  align-self: center;
  justify-self: center;
  width: min(660px, 88vw, 72svh);
  aspect-ratio: 1;
}
.layer--visual svg { width: 100%; height: 100%; overflow: visible; }

/* Floating object - sits at the center of the network and acts as the hub, so
   the spokes radiate from a real object rather than a drawn circle. Shares the
   visual layers' grid cell and box so it lands exactly on center.

   PLACEHOLDER ASSET: assets/hero-object.png is third-party artwork pulled from
   the reference site. Replace before this goes anywhere near the client. */
.layer--float,
.layer--float .layer__inner { grid-area: 2 / 2 / 5 / 3; }
.layer--float {
  align-self: center;
  justify-self: center;
  z-index: 1;
  display: grid;
  place-items: center;
  width: min(660px, 88vw, 72svh);
  aspect-ratio: 1;
}
.layer--float .layer__inner {
  align-self: center;
  justify-self: center;
  width: min(660px, 88vw, 72svh);
  aspect-ratio: 1;
  display: grid;
  place-items: center;
}
.float-obj {
  width: 34%;
  height: auto;
  filter: drop-shadow(0 26px 44px rgba(20, 30, 38, 0.3));
  /* a third nesting level: parallax owns .layer, the entrance owns
     .layer__inner, so the idle float needs its own element to animate */
  animation: hero-float 7.5s ease-in-out infinite;
}
@keyframes hero-float {
  0%, 100% { transform: translate3d(0, -1.6%, 0) scale(1); }
  50%      { transform: translate3d(0, 1.6%, 0) scale(1.015); }
}
/* The object is the hero's centerpiece; at the desktop ratio it drops to
   ~100px on a phone, too small to read as anything. Give it more of the box
   as the box itself gets smaller. */
@media (max-width: 900px), (max-height: 900px) { .float-obj { width: 40%; } }
@media (max-width: 620px), (max-height: 620px) { .float-obj { width: 46%; } }

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

.layer--eyebrow { grid-row: 3; margin-bottom: clamp(0.7rem, 1.8vh, 1.35rem); }
.layer--title   { grid-row: 4; }
.layer--body    { grid-row: 5; margin-top: clamp(0.9rem, 2.2vh, 1.6rem); }

@media (min-width: 900px) {
  .layer--body {
    grid-row: 4;
    margin-top: 0;
    display: flex;
    justify-content: flex-end;
    align-items: flex-end;
  }
  .layer--body .layer__inner { max-width: 34ch; padding-bottom: 0.55rem; }
  .layer--title .layer__inner { max-width: 60%; }
}

.hero .title {
  font-size: clamp(2.4rem, 7.4vw, 6.2rem);
  font-weight: 600;
  line-height: 0.98;
  letter-spacing: -0.038em;
  color: var(--hero-ink);
}
.hero .title em { font-style: normal; color: var(--hero-accent); }

/* js/entrance.js splits the title into these so the line can arrive as a
   cascade. inline-block is what lets each one take a transform; the split
   preserves literal spaces, so the line still breaks and copies normally. */
.hero .title .ch {
  display: inline-block;
  will-change: transform;
}

/* A flare travels across the accent word every few seconds, one character at a
   time - `--n` is the character's index within its line, set during the split.
   Colour and shadow only, so it never fights the entrance's transforms. */
.hero .title em .ch {
  animation: title-flare 5.2s ease-in-out infinite;
  animation-delay: calc(1.9s + var(--n, 0) * 0.05s);
}
@keyframes title-flare {
  0%, 84%, 100% { color: var(--hero-accent); text-shadow: none; }
  90%           { color: var(--hero-flare); text-shadow: 0 0 20px var(--hero-halo); }
}

.hero .body {
  font-size: clamp(0.98rem, 1.2vw, 1.12rem);
  line-height: 1.6;
  color: var(--hero-body);
}

.hero .eyebrow { color: var(--hero-eyebrow); }

/* clip-mask used by the entrance animation.
   `width: max-content` matters: overflow:hidden clips both axes, so a mask
   sized to its parent would shave a word wider than the column. */
.mask {
  display: block;
  overflow: hidden;
  width: max-content;
  max-width: 100%;
  padding-bottom: 0.1em;
  margin-bottom: -0.1em;
}
.mask__inner { display: block; will-change: transform; }

/* hero progress */
.hero__progress {
  position: absolute;
  z-index: 3;
  left: var(--pad);
  bottom: clamp(1.4rem, 4vh, 2.5rem);
  display: flex;
  align-items: center;
  gap: 0.85rem;
  font-size: var(--fs-micro);
  letter-spacing: 0.16em;
  color: var(--hero-meta);
  font-variant-numeric: tabular-nums;
}
.hero__bar { display: block; width: clamp(70px, 12vw, 150px); height: 1.5px; background: var(--hero-rule); }
/* scaleX rather than width: the fill is transitioned on every slide change,
   and animating width would lay out the bar on each of those frames. */
.hero__bar i {
  display: block;
  height: 100%;
  width: 100%;
  background: var(--hero-accent);
  transform: scaleX(0.3333);
  transform-origin: left center;
  transition: transform 0.8s var(--ease-out);
  will-change: transform;
}

/* ── act 3 key ──
   The radar is the only part of the hero that asks the reader to interpret a
   shape rather than read a sentence, so it is the only part that gets a legend.
   Positioned against the viewport rather than placed in the stage grid: the
   band under the header on the right is the one region the hero leaves empty,
   which keeps the key clear of the headline (lower left), the body copy (lower
   right) and the bottom row of controls.

   Opacity comes from --radar-key, written by js/hero-build.js on the radar's
   own fade curve - scrubbed, not transitioned, so it tracks the scroll exactly
   and cannot be left stranded on screen above act 3.

   THE SIDE IS LOGICAL, NOT PHYSICAL. "The band under the header on the right"
   is only the empty region in English. `dir="rtl"` mirrors the header's flex
   row, which sends the wide logo lockup to the physical right and the narrow
   language button to the left - so a key pinned with `right` lands underneath
   the logo on every Arabic page, while the corner it was written for sits
   empty on the other side. inset-inline-end follows the header instead of
   fighting it: the key stays opposite the lockup in both languages, which is
   also the only place it is guaranteed to have room. Nothing is needed in
   css/rtl.css because there is no longer a physical side to flip.

   The top floor clears the header rather than guessing at it. 14vh is the
   intent - a band below the bar, proportional to the window - but the old
   5.5rem floor put the key at 88px on any viewport under about 700px tall,
   which is inside the bar in either language. 7rem is the resting bar plus the
   language button plus a gap. It is a floor and not a fixed value, so on a tall
   window the key still sits where it always did. */
.hero__key {
  position: absolute;
  z-index: 3;
  top: clamp(7rem, 14vh, 8.5rem);
  inset-inline-end: var(--pad);
  width: min(17rem, 42vw);
  opacity: var(--radar-key, 0);
  color: var(--hero-meta);
  font-size: var(--fs-micro);
  line-height: 1.45;
  /* it explains the graphic, it is not a control - never eat a CTA click */
  pointer-events: none;
}
.hero__key-head {
  font-size: var(--fs-micro);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--hero-eyebrow);
  margin-bottom: 0.75rem;
}
.hero__key ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.55rem; }
.hero__key li { display: grid; grid-template-columns: 1.5rem 1fr; align-items: center; }

/* Each swatch is painted from the same token as the mark it stands for, so the
   key cannot drift out of sync with the radar when a colour is retuned. The
   inner element is centered with inset+auto margins, which keeps a 2px rule and
   a 0.45rem dot on the same optical center line. */
.hero__key-mark { position: relative; display: block; width: 0.95rem; height: 0.7rem; }
.hero__key-mark::before { content: ""; position: absolute; inset: 0; margin: auto; }
.hero__key-mark--point::before {
  width: 0.45rem; height: 0.45rem; border-radius: 50%;
  background: var(--gold-300);
}
.hero__key-mark--now::before    { width: 100%; height: 2px; background: var(--hero-accent); }
.hero__key-mark--target::before { width: 100%; height: 0; border-top: 1.5px dashed var(--gold-300); }
.hero__key-mark--gap::before {
  width: 100%; height: 0.62rem;
  background: color-mix(in srgb, var(--gold-300) 26%, transparent);
}

/* Prose fallback for the axis labels, which are dropped on a small box */
.hero__key-axes { display: none; }
@media (max-width: 700px) {
  .hb-label { display: none; }
  .hero__key { width: min(13.5rem, 58vw); font-size: var(--fs-micro); }
  .hero__key-axes { display: block; margin-top: 0.75rem; }
}
/* Same call as .hero__cta below: a short viewport has no room to spare, and
   losing the key costs less than pushing it into the headline. */
@media (max-height: 620px) { .hero__key { display: none; } }

/* ── hero call to action ───────────────────────────────────────────────
   Third element of the hero's bottom row, between the progress readout and the
   scroll hint. It has to sit inside the band the slide grid reserves at the
   foot (the last grid row), or it runs into the headline - which is why this is
   a little lower than the other two rather than sharing their baseline: the
   button is taller, and matching their `bottom` would push its top into the
   title. It arrives after the headline rather than being there from frame one. */
.hero__cta {
  position: absolute;
  z-index: 3;
  left: 50%;
  bottom: clamp(1rem, 3.2vh, 2rem);
  transform: translateX(-50%);
  display: inline-grid;
  place-items: center;
  opacity: 0;
  animation: cta-arrive 0.85s var(--ease-out) 1.45s forwards;
}

/* Two rings emitted from the button's own box, half a cycle apart, so there is
   always one in flight. Scaled less on x than y: a uniform scale on a wide box
   travels much further horizontally and reads as a stretch, not a ripple. */
.hero__cta::before,
.hero__cta::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: var(--corner-btn);
  border: 1px solid var(--cta-ring);
  opacity: 0;
  pointer-events: none;
  animation: cta-emit 3.4s ease-out infinite;
}
.hero__cta::after { animation-delay: 1.7s; }

.hero__cta-btn {
  position: relative;
  isolation: isolate;   /* keeps the sheen above the fill and below the label */
  padding: 0.85rem 1.6rem;
  font-size: 0.95rem;
  animation: cta-breathe 3.4s ease-in-out infinite;
}

/* the sheen: a band of light crossing the button, then a pause before the next */
.hero__cta-btn::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: linear-gradient(105deg,
    transparent 36%, var(--cta-sheen) 50%, transparent 64%);
  background-size: 260% 100%;
  animation: cta-sheen 3.4s ease-in-out infinite;
}

/* solid navy on near-black has almost no contrast - invert it, as the nav does */
.hero[data-theme="dark"] .hero__cta-btn { background: #ffffff; color: var(--blue-900); }
.hero[data-theme="dark"] .hero__cta-btn:hover { background: var(--blue-100); }

/* under the cursor the whole thing runs hot */
.hero__cta:hover::before,
.hero__cta:hover::after,
.hero__cta-btn:hover,
.hero__cta-btn:hover::before { animation-duration: 1.5s; }

@keyframes cta-arrive {
  from { opacity: 0; translate: 0 14px; }
  to   { opacity: 1; translate: 0 0; }
}
@keyframes cta-emit {
  0%   { transform: scale(1); opacity: 0.75; }
  70%  { opacity: 0; }
  100% { transform: scale(1.16, 1.8); opacity: 0; }
}
@keyframes cta-breathe {
  0%, 100% { box-shadow: 0 0 0 0 var(--cta-halo); }
  50%      { box-shadow: 0 0 26px 3px var(--cta-halo); }
}
@keyframes cta-sheen {
  0%, 52%   { background-position: 170% 0; }
  86%, 100% { background-position: -70% 0; }
}

/* The CTA on a phone. At 390px the centred, shrink-wrapped pill's label wraps
   to two lines, and the taller pill then lands in the body copy and on the
   progress readout. To give the button a row of its own at the foot of the
   stage the button has to stop being centred, and it has to stop being
   shrink-wrapped, because a two-line label inside a centred pill is what put
   it in the copy's way to begin with.

   Full width and split - label at the reading edge, arrow at the far one. That
   keeps the label on one line down to 320px, and it puts the whole band the
   thumb reaches for at the bottom of the screen rather than a 200px pill in
   the middle of it.

   The grid rows are restated rather than tweaked because .hero__stage and
   .slide have to stay identical - the note on .hero__stage says why - so the
   pair are set together here as they are there. 9.25rem holds the button
   (~3.1rem), the gap above the readout (1rem) and the readout's own inset.

   Guarded on height as well as width: below 621px tall the rule under this one
   drops the button entirely, and reserving a bottom row for a button that is
   not there would push the headline off a landscape phone. */
@media (max-width: 620px) and (min-height: 621px) {
  .hero__stage,
  .slide {
    grid-template-rows: clamp(4.5rem, 11vh, 7rem) 1fr auto auto auto 9.25rem;
  }

  .hero__cta {
    left: var(--pad);
    right: var(--pad);
    bottom: 4rem;
    transform: none;
    display: block;
  }
  .hero__cta-btn {
    display: flex;
    width: 100%;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.95rem 1.15rem;
  }
}

/* Landscape phones have no room for it between the body copy and the readout,
   and the header keeps its own copy of the same control. */
@media (max-height: 620px) { .hero__cta { display: none; } }

/* The two cues that offer slide 1's build - the press and the scroll. Pinned
   where the scroll cue alone used to be, so the pair reads as one row of small
   print in the corner rather than as a control and a caption. */
.hero__cues {
  position: absolute;
  z-index: 3;
  right: var(--pad);
  bottom: clamp(1.4rem, 4vh, 2.5rem);
  display: flex;
  align-items: center;
  gap: clamp(0.9rem, 2vw, 1.6rem);
}

/* Still pinned on its own, because index-v1.html and index-light.html carry the
   scroll cue without the .hero__cues row around it - only index.php has the
   second cue to sit beside. Inside the row the position is the row's. */
.hero__hint {
  position: absolute;
  z-index: 3;
  right: var(--pad);
  bottom: clamp(1.4rem, 4vh, 2.5rem);
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-size: var(--fs-micro);
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--hero-meta);
}
.hero__cues .hero__hint { position: static; right: auto; bottom: auto; }
.hero__hint i {
  display: block; width: 1px; height: 26px;
  background: linear-gradient(currentColor, transparent);
}

/* Set like the hint rather than like a button, because it sits beside the hint
   and the pair have to read as one row of small print. What marks it as
   pressable is the key cap and the hover, not a filled pill - a second solid
   button in the corner would compete with "Request an assessment" on the other
   side of the same viewport, and that one is the page's actual call to action. */
.hero__start {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: var(--fs-micro);
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--hero-meta);
  cursor: pointer;
  transition: color 0.2s var(--ease-out);
}
.hero__start:hover { color: #fff; }
.hero__start:hover kbd { border-color: var(--gold-300); color: var(--gold-300); }
.hero__start:focus-visible {
  outline: 2px solid var(--gold-300);
  outline-offset: 6px;
  border-radius: 2px;
}
/* Letter-spacing is uppercase small print's, and on a single word inside a box
   it pushes the glyphs off their own centre - so the cap sets its own. */
.hero__start kbd {
  font: inherit;
  letter-spacing: 0.12em;
  padding: 0.34em 0.6em 0.3em;
  border: 1px solid currentColor;
  border-radius: 4px;
  line-height: 1;
  transition: border-color 0.2s var(--ease-out), color 0.2s var(--ease-out);
}

/* Both cues leave as soon as the build is running, whichever switch started it.
   js/main-v2.js sets the class from the build itself rather than from scroll,
   so the press and the scroll dismiss them the same way.

   Faded and lifted rather than display:none, so the press the reader just made
   does not make the thing they pressed vanish in the same frame. It is taken
   out of the tab order and out of the tree on the way, because a control that
   is on screen only as an outgoing animation must not still be reachable. */
.hero__cues > * {
  transition: opacity 0.4s var(--ease-out), transform 0.4s var(--ease-out),
              visibility 0s;
}
.hero.is-building .hero__cues > * {
  opacity: 0;
  transform: translateY(8px);
  pointer-events: none;
  visibility: hidden;
  /* Visibility waits for the fade; opacity and transform do not. Listed in the
     order the shorthand above declares them. */
  transition-delay: 0s, 0s, 0.4s;
}

@media (max-width: 680px) { .hero__cues { display: none; } }
@media (prefers-reduced-motion: reduce) {
  .hero__cues > * { transition: opacity 0.4s linear; }
  .hero.is-building .hero__cues > * { transform: none; }
}

/* ─────────────────────────── network graphic ─────────────────────────── */
.net-node { fill: var(--blue-700); }
.net-node--hub { fill: var(--blue-800); }
.net-sat { fill: var(--blue-300); }
.net-spoke { stroke: var(--blue-500); fill: none; }
.net-ring { stroke: var(--blue-300); fill: none; }
.net-halo { fill: var(--blue-600); }
.net-label { fill: var(--ink-2); font-size: 15px; font-weight: 500; letter-spacing: 0.02em; }
.net-hub-label { fill: #fff; font-size: 13px; font-weight: 600; letter-spacing: 0.06em; }
.net-accent { fill: var(--gold-500); }
.net-node-core { fill: #ffffff; }

/* Node marker sizes, in SVG user units of the 600-unit viewBox.
   The graphic scales with its container, so these grow on small screens to
   keep each state's accent readable - at the desktop radii the accent core
   lands at ~4px on a phone, which reads as no colour at all. */
.net-halo-out { r: 22px; }
.net-halo-in  { r: 13px; }
.net-node     { r: 9.5px; }
.net-core     { r: 4.2px; }

@media (max-width: 900px) {
  .net-halo-out { r: 28px; }
  .net-halo-in  { r: 17px; }
  .net-node     { r: 12.5px; }
  .net-core     { r: 5.6px; }
}
/* Height matters as much as width: the graphic is capped at 72svh, so on a
   landscape phone it is small even though the viewport is wide. Match on
   either axis. */
@media (max-width: 620px), (max-height: 620px) {
  .net-halo-out { r: 36px; }
  .net-halo-in  { r: 22px; }
  .net-node     { r: 16px; }
  .net-core     { r: 7.4px; }
  /* the spokes need matching weight or the markers overpower them */
  .net-pulse { stroke-width: 4; }
}

/* travelling dash - reads as data moving through the hub */
.net-pulse {
  stroke: var(--gold-500);
  stroke-dasharray: 16 172;
  stroke-linecap: round;
  animation: net-flow 4.2s linear infinite;
  animation-delay: calc(var(--i) * 0.7s);
}
@keyframes net-flow {
  from { stroke-dashoffset: 188; }
  to   { stroke-dashoffset: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .net-pulse { animation: none; opacity: 0; }
}

/* ─────────────────────────── KPI band ─────────────────────────── */
.kpi { background: var(--blue-900); color: #fff; }
.kpi .wrap { padding-block: clamp(2.75rem, 6vh, 4.25rem); }
.kpi__head { display: flex; flex-wrap: wrap; gap: 1rem; align-items: baseline; justify-content: space-between; margin-bottom: 2.25rem; }
.kpi__head h2 { font-size: clamp(1.2rem, 2vw, 1.5rem); color: #fff; font-weight: 500; }
.kpi .placeholder-note { color: rgba(255,255,255,0.55); }

.kpi__grid { display: grid; gap: 1px; background: rgba(255,255,255,0.14); grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); }
/* Opaque, and no band art behind it. Tried and rejected 19 Aug: this band is
   ~350px tall and the darkest ground in the page, so art placed here reads as
   nothing at all unless the scrim is lightened far enough to cost the figures
   their contrast. The indicators stay a flat dark band on purpose. */
.kpi__cell { background: var(--blue-900); padding: 1.5rem 1.4rem 1.6rem; }
.kpi__val { font-size: clamp(2.4rem, 4.4vw, 3.4rem); font-weight: 600; letter-spacing: -0.04em; line-height: 1; color: #fff; font-variant-numeric: tabular-nums; }
.kpi__val span { font-size: 0.45em; color: var(--gold-300); margin-inline-start: 0.15em; letter-spacing: 0; }
.kpi__label { margin-top: 0.6rem; font-size: 0.92rem; color: rgba(255,255,255,0.72); line-height: 1.45; }

/* ─────────────────────────── ecosystem ─────────────────────────── */
.eco { background: var(--surface-2); }
.eco__col-label { margin-bottom: 0.9rem; text-align: center; }

/* A hairline-divider card grid, not a stacked list: four items read faster as
   a compact block than as a tall column, and it reuses the .proof__grid
   technique (1px gaps over --line double as the dividers) instead of a new
   pattern. Supersedes the translateX hover from the 2026-08-21 pass - a card
   in a grid has no "row" to slide along, so it takes the gold top-bar reveal
   already established on .proof__item instead. */
.eco__list {
  display: grid;
  gap: 1px;
  background: var(--line);
  grid-template-columns: 1fr;
  margin-top: clamp(2.5rem, 5vh, 3.5rem);
}
@media (min-width: 560px)  { .eco__list { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1040px) { .eco__list { grid-template-columns: repeat(4, 1fr); } }
.eco__item {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  padding: 1.5rem 1.3rem 1.6rem;
  background: var(--surface);
  transition: background 0.22s var(--ease-out);
}
.eco__item::after {
  content: '';
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  height: 2px;
  background: var(--gold-500);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.28s var(--ease-out);
}
.eco__item:hover { background: var(--surface-2); }
.eco__item:hover::after { transform: scaleX(1); }
.eco__item b { font-weight: 600; }
.eco__item-head { display: flex; align-items: center; justify-content: space-between; }
.eco__icon {
  display: grid;
  place-items: center;
  width: 2.3rem;
  height: 2.3rem;
  color: var(--blue-600);
  transition: color 0.3s var(--ease-out);
}
.eco__icon svg { width: 100%; height: 100%; }
.eco__item:hover .eco__icon { color: var(--blue-700); }
.eco__num {
  font-size: var(--fs-micro);
  font-weight: 600;
  color: var(--blue-500);
  font-variant-numeric: tabular-nums;
  transition: color 0.3s var(--ease-out);
}
.eco__item:hover .eco__num { color: var(--blue-700); }
.eco__item p { color: var(--ink-2); font-size: 1rem; margin-top: 0; }

/* the four connect-cards drift at their own rate, same continuous-parallax
   primitive as #about's KPI cells and #mandate's actor row */
.eco__item[data-parallax] {
  translate: 0 calc((0.5 - var(--pv, 0.5)) * var(--sp, 1) * 26px);
}
.route__states[data-parallax],
.route__hub[data-parallax],
.route__actors[data-parallax] {
  translate: 0 calc((0.5 - var(--pv, 0.5)) * var(--sp, 1) * 26px);
}

/* ─────────────────────────── services ─────────────────────────── */
.svc__grid { display: grid; gap: 1.5rem; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
.card {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: 1.75rem 1.6rem 1.6rem;
  border: 1px solid var(--line);
  border-radius: var(--corner-box);
  background: var(--surface);
  text-decoration: none;
  overflow: hidden;
  transition: border-color 0.35s var(--ease-out), transform 0.35s var(--ease-out),
              box-shadow 0.35s var(--ease-out);
}
.card::before {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 2px;
  background: var(--blue-600);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.45s var(--ease-out);
}
.card:hover {
  border-color: var(--blue-300);
  transform: translateY(-3px);
  box-shadow: 0 18px 40px -24px rgba(20, 30, 38, 0.35);
}
.card:hover::before { transform: scaleX(1); }
.card__icon { width: 44px; height: 44px; color: var(--blue-600); }
.card h3 { font-size: 1.3rem; margin-bottom: 0.6rem; }
.card p { color: var(--ink-2); font-size: 1rem; }
.card__meta {
  margin-top: 1.35rem;
  padding-top: 1rem;
  border-top: 1px solid var(--line-soft);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  font-size: 0.88rem;
  color: var(--ink-3);
}
.card__meta b { color: var(--blue-700); font-weight: 600; }

/* ─────────────────────────── frameworks ─────────────────────────── */
.fw { background: var(--surface-2); }
.fw__grid { display: grid; gap: 1.5rem; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
.fw__item {
  padding: 1.9rem 1.6rem 2rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 3px;
  overflow: hidden;
  transition: background 0.3s var(--ease-out);
}
.fw__item--siri::before { background: var(--blue-600); }
.fw__item--itti::before { background: var(--gold-600); }
.fw__item:hover { background: var(--surface-2); }
.fw__item h3 { font-size: 1.2rem; margin-block: 0.85rem 0.5rem; }
.fw__item p { color: var(--ink-2); font-size: 1rem; }
.fw__item a { color: var(--blue-700); font-size: 0.92rem; font-weight: 500; text-decoration: none; display: inline-flex; gap: 0.4rem; margin-top: 1rem; }
.fw__item a:hover { text-decoration: underline; }

/* ─────────────────────────── knowledge hub ─────────────────────────── */
.kh__split { display: grid; gap: clamp(2rem, 4vw, 3.5rem); grid-template-columns: 1fr; }
@media (min-width: 1000px) { .kh__split { grid-template-columns: 1.25fr 1fr; } }

.usecase {
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  gap: 1.15rem;
  align-items: center;
  padding: 1.15rem 0;
  border-bottom: 1px solid var(--line);
  cursor: pointer;
  /* transform, not padding: six of these animate on hover and padding would
     lay the row out on every frame */
  transition: transform 0.35s var(--ease-out);
}
.usecase:first-child { border-top: 1px solid var(--line); }
.usecase:hover { transform: translateX(0.6rem); }
/* transforms have no logical equivalent, so the Arabic build needs the mirror */
[dir="rtl"] .usecase:hover { transform: translateX(-0.6rem); }
.usecase__flag {
  width: 42px; height: 42px; flex: none;
  border-radius: 50%;
  background: var(--blue-50);
  color: var(--blue-700);
  display: grid; place-items: center;
  font-size: var(--fs-micro); font-weight: 700; letter-spacing: 0.04em;
}
.usecase h4 { font-size: 1.05rem; font-weight: 550; }
.usecase p { font-size: 0.92rem; color: var(--ink-3); margin-top: 0.15rem; }
/* The gain is set in gold, not green. Green was the one colour on the page that
   is not in the palette, and a maturity gain is exactly what gold is for here -
   the same accent the section rule above the rows is drawn in. */
.usecase__delta { font-size: 0.92rem; font-weight: 600; color: var(--gold-700); white-space: nowrap; font-variant-numeric: tabular-nums; }
/* the disclosure affordance: a chevron that rotates open, same device the
   framework toggle uses, so the two "there is more here" cues read the same */
.usecase__chev {
  grid-column: 4;
  color: var(--ink-3);
  font-size: 1.1rem;
  transition: transform 0.35s var(--ease-out), color 0.35s var(--ease-out);
}
.usecase[aria-expanded="true"] .usecase__chev { transform: rotate(90deg); color: var(--blue-700); }
[dir="rtl"] .usecase[aria-expanded="true"] .usecase__chev { transform: rotate(-90deg); }

/* Narrow screens: drop the delta under the title rather than holding a third
   column - at 375px it squeezed titles into ~187px and wrapped them to three
   lines. */
@media (max-width: 560px) {
  .usecase { grid-template-columns: auto 1fr auto; align-items: start; }
  .usecase__flag { margin-top: 0.15rem; }
  .usecase__delta { grid-column: 2; grid-row: 2; justify-self: start; margin-top: 0.4rem; }
  .usecase__chev { grid-column: 3; margin-top: 0.15rem; }
}

.report {
  display: grid;
  grid-template-columns: 74px 1fr;
  gap: 1.1rem;
  padding: 1.05rem 0;
  border-bottom: 1px solid var(--line);
  text-decoration: none;
}
.report:first-of-type { border-top: 1px solid var(--line); }
.report__cover {
  aspect-ratio: 3 / 4;
  border-radius: var(--corner-box);
  background: linear-gradient(150deg, var(--blue-700), var(--blue-900) 70%);
  position: relative;
  overflow: hidden;
}
/* Placeholder cover furniture: a gold mark echoing the monogram's hub, plus
   two faint rules standing in for a title block. Reads as a publication cover
   rather than an accent stripe on a card. */
.report__cover::after {
  content: '';
  position: absolute;
  top: 9px;
  inset-inline-start: 9px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--gold-500);
}
.report__cover::before {
  content: '';
  position: absolute;
  inset-inline: 9px;
  bottom: 13px;
  height: 1px;
  background: rgba(255, 255, 255, 0.26);
  box-shadow: 0 -7px 0 rgba(255, 255, 255, 0.16);
}
.report h4 { font-size: 1.02rem; font-weight: 550; line-height: 1.3; }
.report p { font-size: 0.9rem; color: var(--ink-3); margin-top: 0.3rem; }
.report__foot { margin-top: 0.55rem; display: flex; gap: 0.8rem; font-size: 0.8rem; color: var(--blue-700); font-weight: 500; }

/* ─────────────────────────── news ─────────────────────────── */
.news { background: var(--blue-900); color: #fff; }
.news .section__head h2 { color: #fff; }
.news .section__head p:not(.eyebrow) { color: rgba(255,255,255,0.7); }
.news .eyebrow { color: var(--gold-300); }
.news__grid { display: grid; gap: 1.5rem; grid-template-columns: repeat(auto-fit, minmax(270px, 1fr)); }
.news__item {
  display: flex;
  flex-direction: column;
  padding-top: 1.15rem;
  border-top: 1px solid rgba(255,255,255,0.22);
  text-decoration: none;
  transition: border-color 0.35s var(--ease-out), transform 0.35s var(--ease-out);
}
.news__item:hover { border-color: var(--gold-300); transform: translateY(-4px); }
.news__date { font-size: 0.75rem; color: rgba(255,255,255,0.55); letter-spacing: 0.06em; }
.news__item h3 { font-size: 1.1rem; font-weight: 500; margin-block: 0.7rem 0.5rem; color: #fff; }
.news__item p { font-size: 0.95rem; color: rgba(255,255,255,0.68); }
.news__item .tag { margin-top: 1rem; align-self: flex-start; background: rgba(255,255,255,0.1); color: var(--gold-300); }

/* A picture above the date. 3:2 rather than the letterbox this started as: the
   files are 4:3 photographs of people, and a 16:7 crop kept the middle 58% of
   the frame - it cut the presenter's head off in item 3 and sliced the hands in
   item 1. 3:2 discards a quarter of the height, and `object-position` spends
   that quarter at the bottom, where these three photographs carry least.

   Dimmed a little so a bright full-colour photograph does not out-shout the
   headline next to it, and lifted to full on hover. */
.news__photo {
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  object-position: center 32%;
  margin-bottom: 1.1rem;
  opacity: 0.9;
  transition: opacity 0.35s var(--ease-out);
}
.news__item:hover .news__photo { opacity: 1; }

/* ─────────────────────────── footer ─────────────────────────── */
.footer { background: var(--surface-2); border-top: 1px solid var(--line); }
.footer .wrap { padding-block: clamp(3rem, 7vh, 5rem) 2rem; }
.footer__top { display: grid; gap: 2.5rem; grid-template-columns: 1fr; }
@media (min-width: 860px) { .footer__top { grid-template-columns: 1.4fr 1fr 1fr; } }
.footer h4 { font-size: 0.75rem; letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-3); margin-bottom: 1rem; font-weight: 600; }
.footer ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.6rem; }
.footer a { color: var(--ink-2); text-decoration: none; font-size: 0.95rem; }
.footer a:hover { color: var(--blue-700); }
/* The lockup row, on its own full-width line above the columns and separated by
   the same hairline the bottom bar uses, so the three marks read as one
   endorsement row rather than as decoration on the intro paragraph. */
.footer__lockups {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  /* Split rather than centred: the Center's mark holds the left edge with the
     columns below it, and the Council's group closes the far right. Centring the
     pair was right while they were two bare marks - once the right-hand one
     carries three lines of label, a centred cluster reads as one wide lockup
     instead of as two parties at either end of a mandate. */
  justify-content: space-between;
  gap: clamp(1.4rem, 3vw, 2.75rem);
  margin-bottom: clamp(2rem, 4vh, 2.75rem);
  padding-bottom: clamp(1.75rem, 3.5vh, 2.5rem);
  border-bottom: 1px solid var(--line);
}

/* Every mark sized by height, never by width: the ratios differ, they are not
   ours to assume, and matching heights is what makes unrelated lockups read as a
   set. Tall enough that the wordmarks are legible rather than decorative - a
   40px-tall lockup puts the Center's name at about 7px, which is a watermark. */
.footer__logo { display: block; width: auto; height: clamp(70px, 7.6vw, 96px); }

/* A round or square mark at the same height as a 3:1 lockup reads far heavier -
   it fills its whole box where a lockup is mostly air, so matched heights make
   the seal look like the biggest thing in the row. Its own clamp rather than a
   fraction of the one above: the Center's lockup is the mark this site is for and
   is set larger than an optical match would give, so the seal holds the height it
   was already right at instead of following it up. Same caveat as above: the
   final number is optical and wants checking against the real emblem. */
.footer__logo--round { height: clamp(51px, 5.4vw, 66px); }

/* Same caveat as the seal above, for the same reason. GOIC's mark is roughly
   2:1 where the Center's lockup is 3:1, and it is the denser of the two - solid
   navy capitals, a filled globe, and two lines of type under them, against a
   lockup that is mostly air. Matched to the Center's height it would be the
   loudest thing in the row, which inverts what the row is for. Set a step down
   so the two read as equals; the number is optical and wants checking against
   the real artwork on a real screen. */
.footer__logo--goic { height: clamp(58px, 6.2vw, 78px); }

/* The Council's crest and GOIC's wordmark, held together as one party. Tighter
   than the row's own gap on purpose: at the row gap the two would read as two
   marks that happen to be adjacent, which is the reading space-between already
   gives the Center at the other end. What makes a lockup is that the pair is
   closer to each other than either is to anything else. */
.footer__parent {
  display: flex;
  align-items: center;
  gap: clamp(0.65rem, 1.3vw, 1rem);
}

/* Label then mark, reading order and visual order agreeing. Right-aligned so
   the three ragged lines close flush against the emblem rather than leaving a
   gap between the label and the thing it labels. */
.footer__mandate {
  display: flex;
  align-items: center;
  gap: clamp(0.8rem, 1.6vw, 1.15rem);
}

.footer__mandate-text {
  display: grid;
  gap: 0.1rem;
  margin: 0;
  text-align: right;
  font-size: 0.8rem;
  line-height: 1.35;
  color: var(--gold-700);
}

/* The Council's name is the loudest of the three lines because it is the one
   that matters; the qualifier above it is small and gold like an eyebrow, and
   the organ below it sits in body ink so the two navy lines do not compete. */
.footer__mandate-text b {
  font-size: 0.9rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink);
}

.footer__mandate-text .footer__mandate-body { color: var(--ink-2); }

[dir="rtl"] .footer__mandate-text { text-align: left; }

/* Under about 620px the row has wrapped to one mark per line, and a group that
   is alone on its line has nothing to be flush against. */
@media (max-width: 620px) {
  .footer__lockups { justify-content: center; }
  .footer__mandate-text { text-align: center; }
}

.footer__intro p { color: var(--ink-2); font-size: 1rem; max-width: 42ch; margin-top: 1rem; }

/* Hours belong with the intro, not in the contact column: they say when the
   organisation answers rather than how to reach it, and that column is already
   carrying an address, two numbers and an inbox. Set a step down from the
   paragraph above so it reads as a note on it. */
.footer__intro .footer__hours { font-size: 0.9rem; color: var(--ink-3); }

/* Most of the contact column is not a link - an address and a fax number have
   nothing to click - so the plain lines have to state the size and colour that
   .footer a states for the rest, or the column reads as two kinds of text. */
.footer__contact li { color: var(--ink-2); font-size: 0.95rem; line-height: 1.5; }

/* Hairlines between the three columns, drawn on the two that have something to
   their left. Logical border so RTL flips it without a second rule, and scoped
   to the three-column breakpoint - stacked, a left border is a stripe down the
   side of the footer rather than a divider between anything. */
@media (min-width: 860px) {
  .footer__top { gap: clamp(2rem, 4vw, 3.5rem); }
  .footer__top > * + * {
    border-inline-start: 1px solid var(--line);
    padding-inline-start: clamp(2rem, 4vw, 3.5rem);
  }
}

/* A chevron per link, in gold at the weight of a hint rather than a control.
   The nudge on :hover further down moves the whole inline-block, chevron
   included, so the mark travels with the label instead of the label sliding out
   from under it. */
.footer__nav a::after {
  content: "";
  display: inline-block;
  width: 0.34em;
  height: 0.34em;
  margin-inline-start: 0.55em;
  border-top: 1.5px solid currentColor;
  border-inline-end: 1.5px solid currentColor;
  rotate: 45deg;
  translate: 0 -0.1em;
  color: var(--gold-600);
}

[dir="rtl"] .footer__nav a::after { rotate: -135deg; }

/* Icon in its own column so a line that wraps stays aligned to the text rather
   than tucking back under the glyph. Baseline-ish alignment by way of a fixed
   line-height on the row and a matched icon box - `align-items: baseline` would
   hang the icon off the first line's baseline and sit it too low. */
.footer__contact li {
  display: grid;
  grid-template-columns: 1.15rem 1fr;
  gap: 0.7rem;
  align-items: start;
}

.footer__icon {
  width: 1.15rem;
  height: 1.15rem;
  margin-top: 0.05em;
  fill: none;
  stroke: var(--gold-600);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Icons in circles at the size of a comfortable tap target. The row is a list
   because it is one, which puts these anchors in reach of the .footer li a
   rules further down - so the ring cancels both the inline-block and the hover
   nudge, either of which would knock a centred glyph off its circle. */
.footer .footer__social { display: flex; flex-wrap: wrap; gap: 0.7rem; margin-top: 1.6rem; }
.footer .footer__social a {
  display: grid;
  place-items: center;
  width: 2.4rem;
  height: 2.4rem;
  border: 1px solid var(--line);
  border-radius: 50%;
  color: var(--ink-2);
}
.footer .footer__social svg { width: 1.05rem; height: 1.05rem; fill: currentColor; }
.footer .footer__social a:is(:hover, :focus-visible) {
  color: #fff;
  background: var(--blue-700);
  border-color: var(--blue-700);
  transform: none;
}
.footer__bottom {
  margin-top: 3rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--line);
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  justify-content: space-between;
  font-size: 0.8rem;
  color: var(--ink-3);
}
.footer .footer__legal {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem 1.25rem;
  list-style: none;
  margin: 0;
  padding: 0;
}
.footer__legal a { color: var(--ink-3); }
.footer__legal a:hover { color: var(--ink); }
.footer__cookie-settings {
  font: inherit;
  color: var(--ink-3);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.footer__cookie-settings:hover { color: var(--ink); }


/* ═══════════════════════════════════════════════════════════════════════
   COOKIE CONSENT
   The banner and its preferences panel, from js/cookie-consent.js. Both sit
   above the nav bar (z-index 40) and the mobile menu (39) - a consent prompt
   that a reopened mobile menu could cover would be a banner the visitor
   cannot act on.
   ═══════════════════════════════════════════════════════════════════════ */
.cookie-banner {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 50;
  background: var(--c-navy);
  color: #fff;
  padding: 1.25rem max(1.25rem, calc((100% - var(--maxw)) / 2 + 1.25rem));
  box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.18);
}
.cookie-banner[hidden] { display: none; }
.cookie-banner__inner {
  max-width: var(--maxw);
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem 2rem;
}
.cookie-banner__text { flex: 1 1 320px; }
.cookie-banner__heading {
  margin: 0 0 0.35rem;
  font-size: var(--fs-base);
  font-weight: 600;
}
.cookie-banner__body {
  margin: 0;
  font-size: var(--fs-micro);
  color: rgba(255, 255, 255, 0.78);
  line-height: 1.6;
}
.cookie-banner__body a {
  color: #fff;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.cookie-banner__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  flex: 0 0 auto;
}
.cookie-banner .btn--ghost {
  border-color: rgba(255, 255, 255, 0.4);
  color: #fff;
}
.cookie-banner .btn--ghost:hover { border-color: #fff; }
/* Phones. The three buttons come to 457px of intrinsic width, and the row is
   flex: 0 0 auto holding three more flex: 0 0 auto children, so nothing
   shrinks and nothing wraps - below 768px the row runs past the right edge,
   and at 414px and under "Accept All" sits entirely off-screen. Give the row
   its own line and lay it out as a grid, so the columns answer to the space
   available rather than to the label widths. The primary action spans both
   columns: it is the most-used button, and it keeps the two-up row to the
   pair of similar-weight choices. The height cap is a floor-of-the-screen
   guard - at 320px the banner is already 37% of a short viewport. */
@media (max-width: 640px) {
  .cookie-banner {
    padding: 1rem;
    max-height: 70vh;
    overflow-y: auto;
  }
  .cookie-banner__inner { gap: 0.9rem; }
  .cookie-banner__text { flex: 1 1 100%; }
  .cookie-banner__actions {
    flex: 1 1 100%;
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    /* Without a cap the columns take the whole line, and at 640px that is a
       608px-wide "Accept All". Stop growing once the buttons are comfortable. */
    max-width: 26rem;
  }
  .cookie-banner__actions .btn {
    justify-content: center;
    text-align: center;
    padding-inline: 0.6rem;
  }
  .cookie-banner__actions .btn--primary { grid-column: 1 / -1; }
}

.cookie-panel {
  position: fixed;
  inset: 0;
  z-index: 51;
  display: grid;
  place-items: center;
  padding: 1.5rem;
  background: rgba(11, 20, 38, 0.55);
}
.cookie-panel[hidden] { display: none; }
.cookie-panel__dialog {
  width: min(480px, 100%);
  max-height: min(560px, 90vh);
  overflow-y: auto;
  background: var(--surface);
  border-radius: var(--r-box);
  padding: 1.75rem;
}
.cookie-panel__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
}
.cookie-panel__head h2 {
  margin: 0;
  font-size: var(--fs-md);
}
.cookie-panel__close {
  background: none;
  border: 0;
  font-size: 1.5rem;
  line-height: 1;
  color: var(--ink-3);
  cursor: pointer;
  padding: 0;
}
.cookie-panel__close:hover { color: var(--ink); }
.cookie-panel__intro {
  color: var(--ink-2);
  font-size: var(--fs-micro);
  line-height: 1.6;
  margin: 0.75rem 0 1.5rem;
}
.cookie-panel__row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  padding: 1rem 0;
  border-top: 1px solid var(--line-soft);
}
.cookie-panel__label { display: flex; flex-direction: column; gap: 0.25rem; }
.cookie-panel__toggle-name { font-weight: 600; color: var(--ink); }
.cookie-panel__toggle-desc { font-size: var(--fs-micro); color: var(--ink-3); }
.cookie-panel__row input[type="checkbox"] {
  flex: 0 0 auto;
  width: 1.15rem;
  height: 1.15rem;
  margin-top: 0.2rem;
  accent-color: var(--c-blue);
}
.cookie-panel__actions {
  margin-top: 1.5rem;
  display: flex;
  justify-content: flex-end;
}


/* ═══════════════════════════════════════════════════════════════════════
   MOTION SYSTEM
   One easing, one trigger, several variants. Reveals animate the CSS
   `translate` property while hovers use `transform`, so an element can be
   mid-reveal and hovered without the two overwriting each other.
   `--d` carries a per-item stagger delay, assigned in js/sections.js.
   ═══════════════════════════════════════════════════════════════════ */
.reveal {
  opacity: 0;
  translate: 0 22px;
  transition: opacity 0.8s var(--ease-out) var(--d, 0s),
              translate 0.8s var(--ease-out) var(--d, 0s);
}
.reveal.is-in { opacity: 1; translate: 0 0; }

.reveal--left  { translate: -30px 0; }
.reveal--right { translate: 30px 0; }
.reveal--far   { translate: 0 48px; }

/* clip wipe: the element is opaque throughout, revealed by an expanding box */
.reveal--wipe {
  opacity: 1;
  translate: 0 0;
  clip-path: inset(0 100% 0 0);
  transition: clip-path 1s var(--ease-out) var(--d, 0s);
}
.reveal--wipe.is-in { clip-path: inset(0 0 0 0); }

/* a rule that draws itself across */
.rule {
  height: 1px;
  background: currentColor;
  opacity: 0.35;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 1.1s var(--ease-out) var(--d, 0s);
}
.rule.is-in { transform: scaleX(1); }

/* ── KPI band: counters and a hover that surfaces the note ─────────────── */
.kpi__cell {
  position: relative;
  transition: background 0.4s var(--ease-out);
}
/* continuous scroll parallax - js/parallax-scroll.js writes --pv for as long
   as the cell is on screen, unlike .scrub's --p which settles once revealed.
   translate (not transform) so it never collides with the hover lift above. */
.kpi__cell[data-parallax] {
  translate: 0 calc((0.5 - var(--pv, 0.5)) * var(--sp, 1) * 32px);
}
.kpi__cell::after {
  content: '';
  position: absolute;
  inset: auto 0 0 0;
  height: 2px;
  background: var(--gold-500);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.5s var(--ease-out);
}
.kpi__cell:hover { background: #094A75; }
.kpi__cell:hover::after { transform: scaleX(1); }
.kpi__val { display: block; transition: transform 0.4s var(--ease-out); }
.kpi__cell:hover .kpi__val { transform: translateY(-2px); }
.kpi__note {
  margin-top: 0.5rem;
  font-size: var(--fs-micro);
  color: rgba(255, 255, 255, 0.5);
  opacity: 0;
  translate: 0 -4px;
  transition: opacity 0.4s var(--ease-out), translate 0.4s var(--ease-out);
}
.kpi__cell:hover .kpi__note { opacity: 1; translate: 0 0; }

/* structure.html still carries the old .kpi__grid markup above, so that CSS
   stays in place. #about itself now reads as one flowing scoreboard rather
   than three equal boxes: the gauge stays the anchor because it is the one
   proportion among counts (see the ring note below), just bigger. Narrow
   screens stack, and from 641px the row lays out horizontally. No rules
   between the stats: the numbers are far enough apart to read as three
   figures on their own, and the band is quieter without them. */
.kpi__board {
  display: flex;
  flex-direction: column;
  margin-top: clamp(2.25rem, 5vh, 3.25rem);
}
.kpi__stat {
  position: relative;
  display: flex;
  align-items: center;
  gap: 1.15rem;
  padding-block: 1.5rem;
}
.kpi__stat:first-child { padding-top: 0; }
.kpi__stat:last-child { padding-bottom: 0; }

.kpi__board .kpi__val { font-size: clamp(3rem, 8vw, 4.5rem); }
.kpi__board .kpi__label { margin-top: 0; max-width: 26ch; }
.kpi__stat--lead .gauge { width: clamp(6.5rem, 12vw, 8rem); }
.kpi__stat--lead .gauge__val { font-size: clamp(1.4rem, 2.2vw, 1.9rem); }

.kpi__stat[data-parallax] {
  translate: 0 calc((0.5 - var(--pv, 0.5)) * var(--sp, 1) * 32px);
}

@media (min-width: 641px) {
  .kpi__board { flex-direction: row; align-items: center; }
  .kpi__stat {
    flex: 1 1 260px;
    padding-block: 0;
    padding-inline: 2.25rem;
  }
  .kpi__stat:first-child { padding-inline-start: 0; }
  .kpi__board .kpi__val { font-size: clamp(3.2rem, 5vw, 4.75rem); }
}

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

/* ── ecosystem: the states legend, the route to GOIC ──────────────────── */
.eco__legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 1.5rem;
}
/* Idle, the six chips are quiet institutional pills and read as equals (§6.3).
   Hovered, one powers up in its own state colour: a scan wipes across the pill,
   a conic sweep traces the border, and the colour dot pings like a beacon.
   The angle is registered so the conic gradient can actually be animated -
   without @property the `from` angle is not an interpolable value. */
@property --chip-a {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

.eco__chip {
  position: relative;
  isolation: isolate;   /* holds the effect layers behind the label */
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.35rem 0.85rem 0.35rem 0.35rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: transparent;
  font: inherit;
  font-size: 0.78rem;
  color: var(--ink-2);
  cursor: pointer;
  transition: border-color 0.3s var(--ease-out), color 0.3s var(--ease-out),
              background 0.3s var(--ease-out), transform 0.4s var(--ease-out),
              box-shadow 0.4s var(--ease-out);
}

/* the scan pass and a faint scanline grid. Both are this element's own
   background, so its border-radius clips them and the chip needs no
   overflow container - which would also have clipped the dot's pings. */
.eco__chip::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background:
    linear-gradient(115deg, transparent 34%,
      color-mix(in srgb, var(--c) 36%, transparent) 50%, transparent 66%),
    repeating-linear-gradient(90deg,
      color-mix(in srgb, var(--c) 8%, transparent) 0 1px, transparent 1px 5px);
  background-size: 260% 100%, 100% 100%;
  background-position: 170% 0, 0 0;
  opacity: 0;
  transition: opacity 0.3s var(--ease-out),
              background-position 0.8s var(--ease-out);
}

/* the traced ring: a conic sweep masked down to a 1px band around the pill */
.eco__chip::after {
  content: '';
  position: absolute;
  inset: -1px;
  z-index: -1;
  border-radius: inherit;
  padding: 1px;
  /* the fallback angle keeps a static ring where @property is unsupported,
     rather than dropping the whole gradient as an invalid value */
  background: conic-gradient(from var(--chip-a, 0deg),
    transparent 0deg, var(--c) 58deg, transparent 132deg, transparent 360deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  opacity: 0;
  transition: opacity 0.35s var(--ease-out);
}

/* the flag itself carries the state's identity now, so the swatch is the
   chip's one focal point: a crisp rounded-rect frame with its own lift,
   glow and shine, layered on top of the pill's own hover state below. */
.eco__chip-flag {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  width: 26px; height: 15.6px;
  flex: none;
  border-radius: 3px;
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--ink) 12%, transparent);
  transition: transform 0.4s var(--ease-out), box-shadow 0.4s var(--ease-out);
}
.eco__chip-flag .flag {
  display: block;
  width: 100%; height: 100%;
  /* Five of the six are drawn 5:3 and fill the tile exactly; Qatar is drawn
     3:2, so `cover` takes about 5% off the top and bottom of a plain maroon
     field rather than letterboxing one tile in a row that must stay uniform.
     See the note in js/flags.js. */
  object-fit: cover;
}
/* a diagonal shine sweeps across the flag on hover, parked off to one side
   at rest so it has somewhere to travel from */
.eco__chip-flag::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(115deg, transparent 40%,
    color-mix(in srgb, #fff 65%, transparent) 50%, transparent 60%);
  background-size: 260% 100%;
  background-position: 170% 0;
  transition: background-position 0.7s var(--ease-out);
}

.eco__chip:hover,
.eco__chip:focus-visible {
  border-color: color-mix(in srgb, var(--c) 48%, transparent);
  color: var(--ink);
  background: #fff;
  transform: translateY(-2px);
  box-shadow: 0 12px 26px -14px color-mix(in srgb, var(--c) 85%, transparent),
              0 0 0 4px color-mix(in srgb, var(--c) 10%, transparent);
}
.eco__chip:hover::before,
.eco__chip:focus-visible::before { opacity: 1; background-position: -100% 0, 0 0; }
.eco__chip:hover::after,
.eco__chip:focus-visible::after { opacity: 1; animation: chip-trace 2.4s linear infinite; }
.eco__chip:hover .eco__chip-flag,
.eco__chip:focus-visible .eco__chip-flag {
  transform: scale(1.14);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--c) 55%, transparent),
              0 6px 14px -4px color-mix(in srgb, var(--c) 80%, transparent);
}
.eco__chip:hover .eco__chip-flag::after,
.eco__chip:focus-visible .eco__chip-flag::after { background-position: -90% 0; }

@keyframes chip-trace { to { --chip-a: 360deg; } }

/* ── services: ghost numerals, icons that draw ───────────────────────────
   These cards used to be offset into a staircase (2.75rem / 5.5rem at >=900px).
   That was a good touch while they were plain - three identical boxes needed the
   asymmetry. Each card now opens with an artwork band, and three bands at three
   different heights read as broken alignment rather than as a cascade, so the
   staircase came out and the asymmetry moved up to the section head
   (.section__head--wide). */
/* Equal columns, not shrink-to-fit. At `start` each card was only as tall as
   its own paragraph, so three cards of the same kind ended at three different
   heights and their meta rows - the count and the tag, the one row the eye
   compares across the three - sat at three different levels. Stretched, and with
   the meta row pushed to the bottom of whatever height the row settles on, the
   three cards read as one set. */
.svc__grid { align-items: stretch; }
/* The auto margin replaces the meta row's own top margin, so the paragraph
   carries that gap instead - the spacing above the rule is then a floor the auto
   margin grows from, rather than something the tallest card loses. */
.svc__grid .card__meta { margin-top: auto; }
.svc__grid .card > p { margin-bottom: 1.35rem; }
/* The numeral sits inside the card, not half out of the top of it. It used to
   be at -0.5rem, and with .card's `overflow: hidden` that sliced 7px off the
   top of every 01/02/03 - a cut-off shape in the corner of three otherwise
   square cards, which is what the client was seeing. */
.card__ghost {
  position: absolute;
  top: 0.25rem;
  inset-inline-end: 0.4rem;
  font-size: 5.5rem;
  font-weight: 600;
  line-height: 1;
  color: var(--blue-900);
  opacity: 0.05;
  pointer-events: none;
  transition: opacity 0.45s var(--ease-out), transform 0.45s var(--ease-out);
}
.svc__grid .card:hover .card__ghost { opacity: 0.1; transform: translateY(3px); }
/* accent wash rising from the base */
.card::after {
  content: '';
  position: absolute;
  inset: auto 0 0 0;
  height: 100%;
  background: linear-gradient(to top, var(--blue-50), transparent 62%);
  transform: scaleY(0);
  transform-origin: bottom;
  transition: transform 0.5s var(--ease-out);
  pointer-events: none;
  z-index: 0;
}
.svc__grid .card:hover::after { transform: scaleY(1); }
/* lift the content above the wash - but not the ghost numeral, which is
   absolutely positioned and must stay behind the text */
.card > *:not(.card__ghost) { position: relative; z-index: 1; }
/* higher specificity than .reveal.is-in so the lift survives the reveal */
.svc__grid .card:hover { transform: translateY(-4px); }
.card__icon path,
.card__icon rect { stroke-dasharray: 240; stroke-dashoffset: 240; }
.card.is-in .card__icon path,
.card.is-in .card__icon rect {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 1.3s var(--ease-out) 0.15s;
}

/* the icon sits in a tinted badge rather than bare on the card, so each
   feature reads as a distinct module and the hover fill gives it somewhere to
   go beyond the shared card lift */
.card__badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 64px;
  height: 64px;
  margin-bottom: 1.15rem;
  border-radius: 50%;
  background: var(--blue-50);
  transition: background 0.35s var(--ease-out), transform 0.35s var(--ease-out);
}
.card__badge .card__icon { color: var(--blue-600); transition: color 0.35s var(--ease-out); }
.svc__grid .card:hover .card__badge {
  background: var(--blue-700);
  transform: scale(1.06);
}
.svc__grid .card:hover .card__badge .card__icon { color: #fff; }

/* The meta tag used to double as a second, button-styled call to action -
   the client asked (2026-09-19) for it to go back to a plain status label,
   since the whole card is already the link and a second CTA inside it read as
   two competing affordances. Clickability is now implied only at the card
   level: .svc__grid .card:hover's own lift, wash and badge fill below. */

/* ── frameworks: a center divider, panels that expand ─────────────────── */
.fw__grid { position: relative; }
@media (min-width: 760px) {
  .fw__grid::before {
    content: '';
    position: absolute;
    left: 50%; top: 0; bottom: 0;
    width: 2px;
    /* a graduated scale at rest - the two indexes plot against a shared
       measure - that resolves to a solid connecting line once compare mode
       draws the two panels together */
    background: repeating-linear-gradient(to bottom, var(--line) 0 5px, transparent 5px 11px);
    transform: scaleY(0);
    transform-origin: top;
    transition: transform 1s var(--ease-out);
  }
  .fw__grid.is-in::before { transform: scaleY(1); }
}
.fw__vs { display: none; }
@media (min-width: 760px) {
  .fw__vs {
    position: absolute;
    left: 50%; top: 50%;
    translate: -50% -50%;
    z-index: 1;
    display: grid;
    place-items: center;
    width: 2.1rem; height: 2.1rem;
    border-radius: 50%;
    background: var(--surface);
    border: 1px solid var(--line);
    font-size: var(--fs-micro);
    font-weight: 700;
    letter-spacing: 0.04em;
    color: var(--ink-3);
    opacity: 0;
    scale: 0.6;
    transition: opacity 0.3s var(--ease-out), scale 0.3s var(--ease-out),
                border-color 0.3s var(--ease-out), color 0.3s var(--ease-out);
  }
  .fw__grid.is-comparing .fw__vs {
    opacity: 1;
    scale: 1;
    border-color: var(--gold-500);
    color: var(--gold-500);
  }
}
.fw__item { position: relative; }
/* each card's own identity rule - blue for the regional baseline, gold for the
   UAE-specific index - draws in the same way the mandate chain and route spine
   do, then stays as a permanent mark rather than a hover cue */
.fw__item::before {
  content: '';
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  height: 3px;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 1s var(--ease-out);
}
.fw__item.is-in::before { transform: scaleX(1); }
.fw__item[data-parallax] {
  translate: 0 calc((0.5 - var(--pv, 0.5)) * var(--sp, 1) * 22px);
}
.fw__toggle {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  width: 100%;
  margin-top: 1rem;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 0.86rem;
  font-weight: 500;
  color: var(--blue-700);
  cursor: pointer;
}
.fw__toggle span:last-child {
  transition: transform 0.4s var(--ease-out);
  display: inline-block;
}
.fw__toggle[aria-expanded="true"] span:last-child { transform: rotate(90deg); }
/* grid-template-rows, not height: it animates without laying out on each frame */
.fw__panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.5s var(--ease-out);
}
.fw__panel > div { overflow: hidden; }
.fw__item.is-open .fw__panel { grid-template-rows: 1fr; }
.fw__panel ul { list-style: none; margin: 0.9rem 0 0; padding: 0; display: grid; gap: 0.55rem; }
.fw__panel li {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.6rem;
  font-size: 0.86rem;
  color: var(--ink-2);
}
.fw__panel li::before { content: ''; width: 5px; height: 5px; border-radius: 50%; background: var(--gold-500); margin-top: 0.5rem; }

/* A switch that opens both panels at once, so the two indexes' scope sits side
   by side rather than the reader having to hold one open panel in memory
   while they open the second. It drives the existing toggle buttons rather
   than a separate open state, so aria-expanded on each stays true to what is
   actually on screen. */
.fw__compare { margin-bottom: 1.5rem; }
.fw__compare-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.55rem 1.1rem 0.55rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  font: inherit;
  font-size: 0.86rem;
  font-weight: 500;
  color: var(--ink-2);
  cursor: pointer;
  transition: border-color 0.3s var(--ease-out), color 0.3s var(--ease-out), background 0.3s var(--ease-out);
}
.fw__compare-btn:hover { border-color: var(--blue-300); }
.fw__compare-dot {
  position: relative;
  width: 1.7rem; height: 1rem; flex: none;
  border-radius: 999px;
  background: var(--line);
  transition: background 0.3s var(--ease-out);
}
.fw__compare-dot::after {
  content: '';
  position: absolute;
  top: 2px; left: 2px;
  width: calc(1rem - 4px); height: calc(1rem - 4px);
  border-radius: 50%;
  background: #fff;
  transition: translate 0.3s var(--ease-out);
}
.fw__compare-btn[aria-pressed="true"] { border-color: var(--blue-600); color: var(--blue-700); }
.fw__compare-btn[aria-pressed="true"] .fw__compare-dot { background: var(--blue-600); }
.fw__compare-btn[aria-pressed="true"] .fw__compare-dot::after { translate: 0.7rem 0; }
/* the center rule glows while the two panels are held open together, so the
   comparison reads as one deliberate state rather than two coincidences */
.fw__grid.is-comparing::before { background: var(--gold-500); }
@media (min-width: 760px) {
  .fw__grid.is-comparing::before { box-shadow: 0 0 14px 1px color-mix(in srgb, var(--gold-500) 55%, transparent); }
}

/* ── knowledge hub: country filter and maturity bars ──────────────────── */
/* ── knowledge hub lead plate ──
   The hub is a filter row and six text rows - the plainest block in the page, and
   the one place a picture has to do the work a coloured ground does elsewhere.
   Cropped shallow on purpose: it is a wide plate under the head, not a picture
   competing with the rows below it. */
.kh__lead {
  width: 100%;
  aspect-ratio: 21 / 6;
  object-fit: cover;
  object-position: center 45%;
  border-radius: var(--corner-box);
  margin-bottom: clamp(1.25rem, 2.5vh, 1.75rem);
}
@media (max-width: 640px) { .kh__lead { aspect-ratio: 16 / 9; } }
/* The video wants a deeper plate than the still did. Fixed 2026-08-29.

   21/6 was chosen against assets/knowledge-hub.jpg, which is 3466x1440 - ratio
   2.41, so the plate showed 69% of its height and the crop was a deliberate
   shallow band. The mp4 that replaced it is 1282x718, ratio 1.79, and the same
   plate shows 51%. Keeping the CSS while changing the source shape underneath it
   is not keeping the crop, and the frames prove it: at 21/6 all six podium discs
   are cut away, the folders are sliced through below their labels, and the top of
   the cloud is clipped. The six folders standing on their discs are the whole
   subject of the picture.

   2/1 rather than 16/9. 16/9 shows the frame essentially whole, but the shot has
   dead server-rack headroom top and bottom that is worth losing; 2/1 trims about
   11%, takes it from that dead space, and keeps every disc and the whole cloud.
   It also keeps this a wide plate under the head rather than a full picture
   block, which is what the note above is about. Do not go past about 2.4 - that
   is where the discs start disappearing again.

   Scoped to `video` rather than written into .kh__lead, because sections.html and
   structure.html still carry the class on an <img> pointing at the 3466x1440
   still. At 2/1 that still would be cropped on its sides instead of its height,
   which would change two reference pages that nobody asked to change. The
   element selector keeps this change to the one element whose source moved.

   object-position needs no change: the existing `center 45%` puts the 2/1 window
   45% down the slack, which is where the discs sit. */
video.kh__lead { aspect-ratio: 2 / 1; }

.kh__filters { display: flex; flex-wrap: wrap; gap: 0.45rem; margin-bottom: 1.25rem; position: relative; }
.kh__filter {
  position: relative;
  z-index: 1;
  padding: 0.38rem 0.8rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: transparent;
  font: inherit;
  font-size: 0.78rem;
  color: var(--ink-2);
  cursor: pointer;
  transition: border-color 0.3s var(--ease-out), background 0.3s var(--ease-out),
              color 0.3s var(--ease-out);
}
.kh__filter:hover { border-color: var(--blue-300); }
.kh__filter[aria-pressed="true"] {
  border-color: var(--blue-900);
  color: #fff;
}
/* Slides behind whichever chip is active. Base width is 1px so js/sections.js
   can size and position it with `translate(x, y) scaleX(px)` alone - a scaleX
   factor against a 1px box lands on the exact pixel width without animating
   `width` itself. Height is set inline too (chip height is uniform, so it
   never actually animates). Tracks wrapped rows and RTL mirroring for free,
   since the offsets come from real getBoundingClientRect() values. */
.kh__filter-pill {
  position: absolute;
  top: 0; left: 0;
  width: 1px;
  transform-origin: left center;
  border-radius: 999px;
  background: var(--blue-900);
  z-index: 0;
  opacity: 0;
  transition: transform 0.35s var(--ease-out), opacity 0.2s var(--ease-out);
}
.kh__filter-pill.is-active { opacity: 1; }
.usecase[hidden] { display: none; }
.usecase__bar {
  grid-column: 2;
  height: 3px;
  border-radius: 2px;
  background: var(--line-soft);
  margin-top: 0.6rem;
  overflow: hidden;
}
.usecase__bar i {
  display: block;
  height: 100%;
  /* the bar is the figure beside it, drawn - so it is the same accent */
  background: var(--gold-500);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 1s var(--ease-out) var(--d, 0s);
}
.usecase.is-in .usecase__bar i { transform: scaleX(var(--v, 0.5)); }
.kh__split .usecase:hover { transform: translateX(0.6rem); }

/* the expanded detail: same grid-template-rows technique as .fw__panel, so a
   row that was one line of context becomes a short brief without a navigation
   away from the list it belongs to */
.usecase__panel {
  grid-column: 1 / -1;
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.5s var(--ease-out);
}
.usecase__panel > div { overflow: hidden; }
.usecase[aria-expanded="true"] .usecase__panel { grid-template-rows: 1fr; }
.usecase__panel p { padding-top: 0.9rem; color: var(--ink-2); font-size: 0.95rem; max-width: 60ch; }
.usecase__link {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.6rem;
  padding-bottom: 0.2rem;
  font-size: 0.84rem;
  font-weight: 500;
  color: var(--blue-700);
  text-decoration: none;
}
.usecase__link:hover { text-decoration: underline; }

/* ── news: a horizontal rail ──────────────────────────────────────────── */
.news__rail {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(268px, 1fr);
  gap: 1.5rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  padding-bottom: 0.5rem;
  scrollbar-width: thin;
  scrollbar-color: rgba(255,255,255,0.3) transparent;
}
.news__rail > * { scroll-snap-align: start; }
@media (min-width: 1000px) { .news__rail { grid-auto-columns: minmax(0, 1fr); overflow: visible; } }
.news__nav { display: flex; gap: 0.5rem; }
.news__nav button {
  width: 40px; height: 40px;
  border: 1px solid rgba(255,255,255,0.28);
  border-radius: 50%;
  background: transparent;
  color: #fff;
  font: inherit;
  cursor: pointer;
  transition: background 0.3s var(--ease-out), border-color 0.3s var(--ease-out), opacity 0.3s;
}
.news__nav button:hover:not(:disabled) { background: rgba(255,255,255,0.12); border-color: var(--gold-300); }
.news__nav button:disabled { opacity: 0.3; cursor: default; }
@media (min-width: 1000px) { .news__nav { display: none; } }
.news__item h3 { transition: color 0.3s var(--ease-out); }
.news__item:hover h3 { color: var(--gold-300); }

/* Position feedback for the rail, since the arrows alone don't say where the
   reader is or how much further there is. Hidden once the rail stops
   scrolling at 1000px, same breakpoint .news__nav uses. */
.news__dots { display: flex; gap: 0.5rem; justify-content: center; margin-top: 1.25rem; }
.news__dot {
  width: 8px; height: 8px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(255,255,255,0.28);
  cursor: pointer;
  transition: background 0.3s var(--ease-out), transform 0.3s var(--ease-out);
}
.news__dot:hover { background: rgba(255,255,255,0.45); }
.news__dot.is-active { background: var(--gold-300); transform: scale(1.25); }
@media (min-width: 1000px) { .news__dots { display: none; } }

/* ── publications + news: two sections, a rail in each ─────────────────────
   What this replaces is the split screen: one band, two grounds, a seam down
   the middle, three thumbnail-and-text .brief items a side. The client asked
   for the two subjects separated again on 2026-08-29, so the seam is gone and
   with it .split, .split__panel, .split__inner, .split__head, .split__title,
   .split__sub, .split__list, .brief and .split__cta. Nothing outside index.php
   referenced any of them, which is why they are deleted rather than left in
   place - a component that styles no markup is a trap for the next person
   reading this file, not a spare.

   The two sections keep the grounds the two halves of the split had. The
   library sits on --surface-2 and the feed on the navy band, so the reader
   still tells them apart by weight before reading either heading, which was
   the one property of the split worth carrying over. It also means the second
   head can be centered without the two centered heads stacking into a column
   of matching type on one uninterrupted ground.

   The cards are .card. See the note on the markup: picture on top, kicker,
   title, sentence, which is the layout the client pointed at and which .card
   was already able to make.

   The rail itself is .news__rail, unchanged - the horizontal snap-scroller
   further up this file that the news section used before the split absorbed
   it. It stops scrolling at 1000px and becomes an even three-column grid, and
   .news__nav and .news__dots hide at the same breakpoint, so above page width
   the "slider" is a row. That is deliberate and it is what the client chose:
   three cards fit the row, so at desktop width there is nothing to advance to
   and the arrows would be dead controls. */
.rail--light { background: var(--surface-2); }

/* ── the article card ──
   No box. Set by the client 2026-08-29: the detail under the picture is not to
   sit on a white plate. It was .card - the services component, a white bordered
   plate with the photograph bled to its edges - and the objection is a fair one.
   By the time the reader reaches these two sections they have already passed
   three of those plates in the services grid, so a fourth, fifth and sixth make
   the page read as one long deck rather than as sections with different jobs.

   So the picture is the only shape here. It takes the corner treatment, which
   is what keeps these cards in the same family as everything else on the page,
   and the title and the sentence are set straight onto the section's own
   ground with nothing drawn behind them. That is the whole idea: the ground
   changes between the two sections - mist under Publications, the navy band
   under News - and with no plate in the way the type changes with it, so the
   two sections look like two different places instead of one component printed
   on two backdrops.

   Two things do the work a border used to. The category is a chip that rides
   inside the picture's bottom corner rather than a line of small caps above
   the title, which anchors the type block to its own image in a row where
   nothing else separates one card from the next; and a short rule sits over the
   title, in the ground's accent, which extends on hover. The rule is the only
   hover affordance besides the photograph's own scale, and it needs to be -
   with no plate there is no border to light up and no shadow to lift. */
.acard {
  display: flex;
  flex-direction: column;
  color: inherit;
  text-decoration: none;
}

/* The picture is the shape. `overflow: hidden` on this rather than on the image
   because the chip is positioned against it and the scale on hover has to be
   clipped by the same corners. */
.acard__media {
  position: relative;
  display: block;
  overflow: hidden;
  border-radius: var(--corner-box);
  aspect-ratio: 4 / 3;
  /* behind the photograph while it decodes, so the row does not flash the
     section's own ground through six card-sized holes */
  background: var(--blue-900);
}
.acard__photo {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* the files are 4:3 photographs and people sit in the upper half of them, so
     any crop is spent at the bottom - same reasoning as .card__photo */
  object-position: center 32%;
  transition: transform 0.6s var(--ease-out);
}
.acard:hover .acard__photo,
.acard:focus-visible .acard__photo { transform: scale(1.05); }

/* A wash up from the bottom edge, so the chip has something to sit on whatever
   the photograph is doing down there. Weak, and only over the bottom third:
   these are not captioned images and a scrim heavy enough to be noticed would
   be reading as a caption bar. */
.acard__media::after {
  content: '';
  position: absolute;
  inset: auto 0 0 0;
  height: 42%;
  background: linear-gradient(to top, rgba(20, 30, 38, 0.62), transparent);
  pointer-events: none;
}
.acard__kicker {
  position: absolute;
  z-index: 1;
  inset-inline-start: 0.85rem;
  bottom: 0.85rem;
  padding: 0.38rem 0.7rem;
  border-radius: var(--corner-chip);
  background: rgba(20, 30, 38, 0.55);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  color: #fff;
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  /* the date runs to three words and must not wrap inside a chip this size */
  white-space: nowrap;
}

/* The rule over the title. A pseudo-element and not a border, because it is a
   third of the width and it grows - a border cannot be either. */
.acard__body {
  position: relative;
  display: block;
  padding-top: 1.15rem;
}
.acard__body::before {
  content: '';
  position: absolute;
  top: 0;
  inset-inline-start: 0;
  width: 34px;
  height: 2px;
  background: var(--blue-600);
  transition: width 0.45s var(--ease-out);
}
.acard:hover .acard__body::before,
.acard:focus-visible .acard__body::before { width: 82px; }

.acard h3 {
  font-size: 1.18rem;
  font-weight: 500;
  line-height: 1.32;
  margin-block: 0.7rem 0.5rem;
  transition: color 0.3s var(--ease-out);
}
.acard p {
  font-size: 0.94rem;
  line-height: 1.55;
  color: var(--ink-2);
}
.acard:hover h3 { color: var(--blue-700); }

/* The focus ring goes on the card, not on the picture: the whole thing is one
   link and a ring around the image alone would leave the title looking
   unselected. Offset so it clears the picture's rounded corners. */
.acard:focus-visible { outline: 2px solid var(--blue-600); outline-offset: 4px; }

/* On the navy band the type is the band's own white and the accent is gold -
   --blue-600 on navy is the same 2.4:1 problem the header logo had. The chip
   keeps its dark glass on both grounds: it sits on a photograph, not on the
   section, so it takes its contrast from the picture underneath it. */
.rail--dark .acard p { color: rgba(255, 255, 255, 0.7); }
.rail--dark .acard__body::before { background: var(--gold-300); }
.rail--dark .acard:hover h3 { color: var(--gold-300); }
.rail--dark .acard:focus-visible { outline-color: var(--gold-300); }

/* The controls, in one row under the rail rather than beside the heading.
   Hidden at the breakpoint where the rail stops scrolling; .news__nav and
   .news__dots each hide themselves there too, but the row has to go as well or
   its margin survives as a 20px gap under every rail on desktop. */
.rail__foot {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1.25rem;
  margin-top: 1.25rem;
}
.rail__foot .news__dots { margin-top: 0; }
@media (min-width: 1000px) { .rail__foot { display: none; } }

/* .news__nav and .news__dot are written white-on-navy, because until now the
   only rail on the site was the one in the dark news section. The publications
   rail is on --surface-2, where white controls are invisible, so the light
   section restates the four colours. The geometry is not restated - only what
   actually changes with the ground - so a change to the button size or the dot
   size still lands on both. */
.rail--light .news__rail { scrollbar-color: rgba(20, 30, 38, 0.25) transparent; }
.rail--light .news__nav button { border-color: var(--line); color: var(--blue-700); }
.rail--light .news__nav button:hover:not(:disabled) {
  background: var(--blue-50);
  border-color: var(--blue-600);
}
.rail--light .news__dot { background: rgba(20, 30, 38, 0.2); }
.rail--light .news__dot:hover { background: rgba(20, 30, 38, 0.38); }
.rail--light .news__dot.is-active { background: var(--blue-600); }

/* ── the link out ──
   Centered, because the head above it is. The arrow is in the markup rather
   than a ::after because it moves on hover and a pseudo-element cannot be
   transformed independently of the label without a second wrapper. */
/* ── the tab strip over the news and publications grids ──────────────────
   Added 2026-08-29: the two sections became one with a tab each, so the section
   needed a control it did not have. There was no tab component anywhere in the
   codebase before this.

   Visually it is the Knowledge Hub filter chip - same pill, same border, same
   filled active state - and it is a separate class on purpose. .kh__filter is a
   multi-select filter: it carries aria-pressed, several can be on at once, and
   js/sections.js measures a .kh__filter-pill and slides it behind the active
   one. These are two mutually exclusive tabs with aria-selected and a roving
   tabindex. Borrowing the class would have meant a control whose ARIA and whose
   behaviour disagree with its markup, and the sliding pill would have needed a
   second set of measurements for a strip that never wraps. The pill is also what
   is not borrowed: with two tabs side by side there is nothing for a slider to
   clarify that the filled state does not already say.

   Centered, because the head above it is. Larger than the filter chip -
   --fs-sm against 0.78rem, and a deeper pad - because this is a section-level
   control rather than a refinement inside one, and it is the only thing standing
   between the head and the cards. */
.rail__tabs {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: clamp(1.25rem, 2.5vh, 1.75rem);
}
.rail__tab {
  padding: 0.5rem 1.15rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: transparent;
  font: inherit;
  font-size: var(--fs-sm);
  color: var(--ink-2);
  cursor: pointer;
  transition: border-color 0.3s var(--ease-out), background 0.3s var(--ease-out),
              color 0.3s var(--ease-out);
}
.rail__tab:hover { border-color: var(--blue-300); color: var(--ink); }
.rail__tab[aria-selected="true"] {
  background: var(--blue-900);
  border-color: var(--blue-900);
  color: #fff;
}
.rail__tab:focus-visible { outline: 2px solid var(--blue-600); outline-offset: 2px; }

/* The tab strip on the dark band, added 2026-08-29 with the section's dark
   treatment. The light rules are not merely dull there, they are unusable: the
   resting label is --ink-2 on navy, and the selected pill fills itself with
   --blue-900, which is the band's own ground - the selected tab would read as
   the unselected one with its label rubbed out. Selected goes to the gold
   instead, the only accent this band carries and the one already on the
   eyebrow and the link out, with its label in the band's dark so the pill
   holds without a border. Geometry is not restated, only what changes with the
   ground, so a change to the pad or the radius still lands on both. */
.rail--dark .rail__tab {
  border-color: rgba(255, 255, 255, 0.28);
  color: rgba(255, 255, 255, 0.78);
}
.rail--dark .rail__tab:hover { border-color: var(--gold-300); color: #fff; }
.rail--dark .rail__tab[aria-selected="true"] {
  background: var(--gold-300);
  border-color: var(--gold-300);
  color: var(--blue-900);
}
.rail--dark .rail__tab:focus-visible { outline-color: var(--gold-300); }

/* Stated rather than left to the UA default. The panel has no display of its
   own today, so `hidden` would work on its own - but the moment anyone gives
   .rail__panel a display it would stop working, silently, by showing both
   panels at once. One line now against a bug that would not look like a CSS
   bug later. */
.rail__panel[hidden] { display: none; }

/* The section lede while it is being exchanged for the other tab's. Added
   2026-08-29 with the swap in js/sections.js initRailTabs. The timeout there
   is 220ms and has to stay in step with this duration: the text is written at
   the far end of the fade, so a shorter transition here would show the change
   happening and a longer one would show a line that is still blank. */
[data-rail-lede] { transition: opacity 0.22s var(--ease-out); }
[data-rail-lede].is-swapping { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  [data-rail-lede] { transition: none; }
}

.rail__more { margin-top: clamp(1.5rem, 3vh, 2.25rem); text-align: center; }
.rail__cta {
  display: inline-flex;
  gap: 0.5rem;
  align-items: center;
  font-weight: 500;
  color: var(--blue-700);
  text-decoration: none;
}
.rail__cta span { transition: transform 0.3s var(--ease-out); }
.rail__cta:hover span { transform: translateX(4px); }
.rail__cta:hover { text-decoration: underline; text-underline-offset: 4px; }
.rail--dark .rail__cta { color: var(--gold-300); }
/* ── footer ───────────────────────────────────────────────────────────── */
/* transform, not padding: these nudge on hover and padding would lay the link
   out on every frame. Transforms have no logical equivalent, so the Arabic
   build needs the explicit mirror. */
.footer a { transition: color 0.25s var(--ease-out), transform 0.25s var(--ease-out); }
.footer li a { display: inline-block; }
.footer li a:hover { transform: translateX(0.35rem); }
[dir="rtl"] .footer li a:hover { transform: translateX(-0.35rem); }

/* ── reduced motion: state without journey ────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  .nav, .hero__bar i, .card, .usecase, .usecase__chev, .btn, .nav__links a::after,
  .kpi__cell, .kpi__val, .kpi__note, .eco__chip, .kh__filter,
  .news__nav button, .news__dot, .footer a, .card__ghost,
  /* the article card's two moving parts: the photograph's scale and the rule
     that grows over the title */
  .acard__photo, .acard__body::before,
  .fw__compare-btn, .fw__compare-dot, .fw__compare-dot::after,
  .eco__item, .eco__item::after, .eco__num, .eco__icon, .news__item, .fw__vs, .kh__filter-pill,
  .route__node, .route::before, .card__badge, .card__badge .card__icon,
  .card__meta .tag { transition: none; }

  .reveal, .reveal--wipe { opacity: 1; translate: 0 0; clip-path: none; transition: none; }
  .rule { transform: scaleX(1); transition: none; }
  .card__icon path, .card__icon rect { stroke-dashoffset: 0; }
  .fw__panel, .usecase__panel { transition: none; }
  .usecase__bar i { transform: scaleX(var(--v, 0.5)); transition: none; }
  .acard:hover .acard__photo, .acard:focus-visible .acard__photo { transform: none; }
  .svc__grid .card:hover, .kh__split .usecase:hover, .news__item:hover,
  .svc__grid .card:hover .card__badge { transform: none; }
  .eco__item::after { transform: scaleX(1); opacity: 0; }
  .eco__item:hover::after { opacity: 1; }
}

/* ═══════════════════════════════════════════════════════════════════════
   HERO COUNTRY PROFILES
   The markers are controls, so they take pointer events back from the
   slide (which is pointer-events:none so it never blocks the wheel).
   ═══════════════════════════════════════════════════════════════════ */
/* SVG defaults to `transform-box: view-box` with a 0,0 origin, so scaling a
   marker would scale it about the SVG's corner and fling it across the frame.
   Under the cursor that is a feedback loop: the marker leaves the pointer,
   pointerout fires, it returns, pointerover fires, and it oscillates. Anchor
   every marker to its own center. */
.net-state { transform-box: fill-box; transform-origin: center; }

/* The selection guards are on the marker rather than on a wrapper because the
   marker is the only thing here a thumb is invited to press. Belt and braces
   since 2026-09-16 rather than the fix: the reported bug was that holding a
   country circle selected text, and the cause was that holding was the only
   way to keep the card open at all - js/hero-countries.js explains that and no
   longer needs anyone to hold. These stop the gesture being answered by the OS
   if somebody holds anyway, out of habit or because they are dragging.

   `-webkit-touch-callout` is iOS's copy/look-up sheet, a separate thing from
   the selection and not covered by user-select. `touch-action: manipulation`
   drops the double-tap-to-zoom wait, which is what would otherwise make the
   second tap - the one that now closes the card - feel late. */
.net-state--live {
  pointer-events: auto;
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}
.net-state--live:focus { outline: none; }
.net-state--live:focus-visible .net-node { stroke: #fff; stroke-width: 3; }
.hero .layer--visual.is-focus .net-state { opacity: 0.22; scale: 0.92; }
.hero .layer--visual.is-focus .net-state.is-active { opacity: 1; scale: 1.22; }

/* ── the marker locks on ───────────────────────────────────────────────
   Selected, one of the six becomes a target: a reticle spins up, four ticks
   snap inward, radar pings run outward, the node glows in its state colour and
   the spoke feeding it goes live while the rest of the graphic falls back. */
.net-state--live { transition: filter 0.4s var(--ease-out); }
.net-state--live.is-active {
  filter: drop-shadow(0 0 6px color-mix(in srgb, var(--c) 75%, transparent))
          drop-shadow(0 0 22px color-mix(in srgb, var(--c) 45%, transparent));
}

/* `display: none`, not `opacity: 0`: `.net-state` scales about its own
   bounding box and js/hero-countries.js anchors the country card to that same
   box, so an idle marker has to keep exactly the box it had without these. */
.net-fx { display: none; pointer-events: none; }
.net-state--live.is-active .net-fx { display: block; }

.net-ping,
.net-reticle,
.net-ticks { transform-box: fill-box; transform-origin: center; }

/* radii in CSS for the same reason as the markers themselves - they grow on
   small screens, where a fixed radius collapses onto the node */
.net-ping { r: 13px; }
.net-state--live.is-active .net-ping {
  animation: node-ping 1.9s var(--ease-out) infinite;
  animation-delay: calc(var(--k) * 0.95s);
}
@keyframes node-ping {
  0%   { scale: 1; opacity: 0.7; }
  75%  { opacity: 0; }
  100% { scale: 3.6; opacity: 0; }
}

/* `rotate` and `scale` are separate properties, so the lock-on and the spin
   run as two animations on one element without overwriting each other */
.net-reticle { r: 30px; opacity: 0; }
.net-state--live.is-active .net-reticle {
  animation: reticle-lock 0.45s var(--ease-out) forwards,
             reticle-spin 7s linear infinite;
}
@keyframes reticle-lock { from { scale: 1.7; opacity: 0; } to { scale: 1; opacity: 0.9; } }
@keyframes reticle-spin { to { rotate: 360deg; } }

/* --s is the resting scale, bumped alongside the markers below; the keyframes
   read it so the snap-in always lands on the right size */
.net-ticks { --s: 1; opacity: 0; }
.net-state--live.is-active .net-ticks {
  animation: tick-lock 0.4s var(--ease-out) 0.06s forwards;
}
@keyframes tick-lock {
  from { scale: calc(var(--s) * 1.9); opacity: 0; }
  to   { scale: var(--s); opacity: 0.95; }
}

/* opacity only - the live spoke's stroke jumps straight to its heavier weight,
   which is invisible underneath the opacity ramp and keeps the transition off a
   geometry property */
.hero .net-spoke { transition: opacity 0.35s var(--ease-out); }
.hero .layer--visual.is-focus .net-spoke { opacity: 0.14; }
.hero .layer--visual.is-focus .net-pulse { opacity: 0.16; }
.hero .layer--visual.is-focus .net-pulse.is-live {
  opacity: 1;
  stroke-width: 4;
  stroke-dasharray: 26 162;
  animation-duration: 1.15s;   /* the same travelling dash, running hot */
  animation-delay: 0s;
  filter: drop-shadow(0 0 7px var(--c));
}

/* The targeting furniture grows with the markers it wraps. These have to sit
   after the declarations above rather than in the radius block further up the
   sheet - same specificity, so source order decides. Strokes are bumped too:
   `r` scales the circle but not its outline. `--s` scales the tick group whole,
   which carries its stroke with it. */
@media (max-width: 900px) {
  .net-reticle { r: 38px; stroke-width: 1.8; }
  .net-ping { r: 17px; stroke-width: 1.9; }
  .net-ticks { --s: 1.28; }
}
@media (max-width: 620px), (max-height: 620px) {
  .net-reticle { r: 49px; stroke-width: 2.3; }
  .net-ping { r: 22px; stroke-width: 2.4; }
  .net-ticks { --s: 1.64; }
}

/* js/hero-countries.js appends this as a sibling of .hero__cta inside
   .hero__viewport, not inside .hero__stage - .hero__stage sets its own
   z-index (1), which would have trapped any z-index set here one level
   below the CTA (z-index 3) regardless of value, the same problem the
   .ccard--sheet note below already ran into. 20 clears the CTA and every
   other hero layer (all at 3 or under). It still cannot clear the site nav
   or the cookie banner: those sit outside .hero__viewport's own
   `isolation: isolate`, which nothing in here can escape. */
.ccard {
  position: absolute;
  z-index: 20;
  /* 400px, not 300px, since 2026-09-16. The grid below went to two columns at
     GOIC's request, and the note there records why 300px could not carry them:
     "Frontier Technologies Readiness" needs roughly a 180px column to wrap at
     two lines. Two of those plus the 1rem column gap and 2.2rem of padding is
     just under 400. The card is only ever anchored above NARROW (720px) in
     js/hero-countries.js, and it is clamped to 12px from either edge, so the
     widest it ever needs is 424px of viewport - clear by nearly 300. Below that
     it becomes .ccard--sheet, which sets width:auto and ignores this. */
  width: 400px;
  padding: 1rem 1.1rem 0.9rem;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-top: 2px solid var(--c, var(--gold-300));
  border-radius: var(--corner-box);
  background: rgba(20, 30, 38, 0.82);
  backdrop-filter: blur(16px);
  color: #fff;
  opacity: 0;
  translate: 0 6px;
  transition: opacity 0.28s var(--ease-out), translate 0.28s var(--ease-out);
  pointer-events: none; /* the card must never steal hover from the marker */
}
.ccard.is-in { opacity: 1; translate: 0 0; }

.ccard__name {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  /* Down a step with the grid below it, 2026-09-16. The name has to stay the
     largest thing on the card - it is what says which marker you are on - so it
     moves with the numbers rather than staying put and outgrowing them. */
  font-size: 0.9rem;
  font-weight: 600;
  letter-spacing: -0.01em;
}
/* The state's flag, in the slot an 8px accent dot used to hold. Same job -
   telling the reader at a glance which node they are on - done better, and the
   accent is not lost: it becomes the frame's hairline, so the card still reads
   as belonging to the marker it is anchored to. */
.ccard__flag {
  flex: none;
  width: 24px;
  aspect-ratio: 5 / 3;
  overflow: hidden;
  border-radius: var(--corner-chip);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--c) 75%, transparent);
}
.ccard__flag .flag {
  display: block;
  width: 100%; height: 100%;
  /* Qatar is drawn 3:2 where the other five are 5:3 - see js/flags.js. */
  object-fit: cover;
}
.ccard__name span {
  margin-inline-start: auto;
  /* Down with the rest of the card, 2026-09-16. The capital is a subtitle to
     the state's name, so it has to stay smaller than it. */
  font-size: 0.6875rem;
  font-weight: 400;
  color: rgba(255, 255, 255, 0.55);
}

/* SUPERSEDED 2026-09-16 by EM's request for two columns. Kept because the
   measurement in it is still the constraint, and is what decided the card's
   new width:

     "One column, not two. The four labels are now full names rather than
      acronyms, and a 268-345px card is nowhere near wide enough to hold a
      half-width column - 'Frontier Technologies Readiness' alone needs a
      ~180px column to wrap at only two lines. A single, full-width column
      holds every label at two lines or fewer without the card ballooning."

   Two columns, then, and the card widened to 400px to pay for them rather than
   the labels being squeezed - see .ccard above. The type comes down a step at
   the same time, which is the other half of what was asked and also what keeps
   a four-row card from becoming a two-row card that is taller than the one it
   replaced: 0.6875rem labels wrap "Frontier Technologies Readiness" at two
   lines in a 180px column, the same fit the note above measured.

   minmax(0, 1fr) rather than 1fr: a grid column's default min-width is auto,
   and the longest unbreakable label would otherwise push the column past its
   half and the card past 400px. */
.ccard__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.75rem 1rem;
  margin: 0.85rem 0 0;
}
/* A cell is a column with its figure pinned to the foot, which is what keeps
   the two figures in a row on one line. Grid makes the cells in a row equal
   height but packs each one's content to its own top, so with a two-line label
   beside a one-line one the right-hand figure floated 17px above the left-hand
   one - measured on the Qatar card, 2026-09-16. This only became visible with
   the two-column layout; in a single column there was no neighbour to be out of
   line with. */
.ccard__grid > div {
  display: flex;
  flex-direction: column;
}
.ccard__grid dt {
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.5);
}
.ccard__grid dd {
  /* padding, not margin, for the gap under the label: margin-block-start is
     spent on the auto that does the pinning. */
  margin: 0;
  margin-block-start: auto;
  padding-block-start: 0.15rem;
  font-size: 0.95rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}
.ccard__grid small { font-size: 0.6em; font-weight: 400; color: rgba(255,255,255,0.55); margin-inline-start: 0.25em; }

.ccard__sector {
  margin-top: 0.9rem;
  padding-top: 0.7rem;
  border-top: 1px solid rgba(255, 255, 255, 0.14);
  font-size: var(--fs-micro);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.5);
}
.ccard__sector b {
  display: block;
  margin-top: 0.25rem;
  font-size: 0.85rem;
  font-weight: 550;
  letter-spacing: 0;
  text-transform: none;
  color: #fff;
}
.ccard__sector span { font-size: var(--fs-micro); letter-spacing: 0; text-transform: none; color: var(--c); }

/* 0.6875rem rather than --fs-micro since 2026-09-16: once the metric labels
   came down to that, a 0.75rem source line was the largest small text on the
   card, which is backwards - it is the least important line on it. */
.ccard__src {
  margin-top: 0.7rem;
  font-size: 0.6875rem;
  letter-spacing: 0.05em;
  color: rgba(255, 255, 255, 0.38);
}
/* derived figures must never be mistaken for sourced ones */
.ccard__src.is-derived { color: var(--gold-300); }
.ccard__src.is-derived::before { content: '● '; font-size: 0.7em; }

/* ── the close button ──────────────────────────────────────────────────────
   Drawn only on the sheet. The anchored card on a pointer device is dismissed
   by moving the cursor off the marker, so a button there would be a control
   for something the reader has already done; and .ccard is `pointer-events:
   none` on that path anyway, so it could not be clicked even if it were shown.

   44px square, which is the smallest target worth giving a thumb, and it is
   the reason the sheet's own top padding is not reduced to match: the glyph
   sits in the corner the card already keeps clear above .ccard__name.

   `inset-inline-end` rather than `right`, so it moves to the other corner on
   the Arabic site without a rule in css/rtl.css. */
.ccard__close {
  display: none;
  position: absolute;
  inset-block-start: 0.15rem;
  inset-inline-end: 0.15rem;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: rgba(255, 255, 255, 0.75);
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.ccard--sheet .ccard__close { display: block; }
/* The name is the one line the button can reach, and it is the line that must
   not be clipped - it says which marker you are on. The capital sits at the end
   of a flex row, so it runs to the content edge and is what actually collides.

   The button intrudes 28.8px into the content box: 44px wide, offset 0.15rem
   from the card, against the card's own 1.1rem inset. 1.9rem was tried first,
   which is that figure plus a rounding allowance, and it measured a 2px gap -
   arithmetically clear and visually touching the glyph. 2.5rem leaves 11px,
   which is a gap a reader sees as deliberate rather than as a near miss. */
.ccard--sheet .ccard__name { padding-inline-end: 2.5rem; }
.ccard__close:hover,
.ccard__close:focus-visible { color: #fff; }
.ccard__close:focus-visible { outline: 2px solid var(--c, var(--gold-300)); outline-offset: -4px; }

/* Narrow screens: a fixed sheet at the base of the hero. An anchored card
   cannot sit beside a ~15px marker on a 375px viewport without covering it.

   It clears the CTA rather than sitting under it. At 4.5rem the sheet's lower
   third ran straight through .hero__cta, and the button won - back when the
   card lived inside .hero__stage, whose own z-index trapped it below the CTA
   regardless of its own value (fixed 2026-09-15: js/hero-countries.js now
   appends the card as .hero__cta's sibling instead, so z-index orders them
   correctly). This sheet still avoids the overlap by spacing rather than
   relying on stacking, since that reads better than the two visibly
   fighting for the same strip. 9.5rem is measured, not guessed: it leaves a
   steady ~25px above the
   button from a 320px phone up to a 412px one, because the CTA's own offset
   from the foot does not move across that range. Covering the headline and the
   body is intended - that is what a sheet does while it is open.

   SUPERSEDED 2026-09-16, and the sentence above kept because it explains the
   9.5rem: this used to end "and the copy comes back the moment the marker is
   released", which was true of a sheet you had to hold open. Holding is gone
   (js/hero-countries.js), so the sheet now stays up until it is dismissed, and
   it takes back `pointer-events` to make that possible. The base .ccard sets
   `none` so an anchored card can never steal hover from the marker underneath
   it; a sheet has no marker underneath it and does have a close button, so the
   reason does not reach this far. */
.ccard--sheet {
  left: var(--pad);
  right: var(--pad);
  bottom: 9.5rem;
  top: auto !important;
  width: auto;
  pointer-events: auto;
}

@media (prefers-reduced-motion: reduce) {
  .ccard { transition: none; }
  .hero .layer--visual.is-focus .net-state.is-active { scale: 1; }

  /* The six chips and the six markers keep the lit state and lose the travel:
     the traced ring stays, frozen, and everything that only exists as
     movement - the scan pass, the flag shine, the radar rings and the live
     spoke - is dropped rather than left flickering in place.
     These sit last in the sheet because several of the rules they undo are
     more specific than the general reduced-motion block further up. */
  .eco__chip,
  .eco__chip::after,
  .eco__chip-flag,
  .eco__chip-flag::after { transition: none; }
  .eco__chip:hover,
  .eco__chip:focus-visible { transform: none; }
  .eco__chip::before { display: none; }
  .eco__chip:hover::after,
  .eco__chip:focus-visible::after { animation: none; }
  .eco__chip:hover .eco__chip-flag,
  .eco__chip:focus-visible .eco__chip-flag { transform: none; }

  .net-state--live,
  .hero .net-spoke { transition: none; }
  .net-state--live.is-active .net-ping { display: none; }
  .net-state--live.is-active .net-reticle { animation: none; opacity: 0.9; }
  .net-state--live.is-active .net-ticks {
    animation: none;
    opacity: 0.95;
    scale: var(--s);
  }
  .hero .layer--visual.is-focus .net-pulse,
  .hero .layer--visual.is-focus .net-pulse.is-live {
    opacity: 0;
    animation: none;
    filter: none;
  }

  /* The CTA keeps its place and its fill and loses the pulse, the sheen and
     the emitted rings; the accent word keeps its colour and loses the flare. */
  .hero__cta { opacity: 1; translate: 0 0; animation: none; }
  .hero__cta::before,
  .hero__cta::after,
  .hero__cta-btn::before { display: none; }
  .hero__cta-btn { animation: none; }
  .hero .title em .ch { animation: none; }
  /* the bar's CTA still hands over, it just does it without the collapse */
  .nav__cta { transition: none; }
}

/* ── indicators: the ring ──
   Only the proportion gets a ring. Giving the counts one too would imply they
   are shares of something, which they are not. */
.kpi__grid--three { grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr)); }

.kpi__cell--gauge {
  display: flex;
  align-items: center;
  gap: 1.15rem;
}

.gauge { position: relative; flex: none; width: clamp(5.5rem, 8vw, 7rem); aspect-ratio: 1; }
.gauge svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.gauge circle { fill: none; stroke-width: 8; stroke-linecap: round; }
.gauge__track { stroke: rgba(255, 255, 255, 0.16); }
.gauge__fill {
  stroke: var(--gold-300);
  /* dasharray and the starting offset are written by js/sections.js from the
     measured circumference, so the ring is correct at any radius. Until that
     runs there is no dasharray, so the arc paints as a *complete* circle -
     the ring read 100% for the first frames, then snapped to empty to fill.
     Hold it invisible until the measurement lands; `is-primed` says it has. */
  stroke-opacity: 0;
  transition: stroke-dashoffset 1.6s var(--ease-out), stroke-opacity 0.25s linear;
}
.gauge.is-primed .gauge__fill { stroke-opacity: 1; }
.gauge__val {
  position: absolute;
  inset: 0;
  /* flex, not grid: the number is a <span> and the "%" is a bare text node, and
     a grid container would make them two rows */
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: clamp(1.15rem, 1.8vw, 1.5rem);
  font-weight: 600;
  letter-spacing: -0.03em;
  color: #fff;
  font-variant-numeric: tabular-nums;
}
.kpi__cell--gauge .kpi__label { margin-top: 0; }

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

/* ── ecosystem: the mandate, and the six actors around GOIC ──
   The mandate statement is the section's claim to authority, so it is set as a
   pull-quote rather than body copy. */
.eco__mandate {
  margin-block: 0.9rem 1.25rem;
  padding-inline-start: 1rem;
  border-inline-start: 2px solid var(--gold-500);
  font-size: clamp(1rem, 1.4vw, 1.12rem);
  line-height: 1.5;
  color: var(--ink);
}
.eco__mandate b { font-weight: 600; }

/* One spine, not two diagrams: the states and the six actors are stops on the
   same line rather than separate networks, so the section reads as a single
   mechanism instead of two unrelated charts. Normal flow, not absolute
   placement, so every tier wraps on its own at any width - there is no
   breakpoint where the layout has to switch shape. */
.route {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(1.1rem, 2.5vh, 1.6rem);
  margin-top: clamp(1.1rem, 2.5vh, 1.5rem);
  padding-block: 0.5rem;
}
/* The line running through all three tiers. Positioned elements paint above
   static ones in the same stacking context regardless of source order, so
   each tier below is `position: relative` and sits over this without needing
   a z-index. */
.route::before {
  content: '';
  position: absolute;
  left: 50%; top: 0; bottom: 0;
  width: 1px;
  background: var(--line);
  transform-origin: top;
  transform: scaleY(0);
  transition: transform 1s var(--ease-out);
}
.route.is-in::before { transform: scaleY(1); }

.route__states,
.route__hub,
.route__actors { position: relative; }

.route__states { justify-content: center; margin-top: 0; }

.route__hub {
  width: max-content;
  max-width: 9.5rem;
  padding: 0.6rem 0.9rem;
  border-radius: 10px 0 10px 0;
  text-align: center;
  line-height: 1.25;
  background: var(--blue-900);
  color: #fff;
  box-shadow: 0 12px 30px -12px rgba(20, 30, 38, 0.5);
}
.route__hub b { display: block; font-size: 0.85rem; font-weight: 600; }
.route__hub span { font-size: var(--fs-micro); color: var(--gold-300); }

.route__actors {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.75rem;
}
.route__node {
  width: max-content;
  max-width: 9.5rem;
  padding: 0.5rem 0.7rem;
  border-radius: 10px 0 10px 0;
  text-align: center;
  line-height: 1.25;
  background: var(--surface);
  border: 1px solid var(--line);
  transition: border-color 0.35s var(--ease-out), box-shadow 0.35s var(--ease-out);
}
.route__node b { display: block; font-size: 0.82rem; font-weight: 600; }
.route__node span { font-size: var(--fs-micro); color: var(--ink-3); }
.route__node:hover {
  border-color: var(--gold-500);
  box-shadow: 0 10px 24px -14px rgba(20, 30, 38, 0.45);
}

/* ─────────────────────────── publications ───────────────────────────
   Built to match the card grid on accenture.com/sa-en, measured rather than
   remembered. What that page actually does, at a 1440 viewport:

     grid           4 columns of 276px with a 48px gutter - separated cards, NOT
                    butted together. An earlier note in this file claimed they
                    were flush over a 1px hairline; they are not.
     card           276x424, so 69/106. padding 0, radius 0, no border, no
                    shadow, overflow hidden.
     label          14px/16.8px, weight 500, uppercase, 0.28px tracking,
                    28px down from the top and 24px in from the left.
     title          20px/24px, weight 500, -0.4px tracking, 24px side margins.
     artwork        absolutely positioned across the whole card, running off its
                    edges, cropped by the card rather than fitted inside it.
     tone           per content type: white, warm grey #f1f1ef, or black.
     hover          the artwork dims and pulls away, the summary arrives under
                    the title, and an "Expand" affordance appears bottom right.

   One structural adaptation. The reference's cards read because its page ground
   is black: a white card is a bright plate on a dark field. On this page the
   section ground is white, so a white card is invisible - the first build of this
   had two cards that simply were not there. The band therefore takes `--surface-2`
   as its own ground and the cards sit on it in three tones, which keeps the
   reference's contrast structure (light plates plus dark plates on a ground that
   is neither) without turning an approved light page dark.

   Two deliberate departures, both forced:

   * Typeface stays IBM Plex Sans, not Graphik. §5 and the bilingual requirement
     settle that - Plex ships a matching Arabic. Sizes, weights, tracking and
     leading are matched to the numbers above.
   * Every card takes the illustration treatment, the one Accenture uses on its
     "Perspective" cards, because there is no real photography to place. Their
     photographic cards are not reproducible without photographs, and the
     abstract renders in assets/pubs/ are not a substitute - they are re-cuts of
     hero art, near-identical to each other, and say nothing about a document.

   The artwork bleeds off the card's edges on purpose. Fitted inside the card
   with air around it, a drawing this size reads as an enlarged icon; cropped by
   the card edge it reads as artwork, which is what the reference does. */
/* the band's own ground, so the light cards have something to read against */
.pubs { background: var(--surface-2); }

.pubs__grid {
  display: grid;
  /* was 48px at desktop, matching the reference gutter. The six cards wrap to
     two rows, so that gutter was charged twice vertically and once horizontally;
     28px keeps the strip reading as a strip without paying for it twice. */
  gap: clamp(1rem, 2vw, 1.75rem);
  grid-template-columns: 1fr;
  margin-top: clamp(1.25rem, 2.5vh, 1.75rem);
}
@media (min-width: 560px) { .pubs__grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 960px) { .pubs__grid { grid-template-columns: repeat(3, 1fr); } }

.pub {
  --pub-ink: var(--ink);
  --pub-ground: var(--surface);
  --pub-draw: #CBD8DF;
  --pub-accent: var(--gold-500);
  --pub-focus: var(--blue-600);
  /* one writer for the type inset, since the label and the title share it and
     the Expand affordance is positioned off it */
  --pub-inset: 1.5rem;

  position: relative;
  display: block;
  /* 276/424 from the reference */
  aspect-ratio: 69 / 106;
  padding: 0;
  background: var(--pub-ground);
  color: var(--pub-ink);
  text-decoration: none;
  overflow: hidden;
  transition: background 0.7s var(--ease-out);
}

/* Tone alternates across the strip so it has a beat rather than six of the same,
   the way the reference mixes white, warm grey and black. Set in the markup, not
   with :nth-child, because which cards carry which tone is editorial - and the
   dark ones are always region-wide items. The two cards that name a member state
   (Oman's guide, the UAE's ITTI report) are never the emphasised ones; giving one
   state's document more weight than the other five would fail MOM §6.3. */
.pub--tint { --pub-ground: var(--surface-3); --pub-draw: #9AC5D3; }
.pub--dark {
  --pub-ground: var(--blue-900);
  --pub-ink: #ffffff;
  --pub-draw: #4B7E99;
  --pub-accent: var(--gold-300);
  --pub-focus: var(--gold-300);
}

/* ── the artwork ──
   Across the whole card and off its edges, so the card crops it. Anchored to the
   bottom right like the reference's illustrations, and scaled past 100% so both
   the left edge and the foot are cut. */
.pub__draw {
  position: absolute;
  /* past the card on three sides, so the card genuinely cuts the drawing. At
     116% it still had air on both sides, because each drawing's subject fills
     only the middle ~75% of its viewBox - the box bled and the picture did not. */
  right: -25%;
  bottom: -20%;
  width: 150%;
  z-index: 0;
  color: var(--pub-draw);
  /* Deliberately slow. The reader is watching one thing leave and another
     arrive in its place, and at the durations this started on (0.55s/0.66s) the
     exchange was over before the eye had followed it. */
  transition: opacity 0.9s var(--ease-out), transform 1.1s var(--ease-out),
              color 0.7s var(--ease-out);
}
.pub__draw .pa {
  display: block;
  width: 100%;
  height: auto;
  /* the source 1.6 is drawn for icon size; at this scale it would render near
     3.3px, so this holds the line around 1.7px */
  stroke-width: 0.82;
}
.pub__draw .pa-fill { fill: var(--pub-accent); }
.pub__draw .pa-stroke-accent { stroke: var(--pub-accent); }

/* ── the artwork, as a photograph ──
   The same slot as .pub__draw and the same choreography, for a card whose art is
   a raster rather than a drawing. It differs in two ways that matter:

   A photograph is not scaled past the card. The drawing is blown up to 150% so
   the card crops it, which is right for line art and wrong for a photograph -
   enlarging one past its frame just softens it. This sits at the card's own width
   and lets `object-fit` do the cropping.

   And it stops short of the type instead of sitting behind it. The drawing is a
   pale line on the card's ground, so the title reads straight over it; a
   photograph does not, on any of the three tones. Rather than laying a scrim over
   the picture and a second one under the type, the picture occupies the lower two
   thirds and its top edge is masked away into the card's own ground - so it fades
   into white, tint or blue-900 without knowing which it is. */
.pub__photo {
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  /* 58% and a long ramp, not 62% and a short one: at the two-column breakpoint
     the cards are at their narrowest, titles run to three lines and the picture's
     box arrives level with the last one. The type stays clear because the top of
     the picture is masked away entirely - so the ramp is the real clearance, and
     it is worth more than the extra 4% of picture. */
  height: 58%;
  width: 100%;
  max-width: none;
  object-fit: cover;
  /* people sit high in these frames; see .card__photo for the same reasoning */
  object-position: center 32%;
  z-index: 0;
  mask-image: linear-gradient(to bottom, transparent 0, #000 30%);
  transition: opacity 0.9s var(--ease-out), transform 1.1s var(--ease-out);
}

/* ── the type ── */
.pub__cat,
.pub__title,
.pub__desc,
.pub__more { position: relative; z-index: 1; }

.pub__cat {
  margin: 0 var(--pub-inset) 1.5rem;
  padding-top: 1.75rem;
  font-size: 0.875rem;
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--pub-ink);
}

.pub__title {
  margin: 0 var(--pub-inset);
  font-size: 1.25rem;
  font-weight: 550;
  line-height: 1.2;
  letter-spacing: -0.02em;
}

/* Arrives on hover, under the title, where the reference puts it. */
.pub__desc {
  margin: 1.5rem var(--pub-inset) 0;
  font-size: 0.9rem;
  line-height: 1.5;
  color: var(--pub-ink);
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 0.72s var(--ease-out) 0.16s, transform 0.9s var(--ease-out) 0.16s;
}

.pub__more {
  position: absolute;
  right: var(--pub-inset);
  bottom: 1.35rem;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 1rem;
  font-weight: 500;
  color: var(--pub-ink);
  opacity: 0;
  transform: translateX(-8px);
  transition: opacity 0.66s var(--ease-out) 0.26s, transform 0.82s var(--ease-out) 0.26s;
}
.pub__more i {
  width: 0.4rem;
  height: 0.4rem;
  border-top: 1.6px solid currentColor;
  border-right: 1.6px solid currentColor;
  transform: rotate(45deg);
  transition: transform 0.66s var(--ease-out) 0.26s;
}

/* ── hover and focus ──
   The artwork leaves so the summary has clean ground to arrive on: it fades and
   pulls down and out, the way the reference's does, rather than cutting. */
.pub:hover, .pub:focus-visible { --pub-ground: var(--blue-50); }
.pub--tint:hover, .pub--tint:focus-visible { --pub-ground: var(--blue-100); }
.pub--dark:hover, .pub--dark:focus-visible { --pub-ground: var(--blue-800); }

.pub:hover .pub__draw,
.pub:focus-visible .pub__draw,
.pub:hover .pub__photo,
.pub:focus-visible .pub__photo {
  opacity: 0;
  transform: translateY(7%) scale(1.06);
}
.pub:hover .pub__desc,
.pub:focus-visible .pub__desc,
.pub:hover .pub__more,
.pub:focus-visible .pub__more { opacity: 1; transform: none; }
.pub:hover .pub__more i,
.pub:focus-visible .pub__more i { transform: rotate(45deg) translate(2px, -2px); }

.pub:focus-visible { outline: 2px solid var(--pub-focus); outline-offset: -4px; }

@media (prefers-reduced-motion: reduce) {
  .pub, .pub__draw, .pub__photo, .pub__desc, .pub__more, .pub__more i { transition: none; }
}

/* Touch has no hover, so the summary would be unreachable. Show both: the card
   grows to its content, the artwork sits as a band under the type at its natural
   aspect, and the summary and Expand are always visible. The reference solves the
   same problem with an explicit toggle button and a close control, which is the
   closer answer if this ever needs to behave identically on a phone. */
@media (hover: none) {
  .pub { aspect-ratio: auto; padding-bottom: 4.25rem; }
  .pub__draw {
    position: relative;
    right: auto;
    bottom: auto;
    width: 150%;
    margin: 1.35rem -25% -18%;
    opacity: 1;
    transform: none;
  }
  /* in flow at the top of the card, where it becomes the card's header. No mask:
     it is a plain band here, not something fading out from behind the title. */
  .pub__photo {
    position: relative;
    height: auto;
    aspect-ratio: 4 / 3;
    mask-image: none;
    opacity: 1;
    transform: none;
  }
  .pub__desc { opacity: 1; transform: none; }
  .pub__more { opacity: 1; transform: none; }
}

/* ═══════════════════ pattern and imagery ═══════════════════
   The sections were a stack of pale bands with no texture and no imagery, so
   the page read as a wireframe however good the individual pieces were. What
   fixes that without inventing content is the commissioned hero art, reused as
   section-scale imagery. A tiled hexagon pattern did the same job here for a
   while; see the plane below for why it went. */

/* The section plane. This was the hex mesh - the identity's hexagonal geometry
   tiled as a faint background texture, in ink on the light sections and in pale
   blue on the dark one. It has been removed on request: the sections carry their
   own washes, art bands and diagrams now, and the pattern behind all of that read
   as noise rather than as paper.

   What is left is the plane itself, which several sections still need: `.eco`
   hangs its drifting bloom off it, and anything else painting at z-index -1 needs
   a positioned, isolated parent or it falls through to the page ground. */
.plane { position: relative; isolation: isolate; }

/* A section whose ground is the commissioned art rather than a flat colour. The
   scrim is a separate layer so the art can be swapped without retuning contrast,
   and the type sits above both. */
.imgband { position: relative; isolation: isolate; overflow: hidden; }
.imgband > .wrap { position: relative; z-index: 2; }
.imgband::before,
.imgband::after { content: ''; position: absolute; inset: 0; pointer-events: none; }
.imgband::before {
  z-index: 0;
  background-image: var(--band-img);
  background-size: cover;
  background-position: center 42%;
  /* the art is wide and abstract; a slow drift keeps the band from reading as a
     flat placed photograph without pulling focus */
  animation: band-drift 34s ease-in-out infinite alternate;
}
.imgband::after {
  z-index: 1;
  background:
    linear-gradient(to right, var(--band-scrim-a) 0%, var(--band-scrim-b) 62%, var(--band-scrim-c) 100%);
}
@keyframes band-drift {
  from { transform: scale(1.04) translate3d(0, 0, 0); }
  to   { transform: scale(1.12) translate3d(-1.5%, -1%, 0); }
}

/* The art itself. Declared here rather than inline in the markup: a relative
   url() inside a custom property resolves against the stylesheet that declares
   it, so setting --band-img from an HTML style attribute fetched
   css/assets/... and silently 404'd. Declared in this file, `../assets/` is
   correct and unambiguous. */
.imgband--region  { --band-img: url(../assets/hero-01-region.webp); }
.imgband--hub     { --band-img: url(../assets/hero-02-hub.webp); }
.imgband--journey { --band-img: url(../assets/hero-03-journey.webp); }

/* The band set in assets/imagery/, generated by tools/make-imagery.py. Unlike
   the three above - which are the hero's own art, reused - these are drawn for
   this slot: the structure sits in the middle 70% of the frame, because a band
   is shorter than 16:9 and cover-fitting crops the top and bottom away.

   Pair a `band-*` with `.imgband--dark` and a `wash-*` with `.imgband--light`;
   the scrim is what keeps the type legible and the two are not interchangeable.
   See imagery.html for what each one looks like under its scrim. */
.imgband--lattice { --band-img: url(../assets/imagery/band-lattice.webp); }
.imgband--rings   { --band-img: url(../assets/imagery/band-rings.webp); }
.imgband--strata  { --band-img: url(../assets/imagery/band-strata.webp); }
.imgband--flow    { --band-img: url(../assets/imagery/band-flow.webp); }
.imgband--wash-lattice { --band-img: url(../assets/imagery/wash-lattice.webp); }
.imgband--wash-rings   { --band-img: url(../assets/imagery/wash-rings.webp); }

/* Supplied artwork, not generated: a faceted navy plane, dark enough to carry
   white type on its own. It replaces the news band's lattice - and the hex mesh
   that used to sit over it - so the section has one ground instead of three
   layers of texture. Pair it with `.imgband--dark`; the facets are large and
   soft, so they survive being cover-cropped to a band. */
.imgband--pattern { --band-img: url(../assets/pattern.png); }

/* A light section flipped to the dark ground so it can carry band art. The
   sections that were born dark (.kpi, .news) set their own colours; this is for
   the ones that were white and are now a band, where every inherited ink colour
   has to be restated or the type goes near-black on near-black. Cards keep their
   white surface deliberately: white cards on a dark band is the point. */
.section--dark {
  background: var(--blue-900);
  color: #fff;
}
/* Only the section's own heading flips. A card on a dark band keeps its white
   surface, so its h3 has to stay ink - a blanket `.section--dark h3` turned
   every card title white on white. */
.section--dark .section__head h2 { color: #fff; }
.section--dark .section__head p:not(.eyebrow) { color: rgba(255, 255, 255, 0.74); }
.section--dark .eyebrow { color: var(--gold-300); }
/* A card is a white plate sitting on the band, so everything inside it goes back
   to the page's ink. Without this the card titles inherit the section's white and
   vanish - the cards set their own background but never their own colour. */
.section--dark .card { color: var(--ink); }
/* the wide head's giant numeral is near-black at 0.045 opacity - invisible on
   this section's dark blue ground, so a dark section needs the light version */
.section--dark .section__head--wide::before { color: #fff; opacity: 0.06; }

/* Dark band: the art carries it, the scrim only protects the type. */
.imgband--dark {
  --band-scrim-a: rgba(20, 30, 38, 0.94);
  --band-scrim-b: rgba(20, 30, 38, 0.72);
  --band-scrim-c: rgba(20, 30, 38, 0.42);
}

/* A band where the art should be seen, not just felt. The default scrim is tuned
   for a headline plus a paragraph running across the band; where the type is a
   short head in one column - the indicators, the news rail - it hides the art
   almost completely. Contrast still holds: white type sits over the .86 end. */
.imgband--dark.imgband--soft {
  --band-scrim-a: rgba(20, 30, 38, 0.86);
  --band-scrim-b: rgba(20, 30, 38, 0.56);
  --band-scrim-c: rgba(20, 30, 38, 0.28);
}

/* Light band: the same structure in white, for a section that keeps the page's
   own ink. Without one of these two classes `--band-scrim-*` is undeclared,
   the gradient is invalid, and the band ships with no scrim at all - which is
   how a headline ends up sitting on bare artwork. */
.imgband--light {
  --band-scrim-a: rgba(255, 255, 255, 0.95);
  --band-scrim-b: rgba(255, 255, 255, 0.78);
  --band-scrim-c: rgba(255, 255, 255, 0.50);
}

@media (prefers-reduced-motion: reduce) {
  .imgband::before { animation: none; transform: scale(1.04); }
}

/* ─────────────────── mandate and recognition ───────────────────
   The Trust & Authority pattern puts proof at position two: logos, certs,
   stats, directly under the hero. The page had the stats and none of the proof,
   which is why it read thin - for an intergovernmental body the first question
   is by what authority, not what services.

   The marks are typographic stand-ins drawn in our own geometry. We do not
   approximate another body's logo; official assets are an open item. */
.proof { background: var(--surface); }

.proof__grid {
  display: grid;
  gap: 1px;
  background: var(--line);
  grid-template-columns: repeat(2, 1fr);
  margin-top: clamp(1.75rem, 3.5vh, 2.5rem);
}
@media (min-width: 720px)  { .proof__grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1040px) { .proof__grid { grid-template-columns: repeat(5, 1fr); } }

.proof__item {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.55rem;
  padding: 1.6rem 1rem 1.5rem;
  text-align: center;
  background: var(--surface);
  /* 220ms: a plate lifting is a micro-interaction, so it belongs in the
     150-300ms envelope rather than the slower content transitions */
  transition: background 0.22s var(--ease-out);
}
.proof__item::after {
  content: '';
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  height: 2px;
  background: var(--gold-500);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.28s var(--ease-out);
}
.proof__item:hover { background: var(--surface-2); }
.proof__item:hover::after { transform: scaleX(1); }

.proof__mark {
  display: grid;
  place-items: center;
  width: 2.9rem;
  height: 2.9rem;
  color: var(--blue-600);
  transition: color 0.22s var(--ease-out);
}
.proof__mark svg { width: 100%; height: 100%; }
.proof__item:hover .proof__mark { color: var(--blue-700); }
/* the one warm mark per plate, so the strip has a rhythm of accents */
.proof__accent { stroke: var(--gold-500); }

.proof__name {
  font-size: 0.92rem;
  font-weight: 600;
  line-height: 1.25;
  color: var(--ink);
}
.proof__role {
  font-size: 0.76rem;
  letter-spacing: 0.04em;
  color: var(--ink-3);
}


@media (prefers-reduced-motion: reduce) {
  .proof__item, .proof__item::after, .proof__mark { transition: none; }
  .proof__item::after { transform: scaleX(1); opacity: 0; }
  .proof__item:hover::after { opacity: 1; }
}

/* structure.html still carries the old .proof__grid markup above, so that CSS
   stays in place. #mandate itself is the chain of authority, and it is built as
   a single dark plate - see the note on the markup in index.html for why dark,
   why GOIC inverts to a light card inside it, and why the three bodies are
   links rather than inert tiles.

   Two custom properties do the work. --mpad is the plate's own inset, and the
   two tiers that have to reach the plate's edges cancel it with a negative
   margin rather than carrying their own numbers, so the bracket above the
   actors and the actor columns it points at always measure the same width.
   --mw-line / --mw-live are the wire's two states, so lighting a run of wire is
   one declaration wherever the trigger happens to be. */
.mandate__chain {
  --mpad: clamp(1.25rem, 3vw, 2.5rem);
  --mw-line: rgba(255, 255, 255, 0.17);
  --mw-live: var(--gold-500);
  position: relative;
  margin-top: clamp(1.4rem, 3vh, 2rem);
  padding: clamp(1.5rem, 3.5vw, 2.4rem) var(--mpad) 0;
  border-radius: 22px;
  /* clips the actor row into the plate's bottom corners, so the row reads as
     part of the plate rather than as three cards resting under it */
  overflow: hidden;
  color: #fff;
  background:
    radial-gradient(115% 85% at 50% 34%, rgba(218, 165, 32, 0.17), transparent 62%),
    linear-gradient(165deg, #094A75, var(--blue-900) 58%, #141E26);
  box-shadow: 0 40px 90px -46px rgba(20, 30, 38, 0.75);
}
/* the seal line across the top, drawn from the center out as the plate arrives */
.mandate__chain::before {
  content: '';
  position: absolute;
  inset-inline: 0;
  top: 0;
  height: 2px;
  background: linear-gradient(90deg, transparent, var(--gold-600) 18%,
              var(--gold-300) 50%, var(--gold-600) 82%, transparent);
  transform-origin: center;
  transform: scaleX(0);
  transition: transform 1.1s var(--ease-out);
}
.mandate__chain.is-in::before { transform: scaleX(1); }

/* ── tier 1: the lintel ──
   A full-width card rather than a rule-and-label: the mandate is issued from
   above and everything below hangs off it, and as a card it matches the three
   bodies at the base, so the plate reads as one family of objects with the
   lockup inverted between them. Three lines and nothing beside them - authority,
   who, and what they provide - which is the order the reader needs them in. A
   mark stood here and was dropped: the card is the whole width of the plate and
   a glyph at one end of it was decoration holding the type off its own edge. */
.mandate__lintel {
  display: flex;
  align-items: center;
  gap: clamp(0.9rem, 2vw, 1.4rem);
  padding: clamp(1.1rem, 2.4vw, 1.6rem) clamp(1.2rem, 2.6vw, 1.75rem);
  border: 1px solid var(--mw-line);
  border-radius: 16px;
  background: rgba(255, 255, 255, 0.045);
}
.mandate__id { flex: 1 1 auto; min-width: 0; }

/* ── the wire from the lintel down to GOIC ──
   Draws down off --p like every other line on the page, then carries one slow
   pulse so the diagram has a pulse of its own rather than only reacting to the
   cursor. Clipped, so the pulse appears out of the join and vanishes into it. */
.mandate__wire--down {
  position: relative;
  width: 1px;
  height: clamp(1.5rem, 3.6vh, 2.4rem);
  margin-inline: auto;
  overflow: hidden;
  background: var(--mw-line);
  transform-origin: top;
  scale: 1 var(--p, 1);
  transition: background 0.3s var(--ease-out);
}
.mandate__pulse {
  position: absolute;
  inset-inline: 0;
  top: 0;
  height: 100%;
  background: linear-gradient(to bottom, transparent, var(--gold-300) 48%, transparent);
  translate: 0 -100%;
}
/* only once the plate has been seen - an animation running against an element
   nobody has scrolled to yet is work for nothing */
.mandate__chain.is-in .mandate__pulse {
  animation: mandate-pulse 3.6s cubic-bezier(0.55, 0, 0.45, 1) infinite;
}
@keyframes mandate-pulse {
  0%   { translate: 0 -100%; opacity: 0; }
  10%  { opacity: 1; }
  48%  { translate: 0 100%; opacity: 0; }
  100% { translate: 0 100%; opacity: 0; }
}

/* ── tier 2: the Center, inverted ──
   The one light object on a dark plate. Inversion is the strongest emphasis
   available here, and the plate's whole claim is about what sits between the
   mandate above and the indexes and states below, so that is the only thing
   lit. It carries the lockup and no type of its own - the artwork already sets
   the name in both languages, and a label beside it would set it twice. */
.mandate__hub {
  position: relative;
  /* Half the plate, not shrink-to-fit. At max-content the card was 318px inside
     a 1064px plate and the diagram read hollow through its middle; at half the
     plate it is wide enough to be the middle, and the two tiers it sits between
     both run the full width, so it lines up with them rather than floating. */
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(1rem, 2.4vw, 1.6rem);
  width: min(100%, 34rem);
  margin-inline: auto;
  padding: clamp(1.15rem, 2.6vw, 1.75rem) clamp(1.4rem, 3vw, 2.1rem);
  border-radius: 16px;
  color: var(--ink);
  background: var(--surface);
  box-shadow: 0 0 0 1px rgba(218, 165, 32, 0.6),
              0 0 0 7px rgba(218, 165, 32, 0.09),
              0 26px 54px -24px rgba(0, 0, 0, 0.6);
}

/* ── the split from GOIC down to the three bodies ──
   Five plain elements rather than an SVG: each run of wire is then a box that
   scales off --p exactly like the wire above it, and each drop is addressable,
   so hovering one body can light its own line back to GOIC. The percentages are
   the three column centers - the split cancels the plate's padding with the
   same negative margin the actor row uses, so both span the same width and the
   drops land on the columns at every viewport. */
.mandate__split {
  position: relative;
  height: clamp(1.8rem, 4.2vh, 2.75rem);
  margin-inline: calc(var(--mpad) * -1);
}
.mandate__split > span {
  position: absolute;
  background: var(--mw-line);
  transition: background 0.3s var(--ease-out);
}
.mandate__stem {
  left: 50%;
  top: 0;
  width: 1px;
  height: 42%;
  transform-origin: top;
  scale: 1 var(--p, 1);
}
.mandate__beam {
  top: 42%;
  left: 16.667%;
  right: 16.667%;
  height: 1px;
  transform-origin: center;
  scale: var(--p, 1) 1;
}
.mandate__drop {
  top: 42%;
  bottom: 0;
  width: 1px;
  transform-origin: top;
  scale: 1 var(--p, 1);
}
.mandate__drop[data-drop='1'] { left: 16.667%; }
.mandate__drop[data-drop='2'] { left: 50%; }
.mandate__drop[data-drop='3'] { left: 83.333%; }

/* Touch a body and the authority path to it lights: the wire from the
   Secretariat, through GOIC, down that one drop. This is the whole point of
   drawing the chain as a circuit instead of a stack of labels. */
.mandate__chain:has(.mandate__actor:is(:hover, :focus-visible))
  :is(.mandate__wire--down, .mandate__stem, .mandate__beam) { background: var(--mw-live); }
.mandate__chain:has(.mandate__actors > li:nth-child(1) .mandate__actor:is(:hover, :focus-visible))
  .mandate__drop[data-drop='1'],
.mandate__chain:has(.mandate__actors > li:nth-child(2) .mandate__actor:is(:hover, :focus-visible))
  .mandate__drop[data-drop='2'],
.mandate__chain:has(.mandate__actors > li:nth-child(3) .mandate__actor:is(:hover, :focus-visible))
  .mandate__drop[data-drop='3'] { background: var(--mw-live); }
/* GOIC is what joins them, so hovering GOIC lights every run at once */
.mandate__chain:has(.mandate__hub:hover) .mandate__split > span,
.mandate__chain:has(.mandate__hub:hover) .mandate__wire--down { background: var(--mw-live); }

/* ── tier 3: the three bodies ──
   Hairline-separated cells flush to the plate's edges and bottom, so they read
   as the base of one object. Links, not tiles: each goes to the section that
   holds what it names, which is what earns the hover state. */
.mandate__actors {
  list-style: none;
  margin: 0 calc(var(--mpad) * -1) 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  /* Four shared rows: label, flags, rule and what the card offers, then the way
     in. The row track is declared here rather than inside each card so all three
     cards measure against the same tracks - the rules land on one line and the
     three links land on one line however many lines the labels above them take.
     Row 3 takes the slack, so the air collects in one band above the links
     rather than being split either side of the rule.

     Row gap is zero and only the columns keep the 1px gutter: the container's
     line colour shows through gaps, so a row gap here would draw a hairline
     across every card at every row boundary. The cards' own vertical rhythm is
     padding on the rows instead. */
  grid-template-rows: auto auto 1fr auto;
  row-gap: 0;
  column-gap: 1px;
  background: var(--mw-line);
  border-top: 1px solid var(--mw-line);
}
/* Both the li and the card pass the tracks through, because the card is the
   thing with the padding and the background and the li is only a list item.
   Scoped to the markup that has a .mandate__head, so sections.html's older
   version of this row keeps the flex layout it was built for. */
.mandate__actors > li:has(.mandate__head),
.mandate__actor:has(.mandate__head) {
  display: grid;
  grid-row: span 4;
  grid-template-rows: subgrid;
}
.mandate__actor {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: clamp(0.85rem, 1.8vw, 1.15rem);
  height: 100%;
  padding: clamp(1.3rem, 2.6vw, 1.85rem) clamp(1.1rem, 2.2vw, 1.5rem)
           clamp(1.4rem, 2.8vw, 1.9rem);
  text-align: start;
  text-decoration: none;
  color: inherit;
  background: rgba(255, 255, 255, 0.035);
  transition: background 0.3s var(--ease-out);
}
/* The card's vertical padding lives on the first and last row rather than on the
   card, because block-axis padding on a subgrid squeezes its own tracks out of
   step with the tracks it is meant to be aligned to. */
.mandate__actor:has(.mandate__head) { padding-block: 0; }

/* The mark and the label on one line. Aligned to the top of the mark rather
   than its centre, because the third card's label runs to two lines and a
   centred mark would then sit low against a name that starts at the top. */
.mandate__head {
  grid-row: 1;
  display: flex;
  align-items: flex-start;
  gap: clamp(0.7rem, 1.5vw, 1rem);
  padding-top: clamp(1.3rem, 2.6vw, 1.85rem);
}
.mandate__actor .mandate__flags { grid-row: 2; padding-top: clamp(0.9rem, 1.9vw, 1.2rem); }
.mandate__actor .mandate__go { grid-row: 4; padding-bottom: clamp(1.4rem, 2.8vw, 1.9rem); }
.mandate__actor::after {
  content: '';
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  height: 2px;
  background: var(--gold-500);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.3s var(--ease-out);
}
.mandate__actor:is(:hover, :focus-visible) { background: rgba(255, 255, 255, 0.1); }
.mandate__actor:is(:hover, :focus-visible)::after { transform: scaleX(1); }
.mandate__actor:focus-visible { outline: 2px solid var(--gold-300); outline-offset: -4px; }

/* ── marks, flags and type ── */
.mandate__mark {
  display: grid;
  place-items: center;
  width: 2.6rem;
  height: 2.6rem;
  color: rgba(255, 255, 255, 0.78);
  transition: color 0.3s var(--ease-out);
}
.mandate__mark svg { width: 100%; height: 100%; }
/* A ring around the mark on the three bodies, and gold rather than white-ish.
   The ring is what makes three different glyphs read as three of the same kind
   of thing, and it holds the mark's size steady when the label beside it wraps
   to two lines. Inset ring, not a border: the box then stays exactly the size
   declared here whatever the ring's weight. */
.mandate__actor .mandate__mark {
  flex: 0 0 auto;
  width: clamp(2.7rem, 5vw, 3.2rem);
  height: clamp(2.7rem, 5vw, 3.2rem);
  padding: 0.66rem;
  border-radius: 50%;
  color: var(--gold-500);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--c-gold) 55%, transparent);
  transition: color 0.3s var(--ease-out), box-shadow 0.3s var(--ease-out);
}
.mandate__actor:is(:hover, :focus-visible) .mandate__mark {
  box-shadow: inset 0 0 0 1px var(--gold-500);
}
.mandate__mark--hub { flex: 0 0 auto; width: 4.4rem; height: 4.4rem; color: var(--blue-700); }
/* A wide lockup in a slot drawn for a square glyph: height leads and the width
   follows the artwork, with the ratio declared so the card does not reflow when
   the SVG finishes decoding. Sized against the card rather than against the
   marks below it: this is the only thing in the card, and at the height a
   line-art glyph wants it left the light plane reading as an empty box with a
   small mark in it. Roughly three fifths of the card's width, which leaves the
   inversion looking deliberate and still keeps the artwork off the edges. */
.mandate__mark--logo {
  width: auto;
  height: clamp(4.4rem, 8.6vw, 6.6rem);
  aspect-ratio: 1014.28 / 332.65;
}
.mandate__mark--logo img { width: 100%; height: 100%; }
.mandate__hub-id { min-width: 0; }
/* Brighter gold, not white: the ring around it is gold, and a white glyph in a
   gold ring reads as two decisions rather than one thing lighting up. */
.mandate__actor:is(:hover, :focus-visible) .mandate__mark { color: var(--gold-300); }
.mandate__accent { stroke: var(--gold-300); }
.mandate__hub .mandate__accent { stroke: var(--gold-600); }

/* The six dots turn slowly - a hub that is a hub. 40s, which is slow enough
   that it registers as presence rather than as something to watch. */
.mandate__chain.is-in .mandate__orbit {
  transform-origin: 22px 22px;
  animation: mandate-orbit 40s linear infinite;
}
@keyframes mandate-orbit { to { transform: rotate(360deg); } }

/* The six real flags, not an abstract dotted ring: the states are the one thing
   in this diagram the reader can recognise on sight. They lift in sequence on
   hover, --i coming from js/sections.js. */
.mandate__flags {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  /* the filled row's own height, reserved: js/sections.js fills this after the
     card has laid out, and without it the rule below the row would jump */
  min-height: 22px;
}
.mandate__flag { display: block; }
.mandate__flag .flag {
  display: block;
  /* an explicit height, not `auto`: Qatar's artwork is 3:2 where the other
     five are 5:3, and left to its intrinsic ratio it would stand taller than
     the row it is in. Six of these plus their gaps fill the card's measure,
     which is what makes the row read as the six states rather than as a strip
     of icons. */
  width: 36px;
  height: 21.6px;
  object-fit: cover;
  border-radius: 2px;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.3);
  transition: translate 0.35s var(--ease-out);
  transition-delay: calc(var(--i, 0) * 35ms);
}
.mandate__actor:is(:hover, :focus-visible) .mandate__flag .flag { translate: 0 -4px; }

.mandate__eyebrow {
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: 0.19em;
  text-transform: uppercase;
  color: var(--gold-300);
}
.mandate__eyebrow--hub { color: var(--gold-700); }
.mandate__name {
  display: block;
  font-size: clamp(1.02rem, 1.5vw, 1.2rem);
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: -0.01em;
  color: #fff;
}
.mandate__name b { font-weight: 700; color: var(--gold-300); }
/* The name at size, its expansion under it in gold. Two lines, one label: the
   acronym is what the reader recognises and the expansion is what it means, so
   the expansion is quieter and warm rather than a second white line competing
   with the first. */
.mandate__sub {
  display: block;
  margin-top: 0.2rem;
  font-size: 0.83rem;
  line-height: 1.35;
  color: var(--gold-300);
}
.mandate__lintel .mandate__name {
  font-size: clamp(1.25rem, 2.1vw, 1.65rem);
  letter-spacing: -0.02em;
}
.mandate__name--hub {
  font-size: clamp(1.9rem, 4.4vw, 2.7rem);
  line-height: 1;
  letter-spacing: -0.035em;
  color: var(--ink);
}
.mandate__role {
  font-size: 0.78rem;
  letter-spacing: 0.03em;
  color: rgba(255, 255, 255, 0.62);
}
.mandate__lintel .mandate__role { margin-top: 0.3rem; font-size: 0.85rem; }
/* A rule above it, not a rule element: the line separates the label from what
   the card is actually offering, and as a border it cannot drift out of step
   with the text it belongs to. */
.mandate__actor .mandate__role {
  grid-row: 3;
  margin-top: clamp(0.9rem, 1.9vw, 1.2rem);
  padding-top: clamp(0.8rem, 1.7vw, 1.05rem);
  border-top: 1px solid var(--mw-line);
  font-size: 0.84rem;
  line-height: 1.45;
  letter-spacing: 0.01em;
  color: rgba(255, 255, 255, 0.72);
}
.mandate__hub .mandate__role { margin-top: 0.4rem; color: var(--ink-2); }

/* The payload line. Visible at rest rather than revealed on hover - a tile that
   only says what it is when touched is the boring version. */
.mandate__go {
  display: block;
  /* pushed to the bottom of the card, so the three links sit on one line across
     the row however many lines the labels above them take */
  margin-top: auto;
  padding-top: 0.2rem;
  font-size: 0.85rem;
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: 0.01em;
  color: var(--gold-300);
  opacity: 0.9;
  transition: opacity 0.3s var(--ease-out);
}
.mandate__go i {
  font-style: normal;
  display: inline-block;
  margin-inline-start: 0.3rem;
  transition: translate 0.3s var(--ease-out);
}
.mandate__actor:is(:hover, :focus-visible) .mandate__go { opacity: 1; }
.mandate__actor:is(:hover, :focus-visible) .mandate__go i { translate: 3px 0; }

/* One column below 720px, and the bracket collapses to the single wire it
   should be when there is nothing to fan out to. */
@media (max-width: 720px) {
  .mandate__actors {
    grid-template-columns: 1fr;
    grid-template-rows: none;
    row-gap: 1px;
  }
  /* Stacked, there is nothing to align across, and the shared tracks would put
     card one's slack row in the middle of the stack. Back to flex, where the
     paddings already on the rows still give the same rhythm. */
  .mandate__actors > li:has(.mandate__head) { display: block; grid-row: auto; }
  .mandate__actor:has(.mandate__head) {
    display: flex;
    flex-direction: column;
    grid-row: auto;
    grid-template-rows: none;
  }
  .mandate__beam, .mandate__drop { display: none; }
  .mandate__stem { height: 100%; }
}
@media (max-width: 600px) {
  .mandate__hub { flex-direction: column; text-align: center; }
}

@media (prefers-reduced-motion: reduce) {
  .mandate__chain::before { transition: none; }
  .mandate__chain.is-in .mandate__pulse { animation: none; opacity: 0; }
  .mandate__chain.is-in .mandate__orbit { animation: none; }
  .mandate__wire--down, .mandate__split > span { scale: 1 1; }
  .mandate__beam { scale: 1 1; }
  .mandate__actor, .mandate__actor::after, .mandate__mark,
  .mandate__actor .mandate__mark,
  .mandate__flag .flag, .mandate__go, .mandate__go i { transition: none; }
  .mandate__actor::after { transform: scaleX(1); opacity: 0; }
  .mandate__actor:is(:hover, :focus-visible)::after { opacity: 1; }
  .mandate__actor:is(:hover, :focus-visible) .mandate__flag .flag { translate: 0 0; }
}

/* The Knowledge Hub carried a second "Reports & publications" column that
   duplicated the publications strip. With it gone the use cases take the full
   width, which is what they needed anyway - the filter row and six rows were
   cramped into half a column. */
.kh__cases { display: grid; gap: 1.25rem; }
.kh__cases .kh__filters { margin-bottom: 1.25rem; }
@media (min-width: 900px) {
  /* two columns of use cases rather than one long list, so the section does not
     become a 6-row scroll on desktop */
  .kh__cases > div { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0 2rem; }
  .kh__cases .kh__filters { grid-column: 1 / -1; }
}

/* ═══════════════ visual craft pass ═══════════════
   Stage one of the design pass. The sections were correct and legible and read
   as a wireframe: one type size, one ground, no artwork, everything centered in
   a 62ch column. Three changes, in order of how much they do:

     1. artwork on the cards that had none
     2. a real type-scale jump at the section head, with the section's own
        numeral behind it
     3. asymmetry - the head runs wide, the lede sits offset instead of stacked */

/* ── card artwork ──
   A band across the top of the card, bled to the card's edges and cropped, so it
   reads as artwork rather than as a centered icon. */
.card__art,
.card__art--wide {
  display: block;
  margin: -1.4rem -1.4rem 1.1rem;
  padding: 1.1rem 1.4rem 0;
  color: var(--blue-600);
  background: linear-gradient(to bottom, var(--surface-2), transparent);
  overflow: hidden;
}
.card__art svg {
  display: block;
  width: 100%;
  max-width: 15rem;
  height: auto;
  /* crops the drawing's lower edge into the card so it does not float */
  margin-bottom: -6%;
}
.card__art--wide svg { max-width: 12rem; }
.card__art .pa-fill { fill: var(--gold-500); }
.card__art .pa-stroke-accent { stroke: var(--gold-500); }

/* ── card imagery ──
   A raster from assets/imagery/ at the top of a card, bled to the card's real
   padding so it reads as the card's own header rather than as an inset picture.
   This and .card__art are alternatives, not layers: a card carries the line
   drawing or the raster, never both, or the reader is told the same thing twice.

   The bleed is written from .card's padding (1.75rem 1.6rem), so if that
   changes these two numbers change with it. */
.card__photo {
  display: block;
  width: calc(100% + 3.2rem);
  /* the global `img { max-width: 100% }` would clamp the bleed straight back to
     the content box, leaving the picture short of the card's right edge */
  max-width: none;
  margin-inline: -1.6rem;
  margin-block: 0 1.2rem;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  /* the files are 4:3 photographs and people sit in the upper half of them, so
     any crop is spent at the bottom. Centered, the 16:6 this started as cut the
     services presenter off at the eyebrows. */
  object-position: center 32%;
  border-block: 1px solid var(--line-soft);
}
/* at the very top of the card, where it becomes the card's header: pull it into
   the padding and drop the hairline it no longer needs */
.card__photo--top { margin-top: -1.75rem; border-top: 0; }
/* shorter crop, for a card whose text is the point and the picture is a note.
   16:9 and not the letterbox this was: at 16:6 only half the frame survives,
   which is fine for a diagram drawn to the middle of its canvas and wrong for a
   photograph of a room full of people. */
.card__photo--strip { aspect-ratio: 16 / 9; }

/* the drawing leans in with the card, so the two move as one object */
.svc__grid .card,
.fw__item { transition: transform 0.26s var(--ease-out), border-color 0.26s var(--ease-out); }
.svc__grid .card .card__art svg,
.fw__item .card__art svg { transition: transform 0.34s var(--ease-out); }
.svc__grid .card:hover .card__art svg,
.fw__item:hover .card__art svg { transform: scale(1.045) translateY(-2%); }

/* a placed raster leans in the same way, so a card with a photograph and a card
   with a drawing behave identically under the pointer */
.card__photo { transition: transform 0.45s var(--ease-out); }
.card:hover .card__photo { transform: scale(1.05); }

/* ── section head: the type jump and the numeral ── */
.section__head--wide {
  position: relative;
  max-width: none;
  display: grid;
  gap: 0.4rem 3.5rem;
  align-items: end;
}
@media (min-width: 900px) {
  /* asymmetric: the heading takes the wide column, the lede sits beside its
     baseline rather than stacked under it */
  .section__head--wide { grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); }
  .section__head--wide .eyebrow { grid-column: 1; }
  .section__head--wide h2 { grid-column: 1; grid-row: 2; }
  .section__head--wide > p:not(.eyebrow) { grid-column: 2; grid-row: 2; margin-bottom: 0.5rem; }
}
/* The centered arrangement. The wide head's default is asymmetric - heading in
   a 1.35fr column with the lede beside its baseline - and this stacks it back
   into one centered column without giving up the type scale, which is the only
   part of --wide the client wanted kept.

   The two max-widths are the whole point of it and are set in ch rather than
   px: a centered heading that runs the full measure is not centered, it is just
   full width with the last line short. At 24ch the h2 breaks into three lines
   of roughly even length and the lede sits under it as a narrower block, which
   is the shape a centered head has to make to read as one. */
/* display and margin-inline are the modifier's own rather than inherited from
   --wide, so --center also works on a plain .section__head. template-about.php
   uses it that way. */
.section__head--center {
  display: grid;
  justify-items: center;
  text-align: center;
  margin-inline: auto;
}
.section__head--center h2 { max-width: 24ch; }
.section__head--center > p:not(.eyebrow) { max-width: 46ch; }
@media (min-width: 900px) {
  .section__head--center { grid-template-columns: minmax(0, 1fr); }
  .section__head--center .eyebrow { grid-column: 1; grid-row: 1; }
  .section__head--center h2 { grid-column: 1; grid-row: 2; }
  .section__head--center > p:not(.eyebrow) {
    grid-column: 1;
    grid-row: 3;
    margin-block: 0.9rem 0;
  }
}

.section__head--wide h2 {
  /* a real jump from the old clamp(1.9rem, 3.6vw, 3rem) - at desktop this is
     roughly half again as large, which is what makes the page feel authored
     rather than defaulted */
  font-size: clamp(2.1rem, 5vw, 4.1rem);
  line-height: 1.02;
  letter-spacing: -0.035em;
  margin-block: 0.5rem 0;
}

/* The section's own ghost numeral was removed 2026-08-29 on the client's
   instruction: the sections are not presented as a numbered sequence, so the
   large figure behind each head was labelling an order the page does not have. */

@media (prefers-reduced-motion: reduce) {
  .svc__grid .card, .fw__item,
  .svc__grid .card .card__art svg, .fw__item .card__art svg { transition: none; }
  .card__photo, .news__photo { transition: none; }
  .card:hover .card__photo { transform: none; }
}

/* ═══════════════════ scroll-driven section motion ═══════════════════
   `js/scrub.js` writes `--p` (0 to 1) to every `[data-scrub]` element on every
   frame the page has moved, derived from where the element sits in the viewport.
   These rules are the only thing that decides what that means.

   Deliberately no `transition`. A transition here would put an animation between
   the scroll position and the pixels, which is the thing being replaced - the
   point is that the reader's scroll *is* the timeline, so half a scroll is half
   the motion, and scrolling back up runs it backwards. Anything with a duration
   would lag the wheel and overshoot on release.

   `translate` and not `transform`, so an element that also has a hover or
   entrance `transform` is not fighting this for the same property. Same split the
   hero uses between its parallax and entrance layers. */
[data-scrub] { --p: 0; }

.scrub {
  opacity: var(--p);
  translate: 0 calc((1 - var(--p)) * 30px);
}
.scrub--far   { translate: 0 calc((1 - var(--p)) * 56px); }
/* --left and --right name the direction of travel, not the side of the page, and
   under RTL they would still slide the same way while the layout mirrors around
   them. css/rtl.css section 9 flips the sign so an element keeps entering from
   its own outer edge; the two must move together. */
.scrub--left  { translate: calc((1 - var(--p)) * -30px) 0; }
.scrub--right { translate: calc((1 - var(--p)) * 30px) 0; }

/* Stacked, the horizontal pair goes back to the vertical entrance.

   WHERE IT CAME FROM: .scrub--right at rest sits 30px to the right of a viewport
   whose gutter is var(--pad), which is 20.7px at 414px. That 9px is the
   horizontal scrollbar the whole site has had on a phone: every page carries the
   footer, so every page had it, and it read as a page bug rather than as an
   animation resting position. Overflow from an offset like this cannot be fixed
   by clipping it without hiding a real one somewhere else later.

   WHY IT IS SAFE FOR EVERY USER, NOT JUST THE ONE THAT BROKE. Two files use
   these modifiers: partials/footer.php (the side columns) and front-page.php
   (five .route__node and four .eco__item). The reason is NOT that .footer__top
   collapses to a single column below 860px - that is true, and it is only about
   the footer. It is that the switch below is unconditional: both modifiers are
   restated on the Y axis for every element carrying them, whatever its layout,
   so no user of either one has a horizontal offset left at these widths.

   Which also means the horizontal case only exists from 860px UP. Measure it at
   861, not on a phone, and measure it before the reveal - these sit at --p=0 on
   load, which is where the offset lives. Note too that .scrub--left travels
   -30px, and a left overflow is clipped without ever raising scrollWidth, so a
   scrollWidth check cannot see it: walk the rects for left < 0 as well. */
@media (max-width: 859px) {
  .scrub--left, .scrub--right { translate: 0 calc((1 - var(--p)) * 30px); }
}

/* The card's artwork travels further and in the opposite direction, so the card
   has depth of its own as it arrives rather than moving as one flat plate. `--p`
   inherits from the card, so the artwork needs no scrub entry of its own. */
.pub .pub__draw { translate: 0 calc((1 - var(--p)) * -26px); }
/* Downward, where the drawing goes up: the photograph is anchored to the card's
   bottom edge, so lifting it would open a strip of bare ground under it. Pushed
   down it is clipped by the card instead, and rises into place as the card
   arrives. `translate` and not `transform` - the hover exchange owns `transform`
   on this element, and the two properties compose. */
.pub .pub__photo { translate: 0 calc((1 - var(--p)) * 18px); }

/* A section head's lede trails its heading slightly. */
.section__head.scrub > p:last-child { translate: 0 calc((1 - var(--p)) * 14px); }

/* ── the things that draw rather than arrive ──────────────────────────────
   Each of these was a one-shot: `initReveals` added `.is-in` and a transition
   with a duration ran it. Restated against `--p` they are the same drawings,
   except the reader's scroll is the timeline. The rules below all sit after
   their `.is-in` counterparts and match on the same weight, so the scrubbed
   value is what lands; the `.is-in` rules stay for any page that carries the
   markup without the module. `transition: none` on every one - see above. */

/* the rule under a band head draws itself across */
.rule[data-scrub] { transform: scaleX(var(--p)); transition: none; }

/* the frameworks' center divider draws down while the two panels arrive */
@media (min-width: 760px) {
  .fw__grid[data-scrub]::before { transform: scaleY(var(--p)); transition: none; }
}

/* each framework card's own accent rule draws in alongside it */
.fw__item[data-scrub]::before { transform: scaleX(var(--p)); transition: none; }

/* a use case's maturity-gain bar fills to its own value */
.usecase[data-scrub] .usecase__bar i {
  transform: scaleX(calc(var(--v, 0.5) * var(--p)));
  transition: none;
}

/* the route's spine draws down through all three tiers - unconditional, since
   the layout has no breakpoint where the tiers stop stacking vertically */
.route[data-scrub]::before { transform: scaleY(var(--p)); transition: none; }

/* the mandate plate's seal line draws out from the center as the plate arrives */
.mandate__chain[data-scrub]::before { transform: scaleX(var(--p)); transition: none; }

/* the about scoreboard's dividers only exist as verticals from 641px - below
   that they're the plain static border set above, nothing to scrub */
@media (min-width: 641px) {
  .kpi__stat:not(:first-child)[data-scrub]::before { transform: scaleY(var(--p)); transition: none; }
}

@media (prefers-reduced-motion: reduce) {
  /* js/scrub.js parks --p at 1 and never starts its loop, so these resolve to the
     resting state; belt and braces in case the module never runs at all. */
  .scrub, .scrub--far, .scrub--left, .scrub--right, .pub .pub__draw,
  .pub .pub__photo,
  .section__head.scrub > p:last-child { opacity: 1; translate: 0 0; }
  .rule[data-scrub] { transform: scaleX(1); }
  .fw__grid[data-scrub]::before { transform: scaleY(1); }
  .fw__item[data-scrub]::before { transform: scaleX(1); }
  .route[data-scrub]::before { transform: scaleY(1); }
  .mandate__chain[data-scrub]::before { transform: scaleX(1); }
  .usecase[data-scrub] .usecase__bar i { transform: scaleX(var(--v, 0.5)); }
}
@media (prefers-reduced-motion: reduce) and (min-width: 641px) {
  .kpi__stat:not(:first-child)[data-scrub]::before { transform: scaleY(1); }
}

/* ═════════════════ section 03: the operating model ═════════════════
   Everything below is scoped to `.eco` and overrides the section's own base
   rules further up the file rather than replacing them, so the markup this
   section shares with sections.html/structure.html still renders there at the
   old scale.

   The section was the page's one flat white stretch, sitting between a light
   band (02) and a dark band (04), with the small `.section__head` type while
   its numbered neighbours all carry the wide head. That, and not the content,
   is why it read as a gap in the page. Three changes: the head is promoted to
   the wide variant with its own ghost numeral, the route diagram is set at
   roughly half again its old scale, and the motion is scroll-driven where the
   reader controls it (the spine, the icons, the accent bars) and looped only
   where it stands in for something continuous (the pulse travelling the spine,
   the hub's aura, the ticks arriving at each actor).
   ─────────────────────────────────────────────────────────────────── */

/* ── the ground ── */
.eco {
  /* the flat --surface-2 stays the base; the two washes give the section a
     top-right gold and bottom-left blue so it is lit rather than filled */
  background:
    radial-gradient(115% 78% at 80% -12%, color-mix(in srgb, var(--c-gold) 13%, transparent), transparent 58%),
    radial-gradient(105% 72% at 6% 110%, color-mix(in srgb, var(--c-blue) 11%, transparent), transparent 60%),
    var(--surface-2);
  padding-block: clamp(4rem, 9vh, 7rem);
  overflow: hidden;
}
/* One slow-drifting bloom behind the diagram. Sized in vmax so it stays a
   single soft light source at any viewport rather than a visible circle. */
.eco::after {
  content: '';
  position: absolute;
  z-index: -1;
  inset-inline-start: 50%;
  inset-block-start: 42%;
  width: 92vmax;
  height: 92vmax;
  margin: -46vmax 0 0 -46vmax;
  pointer-events: none;
  background: radial-gradient(closest-side,
    color-mix(in srgb, var(--c-gold) 16%, transparent),
    color-mix(in srgb, var(--c-blue) 7%, transparent) 46%,
    transparent 72%);
  animation: eco-bloom 26s ease-in-out infinite alternate;
}
@keyframes eco-bloom {
  from { transform: scale(0.82) translate3d(-4%, 2%, 0); opacity: 0.75; }
  to   { transform: scale(1.06) translate3d(5%, -3%, 0); opacity: 1; }
}

/* ── the head ── */
/* The numeral the section never had. Larger than the shared one and painted as
   a navy-to-gold wash instead of flat ink at 4.5% - at this size a single flat
   tint reads as a smudge. It drifts on --pv, so it is not glued to the type. */
.eco__head::before {
  font-size: clamp(7rem, 19vw, 17rem);
  opacity: 1;
  color: transparent;
  background-image: linear-gradient(170deg,
    color-mix(in srgb, var(--c-navy) 13%, transparent) 0%,
    color-mix(in srgb, var(--c-gold) 20%, transparent) 100%);
  -webkit-background-clip: text;
          background-clip: text;
  translate: 0 calc((0.5 - var(--pv, 0.5)) * 34px);
}
.eco__head h2 {
  /* one step past the shared wide head: this is the section that explains the
     mechanism the rest of the page refers back to */
  font-size: clamp(2.3rem, 5.6vw, 4.6rem);
  font-weight: 600;
  letter-spacing: -0.04em;
  line-height: 0.98;
  text-wrap: balance;
}
/* The phrase the heading turns on. A gold highlight travels across it. Both
   ends of the gradient are --ink and its peak is --gold-700 - the gold the
   palette notes measure at 5.2:1 on white - so the phrase clears normal-text
   contrast at every point in the sweep, not just large-text. --gold-600 was
   brighter and measured 2.99:1, which is under even the large-text 3:1. */
.eco__head h2 em {
  position: relative;
  font-style: normal;
  background-image: linear-gradient(96deg,
    var(--ink) 0 34%,
    var(--gold-700) 47%,
    var(--gold-700) 53%,
    var(--ink) 66% 100%);
  background-size: 320% 100%;
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
  animation: eco-sweep 7.5s cubic-bezier(0.55, 0, 0.45, 1) infinite;
}
@keyframes eco-sweep {
  0%, 14%   { background-position: 120% 0; }
  64%, 100% { background-position: -20% 0; }
}
/* And a rule that draws with the scroll, so the heading is underscored by the
   reader's own movement rather than by a timer. On the heading and not on the
   <em>: at this size the phrase wraps to a second line, and an absolutely
   placed underline inside a fragmented inline resolves against the union of
   its fragments - so it drew one bar under the last line and nothing under the
   first. The sweep itself is fine there, since a sliced inline background is
   painted as though the box were continuous. */
.eco__head h2::after {
  content: '';
  display: block;
  width: min(100%, 20rem);
  height: 4px;
  margin-block-start: clamp(1rem, 2vh, 1.5rem);
  border-radius: 99px;
  background: linear-gradient(90deg, var(--gold-600), var(--gold-500) 55%, var(--gold-300));
  transform: scaleX(var(--p, 1));
  transform-origin: left center;
}
/* The rule follows the head. Under a centered heading a bar that starts at the
   left edge of the measure and grows rightwards is the one element still
   declaring which way the text is set, and it reads as a mistake rather than as
   a flourish. Centered, it opens from the middle in both directions, which is
   also the more natural reading of a scroll-drawn rule under centered type. */
.section__head--center.eco__head h2::after {
  margin-inline: auto;
  transform-origin: center;
}
.eco__head > p:not(.eyebrow) {
  font-size: clamp(1.02rem, 1.35vw, 1.18rem);
  line-height: 1.6;
}

/* ── the mandate, now across the full measure ── */
.eco .eco__mandate {
  max-width: 46ch;
  margin-block: clamp(2rem, 4.5vh, 3rem) 0;
  margin-inline: auto;
  padding-block: 0.2rem 0.3rem;
  padding-inline-start: 0;
  border-inline-start: none;
  text-align: center;
  font-size: clamp(1.1rem, 1.9vw, 1.45rem);
  line-height: 1.45;
  letter-spacing: -0.015em;
}

/* ── the route ── */
/* Its own stacking context, so the pulse below can be layered against the
   three tiers without reaching the section's own bloom at z-index -1. */
.eco .route {
  isolation: isolate;
  gap: clamp(1.9rem, 4.5vh, 3rem);
  margin-top: clamp(2.25rem, 5.5vh, 3.5rem);
  padding-block: 1.25rem;
}
.eco .route__crest,
.eco .route__states,
.eco .route__hub,
.eco .route__actors { z-index: 1; }

/* ── the web ──
   The strands are drawn by js/eco-web.js into this one absolutely-placed SVG,
   sized to the container on every redraw. `inset: 0` rather than a width and
   height in CSS: the script writes the viewBox from the same box, so the two
   can never disagree about what one user unit is.

   z-index 0 against the tiers' 1, inside .route's own isolation, so the web
   passes behind every tile without any tile needing a background it did not
   already have. */
.eco .route__web {
  position: absolute;
  z-index: 0;
  inset: 0;
  overflow: visible;
  pointer-events: none;
}

/* Two weights, reading from quietest to loudest: the mandate down to the
   states, and the radials that carry the traffic. Both are hairlines - a web at
   2px reads as plumbing.

   A third weight sat between them until 2026-08-29: `--ring`, a dashed strand
   threading each row. Removed on the client's instruction - see js/eco-web.js
   for what it was doing and why it is not missed.

   Darkened on the client's instruction, 2026-08-29. All three strands used to
   be --blue-100 (#CBD8DF), and that colour was chosen against a white card.
   The section's ground is the mist wash, so a 1px #CBD8DF hairline was laid on
   a tint of itself: even at opacity 0.9 the radials were close to invisible on
   a normal screen and gone entirely on a projector. --blue-500 (#4B7E99) is
   the same hue two steps down and reads as a drawn line rather than a smudge.
   The hierarchy is kept, and kept in the opacities rather than in the colour,
   so the mandate still sits behind the traffic. */
.eco .route__strand {
  fill: none;
  stroke-width: 1;
  stroke-linecap: round;
  transition: stroke 0.4s var(--ease-out), opacity 0.4s var(--ease-out);
}
.eco .route__strand--crest { stroke: var(--blue-500); opacity: 0.5; }
.eco .route__strand--in,
.eco .route__strand--out   { stroke: var(--blue-500); opacity: 0.92; }

/* Pointing at one tile answers "what is this connected to": its own radial
   goes gold and everything else drops back far enough to leave it alone. */
.eco .route__web.is-lit .route__strand { opacity: 0.14; }
.eco .route__web .route__strand.is-on {
  stroke: var(--gold-500);
  stroke-width: 1.6;
  opacity: 1;
}

/* What travels the strands. Gold, small, and slightly bloomed so a 3px dot
   still reads against a 1px line at the scale this diagram is drawn. */
.eco .route__spark {
  fill: var(--gold-500);
  filter: drop-shadow(0 0 4px color-mix(in srgb, var(--c-gold) 70%, transparent));
}

/* The Council's emblem, tier one. Sized off the hub rather than set at a fixed
   px so it stays in proportion when the diagram scales, and given the same
   round plate the footer uses - the artwork is a circular seal on a light
   ground, so a square card would show its corners. */
.eco .route__crest { position: relative; line-height: 0; }
.eco .route__crest img {
  width: clamp(3.9rem, 7.5vw, 5.6rem);
  height: auto;
  border-radius: 50%;
  background: #fff;
  padding: 0.35rem;
  box-shadow:
    0 10px 26px -14px rgba(20, 30, 38, 0.5),
    0 0 0 1px color-mix(in srgb, var(--c-gold) 30%, transparent);
}

/* The straight spine and its running segment were the whole connection story
   before the web existed. Both off here, or the web is drawn over a diagram
   that already claims to be one. */
.eco .route--web::before,
.eco .route--web::after { content: none; }

/* The tier labels. They sit on the spine, and at 0.72rem with the line running
   straight through the letterforms they read as a collision rather than as a
   caption. Set as a quiet pill so each one is a stop on the line - the ground
   here is a gradient, so a flat knockout would not have matched it. */
.eco .eco__col-label {
  position: relative;
  z-index: 2;
  margin: 0;
  padding: 0.4rem 1rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  font-size: var(--fs-micro);
  letter-spacing: 0.22em;
  color: var(--blue-700);
  box-shadow: 0 6px 16px -12px rgba(20, 30, 38, 0.5);
}

/* The spine, at weight. Faded at both ends so it reads as a line passing
   through the section rather than one that starts and stops. */
.eco .route::before {
  z-index: 0;
  width: 2px;
  margin-inline-start: -1px;
  background: linear-gradient(to bottom,
    transparent, var(--blue-100) 7%, var(--blue-100) 93%, transparent);
}
/* What moves along it: one bright segment running top to bottom, on the same
   reading as .net-pulse in the hero - the route carries something. Tied to --p
   so it does not run before the spine it travels has been drawn. */
.eco .route::after {
  content: '';
  position: absolute;
  z-index: 0;
  left: 50%;
  top: 0;
  bottom: 0;
  width: 2px;
  margin-inline-start: -1px;
  pointer-events: none;
  opacity: var(--p, 1);
  background-image: linear-gradient(to bottom,
    transparent 0 43%,
    var(--gold-500) 47%,
    var(--gold-300) 50%,
    var(--gold-500) 53%,
    transparent 57% 100%);
  background-size: 100% 240%;
  background-repeat: no-repeat;
  animation: eco-spine 3.8s linear infinite;
}
@keyframes eco-spine {
  from { background-position: 0 -140%; }
  to   { background-position: 0 140%; }
}

/* The hub. It is the one node the whole diagram routes through, so it is the
   one that is lit rather than outlined. */
.eco .route__hub {
  max-width: none;
  padding: 1.35rem 2.4rem 1.45rem;
  border-radius: 16px 0 16px 0;
  box-shadow:
    0 26px 56px -18px rgba(20, 30, 38, 0.58),
    0 0 0 1px color-mix(in srgb, var(--c-gold) 42%, transparent);
}
.eco .route__hub b {
  font-size: clamp(1.5rem, 3vw, 2.15rem);
  letter-spacing: -0.025em;
}
.eco .route__hub span {
  font-size: 0.82rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
/* A conic light travelling around the plate's edge.
   Two things had to be true for this to stay on the edge, and the first fix only
   got one of them:

   1. It has to *be* a ring. A `z-index: -1` pseudo-element cannot hide behind
      its own parent's background - a stacking context paints its own background
      first and negative-z descendants immediately after - so an unmasked conic
      is a wedge painted straight across the GOIC plate. Hence the 2px
      content-box/exclude mask, the same pair the state chips use.

   2. The element must not move. Rotating it was the leak in the screenshot: the
      plate is a 539x216 rectangle, and a rectangle that wide rotated 45 degrees
      reaches roughly 267px from its own center, so the ring swept far outside
      the plate and one of its long edges read as a gold diagonal cutting across
      the artwork. Only a square would have survived that.

   So the element stays exactly on the plate's edge and the *gradient* turns
   inside it, via the registered angle below - which is how `.eco__chip::after`
   has always done it. */
@property --hub-a {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}
.eco .route__hub::before {
  content: '';
  position: absolute;
  z-index: -1;
  inset: -2px;
  padding: 2px;
  border-radius: 18px 0 18px 0;
  /* the fallback angle keeps a static ring where @property is unsupported,
     rather than dropping the whole gradient as an invalid value */
  background: conic-gradient(from var(--hub-a, 0deg),
    transparent 0deg,
    color-mix(in srgb, var(--c-gold) 90%, transparent) 46deg,
    transparent 104deg,
    transparent 360deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  animation: eco-orbit 5.5s linear infinite;
}
/* and one ring breathing outward, so the hub has a pulse of its own even when
   the page is still */
.eco .route__hub::after {
  content: '';
  position: absolute;
  z-index: -2;
  inset: 0;
  border-radius: 16px 0 16px 0;
  border: 1px solid var(--gold-500);
  animation: eco-halo 3.4s var(--ease-out) infinite;
  pointer-events: none;
}
@keyframes eco-orbit { to { --hub-a: 360deg; } }
@keyframes eco-halo {
  0%   { transform: scale(1);    opacity: 0.55; }
  70%  { transform: scale(1.28); opacity: 0; }
  100% { transform: scale(1.28); opacity: 0; }
}

/* ── the member states: flag tiles, name on highlight ──
   The chips were 26x17px flags beside 0.78rem names - six small pills where the
   diagram's top tier should be its most recognisable row. The flag becomes the
   whole control at roughly three times the area, and the name is a caption that
   appears when the tile is highlighted.

   The caption is absolutely placed and the row reserves the space for it, so
   revealing one name never reflows the other five. It is hidden with `opacity`
   and never with `display: none` or `visibility: hidden`, so it stays in the
   accessibility tree and each button keeps its accessible name at rest - the
   flags themselves are aria-hidden, so without that the row would be six
   unlabelled buttons. */
.eco .eco__legend {
  gap: clamp(0.7rem, 1.7vw, 1.25rem);
  margin-top: 0;
  padding-block-end: 2.5rem;   /* the caption's lane */
}
.eco .eco__chip {
  padding: 0;
  gap: 0;
  border: 0;
  border-radius: 10px 0 10px 0;
  background: none;
  box-shadow: none;
}
.eco .eco__chip-flag {
  width: clamp(64px, 7.2vw, 96px);
  height: auto;
  aspect-ratio: 5 / 3;
  border-radius: 9px 0 9px 0;
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--ink) 14%, transparent),
    0 8px 18px -12px rgba(20, 30, 38, 0.55);
}
/* The tile grows, and the whole chip lifts - so the transform lives on the
   chip and the flag's own scale comes off, or the two would compound into a
   jump at this size. */
.eco .eco__chip:hover,
.eco .eco__chip:focus-visible {
  background: none;
  transform: translateY(-5px);
  box-shadow: none;
  /* The chip is its own stacking context, so its caption cannot escape above a
     later sibling from the inside - the chip itself has to be raised. Needed
     wherever the row wraps: on a phone the six tiles are 4 + 2, and a caption
     on the first row lands over the second. Raised, with an opaque pill and a
     shadow, it reads as the tooltip it is instead of a collision. */
  z-index: 5;
}
.eco .eco__chip:hover .eco__chip-flag,
.eco .eco__chip:focus-visible .eco__chip-flag {
  transform: none;
  box-shadow:
    0 0 0 2px color-mix(in srgb, var(--c) 70%, transparent),
    0 16px 30px -12px color-mix(in srgb, var(--c) 75%, transparent);
}
/* The pill's scan pass has nowhere to show now that the flag fills the chip
   edge to edge, so it becomes the tile's glow instead: the state's own colour
   bloomed out behind it. */
.eco .eco__chip::before {
  /* a length and not a percentage: `inset: -14%` resolves against the inline
     axis for the sides and the block axis for top and bottom, so on a 3:2 tile
     it came out -9px/-13px and the bloom was an oval */
  inset: -10px;
  border-radius: 16px 0 16px 0;
  background: radial-gradient(closest-side,
    color-mix(in srgb, var(--c) 46%, transparent), transparent 72%);
  background-size: auto;
  background-position: 0 0;
  filter: blur(9px);
  transition: opacity 0.32s var(--ease-out);
}
.eco .eco__chip:hover::before,
.eco .eco__chip:focus-visible::before { opacity: 1; background-position: 0 0; }
/* the traced ring keeps working - it now traces the tile */
.eco .eco__chip::after { inset: -2px; padding: 2px; border-radius: 11px 0 11px 0; }

.eco .eco__chip-name {
  position: absolute;
  inset-block-start: calc(100% + 0.6rem);
  inset-inline-start: 50%;
  z-index: 3;
  padding: 0.28rem 0.7rem;
  border-radius: 999px;
  white-space: nowrap;
  font-size: 0.8rem;
  font-weight: 600;
  line-height: 1.2;
  color: var(--ink);
  background: var(--surface);
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--c) 40%, transparent),
    0 8px 18px -10px rgba(20, 30, 38, 0.45);
  opacity: 0;
  translate: -50% 5px;
  pointer-events: none;
  transition: opacity 0.26s var(--ease-out), translate 0.26s var(--ease-out);
}
.eco .eco__chip:hover .eco__chip-name,
.eco .eco__chip:focus-visible .eco__chip-name { opacity: 1; translate: -50% 0; }

/* The six actors, at card scale rather than chip scale.
   `width: 100%` because the tiers are centered flex items in a column: left to
   size themselves the row takes its max-content width and hangs past the wrap
   on a phone, which the section's new `overflow: hidden` would clip rather than
   spill. Full width plus `justify-content: center` wraps inside the measure and
   still centers each row. */
.eco .route__states,
.eco .route__actors { width: 100%; }
.eco .route__actors { gap: 1rem 1.15rem; }
.eco .route__node {
  position: relative;
  overflow: hidden;
  max-width: 14rem;
  padding: 1.1rem 1.35rem 1.15rem;
  border-radius: var(--corner-box);
  background: var(--surface);
  box-shadow: 0 8px 20px -18px rgba(20, 30, 38, 0.5);
  transition:
    border-color 0.35s var(--ease-out),
    box-shadow 0.35s var(--ease-out),
    transform 0.35s var(--ease-out);
}
/* One line, on the client's instruction 2026-08-29. "Technology Providers" is
   the longest of the five and at the old 12.5rem cap it broke after
   "Technology", which left that one tile two lines taller than its neighbours
   and pushed its own subtitle out of the row's baseline. Widening the cap alone
   would not have been enough on its own - the break point moves with the font -
   so the title is also told not to wrap, and the cap is set from the width that
   title actually needs. The subtitles still wrap; they are allowed to. */
.eco .route__node b {
  font-size: 1.08rem;
  letter-spacing: -0.02em;
  white-space: nowrap;
}
.eco .route__node span { font-size: 0.82rem; }
.eco .route__node:hover {
  transform: translateY(-4px);
  border-color: var(--gold-500);
  box-shadow: 0 16px 32px -16px rgba(20, 30, 38, 0.45);
}
/* a gold sheen crossing the node under the pointer - the same device the
   service cards use, at node size */
.eco .route__node::before {
  content: '';
  position: absolute;
  inset: 0;
  translate: -110% 0;
  pointer-events: none;
  background: linear-gradient(105deg,
    transparent 20%,
    color-mix(in srgb, var(--c-gold) 22%, transparent) 50%,
    transparent 80%);
  transition: translate 0.7s var(--ease-out);
}
.eco .route__node:hover::before { translate: 110% 0; }
/* Each node's arrival tick. Six dots lighting in sequence reads as the route
   reaching one actor after another; the delay is the node's own index, so the
   order follows the list and not a separate variable. */
.eco .route__node::after {
  content: '';
  position: absolute;
  inset-block-start: 0.55rem;
  inset-inline-end: 0.55rem;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--gold-500);
  opacity: 0;
  animation: eco-tick 4.2s var(--ease-out) infinite;
}
.eco .route__node:nth-child(1)::after { animation-delay: 0s; }
.eco .route__node:nth-child(2)::after { animation-delay: 0.28s; }
.eco .route__node:nth-child(3)::after { animation-delay: 0.56s; }
.eco .route__node:nth-child(4)::after { animation-delay: 0.84s; }
.eco .route__node:nth-child(5)::after { animation-delay: 1.12s; }
.eco .route__node:nth-child(6)::after { animation-delay: 1.4s; }
@keyframes eco-tick {
  0%  { opacity: 0; transform: scale(0.4); }
  8%  { opacity: 1; transform: scale(1); }
  34% { opacity: 0; transform: scale(1.9); }
  100%{ opacity: 0; transform: scale(1.9); }
}

/* ── the four connect-cards ── */
.eco .eco__list {
  gap: 2px;
  margin-top: clamp(3rem, 6.5vh, 4.75rem);
}
.eco .eco__item {
  overflow: hidden;
  gap: 0.65rem;
  padding: 2.1rem 1.7rem 2.2rem;
  transition: background 0.3s var(--ease-out), transform 0.35s var(--ease-out);
}
.eco .eco__item:hover { transform: translateY(-4px); }
/* the accent bar draws with the scroll instead of waiting for a pointer, so
   the row still animates for a reader who never hovers anything */
.eco .eco__item[data-scrub]::after {
  height: 3px;
  transform: scaleX(var(--p));
  transition: none;
}
/* the wash the hover has instead */
.eco .eco__item::before {
  content: '';
  position: absolute;
  inset: auto 0 0 0;
  z-index: 0;
  height: 100%;
  pointer-events: none;
  background: linear-gradient(to top, var(--gold-100), transparent 58%);
  transform: scaleY(0);
  transform-origin: bottom;
  transition: transform 0.5s var(--ease-out);
}
.eco .eco__item:hover::before { transform: scaleY(1); }
.eco .eco__item > * { position: relative; z-index: 1; }

.eco .eco__item-head { align-items: flex-start; margin-bottom: 0.35rem; }
/* the icon in a tinted badge, same as the service cards - at 2.3rem bare on
   the card it was the smallest mark in the section */
.eco .eco__icon {
  width: 3.5rem;
  height: 3.5rem;
  border-radius: 50%;
  background: var(--blue-50);
  transition: background 0.35s var(--ease-out), color 0.35s var(--ease-out);
}
.eco .eco__icon svg { width: 58%; height: 58%; }
.eco .eco__item:hover .eco__icon {
  background: var(--gold-100);
  color: var(--gold-700);
}
/* each icon draws itself as the card arrives, scrubbed off --p inherited from
   the card rather than run on a timer */
.eco .eco__icon path,
.eco .eco__icon rect {
  stroke-dasharray: 200;
  stroke-dashoffset: calc((1 - var(--p, 1)) * 200);
}
/* The number, which was 0.74rem of grey. It is now the card's largest mark.
   It keeps the slate it always had rather than dropping to a --blue-100 ghost:
   at this size it is the card's index, something to be read, and #CBD8DF on a
   white card is about 1.3:1. Gold on hover is --gold-700 for the same reason
   the heading's sweep is - --gold-500 on white is 2.4:1. */
.eco .eco__num {
  font-size: clamp(2.2rem, 3.8vw, 3.1rem);
  font-weight: 600;
  line-height: 0.82;
  letter-spacing: -0.055em;
  color: var(--blue-500);
  transition: color 0.35s var(--ease-out);
}
.eco .eco__item:hover .eco__num { color: var(--gold-700); }
.eco .eco__item b {
  font-size: clamp(1.08rem, 1.7vw, 1.32rem);
  letter-spacing: -0.02em;
  line-height: 1.2;
}
.eco .eco__item p { font-size: 1.02rem; line-height: 1.55; }

/* ── reduced motion ──
   Everything loop-driven stops; everything scroll-driven is parked at its
   resting state. The section keeps its scale, its colour and its hovers - the
   preference is about movement, not about going back to the old design. */
@media (prefers-reduced-motion: reduce) {
  .eco::after,
  .eco__head h2 em,
  .eco .route::after,
  .eco .route__hub::before,
  .eco .route__hub::after,
  .eco .route__node::after { animation: none; }
  .eco::after { transform: none; }
  .eco .route::after,
  .eco .route__node::after,
  .eco .route__hub::after { opacity: 0; }
  /* the sweep's gradient never moves, so the phrase would sit on whichever
     colour the resting position happens to be - give it flat ink instead */
  .eco__head h2 em { background-image: none; color: var(--ink); }
  .eco__head h2::after { transform: scaleX(1); }
  .eco__head::before { translate: 0 0; }
  .eco .eco__item[data-scrub]::after { transform: scaleX(1); opacity: 1; }
  /* the scoped rules above are more specific than the shared `transition: none`
     block further up the file, so they have to be turned off here by name */
  .eco .eco__item,
  .eco .eco__icon,
  .eco .eco__num,
  .eco .eco__item::before,
  .eco .route__node { transition: none; }
  .eco .eco__item:hover { transform: none; }
  .eco .route__node:hover { transform: none; }
  .eco .route__node::before,
  .eco .route__node:hover::before { display: none; }
  /* same reason as the block above: these scoped rules outrank the shared
     reduced-motion rules for .eco__chip further up the file */
  .eco .eco__chip-name { transition: none; }
  .eco .eco__chip:hover,
  .eco .eco__chip:focus-visible { transform: none; }
  .eco .eco__icon path,
  .eco .eco__icon rect { stroke-dashoffset: 0; }
}


/* ══════════════════════════ reCAPTCHA ══════════════════════════
   Google's floating badge, suppressed.

   Their terms allow the badge to be hidden only if the page says the line
   goic_recaptcha_notice() prints instead, with both links live. The two go
   together: removing the notice without restoring the badge is a terms
   breach, not a tidy-up.

   Hidden rather than not loaded - grecaptcha needs its own iframe to work,
   so the widget is moved out of sight rather than removed. */
.grecaptcha-badge {
  visibility: hidden;
}

.goic-recaptcha-notice {
  margin: 1rem 0 0;
  font-size: .75rem;
  line-height: 1.5;
  color: var(--ink-3, #6b7280);
}

.goic-recaptcha-notice a {
  color: inherit;
  text-decoration: underline;
}

.goic-recaptcha-notice a:hover,
.goic-recaptcha-notice a:focus-visible {
  color: var(--ink, #111827);
}

/* ── touch devices: never let a field size trigger Safari's zoom ────────────
   iOS Safari zooms the whole page in when a field is focused whose computed
   font-size is under 16px, and it does not zoom back out. The reader is left on
   a page wider than the screen, panning sideways to read what they are typing.
   Reported on the Services enquiry panel, 2026-09-16, but the panel was only
   where it was noticed: --fs-xs is 14px and every form on the site sets its
   controls from that scale, so the contact form and the four register
   application forms all did it too.

   16px exactly, and only where it matters. `pointer: coarse` is true on phones
   and tablets and false on a mouse-driven screen, so the desktop typography is
   untouched - this is not a design change, it is the one measurement Safari
   reads as permission to leave the page alone. Anything at or above 16px is
   left alone, so a control that is already large does not shrink.

   Placed here rather than beside any one component because every one of them
   had the same bug, and a fix copied into five files drifts.

   !important, deliberately. Every control this has to reach is sized by a
   class - `.enq__field input`, `.goic-form input` - which outranks a bare
   element selector, and services.css and forms.css are enqueued after this
   file, so an equal-specificity rule would lose on order as well. The
   alternative is a copy of this block at the end of each component sheet,
   which is the drift the paragraph above is avoiding. This is a platform
   workaround rather than a style, and nothing should be overriding it.

   Two conditions, not one. `pointer: coarse` is the honest test - it is what
   Safari's behaviour actually keys on - but it is false in every desktop
   browser, which means a rule written on it alone can never be checked on the
   machine the CSS is written on. The width clause makes it observable at a
   phone viewport and is correct on its own terms anyway: a 480px-wide window is
   a phone-sized reading column whatever is pointing at it. */
@media (pointer: coarse), (max-width: 30rem) {
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
  select,
  textarea {
    font-size: max(16px, 1em) !important;
  }
}

/* ── touch devices: controls a thumb has to land on ─────────────────────────
   Same two conditions as the block above, and for the same reason: `pointer:
   coarse` is the honest test and the width clause is what makes it checkable.

   44px is the figure both Apple and the WCAG 2.2 target-size rule land on, and
   the three controls below were the only ones measured under it at 390px that
   are genuinely a target rather than a line of text. The header burger is
   already 44x44 and the mobile menu's own links are far larger, so the bar and
   the sheet needed nothing.

   Footer text links are deliberately left alone. They are 16-24px tall because
   they are inline links in a paragraph-like column, which the same WCAG rule
   exempts by name, and padding them into 44px boxes would turn a quiet footer
   list into a stack of buttons. The exemption is for text in a sentence, not a
   licence for small buttons - which is why the social icons beside them, which
   are buttons, are raised here. */
@media (pointer: coarse), (max-width: 30rem) {
  /* Measured 61x31. It sits next to a 44px burger, so the bar already has the
     height and only the chip was short.

     `a.lang`, not `.lang`. This is the trap the note beside the .nav__login
     rule up at line 464 describes, walked straight into: `a.lang` there is
     (0,1,1) and sets `display: inline-block`, a media query adds no
     specificity, so a bare `.lang` at (0,1,0) here loses the display and keeps
     only the height - which is how the chip grew to 44px with its label
     hanging at the top instead of centred. Matching the element selector is
     what makes inline-flex, and therefore align-items, actually apply. */
  a.lang { min-height: 2.75rem; display: inline-flex; align-items: center; }

  /* Measured 38.4px square, under a comment that already called it "the size of
     a comfortable tap target" - it was, on the desk it was written at. */
  .footer .footer__social a { width: 2.75rem; height: 2.75rem; }

  /* Measured 64x38. The chip on the ecosystem map is the flag itself, and the
     flag's 5:3 ratio is not negotiable, so the height goes on the button and
     the flag stays centred inside it at the size it was drawn. */
  .eco .eco__chip { min-height: 2.75rem; }
}
