/* OUTFIT, SELF-HOSTED.
   Replaces the fonts.googleapis.com stylesheet that used to sit in the <head>.
   That was a RENDER-BLOCKING THIRD-PARTY REQUEST, so first paint tracked
   Google's response time one for one — measured on /contact.html by delaying
   that one request and nothing else: FCP 272ms at 0s delay, 1092ms at 1s,
   3076ms at 3s. A blank white page for however long a third party took to
   answer, on all 15 pages, with every one of the site's own assets already on
   disk. Its two origins came out of the CSP in the same change.

   ONE FILE PER UNICODE RANGE, NOT ONE PER WEIGHT. Outfit is a variable font, so
   Google served the same woff2 for every weight it listed; `font-weight:200 700`
   declares that range to the browser and the whole design's weights come out of
   one download. Declaring separate 300/400/500/600 faces against this file would
   fetch it four times.

   The `unicode-range` values are Google's own, kept verbatim, so a browser still
   fetches only the subset it needs to paint: a page with no accented characters
   never asks for latin-ext at all. Dropping the ranges would make both files
   mandatory on every page.

   THERE IS DELIBERATELY NO <link rel="preload"> FOR THESE, and it is not an
   oversight — one was written, measured, and taken back out. A preloaded font is
   fetched at high priority, which on a slow link puts 32KB in front of the
   render-blocking stylesheet and delays FIRST PAINT. Median FCP over three runs,
   Fast 3G at 1440px: /contact.html 776ms with the preload against 620ms without,
   /services/ 944ms against 732ms — 156 to 212ms of extra blank screen bought
   nothing but a shorter swap window, and `font-display:swap` already paints
   every word immediately in the fallback face. Do not add one back without
   re-running that A/B. */
@font-face{
  font-family:"Outfit";font-style:normal;font-weight:200 700;font-display:swap;
  src:url(/assets/fonts/outfit-latin-ext.woff2) format("woff2");
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face{
  font-family:"Outfit";font-style:normal;font-weight:200 700;font-display:swap;
  src:url(/assets/fonts/outfit-latin.woff2) format("woff2");
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}

:root{
  --red:#d60027; --red-dark:#a8001f;   /* logo hue 349°, darkened to clear AA */
  --ink:#1c1714; --ink-soft:#3a312b;
  --muted:#6b6058;               /* darkened from the original tone — that one failed AA on sand */
  --line:#e0d6cb; --sand:#f3eee8; --warm:#ebe4db;
  --sans:"Outfit",system-ui,-apple-system,sans-serif;
  --s1:8px;--s2:14px;--s3:22px;--s4:34px;--s5:52px;--s6:78px;--s7:108px;
  /* THE NAV BAND'S HEIGHT, DERIVED RATHER THAN TYPED. The band is
     `position:sticky` now, so it occupies its own space in the flow and no
     page has to reserve room for it — that reservation, and the phone bug it
     was fixing (the overlay nav printing on top of the hero h1), are both gone
     with the overlay. What still needs the number is SCROLL POSITION: a sticky
     bar covers the top of whatever an in-page jump lands on, so #content and
     any :target carry a scroll-margin of exactly one band. A second hardcoded
     80 would be free to drift the moment the logo or the padding changed, and
     the failure mode of that drift is a heading hidden under the bar. These
     two are the only place either number lives — .nav's vertical padding and
     .nav-logo's height read them directly, and the phone values below override
     both inputs so --nav-h recomputes on its own. */
  --nav-pad-y:14px; --nav-logo-h:52px;
  --nav-h:calc(var(--nav-logo-h) + var(--nav-pad-y) * 2);
}
*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth}
body{
  background:var(--sand);color:var(--ink);font-family:var(--sans);
  font-size:17px;              /* was 15px */
  line-height:1.7;             /* was 1.55 */
  -webkit-font-smoothing:antialiased;
}
img{max-width:100%;display:block}
a{color:inherit;text-decoration:none}
button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}
h1,h2{font-family:var(--sans);font-weight:300;letter-spacing:-.025em;line-height:1.04;}
h3{font-family:var(--sans);font-weight:400;letter-spacing:-.015em;line-height:1.14;}
a:focus-visible,button:focus-visible{outline:2px solid var(--red);outline-offset:3px}
.on-dark a:focus-visible,.on-dark button:focus-visible{outline-color:#fff}

/* Brand palette: the logo's own hue (349°). #e4002b itself is 4.20:1 on sand,
   under the AA floor for 13px labels, so the accent is darkened to #d60027 (4.68:1).
   The logo keeps its true colour; only type and fills use the accent. */


/* SKIP LINK — WCAG 2.4.1 Bypass Blocks (Level A). Every page opens with the
   same 7 chrome controls before any content, and at 1440px a keyboard visitor
   had 15–20 tab stops to walk on every single page with no way past them and no
   landmark to jump to either. It is the FIRST thing in the document and the
   first tab stop.

   Positioned off-screen rather than `display:none` or `visibility:hidden`:
   those two remove an element from the tab order entirely, which would make
   this a link nobody can ever reach — the exact failure it exists to fix.
   `clip-path` + a 1px box, not the old `clip:rect()`, and it takes a real box
   back on focus so it is visible where it lands. */
.skip{position:absolute;left:-9999px;top:0;z-index:100;
  background:var(--red);color:#fff;padding:14px 22px;
  font-size:14px;font-weight:600;letter-spacing:.08em;text-transform:uppercase}
.skip:focus{left:0}
/* `main` has no default display in older engines and this file sets none.
   The outline is dropped on purpose: it carries tabindex="-1" so programmatic
   focus into the landmark does not draw a 2px ring around the entire page.
   Skip-to-content targets #content (first real content), not #main — on the
   home page the nav lives inside <main>, so skipping to <main> skipped nothing. */
main{display:block}
main:focus,main:focus-visible{outline:none}
#content:focus,#content:focus-visible{outline:none}
/* The nav band is sticky, so it sits over the top of anything an in-page jump
   scrolls to — including #content, which the skip link targets on every page.
   Without this the skip link lands the reader under the band. One band's
   height, derived from the two inputs above rather than typed again. */
#content,:target{scroll-margin-top:var(--nav-h)}

.u{max-width:1500px;margin:0 auto;padding:0 52px}

/* labels: 13px minimum, tracking eased from .2em */
.eyebrow{color:var(--red);font-size:13px;font-weight:600;letter-spacing:.13em;text-transform:uppercase;margin:0 0 var(--s2);line-height:1.4}
.lead{font-size:19px;color:var(--muted);max-width:38rem;line-height:1.65;margin:0}

.btn{display:inline-flex;align-items:center;gap:10px;padding:16px 28px;font-weight:600;
  font-size:14px;letter-spacing:.09em;text-transform:uppercase;transition:background .18s,transform .18s,border-color .18s}
.btn-primary{background:var(--red);color:#fff}
.btn-primary:hover{background:var(--red-dark);transform:translateY(-2px)}
.btn-ghost{background:transparent;color:#fff;border:1px solid rgba(255,255,255,.45)}
.btn-ghost:hover{border-color:#fff;background:rgba(255,255,255,.09)}
.btn-line{background:transparent;color:var(--ink);border:1px solid var(--line)}
.btn-line:hover{border-color:var(--ink)}
.ctas{display:flex;gap:14px;flex-wrap:wrap}

/* NAV — 14px links, one solid sticky band on every page */
/* STICKY, NOT AN OVERLAY. The home page used to wear this band transparently
   over its hero photograph and every other page wore a solid copy, which meant
   the one piece of chrome every page shares had two appearances, two position
   schemes, and a hero that had to reserve space for a bar that occupied none.
   One band now: solid, in the flow, pinned to the top of the viewport as the
   page scrolls. `top:0` is what makes `sticky` do anything at all — without an
   inset it behaves as `static`. z-index above the hero's own stacking (2) and
   below the lightbox (200) and the skip link (100).

   The nav paints its own background, so it must NOT carry max-width — that clips the
   paint to 1500px and lets the page background show at the edges above that width.
   The footer already does this correctly: full-bleed element, inner .u constrains.
   Here there is no inner wrapper, so the gutter absorbs the centring instead, which
   lands the content on exactly the same line as .u without any markup change. */
.nav{position:sticky;top:0;z-index:60;width:100%;background:var(--ink);border-bottom:1px solid rgba(255,255,255,.1);
  display:flex;align-items:center;gap:24px;padding:var(--nav-pad-y) max(52px, calc((100% - 1500px) / 2 + 52px))}
.nav-brand{display:flex;align-items:center;gap:13px;flex-shrink:0}
.nav-logo{height:var(--nav-logo-h);width:auto;object-fit:contain}

.nav-links{display:flex;align-items:center;gap:2px;margin-left:auto}
.nav-links a{color:rgba(255,255,255,.86);font-size:14px;font-weight:500;letter-spacing:.07em;text-transform:uppercase;padding:11px 15px;position:relative;transition:color .15s}
.nav-links a:hover,.nav-links a.here{color:#fff}
.nav-links a.here::after{content:"";position:absolute;left:15px;right:15px;bottom:2px;height:2px;background:var(--red)}
/* THE PHONE NUMBER, VISIBLE RATHER THAN HUNTED FOR. The people this site is
   written for — a QS or a project manager with a tender return date — reach for
   a phone before a form, and until now the number lived only in the footer and
   on /contact.html. It is a link, not a button, because `tel:` is a
   destination; it keeps its own casing (a phone number is not a label) and its
   own 15px, which is why it does not inherit the uppercase 14px above. */
/* THE SELECTOR CARRIES `.nav-links` ON PURPOSE, and so does .nav-cta below it.
   `.nav-links a` above is 0,1,1 — a class AND a type — so a bare `.nav-tel` at
   0,1,0 loses every property the two share no matter where it sits in the file.
   That is not hypothetical: `.nav-cta{font-size:13.5px}` was written bare and
   lost to `.nav-links a{font-size:14px}`, so the button had been painting at
   14px for as long as both rules existed. Both are qualified now. Do not
   un-qualify either one. */
.nav-links a.nav-tel{display:flex;align-items:center;gap:9px;color:#fff;font-size:15px;font-weight:500;
  letter-spacing:0;text-transform:none;padding:11px 18px;margin-left:10px;
  border:1px solid rgba(255,255,255,.28);transition:border-color .18s,color .15s}
.nav-links a.nav-tel:hover{border-color:#fff}
.nav-tel svg{width:17px;height:17px;flex:none}
.nav-links a.nav-cta{background:var(--red);color:#fff;padding:14px 22px;font-size:13.5px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;margin-left:8px;transition:background .18s,color .15s}
.nav-links a.nav-cta:hover{background:var(--red-dark)}
.nav-toggle{display:none;width:48px;height:48px;align-items:center;justify-content:center;border:1px solid rgba(255,255,255,.32);margin-left:auto}
.nav-toggle i{display:block;width:20px;height:2px;background:#fff;position:relative}
.nav-toggle i::before,.nav-toggle i::after{content:"";position:absolute;left:0;right:0;height:2px;background:#fff}
.nav-toggle i::before{top:-7px}.nav-toggle i::after{top:7px}

/* HERO */
/* NO RESERVED SPACE FOR THE NAV, and that is the point of the sticky band.
   This used to carry `padding-top:calc(var(--nav-h) + var(--s2))` because the
   nav lived inside the hero as an absolutely positioned overlay and therefore
   occupied nothing: on a phone the content plus the bar measured more than the
   min-height, the free space went to zero, and the logo printed on top of the
   h1. The band is in the flow above this element now, so the collision cannot
   occur and the reservation would only push the copy down. The generous
   padding on .hero-content below is composition, not clearance. */
.hero{position:relative;min-height:82vh;display:flex;flex-direction:column;justify-content:flex-end;color:#fff;overflow:hidden;background:var(--ink)}
.hero-media{position:absolute;inset:0}
.hero-media img{width:100%;height:100%;object-fit:cover}
/* Darker on the left than the band it replaces: the h1 is 200-weight at up to
   112px and a thin face over a photograph is the first thing to go illegible.
   Two stacked gradients — one across for the copy, one down so the bottom edge
   meets the capability band's ink instead of stopping on a lit wall. */
.hero-shade{position:absolute;inset:0;background:linear-gradient(90deg,rgba(18,14,12,.92) 0%,rgba(18,14,12,.6) 46%,rgba(18,14,12,.22) 100%),linear-gradient(180deg,rgba(18,14,12,.35) 0%,rgba(18,14,12,0) 40%,rgba(18,14,12,.75) 100%)}
.hero-content{position:relative;z-index:2;width:100%;max-width:1500px;margin:0 auto;padding:170px 52px 78px}
.hero-eyebrow{display:flex;align-items:center;gap:14px;margin:0 0 30px;
  font-size:13px;font-weight:600;letter-spacing:.13em;text-transform:uppercase;color:#ea9aa9}
/* the rule beside the label — decorative, and an empty <i> so a screen reader
   reads nothing for it, the same shape .plist uses for its square bullet */
.hero-eyebrow i{width:34px;height:1px;background:var(--red);flex:none;font-style:normal}
.hero h1{font-size:clamp(58px,7.4vw,112px);font-weight:200;letter-spacing:-.035em;line-height:.92;color:#fff;margin:0 0 30px;max-width:16ch;text-wrap:balance}
.hero h1 span{color:#ea9aa9}
.hero .sub{font-size:21px;line-height:1.6;color:rgba(255,255,255,.86);max-width:44ch;margin:0 0 42px}

/* CAPABILITY BAND — the four claims that used to sit in a translucent sand bar
   inside the hero. Out of the hero and onto its own ink ground: the bar was
   overlapping the photograph it was meant to sit under, and being inside the
   hero was what made the phone-height collision possible in the first place. */
.capband{background:var(--ink);color:#fff;border-top:1px solid rgba(255,255,255,.12)}
.capband-in{max-width:1500px;margin:0 auto;padding:0 52px;display:grid;grid-template-columns:repeat(4,1fr)}
.cap{padding:var(--s4);border-right:1px solid rgba(255,255,255,.12)}
.cap:first-child{padding-left:0}.cap:last-child{border-right:0;padding-right:0}
.cap svg{display:block;width:32px;height:32px;color:var(--red);margin-bottom:16px}
.cap b{display:block;font-size:19px;font-weight:500;letter-spacing:-.01em;margin-bottom:8px;line-height:1.25}
.cap span{display:block;font-size:15px;color:rgba(255,255,255,.66);line-height:1.45}

/* TRUSTED-BY STRIP. The same seven marks that used to sit in a bordered grid
   two thirds of the way down the page, moved above the fold where they do the
   credibility work they were put there for.
   THE HEIGHTS ARE NOT FREEHAND. Each mark is a single file with one rendition,
   so group Z of scripts/browser-check.mjs measures its natural width against
   the box it is painted into and fails anything over 4x. The square marks
   (taylor-hart and tudor-may, both 200x200) and mp-drywall (320x197) are the
   binding cases: below about 50px of height their boxes are narrow enough to
   cross that line, which is why the squares sit at 52 and the wordmarks lower.
   Optically that is the right way round anyway — a square mark needs more
   height than a wordmark to carry the same weight. Do not shrink these
   without re-reading that group. */
.trust{background:#fff;border-bottom:1px solid var(--line);padding:30px 0}
.trust-in{display:flex;align-items:center;gap:var(--s4);flex-wrap:wrap}
.trust-lb{font-size:13px;font-weight:600;letter-spacing:.13em;text-transform:uppercase;color:var(--muted);flex:none}
.trust-row{display:flex;align-items:center;gap:44px;flex-wrap:wrap;flex:1}
.tl{width:auto;height:auto;object-fit:contain;filter:grayscale(1);opacity:.6;transition:filter .25s,opacity .25s}
.tl:hover{filter:none;opacity:1}
.tl-s{max-height:30px}.tl-m{max-height:38px}.tl-xl{max-height:52px}
/* taylor-hart.jpg is a 200x200 square whose logo occupies only the middle
   third — the rest of the file is white. Sized like the other square marks it
   paints a legible band about 20px tall and reads as a smudge; the box has to
   be taller for the MARK inside it to match the others optically. This is the
   per-mark tuning the strip asks for, done against the files as they actually
   are. */
.tl-xxl{max-height:64px}

/* SECTIONS */
.sec{padding:var(--s7) 0}
.sec h2{font-size:clamp(40px,4.8vw,58px);margin:0 0 var(--s2)}
.sec-head{max-width:40rem;margin-bottom:var(--s5)}
.sand{background:var(--sand)}.white{background:#fff}
.dark{background:var(--ink);color:#fff}
.dark h2,.dark h3{color:#fff}
.dark .eyebrow{color:#ea9aa9}
.dark .lead{color:rgba(255,255,255,.76)}

.about-grid{display:grid;grid-template-columns:1.02fr .98fr;gap:var(--s6);align-items:center}
.about-visual{aspect-ratio:4/5;overflow:hidden;background:var(--warm)}
.about-visual img{width:100%;height:100%;object-fit:cover;transition:transform .8s ease}
.about-visual:hover img{transform:scale(1.03)}
.pts{display:grid;gap:var(--s4)}
.pt h3{font-size:28px;margin-bottom:8px}
.pt p{color:var(--muted);font-size:17px;line-height:1.7;max-width:36ch}

/* SELECTED WORK. One feature tile and two stacked ones rather than three equal
   cards: the photography is the argument this section is making, and three
   cards at a third of the width each made none of it. */
.work-head{display:flex;align-items:flex-end;justify-content:space-between;gap:var(--s5);flex-wrap:wrap;margin-bottom:var(--s5)}
.work-head>div{max-width:46rem}
.wgrid{display:grid;grid-template-columns:1.35fr 1fr;gap:var(--s4)}
.wstack{display:grid;gap:var(--s4)}
.wtile{position:relative;display:block;overflow:hidden;background:var(--ink);color:#fff}
.wtile img{width:100%;height:100%;object-fit:cover;opacity:.92;transition:transform .7s ease}
.wtile:hover img{transform:scale(1.04)}
.wtile-feature{aspect-ratio:4/3}
.wtile-small{aspect-ratio:16/9}
/* A SIBLING OF THE CAPTION, NOT ITS BACKGROUND. Put this gradient on .wtile-cap
   and group W of the browser harness stops judging the caption's contrast at
   all — an element whose own background is an image is skipped, because the
   probe cannot know what colour it composites to. Keeping the scrim separate
   leaves .wtile's opaque ink as the caption's nearest painted ground, which is
   also what the bottom of the gradient resolves to. */
.wtile-shade{position:absolute;inset:0;background:linear-gradient(180deg,rgba(18,14,12,0) 42%,rgba(18,14,12,.88) 100%)}
.wtile-cap{position:absolute;left:0;right:0;bottom:0}
.wtile-feature .wtile-cap{padding:44px}
.wtile-small .wtile-cap{padding:30px}
.wtile-meta{font-size:13px;letter-spacing:.13em;text-transform:uppercase;color:#ea9aa9;font-weight:600;margin-bottom:12px}
.wtile-small .wtile-meta{margin-bottom:8px}
.wtile-feature h3{font-size:44px;font-weight:300;letter-spacing:-.025em;line-height:1.05;margin-bottom:12px}
.wtile-small h3{font-size:30px;font-weight:300;letter-spacing:-.02em;line-height:1.1}
.wtile p{font-size:17px;color:rgba(255,255,255,.8);max-width:52ch}

/* SCOPE GRID — all five services, which is docs/andrei-decisions.md item 4
   answered: the home page used to show two of them and silently omit SFS,
   shaft walls and acoustic linings. auto-fit rather than a fixed column count
   so the row reflows without a second breakpoint; the border-top/left on the
   grid plus border-right/bottom on each cell is what makes the rules meet at
   the edges without doubling. */
.scope-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));border-top:1px solid var(--line);border-left:1px solid var(--line)}
.scope-cell{display:flex;flex-direction:column;gap:var(--s2);padding:38px 34px;min-height:230px;
  border-right:1px solid var(--line);border-bottom:1px solid var(--line);transition:background .18s}
.scope-cell:hover{background:var(--sand)}
.scope-no{font-size:13px;font-weight:600;letter-spacing:.13em;text-transform:uppercase;color:var(--red)}
.scope-cell h3{font-size:26px;font-weight:400;letter-spacing:-.015em;line-height:1.12}
.scope-cell p{font-size:16px;color:var(--muted);line-height:1.6}

/* HOW WE WORK — four rows separated by hairlines. The 2px grid gap over a
   --line ground is the same trick .srvs uses for the services index, rather
   than a second set of border rules. */
.hlist{display:grid;gap:2px;background:var(--line);border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
.hrow{background:var(--sand);padding:26px 0;display:grid;grid-template-columns:44px 1fr;gap:22px;align-items:start}
.hrow>span{font-size:13px;font-weight:600;letter-spacing:.1em;color:var(--red);padding-top:5px}
.hrow h3{font-size:22px;font-weight:500;letter-spacing:-.01em;margin-bottom:6px}
.hrow p{font-size:16.5px;color:var(--muted);line-height:1.6;max-width:46ch}

/* .plink is .more's twin for a link that is not "read more about this" — "All
   projects", "Price this scope". One rule for both so the two cannot drift
   into two different red underlined links. */
.more,.plink{font-size:14px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--red);border-bottom:1.5px solid currentColor;padding-bottom:3px}
.more:hover,.plink:hover{color:var(--red-dark)}
.work-head .plink{white-space:nowrap;padding-bottom:4px}

.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--s4)}
.card-img{aspect-ratio:4/3;overflow:hidden;background:var(--warm);margin-bottom:var(--s3)}
.card-img img{width:100%;height:100%;object-fit:cover;transition:transform .7s cubic-bezier(.2,.6,.2,1)}
.card:hover .card-img img{transform:scale(1.045)}
.card-meta{font-size:13px;letter-spacing:.12em;text-transform:uppercase;color:var(--red);margin-bottom:9px;font-weight:600}
.dark .card-meta{color:#ea9aa9}
.card h3{font-size:32px;margin-bottom:9px}
.card p{font-size:16.5px;color:var(--muted);line-height:1.65;margin-bottom:var(--s2)}
.dark .card p{color:rgba(255,255,255,.74)}
.card .go{font-size:14px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--red)}
.dark .card .go{color:#ea9aa9}

/* THE PROJECTS INDEX, two up instead of three. A case study is read, not
   scanned past: at a third of the width the photograph is a thumbnail and the
   teaser is four lines of wrap. Half the width gives the image something to
   say and the copy a measure. */
.cards-2{grid-template-columns:repeat(2,1fr);gap:var(--s5) var(--s4)}
.cards-2 .card h3{font-size:34px;font-weight:300;letter-spacing:-.025em;line-height:1.06;margin-bottom:12px}
.cards-2 .card p{font-size:17px;max-width:52ch;margin-bottom:18px}
.cards-2 .card .go{display:inline-block;border-bottom:1.5px solid currentColor;padding-bottom:3px}
/* The meta row is sector-then-place. `b` is the sector in the accent, `i` the
   place and year in muted — two elements rather than one string, because they
   are two different facts and the chip row filters on only one of them. */
.card-meta b{font-weight:600}
.card-meta i{font-style:normal;font-weight:400;color:var(--muted);letter-spacing:.1em;margin-left:14px}
/* WHAT WE ACTUALLY SELF-DELIVERED, on the index rather than one click in. A QS
   comparing subcontractors is looking for the scope, and until now the only
   way to see it was to open each case study in turn. */
.scopes{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:20px}
.scopes span{font-size:13px;color:var(--ink-soft);border:1px solid var(--line);background:#fff;padding:6px 12px;line-height:1.3}

.cta-grid{display:grid;grid-template-columns:1.3fr 1fr;gap:var(--s5);align-items:end}
.cta-grid p{color:rgba(255,255,255,.76);font-size:19px;max-width:34rem;margin-bottom:var(--s4);line-height:1.65}
.cside{border-left:3px solid var(--red);padding-left:var(--s3)}
.cside dt{font-size:13px;letter-spacing:.13em;text-transform:uppercase;color:#ea9aa9;margin-bottom:5px}
.cside dd{margin-bottom:var(--s3);font-size:18px;color:var(--warm)}
.cside dd:last-child{margin-bottom:0}
/* The home page's closing band. Same component as every other page's, one
   notch wider and vertically centred, because it carries a display-weight
   heading rather than the standard one. */
.cta-grid.cta-wide{grid-template-columns:1.25fr 1fr;gap:var(--s6);align-items:center}
.cta-grid.cta-wide p{max-width:46ch}

/* THE DARK PAGE HEAD, one shape on every inner page. 78 above and 52 below:
   the head introduces the page rather than being a band of its own, so it sits
   closer to the content it opens than to the nav above it. */
.phead{background:var(--ink);color:#fff;padding:var(--s6) 0 var(--s5)}
/* The page head is a DARK ground, so the accent eyebrow has to be the light
   tint, not --red. `.eyebrow` above sets colour:var(--red), which is 4.68:1 on
   sand and passes; on --ink it computes to 3.24:1 at 13px/600 and fails AA,
   which it was doing on 14 of the 15 pages. The stylesheet already had the
   answer — `.dark .eyebrow{color:#ea9aa9}` (8.08:1 on --ink) — but .phead is
   its own dark block and is not `.dark`, so it never picked it up. Keep this in
   step with that rule; if a third dark surface appears, give it the same tint
   rather than a new one. */
.phead .eyebrow{color:#ea9aa9}
.crumb{font-size:14px;letter-spacing:.07em;text-transform:uppercase;color:rgba(255,255,255,.68);margin-bottom:var(--s3);display:flex;gap:11px;align-items:center;flex-wrap:wrap}
.crumb a:hover{color:#fff}.crumb i{opacity:.45;font-style:normal}
.phead h1{font-size:clamp(44px,5.4vw,76px);font-weight:200;letter-spacing:-.035em;line-height:1.02;margin-bottom:var(--s3);max-width:18ch}
.phead .lead{color:rgba(255,255,255,.76);max-width:52ch}
.pmeta{display:flex;gap:var(--s5);flex-wrap:wrap;margin-top:var(--s5);padding-top:var(--s3);border-top:1px solid rgba(255,255,255,.16)}
.pmeta dt{font-size:13px;letter-spacing:.13em;text-transform:uppercase;color:#ea9aa9;margin-bottom:6px}
.pmeta dd{font-size:18px;color:#fff;font-weight:500}

.filters{display:flex;gap:9px;flex-wrap:wrap;margin-bottom:var(--s5)}
/* `transition` NAMES ITS PROPERTIES — it must never go back to `all`. `all`
   includes outline-color, so the focus ring animated from whatever the chip's
   currentColor happened to be: on the pressed chip that is #fff, which is
   1.0:1 against the sand page, so a keyboard visitor who tabbed onto it got no
   visible ring at all for the first ~180ms. Measured 25ms after a real Tab
   keystroke: outline-color rgb(252,234,237). A focus indicator has to be there
   the instant focus arrives. */
/* `display:inline-flex` is here so the class can dress something other than a
   <button>: the enquiry form's scope chips are <label>s wrapping a real
   checkbox, and an inline element ignores min-height entirely. A <button> is
   blockified as a flex item inside .filters either way, so nothing about the
   sector row changes. */
.filt{display:inline-flex;align-items:center;justify-content:center;
  border:1px solid var(--line);background:#fff;padding:12px 20px;font-size:13.5px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);transition:background .18s,border-color .18s,color .18s;min-height:44px}
.filt:hover{border-color:var(--ink);color:var(--ink)}
.filt.on{background:var(--ink);border-color:var(--ink);color:#fff}
/* The filtered-count line, announced via role="status". It rides in the chip
   row as its last flex item so it inherits the row's own bottom margin and
   nothing above the grid moves when it changes. */
.filt-count{margin-left:auto;align-self:center;font-size:14px;color:var(--muted)}

.phero{aspect-ratio:21/9;overflow:hidden;background:var(--warm)}
.phero img{width:100%;height:100%;object-fit:cover}
.pbody{display:grid;grid-template-columns:1.55fr 1fr;gap:var(--s6)}
.pbody .rt p{margin-bottom:var(--s3);color:var(--ink-soft);font-size:18px;line-height:1.78;max-width:58ch}
.pbody h2{font-size:28px;font-weight:400;letter-spacing:-.015em;line-height:1.14;margin:var(--s5) 0 var(--s2)}
.pbody h3{font-size:28px;margin:var(--s5) 0 var(--s2)}
.plist{list-style:none;border-top:1px solid var(--line)}
.plist li{padding:14px 0;border-bottom:1px solid var(--line);font-size:17px;color:var(--ink-soft);display:flex;gap:14px}
.plist li i{width:6px;height:6px;background:var(--red);flex:none;margin-top:10px;font-style:normal}
.facts{background:#fff;border:1px solid var(--line);padding:var(--s4);align-self:start}
.facts dt{font-size:13px;letter-spacing:.13em;text-transform:uppercase;color:var(--muted);margin-bottom:5px}
.facts dd{margin-bottom:var(--s3);font-size:18px;font-weight:500}
.facts dd:last-child{margin-bottom:0}

/* THE ENQUIRY FORM.
   It posts to Netlify Forms — the one mechanism that needs no build step, no
   backend and no dependency, which is the whole reason this page carried a
   "there is no form on this site" paragraph until now. The success state is a
   real page (/contact-thanks.html) rather than scripted state, so it survives
   with JavaScript disabled; see docs/andrei-decisions.md item 17.

   NOTHING HERE NEEDS JAVASCRIPT. The scope chips are real checkboxes wearing
   .filt — Space toggles them, a screen reader announces them as checkboxes,
   and they submit as repeated `scope` values. A scripted toggle would have had
   to be hidden from a non-scripted visitor the way the sector filter is, and
   that would have withheld half the form. */
.cgrid{display:grid;grid-template-columns:1.5fr 1fr;gap:var(--s6);align-items:start}
.enq{background:#fff;border:1px solid var(--line);padding:var(--s5)}
.fgrid{display:grid;grid-template-columns:1fr 1fr;gap:26px}
.fld{display:grid;gap:var(--s1)}
.fld>span{font-size:13px;font-weight:600;letter-spacing:.13em;text-transform:uppercase;color:var(--muted)}
/* 16px IS LOAD-BEARING, not a rounding of 15 or 17: iOS Safari zooms the page
   in on focus for any field below 16px and does not zoom back out, which
   leaves the rest of the form off-screen mid-entry. */
.fld input,.fld textarea{width:100%;border:1px solid var(--line);background:#fff;color:var(--ink);
  font-family:inherit;font-size:16px;line-height:1.4;padding:14px 16px;min-height:52px;
  transition:border-color .18s}
.fld textarea{resize:vertical}
.fld input:focus,.fld textarea:focus{border-color:var(--ink)}
.fld input::placeholder,.fld textarea::placeholder{color:var(--muted);opacity:1}
.fset{border:0;margin-top:30px}
.fset legend{font-size:13px;font-weight:600;letter-spacing:.13em;text-transform:uppercase;color:var(--muted);margin-bottom:12px}
.chips{display:flex;flex-wrap:wrap;gap:9px}
/* The checkbox itself covers the chip and is transparent, so a click anywhere
   on the chip toggles it and the control keeps its own hit area rather than
   depending on a label association alone. It is NOT display:none — that would
   take it out of the tab order and out of the accessibility tree, which is
   the whole thing being borrowed here. */
.chip{position:relative;display:inline-flex}
.chip input{position:absolute;inset:0;width:100%;height:100%;margin:0;opacity:0;cursor:pointer}
.chip .filt{text-transform:none;letter-spacing:0;font-weight:500;font-size:14px;padding:11px 18px}
.chip input:checked+.filt{background:var(--ink);border-color:var(--ink);color:#fff}
.chip input:hover+.filt{border-color:var(--ink);color:var(--ink)}
.chip input:checked:hover+.filt{color:#fff}
/* The ring has to follow the checkbox's focus onto the thing the eye sees;
   the input itself is transparent and would draw an invisible one. */
.chip input:focus-visible+.filt{outline:2px solid var(--red);outline-offset:3px}
.fld-full{margin-top:30px}
.btn-send{margin-top:30px;min-height:56px}
.enq-note{margin-top:16px;font-size:14.5px;color:var(--muted)}
.enq-note a{color:var(--red);border-bottom:1px solid currentColor}
.enq-note a:hover{color:var(--red-dark)}
/* A honeypot: named in the form's netlify-honeypot attribute, hidden from
   everyone, and filled only by something that is not reading the page. */
.hp{display:none}

/* The sidebar: one dark block for the ways to reach a person, one white block
   for the registrations, sharing an edge. */
.cdark{background:var(--ink);color:#fff;padding:42px}
.sec .cdark h2{font-size:26px;font-weight:400;letter-spacing:-.015em;margin-bottom:26px}
.cside-flush{border-left:0;padding-left:0}
.cbox{background:#fff;border:1px solid var(--line);border-top:0;padding:42px}
.cbox h3{font-size:20px;font-weight:500;margin-bottom:18px}
/* .facts brings its own card; inside .cbox that card is already drawn. */
.cbox .facts{background:none;border:0;padding:0}

/* THE SUCCESS PAGE. A page, not a state — see the note at the top of this
   block. The tick is a decorative glyph beside a real heading, so it carries
   no text a screen reader has to read out. */
.sent{background:#fff;border:1px solid var(--line);padding:var(--s5);max-width:52rem}
.sent-tick{width:52px;height:52px;background:var(--red);color:#fff;display:flex;align-items:center;justify-content:center;font-size:26px;line-height:1;margin-bottom:18px}
.sec .sent h2{font-size:34px;font-weight:300;letter-spacing:-.025em;margin-bottom:18px}
.sent p{color:var(--muted);font-size:18px;max-width:46ch;margin-bottom:var(--s3)}
.sent .ctas{margin-top:var(--s4)}
.gal{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
.gt{aspect-ratio:1;overflow:hidden;background:var(--warm);width:100%;display:block;position:relative;padding:0}
.gt img{width:100%;height:100%;object-fit:cover;transition:transform .5s ease}
.gt:hover img{transform:scale(1.06)}
.gt::after{content:"";position:absolute;inset:0;transition:background .2s}
.gt:hover::after{background:rgba(214,0,39,.22)}

.srvs{display:grid;gap:2px;background:var(--line);border:1px solid var(--line)}
.srv{background:#fff;display:grid;grid-template-columns:minmax(0,.9fr) minmax(0,1.1fr);gap:var(--s5);align-items:center;padding:var(--s5)}
.srv:nth-child(even){grid-template-columns:minmax(0,1.1fr) minmax(0,.9fr)}
.srv:nth-child(even) .srv-img{order:2}
.srv-img{aspect-ratio:4/3;overflow:hidden;background:var(--warm)}
.srv-img.diagram{background:#fff;display:flex;align-items:center;justify-content:center;padding:var(--s3)}
.srv-img.diagram img{width:auto;height:100%;object-fit:contain}
.srv:hover .srv-img.diagram img{transform:none}
.srv-img img{width:100%;height:100%;object-fit:cover;transition:transform .7s ease}
.srv:hover .srv-img img{transform:scale(1.04)}
/* The row's number. Generated from position, like the scope grid's, so the
   index reads 01..05 down the page and reordering the data cannot desync it. */
.srv-no{font-size:13px;font-weight:600;letter-spacing:.13em;text-transform:uppercase;color:var(--red);margin-bottom:14px}
.srv h3{font-size:40px;font-weight:300;letter-spacing:-.025em;line-height:1.06;margin-bottom:14px}
.srv>.srv-tx>p{color:var(--muted);font-size:18px;margin-bottom:var(--s3);max-width:50ch}
/* Two ways out of a row, and they go to different places: the service's own
   page, and the enquiry form. The second is what the redesign asked for; the
   first is the only entry point three of the five service pages have anywhere
   on the site, so it stays. */
.srv-links{display:flex;gap:var(--s4);flex-wrap:wrap;align-items:center;margin-top:var(--s5)}
@media(max-width:980px){
  .srv,.srv:nth-child(even){grid-template-columns:1fr;gap:var(--s3);padding:var(--s4) var(--s3)}
  .srv-links{gap:var(--s3);margin-top:var(--s4)}
  .srv:nth-child(even) .srv-img{order:0}
  .srv h3{font-size:28px}
}


/* LIGHTBOX */
.lb{position:fixed;inset:0;z-index:200;background:rgba(18,14,12,.97);display:none;align-items:center;justify-content:center;padding:28px}
.lb.on{display:flex}
.lb img{max-width:min(1200px,94vw);max-height:86vh;width:auto;height:auto;object-fit:contain}
.lb button{position:absolute;color:#fff;width:54px;height:54px;display:flex;align-items:center;justify-content:center;
  font-size:30px;line-height:1;background:rgba(255,255,255,.1);transition:background .16s}
.lb button:hover{background:var(--red)}
.lb .x{top:20px;right:20px}
.lb .pv{left:20px;top:50%;transform:translateY(-50%)}
.lb .nx{right:20px;top:50%;transform:translateY(-50%)}
.lb .cap{position:absolute;bottom:22px;left:50%;transform:translateX(-50%);color:#cfc7bf;font-size:14px;letter-spacing:.1em;text-transform:uppercase}
@media(max-width:980px){.lb .pv,.lb .nx{bottom:20px;top:auto;transform:none}.lb .pv{left:20px}.lb .nx{right:20px}}
.nextp{border-top:1px solid var(--line);padding:var(--s5) 0 0;display:flex;justify-content:space-between;align-items:center;gap:var(--s4);flex-wrap:wrap;margin-top:var(--s5)}
/* Was `.nextp .lb` — a name collision with the lightbox overlay above, which
   is `.lb{display:none}`. This rule sets no `display`, so the label inherited
   none and the "Next project" eyebrow rendered as nothing at all. The overlay
   owns `.lb`; the label is `.nextp-lb`. Do not put it back. */
.nextp-lb{font-size:13px;letter-spacing:.13em;text-transform:uppercase;color:var(--muted);margin-bottom:7px}
.nextp h3{font-size:32px}
/* Page scroll is locked while the lightbox is up: it is a modal over the whole
   viewport, and scrolling the document behind it moves the page the visitor is
   about to be returned to. */
.lb-open{overflow:hidden}

.ft{background:#120e0c;color:#a2978d;padding:var(--s6) 0 var(--s4);font-size:16px}
.ft-grid{display:grid;grid-template-columns:2fr 1fr 1fr 1fr;gap:var(--s5);margin-bottom:var(--s5)}
.ft h3{color:#fff;font-size:14px;letter-spacing:.11em;text-transform:uppercase;margin-bottom:var(--s3);font-weight:600;font-family:var(--sans)}
.ft ul{list-style:none}.ft li{margin-bottom:11px}
.ft a:hover{color:#fff}
/* REFLOW GUARD (WCAG 2.1 AA, 1.4.10). A grid item's default `min-width:auto`
   makes its track at least as wide as the longest unbreakable word inside it,
   and the Contact column holds a 30-character email address whose min-content
   width measures 145.5px. Two of those plus the 34px gap and 44px of page
   padding demand 369px of DEVICE width — so at 320px and at 360px the document
   grew to 369px and every page scrolled sideways, while 375px cleared it by
   6px and looked fine. These two declarations remove the mechanism rather than
   the symptom: the track may shrink past its longest word, and a word that no
   longer fits wraps instead of pushing the page wider. They are inert at every
   width the site currently renders (the tracks are all above min-content
   there), so nothing visible changes today — they are what stops one longer
   link ever reopening this. The stacking below is the layout answer. */
.ft-grid>*{min-width:0}
.ft li{overflow-wrap:break-word}
.ft-mark{width:124px;height:auto;margin-bottom:var(--s3);object-fit:contain}
.ft-legal{font-size:13px;line-height:1.6;color:#8a7f76;padding-top:var(--s3);border-top:1px solid #251d18}
.ft-legal+.ft-bot{border-top:0;padding-top:var(--s2)}
.ft-bot{border-top:1px solid #251d18;padding-top:var(--s3);display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;font-size:14px;letter-spacing:.05em}


@media(max-width:980px){
  .u{padding:0 22px}
  /* The two inputs to --nav-h above; the scroll-margin on #content follows. */
  :root{--nav-pad-y:12px; --nav-logo-h:44px}
  .nav{padding:var(--nav-pad-y) 22px}
  /* Collapsed by default, toggle visible: this is the state a browser WITH
     JavaScript ends up in, so making it the default means first paint is
     already correct and there is nothing to flash. The no-JS state is
     restored by assets/css/nojs.css, which only a browser without scripting
     ever requests (see the <noscript> link in templates/layout.html). */
  .nav-links{display:none}
  .nav-toggle{display:flex}
  .hero-content{padding:var(--s6) 22px var(--s5)}
  .capband-in{padding:0 22px;grid-template-columns:1fr 1fr}
  .cap{border-right:none;border-bottom:1px solid rgba(255,255,255,.12);padding:var(--s3) 0}
  .cap:nth-child(odd){padding-right:14px}
  .cap:nth-child(even){padding-left:14px;border-left:1px solid rgba(255,255,255,.12)}
  /* The label goes above the marks rather than beside them: at this width the
     row wraps to three or four lines and a vertically centred label ends up
     floating halfway down a block it is supposed to introduce. */
  .trust-in{flex-direction:column;align-items:flex-start;gap:var(--s3)}
  .trust-row{gap:var(--s3) var(--s4);flex:none}
  .wtile-feature .wtile-cap,.wtile-small .wtile-cap{padding:var(--s3)}
  .wtile-feature h3{font-size:30px}
  .wtile p{font-size:16px}
  .scope-cell{padding:var(--s4) var(--s3);min-height:0}
  .sec{padding:var(--s6) 0}
  .about-grid,.wgrid,.cards,.cards-2,.cta-grid,.cta-grid.cta-wide,.pbody,.cgrid,.fgrid{grid-template-columns:1fr;gap:var(--s4)}
  .enq,.cdark,.cbox,.sent{padding:var(--s4) var(--s3)}
  .fgrid{gap:var(--s3)}
  .cards-2 .card h3{font-size:28px}
  .gal{grid-template-columns:repeat(3,1fr)}
  .phead{padding:var(--s5) 0}
  .pmeta{gap:var(--s3) var(--s4)}
  .ft-grid{grid-template-columns:1fr 1fr;gap:var(--s4)}
}

/* The footer stacks on a phone. Two columns need 369px of device width to hold
   the email address on one line (see the reflow guard above), so at 375px the
   layout cleared its own constraint by 6px — coincidence, not a margin, and any
   one link growing a word would have put every page back into horizontal scroll.
   420px puts real distance between the layout and the constraint and covers every
   common phone from the 320px iPhone SE to the 414px Plus sizes. Above 420px
   nothing changes; the same links, in the same order, in one column instead of
   two. Do not tighten this towards 369 — that is the number that broke. */
@media(max-width:420px){
  .ft-grid{grid-template-columns:1fr;gap:var(--s4)}
}

/* Utility/component classes replacing the preview's inline style="" attributes
   (production CSP forbids inline styles). See task-3-report.md for the full
   attribute -> class conversion table. */
.obj-top{object-position:center 45%}
.ft-bot-flush{border:0;padding:0}
.mt-s5{margin-top:var(--s5)}
.max-34ch{max-width:34ch}
.sec-flush-top{padding-top:0}
/* The projects index opens on its chip row rather than a heading, so it sits
   closer under the page head than a full section would. */
.sec-tight-top{padding-top:44px}
.sec-head-top{padding-top:var(--s6)}
.sec-head-tight{margin-bottom:var(--s4)}
.sec-head-wide{max-width:46rem}
.lead-wide{max-width:44rem}
.h2-sm{font-size:40px}
/* THESE TWO CARRY `.sec h2` ON PURPOSE. `.sec h2` above is 0,1,1 — a class and
   a type — so a bare `.h2-display` at 0,1,0 loses its font-size to it wherever
   it sits in the file, which is exactly what has been happening to `.h2-sm`
   directly above: that class has never once taken effect on a page, because
   every element wearing it is inside a .sec. It is left alone here rather than
   quietly changing four pages the redesign does not touch — see
   docs/andrei-decisions.md item 20 — but the new ones are qualified. */
.sec h2.h2-display{font-size:clamp(40px,4.6vw,64px);font-weight:200;letter-spacing:-.03em;line-height:1.02;margin-bottom:18px}
.sec h2.h2-display-sm{font-size:clamp(36px,3.6vw,52px);font-weight:200;letter-spacing:-.03em;line-height:1.04;margin-bottom:42px}
.btn-lg{padding:19px 34px}

@media(prefers-reduced-motion:reduce){
  *{transition-duration:.01ms!important;animation-duration:.01ms!important}
  html{scroll-behavior:auto}
}

/* MOBILE NAV PANEL — the shape the collapsed links take when the toggle
   opens them. No `display` is declared here: the baseline block above owns
   that (`none`), and `.open` below turns it on. Declaring it twice is how
   these two blocks would start fighting. */
@media(max-width:980px){
  .nav-links{
    position:absolute;top:100%;left:0;right:0;width:auto;
    flex-direction:column;align-items:stretch;gap:0;
    background:var(--ink);border-top:1px solid rgba(255,255,255,.1);
    padding:var(--s2) 22px var(--s3);
  }
  .nav-links.open{display:flex;animation:nav-drop .18s ease}
  .nav-links a{padding:14px 0;width:100%;text-align:center}
  /* the accent underline is a desktop affordance; in the panel it would sit
     under a full-width row rather than under the label */
  .nav-links a.here::after{left:50%;right:auto;width:22px;margin-left:-11px;bottom:6px}
  /* Both qualified with `.nav-links` for the same specificity reason as the
     desktop rules above — `.nav-links a` is 0,1,1 and would otherwise win the
     padding back. */
  .nav-links a.nav-tel{margin-left:0;margin-top:var(--s2);justify-content:center;padding:14px 18px}
  .nav-links a.nav-cta{margin-left:0;margin-top:var(--s1);text-align:center;padding:15px 20px}
}
/* Guarded by the existing @media(prefers-reduced-motion:reduce) block above,
   which caps every animation-duration in this stylesheet at .01ms. */
@keyframes nav-drop{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:none}}
