/* ==========================================================================
   BASE - reset, typography, layout primitives, media engine styles
   Depends on tokens.css. Load tokens.css first.
   ========================================================================== */

/* ---- Reset ------------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }

* { margin: 0; }

/* The `hidden` attribute wins over any layout rule.

   The browser's own `[hidden] { display: none }` is the weakest kind of
   declaration, so a single class that sets `display` puts the element back on
   screen. Because that element is usually a dialog or overlay sitting at
   `inset: 0`, it then swallows every click on the page behind it while being
   completely invisible, which looks exactly like the page having hung. This
   rule is the one place to settle it: nothing here toggles `hidden` on an
   element that needs a display value, so `!important` costs nothing. */
[hidden] { display: none !important; }

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  scroll-behavior: smooth;
}

/* Lenis drives scrolling when present, so the native smooth behaviour is
   turned off to stop the two fighting each other. */
html.lenis { scroll-behavior: auto; }
html.lenis body { height: auto; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-stopped { overflow: hidden; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  font-family: var(--font-sans);
  font-size: var(--text-base);
  font-weight: var(--weight-regular);
  line-height: var(--leading-normal);
  color: var(--color-foreground);
  background: var(--color-background);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* No page-level horizontal scrollbar, ever. */
  overflow-x: hidden;
  min-height: 100vh;
}

img, picture, video, canvas, svg {
  display: block;
  max-width: 100%;
}

input, button, textarea, select { font: inherit; color: inherit; }

button { cursor: pointer; background: none; border: none; padding: 0; }

p, h1, h2, h3, h4, h5, h6 { overflow-wrap: break-word; }

ul, ol { padding: 0; list-style: none; }

a { color: var(--color-primary); text-decoration-thickness: 1px; text-underline-offset: 2px; }

table { border-collapse: collapse; width: 100%; }

/* ---- Focus ------------------------------------------------------------- */

/* Visible for keyboard users, invisible for mouse users. */
:focus { outline: none; }

:focus-visible {
  outline: 3px solid var(--color-ring);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* ---- Typography -------------------------------------------------------- */

h1, h2, h3, h4, h5, h6 {
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  color: var(--color-foreground);
  text-wrap: balance;
}

h1 { font-size: var(--text-4xl); }
h2 { font-size: var(--text-3xl); }
h3 { font-size: var(--text-2xl); }
h4 { font-size: var(--text-xl); }
h5 { font-size: var(--text-lg); }
h6 { font-size: var(--text-base); }

p { text-wrap: pretty; }

/* The small uppercase label above a heading. */
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--color-accent);
}

.eyebrow::before {
  content: "";
  width: 28px;
  height: 2px;
  background: var(--grad-accent);
  flex: none;
}

.section-title {
  font-size: clamp(1.75rem, 1.2rem + 1.8vw, var(--text-4xl));
  margin-block: var(--space-3) var(--space-4);
}

.lede {
  font-size: var(--text-lg);
  line-height: var(--leading-relaxed);
  color: var(--color-muted-foreground);
  max-width: 62ch;
}

.prose { max-width: var(--container-prose); }
.prose > * + * { margin-top: var(--space-4); }
.prose h2 { margin-top: var(--space-10); }
.prose h3 { margin-top: var(--space-8); }
.prose ul, .prose ol { padding-left: var(--space-6); }
.prose ul { list-style: disc; }
.prose ol { list-style: decimal; }
.prose li + li { margin-top: var(--space-2); }

/* ---- Layout ------------------------------------------------------------ */

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.container--wide { max-width: var(--container-wide); }
.container--narrow { max-width: 900px; }

.section { padding-block: var(--section-y); position: relative; }
.section--tight { padding-block: calc(var(--section-y) * 0.6); }
.section--flush-top { padding-top: 0; }

.grid { display: grid; gap: var(--space-6); }

/* Auto-fitting card grid. The min width stops cards collapsing into
   unreadable columns on a tablet. */
.grid--cards { grid-template-columns: repeat(auto-fit, minmax(clamp(240px, 28vw, 320px), 1fr)); }
.grid--2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr)); }
.grid--3 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); }
.grid--4 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 230px), 1fr)); }

.split {
  display: grid;
  gap: clamp(var(--space-8), 4vw, var(--space-16));
  align-items: center;
  grid-template-columns: 1fr;
}

@media (min-width: 900px) {
  .split { grid-template-columns: 1fr 1fr; }
  .split--media-first > :first-child { order: 2; }
}

.stack > * + * { margin-top: var(--space-4); }
.stack--lg > * + * { margin-top: var(--space-8); }

.cluster {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: center;
}

/* ---- Utilities --------------------------------------------------------- */

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Keyboard users land on this first and can jump past the navigation. */
.skip-link {
  position: absolute;
  top: var(--space-2);
  left: var(--space-2);
  z-index: calc(var(--z-navbar) + 10);
  padding: var(--space-3) var(--space-5);
  background: var(--color-primary);
  color: var(--color-on-primary);
  border-radius: var(--radius-md);
  font-weight: var(--weight-semibold);
  transform: translateY(-200%);
  transition: transform var(--dur-fast) var(--ease-out);
}

.skip-link:focus-visible { transform: translateY(0); }

.text-center { text-align: center; }
.text-balance { text-wrap: balance; }
.mx-auto { margin-inline: auto; }
.hide { display: none !important; }

@media (max-width: 767px) { .hide-mobile { display: none !important; } }
@media (min-width: 768px) { .hide-desktop { display: none !important; } }

/* The faint engineering-drawing grid used behind several sections. */
.has-grid-texture { position: relative; }

.has-grid-texture::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: var(--pattern-grid);
  background-size: var(--grid-size) var(--grid-size);
  pointer-events: none;
  z-index: 0;
}

.has-grid-texture > * { position: relative; z-index: 1; }

/* ==========================================================================
   MEDIA ENGINE
   Styles for the markup that media.js generates.
   ========================================================================== */

.media {
  position: relative;
  overflow: hidden;
  display: block;
  /* Holds space before the file arrives so the layout never jumps. */
  background: var(--color-surface);
}

.media--background {
  position: absolute;
  inset: 0;
  z-index: 0;
  /* Decorative, so pointer events pass through to the content above. */
  pointer-events: none;
}

.media-asset {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  opacity: 0;
  transition: opacity var(--dur-slow) var(--ease-out);
}

.media-asset.is-loaded { opacity: 1; }

/* A banner picture written into the page by the server sits in a <picture>
   wrapper. The wrapper takes the whole box, so the picture inside fills it
   exactly as one created by media.js does. */
picture[data-ssr-media] { position: absolute; inset: 0; display: block; }

/* An image in the content flow keeps its own height rather than filling a
   positioned parent. */
.media--image .media-asset { height: auto; }

.media-overlay {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    linear-gradient(
      105deg,
      rgba(4, 20, 47, calc(var(--overlay-strength, 0.55) * 1.35)) 0%,
      rgba(7, 34, 79, calc(var(--overlay-strength, 0.55) * 0.92)) 45%,
      rgba(4, 20, 47, calc(var(--overlay-strength, 0.55) * 0.45)) 100%
    );
}

/* ---- Placeholder for an empty or broken slot ---------------------------

   Light, not brand blue.

   This was a saturated blue gradient panel. On a white page, every unfilled
   slot became a large blue rectangle, and a homepage with six of them read as
   broken rather than unfinished. It now matches the icon tile: a pale steel
   surface with a faint grid, so an empty slot sits quietly in the layout
   until real media replaces it.
   ---------------------------------------------------------------------- */

.media-placeholder {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  overflow: hidden;
  background:
    radial-gradient(120% 90% at 50% 0%, #FFFFFF 0%, var(--steel-50) 55%, var(--steel-100) 100%);
}

.media-placeholder__grid {
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(rgba(11, 62, 151, 0.055) 1px, transparent 1px),
    linear-gradient(90deg, rgba(11, 62, 151, 0.055) 1px, transparent 1px);
  background-size: 30px 30px;
  mask-image: radial-gradient(75% 75% at 50% 50%, transparent 15%, #000 100%);
  -webkit-mask-image: radial-gradient(75% 75% at 50% 50%, transparent 15%, #000 100%);
}

/* A slow, very faint sheen, so the surface is not dead flat. */
.media-placeholder__sheen {
  position: absolute;
  inset: -40%;
  background: linear-gradient(
    115deg,
    transparent 38%,
    rgba(11, 62, 151, 0.05) 50%,
    transparent 62%
  );
  animation: sheen-drift 9s var(--ease-in-out) infinite;
}

@keyframes sheen-drift {
  0%, 100% { transform: translateX(-16%); }
  50%      { transform: translateX(16%); }
}

.media-placeholder__label {
  position: relative;
  z-index: 2;
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--steel-500);
  text-align: center;
  padding: var(--space-4);
}

@media (prefers-reduced-motion: reduce) {
  .media-placeholder__sheen { animation: none; }
}

/* A placeholder inside a section that sits on a dark background still needs
   to read as a surface, so it flips to a translucent light panel. */
.section--dark .media-placeholder,
.journey .media-placeholder,
.page-header .media-placeholder {
  background: linear-gradient(145deg, var(--blue-800), var(--blue-950));
}

.section--dark .media-placeholder__grid,
.journey .media-placeholder__grid,
.page-header .media-placeholder__grid {
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.07) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.07) 1px, transparent 1px);
}

.section--dark .media-placeholder__label,
.journey .media-placeholder__label,
.page-header .media-placeholder__label {
  color: rgba(255, 255, 255, 0.6);
}

/* ---- Loading shimmer --------------------------------------------------- */

.media.is-shimmering::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  background: linear-gradient(
    100deg,
    transparent 30%,
    rgba(255, 255, 255, 0.10) 50%,
    transparent 70%
  );
  background-size: 220% 100%;
  animation: shimmer 1.4s linear infinite;
  pointer-events: none;
}

@keyframes shimmer {
  from { background-position: 120% 0; }
  to   { background-position: -120% 0; }
}

@media (prefers-reduced-motion: reduce) {
  .media.is-shimmering::after { animation: none; }
}

/* ---- Fixed aspect boxes ------------------------------------------------ */

.ratio { position: relative; width: 100%; }
.ratio > .media,
.ratio > img,
.ratio > video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

.ratio--16-9  { aspect-ratio: 16 / 9; }
.ratio--21-9  { aspect-ratio: 21 / 9; }
.ratio--4-3   { aspect-ratio: 4 / 3; }
.ratio--3-2   { aspect-ratio: 3 / 2; }
.ratio--1-1   { aspect-ratio: 1 / 1; }
.ratio--4-5   { aspect-ratio: 4 / 5; }

/* The shape of the picture in the home page's introduction, so that image is
   shown whole rather than cropped to a standard ratio. */
.ratio--intro { aspect-ratio: 1330 / 862; }

/* ==========================================================================
   SCROLL REVEAL
   Set up by motion.js. The initial state lives here so content is hidden
   before the script runs, but a visitor with reduced motion or with
   JavaScript disabled still sees everything.
   ========================================================================== */

.reveal {
  opacity: 0;
  /* Rises and settles from very slightly small. The scale is deliberately
     tiny: enough that the element reads as arriving rather than blinking on,
     not enough to look like a zoom effect. translate3d keeps it on the
     compositor. */
  transform: translate3d(0, 26px, 0) scale(0.988);
  transition:
    opacity 560ms var(--ease-out),
    transform 640ms var(--ease-metal);
  will-change: opacity, transform;
}

.reveal.is-visible { opacity: 1; transform: none; }

/* A heading or lede rising on its own looks better slightly sooner than
   the cards below it, which is what the group stagger handles. */
.reveal-group > .reveal { transition-delay: calc(var(--reveal-i, 0) * 80ms); }

/* Staggered children, driven by an index custom property. */
.reveal-group > .reveal { transition-delay: calc(var(--reveal-i, 0) * 80ms); }

/* No JavaScript means no reveal classes get toggled, so show everything. */
.no-js .reveal { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
}

/* The icon inside an empty media slot. */
.media-placeholder__mark {
  position: relative;
  z-index: 2;
  display: grid;
  place-items: center;
  width: 72px;
  height: 72px;
  border-radius: 50%;
  background: #fff;
  border: 1px solid var(--blue-100);
  color: var(--color-primary);
  box-shadow: 0 6px 18px -8px rgba(11, 62, 151, 0.26);
}

.media-placeholder__mark svg { width: 34px; height: 34px; }

/* Over a dark section the white disc would glare, so it inverts. */
.section--dark .media-placeholder__mark,
.journey .media-placeholder__mark {
  background: rgba(255, 255, 255, 0.10);
  border-color: rgba(255, 255, 255, 0.24);
  color: #fff;
  box-shadow: none;
}

/* A page header's background spans the full banner, so an icon centred in it
   lands directly on top of the page title. There is nothing to signal there
   anyway: the heading already says what the page is. */
.page-header .media-placeholder__mark,
.page-header .media-placeholder__label,
.hero .media-placeholder__mark,
.hero .media-placeholder__label {
  display: none;
}

/* When both an icon and a label are present the icon shifts up to make room. */
.media-placeholder__mark:has(+ .media-placeholder__label) { transform: translateY(-14px); }

/* ==========================================================================
   THE OPENING

   A cover carrying the logo and the name, which swings open from its left
   edge like the front of a hardback, with a page turning just behind it. The
   markup is inserted by the inline script in src/config/intro.js; this is
   only how it looks and moves.

   It sits in base.css because base.css is loaded in <head>: the cover has to
   be styled on the very first paint, or the site shows through for a frame
   before it is covered.
   ========================================================================== */

html.intro-on,
html.intro-on body { overflow: hidden; }

.intro {
  position: fixed;
  inset: 0;
  z-index: 10000;
  /* The hinge is the left edge, so the perspective is taken from there: the
     cover swings towards the reader rather than spinning in place. */
  perspective: 2400px;
  perspective-origin: 0% 50%;
  cursor: pointer;
}

/* The page under the cover. Paper, faintly ruled, turning a beat after the
   cover so the opening reads as a book rather than a door. */
.intro__leaf {
  position: absolute;
  inset: 0;
  transform-origin: 0% 50%;
  background:
    repeating-linear-gradient(0deg, transparent 0 31px, rgba(11, 62, 151, 0.05) 31px 32px),
    linear-gradient(90deg, #E9EEF6 0%, #F8FAFD 6%, #FFFFFF 50%, #F3F6FB 100%);
  box-shadow: inset 24px 0 40px -24px rgba(4, 20, 47, 0.22);
}

.intro__cover {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  transform-origin: 0% 50%;
  backface-visibility: hidden;
  background:
    radial-gradient(ellipse at 50% 42%, rgba(51, 140, 242, 0.28), transparent 62%),
    linear-gradient(145deg, #0B3E97 0%, #07224F 58%, #04142F 100%);
  /* The spine: a lit edge down the hinge side, the one detail that makes a
     flat rectangle read as a cover. */
  box-shadow:
    inset 14px 0 0 -6px rgba(255, 255, 255, 0.10),
    inset 34px 0 40px -20px rgba(0, 0, 0, 0.45);
}

/* Shade that deepens as the cover turns away from the light. */
.intro__cover::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, rgba(0, 0, 0, 0.45), rgba(0, 0, 0, 0) 70%);
  opacity: 0;
  pointer-events: none;
}

/* A faint engineering grid across the cover, in keeping with the site. */
.intro__cover::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.045) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.045) 1px, transparent 1px);
  background-size: 48px 48px;
  mask-image: radial-gradient(ellipse at 50% 45%, #000 20%, transparent 75%);
  -webkit-mask-image: radial-gradient(ellipse at 50% 45%, #000 20%, transparent 75%);
  pointer-events: none;
}

.intro__face {
  position: relative;
  z-index: 1;
  display: grid;
  justify-items: center;
  gap: clamp(0.75rem, 2vh, 1.25rem);
  padding: 1.5rem;
  text-align: center;
  color: #FFFFFF;
}

.intro__logo {
  width: clamp(92px, 16vw, 132px);
  height: auto;
  padding: 10px;
  border-radius: 50%;
  background: #FFFFFF;
  box-shadow:
    0 0 0 6px rgba(255, 255, 255, 0.10),
    0 18px 50px -12px rgba(0, 0, 0, 0.55);
  animation: intro-logo 0.75s cubic-bezier(0.22, 1, 0.36, 1) both;
}

.intro__name {
  font-family: var(--font-sans);
  font-size: clamp(1.6rem, 1rem + 3.4vw, 3.4rem);
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -0.02em;
  max-width: 16ch;
  text-wrap: balance;
}

.intro__name .intro__word > span {
  display: inline-block;
  opacity: 0;
  transform: translateY(0.45em);
  animation: intro-letter 0.55s cubic-bezier(0.22, 1, 0.36, 1) forwards;
  animation-delay: calc(0.28s + var(--i) * 0.022s);
}

/* A word never breaks across lines; the line breaks between words. */
.intro__name .intro__word {
  display: inline-block;
  white-space: nowrap;
  opacity: 1;
  transform: none;
  animation: none;
}

.intro__tag {
  font-size: clamp(0.72rem, 0.65rem + 0.35vw, 0.9rem);
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgba(214, 228, 255, 0.82);
  opacity: 0;
  animation: intro-fade-in 0.5s ease-out 0.85s forwards;
}

.intro__rule {
  display: block;
  height: 2px;
  width: 0;
  border-radius: 2px;
  background: linear-gradient(90deg, transparent, #6FB0FB, transparent);
  animation: intro-rule 0.7s cubic-bezier(0.22, 1, 0.36, 1) 0.7s forwards;
}

/* ---- Opening ------------------------------------------------------------ */

.intro.is-opening { cursor: default; pointer-events: none; }

.intro.is-opening .intro__cover { animation: intro-open 1.05s cubic-bezier(0.65, 0, 0.35, 1) forwards; }
.intro.is-opening .intro__cover::after { animation: intro-shade 1.05s ease-in forwards; }
.intro.is-opening .intro__leaf { animation: intro-open 1.05s cubic-bezier(0.65, 0, 0.35, 1) 0.14s forwards; }

/* The last moment is a fade rather than a cut, so the site does not appear
   with a jolt when the overlay is removed. */
.intro.is-opening { animation: intro-fade-out 0.3s ease-out 0.95s forwards; }

.intro.is-fast .intro__cover,
.intro.is-fast .intro__cover::after { animation-duration: 0.38s; }
.intro.is-fast .intro__leaf { animation-duration: 0.38s; animation-delay: 0.04s; }
.intro.is-fast { animation-duration: 0.18s; animation-delay: 0.26s; }

@keyframes intro-logo {
  from { opacity: 0; transform: scale(0.82) translateY(10px); }
  to { opacity: 1; transform: none; }
}

@keyframes intro-letter {
  to { opacity: 1; transform: none; }
}

@keyframes intro-fade-in { to { opacity: 1; } }

@keyframes intro-rule { to { width: clamp(90px, 18vw, 150px); } }

@keyframes intro-open {
  0% { transform: rotateY(0deg); }
  100% { transform: rotateY(-102deg); }
}

@keyframes intro-shade { to { opacity: 1; } }

@keyframes intro-fade-out { to { opacity: 0; visibility: hidden; } }

/* The script already declines to play with reduced motion on. This is the
   belt to its braces: if the overlay is ever present anyway, nothing moves. */
@media (prefers-reduced-motion: reduce) {
  .intro { display: none; }
  html.intro-on, html.intro-on body { overflow: auto; }
}
