@charset "UTF-8";
/* ==========================================================================
   Pricom — main stylesheet
   Hand-written, no build step. Organised as:
   1 Tokens · 2 Reset · 3 Typography · 4 Layout · 5 Motion · 6 Components
   7 Header · 8 Hero · 9 Sections · 10 Footer · 11 Preferences
   ========================================================================== */

/* 0 — Typefaces ===========================================================
   Two families, three roles:

     display  Saira Condensed 800 — headings and figures. Condensed and very
                                    heavy: the register of materiel and unit
                                    markings rather than of software.
     body     Inter Tight         — running text. One variable file, every weight.
     mono     system              — the small technical labels. No download.

   Saira Condensed replaced Chakra Petch. Chakra Petch echoed the wordmark's cut
   corners but topped out at weight 700, so the headings could not be made any
   heavier without changing face. Condensed also matters here: the headings are
   uppercase, uppercase runs wide, and a condensed face fits the long ones on
   fewer lines.

   Self-hosted: no CDN, no third-party DNS, nothing for a privacy review to
   object to.

   The files are NOT in the repository — see assets/fonts/README.md for the
   download links. Until they are present these rules simply fail and the stacks
   fall through to system fonts, so nothing breaks; the page just looks like it
   did before. font-display: swap means text is never invisible.

   The unicode-range split matters: the latin-ext files carry the Albanian and
   wider Balkan characters, and are only fetched when such a character is used.
   ======================================================================== */
@font-face {
  font-family: "Saira Condensed";
  src: url("../fonts/saira-condensed-latin-800.woff2") format("woff2");
  font-weight: 800;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+2000-206F,
                 U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215;
}
@font-face {
  font-family: "Saira Condensed";
  src: url("../fonts/saira-condensed-latin-ext-800.woff2") format("woff2");
  font-weight: 800;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+1E00-1EFF,
                 U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
                 U+A720-A7FF;
}

@font-face {
  font-family: "Inter Tight";
  src: url("../fonts/inter-tight-latin.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+2000-206F,
                 U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215;
}
@font-face {
  font-family: "Inter Tight";
  src: url("../fonts/inter-tight-latin-ext.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+1E00-1EFF,
                 U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
                 U+A720-A7FF;
}

/* 1 — Design tokens ======================================================= */
:root {
  /* Brand palette taken from the logo: graphite and gold. Two hues plus
     neutrals, nothing else. (Was navy — corrected once the real logo arrived.) */
  --ink-950: #08080a;
  --ink-900: #121215;
  --ink-800: #1e1e22;
  --ink-700: #2b2b31;
  --ink-600: #3d3d45;
  --gold-600: #a67f2e;
  --gold-500: #c69c43;
  --gold-400: #d9b665;
  --gold-300: #e8ce96;

  --gray-50:  #f7f8fa;
  --gray-100: #eef1f5;
  --gray-200: #dfe4ea;
  --gray-300: #c8d0da;
  --gray-400: #9aa5b1;
  --gray-600: #5b6672;
  --gray-800: #2a323c;
  --white:    #ffffff;

  /* Semantic aliases — restyle the site by editing these, not the rules. */
  --color-text:        var(--gray-800);
  --color-text-muted:  var(--gray-600);
  --color-heading:     var(--ink-800);
  --color-bg:          var(--white);
  --color-bg-tint:     var(--gray-50);
  --color-border:      var(--gray-200);
  --color-accent:      var(--gold-500);

  /* Type: fluid scale, no external font request (self-hosted woff2 in Phase 8). */
  --font-sans: "Inter Tight", "Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
  /* NOTE: no semibold family named in the fallback. Naming one pins the
     fallback at weight 600, so font-weight: 800 has nothing to do and headings
     render LIGHTER before the webfont loads. Falling through to the regular
     sans lets the requested weight actually apply. */
  --font-display: "Saira Condensed", var(--font-sans);
  --font-mono: ui-monospace, "Cascadia Mono", "SF Mono", Menlo, Consolas, monospace;
  --fs-xs:   0.8125rem;
  --fs-sm:   0.9375rem;
  --fs-base: 1.0625rem;
  --fs-lg:   1.1875rem;
  --fs-xl:   clamp(1.25rem, 0.4vw + 1.15rem, 1.4375rem);
  --fs-2xl:  clamp(1.5rem, 1vw + 1.25rem, 2rem);
  /* Nudged back up now the display face is condensed. A condensed 800 occupies
     roughly a fifth less width than the previous face at the same point size,
     so it can be set larger without the headlines running long — and larger is
     where a heavy condensed face earns its presence. */
  --fs-3xl:  clamp(1.75rem, 1.7vw + 1.25rem, 2.5rem);
  --fs-4xl:  clamp(2.25rem, 3.2vw + 1.2rem, 3.75rem);

  /* Spacing: 4px base, used everywhere so vertical rhythm stays consistent. */
  --sp-1: 0.25rem;  --sp-2: 0.5rem;   --sp-3: 0.75rem;  --sp-4: 1rem;
  --sp-5: 1.5rem;   --sp-6: 2rem;     --sp-7: 2.5rem;   --sp-8: 3rem;
  --sp-9: 4rem;     --sp-10: 5rem;    --sp-11: 6rem;

  --container: 76rem;
  /* Near-square. Rounded corners read as consumer software; equipment cases,
     armour plate and ammunition boxes have hard edges, and the whole page reads
     differently for this one pair of values. */
  --radius:    2px;
  --radius-lg: 3px;
  --bracket:   16px;  /* arm length of the corner registration marks on cards */

  --shadow-sm: 0 1px 2px rgb(8 8 10 / 6%), 0 1px 3px rgb(8 8 10 / 8%);
  --shadow-md: 0 4px 12px rgb(8 8 10 / 8%), 0 2px 4px rgb(8 8 10 / 4%);
  --shadow-lg: 0 18px 40px rgb(8 8 10 / 14%), 0 6px 12px rgb(8 8 10 / 7%);

  /* Tall enough for the full lockup, including the tagline. Collapses to
     --header-h-min once the page is scrolled. */
  --header-h: 96px;
  --header-h-min: 70px;
  --logo-h: 62px;
  --logo-h-min: 44px;
  --ease:     cubic-bezier(0.4, 0, 0.2, 1);
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}

/* 2 — Reset =============================================================== */
*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-h) + var(--sp-4));
}

body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--fs-base);
  line-height: 1.65;
  color: var(--color-text);
  background: var(--color-bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* Let a variable font pick the right optical size, and turn on the contextual
     alternates that keep letter pairs even at display sizes. */
  font-optical-sizing: auto;
  font-synthesis-weight: none;
  font-feature-settings: "cv11", "calt" 1;
}
/* Note: deliberately no overflow-x:hidden on body — it would create a scroll
   container and break the sticky header. Everything wide (the ticker track, the
   hero glows) is clipped by its own overflow:hidden parent instead. */

h1, h2, h3, h4, p, ul, ol, dl, dd, figure, blockquote { margin: 0; }
ul, ol { padding: 0; list-style: none; }
img, svg, video { display: block; max-width: 100%; height: auto; }
button { font: inherit; color: inherit; }

a {
  color: var(--ink-700);
  text-decoration-color: color-mix(in srgb, var(--ink-700) 35%, transparent);
  text-underline-offset: 0.2em;
  transition: color 0.15s var(--ease);
}
a:hover { color: var(--gold-600); }

:focus-visible {
  outline: 2px solid var(--gold-500);
  outline-offset: 3px;
  border-radius: 2px;
}

.svg-sprite { display: none; }

/* Small details that cost nothing and are noticed when missing. */
::selection { background: var(--gold-500); color: var(--ink-950); }

html { scrollbar-width: thin; scrollbar-color: var(--gray-300) transparent; }
::-webkit-scrollbar { width: 11px; height: 11px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--gray-300);
  border-radius: 99px;
  border: 3px solid var(--white);
}
::-webkit-scrollbar-thumb:hover { background: var(--gray-400); }

/* ---- Cross-document view transitions ------------------------------------
   Two declarations turn every page change into an animated transition instead
   of a white flash. Browsers without support simply navigate as before, and
   naming the header keeps it visually anchored while the body crossfades. */
@view-transition { navigation: auto; }

@keyframes vt-out { to   { opacity: 0; transform: translateY(-10px); } }
@keyframes vt-in  { from { opacity: 0; transform: translateY(14px);  } }

::view-transition-old(root) { animation: vt-out 0.25s var(--ease) both; }
::view-transition-new(root) { animation: vt-in 0.4s var(--ease-out) both; }

.skip-link {
  position: absolute;
  left: var(--sp-4);
  top: var(--sp-4);
  z-index: 200;
  padding: var(--sp-3) var(--sp-4);
  background: var(--ink-800);
  color: var(--white);
  font-size: var(--fs-sm);
  font-weight: 600;
  border-radius: var(--radius);
  text-decoration: none;
  transform: translateY(-200%);
  transition: transform 0.2s var(--ease);
}
.skip-link:focus { transform: translateY(0); color: var(--white); }

/* 3 — Typography ========================================================== */
/* Headings carry the display face. Body copy and lists stay on the body sans —
   an angular face with cut corners is unreadable at 17px running text. */
h1, h2, h3,
.hero__title, .section__title {
  font-family: var(--font-display);
  color: var(--color-heading);
  /* 800 — the weight actually downloaded. Asking for 700 would leave the
     browser matching the nearest face, and with font-synthesis disabled the
     result is unpredictable across engines. */
  font-weight: 800;
  line-height: 1.12;
  letter-spacing: 0;
  text-wrap: balance;
}

/*
   Uppercase headline layer — the hero, page headers, section titles and the
   closing band. This is what makes the page agree with the logo, which is
   uppercase in both the wordmark and the tagline.

   It stops there deliberately. Card and step titles (h3) stay sentence case:
   uppercase removes ascenders and descenders, and word shape is most of how we
   read. A title read once, at 40px, loses little; six card titles scanned
   across a grid lose a lot.

   Tracking goes POSITIVE here. Uppercase needs air between letters — the
   negative tracking that suits mixed case makes caps look jammed together.
   Leading tightens, because there are no descenders to clear.
*/
.hero__title,
.page-hero__title,
.section__title,
.cta-band__title,
.page-head__title {
  text-transform: uppercase;
  /* Condensed caps sit close together by construction, so they need MORE air
     than a normal-width face would — the opposite of the usual instinct. */
  letter-spacing: 0.03em;
  font-weight: 800;
}

/* Small monospace label with a gold rule — the one repeating brand flourish. */
.eyebrow {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-bottom: var(--sp-4);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--gold-600);
}
.eyebrow::before {
  content: "";
  width: 28px;
  height: 2px;
  flex: none;
  background: var(--gold-500);
}
.eyebrow--light { color: var(--gold-400); }
.eyebrow--light::before { background: var(--gold-400); }

/* Numbered variant: data-n="01" turns the gold rule into a section reference
   followed by a vertical divider — "01 | WHAT WE SUPPLY". This is what replaced
   the fixed index rail: the same document-like structure, sitting inline with
   the content instead of as a permanent overlay, and it costs no JavaScript. */
.eyebrow[data-n]::before {
  content: attr(data-n);
  width: auto;
  height: auto;
  padding-right: var(--sp-3);
  background: none;
  border-right: 2px solid var(--gold-500);
  font-variant-numeric: tabular-nums;
  color: inherit;
}
.eyebrow--light[data-n]::before { border-right-color: var(--gold-400); }

.prose {
  max-width: 62ch;
  color: var(--color-text-muted);
  font-size: var(--fs-lg);
}
.prose + .prose { margin-top: var(--sp-4); }

.icon {
  width: 24px;
  height: 24px;
  flex: none;
  stroke: currentColor;
  fill: none;
}

/* 4 — Layout ============================================================== */
.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--sp-5);
}
.container.narrow { max-width: 46rem; }
.center { text-align: center; }

.section { padding-block: clamp(var(--sp-9), 7vw, var(--sp-11)); }
.section--tint {
  background: var(--color-bg-tint);
  border-block: 1px solid var(--color-border);
}
.section--tall { padding-block: var(--sp-11); }

/* Dark band, used to break up the vertical rhythm. */
.section--dark {
  position: relative;
  overflow: hidden;
  /* Hairline at the foot of every dark band. With several dark sections now
     stacked, without this they merge into one undifferentiated block and the
     page loses its structure. */
  border-bottom: 1px solid rgb(255 255 255 / 8%);
  background: var(--ink-900);
  background-image:
    radial-gradient(70% 100% at 20% 0%, rgb(190 180 165 / 12%), transparent 60%),
    radial-gradient(50% 80% at 100% 100%, rgb(198 156 67 / 14%), transparent 60%);
  color: rgb(255 255 255 / 74%);
}
.section--dark .section__title { color: var(--white); }

/*
   Tonal levels, so a dark-dominant page does not read as one flat black. The
   page steps between them rather than repeating one value:

     hero        ink-950  deepest
     stats       ink-900
     --dark      ink-900 + blue/gold wash   (products, range, process, why)
     --deep      ink-950 + tighter wash     (references)
     cta         gradient across ink-900 → ink-800

   Light sections (services, about, contact) are the relief. Without them the
   page becomes a tunnel; three of them is enough to keep it breathing.
*/
.section--deep {
  background: var(--ink-950);
  background-image:
    radial-gradient(60% 90% at 80% 0%, rgb(190 180 165 / 9%), transparent 62%),
    radial-gradient(50% 70% at 0% 100%, rgb(198 156 67 / 8%), transparent 60%);
}

/*
   Survey marks for the References section — the only dark band that had no
   texture at all, so it read as a gap between the radar above and the hatched
   closing band below.

   Three crosshair reticles at fixed points rather than a repeating field. This
   section is about specific delivered contracts, so marked positions say more
   than a pattern would — and three sparse marks cannot turn into the "too many
   lines" problem.

   Deliberately static. There are already three rotating things on this page;
   a fourth would start to look like a screensaver.
*/
.section--deep::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.5;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%23c69c43' stroke-width='1'%3E%3Ccircle cx='12' cy='12' r='6.5'/%3E%3Cpath d='M12 0v5M12 19v5M0 12h5M19 12h5'/%3E%3C/g%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%23c69c43' stroke-width='1'%3E%3Ccircle cx='12' cy='12' r='6.5'/%3E%3Cpath d='M12 0v5M12 19v5M0 12h5M19 12h5'/%3E%3C/g%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%23c69c43' stroke-width='1'%3E%3Ccircle cx='12' cy='12' r='6.5'/%3E%3Cpath d='M12 0v5M12 19v5M0 12h5M19 12h5'/%3E%3C/g%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-size: 34px 34px, 26px 26px, 30px 30px;
  background-position: 8% 22%, 88% 16%, 71% 86%;
}
.section--deep > .container { position: relative; z-index: 1; }

/*
   Stepping the products band through the dark tones was not enough — at
   #1e1e22 it still read as part of the same dark mass as the hero above it.
   It is now a light section (.section--tint), which is the only change that
   separates the two decisively.

   It also suits the content better than the dark version did: the product
   photographs are dark images, and they carry far more punch against a light
   ground than they did against a dark one.

   The page is still dark-dominant — hero, stats, process, why, references and
   the closing band. This is the relief, and it starts here.
*/

/*
   Radar ground — for the Why Pricom section. Light, with the one moving
   background on the page that means something: a detection sweep.

   The company supplies counter-drone and observation equipment, so range rings
   and a rotating sweep are its own subject matter rather than borrowed sci-fi.
   Placed off to the top right, where the section head does not reach and the
   cards below do not cover it.

   Deliberately slow — 16 seconds for a full revolution. Anything faster reads
   as a loading spinner.
*/
.section--radar {
  position: relative;
  overflow: hidden;
  border-block: 1px solid var(--color-border);
  background-color: var(--color-bg-tint);
}

/* Static range rings. */
.section--radar::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    repeating-radial-gradient(circle at 86% 24%, transparent 0 58px, rgb(198 156 67 / 18%) 58px 59px);
  -webkit-mask-image: radial-gradient(48% 72% at 86% 24%, #000 12%, transparent 78%);
  mask-image: radial-gradient(48% 72% at 86% 24%, #000 12%, transparent 78%);
}

/* The sweep. A conic gradient on a circle, spun with the independent `rotate`
   property so it composes with the centring `translate` instead of overwriting
   it — a `transform: rotate()` here would cancel the offset and throw the
   circle back to the corner. */
.section--radar::after {
  content: "";
  position: absolute;
  top: 24%;
  left: 86%;
  z-index: 0;
  width: min(58rem, 150vw);
  aspect-ratio: 1;
  translate: -50% -50%;
  border-radius: 50%;
  pointer-events: none;
  background: conic-gradient(
    from 0deg,
    rgb(198 156 67 / 26%) 0deg,
    rgb(198 156 67 / 8%) 32deg,
    transparent 62deg,
    transparent 360deg
  );
  -webkit-mask-image: radial-gradient(circle, #000 10%, transparent 66%);
  mask-image: radial-gradient(circle, #000 10%, transparent 66%);
  animation: radar-sweep 16s linear infinite;
}
.section--radar > .container { position: relative; z-index: 1; }

/*
   Field ground — for the About section. Light.

   Two earlier attempts here were wrong in the same way: both answered "make it
   more military" by adding pattern — a ruled blueprint grid, then contour
   rings. Pattern on a light panel behind body copy reads as busy, not as
   premium, and it fights the two photographs that are supposed to carry the
   section.

   This does it with restraint instead: a two-stop colour wash for depth, film
   grain so the surface is not flat paper, and gold registration marks at the
   four corners. Eight short strokes of decoration in total. The military
   register comes from precision and the palette, not from covering the panel.
*/
.section--field {
  position: relative;
  overflow: hidden;
  border-block: 1px solid var(--color-border);
  background-color: var(--color-bg-tint);
  /* A two-stop wash rather than a flat fill: warm gold entering from the top
     left, a cool graphite weight settling bottom right. Depth without pattern. */
  background-image:
    radial-gradient(78% 68% at 6% 0%, rgb(198 156 67 / 13%), transparent 62%),
    radial-gradient(65% 85% at 100% 100%, rgb(8 8 10 / 7%), transparent 64%);
}

/* Film grain, the same layer the hero uses. This is what stops a large light
   panel reading as flat printer paper — it gives the surface a texture without
   drawing a single line on it. */
.section--field::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.05;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* Registration marks at the four corners of the section — the same device as
   the hero frame and the product tiles. Eight short strokes in total, which is
   the whole point: the military language here comes from a few precise marks,
   not from covering the panel in ruling. */
.section--field::after {
  content: "";
  position: absolute;
  inset: clamp(1rem, 2.5vw, 2.25rem);
  z-index: 0;
  pointer-events: none;
  opacity: 0.5;
  --arm: 22px;
  --hair: 2px;
  background:
    linear-gradient(var(--gold-500) 0 0) left  top   / var(--arm) var(--hair) no-repeat,
    linear-gradient(var(--gold-500) 0 0) left  top   / var(--hair) var(--arm) no-repeat,
    linear-gradient(var(--gold-500) 0 0) right top   / var(--arm) var(--hair) no-repeat,
    linear-gradient(var(--gold-500) 0 0) right top   / var(--hair) var(--arm) no-repeat,
    linear-gradient(var(--gold-500) 0 0) left  bottom/ var(--arm) var(--hair) no-repeat,
    linear-gradient(var(--gold-500) 0 0) left  bottom/ var(--hair) var(--arm) no-repeat,
    linear-gradient(var(--gold-500) 0 0) right bottom/ var(--arm) var(--hair) no-repeat,
    linear-gradient(var(--gold-500) 0 0) right bottom/ var(--hair) var(--arm) no-repeat;
}
.section--field > .container { position: relative; z-index: 1; }

/*
   Compass rose, turning once every three minutes.

   Taken from the mark between PRICOM and its tagline in the logo, so the device
   is the company's own rather than generic military dressing. Navigation and
   bearing are the register here — the radar sweep in Why Pricom is detection,
   and the two should not be the same idea twice.

   Bled off the bottom-left corner so most of it sits outside the canvas and
   only the upper quadrant shows, beneath the checklist and button. At 5.5%
   opacity it is texture, not an illustration, and body copy reads straight
   over it.

   Three minutes per revolution is deliberate: fast enough that the section is
   not static, slow enough that you never catch it moving.
*/
.section__emblem {
  position: absolute;
  left: -16%;
  bottom: -28%;
  z-index: 0;
  width: min(38rem, 85vw);
  aspect-ratio: 1;
  pointer-events: none;
  opacity: 0.055;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Cg fill='none' stroke='%23080810' stroke-width='1.5'%3E%3Ccircle cx='100' cy='100' r='94'/%3E%3Ccircle cx='100' cy='100' r='70'/%3E%3Cpath d='M33.4 33.4l12 12M166.6 33.4l-12 12M33.4 166.6l12-12M166.6 166.6l-12-12'/%3E%3C/g%3E%3Cpath d='M100 4 110 90 196 100 110 110 100 196 90 110 4 100 90 90Z' fill='%23080810'/%3E%3C/svg%3E") center / contain no-repeat;
  animation: emblem-turn 180s linear infinite;
}

/* Faint dot matrix, the same measured-surface texture as the hero. Masked so it
   fades before the section edges and never turns into visible banding.
   .section--grid can sit on a light section too, so it carries its own
   position/overflow rather than relying on .section--dark for them. */
.section--grid {
  position: relative;
  overflow: hidden;
}
.section--grid::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  /* Dark dots by default — the light-section case. White dots are the override
     below, not the other way round, so a plain .section--grid is never invisible. */
  background-image: radial-gradient(circle at center, rgb(8 8 10 / 7%) 1px, transparent 1.6px);
  background-size: 30px 30px;
  -webkit-mask-image: radial-gradient(80% 70% at 50% 40%, #000 5%, transparent 78%);
  mask-image: radial-gradient(80% 70% at 50% 40%, #000 5%, transparent 78%);
}
.section--dark.section--grid::before {
  background-image: radial-gradient(circle at center, rgb(255 255 255 / 9%) 1px, transparent 1.6px);
}
/* Content clears the texture on light sections as well as dark ones. */
.section--grid > .container { position: relative; z-index: 1; }

/* Reconnaissance UAVs -----------------------------------------------------
   Four aircraft crossing the home page's product section at four altitudes, on
   thin contrails.

   Four rather than one because the band is very wide: a single aircraft left
   most of it empty at any given moment. Every per-aircraft value is set from
   the markup — altitude, size, duration, delay, opacity — so the flight is
   tuned in one array in the view rather than by editing selectors here.

   The fastest is the fastest motif on the site by a wide margin: 20s edge to
   edge, against the Services drone's 46 and the Contact vehicle's 50. Those two
   are heavy things moving at their own weight; these are aircraft at altitude,
   and the speed is what says so. */
.uav {
  position: absolute;
  top: var(--top, 13%);
  left: 0;
  z-index: 0;
  width: var(--w, 7rem);
  pointer-events: none;
  color: var(--gold-600);
  opacity: var(--op, 0.28);
  animation: uav-cross var(--dur, 24s) linear var(--delay, 0s) infinite;
}
/*
   A straight crossing with no hold. A single aircraft wanted a pause at the end
   — without one it reappeared instantly and read as traffic rather than as one
   pass. With four at four speeds there is already variation, and holds would
   only open gaps in a band that is meant to feel occupied.

   -420% clears the left edge including the contrail, which extends three widths
   behind the aircraft. All the offsets are self-relative, so this holds at every
   size in the array.

   On `transform` rather than `left`, as with every other travelling motif:
   animating `left` relayouts the band on every frame for as long as the page is
   open — and here that would be four elements doing it at once.
*/
@keyframes uav-cross {
  from { transform: translateX(-420%); }
  to   { transform: translateX(calc(100vw + 6rem)); }
}
.uav__craft { display: block; width: 100%; height: auto; }

/* Contrail, extending back from the tail and fading out. It does more work than
   the aircraft: at this size and opacity the silhouette alone reads as a mark
   on the page, and the trail is what makes it read as travelling.

   Sized as a percentage of the aircraft, not in rem — a fixed length would give
   the small, distant one the same trail as the large, near one and flatten the
   depth the array is trying to build. */
.uav__trail {
  position: absolute;
  top: 50%;
  right: 88%;
  width: 300%;
  height: 2px;
  translate: 0 -50%;
  background: linear-gradient(90deg, transparent, currentColor);
  opacity: 0.4;
}

/* Ground band -------------------------------------------------------------
   An armored vehicle with a quadcopter running overwatch above it, along the
   bottom of the home page's product section.

   Both are product lines, so neither claims anything the company does not
   carry, and putting them in the same band makes them read as one scene rather
   than two separate decorations.

   The section buys the room for this: .section--fleet adds to the bottom
   padding, because .tile is fully opaque and anything placed behind the tile
   grid is invisible. The whole band lives in that added space. */
.section--fleet {
  padding-bottom: calc(clamp(var(--sp-9), 7vw, var(--sp-11)) + 6.5rem);
}
.fleet {
  --gline: 2.8rem; /* height of the horizon above the section's bottom edge */

  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 12rem;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  color: var(--gold-600);
}

/* Static horizon with scale marks below it. Separate from the vehicle because
   it must stay still while the vehicle travels over it. */
.fleet__horizon {
  position: absolute;
  left: 0;
  right: 0;
  bottom: var(--gline);
  height: 6px;
  opacity: 0.3;
  background-image:
    linear-gradient(currentColor 0 0),
    repeating-linear-gradient(90deg, currentColor 0 1px, transparent 1px 26px);
  background-repeat: no-repeat, repeat-x;
  background-size: 100% 1px, 26px 4px;
  background-position: left top, left bottom;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}

/*
   The vehicle. svg-apc.php is 240x96 with its ground line at y=78 — 18 units up
   from the bottom. Rendered 4.4rem tall that is (18/96) x 4.4rem = 0.825rem, so
   the wrapper sits that far BELOW the horizon for the wheels to land on it.
   Change the height and this number has to change with it.
*/
.fleet__apc {
  position: absolute;
  left: 0;
  bottom: calc(var(--gline) - 0.825rem);
  width: 11rem;
  opacity: 0.3;
  /* Slowest thing in the band — it is also the heaviest. */
  animation:
    fleet-cross 52s linear -8s infinite,
    patrol-bob 1.7s ease-in-out infinite;
}
.fleet__apc-craft { display: block; width: 100%; height: auto; }

/* Overwatch. Faster than the vehicle and higher up, so it overtakes — which is
   what makes the pair read as escort rather than as two things that happen to
   be moving. */
.fleet__drone {
  position: absolute;
  left: 0;
  bottom: 7.4rem;
  width: 5.2rem;
  opacity: 0.22;
  animation:
    fleet-cross 30s linear -17s infinite,
    drone-bob 6.5s ease-in-out infinite;
}
.fleet__drone-craft { display: block; width: 100%; height: auto; }

/* Shared by both: self-relative start, so each clears the left edge whatever
   its width. transform, not left — two elements relayouting the section on
   every frame for as long as the page is open is not a trade worth making. */
@keyframes fleet-cross {
  from { transform: translateX(calc(-100% - 3rem)); }
  to   { transform: translateX(calc(100vw + 3rem)); }
}

/* Below the desktop breakpoint the heading and the stacked tiles fill the band
   and the motifs would cross straight through the content. The horizon stays as
   a baseline under the section. */
@media (max-width: 47.99rem) {
  .uav, .fleet__apc, .fleet__drone { display: none; }
  .section--fleet { padding-bottom: clamp(var(--sp-9), 7vw, var(--sp-11)); }
  .fleet { height: 3rem; }
}

/* Ambient light that follows the cursor across the whole band. */
.section--dark::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.6s var(--ease);
  background: radial-gradient(
    30rem circle at var(--mx, 50%) var(--my, 50%),
    rgb(198 156 67 / 9%),
    transparent 62%
  );
}
.section--dark:hover::after { opacity: 1; }
/* Keep the content above the light layer. */
.section--dark > .container { position: relative; z-index: 1; }

/*
   Optional extra layer for a dark band: two soft gold fields drifting slowly
   against each other, which stops a large dark section reading as a flat slab.

   Its own element rather than a third pseudo — .section--dark already spends
   ::after on the cursor spotlight and .section--grid spends ::before on the dot
   matrix, and this has to be able to sit on top of both.

   Ambient on purpose: no edges, no object, nothing that resolves into a shape.
   It is the ground the content sits on, not something happening on the page.
*/
.section__wash {
  position: absolute;
  /* Overscanned, so the drift can never pull an edge of the gradient into view. */
  inset: -20%;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(48% 44% at 22% 18%, rgb(198 156 67 / 15%), transparent 70%),
    radial-gradient(44% 40% at 80% 80%, rgb(198 156 67 / 11%), transparent 72%);
  animation: wash-drift 38s ease-in-out infinite alternate;
}
@keyframes wash-drift {
  from { translate: -2.5% -1.5%; }
  to   { translate: 2.5% 1.5%; }
}

.section__head { margin-bottom: var(--sp-8); max-width: 46rem; }
.section__title { font-size: var(--fs-3xl); line-height: 1.04; }
.section__intro {
  margin-top: var(--sp-4);
  font-size: var(--fs-lg);
  color: var(--color-text-muted);
  max-width: 60ch;
}
.section--dark .section__intro { color: rgb(255 255 255 / 70%); }

.section__head--row {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  justify-content: space-between;
  gap: var(--sp-5);
  max-width: none;
}

.split {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--sp-8);
  align-items: start;
}
@media (min-width: 62rem) {
  .split { grid-template-columns: 1.35fr 1fr; gap: var(--sp-10); }
}

/* 5 — Motion ============================================================== */
/*
   Scroll reveal. The hidden state is scoped to .js so that without JavaScript
   nothing is ever hidden — main.js adds .is-visible via IntersectionObserver.
   Only opacity and transform are animated, both compositor-only properties, so
   this stays cheap on low-end phones.
   --d staggers siblings; set it inline in the view.
*/
/*
   IMPORTANT: reveal uses the independent `translate` and `rotate` properties,
   never `transform`.

   `.js [data-reveal].is-visible` is a 3-class selector. If it set
   `transform: none`, it would outrank `.tile:hover` (2 classes) and every
   other hover/tilt rule on the same elements — silently disabling them. Using
   the independent properties keeps `transform` free for hover and pointer tilt,
   and the two compose instead of fighting.

   No will-change: ~40 elements each promoted to their own compositor layer
   costs real memory on low-end phones, and these properties are composited
   anyway.
*/
/*
   Reveal is a clean rise and fade — no 3D rotation. Cards tipping in perspective
   drew attention to the animation rather than the content, which is the wrong
   trade for a page whose job is to look composed and credible.

   It still uses the independent `translate` property, never `transform`:
   `.js [data-reveal].is-visible` is a 3-class selector and would otherwise
   outrank `.tile:hover` and silently disable every hover effect.
*/
.js [data-reveal] {
  opacity: 0;
  translate: 0 24px;
  transition:
    opacity 0.7s var(--ease-out) var(--d, 0ms),
    translate 0.8s var(--ease-out) var(--d, 0ms);
}
.js [data-reveal].is-visible {
  opacity: 1;
  translate: none;
}

@keyframes ticker-scroll {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}

/* Two large blurred blobs drifting behind the hero. */
@keyframes drift-a {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  50%      { transform: translate3d(6%, 8%, 0) scale(1.12); }
}
@keyframes drift-b {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1.05); }
  50%      { transform: translate3d(-8%, -6%, 0) scale(1); }
}

@keyframes nudge-down {
  0%, 100% { transform: translateY(0); opacity: 0.55; }
  50%      { transform: translateY(6px); opacity: 1; }
}

/* Icons redraw their own outline. Works because every shape in the sprite is
   declared pathLength="100", so one offset value fits all of them.
   stroke-dasharray/-dashoffset are inherited SVG properties, which is how they
   reach through <use> into the referenced symbol. */
@keyframes icon-draw {
  from { stroke-dashoffset: 100; }
  to   { stroke-dashoffset: 0; }
}

/* Slow band of light crossing the hero. */
@keyframes hero-sweep {
  0%   { transform: translateX(-70%) skewX(-14deg); opacity: 0; }
  12%  { opacity: 1; }
  88%  { opacity: 1; }
  100% { transform: translateX(300%) skewX(-14deg); opacity: 0; }
}

@keyframes radar-sweep { to { rotate: 360deg; } }
@keyframes emblem-turn { to { rotate: 360deg; } }

/* Corner registration marks fading in after the headline has landed. */
@keyframes frame-in {
  from { opacity: 0; }
  to   { opacity: 0.45; }
}

/* Headline words flipping up out of their own clipping box, hinged on their
   baseline. The perspective lives on .word so each word has its own camera and
   the row does not shear as a single plane. */
@keyframes word-up {
  from { transform: translateY(105%) rotateX(48deg); }
  to   { transform: translateY(0) rotateX(0deg); }
}

/* Generic entrance for hero elements that are always above the fold, so they
   animate on load rather than waiting for an IntersectionObserver. */
@keyframes fade-up {
  from { opacity: 0; transform: translateY(20px); }
  to   { opacity: 1; transform: none; }
}

/* ---- Pointer-driven effects -------------------------------------------- */
/*
   --mx/--my are pointer coordinates written by main.js, --tx/--ty are the
   magnetic-button offsets. All are plain custom properties, so the browser
   only recomputes paint for the one element under the cursor.
   Everything here is opt-in: main.js attaches these only on devices with a
   fine pointer, so touch screens never pay for them.
*/
.scroll-progress {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 150;
  height: 3px;
  background: rgb(8 8 10 / 8%);
  pointer-events: none;
}
.scroll-progress span {
  display: block;
  height: 100%;
  background: linear-gradient(90deg, var(--gold-600), var(--gold-400));
  transform: scaleX(var(--p, 0));
  transform-origin: left;
}

/* Cursor spotlight shared by [data-spotlight] cards.
   The selector list has to include every component that carries the attribute —
   main.js writes --mx/--my onto any [data-spotlight] element, but nothing is
   drawn unless the component is named here. */
.tile::after,
.feature::after,
.contact-card::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: radial-gradient(
    16rem circle at var(--mx, 50%) var(--my, 50%),
    rgb(198 156 67 / 13%),
    transparent 62%
  );
  opacity: 0;
  transition: opacity 0.35s var(--ease);
  pointer-events: none;
}
.feature { position: relative; }
.tile:hover::after,
.feature:hover::after,
.contact-card:hover::after { opacity: 1; }

/* Back to top ------------------------------------------------------------ */
.to-top {
  position: fixed;
  right: var(--sp-5);
  bottom: var(--sp-5);
  z-index: 90;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 46px;
  height: 46px;
  border-radius: var(--radius);
  background: var(--ink-800);
  color: var(--white);
  box-shadow: var(--shadow-lg);
  opacity: 0;
  visibility: hidden;
  transform: translateY(12px);
  transition: opacity 0.3s var(--ease), transform 0.3s var(--ease),
              visibility 0.3s, background-color 0.2s var(--ease);
}
.to-top .icon { transform: rotate(180deg); }
.to-top:hover { background: var(--gold-500); color: var(--ink-950); }
.to-top.is-visible { opacity: 1; visibility: visible; transform: none; }
/* Without JavaScript it never gets .is-visible, so it stays out of the way. */

/* 5b — Photo grade ========================================================
   Photography will arrive from several manufacturers, all shot differently:
   white-background product shots, glossy brochure renders, phone photos from a
   warehouse. Dropped in raw they look like a scrapbook.

   Every image on the site therefore passes through one shared treatment —
   slight desaturation, lifted contrast, and a graphite-to-gold duotone at low
   opacity. Mixed sources then read as one deliberate set, which is the single
   thing that separates a considered brand from a stock-photo collage.

   Tune the whole site's look from these three custom properties.
   ======================================================================== */
:root {
  --grade-saturate: 0.78;
  --grade-contrast: 1.06;
  --grade-tint: 0.14;   /* strength of the gold/graphite duotone */
}

.grade {
  position: relative;
  overflow: hidden;
  background-color: var(--ink-900);
}
.grade > img,
.grade__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: saturate(var(--grade-saturate)) contrast(var(--grade-contrast));
}
/* The duotone. multiply darkens the shadows toward graphite, the gold radial
   warms the midtones — together they unify colour casts between sources. */
.grade::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: var(--grade-tint);
  background:
    radial-gradient(120% 100% at 20% 0%, rgb(198 156 67 / 65%), transparent 60%),
    linear-gradient(180deg, transparent, rgb(8 8 10 / 85%));
  mix-blend-mode: multiply;
}

/* 6 — Components ========================================================== */

/* Buttons ---------------------------------------------------------------- */
.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  padding: 0.75rem 1.375rem;
  border: 1px solid transparent;
  border-radius: var(--radius);
  font-size: var(--fs-sm);
  font-weight: 600;
  letter-spacing: 0.01em;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  overflow: hidden;
  /* --tx/--ty are the magnetic offsets written by main.js on fine-pointer
     devices. Folding them into the base transform keeps the hover lift and the
     magnetic pull from overwriting each other. */
  transform: translate3d(var(--tx, 0px), var(--ty, 0px), 0);
  transition: background-color 0.18s var(--ease), border-color 0.18s var(--ease),
              color 0.18s var(--ease), transform 0.18s var(--ease),
              box-shadow 0.18s var(--ease);
}
.btn:hover { transform: translate3d(var(--tx, 0px), calc(var(--ty, 0px) - 2px), 0); }
/* Presses into the page rather than just returning to rest. */
.btn:active {
  transform: translate3d(var(--tx, 0px), var(--ty, 0px), 0) scale(0.975);
  transition-duration: 0.08s;
}
.btn .icon { width: 18px; height: 18px; transition: transform 0.22s var(--ease); }
.btn:hover .icon { transform: translateX(3px); }

/* A soft sheen wipes across the button on hover. Purely decorative. */
.btn::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(105deg, transparent 35%, rgb(255 255 255 / 28%) 50%, transparent 65%);
  transform: translateX(-120%);
  transition: transform 0.6s var(--ease-out);
  pointer-events: none;
}
.btn:hover::after { transform: translateX(120%); }

.btn--primary { background: var(--ink-800); color: var(--white); }
.btn--primary:hover { background: var(--ink-700); color: var(--white); box-shadow: var(--shadow-md); }

.btn--accent { background: var(--gold-500); color: var(--ink-950); }
.btn--accent:hover {
  background: var(--gold-400);
  color: var(--ink-950);
  box-shadow: 0 10px 24px rgb(198 156 67 / 32%);
}

.btn--outline { background: transparent; border-color: var(--gray-300); color: var(--ink-800); }
.btn--outline:hover { border-color: var(--ink-800); background: var(--white); color: var(--ink-800); }

/* On dark backgrounds only. */
.btn--ghost {
  background: rgb(255 255 255 / 8%);
  border-color: rgb(255 255 255 / 28%);
  color: var(--white);
  backdrop-filter: blur(2px);
}
.btn--ghost:hover { background: rgb(255 255 255 / 16%); border-color: rgb(255 255 255 / 55%); color: var(--white); }

.btn--sm { padding: 0.5rem 0.9375rem; font-size: var(--fs-xs); }
.btn--lg { padding: 0.9375rem 1.75rem; font-size: var(--fs-base); }

.stack-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  justify-content: center;
  margin-top: var(--sp-6);
}

/* Arrow link ------------------------------------------------------------- */
.link-arrow {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  margin-top: var(--sp-5);
  font-size: var(--fs-sm);
  font-weight: 700;
  color: var(--ink-800);
  text-decoration: none;
  letter-spacing: 0.01em;
}
.link-arrow .icon { width: 18px; height: 18px; transition: transform 0.2s var(--ease); }
.link-arrow:hover { color: var(--gold-600); }
.link-arrow:hover .icon { transform: translateX(4px); }

/* Tag -------------------------------------------------------------------- */
.tag {
  display: inline-block;
  padding: 0.1875rem 0.5rem;
  background: color-mix(in srgb, var(--ink-800) 8%, transparent);
  color: var(--ink-700);
  border-radius: 2px;
  font-family: var(--font-mono);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* Card grid -------------------------------------------------------------- */
.card-grid { display: grid; gap: var(--sp-5); }
@media (min-width: 40rem) { .card-grid--2, .card-grid--3 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 62rem) { .card-grid--3 { grid-template-columns: repeat(3, 1fr); } }

/* Tile card ---------------------------------------------------------------
   Used for product categories on the home page and for client types on the
   About page — hence the neutral name. */
.tile {
  position: relative;
  padding: var(--sp-6);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  /* Gold registration marks at opposite corners — the same drawing language as
     the hero frame, and the reason these read as equipment tiles rather than
     content cards. Four background gradients, so no extra markup.
     NOTE: hover must set background-COLOR, never the background shorthand, or
     these are wiped out. */
  background-color: var(--white);
  background-image:
    linear-gradient(var(--gold-500) 0 0), linear-gradient(var(--gold-500) 0 0),
    linear-gradient(var(--gold-500) 0 0), linear-gradient(var(--gold-500) 0 0);
  background-repeat: no-repeat;
  background-size:
    var(--bracket) 2px, 2px var(--bracket),
    var(--bracket) 2px, 2px var(--bracket);
  background-position: left top, left top, right bottom, right bottom;
  transition: border-color 0.25s var(--ease), box-shadow 0.25s var(--ease),
              background-color 0.25s var(--ease);
}
/*
   Cards do not move on hover — no lift, no tilt. Elevation is expressed by the
   shadow growing and the border warming, which reads as the card rising without
   anything actually shifting position. A grid of cards that all jump under the
   cursor is restless; a grid that lights up is not.

   The rest of the feedback carries it: the gold rule wiping across the top, the
   cursor spotlight, and the icon redrawing its own outline.
*/
.tile:hover {
  border-color: var(--gold-500);
  /* background-color, not the shorthand — see the note above. */
  background-color: color-mix(in srgb, var(--gold-500) 3%, var(--white));
  box-shadow: var(--shadow-lg);
}
/* Gold rule wipes across the top edge on hover. */
.tile::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 3px;
  background: linear-gradient(90deg, var(--gold-500), var(--gold-300));
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.35s var(--ease-out);
}
.tile:hover::before { transform: scaleX(1); }

/* Photograph, pulled flush to the tile edges through the padding. The negative
   margin is what stops it looking like an inset thumbnail. Rendered even when
   no photograph exists — the generated cover below keeps the grid even. */
.tile__media {
  display: block;
  position: relative;
  margin: calc(var(--sp-6) * -1) calc(var(--sp-6) * -1) var(--sp-6);
  aspect-ratio: 3 / 2;
  overflow: hidden;
  background:
    repeating-linear-gradient(135deg, rgb(255 255 255 / 4%) 0 1px, transparent 1px 12px),
    linear-gradient(150deg, var(--ink-800), var(--ink-950));
}

/* Icon as a badge on the photograph rather than a second symbol above the
   title. Scoped to .tile__media so the About page's icon-above-title tiles,
   which have no media block, are untouched. */
.tile__media .tile__icon {
  position: absolute;
  left: var(--sp-4);
  bottom: var(--sp-4);
  z-index: 2;
  width: 40px;
  height: 40px;
  margin: 0;
  background: rgb(8 8 10 / 72%);
  border: 1px solid rgb(255 255 255 / 16%);
  color: var(--white);
  backdrop-filter: blur(4px);
}
.tile:hover .tile__media .tile__icon {
  background: var(--gold-500);
  border-color: var(--gold-500);
  color: var(--ink-950);
}
.tile__media .tile__icon .icon { width: 20px; height: 20px; }
.tile__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Shared grade — see the photo grade section. These ten photographs come from
     different sources with different colour casts; one treatment unifies them. */
  filter: saturate(var(--grade-saturate)) contrast(var(--grade-contrast));
  transition: transform 0.6s var(--ease-out), filter 0.3s var(--ease);
}
.tile:hover .tile__media img {
  transform: scale(1.04);
  filter: saturate(0.95) contrast(var(--grade-contrast));
}
/* Duotone over the photo, and a fade into the tile so the type below sits on a
   clean edge rather than against busy image detail. */
.tile__media::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: var(--grade-tint);
  background:
    radial-gradient(120% 100% at 20% 0%, rgb(198 156 67 / 65%), transparent 60%),
    linear-gradient(180deg, transparent 40%, rgb(8 8 10 / 90%));
  mix-blend-mode: multiply;
}
.tile__media { position: relative; }

/* The "01 / 06" catalogue chip was removed — it implied a fixed catalogue size,
   which stops being true the moment a category is added. Section numbering in
   the eyebrows carries the same structure without the maintenance problem. */

.tile__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  margin-bottom: var(--sp-5);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--ink-800) 6%, transparent);
  color: var(--ink-700);
  transition: background-color 0.25s var(--ease), color 0.25s var(--ease),
              transform 0.25s var(--ease);
}
/* Colour change only — the icon does not shift either. Its feedback is the
   outline redrawing itself, below. */
.tile:hover .tile__icon {
  background: color-mix(in srgb, var(--gold-500) 18%, transparent);
  color: var(--gold-600);
}
/* Dash of the full nominal length = a solid line at rest; the keyframes then
   pull it back and let it draw itself in on hover. */
.tile__icon .icon { stroke-dasharray: 100; stroke-dashoffset: 0; }
.tile:hover .tile__icon .icon { animation: icon-draw 0.85s var(--ease-out); }

.feature:hover .feature__icon .icon,
.contact-card:hover .contact-card__icon { animation: icon-draw 0.85s var(--ease-out); }
.feature__icon .icon,
.contact-card__icon { stroke-dasharray: 100; stroke-dashoffset: 0; }
.tile__title { font-size: var(--fs-xl); }
.tile__text { margin-top: var(--sp-3); color: var(--color-text-muted); font-size: var(--fs-sm); }

/* The actual product names. These came back when the full range index moved to
   the Products page — without them the home page named no products at all, and
   "night vision" is exactly what someone arrives looking for.

   Mono, uppercase, gold square markers: the same micro-type as the hero meta
   row and the ticker, so the two sections read as one system. */
.tile__list {
  display: grid;
  gap: var(--sp-2);
  margin-top: var(--sp-5);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--color-border);
}
.tile__list li {
  position: relative;
  padding-left: var(--sp-4);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.tile__list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.45em;
  width: 4px;
  height: 4px;
  background: var(--gold-500);
}

/* Stretched link — the anchor covers the tile while the accessible name stays
   the title. z-index sits above the spotlight and photo layers so the whole
   card is clickable. */
.tile__link {
  color: inherit;
  text-decoration: none;
}
.tile__link::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 4;
}
.tile:hover .tile__link { color: var(--gold-600); }
.tile__link:focus-visible { outline: none; }
/* Focus ring belongs on the whole card, not on the title text alone. */
.tile:has(.tile__link:focus-visible) {
  outline: 2px solid var(--gold-500);
  outline-offset: 3px;
}

/* Feature card -------------------------------------------------------------
   Light by default, with a dark variant scoped to .section--dark further down.
   It used to be the other way round, which broke the moment this card was
   needed on a light section: white text on a pale ground is invisible, and the
   failure is silent. Light-by-default is the safer direction — the About page's
   "How we work" band still sits on .section--dark and picks up the override. */
/* Registering --angle as a real <angle> is what lets it be animated at all;
   without @property a custom property is just a string and jumps rather than
   interpolates. Unsupported browsers keep a static gradient edge, which still
   looks intentional. */
@property --angle {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}
@keyframes spin-angle { to { --angle: 360deg; } }

.feature {
  padding: var(--sp-6);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  background-color: var(--white);
  transition: background-color 0.25s var(--ease), border-color 0.25s var(--ease),
              box-shadow 0.25s var(--ease);
}
.feature:hover {
  background-color: color-mix(in srgb, var(--gold-500) 3%, var(--white));
  border-color: var(--gold-500);
  box-shadow: var(--shadow-md);
}

/* A gold light travelling around the card edge on hover. The two-layer mask
   punches out the middle, leaving only a 1px frame of the conic gradient. */
.feature::before {
  content: "";
  position: absolute;
  inset: -1px;
  padding: 1px;
  border-radius: inherit;
  background: conic-gradient(
    from var(--angle),
    transparent 55%,
    var(--gold-500) 78%,
    var(--gold-300) 85%,
    transparent 95%
  );
  -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);
  pointer-events: none;
}
.feature:hover::before {
  opacity: 1;
  animation: spin-angle 4s linear infinite;
}
.feature__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  margin-bottom: var(--sp-5);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--gold-500) 14%, transparent);
  color: var(--gold-600);
}
.feature__title { font-size: var(--fs-lg); letter-spacing: -0.01em; }
.feature__text { margin-top: var(--sp-3); font-size: var(--fs-sm); color: var(--color-text-muted); }

/* Project card ----------------------------------------------------------- */
.project {
  position: relative;
  background: var(--white);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  transition: border-color 0.25s var(--ease), box-shadow 0.25s var(--ease);
}
.project:hover {
  border-color: var(--gray-300);
  box-shadow: var(--shadow-lg);
}

/* Generated cover, standing in until real photography exists. */
.project__media {
  aspect-ratio: 16 / 10;
  background:
    repeating-linear-gradient(135deg, rgb(255 255 255 / 5%) 0 1px, transparent 1px 12px),
    linear-gradient(150deg, var(--ink-700), var(--ink-950));
  transition: transform 0.5s var(--ease-out);
}
/* Real photograph supplied — clears the generated pattern and sets the sizing.
   The image itself comes from the inline style on the element, which outranks
   this rule, so the shorthand reset here is safe. */
.project__media--photo {
  background: var(--ink-900) center / cover no-repeat;
  filter: saturate(var(--grade-saturate)) contrast(var(--grade-contrast));
}
/* Duotone over the cover, matching .grade — see the photo grade section. */
.project__media--photo::after {
  content: "";
  position: absolute;
  inset: 0;
  opacity: var(--grade-tint);
  background:
    radial-gradient(120% 100% at 20% 0%, rgb(198 156 67 / 65%), transparent 60%),
    linear-gradient(180deg, transparent, rgb(8 8 10 / 85%));
  mix-blend-mode: multiply;
}
.project__media { position: relative; }
.project:hover .project__media { transform: scale(1.04); }

/* Native scroll-driven parallax: the cover settles from slightly oversized to
   its resting size as the card travels up the viewport. No JavaScript and no
   scroll listener — the browser drives it off the scroll position directly.
   Uses the independent scale/translate properties rather than transform, so the
   hover zoom above still composes with it instead of being overwritten. */
@supports (animation-timeline: view()) {
  @keyframes media-drift {
    from { scale: 1.16; translate: 0 -3%; }
    to   { scale: 1;    translate: 0 0; }
  }
  .project__media {
    animation: media-drift linear both;
    animation-timeline: view();
    animation-range: entry 10% cover 55%;
  }
}

.project__body { padding: var(--sp-5) var(--sp-5) var(--sp-6); }
.project__meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  margin-bottom: var(--sp-4);
}
.project__title { font-size: var(--fs-lg); letter-spacing: -0.01em; }

/* Stretched link — the anchor names the card, its ::after makes the whole card
   clickable. z-index clears the cover photo and the spotlight layer. */
.project__link { color: inherit; text-decoration: none; }
.project__link::after { content: ""; position: absolute; inset: 0; z-index: 3; }
.project:hover .project__link { color: var(--gold-600); }
.project__link:focus-visible { outline: none; }
.project:has(.project__link:focus-visible) {
  outline: 2px solid var(--gold-500);
  outline-offset: 3px;
}
.project__text { margin-top: var(--sp-3); color: var(--color-text-muted); font-size: var(--fs-sm); }

/* Checklist ----------------------------------------------------------------
   An editorial ruled list, not a card. As a white panel with a border and a
   shadow it was the heaviest object in the column and broke the reading rhythm
   between the prose and the button. Ruled, it sits with the text and lets the
   images carry the visual weight — which is the point of this section. */
.checklist {
  display: grid;
  border-top: 1px solid var(--color-border);
}
/* Clears the .list-label above it — without this the label sits flush on the
   list's own top rule and reads as part of the first item. */
.checklist--labelled { margin-top: var(--sp-4); }
.checklist__item {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  padding-block: var(--sp-4);
  border-bottom: 1px solid var(--color-border);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--ink-800);
  transition: color 0.2s var(--ease);
}
.checklist__item:hover { color: var(--gold-600); }

/* The tick in a small gold chip — square, matching the markers in the product
   and service lists rather than introducing a third shape. */
.checklist__icon {
  flex: none;
  width: 26px;
  height: 26px;
  padding: 4px;
  background: color-mix(in srgb, var(--gold-500) 16%, transparent);
  color: var(--gold-600);
  stroke-width: 2.5;
}

/* Process timeline ------------------------------------------------------- */
.process {
  display: grid;
  gap: var(--sp-6);
  counter-reset: step;
}
@media (min-width: 48rem) { .process { grid-template-columns: repeat(2, 1fr); gap: var(--sp-8); } }
@media (min-width: 68rem) { .process { grid-template-columns: repeat(4, 1fr); } }

.process__step {
  position: relative;
  padding-top: var(--sp-6);
  border-top: 2px solid var(--color-border);
  transition: border-color 0.3s var(--ease);
}
.process__step:hover { border-top-color: var(--gold-500); }
/* Dot sitting on the rule. */
.process__step::before {
  content: "";
  position: absolute;
  top: -6px;
  left: 0;
  z-index: 1;
  width: 10px;
  height: 10px;
  /* Square node, not a dot — matches the markers in the capability lists. */
  background: var(--gold-500);
  transform: scale(0);
  transition: transform 0.4s var(--ease-out) var(--d, 0ms);
}
.js [data-reveal].is-visible.process__step::before,
html:not(.js) .process__step::before { transform: scale(1); }

/* Gold connector drawn over the grey rule. Because each step carries the next
   --d in the stagger, the four segments draw in sequence and read as one line
   travelling across the whole timeline rather than four separate borders. */
.process__step::after {
  content: "";
  position: absolute;
  top: -2px;
  left: 0;
  right: 0;
  height: 2px;
  background: linear-gradient(90deg, var(--gold-500), var(--gold-400));
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.85s var(--ease-out) var(--d, 0ms);
}
.js [data-reveal].is-visible.process__step::after,
html:not(.js) .process__step::after { transform: scaleX(1); }

/*
   Bridge the connector across the grid gap.

   Each segment was bounded by its own column, so with a 3rem gap between them
   the "timeline" was four separate rules with holes in it — the one thing a
   process diagram must not look like. Extending each segment into the gap makes
   the gold line travel the full width, while the grey track stays per-step and
   reads as the step's own header rule.

   The last item in each ROW must not extend, or the line runs off the edge —
   which is item 2 and 4 at two columns, and only item 4 at four.
*/
@media (min-width: 48rem) {
  .process__step::after { right: calc(var(--sp-8) * -1); }
  .process__step:nth-child(2n)::after { right: 0; }
}
@media (min-width: 68rem) {
  .process__step:nth-child(2n)::after { right: calc(var(--sp-8) * -1); }
  .process__step:last-child::after { right: 0; }
}

/* A large ghosted figure rather than a small mono label. At this size the step
   number becomes the structure of the section rather than an annotation on it,
   and it fills the vertical space the old layout left empty. */
.process__num {
  display: block;
  margin-bottom: var(--sp-4);
  font-family: var(--font-display);
  font-size: clamp(3rem, 4.4vw, 4.25rem);
  font-weight: 800;
  line-height: 0.85;
  letter-spacing: 0.01em;
  font-variant-numeric: tabular-nums;
  color: rgb(198 156 67 / 30%);
  transition: color 0.3s var(--ease);
}
.process__step:hover .process__num { color: var(--gold-400); }

.process__title { font-size: var(--fs-lg); letter-spacing: -0.01em; }
.process__text { margin-top: var(--sp-3); font-size: var(--fs-sm); color: var(--color-text-muted); }

/* What the client holds at the end of the stage. Set apart above a rule and in
   gold, because it is the answer to the question the reader actually has. */
.process__output {
  margin-top: var(--sp-5);
  padding-top: var(--sp-4);
  border-top: 1px solid rgb(255 255 255 / 12%);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  line-height: 1.5;
  color: var(--gold-400);
}
.process__output-label {
  display: block;
  margin-bottom: var(--sp-1);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgb(255 255 255 / 45%);
}

/* The compliance strip was removed along with its invented certifications.
   Its styles went with it — restore from git history if the real
   certifications are confirmed later. */

/* 7 — Header ============================================================== */
.site-header {
  /* Named so it is snapshotted separately during a view transition and stays
     anchored while the rest of the page crossfades under it. */
  view-transition-name: site-header;
  position: sticky;
  top: 0;
  z-index: 100;
  /* Solid white, not translucent. The supplied logo is a JPEG, which has no
     alpha channel — its own white background would show as a visible rectangle
     against a see-through header. Once a PNG or SVG logo with real transparency
     is supplied, this can go back to rgb(255 255 255 / 88%) plus a
     backdrop-filter blur. */
  background: var(--white);
  border-bottom: 1px solid transparent;
  transition: border-color 0.25s var(--ease), box-shadow 0.25s var(--ease),
              background-color 0.25s var(--ease);
}
/* main.js toggles this past ~8px of scroll. */
.site-header.is-scrolled {
  border-bottom-color: var(--color-border);
  box-shadow: 0 1px 12px rgb(8 8 10 / 6%);
}

.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-5);
  min-height: var(--header-h);
  transition: min-height 0.25s var(--ease);
}
/* Compacts once scrolled, so the full lockup gets its space at the top of the
   page without permanently eating 96px of every viewport. */
.site-header.is-scrolled .site-header__inner { min-height: var(--header-h-min); }

.brand { flex: none; display: flex; align-items: center; }
.brand img {
  width: auto;
  height: var(--logo-h);
  transition: height 0.25s var(--ease);
}
.site-header.is-scrolled .brand img { height: var(--logo-h-min); }

/* Narrow screens: the lockup is ~3.7:1, so it has to come down or it crowds
   the menu button. */
@media (max-width: 30rem) {
  :root { --logo-h: 46px; --logo-h-min: 38px; --header-h: 78px; }
}

.nav-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin-right: calc(var(--sp-2) * -1);
  background: none;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  color: var(--ink-800);
  cursor: pointer;
}
.nav-toggle__close { display: none; }
.nav-toggle[aria-expanded="true"] .nav-toggle__open { display: none; }
.nav-toggle[aria-expanded="true"] .nav-toggle__close { display: block; }

.site-nav__list { display: flex; }
.site-nav__aside { display: flex; align-items: center; gap: var(--sp-4); }

/* Uppercase, mono, tracked out — the same voice as the language switch beside
   it, the ticker label, the hero meta row and every section eyebrow. The nav
   was the last piece of chrome still set in the body face, which made the
   header read as belonging to a different design from the page under it.

   Smaller than before, because uppercase occupies more width and reads larger
   than mixed case at the same point size. */
.site-nav__link {
  position: relative;
  display: block;
  padding: var(--sp-2) 0;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  font-weight: 600;
  /* Tracking eased back as the size went up — the two compound, and holding
     0.12em here would have widened the whole nav by more than the size change
     alone. */
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--gray-800);
  text-decoration: none;
  transition: color 0.18s var(--ease);
}
.site-nav__link::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 2px;
  background: var(--gold-500);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.25s var(--ease-out);
}
.site-nav__link:hover { color: var(--ink-800); }
.site-nav__link:hover::after,
.site-nav__link[aria-current="page"]::after { transform: scaleX(1); }
/* The current page reads in gold, not just underlined. At 12px uppercase a
   colour shift from grey to near-black is easy to miss; gold is not, and it
   matches the marker colour used for "you are here" everywhere else. */
.site-nav__link[aria-current="page"] { color: var(--gold-600); }

.lang-switch {
  display: flex;
  align-items: center;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  overflow: hidden;
}
.lang-switch__item {
  padding: 0.3125rem 0.5625rem;
  font-family: var(--font-mono);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  color: var(--gray-600);
  text-decoration: none;
  background: var(--white);
  transition: background-color 0.15s var(--ease), color 0.15s var(--ease);
}
.lang-switch__item + .lang-switch__item { border-left: 1px solid var(--color-border); }
.lang-switch__item:hover { background: var(--gray-100); color: var(--ink-800); }
.lang-switch__item.is-active { background: var(--ink-800); color: var(--white); }

@media (min-width: 62rem) {
  .nav-toggle { display: none; }
  .site-nav { display: flex; align-items: center; gap: var(--sp-7); flex-wrap: nowrap; }
  .site-nav__list { gap: var(--sp-6); }
  /* Rule separating navigation from the utilities. Without it the language
     switch and the quote button read as two more menu items rather than as a
     different kind of control. */
  .site-nav__aside {
    padding-left: var(--sp-7);
    border-left: 1px solid var(--color-border);
  }
}

/*
   The squeeze zone. Between the desktop breakpoint and roughly 1280px the row
   has to carry a ~230px logo lockup, five nav items and another ~280px of
   utilities — which does not fit at full spacing, and larger nav type makes it
   tighter still.

   Tightening the gaps and the logo through this range keeps everything on one
   line. Without it the nav wraps under the logo, which looks broken rather
   than responsive.
*/
@media (min-width: 62rem) and (max-width: 79.999rem) {
  :root { --logo-h: 52px; --logo-h-min: 40px; --header-h: 84px; }
  .site-nav { gap: var(--sp-5); }
  .site-nav__list { gap: var(--sp-4); }
  .site-nav__aside { padding-left: var(--sp-5); gap: var(--sp-3); }
}

/*
   Mobile menu — a full-height dark panel, not a dropdown.

   It used to be a short white list tucked under the header: functional, but it
   read as a fragment of the page rather than a place you had gone to. Filling
   the screen in the site's own near-black, with the entries at display size and
   numbered, makes opening the menu feel like a deliberate move.

   Full-bleed via `margin-inline: calc(50% - 50vw)` — the nav sits inside a
   padded .container, so without that the dark panel would be inset by the
   container gutter and float in the middle of a white strip.
*/
@media (max-width: 61.999rem) {
  .site-header__inner { flex-wrap: wrap; }

  .site-nav {
    display: none;
    order: 3;
    width: 100vw;
    margin-inline: calc(50% - 50vw);
    padding: var(--sp-6) var(--sp-5) var(--sp-7);
    min-height: calc(100svh - var(--header-h));
    background: var(--ink-950);
    background-image: radial-gradient(80% 60% at 100% 0%, rgb(198 156 67 / 12%), transparent 60%);
    border-top: 1px solid rgb(198 156 67 / 30%);
    color: rgb(255 255 255 / 78%);
  }
  .site-nav.is-open { display: flex; flex-direction: column; }

  .site-nav__list {
    flex-direction: column;
    flex: 1;
    gap: 0;
    counter-reset: navitem;
  }
  .site-nav__list li { counter-increment: navitem; }

  /* Display face at size, with a mono index — the same pairing the process
     steps and the range index use. */
  .site-nav__link {
    display: flex;
    align-items: baseline;
    gap: var(--sp-4);
    padding: var(--sp-5) 0;
    font-family: var(--font-display);
    font-size: clamp(1.5rem, 6.5vw, 2.125rem);
    font-weight: 800;
    letter-spacing: 0.02em;
    color: var(--white);
    border-bottom: 1px solid rgb(255 255 255 / 12%);
  }
  .site-nav__link::before {
    content: counter(navitem, decimal-leading-zero);
    flex: none;
    font-family: var(--font-mono);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.1em;
    color: var(--gold-500);
  }
  .site-nav__link::after { display: none; }
  .site-nav__link[aria-current="page"] { color: var(--gold-400); }

  /* Entries arrive one after another once the panel opens. Applied only under
     .is-open so they animate on each opening, not once on page load. */
  .site-nav.is-open .site-nav__list li {
    animation: fade-up 0.45s var(--ease-out) both;
  }
  .site-nav.is-open li:nth-child(1) { animation-delay: 60ms; }
  .site-nav.is-open li:nth-child(2) { animation-delay: 110ms; }
  .site-nav.is-open li:nth-child(3) { animation-delay: 160ms; }
  .site-nav.is-open li:nth-child(4) { animation-delay: 210ms; }
  .site-nav.is-open li:nth-child(5) { animation-delay: 260ms; }

  /* Utilities pinned to the foot of the panel. */
  .site-nav__aside {
    justify-content: space-between;
    margin-top: auto;
    padding-top: var(--sp-6);
    border-top: 1px solid rgb(255 255 255 / 12%);
    animation: fade-up 0.45s var(--ease-out) 310ms both;
  }
  .site-nav__aside .lang-switch { border-color: rgb(255 255 255 / 22%); }
  .site-nav__aside .lang-switch__item {
    background: transparent;
    color: rgb(255 255 255 / 70%);
    border-color: rgb(255 255 255 / 22%);
  }
  .site-nav__aside .lang-switch__item:hover { background: rgb(255 255 255 / 8%); color: var(--white); }
  .site-nav__aside .lang-switch__item.is-active { background: var(--gold-500); color: var(--ink-950); }
}

/* The page behind the open panel must not scroll — otherwise a swipe moves the
   page under a menu that fills the screen, which feels broken. main.js toggles
   this class on <html>. */
html.nav-open { overflow: hidden; }

/* 8 — Hero ================================================================ */
.hero {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: var(--ink-950);
  color: var(--white);
  padding-block: clamp(var(--sp-9), 9vw, 7rem) clamp(var(--sp-11), 12vw, 9.5rem);

  /*
     The hero was sized purely by its content, so on a large screen it ended
     part-way down and read as a banner rather than a hero — the single biggest
     thing holding this section back.

     Now it claims most of the viewport and centres its content, but stops at
     900px so it never becomes an empty field on a tall monitor, and leaves the
     stats band just visible below the fold so the page invites a scroll.

     svh, not vh: on mobile, vh is the viewport with browser chrome HIDDEN, so a
     100vh hero is taller than the visible screen on first paint and the content
     sits off-centre until the user scrolls. svh is the small (chrome-visible)
     viewport, which is what we actually want. The vh line above is the fallback
     for browsers that do not know svh.
  */
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: min(calc(100vh - var(--header-h)), 900px);
  min-height: min(calc(100svh - var(--header-h)), 900px);
}

/* Layered gradients + a drifting grid stand in for photography.
   Swap for a real hero image once brand photos exist. */
.hero__bg { position: absolute; inset: 0; z-index: -1; overflow: hidden; }

/*
   The background is four layers moving at different rates, not one texture:

     .hero__glow        two soft light fields   parallax  -1.5 / +2.2 (deepest)
     .hero__topo        survey contour rings    parallax  -0.9  (opposite way)
     .hero__dots        fine dot matrix         parallax  +0.5  (nearest)
     .hero__dots--near  the same dots, lit under the cursor
     .hero__sweep       a slow travelling band

   A dot matrix instead of a ruled grid: at this scale a line grid reads as
   graph paper, dots read as a measured surface. Opposing parallax directions
   are what the eye interprets as depth.

   Two independent parallaxes are kept on separate properties so neither
   clobbers the other:
     transform  <- scroll position (--py), updated per frame, no transition
     translate  <- pointer position (--pmx/--pmy), eased so it floats
   Scroll parallax touches only the dot layer, never the blurred glows — moving
   a 70px-blur layer every scroll frame is expensive on low-end phones.
*/
/* Contour rings, as on a survey map — concentric repeating-radial-gradients from
   two off-canvas centres. Chosen over diagonal hairlines because terrain and
   mapping are what this company actually works on, and because rings and a
   square dot grid read as different orders of information rather than as two
   competing patterns. */
/* Optional client photograph, sitting under every generated layer. Held dark
   and slightly desaturated so the headline stays readable over any image — the
   vignette at z-index 5 finishes the job. */
.hero__photo {
  position: absolute;
  inset: 0;
  z-index: 0;
  background-size: cover;
  background-position: center;
  opacity: 0.42;
  /* Same grade tokens as every other image on the site, so the hero shot and
     the reference covers look like they came from one shoot. */
  filter: saturate(var(--grade-saturate)) contrast(var(--grade-contrast));
}

.hero__topo {
  position: absolute;
  inset: -25%;
  z-index: 1;
  translate: calc(var(--pmx, 0px) * -0.9) calc(var(--pmy, 0px) * -0.9);
  transition: translate 0.85s var(--ease);
  /* Only the lower-left contour field now. The upper-right set was removed when
     the radar moved in there — two systems of concentric rings overlapping in
     the same corner read as interference, not as depth. */
  background-image:
    repeating-radial-gradient(circle at 10% 88%, transparent 0 58px, rgb(198 156 67 / 6%) 58px 59px);
  -webkit-mask-image: radial-gradient(85% 80% at 30% 60%, #000 8%, transparent 74%);
  mask-image: radial-gradient(85% 80% at 30% 60%, #000 8%, transparent 74%);
}

/*
   Radar — range rings and a rotating sweep, upper right, bled off the edge.

   Placed where the headline block does not reach: .hero__inner is capped at
   54rem, so on a wide screen the right-hand third is empty and this fills it
   with something that means something. The same device as the Why Pricom
   section, which is what ties the two together.

   18s per revolution, two seconds slower than the one further down the page, so
   the two are never quite in step when both happen to be in view.
*/
.hero__radar {
  position: absolute;
  top: 8%;
  right: -10%;
  z-index: 1;
  width: min(44rem, 92vw);
  aspect-ratio: 1;
  border-radius: 50%;
  /* Its own parallax rate — between the contours (-0.9) and the dots (+0.5) —
     so it sits at its own depth rather than travelling with either. */
  translate: calc(var(--pmx, 0px) * -0.35) calc(var(--pmy, 0px) * -0.35);
  transition: translate 0.75s var(--ease);
  background-image:
    repeating-radial-gradient(circle at center, transparent 0 54px, rgb(198 156 67 / 11%) 54px 55px);
  -webkit-mask-image: radial-gradient(circle, #000 6%, transparent 66%);
  mask-image: radial-gradient(circle, #000 6%, transparent 66%);
}
.hero__radar::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: conic-gradient(
    from 0deg,
    rgb(198 156 67 / 22%) 0deg,
    rgb(198 156 67 / 7%) 34deg,
    transparent 64deg,
    transparent 360deg
  );
  -webkit-mask-image: radial-gradient(circle, #000 4%, transparent 64%);
  mask-image: radial-gradient(circle, #000 4%, transparent 64%);
  animation: radar-sweep 18s linear infinite;
}

/* Both dot layers share identical geometry and parallax so they register
   exactly on top of each other — the lit layer has to look like the SAME dots
   brightening, not a second set drifting over the first. */
.hero__dots {
  position: absolute;
  inset: -15%;
  z-index: 2;
  transform: translate3d(0, var(--py, 0px), 0);
  translate: calc(var(--pmx, 0px) * 0.5) calc(var(--pmy, 0px) * 0.5);
  transition: translate 0.55s var(--ease);
  background-image: radial-gradient(circle at center, rgb(255 255 255 / 17%) 1px, transparent 1.6px);
  background-size: 26px 26px;
  -webkit-mask-image: radial-gradient(68% 60% at 50% 32%, #000 12%, transparent 76%);
  mask-image: radial-gradient(68% 60% at 50% 32%, #000 12%, transparent 76%);
}

/* The surface responds to the cursor: gold dots revealed only inside a circle
   that follows the pointer, so the light appears to fall on the matrix rather
   than float above it. --mx/--my come from main.js. */
.hero__dots--near {
  z-index: 3;
  opacity: 0;
  transition: opacity 0.45s var(--ease), translate 0.55s var(--ease);
  background-image: radial-gradient(circle at center, rgb(217 182 101 / 62%) 1px, transparent 1.7px);
  -webkit-mask-image: radial-gradient(12rem circle at var(--mx, 50%) var(--my, 40%), #000 0%, transparent 62%);
  mask-image: radial-gradient(12rem circle at var(--mx, 50%) var(--my, 40%), #000 0%, transparent 62%);
}
.hero.is-live .hero__dots--near { opacity: 1; }

/* A band of light crossing the hero every 16s. Long, slow and low contrast —
   the point is that you notice the hero is alive, not the effect itself. */
.hero__sweep {
  position: absolute;
  top: -30%;
  bottom: -30%;
  left: 0;
  z-index: 4;
  width: 42%;
  filter: blur(22px);
  background: linear-gradient(
    100deg,
    transparent,
    rgb(255 255 255 / 5%) 42%,
    rgb(198 156 67 / 8%) 58%,
    transparent
  );
  animation: hero-sweep 16s var(--ease) infinite;
}

/* Vignette, so the headline always sits on the darkest part of the field.
   ::before is the FIRST child in paint order, so without an explicit z-index it
   would sit behind every layer above and do nothing. */
.hero__bg::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 5;
  background: radial-gradient(125% 105% at 50% 38%, transparent 42%, rgb(8 8 10 / 62%) 100%);
}

/* Registration marks, as on a technical drawing. Inset clear of the ticker. */
.hero__frame {
  --fi: clamp(1rem, 3vw, 2.5rem);
  --arm: 26px;
  --hair: 1px;
  position: absolute;
  top: var(--fi);
  left: var(--fi);
  right: var(--fi);
  bottom: calc(var(--fi) + 3.25rem);
  pointer-events: none;
  opacity: 0;
  background:
    linear-gradient(var(--gold-500) 0 0) left  top   / var(--arm) var(--hair) no-repeat,
    linear-gradient(var(--gold-500) 0 0) left  top   / var(--hair) var(--arm) no-repeat,
    linear-gradient(var(--gold-500) 0 0) right top   / var(--arm) var(--hair) no-repeat,
    linear-gradient(var(--gold-500) 0 0) right top   / var(--hair) var(--arm) no-repeat,
    linear-gradient(var(--gold-500) 0 0) left  bottom/ var(--arm) var(--hair) no-repeat,
    linear-gradient(var(--gold-500) 0 0) left  bottom/ var(--hair) var(--arm) no-repeat,
    linear-gradient(var(--gold-500) 0 0) right bottom/ var(--arm) var(--hair) no-repeat,
    linear-gradient(var(--gold-500) 0 0) right bottom/ var(--hair) var(--arm) no-repeat;
  animation: frame-in 1.2s var(--ease-out) 700ms forwards;
}
/* Ruler ticks running in from the top-left corner, fading out as they go —
   the same language as a dimensioned technical drawing. */
.hero__frame::before,
.hero__frame::after {
  content: "";
  position: absolute;
  background-image: repeating-linear-gradient(var(--tick-angle), var(--gold-500) 0 1px, transparent 1px 15px);
  opacity: 0.55;
}
.hero__frame::before {
  --tick-angle: 90deg;
  top: 0;
  left: 0;
  right: 0;
  height: 7px;
  -webkit-mask-image: linear-gradient(90deg, #000, transparent 42%);
  mask-image: linear-gradient(90deg, #000, transparent 42%);
}
.hero__frame::after {
  --tick-angle: 180deg;
  top: 0;
  bottom: 0;
  left: 0;
  width: 7px;
  -webkit-mask-image: linear-gradient(180deg, #000, transparent 38%);
  mask-image: linear-gradient(180deg, #000, transparent 38%);
}

.hero__glow {
  position: absolute;
  border-radius: 50%;
  filter: blur(90px);
  opacity: 0.62;
}
/* The glows keep `transform` for their slow idle drift and use `translate` for
   pointer parallax. Opposite sign and different multipliers to the grid, so the
   layers separate in depth as the cursor moves. */
.hero__glow--a {
  width: 62vw; height: 62vw;
  max-width: 760px; max-height: 760px;
  top: -22%; right: -12%;
  /* Warm stone light rather than the old blue — the brand is graphite and gold,
     so the secondary light source is neutral-warm, never cool. */
  background: radial-gradient(circle, rgb(196 186 170 / 40%), transparent 68%);
  animation: drift-a 26s var(--ease) infinite;
  translate: calc(var(--pmx, 0px) * -1.5) calc(var(--pmy, 0px) * -1.5);
  transition: translate 0.8s var(--ease);
}
.hero__glow--b {
  width: 52vw; height: 52vw;
  max-width: 620px; max-height: 620px;
  bottom: -26%; left: -10%;
  background: radial-gradient(circle, rgb(198 156 67 / 34%), transparent 68%);
  animation: drift-b 32s var(--ease) infinite;
  translate: calc(var(--pmx, 0px) * 2.2) calc(var(--pmy, 0px) * 2.2);
  transition: translate 1.1s var(--ease);
}

/* Fine film grain. Costs one small inline SVG and no request, and stops the
   large flat gradients from looking like banded plastic. */
.hero__bg::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 6;
  opacity: 0.038;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* Warm spotlight that follows the cursor. main.js writes --mx/--my onto .hero
   itself so both this layer and .hero__dots--near can read them, and toggles
   .is-live on .hero — fine-pointer devices only. */
.hero__spot {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.5s var(--ease);
  background: radial-gradient(
    22rem circle at var(--mx, 50%) var(--my, 40%),
    rgb(198 156 67 / 15%),
    transparent 60%
  );
}
.hero.is-live .hero__spot { opacity: 1; }

.hero__inner { max-width: 54rem; position: relative; }

.hero__eyebrow { animation: fade-up 0.8s var(--ease-out) 60ms both; }

.hero__title {
  font-size: var(--fs-4xl);
  color: var(--white);
  /* Tightest on the page: at display size the letters already have air, and
     the line should read as one solid block. */
  letter-spacing: 0.002em;
  line-height: 0.96;
}

/* Each word gets a clipping box it rises out of. The padding/margin pair keeps
   descenders (g, j, ë) from being sheared off by the overflow. */
.word {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  padding-bottom: 0.14em;
  margin-bottom: -0.14em;
  perspective: 520px;
}
.word__i {
  display: inline-block;
  transform-origin: 50% 100%;
  animation: word-up 0.95s var(--ease-out) var(--wd, 0ms) both;
}
/* Second half of the headline in gold, so the line reads as one thought.
   Flat gold is the base; the gradient only applies where background-clip:text
   is supported, because "color: transparent" without it means invisible text. */
.hero__accent {
  display: block;
  color: var(--gold-400);
  animation: fade-up 0.9s var(--ease-out) var(--wd, 0ms) both;
}
@supports (background-clip: text) or (-webkit-background-clip: text) {
  .hero__accent {
    background: linear-gradient(92deg, var(--gold-400), var(--gold-300) 55%, var(--gold-500));
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    -webkit-text-fill-color: transparent;
  }
}

.hero__text {
  margin-top: var(--sp-5);
  max-width: 52ch;
  font-size: var(--fs-lg);
  color: rgb(255 255 255 / 76%);
  animation: fade-up 0.8s var(--ease-out) var(--wd, 0ms) both;
}
.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin-top: var(--sp-7);
  animation: fade-up 0.8s var(--ease-out) var(--wd, 0ms) both;
}

/* Sector qualifiers under the actions — mono, spaced, divided by gold rules.
   Fills the composition below the buttons, which was previously dead space, and
   answers "is this page for me?" without another paragraph. */
.hero__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3) var(--sp-5);
  margin-top: var(--sp-8);
  padding-top: var(--sp-5);
  border-top: 1px solid rgb(255 255 255 / 14%);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgb(255 255 255 / 72%);
  animation: fade-up 0.8s var(--ease-out) var(--wd, 0ms) both;
}
.hero__meta li {
  display: flex;
  align-items: center;
  gap: var(--sp-5);
}
.hero__meta li + li::before {
  content: "";
  width: 3px;
  height: 3px;
  background: var(--gold-500);
}

/* Scroll cue. A bare chevron is ambiguous — the label says what it does, and
   the mono setting keeps it in the same voice as the rest of the micro-type. */
.hero__scroll {
  position: absolute;
  left: 50%;
  bottom: calc(var(--sp-9) + 8px);
  translate: -50% 0;
  display: none;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-2);
  color: rgb(255 255 255 / 70%);
  text-decoration: none;
  animation: nudge-down 2.4s var(--ease) infinite;
}
.hero__scroll-label {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
}
.hero__scroll .icon { width: 18px; height: 18px; }
.hero__scroll:hover { color: var(--gold-400); }
@media (min-width: 48rem) and (min-height: 40rem) { .hero__scroll { display: flex; } }

/* Keyword ticker --------------------------------------------------------- */
.ticker {
  position: absolute;
  inset: auto 0 0 0;
  display: flex;
  align-items: center;
  gap: var(--sp-5);
  padding: var(--sp-4) 0 var(--sp-4) var(--sp-5);
  border-top: 1px solid rgb(255 255 255 / 10%);
  background: rgb(8 8 10 / 55%);
  backdrop-filter: blur(6px);
  overflow: hidden;
  /* Fades on the RIGHT only. The old rule faded both edges, which would now
     dissolve the label sitting at the left. */
  -webkit-mask-image: linear-gradient(90deg, #000 0 86%, transparent);
  mask-image: linear-gradient(90deg, #000 0 86%, transparent);
}
/* Fixed label, so the strip reads as a labelled readout rather than words
   drifting past for decoration. */
.ticker__label {
  flex: none;
  padding-right: var(--sp-5);
  border-right: 1px solid rgb(255 255 255 / 18%);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--gold-400);
}
/* The track holds the item list twice, so translating by -50% loops seamlessly. */
.ticker__track {
  display: flex;
  width: max-content;
  animation: ticker-scroll 44s linear infinite;
}
.ticker:hover .ticker__track { animation-play-state: paused; }

.ticker__item {
  display: flex;
  align-items: center;
  padding-inline: var(--sp-6);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: rgb(255 255 255 / 55%);
  white-space: nowrap;
}
.ticker__item::after {
  content: "";
  width: 4px;
  height: 4px;
  margin-left: var(--sp-6);
  background: var(--gold-500);
  opacity: 0.65;
}

/* 9 — Sections ============================================================ */

/* Stats ------------------------------------------------------------------ */
/*
   The stats band is not a section of its own — it is the foot of the hero.

   It used to sit a tone lighter than the hero with a rule between them, which
   put a seam across the opening block, and then cut into the light Products
   section with a diagonal. That diagonal worked when both sides were dark; once
   Products went light it became a large pale wedge driven into the dark band,
   which is what made the join look wrong.

   Now: the same near-black as the hero, no rule above, the hero's dot texture
   carried through, and a single gold hairline closing the dark block off. The
   opening reads as one panel, and the light section starts cleanly beneath it.
*/
.stats {
  position: relative;
  overflow: hidden;
  background: var(--ink-950);
  border-bottom: 1px solid rgb(198 156 67 / 35%);
  color: var(--white);
}
/* The hero's measured surface, continued. Masked to fade out before the edges
   so it never reads as a second, separate texture. */
.stats::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image: radial-gradient(circle at center, rgb(255 255 255 / 10%) 1px, transparent 1.6px);
  background-size: 26px 26px;
  -webkit-mask-image: radial-gradient(80% 120% at 50% 0%, #000 5%, transparent 80%);
  mask-image: radial-gradient(80% 120% at 50% 0%, #000 5%, transparent 80%);
}
.stats > .container { position: relative; z-index: 1; }
.stats__list { display: grid; grid-template-columns: repeat(2, 1fr); }
@media (min-width: 48rem) { .stats__list { grid-template-columns: repeat(4, 1fr); } }

.stats__item {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-7) var(--sp-5);
  border-bottom: 1px solid rgb(255 255 255 / 10%);
}
/* The column divider is a pseudo-element rather than a border so it can draw
   itself downward as the row is revealed, on the same --d stagger. */
.stats__item::after {
  content: "";
  position: absolute;
  right: 0;
  top: 0;
  bottom: 0;
  width: 1px;
  background: rgb(255 255 255 / 12%);
  transform: scaleY(0);
  transform-origin: top;
  transition: transform 0.7s var(--ease-out) var(--d, 0ms);
}
.js [data-reveal].is-visible.stats__item::after,
html:not(.js) .stats__item::after { transform: scaleY(1); }

/* Two columns on small screens: no divider on the right-hand column. */
.stats__item:nth-child(2n)::after { display: none; }
@media (min-width: 48rem) {
  .stats__item { border-bottom: 0; }
  /* Same selector, not a bare .stats__item::after — the :nth-child rule above
     is more specific and would otherwise keep column 2 hidden at four across. */
  .stats__item:nth-child(2n)::after { display: block; }
  .stats__item:last-child::after { display: none; }
  .stats__item:first-child { padding-left: 0; }
}
/* Figures set as a readout: display face, tabular so digits do not jitter while
   counting up, on a gold tick that ties them to the rest of the marks. */
/* Larger than a section title — these are the only figures on the page and they
   are what a reader scans for first. Tabular so the digits do not shuffle
   sideways while the counter runs. */
.stats__value {
  font-family: var(--font-display);
  font-size: clamp(2.5rem, 3.2vw + 1.25rem, 3.75rem);
  font-weight: 800;
  line-height: 0.9;
  letter-spacing: 0.01em;
  color: var(--gold-400);
  font-variant-numeric: tabular-nums;
}
.stats__item::before {
  content: "";
  display: block;
  width: 22px;
  height: 2px;
  margin-bottom: var(--sp-4);
  background: var(--gold-500);
}
/* Mono, like every other micro-label on the site — the hero meta row, the
   ticker, the product lists. It was the one small label still set in the body
   face, which made the band feel like it belonged to a different page. */
.stats__label {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-weight: 600;
  line-height: 1.5;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgb(255 255 255 / 60%);
}

/* About teaser ------------------------------------------------------------ */
/* Text and a photograph side by side. The image is what lifts this section —
   a wall of prose next to a bullet list was the flattest thing on the page. */
.about-teaser {
  display: grid;
  gap: var(--sp-8);
  align-items: center;
}
@media (min-width: 62rem) {
  .about-teaser { grid-template-columns: 1.05fr 0.95fr; gap: var(--sp-10); }
}
/* The reset strips paragraph margins, so each context sets its own rhythm.
   .editorial__lead gets no default margin because in the .editorial grid it
   sits BESIDE the heading, where a top margin would push it out of line — so
   every stacked use of it has to opt in. */
.about-teaser__body .editorial__lead,
.split__main .editorial__lead { margin-top: var(--sp-5); }
.about-teaser__body .prose { margin-top: var(--sp-4); }
.about-teaser__body .checklist { margin-top: var(--sp-6); }

.about-teaser__action { margin-top: var(--sp-7); }

/* Image pair. The container reserves the space; the inset is positioned inside
   it and overlaps the main image's lower-left corner. */
.about-teaser__media { position: relative; }

.about-figure {
  margin: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
}
.about-figure--main { aspect-ratio: 4 / 5; }
@media (max-width: 61.999rem) {
  .about-figure--main { aspect-ratio: 16 / 10; }
}

/*
   The inset sits over the main image, offset out of the grid column so the pair
   reads as arranged rather than stacked. The thick outline in the section's own
   background colour is what separates it from the image beneath — without it
   the two photographs bleed into one another.

   Hidden below 62rem: at phone width it would cover the main image instead of
   sitting beside it.
*/
.about-figure--inset {
  display: none;
  position: absolute;
  /* Offsets stay inside the grid gap (--sp-10) and the section's own vertical
     padding. The section carries overflow:hidden for its dot texture, so
     anything reaching past the container would be clipped. */
  left: calc(var(--sp-6) * -1);
  bottom: calc(var(--sp-6) * -1);
  width: 58%;
  aspect-ratio: 4 / 3;
  box-shadow: 0 0 0 8px var(--color-bg-tint), 0 22px 44px rgb(8 8 10 / 22%);
}
@media (min-width: 62rem) { .about-figure--inset { display: block; } }

/* Large imagery that does not respond to the cursor reads as a placeholder.
   Slow and small — 5% over 0.8s is a drift, not a zoom. */
.about-figure img { transition: transform 0.8s var(--ease-out), filter 0.4s var(--ease); }
.about-figure:hover img {
  transform: scale(1.05);
  filter: saturate(0.95) contrast(var(--grade-contrast));
}

/* Gold registration marks, the same language as the tiles and the hero frame. */
.about-figure::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background:
    linear-gradient(var(--gold-500) 0 0), linear-gradient(var(--gold-500) 0 0),
    linear-gradient(var(--gold-500) 0 0), linear-gradient(var(--gold-500) 0 0);
  background-repeat: no-repeat;
  background-size:
    var(--bracket) 2px, 2px var(--bracket),
    var(--bracket) 2px, 2px var(--bracket);
  background-position: left top, left top, right bottom, right bottom;
}

/* Services --------------------------------------------------------------- */
/* Wide numbered rows, not a card grid. Three items with longer copy read better
   across the full measure, and the change of layout keeps the page from being
   the same card pattern repeated top to bottom. */
.services {
  display: grid;
  border-top: 1px solid var(--color-border);
}
.service {
  position: relative;
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-areas:
    "num  icon"
    "body body";
  align-items: start;
  gap: var(--sp-4) var(--sp-5);
  padding-block: var(--sp-6);
  border-bottom: 1px solid var(--color-border);
  transition: background-color 0.25s var(--ease);
}
@media (min-width: 48rem) {
  .service {
    /* The trailing auto column holds the arrow, which is what signals the row
       is a link. Without it the rows look like a list that happens to react. */
    grid-template-columns: 4.5rem 3.5rem 1fr auto;
    grid-template-areas: "num icon body arrow";
    gap: var(--sp-6);
    align-items: center;
    padding-block: var(--sp-7);
    padding-inline: var(--sp-4);
    /* Pull the padding back out so the rules still span the full measure. */
    margin-inline: calc(var(--sp-4) * -1);
  }
}
/* A warm wash and a gold rule rather than any movement — consistent with the
   tiles, which do not shift on hover either. */
.service:hover { background-color: color-mix(in srgb, var(--gold-500) 3%, transparent); }

.service__num {
  grid-area: num;
  font-family: var(--font-display);
  font-size: var(--fs-2xl);
  font-weight: 800;
  line-height: 1;
  color: var(--gray-300);
  font-variant-numeric: tabular-nums;
  transition: color 0.25s var(--ease);
}
.service:hover .service__num { color: var(--gold-500); }

.service__icon {
  grid-area: icon;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--ink-800) 6%, transparent);
  color: var(--ink-700);
  transition: background-color 0.25s var(--ease), color 0.25s var(--ease);
}
.service:hover .service__icon {
  background: color-mix(in srgb, var(--gold-500) 16%, transparent);
  color: var(--gold-600);
}
.service__icon .icon { stroke-dasharray: 100; stroke-dashoffset: 0; }
.service:hover .service__icon .icon { animation: icon-draw 0.85s var(--ease-out); }

.service__body { grid-area: body; }
.service__title { font-size: var(--fs-xl); }
.service__text {
  margin-top: var(--sp-3);
  max-width: 62ch;
  font-size: var(--fs-sm);
  color: var(--color-text-muted);
}

/* What each service actually covers, set inline in the same mono micro-type as
   the hero meta row and the product line lists. Inline rather than bulleted
   because these rows are wide — a vertical list would leave the right half
   empty and stretch each row for no gain. */
.service__list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-4);
  margin-top: var(--sp-4);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-700);
}
.service__list li {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
}
.service__list li + li::before {
  content: "";
  width: 4px;
  height: 4px;
  flex: none;
  background: var(--gold-500);
}

/* Stretched link — the anchor names the row, its ::after makes the whole row
   clickable. Same pattern as the product tiles. */
.service__link { color: inherit; text-decoration: none; }
.service__link::after { content: ""; position: absolute; inset: 0; z-index: 2; }
.service:hover .service__link { color: var(--gold-600); }
.service__link:focus-visible { outline: none; }
.service:has(.service__link:focus-visible) {
  outline: 2px solid var(--gold-500);
  outline-offset: 2px;
}

.service__arrow {
  grid-area: arrow;
  display: none;
  width: 20px;
  height: 20px;
  color: var(--gray-400);
  transition: transform 0.25s var(--ease), color 0.25s var(--ease);
}
@media (min-width: 48rem) { .service__arrow { display: block; } }
.service:hover .service__arrow { transform: translateX(4px); color: var(--gold-600); }

/* ==========================================================================
   Dark-section variants
   Components are light by default and get overridden inside .section--dark.
   Scoping it this way means the About page's client-type tiles, which sit on a
   light section, keep the light treatment with no extra class in the markup.
   ========================================================================== */

/*
   Two ways a tile goes dark, and they need different backgrounds:

     .section--dark .tile   sits ON a dark ground, so a translucent white lift
                            is right — it picks up the section's own tone.
     .tile--dark            sits on a LIGHT section, so it needs a solid dark
                            fill. A translucent white would just go pale.

   Everything inside the tile — type, rules, icon — is shared between the two.
*/
.section--dark .tile {
  background-color: rgb(255 255 255 / 3.5%);
  border-color: rgb(255 255 255 / 12%);
}
.section--dark .tile:hover {
  background-color: rgb(255 255 255 / 6%);
  border-color: rgb(198 156 67 / 55%);
  box-shadow: 0 18px 44px rgb(0 0 0 / 45%);
}

/* Inverted card: solid graphite on the light products section, so the grid
   reads as hardware laid out on a light bench. The gold corner brackets and the
   photography both gain from the darker face. */
.tile--dark {
  background-color: var(--ink-900);
  border-color: rgb(255 255 255 / 10%);
}
.tile--dark:hover {
  background-color: var(--ink-800);
  border-color: var(--gold-500);
  /* A cast shadow, not a glow — these sit on a light ground. */
  box-shadow: 0 20px 44px rgb(8 8 10 / 26%);
}

.section--dark .tile__title,
.tile--dark .tile__title { color: var(--white); }
.section--dark .tile__text,
.tile--dark .tile__text { color: rgb(255 255 255 / 68%); }
.section--dark .tile__list,
.tile--dark .tile__list { border-top-color: rgb(255 255 255 / 14%); }
.section--dark .tile__list li,
.tile--dark .tile__list li { color: rgb(255 255 255 / 62%); }
.section--dark .tile:hover .tile__link,
.tile--dark:hover .tile__link { color: var(--gold-400); }

/* The child combinator matters: it targets only an icon sitting directly in the
   tile body, as on the About page. Without it this would also capture the badge
   inside .tile__media and undo its styling. */
.section--dark .tile > .tile__icon,
.tile--dark > .tile__icon {
  background: rgb(255 255 255 / 7%);
  color: rgb(255 255 255 / 82%);
}
.section--dark .tile:hover > .tile__icon,
.tile--dark:hover > .tile__icon {
  background: rgb(198 156 67 / 18%);
  color: var(--gold-400);
}

/* The photo already sits on dark, so the fade into the tile needs to reach
   further to stop the type below competing with image detail. */
.section--dark .tile__media,
.tile--dark .tile__media { background: var(--ink-950); }

/* Feature cards on dark — used by the About page's "How we work" band. */
.section--dark .feature {
  background-color: rgb(255 255 255 / 3.5%);
  border-color: rgb(255 255 255 / 12%);
}
.section--dark .feature:hover {
  background-color: rgb(255 255 255 / 7%);
  border-color: rgb(198 156 67 / 55%);
  box-shadow: 0 18px 44px rgb(0 0 0 / 45%);
}
.section--dark .feature__icon {
  background: rgb(198 156 67 / 16%);
  color: var(--gold-400);
}
.section--dark .feature__title { color: var(--white); }
.section--dark .feature__text { color: rgb(255 255 255 / 68%); }

/* Process on dark. */
.section--dark .process__step { border-top-color: rgb(255 255 255 / 16%); }
.section--dark .process__step:hover { border-top-color: var(--gold-500); }
.section--dark .process__title { color: var(--white); }
.section--dark .process__text { color: rgb(255 255 255 / 68%); }
.section--dark .process__num { color: var(--gold-400); }

/* Reference cards on dark. */
.section--dark .project {
  background: rgb(255 255 255 / 3.5%);
  border-color: rgb(255 255 255 / 12%);
}
.section--dark .project:hover {
  border-color: rgb(198 156 67 / 55%);
  box-shadow: 0 18px 44px rgb(0 0 0 / 45%);
}
.section--dark .project__title { color: var(--white); }
.section--dark .project:hover .project__link { color: var(--gold-400); }
.section--dark .project__text { color: rgb(255 255 255 / 68%); }
.section--dark .tag {
  background: rgb(198 156 67 / 16%);
  color: var(--gold-300);
}

/* Outline button on dark. */
.section--dark .btn--outline {
  border-color: rgb(255 255 255 / 28%);
  color: var(--white);
}
.section--dark .btn--outline:hover {
  border-color: var(--gold-500);
  background: rgb(198 156 67 / 12%);
  color: var(--white);
}

/* Link arrow on dark. */
.section--dark .link-arrow { color: var(--gold-400); }
.section--dark .link-arrow:hover { color: var(--gold-300); }

/* Catalogue rows (Products page) -------------------------------------------
   Alternating full-width rows rather than a card grid. The home page already
   shows these six as tiles; repeating that here would give a visitor who
   clicked through nothing new. A row gives each category its photograph at
   size and room for the full line list underneath. */
.catalog {
  display: grid;
  gap: var(--sp-10);
}
.catalog__row {
  position: relative;
  display: grid;
  gap: var(--sp-6);
  align-items: center;
  /* The ids stay so /products#cat-3 keeps working as a deep link from
     elsewhere. html's own scroll-padding-top already clears the header, so no
     per-row margin is needed on top of it. */
}
@media (min-width: 62rem) {
  .catalog { gap: clamp(var(--sp-10), 8vw, 7rem); }
  .catalog__row { grid-template-columns: 1fr 1fr; gap: var(--sp-10); }
  /* Every second row flips, so the eye zig-zags down the page instead of
     tracking one straight edge. `order` works on grid items, so this needs no
     change in the markup and no second template. */
  .catalog__row:nth-child(even) .catalog__media { order: 2; }
}

.catalog__media {
  margin: 0;
  aspect-ratio: 4 / 3;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
}
/* Registration marks, as on the About figures and the product tiles. */
.catalog__media::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background:
    linear-gradient(var(--gold-500) 0 0), linear-gradient(var(--gold-500) 0 0),
    linear-gradient(var(--gold-500) 0 0), linear-gradient(var(--gold-500) 0 0);
  background-repeat: no-repeat;
  background-size:
    var(--bracket) 2px, 2px var(--bracket),
    var(--bracket) 2px, 2px var(--bracket);
  background-position: left top, left top, right bottom, right bottom;
}
.catalog__media img { transition: transform 0.8s var(--ease-out); }
.catalog__media:hover img { transform: scale(1.04); }

/* Native scroll-driven parallax, as on the home page reference covers: the
   photograph settles from slightly oversized as the row travels up the
   viewport. Uses scale/translate rather than transform so the hover zoom above
   still composes with it instead of being overwritten. */
@supports (animation-timeline: view()) {
  .catalog__media img {
    animation: media-drift linear both;
    animation-timeline: view();
    animation-range: entry 5% cover 50%;
  }
}

/* Icon chip and a large ghosted numeral. The numeral was a 15px mono label —
   at that size it was an annotation on the row; at display size it becomes the
   row's header, the same way the process steps are numbered. */
.catalog__index {
  display: flex;
  align-items: center;
  gap: var(--sp-5);
  margin-bottom: var(--sp-5);
}
.catalog__num {
  font-family: var(--font-display);
  font-size: clamp(2.5rem, 4vw, 3.5rem);
  font-weight: 800;
  line-height: 0.85;
  letter-spacing: 0.01em;
  font-variant-numeric: tabular-nums;
  color: rgb(198 156 67 / 32%);
  transition: color 0.3s var(--ease);
}
.catalog__row:hover .catalog__num { color: var(--gold-500); }
.catalog__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 46px;
  height: 46px;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--gold-500) 14%, transparent);
  color: var(--gold-600);
}
.catalog__icon .icon { stroke-dasharray: 100; stroke-dashoffset: 0; }
.catalog__row:hover .catalog__icon .icon { animation: icon-draw 0.85s var(--ease-out); }

.catalog__title { font-size: var(--fs-2xl); }
.catalog__text {
  margin-top: var(--sp-4);
  max-width: 52ch;
  font-size: var(--fs-lg);
  color: var(--color-text-muted);
}

.catalog__lines-label {
  margin-top: var(--sp-6);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--color-border);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--gray-600);
}
/* The lines set larger here than on the home tiles — on this page they are the
   content, not a summary of it. */
.catalog__lines {
  display: grid;
  gap: var(--sp-2);
  margin-top: var(--sp-4);
}
.catalog__lines li {
  position: relative;
  padding-left: var(--sp-5);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--ink-800);
}
.catalog__lines li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.55em;
  width: 5px;
  height: 5px;
  background: var(--gold-500);
}

/* Full range index ------------------------------------------------------- */
.range {
  display: grid;
  gap: var(--sp-7) var(--sp-6);
}
@media (min-width: 40rem) { .range { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 62rem) { .range { grid-template-columns: repeat(3, 1fr); } }

.range__title {
  padding-bottom: var(--sp-3);
  border-bottom: 1px solid rgb(198 156 67 / 45%);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--gold-400);
}
.range__list { display: grid; gap: var(--sp-1); margin-top: var(--sp-4); }
.range__list li {
  display: flex;
  align-items: baseline;
  gap: var(--sp-4);
  padding-block: var(--sp-2);
  border-bottom: 1px solid rgb(255 255 255 / 8%);
  transition: border-color 0.2s var(--ease);
}
.range__list li:hover { border-bottom-color: rgb(198 156 67 / 55%); }
/* Reference number, tabular so the column stays straight down the list. */
.range__num {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  color: rgb(255 255 255 / 42%);
}
.range__line {
  font-size: var(--fs-sm);
  font-weight: 500;
  color: rgb(255 255 255 / 88%);
}

/* Service bands (Services page) -------------------------------------------
   Products alternates photograph and text because a product is a thing you
   look at. A service is a sequence you follow, so these are typographic bands:
   a numbered aside column, then the writing, then the deliverables as a
   measured grid. No imagery, deliberately — stock photographs of workshops
   would say less than the numbering does. */
.svc-list {
  position: relative;
  display: grid;
  gap: var(--sp-9);
}
.svc {
  position: relative;
  display: grid;
  gap: var(--sp-6);
  padding-top: var(--sp-8);
  border-top: 1px solid var(--color-border);
}
/* First band sits directly under the section intro, which already has its own
   spacing — a rule there would read as a double divider. */
.svc:first-child { padding-top: 0; border-top: 0; }

@media (min-width: 62rem) {
  .svc-list { gap: var(--sp-10); }
  .svc { grid-template-columns: 11rem 1fr; gap: var(--sp-10); }

  /*
     Hairline rail down the aside column, drawn by the browser off the scroll
     position — no listener, no observer. transform-origin: top means it grows
     downward as the list travels up the viewport, so it reads as the three
     bands being threaded together rather than as a static divider.
  */
  @supports (animation-timeline: view()) {
    .svc-list::before {
      content: "";
      position: absolute;
      top: 0.5rem;
      bottom: 0.5rem;
      /* Centred in the gutter between the aside column and the body, so it
         separates number from text instead of running through the numerals. */
      left: calc(11rem + var(--sp-10) / 2);
      width: 1px;
      background: linear-gradient(180deg,
        var(--gold-500) 0%,
        color-mix(in srgb, var(--gold-500) 25%, transparent) 100%);
      transform-origin: top;
      animation: rail-draw linear both;
      animation-timeline: view();
      animation-range: entry 40% cover 60%;
    }
  }
}
@keyframes rail-draw {
  from { transform: scaleY(0); }
  to   { transform: scaleY(1); }
}

.svc__aside {
  display: flex;
  align-items: center;
  gap: var(--sp-5);
}
@media (min-width: 62rem) {
  /* Stacks under the numeral once there is a column to stack in. */
  .svc__aside { flex-direction: column; align-items: flex-start; gap: var(--sp-6); }
}
.svc__num {
  font-family: var(--font-display);
  font-size: clamp(3rem, 6vw, 5rem);
  font-weight: 800;
  line-height: 0.8;
  letter-spacing: 0.01em;
  font-variant-numeric: tabular-nums;
  color: rgb(198 156 67 / 30%);
  transition: color 0.35s var(--ease);
}
.svc:hover .svc__num { color: var(--gold-500); }
.svc__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--gold-500) 10%, transparent);
  color: var(--gold-600);
}
.svc__icon .icon { width: 24px; height: 24px; stroke-dasharray: 100; stroke-dashoffset: 0; }
.svc:hover .svc__icon .icon { animation: icon-draw 0.85s var(--ease-out); }

.svc__title { font-size: var(--fs-2xl); }
/* Standfirst then body: the same two-weight opening the About page uses, so a
   reader who has been there recognises the hierarchy. */
.svc__lead {
  margin-top: var(--sp-4);
  max-width: 56ch;
  font-size: var(--fs-lg);
  font-weight: 500;
  color: var(--color-heading);
}
.svc__text {
  margin-top: var(--sp-4);
  max-width: 62ch;
  color: var(--color-text-muted);
}

/* Small technical label introducing a list. Shared — the Services bands use it
   above the deliverables grid, the References page above the on-request list.
   Named for what it does rather than for the first block that needed it. */
.list-label {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--gray-600);
}
.svc .list-label { margin-top: var(--sp-7); }
/* Deliverables as bordered cells rather than a bullet list. A list of four
   words reads as an afterthought; boxed and numbered, it reads as a scope. */
.svc__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(10.5rem, 1fr));
  gap: var(--sp-3);
  margin-top: var(--sp-4);
}
.svc__cell {
  display: flex;
  align-items: baseline;
  gap: var(--sp-3);
  padding: var(--sp-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-bg-tint);
  transition: border-color 0.25s var(--ease), background-color 0.25s var(--ease);
}
.svc__cell:hover { border-color: var(--gold-500); background: var(--white); }
.svc__cell-num {
  flex: none;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-variant-numeric: tabular-nums;
  color: var(--gold-600);
}
.svc__cell-label {
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--ink-800);
}
.svc__cta { margin-top: var(--sp-6); }

/* Legal documents ---------------------------------------------------------
   Set as a document, not as marketing: one narrow measure, numbered sections,
   and a contents list that stays put while you read. */
.legal { display: grid; gap: var(--sp-8); align-items: start; }
@media (min-width: 62rem) {
  .legal { grid-template-columns: 16rem minmax(0, 1fr); gap: var(--sp-10); }
  /* Sticky only here. A legal document is the one place on this site where a
     reader genuinely jumps around rather than reading top to bottom. */
  .legal__toc {
    position: sticky;
    top: calc(var(--header-h-min) + var(--sp-5));
  }
}
.legal__toc ol { display: grid; gap: var(--sp-1); margin-top: var(--sp-4); }
.legal__toc a {
  display: flex;
  gap: var(--sp-3);
  padding-block: var(--sp-2);
  font-size: var(--fs-sm);
  color: var(--color-text-muted);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: color 0.2s var(--ease), border-color 0.2s var(--ease);
}
.legal__toc a:hover { color: var(--ink-800); border-bottom-color: var(--color-border); }
.legal__toc-num {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  color: var(--gold-600);
}

.legal__updated {
  padding-bottom: var(--sp-5);
  border-bottom: 1px solid var(--color-border);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--gray-600);
}
.legal__updated time { color: var(--gold-600); }

.legal__section { margin-top: var(--sp-8); scroll-margin-top: calc(var(--header-h-min) + var(--sp-5)); }
.legal__title {
  display: flex;
  gap: var(--sp-4);
  align-items: baseline;
  font-size: var(--fs-xl);
}
.legal__num {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
  color: var(--gold-500);
}
/* 68ch, not the 52ch used elsewhere: this is text people read in full, and a
   narrow measure over a long document means far more line breaks to track. */
.legal__text { margin-top: var(--sp-4); max-width: 68ch; color: var(--color-text-muted); }
.legal__list { display: grid; gap: var(--sp-2); margin-top: var(--sp-4); max-width: 68ch; }
.legal__list li {
  position: relative;
  padding-left: var(--sp-5);
  color: var(--color-text-muted);
}
.legal__list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.62em;
  width: 5px;
  height: 5px;
  background: var(--gold-500);
}

/*
   Passages awaiting confirmation from the client or their lawyer. Marked on the
   page rather than only in a code comment: an unreviewed legal document that
   looks finished is worse than one that admits what is still open, and a note
   nobody can see is a note nobody acts on.

   These should be deleted from the language files — with the surrounding text
   corrected — before the site goes live.
*/
.legal__note {
  margin-top: var(--sp-5);
  padding: var(--sp-4) var(--sp-5);
  max-width: 68ch;
  border: 1px dashed var(--gold-500);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--gold-500) 7%, transparent);
  font-size: var(--fs-sm);
  color: var(--ink-800);
}
.legal__note::before {
  content: "⚠ ";
  color: var(--gold-600);
}

.legal__address {
  margin-top: var(--sp-4);
  font-style: normal;
  line-height: 1.9;
  color: var(--color-text-muted);
}
.legal__address strong { color: var(--color-heading); }

/* Policy links in the footer's bottom bar. */
.site-footer__policies { display: flex; flex-wrap: wrap; gap: var(--sp-5); }
.site-footer__policies a {
  font-size: var(--fs-xs);
  color: rgb(255 255 255 / 60%);
  text-decoration: none;
  transition: color 0.2s var(--ease);
}
.site-footer__policies a:hover,
.site-footer__policies a[aria-current="page"] { color: var(--gold-400); }

/* Contact page ------------------------------------------------------------
   The only page that takes input, so the only one that needs form controls.
   Square corners and hairline borders, like every other surface on the site —
   a rounded, shadowed input would read as a different product. */
.contact-note {
  margin-top: var(--sp-6);
  max-width: 62ch;
  font-size: var(--fs-xs);
  color: var(--color-text-muted);
}
.contact-card__action { margin-top: var(--sp-4); font-size: var(--fs-sm); }

/* Form beside its guidance panel. The panel comes second in the source, so a
   keyboard or screen-reader user reaches the form first — it is what they came
   for — and the grid puts the panel on the right without reordering anything. */
.enquiry { display: grid; gap: var(--sp-8); align-items: start; }
@media (min-width: 62rem) {
  .enquiry { grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); gap: var(--sp-10); }
}
.enquiry__aside {
  padding: var(--sp-6);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--white) 72%, transparent);
}

.form__grid { display: grid; gap: var(--sp-5); }
@media (min-width: 40rem) {
  .form__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .field--wide { grid-column: 1 / -1; }
}

.field { display: grid; gap: var(--sp-2); margin: 0; }
.field__label {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--gray-600);
}
.field__req { color: var(--gold-600); }
.field__opt { text-transform: none; letter-spacing: 0.04em; color: var(--gray-400); }

.field__input {
  width: 100%;
  padding: var(--sp-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--white);
  font-family: inherit;
  font-size: var(--fs-base);
  color: var(--color-text);
  transition: border-color 0.2s var(--ease), box-shadow 0.2s var(--ease);
}
.field__input::placeholder { color: var(--gray-400); }
.field__input:hover { border-color: var(--gray-300); }
.field__input:focus-visible {
  outline: none;
  border-color: var(--gold-500);
  /* A ring rather than a glow: it reads at any zoom and against any background,
     and it does not shift the control by a pixel the way a border change would. */
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold-500) 28%, transparent);
}
/* Server-side errors set aria-invalid, so the styling follows the state that is
   actually announced rather than a separate class that could disagree with it. */
.field__input[aria-invalid="true"] {
  border-color: #b4402f;
  background: #fdf6f5;
}
.field__textarea { resize: vertical; min-height: 11rem; line-height: 1.6; }

/* Native select, restyled. The chevron is ours; the menu stays the platform's,
   which is the part users actually need to be familiar. */
.field__select { position: relative; display: block; }
.field__select select {
  appearance: none;
  padding-right: var(--sp-9);
  cursor: pointer;
}
.field__chevron {
  position: absolute;
  top: 50%;
  right: var(--sp-4);
  translate: 0 -50%;
  pointer-events: none;
  color: var(--gray-600);
}

.field__error {
  display: block;
  font-size: var(--fs-xs);
  font-weight: 500;
  color: #a3372a;
}

/* Honeypot. Positioned off screen rather than display:none — a bot that skips
   unrendered fields would sail past a hidden one, and this needs to be filled
   in to work. aria-hidden and tabindex keep it away from real users. */
.form__trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* Submit in flight. Kept enabled — main.js drops the duplicate submit instead
   of disabling the control, which would strip its accessible name mid-action. */
.btn.is-busy { opacity: 0.7; cursor: progress; }
.btn.is-busy .icon { animation: btn-busy 0.9s linear infinite; }
@keyframes btn-busy {
  0%   { opacity: 0.35; }
  50%  { opacity: 1; }
  100% { opacity: 0.35; }
}

.form__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-5);
  margin-top: var(--sp-7);
}
.form__required, .form__privacy {
  font-size: var(--fs-xs);
  color: var(--color-text-muted);
}
.form__privacy {
  margin-top: var(--sp-6);
  padding-top: var(--sp-5);
  max-width: 62ch;
  border-top: 1px solid var(--color-border);
}

/* Result banners. The left bar carries the meaning, so the state survives being
   read in greyscale or by someone who cannot separate red from green. */
.form-note {
  display: flex;
  gap: var(--sp-4);
  margin-bottom: var(--sp-6);
  padding: var(--sp-5);
  border: 1px solid var(--color-border);
  border-left-width: 3px;
  border-radius: var(--radius);
}
.form-note--ok  { border-left-color: var(--gold-500); background: color-mix(in srgb, var(--gold-500) 8%, var(--white)); }
.form-note--bad { border-left-color: #b4402f; background: #fdf6f5; }
.form-note__icon { flex: none; margin-top: 2px; color: var(--gold-600); stroke-width: 2.5; }
.form-note__title { font-weight: 700; color: var(--color-heading); }
.form-note__text { margin-top: var(--sp-2); font-size: var(--fs-sm); color: var(--color-text-muted); }
.form-note__text + .link-arrow { margin-top: var(--sp-4); }

/* Delivery log (References page) -------------------------------------------
   The third presentation of projects.items. Home renders them as covers in a
   row; this renders them as a log — index, thumbnail, category, scope, on one
   line each.

   A log rather than a card grid for two reasons. It is the shape a reference
   list actually has, and it survives its own content: three entries look
   deliberate and thirty would still read, where a three-card row looks like a
   page that ran out of things to say. */
.records {
  display: grid;
  gap: 0;
  border-top: 1px solid var(--color-border);
}
.record {
  position: relative;
  display: grid;
  gap: var(--sp-4) var(--sp-6);
  align-items: center;
  padding-block: var(--sp-6);
  border-bottom: 1px solid var(--color-border);
  transition: border-color 0.3s var(--ease);
}
.record:hover { border-bottom-color: var(--gold-500); }

@media (min-width: 62rem) {
  /* index · thumbnail · body · arrow */
  .record {
    grid-template-columns: 5.5rem 15rem 1fr 2.5rem;
    padding-block: var(--sp-7);
  }
}
/* Below that the arrow column would sit alone on its own line. */
@media (max-width: 61.99rem) {
  .record__arrow { display: none; }
}

/* Record number, set as a technical marking rather than a heading. */
.record__index {
  display: flex;
  align-items: baseline;
  gap: var(--sp-3);
  font-family: var(--font-mono);
}
@media (min-width: 62rem) {
  .record__index { flex-direction: column; gap: var(--sp-1); }
}
.record__label {
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--gray-400);
}
.record__num {
  font-family: var(--font-display);
  font-size: clamp(1.75rem, 3vw, 2.5rem);
  font-weight: 800;
  line-height: 0.85;
  font-variant-numeric: tabular-nums;
  color: rgb(198 156 67 / 40%);
  transition: color 0.3s var(--ease);
}
.record:hover .record__num { color: var(--gold-500); }

.record__media {
  margin: 0;
  aspect-ratio: 3 / 2;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
}
.record__media img { transition: transform 0.8s var(--ease-out); }
.record:hover .record__media img { transform: scale(1.05); }

.record__cat {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--gold-600);
}
.record__title { margin-top: var(--sp-2); font-size: var(--fs-xl); }
/* Stretched link: the row is the target, so the small print does not have to
   be aimed at. z-index keeps it above the thumbnail's own stacking context. */
.record__link { color: inherit; text-decoration: none; }
.record__link::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 3;
}
.record__text {
  margin-top: var(--sp-3);
  max-width: 58ch;
  font-size: var(--fs-sm);
  color: var(--color-text-muted);
}
/* Named action, revealed on hover. Present but silent at rest — a row of live
   "ask about this" labels would compete with the entries themselves. */
.record__cue {
  display: inline-block;
  margin-top: var(--sp-4);
  font-family: var(--font-mono);
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--gold-600);
  opacity: 0;
  translate: -6px 0;
  transition: opacity 0.3s var(--ease), translate 0.3s var(--ease-out);
}
.record:hover .record__cue,
.record:focus-within .record__cue { opacity: 1; translate: 0 0; }

.record__arrow {
  justify-self: end;
  color: var(--gray-300);
  transition: color 0.3s var(--ease), translate 0.3s var(--ease-out);
}
.record:hover .record__arrow { color: var(--gold-500); translate: 5px 0; }

/* Touch devices never fire hover, so the cue would stay invisible and the row
   would give no sign it is clickable. */
@media (hover: none) {
  .record__cue { opacity: 1; translate: 0 0; }
}

/* Stepped sequence ---------------------------------------------------------
   Used twice: the maintenance cycle on Services (four stages) and "what happens
   next" on Contact (three). It only ever appears on a dark band. */
.cycle {
  position: relative;
  display: grid;
  gap: var(--sp-6);
}
/* auto-fit, not a fixed count. A hardcoded repeat(4, 1fr) fitted the four
   maintenance stages and left the three Contact steps sitting in three of four
   tracks with a visible empty column at the end. */
@media (min-width: 48rem) {
  .cycle {
    grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
    gap: var(--sp-7) var(--sp-6);
  }
}

.cycle__step { position: relative; padding-top: var(--sp-5); }
/* Node at the left end of the rule — where the step starts, marked. */
.cycle__step::before {
  content: "";
  position: absolute;
  top: -2px;
  left: 0;
  width: 6px;
  height: 6px;
  background: var(--gold-500);
}
/* Chevron in the gutter, pointing at the next step. Drawn as two borders
   rotated 45 degrees rather than as a glyph: no icon needed and it stays crisp
   at any zoom.

   Only from 68rem, not from the 48rem where the grid starts. Between the two,
   four steps wrap to 2x2 — and the item ending the first row is not
   :last-child, so it would point right at the edge of the grid and nothing
   else. Above 68rem every step is guaranteed on one row. */
@media (min-width: 68rem) {
  .cycle__step:not(:last-child)::after {
    content: "";
    position: absolute;
    top: 2.4rem;
    /* Right edge one half-gutter out, then pushed back by half its own width,
       which lands it centred in the gap rather than against the far side. */
    right: calc(var(--sp-6) / -2);
    translate: 50% 0;
    width: 7px;
    height: 7px;
    border-top: 1px solid rgb(198 156 67 / 55%);
    border-right: 1px solid rgb(198 156 67 / 55%);
    rotate: 45deg;
  }
}
/*
   The rule above each stage wipes in from the left when the step reveals.
   Because the steps are staggered by --d, the four wipes run in sequence
   across the row, which is what makes it read as a progression rather than
   four boxes that appeared at once.
*/
.cycle__rule {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 2px;
  background: linear-gradient(90deg, var(--gold-500), rgb(198 156 67 / 15%));
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.9s var(--ease-out);
  transition-delay: var(--d, 0ms);
}
.js [data-reveal].is-visible .cycle__rule { transform: scaleX(1); }
/* Without JavaScript there is no .is-visible, so the rules must already be
   drawn — otherwise the row loses its only structural line. */
html:not(.js) .cycle__rule { transform: scaleX(1); }

.cycle__num {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0.14em;
  font-variant-numeric: tabular-nums;
  color: var(--gold-400);
}
/*
   White, explicitly.
   h3 inherits color: var(--color-heading) — ink-800 — from the base heading
   rule, which on a dark band is very nearly the background colour. Every other
   dark-band component states its own white (.feature__title, .process__title,
   .tile__title); this one did not, so the step headings were reading as almost
   invisible on both pages that use it.
*/
.cycle__title {
  margin-top: var(--sp-3);
  font-size: var(--fs-xl);
  color: var(--white);
}
.cycle__text {
  margin-top: var(--sp-3);
  font-size: var(--fs-sm);
  /* 68% was too close to the ground once the headings above it went properly
     white — the pair read as one grey block. */
  color: rgb(255 255 255 / 76%);
}

/* Closing line. Four stages laid left to right imply an end point; this says
   the sequence returns to the start, which is the whole claim of the section. */
.cycle__loop {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  margin-top: var(--sp-9);
  padding-top: var(--sp-6);
  border-top: 1px solid rgb(255 255 255 / 10%);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgb(255 255 255 / 60%);
}
/* A dashed ring turning slowly. 24s is its own period — no other moving element
   on the site shares it, so nothing looks driven by a common clock. */
.cycle__loop-mark {
  flex: none;
  width: 26px;
  height: 26px;
  border: 1px dashed rgb(198 156 67 / 65%);
  border-radius: 50%;
  animation: loop-turn 24s linear infinite;
}
@keyframes loop-turn {
  to { rotate: 360deg; }
}

/* The fixed section-index rail was removed. Section numbering now lives inline
   in each section's eyebrow (see .eyebrow[data-n]), which gives the same
   document-like structure without a permanent overlay competing with content. */

/* CTA band --------------------------------------------------------------- */
/*
   The closing band. It is the last thing on the page and the only place a
   visitor is asked to do something, so it gets the most light in the palette:
   a strong gold field entering from the top right, over the deepest base.

   The diagonal hatch that used to cover it is gone. A repeating line pattern
   behind the one call to action on the page competes with it, and this section
   should be the quietest surface with the loudest content.
*/
.cta-band {
  position: relative;
  overflow: hidden;
  padding-block: clamp(var(--sp-10), 9vw, 7.5rem);
  color: var(--white);
  background: var(--ink-900);
  background-image:
    radial-gradient(72% 130% at 88% 0%, rgb(198 156 67 / 30%), transparent 62%),
    radial-gradient(60% 110% at 0% 100%, rgb(190 180 165 / 12%), transparent 60%),
    linear-gradient(125deg, var(--ink-950) 0%, var(--ink-800) 100%);
}

/* Angled top edge, filled with the colour of the References band above.
   This is now the only angled join on the page — the matching one under the
   stats band was removed, because a diagonal reads well between two dark tones
   and badly as a pale wedge driven into a dark band. Here both sides are dark,
   so it stays: a quiet closing gesture rather than a hard transition. */
.cta-band::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  z-index: 1;
  height: 3.25rem;
  background: var(--ink-950);
  clip-path: polygon(0 0, 100% 0, 0 100%);
  pointer-events: none;
}

/* Grain, so the large gold field does not band. */
.cta-band::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  opacity: 0.04;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

.cta-band__inner {
  position: relative;
  z-index: 3;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-7) var(--sp-6);
}
/* This band closes the page now that the contact strip has gone, so it carries
   more weight: a section label, a larger title, and the email alongside the
   button for anyone who would rather write directly. */
/* Between a section title and the hero headline in scale. This is the closing
   statement, so it outweighs every other section title on the page without
   competing with the hero. */
.cta-band__title {
  font-size: clamp(2rem, 2.5vw + 1.25rem, 3.125rem);
  color: var(--white);
}
.cta-band__text {
  margin-top: var(--sp-4);
  max-width: 46ch;
  color: rgb(255 255 255 / 80%);
  font-size: var(--fs-lg);
}

/* Stacked, not inline: the button and the email are two different commitments —
   one opens a form, the other opens a mail client — and side by side they read
   as a pair of equal options. Stacked, the button leads. */
.cta-band__actions {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-4);
}
.cta-band__email {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  letter-spacing: 0.04em;
  color: rgb(255 255 255 / 82%);
  text-decoration: none;
  border-bottom: 1px solid rgb(198 156 67 / 45%);
  padding-bottom: 2px;
  transition: color 0.2s var(--ease), border-color 0.2s var(--ease);
}
.cta-band__email .icon { width: 18px; height: 18px; color: var(--gold-400); }
.cta-band__email:hover { color: var(--white); border-bottom-color: var(--gold-400); }

/* Contact grid -------------------------------------------------------------
   No longer used on the home page — the strip was removed as it duplicated the
   footer. Kept because the Contact page will need exactly this, in the same way
   .range is kept for the Products page. */
/* Patrol ------------------------------------------------------------------
   An armored vehicle crossing the band on a faint ground line. The most literal
   motif on the site, and the right one on the contact page: this is the range
   the company leads with, so it is the product that should be moving rather
   than another abstract instrument.

   Geometry note — the numbers below are tied to the SVG and must move together.
   The drawing is 240x96 units rendered at 15rem x 6rem, so one unit is
   0.0625rem. Its ground line sits at y=78, which is 18 units up from the
   bottom: 18 x 0.0625rem = 1.125rem. That is where --ground has to be, or the
   vehicle floats above the line or sinks through it. */
.section--patrol { position: relative; overflow: hidden; }
.patrol {
  --ground: 1.125rem;

  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  translate: 0 -50%;
  z-index: 0;
  height: 6rem;
  pointer-events: none;
  color: var(--gold-600);
  opacity: 0.3;
}

/* Static ground: the vehicle travels over it, so it cannot be part of the
   drawing. Hairline plus a run of short scale marks below it, fading out at
   both edges of the band. */
.patrol__ground {
  position: absolute;
  left: 0;
  right: 0;
  bottom: var(--ground);
  height: 7px;
  background-image:
    linear-gradient(currentColor 0 0),
    repeating-linear-gradient(90deg, currentColor 0 1px, transparent 1px 22px);
  background-repeat: no-repeat, repeat-x;
  background-size: 100% 1px, 22px 5px;
  background-position: left top, left bottom;
  opacity: 0.5;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}

/*
   Two animations on one element, targeting two different properties. `transform`
   carries the crossing and `translate` carries the suspension bob — they
   compose rather than overwrite, so neither needs a wrapper element.

   The crossing runs on transform rather than `left` for the same reason as the
   Services drone: animating `left` relayouts the band on every frame, for as
   long as the page is open. Offsets are self-relative (100% = the vehicle's own
   width) so it clears both edges completely at any size.
*/
.patrol__craft {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 15rem;
  height: 6rem;
  animation:
    patrol-cross 50s linear infinite,
    patrol-bob 1.7s ease-in-out infinite;
}
@keyframes patrol-cross {
  from { transform: translateX(calc(-100% - 3rem)); }
  to   { transform: translateX(calc(100vw + 3rem)); }
}
/* Barely a pixel and a half. Enough to stop the vehicle looking like a decal
   sliding on glass; not enough to read as bouncing. */
@keyframes patrol-bob {
  0%, 100% { translate: 0 0; }
  50%      { translate: 0 -1.5px; }
}

/* Tyres stay still and the hubs turn. A rotating tyre outline shows nothing —
   it is a circle — so the spoked hub is what carries the movement.
   transform-box: fill-box makes the origin the hub's own centre rather than the
   whole SVG's, which is the difference between a wheel turning and the wheel
   orbiting the vehicle.

   Lives inside svg-apc.php, so this rule serves every use of that partial —
   the Contact band and the home page ground band alike. */
.motif-hub {
  transform-box: fill-box;
  transform-origin: center;
  animation: hub-spin 2.8s linear infinite;
}
@keyframes hub-spin {
  to { rotate: 360deg; }
}

/* On a phone the band is narrow and the cards fill it; a 15rem vehicle behind
   them is clutter. The ground line stays as a horizon. */
@media (max-width: 47.99rem) {
  .patrol__craft { display: none; }
}

/* Contact cards ------------------------------------------------------------ */
.contact-grid {
  position: relative;
  z-index: 1; /* above the patrol motif */
  display: grid;
  gap: var(--sp-5);
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
}

.contact-card {
  position: relative;
  overflow: hidden;
  padding: var(--sp-6);
  /* Slightly translucent rather than flat grey, so the vehicle and its ground
     line read faintly behind the cards instead of stopping dead at their
     edges. */
  background: color-mix(in srgb, var(--gray-50) 88%, transparent);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  transition: border-color 0.25s var(--ease), box-shadow 0.25s var(--ease),
              background-color 0.25s var(--ease);
}
/* Same rule as the card grids above: lights up, does not move. */
.contact-card:hover {
  border-color: var(--gold-500);
  background: var(--white);
  box-shadow: var(--shadow-md);
}
/* Gold rule that wipes across the top edge on hover. Its own element rather
   than a pseudo: ::after is the cursor spotlight, and ::before would sit under
   the card background rather than on its top edge without extra stacking work
   that buys nothing. */
.contact-card__rule {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 2px;
  background: linear-gradient(90deg, var(--gold-500), rgb(198 156 67 / 20%));
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.55s var(--ease-out);
}
.contact-card:hover .contact-card__rule,
.contact-card:focus-within .contact-card__rule { transform: scaleX(1); }

.contact-card__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  margin-bottom: var(--sp-5);
}
.contact-card__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 42px;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--gold-500) 12%, transparent);
  color: var(--gold-600);
}
.contact-card__icon .icon { stroke-dasharray: 100; stroke-dashoffset: 0; }
.contact-card:hover .contact-card__icon .icon { animation: icon-draw 0.85s var(--ease-out); }

/* Channel index, in the same technical register as the catalogue and record
   numbering, so the page reads as part of the same document. */
.contact-card__num {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0.1em;
  font-variant-numeric: tabular-nums;
  color: var(--gray-300);
  transition: color 0.25s var(--ease);
}
.contact-card:hover .contact-card__num { color: var(--gold-500); }

.contact-card__label {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--gray-600);
}
.contact-card__value {
  margin-top: var(--sp-2);
  font-size: var(--fs-base);
  font-weight: 500;
  color: var(--ink-800);
  /* Long addresses must not push the card wider than its grid track. */
  overflow-wrap: anywhere;
}
.contact-card__value a { color: inherit; text-decoration: none; }
.contact-card__value a:hover { text-decoration: underline; }

/* Copy to clipboard. Rendered with the `hidden` attribute and unhidden by
   main.js only where the Clipboard API actually exists — which excludes any
   page served over plain HTTP, browser regardless. */
.copy {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  margin-top: var(--sp-4);
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: transparent;
  font-family: var(--font-mono);
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--gray-600);
  cursor: pointer;
  transition: color 0.2s var(--ease), border-color 0.2s var(--ease),
              background-color 0.2s var(--ease);
}
.copy:hover { color: var(--ink-800); border-color: var(--gray-300); background: var(--white); }
.copy__icon { width: 15px; height: 15px; }
.copy__icon--done { display: none; }
.copy.is-copied { color: var(--gold-600); border-color: var(--gold-500); }
.copy.is-copied .copy__icon--idle { display: none; }
.copy.is-copied .copy__icon--done { display: inline-block; }

/* Page hero (inner pages) ------------------------------------------------
   Same visual language as the home hero, deliberately quieter: one glow rather
   than two, no sweep, no ticker. An inner page should feel like the same site,
   not compete with the front door. */
.page-hero {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: var(--ink-950);
  color: var(--white);
  padding-block: clamp(var(--sp-9), 8vw, 6.5rem) clamp(var(--sp-9), 8vw, 6rem);
  /* Enough of the screen to feel like the top of a page rather than a caption
     bar, but well short of the home hero's 900px — an inner page should get to
     its content quickly. */
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: min(calc(100vh - var(--header-h)), 520px);
  min-height: min(calc(100svh - var(--header-h)), 520px);
}

/*
   The home hero's layers, reused and re-scaled. The radar and the registration
   frame were built there, so an inner page gets the same instrument set without
   a second implementation — only the geometry differs, because this band is
   roughly half the height.
*/
.page-hero .hero__radar {
  top: -26%;
  right: -14%;
  width: min(34rem, 80vw);
}
/* The home hero insets the frame to clear its ticker; there is no ticker here. */
.page-hero .hero__frame { bottom: var(--fi); }

/* Tracked drone -----------------------------------------------------------
   The alternative to the radar, used where a third turning dish would read as
   the same effect reused. It is a different KIND of motion: the radar rotates
   in place, this crosses. Nothing here shares a period with anything else on
   the site — traverse 46s, bob 6.5s, dashes 40s, lock pulse 3.2s.

   Altitude is set once, as a percentage of the band, and the track line and the
   craft both read it — so they can never drift apart. */
.page-hero { --alt: 30%; }

/* The flight track: a dashed line at the craft's altitude, drifting slowly in
   the direction of travel. It is what makes the traverse read as a path being
   followed rather than an object sliding past. */
.hero__track {
  position: absolute;
  top: var(--alt);
  left: 0;
  right: 0;
  z-index: 1;
  height: 1px;
  background-image: repeating-linear-gradient(
    90deg,
    rgb(198 156 67 / 22%) 0 14px,
    transparent 14px 30px
  );
  background-size: 30px 1px;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 18%, #000 82%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 18%, #000 82%, transparent);
  animation: track-drift 40s linear infinite;
}
@keyframes track-drift {
  to { background-position: -600px 0; }
}

/* Outer: the traverse. Starts and ends fully outside the band, so the craft is
   never seen appearing or vanishing mid-air. */
.hero__drone {
  position: absolute;
  top: var(--alt);
  left: 0;
  z-index: 2;
  width: clamp(7rem, 13vw, 10.5rem);
  /* Lifts the craft so the track line runs through its body rather than under
     its rotors. */
  margin-top: -1.4rem;
  color: var(--gold-500);
  opacity: 0.5;
  /* Its own parallax rate, between the contours and the dots, so it sits at a
     depth of its own like the radar it replaces. */
  translate: calc(var(--pmx, 0px) * -0.3) calc(var(--pmy, 0px) * -0.3);
  transition: translate 0.75s var(--ease);
  animation: drone-cross 46s linear infinite;
}
/* Travels on `transform`, not on `left`: animating `left` relayouts the hero on
   every frame for 46 seconds, while a transform stays on the compositor. It
   composes with the `translate` parallax above rather than overwriting it,
   which is why the two can share one element.

   The offsets are self-relative (100% = the craft's own width), so it clears
   the edge completely at both ends whatever size it renders at. */
@keyframes drone-cross {
  from { transform: translateX(calc(-100% - 4rem)); }
  to   { transform: translateX(calc(100vw + 4rem)); }
}

/* Middle: the bob. A drone holding altitude is never perfectly still, and the
   traverse alone looks like a sprite on a rail. */
.hero__drone-bob {
  display: block;
  position: relative;
  animation: drone-bob 6.5s ease-in-out infinite;
}
@keyframes drone-bob {
  0%, 100% { translate: 0 0; rotate: -1.2deg; }
  50%      { translate: 0 -9px; rotate: 1.2deg; }
}

.hero__drone-craft { display: block; width: 100%; height: auto; }

/* Sensor cone hanging from the gimbal. Clipped to a trapezoid rather than a
   triangle — a real optic has a footprint, not a point. */
.hero__drone-scan {
  position: absolute;
  top: 92%;
  left: 50%;
  width: 260%;
  height: 15rem;
  translate: -50% 0;
  background: linear-gradient(180deg,
    rgb(198 156 67 / 26%) 0%,
    rgb(198 156 67 / 6%) 45%,
    transparent 100%);
  clip-path: polygon(44% 0%, 56% 0%, 100% 100%, 0% 100%);
  animation: scan-breathe 5.5s ease-in-out infinite;
}
@keyframes scan-breathe {
  0%, 100% { opacity: 0.5; }
  50%      { opacity: 1; }
}

/* Lock bracket. The same corner registration marks the cards use — here they
   mean the craft has been acquired, which is the counter-drone half of the
   range doing its job. */
.hero__drone-lock {
  position: absolute;
  inset: -26% -12%;
  background:
    linear-gradient(currentColor 0 0), linear-gradient(currentColor 0 0),
    linear-gradient(currentColor 0 0), linear-gradient(currentColor 0 0),
    linear-gradient(currentColor 0 0), linear-gradient(currentColor 0 0),
    linear-gradient(currentColor 0 0), linear-gradient(currentColor 0 0);
  background-repeat: no-repeat;
  background-size: 12px 1px, 1px 12px, 12px 1px, 1px 12px,
                   12px 1px, 1px 12px, 12px 1px, 1px 12px;
  background-position:
    left top, left top, right top, right top,
    left bottom, left bottom, right bottom, right bottom;
  animation: lock-pulse 3.2s ease-in-out infinite;
}
@keyframes lock-pulse {
  0%, 100% { opacity: 0.35; }
  50%      { opacity: 0.9; }
}

/* Below the desktop breakpoint the band is short and the headline fills it, so
   the cone and the lock become clutter behind text. The craft and its track
   stay — they are the part that reads at a glance. */
@media (max-width: 47.99rem) {
  .hero__drone-scan, .hero__drone-lock { display: none; }
  .hero__drone { opacity: 0.34; }
}

/* Scanning sweep ----------------------------------------------------------
   The third motif. A beam descends the band and the marks it crosses light up
   as it reaches them — the fourth kind of motion on the site, after the
   radar's rotation, the About ping's expansion and the drone's traverse.

   One cycle is 7s: 4.34s of travel, then a 2.66s hold off the bottom edge. The
   hold matters. A beam that restarts the instant it leaves reads as a loop; a
   beam that pauses reads as a sweep being repeated. */
.hero__scan {
  position: absolute;
  inset: 0;
  z-index: 1;
  /* Its own parallax rate, as the radar and the drone have. */
  translate: calc(var(--pmx, 0px) * -0.22) calc(var(--pmy, 0px) * -0.22);
  transition: translate 0.75s var(--ease);
}

/*
   The beam box is the full height of the band and its leading edge is drawn at
   the box's own top, so a `translate` of 100% moves that edge exactly one band
   height — self-relative, no viewport units, and correct at any size.

   It also keeps the animation on `translate` alone. Animating `top` would
   relayout the band on every frame, for as long as the page is open.
*/
.hero__scan-beam {
  position: absolute;
  inset: 0;
  animation: scan-sweep 7s var(--ease) infinite;
}
@keyframes scan-sweep {
  0%   { translate: 0 -8%; }
  62%  { translate: 0 108%; }
  100% { translate: 0 108%; }
}
/* Trail above the line — what the beam has just passed over. */
.hero__scan-beam::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 8rem;
  translate: 0 -100%;
  background: linear-gradient(180deg, transparent, rgb(198 156 67 / 14%));
}
/* The leading edge. Faded at both ends so it never collides with the band's
   own edges as a hard horizontal rule. */
.hero__scan-beam::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 2px;
  background: linear-gradient(90deg,
    transparent,
    rgb(198 156 67 / 70%) 20%,
    rgb(217 182 101 / 85%) 50%,
    rgb(198 156 67 / 70%) 80%,
    transparent);
}

/* Detection marks. Each one flashes at the delay the partial worked out for its
   depth, which is what makes the beam look like it is finding them rather than
   like separate lights blinking on a timer. */
.hero__scan-node {
  position: absolute;
  left: var(--nx);
  top: var(--ny);
  width: 15px;
  height: 15px;
  translate: -50% -50%;
  opacity: 0.16;
  background:
    linear-gradient(var(--gold-400) 0 0) center / 100% 1px no-repeat,
    linear-gradient(var(--gold-400) 0 0) center / 1px 100% no-repeat;
  animation: node-flash 7s linear var(--nd) infinite;
}
@keyframes node-flash {
  0%       { opacity: 0.16; scale: 1; }
  3%       { opacity: 1;    scale: 1.4; }
  12%      { opacity: 0.16; scale: 1; }
  100%     { opacity: 0.16; scale: 1; }
}

/* On a phone the band is short and the headline fills it. The beam still reads;
   eight crosshairs behind three lines of text do not. */
@media (max-width: 47.99rem) {
  .hero__scan-node { display: none; }
}
.page-hero__bg { position: absolute; inset: 0; z-index: -1; overflow: hidden; }
.page-hero__bg::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 5;
  background: radial-gradient(125% 110% at 45% 35%, transparent 40%, rgb(8 8 10 / 66%) 100%);
}
.page-hero__inner { max-width: 54rem; position: relative; }

.page-hero__title {
  font-size: var(--fs-3xl);
  color: var(--white);
  letter-spacing: 0.006em;
  line-height: 1;
}
.page-hero__lead {
  margin-top: var(--sp-5);
  max-width: 54ch;
  font-size: var(--fs-lg);
  color: rgb(255 255 255 / 76%);
  animation: fade-up 0.8s var(--ease-out) var(--wd, 0ms) both;
}
.page-hero.is-live .hero__spot { opacity: 1; }

/* The visual breadcrumb was removed from the page header — on a site three
   levels deep at most, it restated the navigation without adding orientation.
   The BreadcrumbList structured data in head.php stays: search engines use it
   for result trails, and that costs the page nothing. */

/* Editorial layout ------------------------------------------------------- */
/* Heading and standfirst sit side by side, the body runs in three columns
   beneath a rule. Reads as a considered article rather than a wall of text —
   and it gives long-form copy somewhere to breathe without a sidebar. */
.editorial {
  display: grid;
  gap: var(--sp-6);
  align-items: start;
}
@media (min-width: 62rem) {
  .editorial { grid-template-columns: 1fr 1fr; gap: var(--sp-9); }
}
/* The standfirst. Larger and heavier than the body — it is the one sentence a
   reader who skims this page will take away, so it should not sit at the same
   weight as the paragraphs beneath it. */
.editorial__lead {
  max-width: 44ch;
  font-size: clamp(1.25rem, 1.2vw + 1rem, 1.625rem);
  line-height: 1.4;
  font-weight: 500;
  letter-spacing: -0.015em;
  color: var(--ink-800);
  text-wrap: pretty;
}
.editorial__body {
  display: grid;
  gap: var(--sp-6);
  margin-top: var(--sp-8);
  padding-top: var(--sp-8);
  border-top: 1px solid var(--color-border);
}
/*
   Three columns only from 68rem, not 62rem. At the narrower breakpoint each
   column came out around 290px, which is roughly 36 characters a line — below
   the point where continuous prose stays comfortable to read. It goes straight
   from one column to three: with three paragraphs, a two-column stage would
   leave one of them orphaned on a second row.
*/
@media (min-width: 68rem) {
  .editorial__body { grid-template-columns: repeat(3, 1fr); gap: var(--sp-7); }
}
.editorial__body .prose { max-width: none; font-size: var(--fs-base); }

/* Numbered rule above each paragraph, so the three read as parts of one
   argument rather than as a wall broken into columns. Counter rather than
   markup, so a fourth paragraph numbers itself. */
@media (min-width: 68rem) {
  .editorial__body { counter-reset: para; }
  .editorial__body .prose { counter-increment: para; }
  .editorial__body .prose::before {
    content: counter(para, decimal-leading-zero);
    display: block;
    margin-bottom: var(--sp-4);
    padding-top: var(--sp-3);
    border-top: 2px solid var(--gold-500);
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.16em;
    color: var(--gold-600);
  }
}

/* Statement ----------------------------------------------------------------
   The strongest single line on the page, so it gets a band of its own: more
   height than a normal section, a gold wash rising from the top edge, and grain
   so the light field is not flat. Nothing else in here — the sentence is the
   content and anything alongside it would compete. */
.statement {
  position: relative;
  overflow: hidden;
  padding-block: clamp(var(--sp-10), 9vw, 8rem);
  background: var(--color-bg-tint);
  background-image: radial-gradient(70% 110% at 50% 0%, rgb(198 156 67 / 12%), transparent 66%);
  border-block: 1px solid var(--color-border);
}
.statement::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.05;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
.statement > .container { position: relative; z-index: 1; }

/*
   Sonar ping behind the sentence: static range rings, with two pulses expanding
   out through them.

   The motion is an expansion, not a rotation — deliberately. Both radars and
   the About compass turn, so a third turning object would have read as the same
   effect used again rather than as a different instrument.

   The container stays still and only the two pseudo-elements animate. If the
   container itself scaled, its children would scale with it and the rings would
   travel together instead of independently.
*/
.statement__ping {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 0;
  width: min(32rem, 82vw);
  aspect-ratio: 1;
  translate: -50% -50%;
  pointer-events: none;
  border-radius: 50%;
  /* Range rings, static. */
  background-image:
    repeating-radial-gradient(circle at center, transparent 0 42px, rgb(8 8 10 / 7%) 42px 43px);
  -webkit-mask-image: radial-gradient(circle, #000 20%, transparent 72%);
  mask-image: radial-gradient(circle, #000 20%, transparent 72%);
}
.statement__ping::before,
.statement__ping::after {
  content: "";
  position: absolute;
  inset: 0;
  border: 1.5px solid var(--gold-500);
  border-radius: 50%;
  opacity: 0;
  /* Eased so the pulse leaves quickly and slows as it widens, the way a real
     return does — a linear expansion reads mechanical. */
  animation: sonar 7s cubic-bezier(0.16, 0.7, 0.3, 1) infinite;
}
/* Half a cycle apart, so there is always one ring in flight. */
.statement__ping::after { animation-delay: 3.5s; }

@keyframes sonar {
  0%   { scale: 0.16; opacity: 0; }
  12%  { opacity: 0.4; }
  100% { scale: 1; opacity: 0; }
}

/* Names what the sentence is. Without it a large centred line reads as a
   pull-quote from somewhere else rather than as a commitment being made. */
.statement__label {
  text-align: center;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--gold-600);
}
.statement__rule {
  display: block;
  width: 48px;
  height: 3px;
  margin: var(--sp-5) auto var(--sp-6);
  background: linear-gradient(90deg, var(--gold-600), var(--gold-400));
}
/* Deliberately NOT uppercase, unlike the other large headings. This is a full
   sentence rather than a label, and it is the one place on the site where
   sentence case earns its keep — it reads as something a person said. */
.statement__quote {
  max-width: 22ch;
  margin-inline: auto;
  text-align: center;
  font-size: clamp(1.625rem, 2.9vw + 0.9rem, 2.875rem);
  line-height: 1.2;
  letter-spacing: -0.022em;
  font-weight: 700;
  color: var(--ink-800);
  text-wrap: balance;
}
/* Second half in gold, on its own line — the same split the hero headline uses,
   and it replaces the dash that used to join the two halves. */
.statement__accent {
  display: block;
  margin-top: 0.25em;
  color: var(--gold-600);
}

/* Principles ------------------------------------------------------------- */
.principles { display: grid; gap: var(--sp-5); }
@media (min-width: 48rem) { .principles { grid-template-columns: repeat(2, 1fr); gap: var(--sp-6); } }

.principle {
  display: flex;
  gap: var(--sp-5);
  padding: var(--sp-6);
  background: var(--white);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  transition: border-color 0.25s var(--ease), box-shadow 0.25s var(--ease);
}
.principle:hover { border-color: var(--gold-500); box-shadow: var(--shadow-md); }
.principle__num {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--fs-lg);
  font-weight: 700;
  line-height: 1.1;
  color: var(--gold-500);
  letter-spacing: 0.04em;
}
.principle__title { font-size: var(--fs-lg); letter-spacing: -0.01em; }
.principle__text { margin-top: var(--sp-3); font-size: var(--fs-sm); color: var(--color-text-muted); }

/* Tile links (compact category rows, About page) -------------------------- */
.tile-links { display: grid; gap: var(--sp-3); }
@media (min-width: 48rem) { .tile-links { grid-template-columns: repeat(2, 1fr); } }

.tile-link {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-4) var(--sp-5);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  background: var(--white);
  text-decoration: none;
  color: var(--ink-800);
  transition: border-color 0.2s var(--ease), background-color 0.2s var(--ease);
}
.tile-link:hover { border-color: var(--gold-500); background: color-mix(in srgb, var(--gold-500) 4%, var(--white)); color: var(--ink-800); }
.tile-link__icon {
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--ink-800) 6%, transparent);
  color: var(--ink-700);
  transition: background-color 0.2s var(--ease), color 0.2s var(--ease);
}
.tile-link:hover .tile-link__icon {
  background: color-mix(in srgb, var(--gold-500) 18%, transparent);
  color: var(--gold-600);
}
.tile-link__title { flex: 1; font-size: var(--fs-sm); font-weight: 600; }
.tile-link__arrow {
  width: 18px;
  height: 18px;
  color: var(--gray-400);
  transition: transform 0.2s var(--ease), color 0.2s var(--ease);
}
.tile-link:hover .tile-link__arrow { transform: translateX(3px); color: var(--gold-600); }

/* Page head (legacy, used by the remaining placeholder pages) ------------ */
.page-head {
  padding-block: var(--sp-9);
  color: var(--white);
  background: var(--ink-900);
  background-image:
    radial-gradient(70% 130% at 80% 0%, rgb(196 186 170 / 18%), transparent 62%),
    linear-gradient(160deg, var(--ink-800), var(--ink-950));
}
.page-head__title { font-size: var(--fs-3xl); color: var(--white); }

.error-code {
  font-size: clamp(4rem, 12vw, 7rem);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.04em;
  color: var(--gray-200);
  margin-bottom: var(--sp-4);
}

/* 10 — Footer ============================================================= */
/*
   The footer was the last flat surface on the page — every dark band above it
   now carries a texture, so plain near-black read as the site running out
   rather than closing.

   A gold hairline defines the top edge against the closing band, the hero's dot
   field fades in from that edge, and grain sits over the whole thing.
*/
.site-footer {
  position: relative;
  overflow: hidden;
  padding-top: var(--sp-9);
  border-top: 1px solid rgb(198 156 67 / 30%);
  background: var(--ink-950);
  background-image:
    /* Light spilling down from the closing band above, as if the gold field up
       there were a source rather than a flat fill. */
    linear-gradient(180deg, rgb(198 156 67 / 12%), transparent 24%),
    radial-gradient(70% 90% at 12% 0%, rgb(198 156 67 / 9%), transparent 60%);
  color: rgb(255 255 255 / 70%);
  font-size: var(--fs-sm);
}

/*
   The wordmark at architectural scale, anchored to the bottom and clipped by
   the footer's own overflow so only its upper two-thirds show.

   3.5% white — at that level it is a change in the surface rather than a word
   you read, which is the difference between this device looking expensive and
   looking like a mistake. It sits under .container (z-index 1), so the legal
   line runs straight over it.
*/
.site-footer__watermark {
  position: absolute;
  left: 50%;
  bottom: -0.2em;
  z-index: 0;
  translate: -50% 0;
  font-family: var(--font-display);
  font-size: clamp(5rem, 21vw, 17rem);
  font-weight: 800;
  line-height: 0.8;
  letter-spacing: 0.06em;
  white-space: nowrap;
  color: rgb(255 255 255 / 3.5%);
  pointer-events: none;
  user-select: none;
}
/* Dot field, fading downward — densest at the join with the closing band, gone
   by the time it reaches the legal line. */
.site-footer::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image: radial-gradient(circle at center, rgb(255 255 255 / 8%) 1px, transparent 1.6px);
  background-size: 28px 28px;
  -webkit-mask-image: linear-gradient(180deg, #000, transparent 58%);
  mask-image: linear-gradient(180deg, #000, transparent 58%);
}
.site-footer::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.04;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
.site-footer > .container { position: relative; z-index: 1; }

.site-footer__grid { display: grid; gap: var(--sp-8); padding-bottom: var(--sp-8); }
/* Two columns before four: at tablet width, four columns of link text would be
   too narrow for "Armored personnel carriers" to sit on one line. */
@media (min-width: 40rem) {
  .site-footer__grid { grid-template-columns: repeat(2, 1fr); gap: var(--sp-8); }
}
@media (min-width: 68rem) {
  .site-footer__grid { grid-template-columns: 1.5fr 1.15fr 0.85fr 1.2fr; gap: var(--sp-9); }
}

/* White plate carrying the logo — see the note in footer.php for why.
   The background must be pure #fff, exactly matching the JPEG's own white, or a
   seam appears at the edge of the image. Do not swap this for a token that might
   later be tinted. */
.site-footer__plate {
  display: inline-block;
  padding: var(--sp-4) var(--sp-5);
  background: #ffffff;
  border-radius: var(--radius-lg);
  box-shadow: 0 10px 28px rgb(0 0 0 / 35%);
}
.site-footer__plate img {
  display: block;
  height: 56px;
  width: auto;
}
@media (max-width: 30rem) {
  .site-footer__plate { padding: var(--sp-3) var(--sp-4); }
  .site-footer__plate img { height: 44px; }
}

.site-footer__about { margin-top: var(--sp-6); max-width: 38ch; }

.site-footer__title {
  margin-bottom: var(--sp-5);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--white);
}
.site-footer__list { display: grid; gap: var(--sp-3); }
.site-footer a {
  color: inherit;
  text-decoration: none;
  transition: color 0.15s var(--ease);
}
/* Underline kept as the hover signal — colour change alone is not enough of an
   affordance — but set thin and dropped clear of the descenders. */
.site-footer a:hover {
  color: var(--gold-400);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.3em;
}

.site-footer__list--contact li { display: flex; gap: var(--sp-3); align-items: start; }
.site-footer__list--contact .icon { width: 18px; height: 18px; margin-top: 4px; color: var(--gold-500); }

.site-footer__bottom {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2) var(--sp-5);
  padding-block: var(--sp-6);
  border-top: 1px solid rgb(255 255 255 / 12%);
  letter-spacing: 0.06em;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: rgb(255 255 255 / 52%);
}
.site-footer__legal { letter-spacing: 0.02em; }

/* Registration marks at the footer's outer corners, matching the hero frame and
   the About section. Two marks, top corners only — the bottom pair would land
   on the watermark and read as clutter. */
.site-footer__grid::before,
.site-footer__grid::after {
  content: "";
  position: absolute;
  top: 0;
  width: 20px;
  height: 20px;
  opacity: 0.45;
  pointer-events: none;
}
.site-footer__grid::before {
  left: 0;
  border-top: 2px solid var(--gold-500);
  border-left: 2px solid var(--gold-500);
}
.site-footer__grid::after {
  right: 0;
  border-top: 2px solid var(--gold-500);
  border-right: 2px solid var(--gold-500);
}
.site-footer__grid { position: relative; padding-top: var(--sp-6); }

/* 11 — Preferences & print ================================================ */
/*
   Honour the OS "reduce motion" setting: keep every state change, drop the
   travel. Reveal elements are forced visible so nothing can stay hidden.
*/
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  /* Reveal now moves on translate/rotate, so those are what must be reset —
     resetting transform alone would leave elements tipped and offset. */
  .js [data-reveal] {
    opacity: 1 !important;
    translate: none !important;
    rotate: none !important;
  }
  .process__step::before { transform: scale(1) !important; }
  .process__step::after { transform: scaleX(1) !important; }
  .stats__item::after { transform: scaleY(1) !important; }
  .tile:hover .tile__icon .icon,
  .feature:hover .feature__icon .icon,
  .service:hover .service__icon .icon,
  .contact-card:hover .contact-card__icon { animation: none; }
  .tile:hover .tile__media img { transform: none; }
  .about-figure:hover img { transform: none; }
  /* Cards no longer move on hover at all, so only the button lift and the
     project cover zoom need neutralising here. */
  .btn:hover { transform: none; }
  .project:hover .project__media { transform: none; }
  .ticker__track { animation: none; }
  /* The radar stops turning but the rings stay — the section keeps its
     character without anything moving. Same for the compass. */
  .section--radar::after { animation: none; opacity: 0.55; }
  .section__emblem { animation: none; }
  /* The pulses stop; the range rings stay, so the section keeps its character
     with nothing in motion. */
  .statement__ping::before, .statement__ping::after { animation: none; opacity: 0.28; }
  .hero__glow, .hero__sweep { animation: none; }
  .hero__sweep { opacity: 0.5; }
  .hero__frame { animation: none; opacity: 0.45; }
  /* Pointer-driven layers are never attached by main.js under reduced motion;
     neutralised here too so nothing lingers if that ever changes. */
  .hero__spot, .hero__dots--near { display: none; }
  .tile::after, .feature::after, .section--dark::after { display: none; }
  .hero__dots { transform: none; translate: none; }
  .hero__topo, .hero__radar, .hero__glow--a, .hero__glow--b { translate: none; }
  /* The traverse must be stopped explicitly, not left to the universal rule
     above: a 0.01ms duration snaps it straight to its end frame, which parks
     the craft off the right-hand edge. Hold station on the track instead. */
  .hero__drone {
    animation: none;
    translate: none;
    transform: translateX(58vw);
  }
  .hero__drone-bob { animation: none; }
  .hero__track { animation: none; }
  /* Same trap as the drone: snapping to the end frame parks the beam below the
     band, where it is simply gone. Hold it mid-sweep instead, so the band keeps
     its horizon line and the marks stay lit. */
  .hero__scan { translate: none; }
  .hero__scan-beam { animation: none; translate: 0 42%; }
  .hero__scan-node { animation: none; opacity: 0.4; scale: 1; }
  /* Same trap as the drone and the hero beam: the universal 0.01ms duration
     snaps the crossing to its end frame and parks the vehicle off the right
     edge. Stand it on the ground line instead. */
  .patrol__craft {
    animation: none;
    transform: translateX(52vw);
    translate: none;
  }
  .motif-hub { animation: none; }
  /* Home page ground band: both stop, at different points, so the scene still
     reads. Without this the universal 0.01ms duration parks them both off the
     right edge. */
  .fleet__apc   { animation: none; transform: translateX(30vw); translate: none; }
  .fleet__drone { animation: none; transform: translateX(58vw); translate: none; }
  /* Keeps both fields, stops them sliding. */
  .section__wash { animation: none; translate: none; }
  /* Same trap as every other travelling motif: the universal 0.01ms duration
     snaps them to the end frame, which is off the right edge — all four of
     them. Each parks at its own --park so they hold station spread across the
     band rather than stacked on one spot. */
  .uav { animation: none; transform: translateX(var(--park, 46vw)); }
  .contact-card__rule { transition: none !important; }
  /* Cone and lock keep their shape, at a fixed brightness rather than pulsing. */
  .hero__drone-scan { animation: none; opacity: 0.7; }
  .hero__drone-lock { animation: none; opacity: 0.6; }
  /* The sweep stops; the range rings stay, so the hero keeps its radar without
     anything turning. */
  .hero__radar::after { animation: none; opacity: 0.55; }
  /* The travelling border and the scroll-driven cover parallax. */
  .feature:hover::before { animation: none; }
  .project__media,
  .catalog__media img { animation: none !important; scale: 1 !important; translate: none !important; }
  .catalog__media:hover img { transform: none; }
  .record:hover .record__media img { transform: none; }
  /* The cue and arrow keep their hover states, minus the travel. */
  .record__cue, .record__arrow,
  .record:hover .record__arrow { translate: none !important; }
  /* Scroll-driven animations ignore animation-duration, so the universal rule
     above does not reach them — the Services rail has to be shown outright. */
  .svc-list::before { animation: none !important; transform: none !important; }
  /* Both keep their end state: the stage rules stay drawn, the loop mark stays
     a ring. Only the drawing and the turning stop. */
  .cycle__rule { transform: scaleX(1) !important; transition: none !important; }
  .cycle__loop-mark { animation: none; }
  /* View-transition pseudo-elements are not matched by the universal selector
     above, so they need disabling explicitly. */
  ::view-transition-old(root), ::view-transition-new(root) { animation: none !important; }
}

@media print {
  .site-header, .site-footer, .hero__bg, .ticker, .cta-band, .hero__scroll { display: none; }
  body { color: #000; font-size: 11pt; }
  .js [data-reveal] { opacity: 1; translate: none; rotate: none; }
  a::after { content: " (" attr(href) ")"; font-size: 9pt; }
}
