/* ============================================================
   G UNIVERSE S.A.R.L. design system
   Design system: paper and ink registers, logo blue accent
   ============================================================ */

/* 1. Tokens */
:root {
  --ink-950: #0A0F13;
  --ink-900: #0D1317;
  --ink-800: #16202A;
  --ink-700: #24313D;

  --paper-0:  #FFFFFF;
  --paper-50: #F6F4EF;
  --paper-100:#EFECE3;
  --paper-200:#E4E0D4;

  --text-strong: #10161B;
  --text-body:   #333E47;
  --text-muted:  #51606C;
  --text-inverse:#F2EFE8;
  --text-inverse-muted: #A7B2BA;

  --line:        #D9D5CA;
  --line-strong: #10161B;
  --line-dark:   #2A3742;

  --accent-800: #16477E;
  --accent-700: #1B5F9E;
  --accent-500: #1B7FC0;
  --accent-300: #7CBAE2;
  --accent-100: #D9E8F4;

  --caution-500: #D9821B;
  --caution-100: #F7E3C3;

  --db-35: #1B7FC0;
  --db-45: #3E8A5C;
  --db-55: #C9A227;
  --db-65: #D9821B;
  --db-75: #C0442C;
  --db-85: #7C2E52;
  --map-gradient: linear-gradient(90deg,
      var(--db-35) 0%,  var(--db-45) 22%,
      var(--db-55) 42%, var(--db-65) 62%,
      var(--db-75) 82%, var(--db-85) 100%);

  --error:   #B23A2A;
  --success: #2E6B47;

  /* Form-control boundary. Non-text contrast (WCAG 1.4.11) against the
     surfaces a control can sit on: 3.67:1 on --paper-0, 3.34:1 on
     --paper-50, 3.11:1 on --paper-100. Replaces the hard-coded #8B97A1
     (2.98:1 on white), which failed. */
  --field-border: #7B8792;

  --font-sans: system-ui, -apple-system, "Segoe UI", Roboto,
               "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, "Cascadia Code", "SF Mono",
               Consolas, "Liberation Mono", monospace;

  --fs-display: clamp(2.5rem, 1.6rem + 3.8vw, 4.25rem);
  --fs-800: clamp(1.9rem, 1.4rem + 2.0vw, 2.75rem);
  --fs-700: clamp(1.5rem, 1.25rem + 1.0vw, 1.9rem);
  --fs-600: 1.3125rem;
  --fs-500: 1.125rem;
  --fs-400: 1rem;
  --fs-300: 0.875rem;
  --fs-200: 0.75rem;

  --lh-display: 1.04;
  --lh-heading: 1.15;
  --lh-body: 1.65;
  --lh-tight: 1.4;

  --ls-display: -0.02em;
  --ls-heading: -0.01em;
  --ls-caps: 0.14em;

  --sp-1: 4px;  --sp-2: 8px;   --sp-3: 12px;  --sp-4: 16px;
  --sp-5: 24px; --sp-6: 32px;  --sp-7: 48px;  --sp-8: 64px;
  --sp-9: 96px; --sp-10: 128px;

  --section-y: clamp(64px, 5vw + 32px, 112px);
  --section-y-tight: clamp(40px, 3vw + 24px, 64px);

  --r-0: 0;
  --r-pill: 999px;

  /* Pill token: one definition shared by .tag and .chip */
  --tag-border: 1px solid var(--line-strong);
  --tag-radius: var(--r-pill);
  --tag-pad: 3px 12px;

  /* Fixed-chrome clearance for scrolled-to focus targets.
     REL-04: scroll-padding-top on the scroll container and scroll-margin-top
     on the target ADD. Two independent 84px / 96px declarations landed
     #reference-list 180px from the top, against the 96px protected register
     item 32 records. --scroll-offset is the TOTAL intent (96px, register 32);
     --scroll-pad is the share carried by the container (84px, SC 2.4.11,
     P1-03); the target contributes only the remainder. Change --scroll-offset
     and both layers stay reconciled. */
  --scroll-pad: 84px;
  --scroll-offset: 96px;
  --scroll-margin-extra: calc(var(--scroll-offset) - var(--scroll-pad));
  --scroll-offset-bottom: 88px;

  --bw-hair: 1px;
  --bw-rule: 2px;
  --bw-mark: 3px;

  --shadow-sticky: 0 1px 0 var(--line), 0 8px 24px -20px rgba(13,19,23,0.5);
  --shadow-overlay: 0 24px 64px -24px rgba(13,19,23,0.45);

  --maxw: 1200px;
  --maxw-prose: 760px;
  --gutter: clamp(20px, 4vw, 32px);
}

/* 2. Reset */
*, *::before, *::after { box-sizing: border-box; }
/* WEB-025: no `scroll-behavior: smooth` on html. Route changes and
   scrollIntoView() must settle immediately; the announced H1 has to be
   the visible H1. If an in-page table of contents is ever added, scope
   smooth scrolling to that component, never to the root. */
/* SC 2.4.11. Focus scrolling and scrollIntoView() honour scroll padding on the
   viewport scroll container; without it a focused element parks flush with a
   viewport edge, under #siteHead (65px) or #mobileBar (57px). */
html { scroll-padding-top: var(--scroll-pad); }
@media (max-width: 1023.98px) { html { scroll-padding-bottom: 72px; } }
body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--fs-400);
  line-height: var(--lh-body);
  color: var(--text-body);
  background: var(--paper-50);
  -webkit-font-smoothing: antialiased;
}
img, svg { display: block; max-width: 100%; }
/* Width and height attributes map as presentational hints; without height:auto
   a narrowed image is distorted rather than scaled. Kept off <svg>, whose
   attribute sizing is load-bearing for the inline icon set. */
img { height: auto; }
h1, h2, h3, h4, p, ul, ol, figure, blockquote { margin: 0; }
ul, ol { padding: 0; }
a { color: var(--accent-700); }
a:hover { color: var(--accent-800); }
button { font: inherit; cursor: pointer; }
[hidden] { display: none !important; }

/* 3. Typography */
h1, h2, h3 { color: var(--text-strong); letter-spacing: var(--ls-heading); }
h1 { font-size: var(--fs-700); font-weight: 700; line-height: var(--lh-heading); }
.h-display { font-size: var(--fs-display); line-height: var(--lh-display); letter-spacing: var(--ls-display); max-width: 16ch; }
.h-view { font-size: var(--fs-800); line-height: var(--lh-heading); max-width: 24ch; }
h2 { font-size: var(--fs-800); font-weight: 700; line-height: var(--lh-heading); max-width: 24ch; }
h3 { font-size: var(--fs-600); font-weight: 600; line-height: var(--lh-heading); }
h1:focus-visible { outline: 2px solid var(--accent-700); outline-offset: 3px; }
p { max-width: 68ch; }
.lede { font-size: var(--fs-500); color: var(--text-body); max-width: 58ch; }
.kicker {
  font: 600 var(--fs-200) var(--font-mono);
  text-transform: uppercase;
  letter-spacing: var(--ls-caps);
  color: var(--text-muted);
  margin: 0 0 var(--sp-3);
}
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.muted { color: var(--text-muted); }
.small { font-size: var(--fs-300); }

/* 4. Layout primitives */
.container { max-width: var(--maxw); margin-inline: auto; padding-inline: var(--gutter); }
.prose { max-width: var(--maxw-prose); }
.section { padding-block: var(--section-y); }
.section--tight { padding-block: var(--section-y-tight); }
.band-soft { background: var(--paper-100); }
.band-dark { background: var(--ink-900); color: var(--text-inverse-muted); position: relative; overflow: hidden; }
.band-dark h1, .band-dark h2, .band-dark h3 { color: var(--paper-50); }
.band-dark p { color: var(--text-inverse-muted); }
.band-dark .kicker { color: var(--text-inverse-muted); }
.band-dark a { color: var(--accent-300); }
.stack > * + * { margin-top: var(--sp-5); }
.flow > * + * { margin-top: var(--sp-4); }

/* WEB-070: the outline suppression on the two programmatically focused
   elements is deliberate — keyboard users get the :focus-visible ring,
   mouse users are not shown a spurious one. But `outline: none` is valid
   everywhere while `:focus-visible` is dropped as an invalid selector on
   Safari <=15.3, which left that browser with no ring at all on the H1 the
   router focuses. The guard means older engines keep the UA ring. */
@supports selector(:focus-visible) {
  h1:focus, main:focus { outline: none; }
}
/* FIN-08: `h1:focus, main:focus { outline: none }` above is (0,1,1). The
   generic `:focus-visible` ring further down is (0,1,0), so it loses to it,
   and `main` — the destination the skip link focuses, and the only one of the
   two the code ever focuses programmatically — was landing silently. `h1` has
   its companion at the top of section 3; this is `main`'s, and it must stay
   AFTER the @supports block, because equal specificity is resolved by source
   order. Outline paints outside the box: no layout, no CLS. */
main:focus-visible { outline: 2px solid var(--accent-700); outline-offset: 3px; }

/* Skip link */
.skip {
  position: absolute; left: var(--sp-4); top: -60px;
  background: var(--ink-900); color: var(--paper-50);
  padding: 12px 20px; z-index: 200; font-size: var(--fs-300); font-weight: 600;
  text-decoration: none; transition: top 160ms ease-out;
}
.skip:focus { top: var(--sp-4); }

/* Focus */
:focus-visible { outline: 2px solid var(--accent-700); outline-offset: 3px; }
.band-dark :focus-visible, .drawer :focus-visible, .mobile-bar :focus-visible { outline-color: var(--accent-300); }
/* WEB-021: 2.4.11 Focus Not Obscured. The sticky header (64px / 76px) and
   the fixed mobile bar (56px + safe area) both hide focused controls that
   the browser scrolls into view. :where() keeps specificity at zero so any
   component-level scroll-margin still wins. */
[id],
:where(a[href], button, input, select, textarea, summary, [tabindex]) {
  scroll-margin-top: var(--scroll-margin-extra);
}
@media (max-width: 767px) {
  :where(a[href], button, input, select, textarea, summary, [tabindex]) {
    scroll-margin-bottom: var(--scroll-offset-bottom);
  }
}

/* Section head: short accent rule, small-caps kicker, generous space, then the title */
.sec-head { margin-bottom: var(--sp-6); }
.sec-head .kicker, .kicker--rule { color: var(--accent-700); }
.sec-head .kicker::before, .kicker--rule::before {
  content: ""; display: block; width: 24px; height: 3px;
  background: var(--accent-500); margin-bottom: var(--sp-3);
}
.sec-head .kicker { margin: 0 0 var(--sp-4); }
.sec-head h2 { margin: 0; }
.sec-head--sm { margin-bottom: var(--sp-5); }
.sec-head--sm h2 { font-size: var(--fs-600); }
.band-dark .sec-head .kicker, .band-dark .kicker--rule { color: var(--accent-300); }
.band-navy .sec-head .kicker, .band-navy .kicker--rule { color: #A9CBE8; }
.band-dark .sec-head .kicker::before, .band-navy .sec-head .kicker::before,
.band-dark .kicker--rule::before, .band-navy .kicker--rule::before { background: var(--accent-300); }

/* dB rule */
.db-rule { height: 3px; width: 72px; border: 0; margin: 0; background: var(--map-gradient); }
.db-rule--full { width: 100%; height: 2px; opacity: 0.9; }
.db-labels { display: flex; justify-content: space-between; font: 600 var(--fs-200) var(--font-mono); color: var(--text-muted); margin-bottom: var(--sp-1); }

/* Contour field */
.contour-field {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background:
    repeating-radial-gradient(circle at 78% 30%,
      transparent 0 27px, rgba(27,127,192,0.10) 27px 28px),
    repeating-radial-gradient(circle at 78% 30%,
      transparent 0 89px, rgba(27,127,192,0.16) 89px 90.5px),
    radial-gradient(90% 120% at 78% 30%,
      rgba(27,127,192,0.10) 0%, rgba(22,71,126,0.05) 45%, transparent 70%);
  mask-image: linear-gradient(105deg, transparent 34%, #000 60%);
  -webkit-mask-image: linear-gradient(105deg, transparent 34%, #000 60%);
}
.contour-field--soft {
  background:
    repeating-radial-gradient(circle at 78% 30%,
      transparent 0 27px, rgba(27,127,192,0.05) 27px 28px),
    repeating-radial-gradient(circle at 78% 30%,
      transparent 0 89px, rgba(27,127,192,0.08) 89px 90.5px),
    radial-gradient(90% 120% at 78% 30%,
      rgba(27,127,192,0.05) 0%, rgba(22,71,126,0.02) 45%, transparent 70%);
}
@media (max-width: 767px) {
  .contour-field, .contour-field--soft {
    background:
      repeating-radial-gradient(circle at 85% 12%,
        transparent 0 27px, rgba(27,127,192,0.10) 27px 28px),
      repeating-radial-gradient(circle at 85% 12%,
        transparent 0 89px, rgba(27,127,192,0.16) 89px 90.5px),
      radial-gradient(90% 120% at 85% 12%,
        rgba(27,127,192,0.10) 0%, rgba(22,71,126,0.05) 45%, transparent 70%);
    mask-image: linear-gradient(160deg, #000 30%, transparent 75%);
    -webkit-mask-image: linear-gradient(160deg, #000 30%, transparent 75%);
  }
}

/* Decay curve */
.decay .axis-label { font: 600 10px var(--font-mono); fill: var(--text-muted); }
.decay .curve { stroke: var(--accent-500); }
.decay .frame { stroke: var(--line); }
.band-dark .decay .curve { stroke: var(--accent-300); }
.band-dark .decay .frame { stroke: var(--line-dark); }
.band-dark .decay .axis-label { fill: var(--text-inverse-muted); }

/* REV-2: the `.cfig` contour figure and every `.fig--dark` rule are deleted.
   The home hero's Fig. 01 was their only consumer on all 24 routes, and WEB-050
   is on the record about what unreferenced figure CSS costs the next person to
   read this file. `.band-dark` keeps the dark-register rules that are still
   used; `.fig-panel` stays because /services/simulation/ still uses it. */

/* Facade exposure figure (simulation service)
   WEB-064. Three faults, three fixes.
   (1) The SVG had no width cap, so at 1440 it rendered 694px wide against a
       320-unit viewBox — 2.17x — and its 10px axis labels came out at
       21.7px, larger than the 17px body text. A figure whose labels outrank
       the prose reads as an infographic, which is the opposite of the
       document register the site is built on. Capped at 480px the scale is
       a fixed 1.5x, so the 8-unit label type renders at 12px on every
       breakpoint at or above 768px.
   (2) Below 768px shrinking the same drawing put the labels at 8.4px, under
       the smallest type token on the site. There it is replaced by a
       vertical stack of the same six values (.ffig-stack), which is why the
       SVG is only hidden when that stack is actually present.
   (3) The bars encode 58/61/63/66/68/71 dB(A) and were painted in four
       tints of the brand blue, so colour carried no information at all.
       They now read against the site's own --db-35...--db-85 acoustic
       ramp. */
.ffig {
  display: block; width: 100%; max-width: 480px; margin-inline: auto;
}
.ffig text { font: 600 8px var(--font-mono); fill: var(--text-muted); }
.ffig .bld { fill: var(--paper-0); stroke: var(--text-muted); stroke-width: 1; }
.ffig .flr { stroke: var(--line); stroke-width: 1; }
.ffig .scale { stroke: var(--text-muted); stroke-width: 1; }

/* Value-to-colour binding. One class set serves both the SVG rects and the
   HTML bars of the small-screen stack. */
.db-band-35 { fill: var(--db-35); background-color: var(--db-35); }
.db-band-45 { fill: var(--db-45); background-color: var(--db-45); }
.db-band-55 { fill: var(--db-55); background-color: var(--db-55); }
.db-band-65 { fill: var(--db-65); background-color: var(--db-65); }
.db-band-75 { fill: var(--db-75); background-color: var(--db-75); }
.db-band-85 { fill: var(--db-85); background-color: var(--db-85); }
/* The interim .bar-1..bar-4 mapping is deleted: svc-simulation.html now
   carries the per-value .db-band-* classes (WEB-064, integrator). */

/* Small-screen vertical stack: level, bar, value. */
.ffig-stack { display: none; list-style: none; margin: 0; width: 100%; }
@media (max-width: 767px) {
  .ffig-stack { display: grid; gap: var(--sp-3); }
  .ffig-stack ~ .ffig { display: none; }
  .ffig-stack li {
    display: grid; grid-template-columns: 2.25rem minmax(0, 1fr) auto;
    align-items: center; gap: var(--sp-3);
  }
  .ffig-stack .l { font: 600 var(--fs-200) var(--font-mono); color: var(--text-muted); }
  .ffig-stack .bar { display: block; height: 12px; width: var(--w, 50%); min-width: 8px; }
  .ffig-stack .v {
    font: 600 var(--fs-200) var(--font-mono); color: var(--text-strong);
    font-variant-numeric: tabular-nums;
  }
}

/* Scale legend. Reuses the existing .db-rule--full + .db-labels pair; one
   gradient instance on one figure on one route, which stays inside
   spec-ux-ui.md §8's budget of two per view. Do not extend it. */
.fig-legend { max-width: 480px; margin: var(--sp-5) auto 0; }
.fig-legend .db-labels { margin-bottom: var(--sp-1); }
.fig-legend .cap {
  display: block; margin-top: var(--sp-2);
  font: 500 var(--fs-200) var(--font-mono); color: var(--text-muted);
  text-transform: uppercase; letter-spacing: 0.06em; text-align: center;
}

/* Figure frame */
.fig { border: 1px solid var(--line); background: var(--paper-0); }
.fig-panel { background: var(--paper-100); padding: var(--sp-7) var(--sp-6); position: relative; overflow: hidden; }
.fig figcaption {
  font-size: var(--fs-300); color: var(--text-muted);
  padding: var(--sp-3) var(--sp-4); border-top: 1px solid var(--line);
  line-height: var(--lh-tight);
}
.fig-id {
  font: 600 var(--fs-200) var(--font-mono);
  text-transform: uppercase; letter-spacing: var(--ls-caps);
  color: var(--accent-700); margin-right: var(--sp-3);
}

/* The placeholder system (`.ph`, `img.ph`, `.ph--3x2/4x3/16x9/1x1/portrait`,
   `.ph > img`, `.ph-cap`, `.band-dark .ph`) is deleted. WEB-013 shipped every
   image slot as a drawn empty frame at the final aspect ratio; every slot now
   holds a real file or a composed plate, so the nine rules matched nothing on
   any of the 24 routes. `assets/placeholder/*.svg` is unreferenced too and is
   listed in HANDOFF.md. */

/* 5. Buttons */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  border-radius: var(--r-0); font-size: var(--fs-300); font-weight: 600;
  letter-spacing: 0.02em; min-height: 48px; padding: 14px 28px;
  text-decoration: none; text-align: center;
  transition: background-color 160ms ease-out, border-color 160ms ease-out, color 160ms ease-out, box-shadow 160ms ease-out;
}
.btn--sm { min-height: 44px; padding: 10px 20px; }
.btn--primary { background: var(--ink-900); color: var(--paper-50); border: 1px solid var(--ink-900); }
.btn--primary:hover { background: var(--ink-700); }
.btn--primary:active { background: var(--ink-950); transform: translateY(1px); }
.band-dark .btn--primary, .drawer .btn--primary { background: var(--paper-50); color: var(--ink-900); border-color: var(--paper-50); }
.band-dark .btn--primary:hover, .drawer .btn--primary:hover { background: var(--paper-200); }
.btn--secondary { background: transparent; border: 1px solid var(--line-strong); color: var(--text-strong); }
.btn--secondary:hover { background: var(--paper-0); box-shadow: inset 0 -2px 0 var(--accent-500); }
.band-dark .btn--secondary { border-color: var(--text-inverse-muted); color: var(--text-inverse); }
.band-dark .btn--secondary:hover { border-color: var(--paper-50); background: transparent; }
.btn-row { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: center; }
@media (max-width: 479px) { .btn-row .btn { width: 100%; } }

/* Tertiary link */
.t-link {
  display: inline-flex; align-items: center; gap: 6px;
  color: var(--accent-700); font-size: var(--fs-300); font-weight: 600;
  text-decoration: none; padding: 4px 0; min-height: 24px;
}
.t-link .arr { transition: transform 200ms ease-out; flex: none; }
.t-link:hover { color: var(--accent-800); text-decoration: underline; text-underline-offset: 4px; }
.t-link:hover .arr { transform: translateX(4px); }
.band-dark .t-link { color: var(--accent-300); }

/* 6. Header */
.site-head {
  position: sticky; top: 0; z-index: 80;
  background: rgba(246,244,239,0.97);
  border-bottom: 1px solid var(--line);
}
/* The translucent register exists only at scroll position zero, where
   nothing has passed under the band yet. Both prefixes, so Chromium and
   WebKit render the same header for the first time. */
@supports ((backdrop-filter: blur(8px)) or (-webkit-backdrop-filter: blur(8px))) {
  .site-head:not(.is-scrolled) {
    background: rgba(246,244,239,0.94);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
  }
}
/* WEB-019: the instant the header detaches it is fully opaque. blur(8px)
   does not erase 44px bold type, it turns it into a grey stain — the white
   hero H1 was ghosting through the nav band in tiles/1440/home/01.png. */
.site-head.is-scrolled {
  background: var(--paper-50);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  box-shadow: var(--shadow-sticky);
}
.head-in { display: flex; align-items: center; gap: var(--sp-4); height: 64px; }
@media (min-width: 768px) { .head-in { height: 76px; } }
.brand { text-decoration: none; margin-right: auto; display: flex; align-items: center; gap: var(--sp-3); }
.brand-mark { height: 36px; width: auto; flex: 0 0 auto; display: block; }
@media (min-width: 768px) { .brand-mark { height: 44px; } }
.brand-text { display: block; }
.brand-name { display: block; font-size: var(--fs-500); font-weight: 700; letter-spacing: var(--ls-heading); color: var(--text-strong); line-height: 1.1; }
/* WEB-072: "G Universe" is a category-silent name. The descriptor is the
   only part of the lockup that fixes the category, so it shows from 480px
   up — shortened below 768px, where the header has less room. */
.brand-sub, .brand-sub-short {
  display: none;
  font: 500 var(--fs-200) var(--font-mono);
  color: var(--text-muted);
  letter-spacing: 0.08em;
  line-height: var(--lh-tight);
}
@media (min-width: 480px) { .brand-sub-short { display: block; } }
@media (min-width: 768px) and (max-width: 1023.98px) {
  .brand-sub-short { display: none; }
  .brand-sub { display: block; }
}
/* VQA-08(b): between 1024 and 1139 the nav is on but the full descriptor
   makes the brand block 356px wide, which is exactly what pushed the header
   CTA onto two lines. The short descriptor holds the category and fits. */
@media (min-width: 1140px) {
  .brand-sub-short { display: none; }
  .brand-sub { display: block; }
}

.nav { display: none; align-items: center; gap: 2px; }
/* VQA-08(a): the desktop header was switched on at 768px, but the lockup,
   five nav items and the CTA need about 1090px. Measured at 768 the CTA
   wrapped to three lines and stood 91px tall inside a 77px header, with the
   brand descriptor broken over four lines beside it — the header overflowed
   its own band from 768 all the way to ~1090, which covers iPad landscape and
   every small laptop. The drawer already handles this width properly, so the
   threshold moves to 1024 and the CTA is kept off two lines above it. */
@media (min-width: 1024px) { .nav { display: flex; } }
.nav-link, .nav-btn {
  font-size: var(--fs-300); font-weight: 500; color: var(--text-strong);
  padding: 10px 14px; text-decoration: none; background: none; border: 0;
  display: inline-flex; align-items: center; gap: 6px;
}
.nav-link:hover, .nav-btn:hover,
.nav-link[aria-current="page"] {
  text-decoration: underline; text-decoration-thickness: 2px;
  text-decoration-color: var(--accent-700); text-underline-offset: 6px;
}
.nav-link[aria-current="page"] { font-weight: 600; }
.chev {
  width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent;
  border-top: 5px solid var(--text-strong); display: inline-block;
}
.drop { position: relative; }
.drop-panel {
  position: absolute; top: 100%; left: 0; min-width: 320px;
  background: var(--paper-0); border: 1px solid var(--line);
  box-shadow: var(--shadow-overlay); padding: var(--sp-3); z-index: 90;
}
.drop-panel a {
  display: block; padding: 12px 16px; font-size: var(--fs-300);
  color: var(--text-strong); text-decoration: none; border-left: 2px solid transparent;
}
.drop-panel a:hover { background: var(--paper-100); border-left-color: var(--accent-500); }
.head-cta { display: none; }
@media (min-width: 1024px) { .head-cta { display: inline-flex; white-space: nowrap; } }

/* Menu toggle */
.menu-btn {
  display: inline-flex; flex-direction: column; justify-content: center; align-items: center; gap: 5px;
  width: 44px; height: 44px; background: none; border: 0; padding: 0;
}
.menu-btn span { display: block; width: 22px; height: 2px; background: var(--text-strong); }
@media (min-width: 1024px) { .menu-btn { display: none; } }

/* Drawer */
.scrim {
  position: fixed; inset: 0; background: rgba(13,19,23,0.5); z-index: 95;
  opacity: 1; transition: opacity 200ms ease-out;
}
.drawer {
  position: fixed; top: 0; right: 0;
  /* WEB-055: fallback first. Without it, Safari <=15.3 and older iPads drop
     the declaration, height resolves to auto, the drawer stops covering the
     screen and overflow-y never engages. */
  height: 100vh;
  height: 100dvh;
  width: min(360px, 88vw);
  background: var(--ink-900); box-shadow: var(--shadow-overlay); z-index: 100;
  padding: var(--sp-6) var(--sp-5); overflow-y: auto;
  transform: translateX(0);
}
.drawer a { color: var(--text-inverse); text-decoration: none; }
.drawer-list { list-style: none; margin: var(--sp-6) 0 0; }
.drawer-list > li { border-bottom: 1px solid var(--line-dark); }
.drawer-list > li > a, .drawer-list summary {
  display: block; font-size: var(--fs-600); font-weight: 600; color: var(--text-inverse);
  padding: 14px 0; cursor: pointer;
}
.drawer-list details summary { list-style: none; position: relative; padding-right: 32px; }
.drawer-list details summary::-webkit-details-marker { display: none; }
.drawer-list details summary::after {
  content: ""; position: absolute; right: 8px; top: 50%; margin-top: -3px;
  width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent;
  border-top: 5px solid var(--text-inverse-muted);
}
.drawer-list details[open] summary::after { border-top: 0; border-bottom: 5px solid var(--accent-300); }
.drawer-sub { list-style: none; margin: 0 0 var(--sp-4); padding-left: var(--sp-4); }
.drawer-sub a { display: flex; gap: 10px; padding: 10px 0; font-size: var(--fs-400); color: var(--text-inverse); }
.drawer-sub .idx { font: 600 var(--fs-300) var(--font-mono); color: var(--accent-300); }
.drawer-close { background: none; border: 0; color: var(--text-inverse); font-size: var(--fs-300); font-weight: 600; padding: 10px 0; display: inline-flex; align-items: center; gap: 8px; min-height: 44px; }
.drawer .btn { width: 100%; margin-top: var(--sp-5); }
.drawer-contact { list-style: none; margin: var(--sp-5) 0 0; }
.drawer-contact a { display: flex; align-items: center; gap: 10px; padding: 12px 0; font-size: var(--fs-300); color: var(--text-inverse); border-bottom: 1px solid var(--line-dark); }
.drawer-contact svg { stroke: var(--accent-300); }

/* 7. Hero
   REV-2 — one column, on purpose.
   The hero used to be a 7fr/5fr split with a contour-field figure in the right
   column. The figure is gone, and what is left is not the same layout with an
   empty half: the grid is a single column, the vertical rhythm is re-cut for a
   composition that has to hold the viewport on typography alone, and the
   actions are separated from the copy by a hairline that runs the full width of
   the container, so the column has a stated base instead of trailing off.

   Three things do the work the figure used to do. (1) `.contour-field` is still
   behind the hero, masked to the right at `105deg` from 34% to 60%, which is
   exactly where the figure sat — so the right of the frame is drawn, not blank.
   (2) The measures are already bounded by tokens, `16ch` on `.h-display` and
   `58ch` on `.lede`, which is why the line lengths do not change when the
   column widens to the full container: the type never spans it. (3) The
   vertical padding is opened up past `--section-y` so the hero is still the
   tallest band on the page, which is what the split used to guarantee for it.
   The padding was cut twice against the measure rather than chosen. At
   `6vw + 40px` the hero was 824px at 1440 and its bottom edge landed at 901px
   against a 900px viewport: the band below it was completely invisible and the
   page read as though it ended there. At `4vw + 28px` it is 742px, the client
   band's top 57px is on screen, and the type still sits inside 86px of air top
   and bottom. Mobile and tablet are unaffected — below about 1100px the 72px
   floor governs, and there the hero fills the viewport anyway.
   Nothing here is viewport-locked in `px`: register 40 holds. */
.hero { position: relative; overflow: hidden; background: var(--ink-900); }
.hero-in {
  position: relative; z-index: 1;
  padding-block: clamp(72px, 4vw + 28px, 112px);
  display: grid; grid-template-columns: minmax(0, 1fr); gap: 0;
}
.hero .kicker { color: var(--text-inverse-muted); }
.hero h1 { color: var(--paper-50); }
.hero .lede { color: var(--text-inverse-muted); margin-top: var(--sp-5); }
.hero .db-rule { margin-bottom: var(--sp-4); }
/* The base of the column. Full container width, so it reads as the closing
   line of a composition and not as a divider between two paragraphs. */
.hero-base {
  height: 1px; width: 100%; border: 0; margin: 0;
  margin-block: clamp(32px, 3vw + 12px, 56px) var(--sp-6);
  background: var(--line-dark);
}

/* Client reference grid
   WEB-017: the 90s infinite marquee is retired. It was the build's only
   WCAG Level A failure (2.2.2 Pause, Stop, Hide) — nothing could stop it
   from the keyboard, because the track contained zero focusable nodes —
   and spec-ux-ui.md §8 item 5 forbids infinite marquee logo strips
   outright. The roster stays; the motion goes. What was the
   prefers-reduced-motion branch is now the only branch: a static wrapped
   grid of bordered tiles that reads as an exhibit list, with every mark
   whole at 320px instead of none. */
.cred { border-bottom: 1px solid var(--line); padding-bottom: var(--sp-6); }
/* The markup class is .client-grid. The `.marquee`, `.marquee-track`,
   `.marquee-group` and `.marquee-group[aria-hidden]` aliases are deleted: they
   were kept so an un-migrated fragment would still lay out, no fragment in the
   tree carries them, and they matched nothing on any of the 24 routes. */
.client-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--sp-5) var(--sp-4);
  width: 100%;
  margin-top: var(--sp-5);
  align-items: start;
}
@media (min-width: 768px) {
  .client-grid { grid-template-columns: repeat(5, 1fr); }
}
.client-tile { margin: 0; width: auto; text-align: center; padding-inline: 0; min-width: 0; }
.client-logo {
  height: 84px; display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--line); background: var(--paper-0); padding: var(--sp-3);
}
/* WEB-013: the grayscale filter went with the marquee. These are real client
   marks, published with permission; showing them at reduced opacity read as
   uncertainty about whether we were allowed to. */
/* VQA-01: capping only the height let the wide lockups (MEA 5.6:1, AUBMC
   4.3:1) render 180px across and touch both tile edges, while the tall marks
   (Waldorf 0.83:1) sat at 36px. Optically the row read as five different
   sizes. A second cap on width normalises the ink area — nothing reaches the
   frame, nothing shouts. */
/* Owner review, 10 Aug: the marks were too small, BAE Systems worst of all,
   because three files carried an opaque grey plate baked into the PNG. The
   plates are gone (flood-fill from the border, plus a saturation test for
   SABIC's dithered 255/219 background), so each file is now trimmed to its
   glyph and the caps below size the ink itself rather than the plate. */
/* F-05: the row's ceiling. 48px rather than 56px, because the tall marks were
   the top of a range that ran to 16px at the bottom and every one of them is
   downscaled from a larger file, so the step down costs no sharpness and closes
   the range from 3.5x to 1.7x. The two per-file exceptions that used to sit
   here — a 40px cap for the wide lockups, keyed on `alt` text, which is copy —
   are now generated by build.js from the real image headers, together with the
   rule that draws a mark too short to reach this band to the row's common width
   instead of at 1:1. See logoSizingCss() in build.js. */
.client-logo img { max-height: 48px; max-width: 92%; width: auto; height: auto; object-fit: contain; }
.client-text {
  font: 600 var(--fs-200) var(--font-mono); text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--text-strong); border: 1px solid var(--line); background: var(--paper-0);
  padding: var(--sp-2) var(--sp-3); line-height: 1.4; display: inline-block; max-width: 150px;
}
.client-tile figcaption { font: 500 var(--fs-200) var(--font-mono); text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted); margin-top: var(--sp-2); line-height: var(--lh-tight); }

/* Client logo panels (case studies and cards) */
/* Project plate: composed figure panel for case work */
.project-plate { display: flex; flex-direction: column; width: 100%; }
.plate-band {
  position: relative; overflow: hidden; flex: 1;
  display: flex; flex-direction: column; justify-content: space-between;
  gap: var(--sp-4); padding: var(--sp-5); min-height: 168px;
  background: var(--accent-100);
}
.plate-band::before {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background:
    repeating-radial-gradient(circle at 84% 14%,
      transparent 0 21px, rgba(27,127,192,0.12) 21px 22px),
    repeating-radial-gradient(circle at 84% 14%,
      transparent 0 67px, rgba(27,127,192,0.18) 67px 68.5px);
}
.plate-band > * { position: relative; }
.plate-meta {
  font: 600 var(--fs-200) var(--font-mono);
  text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--text-muted);
}
.plate-name { font-size: var(--fs-600); font-weight: 700; color: var(--text-strong); line-height: var(--lh-heading); max-width: 20ch; margin: 0; }
.plate-foot {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4); border-top: 1px solid var(--line); background: var(--paper-0);
}
/* VQA-13: the chip shrink-wrapped its mark, so the client slot changed size
   from card to card — a 46px box holding a 22px Waldorf monogram beside a
   150px box holding the MEA lockup, at the same position in the same row.
   A floor on the width makes the slot constant and gives the tall marks room
   to be read. */
.plate-chip {
  display: inline-flex; align-items: center; justify-content: center;
  height: 44px; min-width: 104px; padding: 4px 12px; border: 1px solid var(--line);
  background: var(--paper-0); max-width: 60%;
}
.plate-chip img { max-height: 30px; max-width: 108px; width: auto; height: auto; }
.plate-chip .client-text { border: 0; padding: 0; font-size: var(--fs-200); max-width: none; }
.plate--hospitality .plate-band { background: #E4EDF5; }
.plate--healthcare-education .plate-band { background: #E6EFEA; }
.plate--commercial-government .plate-band { background: #ECEBDF; }
.plate--industrial-power .plate-band { background: #F2EADC; }
.fig .project-plate .plate-band { min-height: 200px; }
/* REV-2: `.feature-fig--plate` is retired with the Waldorf feature card and
   replaced by `--photo`, whose field is a photograph. WEB-050's finding still
   governs the selector: `.feature-fig` is declared 200 lines below this point,
   so a single-class modifier at (0,1,0) would lose on source order. The class
   is doubled to (0,2,0), which wins wherever it sits. `min-height` is released
   because the picture's own aspect ratio decides the height, and the 260px
   floor would only ever fight it. */
.feature-fig.feature-fig--photo { padding: 0; align-items: stretch; min-height: 0; }
.feature-fig--photo .project-plate { height: 100%; }
.p-card .plate-band { min-height: 128px; }
/* `--case` is the two cards that lead to a case study; `--plate` is a card of
   the same weight whose project has no case study to lead to. Same frame,
   same field geometry; the difference is what the card offers. */
.p-card--case, .p-card--plate { border-color: var(--line-strong); }
.proj-group + .proj-group { margin-top: var(--sp-8); }

/* Photography inside the reference plate (owner delivery, 10 Aug).
   The plate is not replaced. The photograph becomes its field and the tinted
   band that used to be the field becomes the label band, so the metadata line,
   the project name, the client chip and the Fig. apparatus all survive in the
   same order and the sector tint still colour-codes the plate. Labels sit on a
   solid tint, never on the photograph, so contrast is the measured
   `.muted`-on-plate-tint pair (5.19 to 5.47:1) at every width rather than
   something that depends on what the picture happens to be doing there. */
.plate-photo { display: block; width: 100%; height: auto; }
.project-plate--photo > .plate-photo { border-bottom: 1px solid var(--line); flex: none; }
.plate-text { display: flex; flex-direction: column; gap: var(--sp-3); min-width: 0; }
/* The band no longer has to hold the composition on its own, so its floor is
   released; it is now sized by the two labels inside it. Written one step above
   `.fig .project-plate .plate-band` so source order cannot decide it. */
.fig .project-plate--photo > .plate-band,
.feature-fig--photo .project-plate--photo > .plate-band { min-height: 0; }
/* F-03: with the duplicated project name gone from the band, the feature card's
   band carries one label, and the plate is stretched to the card's height at
   1024 and up, so `space-between` left that label at the top of a 98px field
   with 60px of contour under it. Centred, the band reads as the caption rail it
   is at every width; below 1024 the band is a strip and this changes nothing. */
.feature-fig--photo .project-plate--photo > .plate-band { justify-content: center; }

/* REV-4: `--photo-inset` and `--mount` are gone. Both existed only to hold a
   photograph at a size a small source file could serve honestly: a 300px inset
   inside `/case/mea/`'s plate band, and a 300px print centred on a paper board
   in the two Proof cards. The owner replaced `case-mea-building.jpg` at
   1600 x 900 and `case-ministry-culture.jpg` at 1920 x 1080, so every surface
   they appear on is now inside their resolution budget and `--photo` above is
   the only photographic plate the site needs. The arithmetic that justified the
   two retired treatments is in `DESIGN-REVISIONS.md` REV-2 §2 and REV-3 §1; the
   arithmetic that retires them is REV-4 §1. Deleted with them:
   `.project-plate--photo-inset > .plate-band`, `.plate-photo--inset`,
   `.project-plate--photo-inset .plate-text`, `.plate-mount`,
   `.plate-photo--mount` and `.fig--measure`, whose only consumer on all 24
   routes was the narrowed MEA figure. */

/* REV-2 — the same substitution inside a card header.
   `.p-fig` is a fixed 3/2 frame, which was right for a plate that had to fill
   it and wrong for a photograph: a 16/9 picture plus a label band plus the
   client foot does not add up to 3/2, and `.p-fig`'s `overflow: hidden` would
   have clipped the foot off the bottom. A photographic card header is sized by
   its contents instead. Nothing depends on the ratio for CLS — every box here
   is derived from the img's own width/height attributes.
   Classes are doubled for the same reason WEB-050 records: `.p-fig` itself is
   declared 160 lines below this block, so a (0,1,0) modifier would lose on
   source order no matter how it reads. */
.p-fig.p-fig--photo { aspect-ratio: auto; display: block; padding: 0; }
.p-fig.p-fig--photo > .project-plate { height: auto; }
/* The band is the label strip now, not the field, so the 128px card floor and
   the 200px figure floor are both released — and a strip is set as a strip.
   Stacked in a column the two labels left a 90px band of empty tint under the
   picture that read as a third, unfinished panel; on one line, at the width of
   the card, the band is what it is: a caption rail between the picture and the
   client foot. It wraps rather than collides at 320px, where the two labels
   need 276px of the 232px available. */
.p-fig.p-fig--photo .plate-band {
  min-height: 0; flex-direction: row; flex-wrap: wrap; align-items: baseline;
  justify-content: space-between; gap: var(--sp-2) var(--sp-4);
  padding: var(--sp-4) var(--sp-5);
}
/* FINAL-QA B-3 — the 14px slip between the two Proof cards.
   Measured on the shipped build: from 491 to 578px inclusive the two cards sit
   side by side at about 215px each, and "Commercial and Government" needs two
   lines of the band where "Industrial and Power" needs one. Three flex lines
   against two is 82px against 68px, so the Ministry card's client foot, its
   name and its whole body sat 14px below MEA's in a row whose entire point is
   that the two cards are the same object twice.
   A floor equal to the taller band aligns them. It is a trade, and the trade is
   scoped to the window that needs it: 82px is 32px of padding plus a three-line
   band (14 + 8 + 28), so above 578px, where both bands are two lines, no rule
   applies and REV-2's removal of the empty tint stands everywhere else. Below
   480px the cards stack and there is nothing to align. `min-height`, not
   `height`: at a 24px root or under the 1.4.12 text-spacing override the band
   grows past the floor instead of clipping (register 40).
   Scoped to `.card-grid--2` because that is the Proof row. On /projects/ and the
   sector pages the case cards share one grid track width, so their bands are
   already equal at every width; at 390 they differ but are stacked.
   REV-5: those grids are `--cases` and `--case-solo` now, and both are one
   column below 768px, so this window cannot arise there at all. `.card-grid--2`
   still selects exactly one row on the site, the home Proof row. */
@media (min-width: 480px) and (max-width: 578px) {
  .card-grid--2 .p-fig.p-fig--photo .plate-band { min-height: 82px; }
}

/* REV-5: `.proof-note`, REV-3's plate line for the Proof row's three
   photographs, is deleted here because the owner deleted the paragraph it
   styled. A rule kept alive for markup that has gone is the exact failure
   tools/check-selectors.js exists to catch, and the four findings it was written
   after were all of that kind. Where each of the three disclosures now stands is
   recorded on the markup in src/views/home.html and in HANDOFF.md. */

/* Footer logo. It anchors the Contact column now rather than heading the whole
   footer, so it carries a little more weight than the old 28px. */
.foot-logo { height: 36px; width: auto; display: block; flex: 0 0 auto; }

/* Office locator panel (contact)
   WEB-065(a): this was the only surface in the file filled with --paper-50,
   the page background, so inside the white .contact-panel it read as a hole
   punched through the card. It now takes the base paper fill and is
   separated by a rule instead of by a nested fill. */
.map-panel {
  display: flex; align-items: center; gap: var(--sp-4);
  border: 0; border-top: 1px solid var(--line);
  background: var(--paper-0);
  padding: var(--sp-5) 0 var(--sp-4);
  margin-bottom: var(--sp-5);
}
.map-panel .map-mark { height: 44px; width: auto; flex: 0 0 auto; }
.map-panel p { margin: 0 0 var(--sp-1); font-size: var(--fs-300); line-height: var(--lh-tight); }

/* Diagram: survey-model-specify-verify */
.arc-diagram { border: 1px solid var(--line); background: var(--paper-0); padding: var(--sp-6); }
/* WEB-063: the diagram is being wrapped in <figure class="fig"> with a
   <figcaption> carrying its Fig. id and the word "Schematic", so that every
   figure on the site either states a real quantity or declares that it does
   not. Inside that wrapper the panel drops its own frame — the .fig owns
   the border and the caption rule — and behaves as the figure body. */
.fig > .arc-diagram, .fig-panel > .arc-diagram { border: 0; background: transparent; }
.fig > .arc-diagram { padding: var(--sp-7) var(--sp-6); }
.fig-panel > .arc-diagram { padding: 0; }
.arc-nodes { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0; margin-bottom: var(--sp-6); }
.arc-nodes span {
  font: 600 var(--fs-200) var(--font-mono); text-transform: uppercase; letter-spacing: var(--ls-caps);
  color: var(--text-strong); border-top: var(--bw-mark) solid var(--accent-500);
  padding-top: var(--sp-2); margin-right: var(--sp-3);
}
/* WEB-020(a): four mono labels on a repeat(4, 1fr) grid inside a 32px
   padded panel forced a ~347px min-content width, which is the /home
   horizontal overflow at 320px — the WCAG 1.4.10 test width. Two columns
   and tighter padding clear it; the letter-spacing and word-break relief is
   required as well, because the column collapse alone runs the labels
   together into SURVEYMODEL SPECIFYVERIFY. */
@media (max-width: 479px) {
  .arc-diagram { padding: var(--sp-5); }
  .arc-nodes { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: var(--sp-4); }
  .arc-nodes span { margin-right: var(--sp-2); letter-spacing: 0.08em; overflow-wrap: anywhere; }
}

/* 8. Cards
   WEB-020: every multi-column grid uses minmax(0, 1fr) rather than 1fr. A
   bare 1fr track is min-content-floored, so one unbreakable string inside a
   card pushes the whole document wider than the viewport. This also lands
   the hairline dividers on the geometric centre at 320-414px. */
.card-grid { display: grid; gap: var(--sp-5); }
@media (min-width: 480px) { .card-grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1024px) { .card-grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } .card-grid--2-lg { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 768px) and (max-width: 1023px) { .card-grid--3 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* Owner review, 10 Aug (item 1). Body copy beside a figure was top-aligned, so
   on /home the "The difference" block hung the prose off the top of a taller
   framed figure and read lopsided. The pair is now optically centred against
   each other. Scoped to the breakpoint at which the two are actually side by
   side, and to the pair only: the `.sec-head` h2 and kicker are a separate
   block above the grid and stay left-aligned at the top, as the owner asked. */
@media (min-width: 1024px) { .card-grid--vcenter { align-items: center; } }

/* REV-5 (owner item 3) — the two case-study grids on /projects/ and the sector
   routes. The problem was one grid doing two jobs: the case group had two cards
   in `card-grid--3`, so from 1024px up its row stopped at the second of three
   tracks and left an empty third on the right, immediately above a reference
   grid that spanned the whole container.

   `--cases` is two columns from 768px up. Not from 480px, which is what
   `card-grid--2` would have given: below 768 these cards carry a photograph, a
   label band and a client foot, and two of them side by side at 480px is a
   112px-tall picture under a 200px-wide title. One column below 768 is also what
   `card-grid--3` already did there, so the mobile arrangement of this group is
   unchanged, and the 491-578px band-height window FINAL-QA §B 2 measured on the
   home Proof row cannot occur in a grid that is one column across all of it.

   `--case-solo` is the group of one, which the two sector routes with a
   `featuredCase` carry. A half-width card with an empty half beside it is the
   same fault as an empty third, so the card takes the full row and splits into
   photograph and body at 768px and up: `.p-fig` becomes the left column with its
   hairline moved from bottom to right, `.p-body` takes the `.feature-body`
   padding, and the read-more link keeps `.card .t-link`'s `margin-top: auto`, so
   it lands on the plate foot's line exactly as `.feature-card` does on /. Both
   groups therefore reach the container's left and right edges at every width, on
   all five routes that carry them.

   No `aspect-ratio` change is needed: `.p-fig`'s 3/2 is already yielded to the
   plate's real content height (photograph 16:9 plus label band plus client
   foot), measured on the shipped build before this round, so the picture is not
   re-proportioned by being moved into a column. */
@media (min-width: 768px) {
  .card-grid--cases { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .card-grid--case-solo > .p-card { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .card-grid--case-solo > .p-card > .p-fig { border-bottom: 0; border-right: 1px solid var(--line); }
  .card-grid--case-solo > .p-card > .p-body { padding: var(--sp-7) var(--sp-6); justify-content: center; }
  /* `.card .t-link { margin-top: auto }` bottom-aligns the action, which is right
     in a grid of equal-height cards and right on the home feature card, whose
     body pane is text-rich enough to fill the photograph's height. This pane
     holds a title, three lines of scope and a link against a 319px picture, so
     bottom-alignment banked 240px of white in the middle of the card, measured at
     1440. The pane is centred against the photograph instead — REV-1 item 1's
     `card-grid--vcenter` decision, which the owner approved for the same
     situation on / — and the link follows the scope at the normal `--sp-5` step. */
  .card-grid--case-solo > .p-card > .p-body > .t-link { margin-top: 0; }
}

/* Grid and flex children default to min-width:auto, which is the other half
   of the same overflow mechanism. */
.card-grid > *, .arc-nodes > *, .fact-panel > div, .data-strip > div,
.mini-strip > div, .steps > *, .geo-grid > *, .foot-grid > *, .pager > * { min-width: 0; }

.card {
  background: var(--paper-0); border: 1px solid var(--line); border-radius: var(--r-0);
  padding: var(--sp-6); display: flex; flex-direction: column;
  transition: border-color 160ms ease-out; position: relative;
}
@media (max-width: 767px) { .card { padding: var(--sp-5); } }
.card:hover, .card:focus-within { border-color: var(--line-strong); }
.card:hover .t-link .arr, .card:focus-within .t-link .arr { transform: translateX(4px); }
.card .t-link { margin-top: auto; padding-top: var(--sp-5); }
.card-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--sp-4); }
.svc-icon { stroke: var(--text-strong); }
.card h3 { margin-bottom: var(--sp-3); }
.card .desc { font-size: var(--fs-300); color: var(--text-muted); line-height: var(--lh-tight); }
.stretch::after { content: ""; position: absolute; inset: 0; }

/* Project card
   WEB-018: the `.p-fig--plate` data panel is gone. Thirty of thirty-three
   cards carried an aria-hidden panel restating the sector, year and country
   printed directly beneath it — 100% duplication, 49% of stacked card
   height, ~6,660px of the mobile Projects page, and a permanent signal that
   the photography never arrived. The card is now a text ledger row.
   `.p-fig` is kept for the cards that carry a real plate or, later, a real
   image, so the presence of a figure becomes a signal instead of a texture. */
.p-card { padding: 0; }
.p-fig { aspect-ratio: 3 / 2; background: var(--paper-100); border-bottom: 1px solid var(--line); display: flex; align-items: center; justify-content: center; padding: var(--sp-5); position: relative; overflow: hidden; }
/* A .p-fig holding a real plate or a real image fills the frame edge to
   edge — same geometry rule as .feature-fig--photo, so the box-in-a-box
   effect cannot come back through this door. */
.p-card--case .p-fig, .p-card--plate .p-fig { padding: 0; align-items: stretch; }
.p-fig > .project-plate { width: 100%; height: 100%; }
.p-fig > img { width: 100%; height: 100%; object-fit: cover; }
.p-body { padding: var(--sp-5); display: flex; flex-direction: column; flex: 1; }
/* VQA-09: on a 363px card the pill plus the country/year needed between 256
   and 411px against 313px available, so the meta wrapped on some cards and
   not on others. Within one row that put the project titles on two different
   baselines — "ACRAK Resort" 26px above "NDU Medical Center" beside it. In
   the card grid the two parts stack always: same block on every card, so the
   titles line up whatever the sector name is. Rows outside the grid (the case
   study header) keep the single-line arrangement. */
.tag-row { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; margin-bottom: var(--sp-3); }
.p-card .tag-row { flex-direction: column; align-items: flex-start; gap: var(--sp-2); }
.tag {
  font-size: var(--fs-200); font-weight: 600; color: var(--text-strong);
  border: var(--tag-border); border-radius: var(--tag-radius); padding: var(--tag-pad);
  /* WEB-020(b): `white-space: nowrap` gave "Industrial and Power" a 221px
     min-content width, which overflowed the two-column card grid from 480
     to 539px. This is the load-bearing half of that fix. */
  white-space: normal;
}
.meta-mono { font: 500 var(--fs-200) var(--font-mono); color: var(--text-muted); letter-spacing: 0.04em; }
.case-flag { font: 600 var(--fs-200) var(--font-mono); color: var(--accent-700); text-transform: uppercase; letter-spacing: var(--ls-caps); }

/* Feature card */
.feature-card { display: grid; background: var(--paper-0); border: 1px solid var(--line); transition: border-color 160ms ease-out; position: relative; }
.feature-card:hover, .feature-card:focus-within { border-color: var(--line-strong); }
.feature-card:hover .t-link .arr, .feature-card:focus-within .t-link .arr { transform: translateX(4px); }
@media (min-width: 1024px) { .feature-card { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } }
.feature-fig { background: var(--paper-100); border-bottom: 1px solid var(--line); display: flex; align-items: center; justify-content: center; padding: var(--sp-6); position: relative; overflow: hidden; min-height: 260px; }
@media (min-width: 1024px) { .feature-fig { border-bottom: 0; border-right: 1px solid var(--line); } }
.feature-body { padding: var(--sp-8) var(--sp-6); display: flex; flex-direction: column; }
.feature-body h3 { font-size: var(--fs-700); margin-bottom: var(--sp-3); }
/* REV-2: the same rule `.card .t-link` has carried since the start. With a
   photograph in the left column the two panes are no longer the same height,
   and the link left 118px of white below itself at 1440 while the plate foot
   sat on the fold line opposite. Bottom-aligned, the link and the client foot
   land on one line and the card closes. */
.feature-body .t-link { margin-top: auto; padding-top: var(--sp-5); }
.mini-strip { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); border: 1px solid var(--line); margin: var(--sp-5) 0; }
.mini-strip > div { padding: var(--sp-3) var(--sp-4); }
.mini-strip > div + div { border-left: 1px solid var(--line); }
.mini-strip .v { font: 700 var(--fs-500) var(--font-mono); color: var(--text-strong); font-variant-numeric: tabular-nums; display: block; }
.mini-strip .l { font: 600 var(--fs-200) var(--font-mono); color: var(--text-muted); text-transform: uppercase; letter-spacing: var(--ls-caps); }

/* Insight card
   WEB-065(b): this was the only member of a five-card family with no left
   or right border, which read as a rendering fault rather than a variant.
   It keeps its heavy top rule as its identity and joins the card family. */
.insight-card {
  border: 1px solid var(--line);
  border-top: var(--bw-mark) solid var(--line-strong);
  background: var(--paper-0);
  padding: var(--sp-5);
  display: flex; flex-direction: column;
  /* Load-bearing, as on `.card`. `.stretch::after` is absolute/inset:0 and
     resolves against the nearest positioned ancestor; without this the six
     insight cards had none and each overlay sized itself to the initial
     containing block (measured 1440x2933), covering the whole page. */
  position: relative;
}
.insight-card .imeta { font: 600 var(--fs-200) var(--font-mono); color: var(--text-muted); text-transform: uppercase; letter-spacing: var(--ls-caps); margin-bottom: var(--sp-3); }
.insight-card h3 { margin-bottom: var(--sp-3); font-size: var(--fs-500); }
.insight-card .desc { font-size: var(--fs-300); color: var(--text-muted); line-height: var(--lh-tight); }
.insight-card .desc { margin-bottom: var(--sp-5); }
.insight-card .t-link { margin-top: auto; padding-top: var(--sp-4); border-top: 1px solid var(--line); width: 100%; }

/* REV-5 (owner item 4) — the published briefing and the five on offer.
   The six cards used to be one object six times, so the only thing carrying the
   section heading's claim was four words inside a mono-caps line. The difference
   is structural now, and every part of it is a device already in this file.

   `--open` is `.feature-card`'s composition: one card across the full container,
   split at 1024px into argument and action with a hairline between, the action
   pane bottom-aligned. Its top rule stays 3px but turns accent, its title takes
   `.feature-body h3`'s `--fs-700` step, and its action is the page's only filled
   button, which is what the site uses everywhere else for a thing that happens
   when you press it.

   `--request` keeps the three-up grid and the card frame and gives up the 3px ink
   top rule for a hairline, because the heavy rule now means "published". Their
   marker is `.iflag`, the mono-caps register on a ruled line at the head of the
   card: muted for an offer, accent for the published one, which is the same
   accent-versus-muted distinction `.kicker` and `.plate-meta` already make.

   The hover promise is honest per variant: the published card's accent rule
   darkens, an offer's whole frame goes to `--line-strong` as `.card`'s does. What
   used to happen was that all six cards promised an accent top rule on hover,
   which is what made the published one's identity read as an accident. */
.insight-card:hover, .insight-card:focus-within { border-top-color: var(--accent-500); }
/* `max-width: none` is load-bearing, not tidying: `.iflag` is a <p>, so the
   global `p { max-width: 68ch }` capped its underline at 490px inside the
   published card's 720px column while the same underline spanned the whole width
   of each 358px request card. One marker drawn two different ways is the kind of
   near-miss FINAL-QA §C 2 called out; the rule is the card's width in both. */
.iflag {
  font: 600 var(--fs-200) var(--font-mono); color: var(--text-muted);
  text-transform: uppercase; letter-spacing: var(--ls-caps);
  border-bottom: 1px solid var(--line); max-width: none;
  padding-bottom: var(--sp-3); margin-bottom: var(--sp-3);
}
.iflag--live { color: var(--accent-700); border-bottom-color: var(--accent-500); }

.insight-card--request { border-top: var(--bw-hair) solid var(--line); }
.insight-card--request:hover, .insight-card--request:focus-within { border-color: var(--line-strong); }

.insight-card--open {
  border-top-color: var(--accent-500);
  display: grid; padding: 0; margin-bottom: var(--sp-6);
}
@media (min-width: 1024px) { .insight-card--open { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); } }
.iopen-lead { padding: var(--sp-6); }
.iopen-lead h3 { font-size: var(--fs-700); }
.iopen-lead .desc { margin-bottom: 0; max-width: 62ch; }
.iopen-action {
  display: flex; flex-direction: column; align-items: flex-start;
  padding: var(--sp-6); border-top: 1px solid var(--line);
}
/* The action pane holds a label, one sentence and a button against a lead column
   of a title and a paragraph, so at 1024 and up it is the shorter of the two by
   about 140px. Bottom-aligning it, which is what `.feature-body .t-link` does on
   the home feature card, would bank all of that as a void above the label and
   reproduce FINAL-QA §C 8's "the composition looks like it ran out". Centred, the
   difference is split either side of a three-part block and the pane reads as the
   plinth it is. Below 1024 the card is one column and the pane is a footer strip
   under a hairline, where centring is a no-op. */
@media (min-width: 1024px) {
  .iopen-action { border-top: 0; border-left: 1px solid var(--line); justify-content: center; }
}
.iopen-action .small { margin-bottom: var(--sp-5); max-width: 40ch; }
.ireq-head { padding-top: var(--sp-5); border-top: 1px solid var(--line); margin-bottom: var(--sp-5); }

/* 9. Service rows (services overview) */
.svc-rows { border-top: 1px solid var(--line-strong); }
.svc-row {
  display: grid; gap: var(--sp-4); padding: 40px 0; border-bottom: 1px solid var(--line);
  transition: background-color 160ms ease-out; position: relative;
}
@media (min-width: 768px) { .svc-row { grid-template-columns: 2fr 3fr; gap: var(--sp-7); } }
.svc-row:hover, .svc-row:focus-within { background: var(--paper-100); }
.svc-row:hover .t-link .arr, .svc-row:focus-within .t-link .arr { transform: translateX(4px); }
.svc-row-head { display: flex; gap: var(--sp-4); align-items: flex-start; }
/* F-01: the heading in a service row is an h2 — the heading-order work
   corrected the level and this rule kept naming `h3`, so it matched nothing on
   any of the 24 routes and the six service names fell through to the global h2
   ramp at exactly the page H1's size. Keyed on `.svc-row-head`, which is the
   element that actually ships. tools/check-selectors.js now fails the build for
   any selector that matches nothing, which is how the fourth mismatch in this
   family became the last. */
.svc-row-head h2 { font-size: var(--fs-600); }
.svc-row .desc { font-size: var(--fs-300); color: var(--text-muted); line-height: var(--lh-tight); margin-bottom: var(--sp-3); max-width: 62ch; }
.scope-tags { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); margin-bottom: var(--sp-3); }
.scope-tags span { font: 500 var(--fs-200) var(--font-mono); color: var(--text-muted); letter-spacing: 0.04em; text-transform: uppercase; }

/* 10. Process steps — the four-phase method
   Owner review, 10 Aug (item 2). The block was four equal columns, each with
   its own 40px accent rule floating over a shared hairline, and the phase
   deliverable — the artefact the client actually receives, and the whole point
   of register 20 — was set in the smallest type in the block. Two faults, two
   fixes, both in the existing language.

   (1) SEQUENCE. The four phases now hang off one continuous 2px ink spine
       (the same weight `.data-strip` uses for its top edge), graduated by a
       3px accent tick at each phase and divided by hairlines, so the row reads
       as one measurement axis with four stations rather than four cards. The
       phase ordinal moves from muted grey to accent, so the numbers are the
       first thing the eye picks up along the axis.
   (2) WEIGHT. Each deliverable sits on its own ruled plinth pushed to the
       bottom of the cell with `margin-top: auto`. Because the four cells are
       grid siblings of equal height, the four plinth rules land on one
       horizontal line: a continuous "deliverable" band across the foot of the
       block. The deliverable itself is now sentence-case ink at body size —
       the second-heaviest thing in the cell after the phase title — with the
       mono caps demoted to the label above it.

   Degradation: below 768px the axis rotates. The spine becomes the vertical
   ink rule the previous mobile treatment used, the ticks rotate with it, and
   each phase is a ruled entry down the page; the deliverable plinth survives
   unchanged, so the small-screen list is at least as legible as the four
   columns. Nothing here relies on JS, adds motion, or changes intrinsic
   height, so the zero-CLS property is untouched. */
.steps { display: grid; gap: 0; border-top: var(--bw-rule) solid var(--line-strong); }
@media (min-width: 768px) { .steps { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1024px) { .steps { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.step { position: relative; display: flex; flex-direction: column; padding: var(--sp-5) var(--sp-5) 0 0; }
/* The graduation: a 3px accent tick descending from the spine at each station. */
.step::before { content: ""; position: absolute; top: 0; left: 0; width: var(--bw-mark); height: 14px; background: var(--accent-500); }
/* Cell dividers. Written per breakpoint because the tracks change: at four
   columns every cell but the first takes a left rule; at two columns the
   second of each pair takes the left rule and the second row takes a top one. */
@media (min-width: 1024px) {
  .step + .step { border-left: 1px solid var(--line); padding-left: var(--sp-5); }
}
@media (min-width: 768px) and (max-width: 1023.98px) {
  .step:nth-child(2n) { border-left: 1px solid var(--line); padding-left: var(--sp-5); }
  .step:nth-child(n+3) { border-top: 1px solid var(--line); }
}
/* The four plinth rules have to land on ONE line for the block to read as a
   single instrument. `margin-top: auto` alone bottom-aligns them, which puts the
   rule of a two-line deliverable 23px above the rule of a one-line one. Subgrid
   shares the parent's rows, so phase, title, description and plinth each get a
   row sized by the tallest of the four and every rule aligns. `grid-auto-rows`
   cycles the four-row pattern, so the two-column arrangement at 768px aligns
   within each of its two rows of cells as well. Engines without subgrid keep the
   flex fallback above, which is the previous behaviour, not a broken one. */
@supports (grid-template-rows: subgrid) {
  @media (min-width: 768px) {
    .steps { grid-auto-rows: auto auto 1fr auto; }
    .step { display: grid; grid-template-rows: subgrid; grid-row: span 4; }
    /* `auto` margins on a grid item absorb the row's free space, so the
       fallback's `margin-top: auto` would bottom-align the plinth inside its
       shared row and undo the alignment subgrid just bought. */
    .step .deliverable { margin-top: 0; }
  }
}
.step .phase {
  font: 600 var(--fs-200) var(--font-mono); color: var(--accent-700);
  text-transform: uppercase; letter-spacing: var(--ls-caps); margin-bottom: var(--sp-2);
}
.step h3 { font-size: var(--fs-500); margin-bottom: var(--sp-2); }
.step .desc { font-size: var(--fs-300); color: var(--text-muted); line-height: var(--lh-tight); margin-bottom: var(--sp-5); }
/* The plinth. margin-top:auto is what lands the four rules on one line. */
.deliverable {
  margin-top: auto; padding-top: var(--sp-4); padding-bottom: var(--sp-5);
  border-top: 1px solid var(--line);
}
.deliv-l {
  font: 600 var(--fs-200) var(--font-mono); color: var(--text-muted);
  text-transform: uppercase; letter-spacing: var(--ls-caps); margin-bottom: var(--sp-2);
}
.deliv-v {
  display: flex; gap: var(--sp-2); align-items: flex-start;
  font-size: var(--fs-400); font-weight: 600; color: var(--text-strong);
  line-height: var(--lh-tight); max-width: 30ch;
}
.deliv-v svg { flex: none; margin-top: 3px; stroke: var(--accent-700); }
@media (max-width: 767.98px) {
  .steps { border-top: 0; border-left: var(--bw-rule) solid var(--line-strong); }
  .step { padding: 0 0 var(--sp-6) var(--sp-5); }
  .step:last-child { padding-bottom: 0; }
  .step::before { top: 6px; width: 14px; height: var(--bw-mark); }
  .deliverable { padding-bottom: 0; }
}

/* 11. Data strip */
.data-strip { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); border-top: 2px solid var(--line-strong); border-bottom: 1px solid var(--line); }
@media (min-width: 768px) { .data-strip { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.data-cell { padding: var(--sp-6) var(--sp-5); border-left: 1px solid var(--line); }
.data-cell:first-child { border-left: 0; }
@media (max-width: 767px) {
  .data-cell:nth-child(3) { border-left: 0; }
  .data-cell:nth-child(n+3) { border-top: 1px solid var(--line); }
}
.data-cell .num {
  font: 700 clamp(2.25rem, 4vw, 3.25rem) var(--font-mono);
  color: var(--text-strong); font-variant-numeric: tabular-nums; line-height: 1;
  display: block;
}
.data-cell .num sup { font-size: 60%; color: var(--accent-700); }
.data-cell .lbl { font: 600 var(--fs-200) var(--font-mono); text-transform: uppercase; letter-spacing: var(--ls-caps); color: var(--text-muted); margin-top: var(--sp-2); display: block; }

/* 11b. The reference scale index
   REV-6 (owner correction, 11 Aug). REV-5 split this into `.practice-index` plus
   a 480px `.scale-key` card. The owner's instruction was that the reference
   scale was "way better as it was", so the REV-1 composition is restored
   verbatim: one `.scale-index` figure in which the graduated ramp is the
   structural top edge of `.data-strip`, so scale and index read as ONE object.
   `.practice-index` and `.scale-key` are deleted with the markup that used them.

   What does not come back is `.scale-index figcaption` — the owner deleted the
   figure captions on this view as a separate instruction, and that stands.
   FINAL-QA §C 1 and §C 2 were closed by the REV-5 composition rather than by the
   caption, so both reopen here, knowingly, on the owner's instruction; recorded
   in DESIGN-REVISIONS.md (REV-6) and FINAL-QA.md rather than dropped quietly.

   The ramp is `--map-gradient`, the site's own dB colour scale (protected), and
   it is graduated at the six positions of its own colour stops (35/45/55/65/75/85
   dB(A), the six --db-* tokens) — the same legend convention `.fig-legend` uses
   on /services/simulation/. Still exactly two gradient instances on this view
   (the hero `.db-rule` is the other), so spec-ux-ui.md §8's budget of two per
   view is unchanged. */
.scale-index { display: block; }
.scale-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-4); }
.scale-head .kicker { margin: 0; }
/* No `text-transform` here: the unit is "dB(A)", and the mono caps register
   would print it "DB(A)". */
.scale-unit {
  font: 600 var(--fs-200) var(--font-mono); color: var(--text-muted);
  letter-spacing: var(--ls-caps); max-width: none;
}
.db-scale { position: relative; margin-top: var(--sp-4); }
.db-ticks { list-style: none; margin: 0; height: 20px; position: relative; }
.db-ticks li {
  position: absolute; bottom: 0; left: var(--x, 0);
  transform: translateX(-50%); padding-bottom: 7px;
  font: 600 var(--fs-200) var(--font-mono); color: var(--text-muted);
  font-variant-numeric: tabular-nums; line-height: 1;
}
.db-ticks li::after {
  content: ""; position: absolute; left: 50%; bottom: 0; width: 1px; height: 5px;
  background: var(--line-strong);
}
/* The end graduations sit inside the rule rather than half outside it. */
.db-ticks li:first-child { transform: none; }
.db-ticks li:first-child::after { left: 0; }
.db-ticks li:last-child { transform: translateX(-100%); }
.db-ticks li:last-child::after { left: auto; right: 0; }
/* The ramp is an instrument here, not a tint: full weight, full opacity. */
.db-rule--scale { height: var(--bw-mark); opacity: 1; }
/* The scale is the strip's top edge, so the strip drops its own. */
.scale-index .data-strip { border-top: 0; }

/* 12. Tables */
.table-scroll { overflow-x: auto; }
table.std { width: 100%; border-collapse: collapse; background: var(--paper-0); border: 1px solid var(--line-strong); }
table.std caption {
  text-align: left; font-size: var(--fs-300); font-weight: 600; color: var(--text-strong);
  margin-bottom: var(--sp-3); caption-side: top;
}
table.std caption .tbl-id { font: 600 var(--fs-200) var(--font-mono); color: var(--accent-700); text-transform: uppercase; letter-spacing: var(--ls-caps); margin-right: var(--sp-3); }
table.std th {
  background: var(--paper-200); font: 600 var(--fs-200) var(--font-mono);
  text-transform: uppercase; letter-spacing: var(--ls-caps); color: var(--text-strong);
  padding: 10px 16px; border-bottom: 2px solid var(--line-strong); text-align: left;
}
table.std td {
  font-size: var(--fs-300); line-height: var(--lh-tight); padding: 12px 16px;
  vertical-align: top; border-bottom: 1px solid var(--line);
}
table.std td:first-child, table.std th:first-child { border-right: 1px solid var(--line); }
table.std tbody tr:hover td { background: var(--paper-100); }
table.std .mono-cell { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* 13. FAQ */
.faq { border-top: 1px solid var(--line-strong); max-width: var(--maxw-prose); }
.faq details { border-bottom: 1px solid var(--line); }
.faq summary {
  font-size: var(--fs-400); font-weight: 600; color: var(--text-strong);
  padding: 20px 48px 20px 0; cursor: pointer; list-style: none; position: relative;
}
@media (min-width: 768px) { .faq summary { font-size: var(--fs-500); } }
.faq summary::-webkit-details-marker { display: none; }
.faq summary:hover { color: var(--accent-700); }
.faq .marker { position: absolute; right: 0; top: 50%; width: 20px; height: 20px; margin-top: -10px; }
.faq .marker::before, .faq .marker::after {
  content: ""; position: absolute; background: var(--text-strong); left: 50%; top: 50%;
}
.faq .marker::before { width: 14px; height: 2px; margin: -1px 0 0 -7px; }
.faq .marker::after { width: 2px; height: 14px; margin: -7px 0 0 -1px; transition: transform 200ms ease-out; }
.faq details[open] .marker::after { transform: rotate(90deg); }
.faq .panel { padding: 0 0 24px; max-width: 62ch; font-size: var(--fs-400); color: var(--text-body); }

/* 14. Forms */
.form { max-width: 560px; display: grid; gap: var(--sp-5); }
.form-2up { display: grid; gap: var(--sp-5); }
@media (min-width: 768px) { .form-2up { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } }
/* WEB-044. The label row carries the field name and, where the field is
   required, a quiet mono "Required" marker — stated before the user can
   fail, not after. The hint is a sibling of the label, referenced by
   aria-describedby, so it stops being spoken as part of a 127-character
   accessible name on every focus and every error correction. */
.field label {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3);
  font-size: var(--fs-300); font-weight: 600; color: var(--text-strong); margin-bottom: 6px;
}
.field .req {
  font: 600 var(--fs-200) var(--font-mono); text-transform: uppercase;
  letter-spacing: var(--ls-caps); color: var(--text-muted); flex: none;
}
.field .hint {
  display: block; font-weight: 400; font-size: var(--fs-300);
  color: var(--text-muted); line-height: var(--lh-tight);
  margin: 0 0 6px; max-width: 60ch;
}
.field input, .field select, .field textarea {
  width: 100%; background: var(--paper-0);
  /* 3.67:1 against --paper-0. The old #8B97A1 measured 2.98:1 and failed
     1.4.11; radius returns to zero because form controls were the only
     rounded elements in a zero-radius language, and the only cool border in
     a warm one. */
  border: 1px solid var(--field-border); border-radius: var(--r-0);
  padding: 13px 14px; font: inherit; font-size: var(--fs-400); min-height: 48px; color: var(--text-strong);
}
.field textarea { min-height: 140px; resize: vertical; }
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none; border-color: var(--ink-900); box-shadow: 0 0 0 1px var(--ink-900);
}
.field input[aria-invalid="true"], .field textarea[aria-invalid="true"], .field select[aria-invalid="true"] {
  border-color: var(--error); box-shadow: 0 0 0 1px var(--error);
}
.f-error { display: flex; gap: 6px; align-items: flex-start; color: var(--error); font-size: var(--fs-300); margin-top: 6px; }
.f-error svg { flex: none; margin-top: 3px; stroke: var(--error); }
.form-note { font-size: var(--fs-300); color: var(--text-muted); }
.form-success { border: 1px solid var(--line-strong); border-left: var(--bw-mark) solid var(--success); background: var(--paper-0); padding: var(--sp-6); }
.form-success h2, .form-success h3 { font-size: var(--fs-600); margin-bottom: var(--sp-3); }

/* Server-redirect confirmations (WEB-001 / WEB-010). The Cloudflare Function
   redirects a no-JS post to /contact/?sent=1#sent or /contact/?error=<code>#form-error,
   so the result is shown by the URL fragment alone. This is not a [hidden]
   violation and not a JS dependency: visibility is driven by :target. */
.form-sent { display: none; }
.form-sent:target {
  display: block;
  border: 1px solid var(--line-strong); border-left: var(--bw-mark) solid var(--success);
  background: var(--paper-0); padding: var(--sp-6); margin-bottom: var(--sp-6);
}
#form-error.form-sent:target, #ref-error.form-sent:target { border-left-color: var(--error); }
.form-sent h3 { font-size: var(--fs-600); margin-bottom: var(--sp-3); }

/* Error summary (WEB-044, serving WEB-030). One block at the top of the
   form listing every failed field as a link to it, rather than announcing
   one problem out of three. Mirrors .form-success so the two states read as
   a pair. */
.form-errors {
  border: 1px solid var(--error); border-left: var(--bw-mark) solid var(--error);
  background: var(--paper-0); padding: var(--sp-5);
  margin-bottom: var(--sp-5); max-width: 560px;
}
.form-errors h2, .form-errors h3 { font-size: var(--fs-500); color: var(--error); margin-bottom: var(--sp-3); max-width: none; }
.form-errors ul { list-style: none; margin: 0; }
.form-errors li { font-size: var(--fs-300); line-height: var(--lh-tight); }
.form-errors li + li { margin-top: var(--sp-2); }
.form-errors a {
  color: var(--error); font-weight: 600;
  display: inline-block; min-height: 24px; padding: 2px 0;
  text-underline-offset: 3px;
}
.form-errors a:hover { color: var(--error); text-decoration-thickness: 2px; }

/* What happens next (WEB-058) */
.next-steps { border-top: 1px solid var(--line); padding-top: var(--sp-5); margin-top: var(--sp-5); }
.next-steps h2, .next-steps h3 { font-size: var(--fs-400); margin-bottom: var(--sp-3); }
.next-steps ol { padding-left: 1.25em; margin: 0; }
.next-steps li { font-size: var(--fs-300); line-height: var(--lh-tight); }
.next-steps li + li { margin-top: var(--sp-2); }
.next-steps p { font-size: var(--fs-300); color: var(--text-muted); margin-top: var(--sp-3); }

/* 15. Contact layout */
.contact-grid { display: grid; gap: var(--sp-7); }
@media (min-width: 1024px) { .contact-grid { grid-template-columns: 3fr 2fr; } }
.contact-panel { background: var(--paper-0); border: 1px solid var(--line); padding: var(--sp-6); align-self: start; }
.contact-rows { list-style: none; margin: 0 0 var(--sp-5); }
.contact-rows li { display: flex; gap: var(--sp-3); align-items: flex-start; padding: 12px 0; border-bottom: 1px solid var(--line); font-size: var(--fs-300); }
.contact-rows svg { flex: none; margin-top: 3px; stroke: var(--accent-700); }
/* WEB-071: the three case-study links in the contact panel measured
   189x16, 217x16 and 237x16. The 2.5.8 Inline exception is arguable, but a
   site that states "every interactive target at least 24x24 CSS pixels" in
   its own spec should not need the argument, and 16px targets are genuinely
   hard to hit with a tremor. */
.contact-rows a,
.contact-panel .rail-list a,
.rail-list a { min-height: 24px; display: inline-block; padding: 2px 0; }

/* 16. Breadcrumbs */
.crumbs { padding: var(--sp-4) 0 0; font-size: var(--fs-300); }
.crumbs ol { list-style: none; display: flex; flex-wrap: wrap; margin: 0; }
.crumbs a { color: var(--text-muted); text-decoration: none; padding: 4px 0; }
.crumbs a:hover { color: var(--accent-700); text-decoration: underline; }
.crumbs .sep { font-family: var(--font-mono); color: var(--line-strong); margin: 0 8px; }
.crumbs [aria-current="page"] { color: var(--text-strong); }

/* 17. Filter chips */
.filter-label { font: 600 var(--fs-200) var(--font-mono); text-transform: uppercase; letter-spacing: var(--ls-caps); color: var(--text-muted); margin-bottom: var(--sp-2); display: block; }
.chip-bar { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; padding: var(--sp-4) 0; border-bottom: 1px solid var(--line); }
/* VQA-03: the markup wraps the sector chips in `.chip-group` (the role=group
   the label points at), which carried no rule at all. As a single block-level
   flex child it swallowed `.chip-bar`'s gap and the chips fell back to inline
   layout: word-space separators, and leading-sized rows once they wrapped. */
.chip-group { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; }
.chip {
  border-radius: var(--tag-radius); border: var(--tag-border); background: transparent;
  font-size: var(--fs-300); font-weight: 500; color: var(--text-strong);
  padding: 9px 18px; min-height: 40px; display: inline-flex; align-items: center; gap: 8px;
  transition: background-color 160ms ease-out, color 160ms ease-out;
}
.chip .n { font: 500 var(--fs-300) var(--font-mono); color: var(--text-muted); }
.chip:hover { background: var(--paper-0); }
a.chip { text-decoration: none; }
/* Active sector. The chips are real links to the pre-rendered sector routes, so
   with JavaScript off they still filter (WEB-010); aria-current is the valid
   state for a link. app.js keeps aria-pressed for the <button> variant. */
.chip[aria-pressed="true"], .chip[aria-current="page"] { background: var(--ink-900); color: var(--paper-50); }
.chip[aria-pressed="true"] .n, .chip[aria-current="page"] .n { color: var(--accent-300); }
/* The country row. `.country-wrap select` matched no element in any built page,
   so the control rendered at the browser default 19px and failed SC 2.5.8. The
   rule now targets the class the markup uses; 40px matches the chips. */
.chip-bar--country { gap: var(--sp-3); }
.chip-bar--country select {
  background: var(--paper-0); border: 1px solid var(--field-border); border-radius: var(--r-0);
  padding: 8px 12px; font: inherit; font-size: var(--fs-300); min-height: 40px; color: var(--text-strong);
}
.vh { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

/* 18. Lens band */
.lens {
  border: 1px solid var(--line); border-left: 3px solid var(--accent-500);
  background: var(--paper-0); padding: var(--sp-6); margin: var(--sp-6) 0;
}
.lens h2 { font-size: var(--fs-700); margin-bottom: var(--sp-3); }
.lens p { font-size: var(--fs-400); max-width: 75ch; }
.lens .rel { margin-top: var(--sp-4); font-size: var(--fs-300); }

/* 20. Pager */
.pager { display: grid; gap: var(--sp-4); margin-top: var(--sp-7); }
@media (min-width: 768px) { .pager { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } }
.pager a {
  border: 1px solid var(--line); background: var(--paper-0); padding: var(--sp-5) var(--sp-6);
  text-decoration: none; display: block; transition: border-color 160ms ease-out;
}
.pager a:hover { border-color: var(--line-strong); }
.pager .dir { font: 600 var(--fs-200) var(--font-mono); text-transform: uppercase; letter-spacing: var(--ls-caps); color: var(--text-muted); display: block; margin-bottom: var(--sp-2); }
.pager .dest { font-weight: 600; color: var(--text-strong); }

/* 21. Detail layout with rail */
.detail-grid { display: grid; gap: var(--sp-7); }
/* P1-01. A grid item's automatic minimum size is its min-content size, so the
   single implicit column cannot shrink below its widest descendant and the
   `overflow-x: auto` on a `.table-scroll` inside it never gets to scroll.
   Single-column only: above 1024px the rail's `1fr` track needs its automatic
   minimum, and zeroing it there spills the rail instead (1044px document at a
   1024px viewport). */
@media (max-width: 1023.98px) { .detail-grid > * { min-width: 0; } }
@media (min-width: 1024px) { .detail-grid { grid-template-columns: minmax(0, var(--maxw-prose)) 1fr; } }
.rail { align-self: start; }
@media (min-width: 1024px) { .rail { position: sticky; top: 100px; } }
.rail-box { border: 1px solid var(--line); background: var(--paper-0); padding: var(--sp-5); }
.rail-box + .rail-box { margin-top: var(--sp-4); }
.rail-box h3 { font-size: var(--fs-400); margin-bottom: var(--sp-3); }
.rail-list { list-style: none; margin: 0; }
.rail-list li { padding: 8px 0; border-bottom: 1px solid var(--line); font-size: var(--fs-300); line-height: var(--lh-tight); }
.rail-list li:last-child { border-bottom: 0; }
.rail-list .meta-mono { display: block; }
.rail-box .btn { width: 100%; margin-top: var(--sp-4); }

/* 22. Fact panel (case studies) */
.fact-panel { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); border: 1px solid var(--line-strong); background: var(--paper-0); margin: var(--sp-6) 0; }
@media (min-width: 768px) { .fact-panel { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.fact-panel > div { padding: var(--sp-4) var(--sp-5); border-left: 1px solid var(--line); }
.fact-panel > div:first-child { border-left: 0; }
@media (max-width: 767px) {
  .fact-panel > div:nth-child(3) { border-left: 0; }
  .fact-panel > div:nth-child(n+3) { border-top: 1px solid var(--line); }
}
.fact-panel .v { font: 700 var(--fs-500) var(--font-mono); color: var(--text-strong); font-variant-numeric: tabular-nums; display: block; line-height: var(--lh-tight); }
.fact-panel .l { font: 600 var(--fs-200) var(--font-mono); color: var(--text-muted); text-transform: uppercase; letter-spacing: var(--ls-caps); }
/* WEB-020(c): "Disciplines engaged" set at 0.14em tracking gives the cell a
   147px min-content width; two of those plus 24px side padding overflow
   /case/waldorf at 320px. Below 360px the panel runs as a single column;
   between 360 and 400 the padding and tracking come in instead, so 375px
   keeps its two-column reading. */
@media (max-width: 400px) {
  .fact-panel > div { padding: var(--sp-4); }
  .fact-panel .l, .data-cell .lbl, .mini-strip .l { letter-spacing: 0.08em; overflow-wrap: anywhere; }
}
@media (max-width: 359px) {
  .fact-panel { grid-template-columns: minmax(0, 1fr); }
  .fact-panel > div { border-left: 0; border-top: 1px solid var(--line); }
  .fact-panel > div:first-child { border-top: 0; }
}

/* 23. CTA bands
   Owner review, 10 Aug (items 4 and 7). One composition now serves every
   closing band on the site: the argument on the left, the actions on the right,
   separated by a hairline, at `--sp-7` block padding. The owner asked for the
   `/services/` pairing on /home (item 4) and for the /projects/ band to stop
   behaving like a billboard (item 7); giving all three the same slim two-column
   composition answers both and makes the nine navy bands read as one device.
   The /projects/ trust line moves out from under the button into the actions
   column, beside the argument rather than below it, which is where the owner
   asked for it. */
.cta-band { position: relative; overflow: hidden; }
.cta-band .container { position: relative; z-index: 1; }
.slim-cta { border-top: 0; border-bottom: 0; }
/* NOT `.in`: that is the reveal class (`.rv.in`), and `.slim-cta .in` also
   matched any revealed child inside the band, turning `.cta-lead` itself into a
   two-column grid the moment the IntersectionObserver fired. */
.slim-cta .cta-in { display: grid; gap: var(--sp-6); align-items: center; padding-block: var(--sp-7); }
/* 1200px, not 1024. The container is capped at `--maxw`, so at every viewport
   from 1200 up the band has exactly the same 1136px of content width to divide,
   and the two-column arrangement is therefore either right or wrong once rather
   than degrading across a range. It needs about that much: the button pair is
   580px, and below ~1130px of container the argument column drops under 400px
   and the heading breaks into three lines. Between 1024 and 1199 the two blocks
   stack, where the pair has the full width and stays on one line. */
@media (min-width: 1200px) {
  /* `stretch`, so the hairline between the two columns runs the full height of
     the band's content box instead of only the height of the button block. */
  .slim-cta .cta-in { grid-template-columns: minmax(0, 1fr) auto; gap: var(--sp-8); align-items: stretch; }
}
.cta-lead > * + * { margin-top: var(--sp-3); }
.cta-lead h2 { font-size: var(--fs-700); }
.cta-lead p { max-width: 54ch; }
.cta-actions { display: grid; gap: var(--sp-4); justify-items: start; align-content: center; }
@media (min-width: 1200px) {
  .cta-actions { border-left: 1px solid var(--cta-rule); padding-left: var(--sp-7); max-width: 40rem; }
}
.cta-actions .trust-line { margin-top: 0; }

/* Navy CTA register: deep brand navy, distinct from the ink footer.
   `--cta-rule` is the hairline between the argument and the actions. It is a
   token on the band rather than a literal so the three band treatments under
   review (section 23b) each set their own without touching the composition. */
.band-navy { background: var(--accent-800); color: #D9E8F4; position: relative; overflow: hidden; --cta-rule: rgba(255,255,255,0.34); }
.band-navy h1, .band-navy h2, .band-navy h3 { color: #FFFFFF; }
.band-navy p { color: #D9E8F4; }
.band-navy .lede { color: #D9E8F4; }
.band-navy .muted, .band-navy .small { color: #BBD3E8; }
.band-navy a { color: #BBDDF2; }
.band-navy .btn--primary { background: #FFFFFF; color: var(--accent-800); border-color: #FFFFFF; }
.band-navy .btn--primary:hover { background: var(--accent-100); }
.band-navy .btn--primary:active { background: var(--accent-100); transform: translateY(1px); }
.band-navy .btn--secondary { background: transparent; border-color: rgba(255,255,255,0.8); color: #FFFFFF; }
.band-navy .btn--secondary:hover { border-color: #FFFFFF; background: rgba(255,255,255,0.08); box-shadow: none; }
.band-navy :focus-visible { outline-color: #FFFFFF; }
.band-navy .t-link { color: #BBDDF2; }
/* VQA-12: same ch-measure trap as VQA-06. The global paragraph cap is 68ch,
   which at 12px mono is 490px, so these one-line eyebrow labels broke with a
   four-character orphan: "...CONSULTANCY, EST. / 2011" under the hero H1,
   "...ACOUSTICS / ONLY" in the closing band. They are labels, not copy. */
.kicker, .trust-line { max-width: none; }
.trust-line {
  font: 600 var(--fs-200) var(--font-mono);
  text-transform: uppercase; letter-spacing: var(--ls-caps);
  color: #A9CBE8; margin-top: var(--sp-6);
}

/* 23b. The three CTA colour experiments (`html.cta-a`, `html.cta-b`,
   `html.cta-c`) are deleted, 57 selectors and about 1 KB gz. The shipped band is
   brand navy and is unchanged by their removal — every rule in those blocks was
   an override that only applied when a class was set on <html>, and nothing sets
   one. The owner has not chosen a colour, so the default is what it always was;
   the two alternatives simply no longer ride along in the stylesheet every
   visitor downloads. HANDOFF.md records the commit and the two-line recipe for
   reinstating them if the owner wants to see them again. */

/* Reference list request block
   Owner review, 10 Aug (item 6). This was a 720px panel with an accent top rule,
   sitting hard against the closing band so the two read as one welded object.
   It is rebuilt as the fourth composition in the project-card grammar: the same
   1px `--line-strong` edge and `--paper-0` fill as the three `.p-card--case`
   cards above it, spanning the same container edges as the grid, and split into
   two panes by a hairline in exactly the way `.feature-card` — the family's
   other full-width member — splits itself. The argument sits left, the action
   right. `--section-y-tight` beneath it detaches it from the CTA band, and the
   accent now arrives through the `.kicker--rule` device instead of a bar across
   the top, which is what put it outside the card family in the first place. */
.ref-request {
  border: 1px solid var(--line-strong);
  background: var(--paper-0); padding: 0;
  margin: var(--sp-8) 0 var(--section-y-tight);
  max-width: none; display: grid;
}
@media (min-width: 900px) { .ref-request { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } }
/* Both panes are centred while stacked, where each is the full width of the
   box and there is nothing to align to. Side by side they are top-aligned
   instead: they are the two halves of one hairline-divided box, and centring a
   short pane against a tall one started the `ON REQUEST` kicker 46px below the
   `Work email` label it should sit level with. */
.ref-lead { padding: var(--sp-6); border-bottom: 1px solid var(--line); display: flex; flex-direction: column; justify-content: center; }
@media (min-width: 900px) {
  .ref-lead { border-bottom: 0; border-right: 1px solid var(--line); justify-content: flex-start; }
  .ref-action { justify-content: flex-start; }
}
.ref-action { padding: var(--sp-6); display: flex; flex-direction: column; justify-content: center; }
.ref-action .form-sent:target { margin-bottom: var(--sp-5); }
/* VQA-04: the global h2 measure cap is 24ch; at this panel's 21px that is
   350px, so a 34-character heading broke with "list" alone on line two
   inside a panel 640px wide. The panel already caps its own measure. */
.ref-request h2 { font-size: var(--fs-600); margin-bottom: var(--sp-3); max-width: none; }
.ref-request .ref-intro { font-size: var(--fs-300); color: var(--text-muted); margin-bottom: 0; }
.ref-form { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: flex-start; }
/* The label used to sit inline to the left of the field, which put it on the
   input's baseline instead of above it and pushed the submit onto its own row.
   Same arrangement as `.field label` now: name above, control and action below. */
.ref-form label {
  flex: 1 0 100%; margin-bottom: calc(var(--sp-3) * -1 + 6px);
  font-size: var(--fs-300); font-weight: 600; color: var(--text-strong);
}
.ref-form input[type="email"] {
  flex: 1 1 220px; background: var(--paper-0); border: 1px solid var(--field-border); border-radius: var(--r-0);
  padding: 13px 14px; font: inherit; font-size: var(--fs-400); min-height: 48px; color: var(--text-strong);
}
.ref-form input[type="email"]:focus { outline: none; border-color: var(--ink-900); box-shadow: 0 0 0 1px var(--ink-900); }
.ref-form input[aria-invalid="true"] { border-color: var(--error); box-shadow: 0 0 0 1px var(--error); }
.ref-confirm {
  border-left: 3px solid var(--success); background: var(--paper-100);
  padding: var(--sp-4) var(--sp-5); font-size: var(--fs-300); color: var(--text-strong);
}
.ref-confirm p + p { margin-top: var(--sp-3); }
/* VQA-05: `.ref-hint` is in the markup on every projects view and carried no
   rule, so the note under the submit button rendered at full body size and
   full ink — heavier than the panel's own intro paragraph above it. */
.ref-hint { font-size: var(--fs-300); color: var(--text-muted); margin-top: var(--sp-3); max-width: 68ch; }

/* Enhancement-only controls. The country filter is computed in the browser and
   cannot be pre-rendered, so with JavaScript off it is absent rather than dead:
   the unfiltered list is already complete and visible. */
.js-only { display: none; }
html.js .js-only { display: block; }

/* 24. Footer */
.site-foot { background: var(--ink-900); color: var(--text-inverse-muted); position: relative; overflow: hidden; border-top: 6px solid var(--paper-50); }
.site-foot .container { position: relative; z-index: 1; }
/* Owner review, 10 Aug (item 5). The mark used to float above column one with
   the entity paragraph and the postal address beneath it, so the first column
   started 180px below the other three and the footer had no common top line.
   The mark now closes the Contact column, on a hairline plinth, and every
   column begins with its own `h2` on the same baseline: four aligned columns,
   Firm / Services / Work / Contact. In the narrow column the lockup stacks
   rather than sitting in a row, which is what lets the wordmark hold its own
   line instead of breaking against the mark. */
.foot-id {
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-3);
  margin-top: var(--sp-5); padding-top: var(--sp-5); border-top: 1px solid var(--line-dark);
}
.foot-brand { font-size: var(--fs-500); font-weight: 700; color: var(--text-inverse); letter-spacing: var(--ls-heading); line-height: 1.2; }
.foot-sub { font: 500 var(--fs-200) var(--font-mono); color: var(--text-inverse-muted); letter-spacing: 0.08em; line-height: var(--lh-tight); }
.foot-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-6); padding: var(--sp-8) 0 var(--sp-7); }
@media (min-width: 1024px) { .foot-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
/* h2, not h3: top-level sections of the contentinfo landmark, and /404.html has
   no page h2 to mask the jump from h1. Visual size unchanged. */
.foot-col h2 { font: 600 var(--fs-200) var(--font-mono); text-transform: uppercase; letter-spacing: var(--ls-caps); color: var(--text-inverse-muted); margin-bottom: var(--sp-3); }
.foot-col ul { list-style: none; margin: 0; }
.foot-col a { display: inline-block; color: var(--text-inverse); text-decoration: none; font-size: var(--fs-300); padding: 8px 0; }
.foot-col a:hover { text-decoration: underline; text-underline-offset: 4px; }
.foot-col .addr { font-size: var(--fs-300); color: var(--text-inverse-muted); font-style: normal; padding: 8px 0; }
.foot-meta { border-top: 1px solid var(--line-dark); padding: var(--sp-5) 0 var(--sp-6); font-size: var(--fs-200); }
.foot-meta p { max-width: none; }
/* VQA-02: the standards line and the copyright line sat on consecutive
   baselines with no separation, so they read as one wrapped paragraph. */
.foot-meta p + p { margin-top: var(--sp-3); }
.foot-meta a { color: var(--text-inverse); }
.proto-line { color: var(--text-inverse-muted); margin-top: var(--sp-3); }

/* Leadership card (about) */
.leader-card { display: grid; background: var(--paper-0); border: 1px solid var(--line); }
/* VQA-10: the portrait is 4:5 and filled the 2fr track, so at 1440 it stood
   455x569 and set the row height. The bio is four short paragraphs and ran
   out at 290px, leaving 279px — half the panel — as blank white beside a
   placeholder. A fixed portrait track keeps the 4:5 crop, gives the prose a
   wider measure, and closes the void.

   Owner delivery, 10 Aug: the portrait that arrived is 200x200. Filling the
   old 600x750 slot would have been a 3.8x upscale at 1x and 7.5x at 2x, so the
   card is rebuilt around what this file can actually serve. The portrait is a
   200 px square printed at exactly its native size, framed, in a tinted
   masthead rail with the role and the LinkedIn line under it, and the biography
   runs beside the rail at a wider measure than before. Nothing is upscaled: the
   image is displayed 1:1 at 1x, and the rail is a deliberate composition rather
   than a slot waiting to be filled. A 1200x1500 headshot (IMAGE-REQUEST row 1)
   would let the rail widen and the portrait return to the 4:5 crop. */
@media (min-width: 768px) { .leader-card { grid-template-columns: minmax(0, 264px) 1fr; } }
.leader-photo {
  background: var(--paper-100); border-bottom: 1px solid var(--line);
  display: flex; flex-direction: column; align-items: flex-start;
  gap: var(--sp-4); padding: var(--sp-6); position: relative;
}
.leader-portrait {
  display: block; width: 200px; height: auto; max-width: 100%;
  border: 1px solid var(--line-strong); background: var(--paper-0);
}
.leader-photo .leader-actions { margin: 0; }
@media (min-width: 768px) { .leader-photo { border-bottom: 0; border-right: 1px solid var(--line); } }
/* Between the stacked layout and the two-column card the rail is a full-width
   strip, so the portrait and its link sit on one row rather than leaving a band
   of empty tint beside a 200 px square. Below 520 the strip is narrower than
   the pair needs and they stack again; 200 px still fits at 320. */
@media (min-width: 520px) and (max-width: 767px) {
  .leader-photo { flex-direction: row; align-items: flex-end; gap: var(--sp-5); }
}
.leader-body { padding: var(--sp-6); }
.leader-body h3 { font-size: var(--fs-700); margin-bottom: var(--sp-1); }
.leader-role { font: 600 var(--fs-200) var(--font-mono); text-transform: uppercase; letter-spacing: var(--ls-caps); color: var(--accent-700); margin-bottom: var(--sp-4); }
.leader-actions { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: center; margin-top: var(--sp-5); }

/* Geographic footprint grid (about) */
.geo-grid { display: grid; grid-template-columns: 1fr; gap: 1px; background: var(--line); border: 1px solid var(--line); }
@media (min-width: 480px) { .geo-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1024px) { .geo-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.geo-cell { background: var(--paper-0); padding: var(--sp-4) var(--sp-5); }
.geo-cell a { display: inline-block; font-weight: 600; color: var(--text-strong); text-decoration: none; padding: 2px 0; }
.geo-cell a:hover { color: var(--accent-700); text-decoration: underline; text-underline-offset: 4px; }
.geo-cell .meta-mono { display: block; margin-top: 2px; }

/* 25. Mobile sticky bar */
.mobile-bar {
  display: none; position: fixed; left: 0; right: 0; bottom: 0; z-index: 70;
  background: var(--ink-900); border-top: 1px solid var(--line-dark); box-shadow: var(--shadow-overlay);
  padding-bottom: env(safe-area-inset-bottom);
}
.mobile-bar a {
  flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  min-height: 56px; font-size: var(--fs-300); font-weight: 600; color: var(--text-inverse);
  text-decoration: none; border-left: 1px solid var(--line-dark);
}
.mobile-bar a:first-child { border-left: 0; }
.mobile-bar a.enq { color: var(--accent-300); }
.mobile-bar svg { stroke: currentColor; }
/* VQA-08(c): the quick-contact bar is the standing CTA wherever the header
   CTA is not shown, so it follows the header threshold rather than 767. */
@media (max-width: 1023.98px) {
  .mobile-bar { display: flex; }
  body { padding-bottom: calc(64px + env(safe-area-inset-bottom)); }
}

/* 26. Noscript */
.noscript-note {
  background: var(--accent-100); color: var(--text-strong);
  padding: var(--sp-3) var(--gutter); font-size: var(--fs-300); text-align: center;
}

/* 27. Motion */
@media (prefers-reduced-motion: no-preference) {
  /* WEB-010: the reveal may only hide content when the script that reveals
     it is actually running. Scoped to html.js — set by an inline script in
     <head> — so with JavaScript off, or after any single JS error, nothing
     on the page is invisible. This was hiding 807 of 940 words on the home
     page while the noscript banner claimed the opposite. The class must be
     set before first paint or the reveal will flash. */
  html.js .rv { opacity: 0; transform: translateY(12px); transition: opacity 240ms cubic-bezier(0.2,0,0,1), transform 240ms cubic-bezier(0.2,0,0,1); }
  html.js .rv.in { opacity: 1; transform: none; }
  .view { animation: none; }
  .view.fade-in { animation: viewfade 200ms ease-out; }
  @keyframes viewfade { from { opacity: 0; } to { opacity: 1; } }
  .drawer { transition: transform 280ms cubic-bezier(0.2,0,0,1); }
  .drawer[data-closing] { transform: translateX(100%); }
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .rv { opacity: 1; transform: none; }
  .t-link .arr, .faq .marker::after { transition: none; }
}

/* 27b. Detail prose headings */
/* WEB-066: /about was the one view where h2 rendered at the same 44px as
   its own h1, so a visitor could not tell from the type where the page
   began and a section began. One selector, one view; the global ramp is
   untouched. */
.detail-grid h2, .prose h2, .view[data-view="about"] h2 { font-size: var(--fs-700); max-width: 40ch; }
/* REV-5 (owner item 5): the two columns of /about/'s standards framework. Both
   open on an h3 of the same rank now, so the two headings share a baseline and
   the two paragraphs start on the line below at 1024 and 1440; at 768 and below
   `card-grid--2-lg` is one column and the pair reads as two subheads. The
   `margin-bottom` was an inline style on one of the two headings, which is
   exactly how the pair came apart in the first place. */
.sf-col h3 { margin-bottom: var(--sp-3); }
.detail-grid h2 { margin-top: var(--sp-6); }
.detail-grid h2:first-child { margin-top: 0; }
/* VQA-07: the rail sized its own headings through `.rail-box h3`, but twelve
   of the eighteen rail boxes in src/views are written with <h2> — six service
   pages plus the briefing. Those fell through to `.detail-grid h2` and
   rendered at the page's own section scale, 32px, inside a 328px rail: "Named
   references" and "Featured case study" both broke over two lines and the
   sidebar out-shouted the article. Element-agnostic, and specific enough
   (0,2,1) that source order cannot undo it again. */
.rail .rail-box h2, .rail .rail-box h3 {
  font-size: var(--fs-400); line-height: var(--lh-heading);
  max-width: none; margin-top: 0; margin-bottom: var(--sp-3);
}
/* VQA-06: the measure cap is expressed in ch, so dropping this paragraph to
   fs-300 also narrowed its box to 605px inside a 693px column — the three
   links then broke one-per-line with the label stranded beside the first.
   Flex row + explicit gaps packs them and wraps predictably. */
.rel-services {
  font-size: var(--fs-300); max-width: none;
  display: flex; flex-wrap: wrap; align-items: baseline;
  column-gap: var(--sp-4); row-gap: 0;
}
.rel-services .t-link { margin-right: 0; }

/* 27c. Forced colours (Windows High Contrast Mode)
   WEB-042. High Contrast Mode users are sighted low-vision users, not
   screen-reader users, so aria-pressed does nothing for them. Measured with
   forced-colors active, the pressed and unpressed sector chips computed
   different backgrounds but rendered identically on a white Canvas, which
   made the Projects page's core interaction stateless; the FAQ +/- marker,
   the section rules and the dB rule rendered white on white; and the
   primary and secondary buttons became indistinguishable.
   forced-color-adjust: none is used only where the element IS a graphic and
   would otherwise vanish entirely. */
@media (forced-colors: active) {

  /* Filter state — the one that matters most. */
  .chip { border: 1px solid ButtonText; background: Canvas; color: ButtonText; }
  .chip .n { color: ButtonText; }
  .chip[aria-pressed="true"], .chip[aria-current="page"] {
    forced-color-adjust: none;
    background: Highlight; color: HighlightText; border-color: Highlight;
  }
  .chip[aria-pressed="true"] .n, .chip[aria-current="page"] .n { color: HighlightText; }

  /* Button hierarchy. */
  .btn--primary,
  .band-dark .btn--primary, .band-navy .btn--primary, .drawer .btn--primary {
    forced-color-adjust: none;
    background: ButtonText; color: Canvas; border: 1px solid ButtonText;
  }
  .btn--secondary,
  .band-dark .btn--secondary, .band-navy .btn--secondary {
    background: Canvas; color: ButtonText; border: 1px solid ButtonText;
  }
  .btn--secondary:hover { box-shadow: none; outline: 1px solid ButtonText; }

  /* The FAQ expand affordance is a pair of pseudo-element bars: without
     this it is the only signal that the rows open, rendered invisibly. */
  .faq .marker::before, .faq .marker::after {
    forced-color-adjust: none;
    background: CanvasText;
  }

  /* Graphic rules that carry structure, not decoration. */
  .db-rule, .db-rule--full {
    forced-color-adjust: none;
    background: CanvasText; opacity: 1;
  }
  .step::before,
  .sec-head .kicker::before, .kicker--rule::before,
  /* The scale graduations are the legend's only tick marks; a forced Canvas
     background would erase them and leave six unanchored numbers. */
  .db-ticks li::after {
    forced-color-adjust: none;
    background: CanvasText;
  }
  .ffig .bar-1, .ffig .bar-2, .ffig .bar-3, .ffig .bar-4,
  .db-band-35, .db-band-45, .db-band-55, .db-band-65, .db-band-75, .db-band-85 {
    forced-color-adjust: none;
    fill: CanvasText; background-color: CanvasText;
  }

  /* Every card family keeps a visible edge. */
  .card, .p-card, .feature-card, .insight-card, .rail-box, .fact-panel,
  .lens, .pager a, .ref-request, .form-success, .form-errors,
  .contact-panel, .map-panel, .arc-diagram, .fig, .client-logo,
  .drop-panel, .drawer, .site-head, .mobile-bar, .geo-cell,
  table.std, .mini-strip, .data-strip, .leader-card, .project-plate {
    border: 1px solid CanvasText;
  }
  .map-panel { border-width: 0; border-top: 1px solid CanvasText; }
  .fig > .arc-diagram, .fig-panel > .arc-diagram { border: 0; }
  /* REV-5: the published briefing is distinguished from the five on offer by an
     accent rule, a type step and a filled button. In forced colours the accent
     is a system colour and the button is a system pair, so the rule WEIGHT is
     the only one of the three that still carries. This used to give all six
     cards the 3px edge, which took the distinction away in the one mode that
     needs it most; the weight now follows the variant. The hairline on the five
     comes from the card-family rule above, so only the published one is named. */
  .insight-card--open { border-top-width: var(--bw-mark); }
  .iflag { border-bottom-color: CanvasText; }
  .data-strip { border-left: 0; border-right: 0; }
  /* The phase spine, the cell dividers and the deliverable plinths are all
     structure in this block, not decoration. */
  .steps, .step, .deliverable, .ref-lead, .ref-action { border-color: CanvasText; }

  /* Focus must survive, including inside the dark and navy registers. */
  :focus-visible,
  .band-dark :focus-visible, .band-navy :focus-visible,
  .drawer :focus-visible, .mobile-bar :focus-visible {
    outline: 2px solid Highlight; outline-offset: 3px;
  }

  /* Links keep the system link colour rather than the brand blue. */
  a, .t-link, .band-dark a, .band-navy a, .band-dark .t-link, .band-navy .t-link { color: LinkText; }

  /* The contour field and the plate texture are pure decoration and are
     dropped rather than forced into system colours. */
  .contour-field, .contour-field--soft, .plate-band::before { display: none; }
  .client-logo img { filter: none; opacity: 1; }
}

/* 28. Print */
@media print {
  .site-head, .site-foot, .mobile-bar, .skip, .crumbs, .btn, .chip-bar { display: none !important; }
  body { background: #fff; color: #000; }
  /* The hash router is gone; on paper an internal link is only useful if the
     real URL is printed beside it. */
  a[href^="/"]::after { content: " (https://www.g-universe.com" attr(href) ")"; font-size: 0.8em; color: #555; }
  a[href^="/"].chip::after, .card-grid a[href^="/"].stretch::after { content: ""; }
}

/* ---- generated by build.js from the real image headers (F-05). Do not edit
   here: edit logoSizingCss() in build.js. Re-derived on every build, so a
   re-cut PNG cannot leave a stale rule behind. ---- */
.client-logo img[src="/assets/logos/bae-systems-9a1b6636.png"] { max-height: 40px; width: 92%; }  /* 100x16, 6.25:1 */
.client-logo img[src="/assets/logos/aubmc-464314be.png"] { max-height: 40px; }  /* 339x80, 4.24:1 */
.client-logo img[src="/assets/logos/middle-east-airlines-768407a5.png"] { max-height: 40px; }  /* 340x61, 5.57:1 */
