@font-face{font-family:'DM Sans';font-style:normal;font-weight:400 450;font-display:swap;src:url('/assets/dm-sans-regular.woff2') format('woff2'),url('/assets/dm-sans-regular.ttf') format('truetype')}@font-face{font-family:'DM Sans';font-style:normal;font-weight:500 550;font-display:swap;src:url('/assets/dm-sans-medium.woff2') format('woff2'),url('/assets/dm-sans-medium.ttf') format('truetype')}@font-face{font-family:'DM Sans';font-style:normal;font-weight:600 700;font-display:swap;src:url('/assets/dm-sans-semibold.woff2') format('woff2'),url('/assets/dm-sans-semibold.ttf') format('truetype')}
:root{--blue:#183377;--ink:#182638;--gold:#eeb72b;--muted:#647080;--pale:#f3f5f7;--line:#dfe3e8;--field:#7d8b9b;--orange:#f3641e}/* The same setting-out grid the footer carries, in ink for light grounds: identical 48px
   minor and 240px major modules, so the two read as one drawing rather than two ideas.
   Ink on white is far more present than white on navy at equal alpha, hence 2% and 6%
   here against the footer's 2.7% and 11%. Each user paints its own ground back over the
   lower part of the stack, which is what stops the grid before the content ends. */
:root{--drawing-grid:repeating-linear-gradient(90deg,#1826380f 0 1px,transparent 1px 240px),repeating-linear-gradient(0deg,#1826380f 0 1px,transparent 1px 240px),repeating-linear-gradient(90deg,#18263805 0 1px,transparent 1px 48px),repeating-linear-gradient(0deg,#18263805 0 1px,transparent 1px 48px)}*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;font-family:'DM Sans',Arial,sans-serif;color:var(--ink);background:white}a{color:inherit;text-decoration:none}button,input,select,textarea{font:inherit}button,a,input,select,textarea{-webkit-tap-highlight-color:transparent}button{cursor:pointer}/* Two-colour focus ring. Gold alone sits at 1.84:1 on white and 1.68:1 on the pale
   ground, well under the 3:1 a focus indicator needs. The two bands contrast with each
   other at 8.33:1, so whichever background is behind them one band always reads: ink
   carries white and pale (15.29:1, 13.99:1), gold carries the navy panel and the footer
   (6.43:1, 8.24:1). Total width matches the 5px offset this replaces, so nothing moves. */
a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible{outline:2px solid var(--ink);outline-offset:3px;box-shadow:0 0 0 3px var(--gold)}img{max-width:100%;display:block}button{color:inherit}h1,h2,h3,p{margin-top:0}h1,h2,h3{font-weight:500;letter-spacing:-.055em}h1{font-size:clamp(48px,6.7vw,100px);line-height:1.02}h2{font-size:clamp(36px,4.1vw,62px);line-height:1.08}h3{font-size:27px;line-height:1.2}p{line-height:1.7}header{height:108px;background:white;display:flex;align-items:center;justify-content:space-between;padding:0 5%;position:relative;z-index:20}.brand img{width:160px;height:69px;object-fit:contain}.nav{display:flex;align-items:center;gap:35px;font-size:14px;font-weight:550}/* 13px of padding over a 17.5px line box lands on 43.5px; naming the line height makes
   the sum exactly the 44px tap target the rest of the chrome is built to. */
.nav a{padding:13px 0;white-space:nowrap;line-height:18px}/* The rule under the current tab is the logo's orange, sampled from the brand mark.
   3.16:1 on white and 3.89:1 over the hero scrim, so it clears the 3:1 a state
   indicator needs — and weight and colour still carry the state alongside it. */
.nav a.active{color:var(--blue);box-shadow:0 2px var(--orange);font-weight:650}.nav .contact-link{padding:14px 22px;background:var(--blue);color:white}/* Padding takes the tap area to 44px without moving the label; the negative margin
   keeps it flush with the page gutter. */
.menu{display:none;border:0;background:none;font-size:16px;padding:12px;margin-right:-12px}.wrap{width:90%;max-width:1440px;margin:auto}.hero{position:relative;height:calc(100svh - 108px);min-height:580px;max-height:940px;color:white;background:var(--blue);overflow:hidden}.hero video,.hero>.hero-image{position:absolute;width:100%;height:100%;object-fit:cover}.hero:after{content:'';position:absolute;inset:0;background:linear-gradient(90deg,rgba(5,19,39,.58),rgba(5,19,39,.05)),linear-gradient(0deg,rgba(5,19,39,.5),transparent 65%)}.hero-content{position:absolute;z-index:1;left:5%;bottom:16%;max-width:880px}.hero h1{margin:0 0 28px}.hero-content p{font-size:18px;max-width:440px;color:#eff3f7}.hero-bottom{position:absolute;z-index:2;bottom:34px;left:5%;right:5%;display:flex;justify-content:space-between;align-items:center;font-size:13px}.hero-bottom span{display:flex;align-items:center;gap:12px}.dot{width:7px;height:7px;border-radius:50%;background:var(--gold)}.video-toggle{border:1px solid #ffffff70;background:#12243840;color:white;padding:10px 16px;border-radius:30px;min-height:44px;display:inline-flex;align-items:center}.button{display:inline-flex;align-items:center;justify-content:space-between;gap:34px;padding:17px 23px;background:var(--blue);color:white;font-size:14px;font-weight:550;border:0;min-height:52px}.button.light{background:white;color:var(--blue)}.arrow{font-size:23px;line-height:1}.text-link{display:inline-flex;align-items:center;gap:35px;padding:12px 0;border-bottom:1px solid currentColor;font-size:14px;font-weight:550}.section{padding:108px 0}.intro{display:grid;grid-template-columns:1fr 1.05fr;gap:12%}.label{font-size:14px;color:var(--muted);margin-bottom:28px;letter-spacing:0}.intro h2{max-width:620px}.intro .body{padding-top:47px}.body p{color:var(--muted);font-size:17px;max-width:580px}.facts{display:flex;gap:75px;margin-top:50px}.fact strong{font-size:48px;letter-spacing:-.06em;font-weight:500;color:var(--blue);display:block}.fact span{font-size:13px;color:var(--muted)}.pale{background:var(--pale);background-image:linear-gradient(180deg,transparent 0 50%,var(--pale) 93%),var(--drawing-grid)}.section-head{display:flex;align-items:end;justify-content:space-between;gap:30px;margin-bottom:45px}.section-head h2{margin-bottom:0}.project-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:45px 28px}.project-card img{width:100%;aspect-ratio:1.5;object-fit:cover;transition:transform .5s}.project-card .photo{overflow:hidden;background:#dfe4ea}.project-card:hover img{transform:scale(1.035)}.project-meta{display:flex;align-items:start;justify-content:space-between;padding-top:22px;gap:20px}.project-meta h3{margin:0 0 8px}.project-meta p{font-size:13px;color:var(--muted);margin:0}.project-meta .arrow{color:var(--blue)}.sector-list{border-top:1px solid var(--line)}/* The icon takes a leading column rather than sitting inside the heading, so the names
   stay on one baseline. It is deliberately quieter than the text — this row is the most
   typographic thing on the site and the mark is a row marker, not a picture. */
.sector-row{padding:26px 0;border-bottom:1px solid var(--line);display:grid;grid-template-columns:auto 1fr 1fr 30px;align-items:center;gap:20px}
.sector-row .icon{width:22px;height:22px;color:var(--field)}
.sector-row:hover .icon{color:var(--blue)}.sector-row h3{margin:0;font-size:31px}.sector-row p{margin:0;color:var(--muted);font-size:14px;max-width:430px}.sector-row:hover{color:var(--blue)}.feature{display:grid;grid-template-columns:1.1fr 1fr;min-height:620px}.feature-media{position:relative;min-height:400px;background:#dbe0e5}.feature-media video,.feature-media img{width:100%;height:100%;position:absolute;object-fit:cover}.feature-copy{padding:85px 12%;background:var(--blue);color:white;display:flex;justify-content:center;flex-direction:column}.feature-copy p{color:#dbe2ef}.feature-copy .label{color:#e0b84c}.feature-copy .text-link{align-self:start}.cta{display:flex;justify-content:space-between;align-items:center;gap:40px}.cta h2{max-width:740px;margin:0}/* The footer is the one full-width navy plate on every page and carried no mark of the
   trade at all. Two grids give it one: a 48px minor module with a 240px major line laid
   over it, the way setting-out lines sit on a drawing rather than graph paper — a single
   module would just read as squared paper. White at 2.7% and 11% over #10254b is texture
   only, so every text colour below keeps the ratio documented above. The first layer
   paints the navy back over the lower half, which stops the grid before the legal strip
   instead of tiling flat into it; masking the element itself would have taken the navy
   with it. Gold on top edge is the same 2px rule the values blocks carry. */
footer{background:#10254b;color:white;padding:70px 5% 25px;border-top:2px solid var(--gold);background-image:linear-gradient(180deg,transparent 0 50%,#10254b 90%),repeating-linear-gradient(90deg,#ffffff1c 0 1px,transparent 1px 240px),repeating-linear-gradient(0deg,#ffffff1c 0 1px,transparent 1px 240px),repeating-linear-gradient(90deg,#ffffff07 0 1px,transparent 1px 48px),repeating-linear-gradient(0deg,#ffffff07 0 1px,transparent 1px 48px)}.footer-grid{display:grid;grid-template-columns:2fr 1fr 1.2fr;gap:60px}.footer-brand{font-size:32px;letter-spacing:-.04em;font-weight:650}.footer-brand small{display:block;font-size:11px;letter-spacing:.15em;font-weight:400;margin-top:12px;color:#bbc7da}.footer-grid p,.footer-grid a{font-size:14px;color:#c2ccdb;line-height:1.9}/* Standalone footer links are tap targets, not prose: inline-block lets the line-height
   apply so padding can carry each one to 44px. */
.footer-grid a{display:inline-block;padding-block:9px}.footer-grid nav{display:grid;gap:0}.footer-bottom a{display:inline-block;padding-block:14px}.footer-bottom{display:flex;justify-content:space-between;border-top:1px solid #ffffff25;margin-top:65px;padding-top:25px;color:#b9c5d9;font-size:12px}/* The heading is a .wrap, so the grid ends where the content column does and the cut
   edge reads as a box. The first layer feathers left and right back into the page;
   the second does the same downwards. Full-bleed bands below need only the vertical. */
.page-heading{padding:80px 0 65px;background-image:linear-gradient(90deg,#fff,transparent 7%,transparent 93%,#fff),linear-gradient(180deg,transparent 0 35%,#fff 92%),var(--drawing-grid)}.page-heading h1{max-width:1100px;margin-bottom:25px}.page-heading p{max-width:650px;font-size:18px;color:var(--muted)}.page-image{width:100%;height:520px;object-fit:cover}.filters{display:flex;flex-wrap:wrap;gap:9px;margin-bottom:35px}.filter{padding:12px 20px;border:1px solid var(--field);background:white;font-size:14px;min-height:44px}.filter.active{background:var(--blue);color:white;border-color:var(--blue)}.search{border:1px solid var(--field);padding:14px 17px;width:100%;margin-bottom:25px;max-width:450px;background:white}.project-count{font-size:13px;color:var(--muted);margin-bottom:30px}/* The index is served whole and trimmed by script; this is the button that reveals the
   rest, so it only ever exists where JavaScript ran. Centred under the grid, and the
   button gap is dropped because there is no arrow to push away from the label. */
.show-more{display:flex;justify-content:center;gap:0;margin:45px auto 0}
/* display:flex here outranks the browser's own [hidden]{display:none}, so the button
   kept showing "Show 0 more" once the whole list was on screen. The attribute is the
   state; this is what makes it mean anything. */
.show-more[hidden]{display:none}.detail-info{display:grid;grid-template-columns:1fr 2fr;gap:100px}.detail-info dl{margin:0}.detail-info dt{color:var(--muted);font-size:13px;margin-bottom:10px}.detail-info dd{margin:0 0 27px;font-size:19px}.gallery{display:grid;grid-template-columns:1fr 1fr;gap:25px}.gallery button{border:0;padding:0;background:var(--pale)}.gallery img{aspect-ratio:1.4;width:100%;object-fit:cover}.values{display:grid;grid-template-columns:repeat(3,1fr);gap:50px}/* Three frames of one job, read left to right. The captions carry the sequence rather
   than numbers, and the shared 1.5 crop is what makes them read as one job rather than
   three pictures; it is the same ratio the project cards use. */
.stages{display:grid;grid-template-columns:repeat(3,1fr);gap:28px}
.stages figure{margin:0}
.stages img{width:100%;aspect-ratio:1.5;object-fit:cover;background:var(--pale)}
.stages figcaption{padding-top:18px;color:var(--muted);font-size:14px;line-height:1.7}
.stages figcaption strong{display:block;color:var(--ink);font-size:18px;font-weight:550;margin-bottom:7px;letter-spacing:-.02em}
/* One stroke weight and one grid for every icon; size and colour come from the place
   each one sits, which is why none of them carry their own. */
.icon{width:24px;height:24px;flex-shrink:0}
.values article{padding-top:24px;border-top:2px solid var(--gold)}
.values .icon{display:block;width:30px;height:30px;color:var(--blue);margin-bottom:20px}.values p{color:var(--muted)}.contact-grid{display:grid;grid-template-columns:1fr 1.3fr;gap:12%}.contact-details{font-size:21px}.contact-details p{margin-bottom:30px}.contact-details small{display:flex;align-items:center;gap:9px;color:var(--muted);font-size:13px;margin-bottom:6px}
.contact-details .icon{width:17px;height:17px}.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:23px}label{display:block;font-size:14px}input,select,textarea{display:block;width:100%;padding:15px;border:1px solid var(--field);background:white;color:var(--ink);margin-top:9px;border-radius:0}textarea{resize:vertical;min-height:145px}.full{grid-column:1/-1}.form-note{font-size:12px;color:var(--muted);margin-top:18px}.form-status{padding:15px;background:var(--pale);margin-top:15px}.skip{position:fixed;top:-80px;left:15px;background:white;padding:20px;z-index:100}.skip:focus{top:10px}dialog{max-width:92vw;max-height:92vh;padding:0;border:0;background:#10254b}dialog::backdrop{background:#000c}dialog img{max-height:80vh;max-width:88vw;object-fit:contain}/* Controls sit on the navy dialog, so they follow the same treatment as the video
   toggle: white label, hairline border, no fill. The previous rule painted them white
   on white — it dated from a dialog that had a single button and no toolbar, which now
   handles the layout. */
dialog button{background:transparent;border:1px solid #ffffff70;color:white;padding:12px 22px;min-width:44px}
dialog button:hover{background:#ffffff20}.empty{padding:50px 0}.breadcrumb{font-size:13px;color:var(--muted);margin-bottom:35px}.policy{max-width:800px}.policy h2{font-size:30px;margin-top:45px}
.project-card img{height:auto}.project-placeholder{aspect-ratio:1.5;background:#e8edf5;padding:10%;display:flex;flex-direction:column;justify-content:space-between;color:var(--blue)}.project-placeholder span{font-size:13px}.project-placeholder h3{font-size:clamp(24px,3vw,45px);line-height:1.1;font-weight:500;letter-spacing:-.04em;margin:0}.project-placeholder small{font-size:12px}.project-placeholder.wide{aspect-ratio:auto;min-height:350px;padding:50px 5%;gap:25px}
@media(min-width:1600px){.hero-content{left:max(5%,calc((100% - 1440px)/2))}}@media(max-width:1000px){.nav{gap:18px}.nav a{font-size:13px}.intro{gap:7%}.facts{gap:35px}.feature-copy{padding:60px 9%}.detail-info{gap:45px}.contact-grid{gap:7%}.footer-grid{gap:30px}}@media(max-width:700px){header{height:86px}.brand img{width:124px;height:54px}.menu{display:block}.nav{display:none;position:absolute;top:86px;left:0;right:0;background:white;padding:20px 5% 30px;box-shadow:0 15px 25px #0002}.nav.open{display:flex;flex-direction:column;align-items:stretch;gap:8px}.nav a{font-size:17px}.hero{height:calc(90svh - 86px);min-height:570px}.hero-content{bottom:19%;right:5%}.hero-content h1{font-size:55px}.hero-content p{font-size:16px}.hero-bottom{font-size:11px;bottom:25px}.hero-bottom .location{display:none}.section{padding:65px 0}.intro,.contact-grid,.detail-info{grid-template-columns:1fr;gap:25px}.intro .body{padding:0}.facts{gap:50px;margin-top:30px}.fact strong{font-size:40px}.section-head{align-items:start}.section-head .text-link{gap:12px;white-space:nowrap}.section-head h2{font-size:35px}.project-grid{grid-template-columns:1fr;gap:32px}.project-meta h3{font-size:25px}.sector-row{grid-template-columns:auto 1fr 25px;padding:23px 0}.sector-row p{grid-column:2;grid-row:2}.sector-row .arrow{grid-column:3;grid-row:1}.sector-row h3{font-size:28px}.feature{grid-template-columns:1fr}.feature-media{min-height:350px}.feature-copy{padding:55px 6%}.cta{align-items:start;flex-direction:column}.footer-grid{grid-template-columns:1fr 1fr;gap:38px}.footer-grid>div:first-child{grid-column:1/-1}.footer-bottom{gap:20px;line-height:1.7;margin-top:40px}.page-heading{padding:55px 0 40px}.page-heading h1{font-size:49px}.page-image{height:330px}.values{grid-template-columns:1fr;gap:25px}.stages{grid-template-columns:1fr;gap:34px}.gallery{gap:13px}.form-grid{grid-template-columns:1fr}.full{grid-column:auto}.filter{padding:10px 14px}.contact-details{font-size:19px}}@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*,*:before,*:after{transition:none!important;animation:none!important}}

/* Cross-document view transitions. A project card's photograph grows into the
   project hero; every other navigation gets a plain cross-fade. Disabled wholesale
   for reduced motion, since ::view-transition animations run on pseudo-elements and
   are not covered by the global animation reset above. */
@media(prefers-reduced-motion:no-preference){@view-transition{navigation:auto}}
/* The hero is a photograph on most project pages and a video on a few; the card
   photograph morphs into whichever one leads the page. */
.project-heading + .page-image,.project-heading + .project-film{view-transition-name:project-hero}
::view-transition-group(project-hero){animation-duration:.45s;animation-timing-function:cubic-bezier(.22,1,.36,1)}
/* The card and the hero crop the same source differently, so both snapshots are made
   to fill the animating box and cover-crop. The browser's own cross-fade and its
   plus-lighter blending are left alone: without them both snapshots stay fully opaque
   at their own natural sizes and show up as two stacked rectangles. */
::view-transition-old(project-hero),::view-transition-new(project-hero){height:100%;object-fit:cover}
::view-transition-old(root),::view-transition-new(root){animation-duration:.3s}
/* Restores heading order on pages whose list items sit straight under the h1. */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}

/* Calls to action.
   The closing block keeps one filled button; the phone sits beside it as a text link so
   the hierarchy stays unambiguous while offering a second channel. */
.cta-actions{display:flex;flex-wrap:wrap;align-items:center;gap:18px 30px}
.cta-call{font-size:15px;color:var(--muted);border-bottom:1px solid var(--line);padding:12px 0;min-height:44px;display:inline-flex;align-items:center}
.cta-call:hover{color:var(--blue);border-bottom-color:var(--blue)}
.hero-link{color:white;border-bottom-color:#ffffff70;gap:14px}
.hero-link:hover{border-bottom-color:white}
.hero-content .button{margin-right:30px}
/* One call link, labelled for the room it has. On phones the navigation collapses behind
   the menu button, taking "Get in touch" with it, so this is the only way to make contact
   from the header and it earns a pill; the number itself would not fit beside the burger,
   so it shows as "Call". On desktop there is room for the number, and it reads as text so
   it does not compete with the "Get in touch" button it sits next to. The aria-label
   carries the full number in both states, so the short label is never what gets announced. */
.header-call{display:inline-flex;align-items:center;min-height:44px;margin-left:auto;margin-right:26px;font-size:14px;font-weight:550;white-space:nowrap}
.call-short{display:none}
/* Six nav items, the number and the button stop fitting on one line below about 1200px,
   and the labels start breaking mid-phrase ("Our / company"). The number is the one part
   with somewhere else to go — the footer and the contact page both carry it, and phones
   get the pill back below 700 — so it is what gives way in between. */
@media(min-width:701px) and (max-width:1200px){.header-call{display:none}}
@media(max-width:700px){
  .call-short{display:inline}
  .call-full{display:none}
  .header-call{display:inline-flex;align-items:center;min-height:44px;padding:0 16px;margin-left:auto;margin-right:6px;background:var(--blue);color:white;font-size:14px;font-weight:550}
  /* Both calls to action are inline-flex, so on a phone they end up on one line with the
     button's right margin removed — touching, no gap between two tap targets, and the
     margin-top below never applies because the link is still inline. Making the button
     block-level gives it its own line (and the full column, which is where a primary
     action wants to be at this width) and lets the link drop underneath as intended. */
  .hero-content .button{margin-right:0;display:flex}
  .hero-content .text-link.hero-link{margin-top:16px}
}

/* Two figures sit in a row; four would wrap 3+1 and leave one stranded, so they take
   an even two-by-two grid inside the narrower body column instead. */
.facts{flex-wrap:wrap}
.facts.grid-2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:34px 40px;max-width:420px}
.inline-link{border-bottom:1px solid var(--line);color:var(--blue)}
.inline-link:hover{border-bottom-color:var(--blue)}

/* Open roles. Each is a two-column row on desktop — title and terms on the left, the
   detail on the right — collapsing to one column on phones. */
.roles{border-top:1px solid var(--line)}
.role{border-bottom:1px solid var(--line);padding:34px 0;display:grid;grid-template-columns:1fr 1.6fr;gap:24px 60px;align-items:start}
.role h3{margin:0 0 10px;font-size:26px}
.role-meta{margin:0;font-size:14px;color:var(--muted)}
.role-body p{color:var(--muted);font-size:16px;margin-bottom:18px;max-width:62ch}
.role-body ul{margin:0 0 20px;padding-left:20px;color:var(--muted);font-size:15px}
.role-body li{margin-bottom:7px;line-height:1.6}
.role-body .text-link{gap:20px}
.checklist{margin:0 0 26px;padding-left:0;list-style:none;color:var(--muted);font-size:15px}
.checklist li{margin-bottom:8px;line-height:1.6;display:flex;gap:11px}
.checklist .icon{width:17px;height:17px;margin-top:4px;color:var(--blue)}
@media(max-width:820px){.role{grid-template-columns:1fr;gap:16px}}

/* ---- Header: fixed, with a compact scrolled state and a transparent state over the
   homepage hero. The hero is the only full-bleed media that sits directly beneath the
   header, so :has() scopes the transparent treatment to that one page without the
   template needing to know the route. ---- */
header{position:fixed;top:0;left:0;right:0;transition:height .25s ease,background-color .25s ease,box-shadow .25s ease}
header .brand img{transition:width .25s ease,height .25s ease,filter .2s ease}
body{padding-top:108px}
/* The hero runs full height under a transparent header rather than starting below it. */
body:has(.hero){padding-top:0}
body:has(.hero) .hero{height:100svh}
/* The hero's own scrim runs from the left and the bottom, leaving the top right — where
   the navigation sits — almost unshaded: white links measured 2.50:1 there. This band
   carries its own shade so the contrast does not depend on what the video is showing. */
body:has(.hero) header:not(.is-stuck){background-color:transparent}
/* The shade is its own layer so it can fade out while the white fades in underneath;
   swapping one background for the other flashed the video through for a frame. */
body:has(.hero) header::before{content:'';position:absolute;inset:0;z-index:-1;pointer-events:none;background:linear-gradient(180deg,rgba(5,19,39,.72),rgba(5,19,39,.38) 62%,transparent);transition:opacity .3s ease}
body:has(.hero) header.is-stuck::before{opacity:0}
/* No reversed logo asset exists, so the mark is knocked out to white over the video.
   The orange accent is lost in this state only; swap in a proper reversed file to keep it. */
body:has(.hero) header:not(.is-stuck) .brand img{filter:brightness(0) invert(1)}
body:has(.hero) header:not(.is-stuck) .nav > a{color:#fff}
body:has(.hero) header:not(.is-stuck) .nav a.contact-link{background:#fff;color:var(--blue)}
body:has(.hero) header:not(.is-stuck) .nav a.active{box-shadow:0 2px var(--orange)}
body:has(.hero) header:not(.is-stuck) .menu{color:#fff}
/* Over the hero the desktop number is plain white text like the rest of the nav; only the
   phone pill needs the knocked-out treatment, and it has to be restated after this rule
   because the breakpoint block above it is earlier in the file. */
body:has(.hero) header:not(.is-stuck) .header-call{color:#fff}
@media(max-width:700px){body:has(.hero) header:not(.is-stuck) .header-call{background:#fff;color:var(--blue)}}
/* Scrolled: solid, shorter, and edged so it reads as a band over white pages too. */
header.is-stuck{height:72px;border-bottom:1px solid var(--line);box-shadow:0 8px 24px #1826380f}
header.is-stuck .brand img{width:132px;height:57px}
@media(max-width:700px){
  body{padding-top:86px}
  body:has(.hero){padding-top:0}
  header.is-stuck{height:68px}
  header.is-stuck .brand img{width:108px;height:47px}
  header.is-stuck .nav{top:68px}
}

/* Projects with no photography still carry a client and a few hundred words of copy, so
   the card reads as a short profile rather than a gap in the grid. Same footprint as a
   photographed card, so rows stay level. */
.project-placeholder{justify-content:flex-start;gap:14px}
.project-placeholder span{color:var(--muted)}
.project-placeholder p{font-size:16px}/* With the heading in the caption below, the excerpt carries the panel, so it gets
   the room the title used to take. */
.project-placeholder{justify-content:center}
.project-placeholder p{margin:0;font-size:15px;line-height:1.6;color:var(--muted);display:-webkit-box;-webkit-line-clamp:5;-webkit-box-orient:vertical;overflow:hidden}
.project-placeholder small{margin-top:auto;color:var(--muted);padding-top:12px;border-top:1px solid #c9d4e6}
.project-card.wide .project-placeholder p{-webkit-line-clamp:3;max-width:64ch}
@media(max-width:700px){.project-placeholder p{-webkit-line-clamp:3;font-size:14px}}
/* ---- Orange accents. The logo's orange is the one warm colour on a navy-and-white site,
   so it is spent on marks that point somewhere — arrows, the tick that opens a section,
   the rule under a figure, the hover state of anything clickable — never on body text.
   Bright orange is 3.16:1 on white: enough for a graphic mark or a rule (3:1), not for
   words, so text that turns orange takes --orange-deep (4.70:1) on light grounds and
   --orange-light (6.02:1) on navy. */
:root{--orange-deep:#c94a0c;--orange-light:#ffa36b}
.label{display:flex;align-items:center;gap:12px}
.label::before{content:'';flex:none;width:18px;height:2px;background:var(--orange)}
.feature-copy .label{color:var(--orange-light)}
.fact strong::after{content:'';display:block;width:28px;height:2px;margin:12px 0 10px;background:var(--orange)}
.arrow{color:var(--orange)}
.button .arrow,.hero .arrow,.feature-copy .arrow{color:inherit}
.text-link:hover{border-bottom-color:var(--orange)}
.hero-link:hover,.feature-copy .text-link:hover{border-bottom-color:var(--orange)}
.sector-row:hover .icon{color:var(--orange)}
.cta-call:hover{border-bottom-color:var(--orange)}
.inline-link:hover{border-bottom-color:var(--orange)}
/* Filled buttons keep their navy; hover lays the orange along the bottom edge, the same
   2px rule the active tab carries, so "you can go here" reads the same everywhere. */
/* Filled calls to action carry the orange itself. White on the bright orange is 3.16:1,
   too little for a 14px label, so the fill is the deep shade (4.70:1) and hover goes
   darker still (6.25:1) rather than lighter. The white hero button stays white — it
   sits over video and needs its own contrast. */
:root{--orange-press:#a93d08}
.button:not(.light){background:var(--orange-deep);color:#fff;transition:background-color .15s}
.button:not(.light):hover{background:var(--orange-press)}
/* The header's "Get in touch" stays navy (white over the hero) so the nav reads as one
   quiet bar; hover lays the active tab's orange rule along its bottom edge. */
.nav .contact-link{transition:box-shadow .15s}
.nav .contact-link:hover{box-shadow:inset 0 -3px var(--orange)}
.fact strong{color:var(--orange-deep)}
.dot{background:var(--orange)}
footer{border-top-color:var(--orange)}
.values article{border-top-color:var(--orange)}
@media(prefers-reduced-motion:reduce){.button,.nav .contact-link{transition:none}}
.project-meta .arrow{color:var(--orange)}
/* ---- Motion. Every animation here moves only transform and opacity, which the browser
   hands to the compositor, so nothing is repainted or laid out per frame and the motion
   holds the display's full refresh rate. Two curves carry all of it: --out starts fast
   and settles long (things arriving), --io is symmetric (lines being drawn). Durations
   are short on purpose — the curve does the smoothing, not the length. Everything is
   gated on no-preference, so with reduced motion the page renders in its final state. */
:root{--out:cubic-bezier(.16,1,.3,1);--io:cubic-bezier(.76,0,.24,1)}
.hero-line{display:block;overflow:hidden;padding-bottom:.08em;margin-bottom:-.08em}
.hero-line>span{display:block}
@media(prefers-reduced-motion:no-preference){
  /* Page load over the hero: the headline rises out of its own line boxes, one line
     after the other, then the copy, the actions and the strip along the bottom follow. */
  @keyframes rise-line{from{transform:translate3d(0,105%,0)}}
  @keyframes rise-in{from{opacity:0;transform:translate3d(0,18px,0)}}
  @keyframes fade-in{from{opacity:0}}
  .hero-line>span{animation:rise-line 1.1s var(--out) both}
  .hero-line+.hero-line>span{animation-delay:.08s}
  .hero-content p{animation:rise-in .9s .3s var(--out) both}
  .hero-content .button,.hero-content .hero-link{animation:rise-in .9s .4s var(--out) both}
  .hero-content .hero-link{animation-delay:.46s}
  .hero-bottom{animation:fade-in .8s .6s ease-out both}

  /* The orange rules are drawn in, like a line being set out, the first time they come
     on screen. site.js adds .motion to the root and .is-drawn to each mark as it
     arrives; without script neither class exists and the rules simply sit there. */
  .motion .label::before,.motion .fact strong::after{transform:scaleX(0);transform-origin:left;transition:transform .8s var(--io)}
  .motion .is-drawn.label::before,.motion .fact.is-drawn strong::after{transform:none}
  .motion .fact+.fact strong::after{transition-delay:.1s}
  /* The values rule rides over the grey border as its own layer rather than replacing it. */
  .motion .values article{position:relative;border-top-color:var(--line)}
  .motion .values article::before{content:'';position:absolute;left:0;right:0;top:-2px;height:2px;background:var(--orange);transform:scaleX(0);transform-origin:left;transition:transform 1s var(--io)}
  .motion .values article.is-drawn::before{transform:none}
  .motion .values article:nth-of-type(3)::before{transition-delay:.08s}
  .motion .values article:nth-of-type(4)::before{transition-delay:.16s}

  /* Pointer answers. The arrow already points up and to the right, so it moves that way. */
  .arrow{display:inline-block;transition:transform .45s var(--out)}
  a:hover .arrow,button:hover .arrow,a:focus-visible .arrow{transform:translate3d(3px,-3px,0)}
  /* A sector row draws an orange rule along its bottom edge, in from the left and out to
     the right, so the rule always travels in the direction of reading. */
  .sector-row{position:relative;transition:color .25s}
  .sector-row::after{content:'';position:absolute;left:0;right:0;bottom:-1px;height:2px;background:var(--orange);transform:scaleX(0);transform-origin:right;transition:transform .6s var(--io)}
  .sector-row:hover::after,.sector-row:focus-visible::after{transform:scaleX(1);transform-origin:left}
  .sector-row .icon{transition:color .25s}
  .project-card img{transition:transform .9s var(--out)}
}

/* ---- Header motion. Height and colours still transition, but only on the fixed header,
   which is its own layer, so the page underneath never re-lays out. The logo shrinks by
   scale rather than by width, which kept it off the compositor. */
header{transition:height .35s var(--out),background-color .35s ease,box-shadow .35s ease,transform .5s var(--out)}
header .brand img,header.is-stuck .brand img{width:160px;height:69px;transform-origin:left center;transition:transform .35s var(--out),filter .25s ease}
header.is-stuck .brand img{transform:scale(.825)}
@media(max-width:700px){header .brand img,header.is-stuck .brand img{width:124px;height:54px}header.is-stuck .brand img{transform:scale(.871)}}
.nav>a{transition:color .25s ease}
.menu,.header-call{transition:color .25s ease,background-color .25s ease}
/* Hover draws the active tab's orange rule in from the left. The filled contact link has
   its own hover, and the active tab already carries the rule. */
.nav>a:not(.contact-link):not(.active){position:relative}
.nav>a:not(.contact-link):not(.active)::after{content:'';position:absolute;left:0;right:0;bottom:-2px;height:2px;background:var(--orange);transform:scaleX(0);transform-origin:right;transition:transform .45s var(--io)}
.nav>a:not(.contact-link):not(.active):hover::after,.nav>a:not(.contact-link):not(.active):focus-visible::after{transform:scaleX(1);transform-origin:left}
/* Scrolling down tucks the header away; any upward scroll brings it back. site.js only
   sets the class once the page is well past the hero and never while the menu is open. */
header.is-hidden{transform:translate3d(0,-100%,0);box-shadow:none}
@media(prefers-reduced-motion:no-preference){
  /* The header joins the hero's opening sequence on the homepage only; on every other
     page it is simply there, so moving between pages never replays it. */
  @keyframes drop-in{from{opacity:0;transform:translate3d(0,-12px,0)}}
  body:has(.hero) header .brand,body:has(.hero) header .header-call,body:has(.hero) header .menu,body:has(.hero) .nav>a{animation:drop-in .8s var(--out) both}
  body:has(.hero) header .brand{animation-delay:.05s}
  body:has(.hero) header .header-call{animation-delay:.12s}
  body:has(.hero) .nav>a:nth-child(1){animation-delay:.16s}
  body:has(.hero) .nav>a:nth-child(2){animation-delay:.2s}
  body:has(.hero) .nav>a:nth-child(3){animation-delay:.24s}
  body:has(.hero) .nav>a:nth-child(4){animation-delay:.28s}
  body:has(.hero) .nav>a:nth-child(5){animation-delay:.32s}
  body:has(.hero) .nav>a:nth-child(6){animation-delay:.36s}
  body:has(.hero) header .menu{animation-delay:.16s}
  /* Phone menu: the panel slides down from under the header and its links follow it. */
  @keyframes slide-down{from{opacity:0;transform:translate3d(0,-16px,0)}}
  @media(max-width:700px){
    .nav.open{animation:slide-down .45s var(--out)}
    .nav.open>a{animation:drop-in .55s var(--out) both}
    .nav.open>a:nth-child(2){animation-delay:.03s}
    .nav.open>a:nth-child(3){animation-delay:.06s}
    .nav.open>a:nth-child(4){animation-delay:.09s}
    .nav.open>a:nth-child(5){animation-delay:.12s}
    .nav.open>a:nth-child(6){animation-delay:.15s}
    .nav>a:not(.contact-link):not(.active)::after{display:none}
  }

  /* ---- Parallax. site.js writes one number per element, --p, running from -1 as it
     enters the bottom of the screen to 1 as it leaves the top, eased toward the scroll
     position every frame so it glides rather than steps. The `translate` property is
     separate from `transform`, so the hover zoom on a card and the scroll drift never
     fight over one value. Media is oversized just enough to cover its travel. */
  .motion .hero video,.motion .hero>.hero-image{translate:0 calc(var(--p,0) * 14%);scale:1.08;will-change:translate}
  .motion .hero-content{translate:0 calc(var(--p,0) * -60px);opacity:calc(1 - max(var(--p,0) - .3,0) * 1.8);will-change:translate,opacity}
  .motion .feature-media{overflow:hidden}
  .motion .feature-media video,.motion .feature-media img{top:-12%;height:124%;translate:0 calc(var(--p,0) * -9%);will-change:translate}
  .motion .project-card .photo img{scale:1.1;translate:0 calc(var(--p,0) * -4.5%);will-change:translate}
}

/* ---- Touch. Phones never hover, so the pointer answers above never show there. They
   get their own: whatever is pressed gives under the finger at once and springs back
   when released, and the sector rows draw their rule as they scroll into view instead
   of on hover. Scoped to hover:none so a mouse click on desktop does not shrink things. */
@media(hover:none) and (prefers-reduced-motion:no-preference){
  .button,.text-link,.header-call,.menu,.nav>a,.filter,.video-toggle{transition:transform .5s var(--out),background-color .25s ease,color .25s ease}
  .button:active,.text-link:active,.header-call:active,.menu:active,.nav>a:active,.filter:active,.video-toggle:active{transform:scale(.96);transition-duration:.08s}
  .project-card{display:block;transition:transform .5s var(--out)}
  .project-card:active{transform:scale(.98);transition-duration:.1s}
  .sector-row{transition:background-color .3s ease}
  .sector-row:active{background-color:var(--pale);transition-duration:.05s}
  /* The rule sweeps through rather than staying: in from the left, out to the right,
     the same path a hover takes on desktop. Left drawn, seven rows of orange read as
     decoration and drown the three places the colour is meant to point to. */
  @keyframes sweep{0%{transform:scaleX(0);transform-origin:left}50%{transform:scaleX(1);transform-origin:left}50.01%{transform:scaleX(1);transform-origin:right}100%{transform:scaleX(0);transform-origin:right}}
  .motion .sector-row.is-drawn::after{animation:sweep 1.3s var(--io) .1s}
  /* Rows that arrive together sweep one after another, not in unison. */
  .motion .sector-row:nth-child(2).is-drawn::after{animation-delay:0.18s}
  .motion .sector-row:nth-child(3).is-drawn::after{animation-delay:0.26s}
  .motion .sector-row:nth-child(4).is-drawn::after{animation-delay:0.34s}
  .motion .sector-row:nth-child(5).is-drawn::after{animation-delay:0.42s}
  .motion .sector-row:nth-child(6).is-drawn::after{animation-delay:0.50s}
  .motion .sector-row:nth-child(7).is-drawn::after{animation-delay:0.58s}
}


/* ---- Footer brand. `.footer-grid a` sets every footer link to 14px and outranks
   `.footer-brand`, so the wordmark rendered as a caption. Restated at the specificity
   it needs. */
.footer-grid .footer-brand{font-size:32px;color:#fff;line-height:1.1;padding-block:0}

/* ---- Phone homepage. The desktop composition squeezed into 390px left a few seams:
   a gap under the intro heading that was two spacings stacked, a section heading
   broken over three lines to make room for its link, a sector list over a thousand
   pixels tall, and a footer whose two columns never lined up. */
@media(max-width:700px){
  /* Intro: the grid gap and the heading's own margin were adding up to 55px. */
  .intro{gap:20px}
  .intro h2{margin-bottom:0}
  .facts{margin-top:36px}
  /* Section heads stack: the heading gets the full width, the link sits under it. */
  .section-head{flex-direction:column;align-items:stretch;gap:18px;margin-bottom:32px}
  .section-head .text-link{align-self:flex-start}
  /* Sector rows: the description runs under the name across to the arrow, so it takes
     one line instead of two, and the row closes up around it. */
  .sector-row{padding:18px 0;row-gap:4px;column-gap:16px}
  .sector-row h3{font-size:24px}
  .sector-row p{grid-column:2/4;font-size:13px;max-width:none}
  .sector-row .icon{width:20px;height:20px}
  /* Footer: the links become a two-column block and the contact details sit under it
     at full width, so nothing has to line up across columns of different rhythm. */
  .footer-grid{grid-template-columns:1fr;gap:28px}
  .footer-grid nav{grid-template-columns:1fr 1fr;column-gap:20px}
  .footer-grid nav a{padding-block:8px}
  .footer-grid>div:last-child{border-top:1px solid #ffffff25;padding-top:22px}
  .footer-grid>div:last-child p{margin:0 0 6px}
}

/* ---- Phone menu over the hero. The transparent-header rules paint the nav links white
   for the video behind them, and the open menu panel is white — so on the homepage the
   links vanished. With the menu open the header takes its solid state instead, the same
   one it has once scrolled, and everything in it reads dark on white. */
@media(max-width:700px){
  body:has(.hero) header:has(.nav.open){background-color:#fff}
  body:has(.hero) header:has(.nav.open)::before{opacity:0}
  body:has(.hero) header:has(.nav.open) .brand img{filter:none}
  body:has(.hero) header:has(.nav.open) .menu{color:var(--ink)}
  body:has(.hero) header:has(.nav.open) .header-call{background:var(--blue);color:#fff}
  body:has(.hero) header:has(.nav.open) .nav>a{color:var(--ink)}
  body:has(.hero) header:has(.nav.open) .nav>a.active{color:var(--blue)}
  body:has(.hero) header:has(.nav.open) .nav>a.contact-link{background:var(--blue);color:#fff}
}

/* ---- Project gallery on phones. Twelve thumbnails in two columns came to 800px of
   postage stamps. On a phone the gallery becomes a strip you swipe: each photograph
   nearly full width and large enough to see, the edge of the next one showing so the
   strip announces itself, snapping to rest on a whole picture. It bleeds to the screen
   edges and keeps the page gutter as its scroll padding, so the first and last photos
   still line up with the text. The meter under it says where you are. */
.gallery-meter{display:none}
@media(max-width:700px){
  .gallery{display:flex;gap:10px;overflow-x:auto;scroll-snap-type:x mandatory;margin-inline:-5vw;padding-inline:5vw;scroll-padding-inline:5vw;scrollbar-width:none;overscroll-behavior-x:contain}
  .gallery::-webkit-scrollbar{display:none}
  .gallery button{flex:0 0 84vw;scroll-snap-align:start}
  .gallery img{aspect-ratio:4/3;height:auto;display:block}
  .gallery-meter{display:flex;align-items:center;gap:16px;margin-top:18px;font-size:13px;color:var(--muted);font-variant-numeric:tabular-nums}
  .gallery-track{flex:1;height:2px;background:var(--line);position:relative;overflow:hidden}
  .gallery-thumb{position:absolute;inset:0;background:var(--orange);transform-origin:left;transform:scaleX(var(--f,0));will-change:transform}
}
/* The enlarged photograph slides in from the side it was swiped or stepped toward. */
@media(prefers-reduced-motion:no-preference){
  @keyframes from-right{from{opacity:0;transform:translate3d(40px,0,0)}}
  @keyframes from-left{from{opacity:0;transform:translate3d(-40px,0,0)}}
  dialog img.from-right{animation:from-right .45s var(--out)}
  dialog img.from-left{animation:from-left .45s var(--out)}
}
dialog img{touch-action:pan-y}
/* On a phone the enlarged photograph takes the whole screen, and the controls move to
   the bottom edge where a thumb already is; the photo fills everything above them. */
@media(max-width:700px){
  dialog[open]{width:100vw;height:100dvh;max-width:none;max-height:none;margin:0;display:flex;flex-direction:column}
  dialog .lightbox-toolbar{order:2;display:flex;align-items:center;gap:10px;padding:14px 5vw calc(14px + env(safe-area-inset-bottom))}
  dialog #image-count{flex:1;text-align:center;color:#fff;font-size:14px;font-variant-numeric:tabular-nums}
  dialog img{order:1;flex:1;min-height:0;width:100%;max-width:100%;max-height:none;object-fit:contain}
}

/* ---- Opening sequence, held for the fonts (see boot.js). Paused animations with a
   `both` fill sit on their first frame, so everything waits hidden rather than visible
   and then jumping back to start. */
.anim-wait .hero-line>span,.anim-wait .hero-content p,.anim-wait .hero-content .button,.anim-wait .hero-content .hero-link,.anim-wait .hero-bottom,.anim-wait header .brand,.anim-wait header .header-call,.anim-wait header .menu,.anim-wait .nav>a{animation-play-state:paused!important}

/* On phones the opening is softened. The desktop curve spends almost all of its travel in
   the first fifth of a second, which on a small screen reads as a snap rather than a
   rise. Here the curve is gentler, the lines travel less and fade up as they rise, and
   the steps are spaced a little further apart. */
@media(max-width:700px) and (prefers-reduced-motion:no-preference){
  @keyframes rise-line-soft{from{opacity:0;transform:translate3d(0,70%,0)}}
  @keyframes rise-in-soft{from{opacity:0;transform:translate3d(0,12px,0)}}
  @keyframes drop-in-soft{from{opacity:0;transform:translate3d(0,-6px,0)}}
  .hero-line>span{animation:rise-line-soft 1.4s cubic-bezier(.33,1,.68,1) both}
  .hero-line+.hero-line>span{animation-delay:.14s}
  .hero-content p{animation:rise-in-soft 1.2s .45s cubic-bezier(.33,1,.68,1) both}
  .hero-content .button,.hero-content .hero-link{animation:rise-in-soft 1.2s .6s cubic-bezier(.33,1,.68,1) both}
  .hero-content .hero-link{animation-delay:.7s}
  .hero-bottom{animation:fade-in 1.2s .9s ease-out both}
  body:has(.hero) header .brand,body:has(.hero) header .header-call,body:has(.hero) header .menu{animation:drop-in-soft 1.1s cubic-bezier(.33,1,.68,1) both}
  body:has(.hero) header .brand{animation-delay:.1s}
  body:has(.hero) header .header-call{animation-delay:.2s}
  body:has(.hero) header .menu{animation-delay:.28s}
}

/* ---- Reading. The site carries a lot of prose and all of it was set in the grey meant
   for captions (#647080, 5.0:1), which is fine for a label and tiring across six
   paragraphs. Running text now takes --text (8.3:1): clearly darker, still softer than
   the ink of the headings, so the page keeps its lightness. Grey stays where it means
   "secondary" — labels, meta lines, captions under figures. Lines are held to a
   readable measure, each run of prose opens with a lead paragraph that gives the eye
   somewhere to start, and wrapping avoids stranding one word on the last line. */
:root{--text:#434f60}
p,li,figcaption{text-wrap:pretty}
h1,h2,h3{text-wrap:balance}
.body p,.role-body p,.role-body ul,.project-description p,.values p,.page-heading p,.feature-copy p,.contact-grid p{color:var(--text)}
.feature-copy p{color:#dbe2ef}
/* full.css loads after this file and sets .project-description p itself, so these
   rules carry one class more. The measure is in em, not ch: DM Sans has wide figures,
   and 64ch came out near 750px — about 90 characters of text. 37em is ~65. */
.project-description p:not(.label){color:var(--text)}
.body p:not(.label),.project-description p:not(.label){max-width:37em}
.body p+p,.project-description p+p{margin-top:1.15em}
.body p.lead,.project-description p.lead,p.lead{font-size:20px;line-height:1.6;color:var(--ink)!important;letter-spacing:-.005em;max-width:32em}
.story-title{font-size:14px;color:var(--muted)!important;margin:0 0 14px;display:flex;align-items:center;gap:12px}
.story-title::before{content:'';width:18px;height:2px;background:var(--orange);flex:none}
/* On a long story the heading stays beside the text it introduces instead of scrolling
   away and leaving an empty column. Desktop only; phones stack the two. */
@media(min-width:701px){
  .intro>div:first-child{position:sticky;top:110px;align-self:start}
}
/* Open roles: the description reads at body size, and the duty list gets real markers —
   the orange rule used everywhere else — instead of grey dots that disappeared. */
.role-body p{font-size:17px;line-height:1.7}
.role-body ul{list-style:none;padding-left:0;font-size:16px}
.role-body li{position:relative;padding-left:24px;margin-bottom:9px}
.role-body li::before{content:'';position:absolute;left:0;top:.8em;width:12px;height:2px;background:var(--orange)}
.stages figcaption{color:var(--text);font-size:15px}
@media(max-width:700px){
  .body p.lead,.project-description p.lead,p.lead{font-size:18px}
  .role-body p{font-size:16px}
}

/* ---- Audit fixes. */
/* The header is fixed, so anything scrolled to — a focused link, the skip-link target, an
   in-page anchor — would otherwise land underneath it (WCAG 2.4.11). */
html{scroll-padding-top:120px}
@media(max-width:700px){html{scroll-padding-top:96px}}
/* Text on the no-photo project cards sat at 4.28:1 on their pale blue; --text is 7:1. */
.project-placeholder p,.project-placeholder span,.project-placeholder small{color:var(--text)}
/* The gallery images carry width/height attributes so the browser can reserve their box
   before they load; the height attribute would otherwise pin them at 643px. */
.gallery img{height:auto}

/* Legal pages: the footer carries both documents side by side, and the cookie table is
   a plain two-column list that reads the same on a phone. */
.footer-legal{display:flex;gap:24px;flex-wrap:wrap}
.policy-table{width:100%;border-collapse:collapse;margin:24px 0;font-size:16px}
.policy-table th,.policy-table td{text-align:left;padding:13px 0;border-bottom:1px solid var(--line);color:var(--text)}
.policy-table th{font-size:13px;font-weight:550;color:var(--muted);border-bottom-color:var(--ink)}
.policy-table td:last-child{width:30%;color:var(--ink)}
.policy p{color:var(--text);max-width:37em}
/* .policy capped the whole .wrap at 800px, and .wrap centres itself, so legal text sat
   in the middle of the page while its heading sat at the left gutter. The column keeps
   the page's left edge; only its contents are narrowed. */
.wrap.policy{max-width:1440px}
.policy>*{max-width:800px}
.policy-table td:last-child{padding-left:16px}

/* ---- Phone pass, second round. */
@media(max-width:700px){
  /* A project's facts (sector, location, client, contractor) stacked one per line took
     most of a screen before the description began; two columns halve that. */
  .detail-info dl{display:grid;grid-template-columns:1fr 1fr;column-gap:20px}
  .detail-info dd{margin-bottom:18px;font-size:17px;line-height:1.35}
  /* Nothing on a phone below 12px: the strip along the hero, the video toggle and the
     footer's group line were at 11. */
  .hero-bottom{font-size:12px}
  .video-toggle{font-size:13px}
  .footer-brand small{font-size:12px}
}
/* The copyright and the legal links sat on different lines' worth of padding. */
.footer-bottom{align-items:center}

/* ---- Phone pass, third round: the header pair, the filter row, and the vertical rhythm.
   Everything here is phone-only; the desktop layout is untouched. */

/* The menu button carried its icon as the text character ☰, which renders in whatever
   the font offers — a different weight and baseline from the word beside it. Three drawn
   bars match the label's colour and stroke, and fold into a cross while the nav is open
   so the button says what it will do next. */
.menu .burger{fill:currentColor;flex:none}
.menu .burger rect{transform-box:fill-box;transform-origin:center}
@media(prefers-reduced-motion:no-preference){
  .menu .burger rect{transition:transform .28s var(--out),opacity .18s ease}
}
header:has(.nav.open) .menu .burger rect:nth-child(1){transform:translateY(5.1px) rotate(45deg)}
header:has(.nav.open) .menu .burger rect:nth-child(2){opacity:0}
header:has(.nav.open) .menu .burger rect:nth-child(3){transform:translateY(-5.3px) rotate(-45deg)}

@media(max-width:700px){
  /* "Call" was a filled navy block next to plain text, so two taps of equal importance
     read as one button and one label. Same height, same type size, same square corner:
     filled for the call, outlined for the menu — primary and secondary of one family. */
  .menu{display:inline-flex;align-items:center;align-self:center;gap:10px;height:42px;padding:0 14px;margin-right:0;border:1px solid currentColor;font-size:14px;font-weight:550}
  .header-call{align-self:center;height:42px;min-height:0;margin-right:10px;padding:0 15px}
  /* On the hero the header sits on the photograph: the call inverts to white (rule
     above), so the menu's border has to follow it rather than stay navy. */
  body:has(.hero) header:not(.is-stuck) .menu{border-color:#ffffff8c}

  /* Eight sectors wrapped into ragged rows of 2, 2, 3 and 1. One row that scrolls keeps
     the shape of the page steady whatever the labels say; the mask fades the chips into
     the gutter so the row reads as continuing rather than cut. Negative margins let it
     bleed to the screen edge while the chips still start on the text column. */
  /* scroll-padding as well as padding: the padding lays the chips out inside the gutter,
     but a snap point aligns to the scrollport, so without it the first chip is dragged
     out to the screen edge and the gutter scrolled away. The fade is on the trailing
     edge only — the leading chip sits on the text column, and fading a chip that is
     already in place reads as a clipped one. */
  .filters{flex-wrap:nowrap;overflow-x:auto;gap:8px;margin-inline:-5vw;padding-inline:5vw;scroll-padding-inline:5vw;margin-bottom:24px;scroll-snap-type:x proximity;scrollbar-width:none;-webkit-overflow-scrolling:touch;-webkit-mask-image:linear-gradient(90deg,#000 calc(100% - 7vw),transparent);mask-image:linear-gradient(90deg,#000 calc(100% - 7vw),transparent)}
  .filters::-webkit-scrollbar{display:none}
  .filter{flex:none;white-space:nowrap;scroll-snap-align:start;padding:11px 16px}

  /* The gaps were the sum of two scales: a section's padding plus each block's own
     margin. One scale — 56 between sections, 14 under a label — closes the dead bands
     between the paragraph and the next thing to do. */
  .section{padding:56px 0}
  .page-heading{padding:44px 0 24px}
  .label{margin-bottom:14px}
  .facts{gap:36px}
  .search{margin-bottom:18px}
  .project-count{margin-bottom:22px}
  .section-head{margin-bottom:32px}

  /* The rule under a text link ran the full width of the flex row — text, a 35px gap,
     then the arrow — so it read as longer than the words it belonged to. */
  .text-link{gap:16px;width:fit-content}

  /* The arrow is set at 23px with line-height 1 and the title at 25px with leading, so
     aligning their boxes put the arrow above the title's cap line. */
  .project-meta .arrow{margin-top:3px}
}

/* The drawing grid is laid out for a desktop column: 240px squares with 48px minor
   lines. At 390px wide that shows one major line and a handful of minors, which read as
   stray marks rather than paper. Halving both keeps it a texture, and the vertical fade
   starts sooner so a short heading block does not end on a hard edge. */
@media(max-width:700px){
  :root{--drawing-grid:repeating-linear-gradient(90deg,#1826380d 0 1px,transparent 1px 120px),repeating-linear-gradient(0deg,#1826380d 0 1px,transparent 1px 120px),repeating-linear-gradient(90deg,#18263803 0 1px,transparent 1px 30px),repeating-linear-gradient(0deg,#18263803 0 1px,transparent 1px 30px)}
  .page-heading{background-image:linear-gradient(180deg,transparent 0 15%,#fff 88%),var(--drawing-grid)}
  .pale{background-image:linear-gradient(180deg,transparent 0 35%,var(--pale) 90%),var(--drawing-grid)}
}

/* ---- Phone pass, fourth round: the projects with no photograph, the top of the hero,
   and the form's own primary action. */
@media(max-width:700px){
  /* Forty-six of the seventy-nine projects have no photograph yet, and on one column
     they became a stack of flat slabs taller than the cards that do have one. Three
     changes, no new content: a shallower panel, the drawing grid so the panel reads as a
     sheet waiting for a photograph rather than a picture that failed to load, and the
     sector dropped from inside it — the caption underneath already says the sector and
     the town, so it was printed twice, two lines apart. */
  /* No fixed ratio on one column: a 1.5 box around three lines of text is mostly slack,
     and the client's name was pushed to the far edge of it with a dead band above. The
     panel takes the height of what it holds, which is also what makes a column of them
     look deliberate rather than like pictures that failed to load. */
  .project-placeholder{aspect-ratio:auto;padding:24px 26px;background-color:#e9eef6;background-image:var(--drawing-grid);justify-content:flex-start;gap:0}
  .project-placeholder span{display:none}
  .project-placeholder p{-webkit-line-clamp:3;font-size:14.5px;line-height:1.55}
  .project-placeholder small{margin-top:16px;padding-top:12px}

  /* The header sits on the photograph, and the logo landed on the busiest part of the
     frame — rooftops at full contrast. The hero's overlay only darkened the left edge
     and the bottom, so the top band got nothing. */
  .hero:after{background:linear-gradient(90deg,rgba(5,19,39,.58),rgba(5,19,39,.05)),linear-gradient(0deg,rgba(5,19,39,.5),transparent 65%),linear-gradient(180deg,rgba(5,19,39,.5),transparent 20%)}

  /* Every field in the form is full width; the button that submits them was not, so the
     column ended on a short orange block set against nothing. The hero's primary action
     already behaves this way on a phone. */
  #enquiry .button{display:flex;width:100%}

  /* The footer's own grid is drawn in white at 11% on navy, which reads heavier than the
     same grid does in ink on white — and the lines are the first thing the eye meets in
     a block that is otherwise text. */
  footer{background-image:linear-gradient(180deg,transparent 0 50%,#10254b 90%),repeating-linear-gradient(90deg,#ffffff0f 0 1px,transparent 1px 120px),repeating-linear-gradient(0deg,#ffffff0f 0 1px,transparent 1px 120px)}
}

/* ---- Phone pass, fifth round: what the eye meets first.
   Measured on a 390x844 screen: the hero filled it exactly, the two brightest solid
   whites were the primary action and the call button, the headline sat over footage that
   reaches luminance 221 in places (white houses, sky), and the only control in the
   bottom-right corner — where a vertical screen's gaze comes to rest — was the video
   toggle. Each rule below moves one of those. */
@media(max-width:700px){
  /* Two chunks of pure white on one screen split the hierarchy: the call sat at the top
     right, which is where the eye enters after the logo, and it read as loud as the
     action the screen is built around. Outlined, it keeps its 42px target and matches
     the video toggle's language, and the white slab is the only white block left. */
  body:has(.hero) header:not(.is-stuck) .header-call{background:transparent;color:#fff;border:1px solid #ffffff8c}

  /* Full screen on a phone, by the owner's call: the footage is the first thing the site
     says, and a strip of the next section under the fold was buying a scroll cue with a
     slice of the picture. svh, not lvh or dvh — svh is the screen with Safari's bars
     showing, so the hero fills what the eye actually sees and the strip along its bottom
     never ends up behind the floating toolbar. */
  body:has(.hero) .hero{height:100svh}

  /* A scrim tied to the text block rather than the whole frame. The overlay gradients
     darken the left edge and the bottom, but the headline's own band could still land on
     a white roof at about 1.4:1. Fading out well before its edges keeps it a shadow in
     the photograph and not a panel behind the words. */
  .hero-content:before{content:'';position:absolute;z-index:-1;inset:-20px -26px -12px -26px;background:radial-gradient(88% 120% at 12% 45%,rgba(5,19,39,.55),rgba(5,19,39,0) 74%),linear-gradient(90deg,rgba(5,19,39,.34),rgba(5,19,39,.18) 62%,rgba(5,19,39,0))}

  /* Both items in the strip move to the left, which leaves the bottom-right corner — the
     terminal fixation on a tall screen — free rather than spending it on a control that
     pauses a video. */
  /* Sampling eight moments of the footage showed why the scrim alone is not enough: it
     lowers the band's average luminance from 72-106 down to 54-80, but the blown-out
     spots — sky, a white gable — stay above 210, where white type has about 1.3:1
     against them. A soft shadow works where the problem is, at the glyph edge, and costs
     nothing when the frame behind is already dark. */
  .hero-content h1{text-shadow:0 1px 2px rgba(5,19,39,.34),0 6px 26px rgba(5,19,39,.44)}
  .hero-content p{text-shadow:0 1px 3px rgba(5,19,39,.4),0 4px 18px rgba(5,19,39,.5)}

  /* With the hero at full height its bottom edge is the bottom of the layout viewport,
     where Safari floats its toolbar over the page. The strip lifts clear of it, and of
     the home indicator on phones that report one. */
  .hero-bottom{justify-content:flex-start;gap:16px;bottom:calc(32px + env(safe-area-inset-bottom,0px))}

  /* The header retracts on the way down and comes back on the way up, so between the
     hero and the closing block — about four thousand pixels — there was no way to call
     without first scrolling up. For a builder that is the main thing a visitor does, so
     on a phone the header stays put. */
  header.is-hidden{transform:none}
  header.is-stuck.is-hidden{box-shadow:0 8px 24px #1826380f}
}

/* ---- Careers: the open roles as a list you can take in at a glance.
   Six vacancies written out in full ran to about 5,500px on a phone, so the page asked
   for ten screens of scrolling before it asked for an application. Each one is a native
   <details>: closed on arrival, open on a tap, and it works with scripts off, which the
   filter above it deliberately does not need to change. */
/* display on a class beats the browser's own [hidden] rule, so the filter's hidden roles
   would stay on the page. Spelling it out is the fix. */
.role{display:block;padding:0}
.role[hidden]{display:none}
/* Two rows, two columns: the title carries the plus on its line and the meta carries the
   chip on its own, so a location long enough to wrap cannot push the control onto a line
   of its own. */
.role summary{display:grid;grid-template-columns:1fr auto;grid-template-areas:"chip open" "title title" "meta meta";align-items:center;gap:6px 14px;padding:22px 0;cursor:pointer;list-style:none}
.role summary::-webkit-details-marker{display:none}
.role summary h3{grid-area:title;margin:0;font-size:27px}
.role summary .role-meta{grid-area:meta}
.role summary .role-chip{grid-area:chip;justify-self:start}
.role summary .role-open{grid-area:open;justify-self:end}
/* Office or site, in the same language as the filter above, so a scan of the list tells
   you which vacancies are yours without opening any of them. */
/* Sitting above the title, in the same voice as a section label, the kind of role reads
   as a heading's kicker rather than a badge competing with it — and the location line
   underneath gets the full width back. */
.role-chip{font-size:12px;letter-spacing:.07em;text-transform:uppercase;color:var(--muted);white-space:nowrap}
/* A plus that loses its upright when the role opens: drawn, like the rest of the icons,
   rather than a glyph whose weight depends on the font. */
.role-open{position:relative;width:15px;height:15px;flex:none;margin-left:auto;color:var(--blue)}
.role-open:before,.role-open:after{content:'';position:absolute;background:currentColor}
.role-open:before{left:0;right:0;top:6.7px;height:1.6px}
.role-open:after{top:0;bottom:0;left:6.7px;width:1.6px}
.role[open] .role-open:after{transform:scaleY(0)}
@media(prefers-reduced-motion:no-preference){.role-open:after{transition:transform .25s var(--out)}}
.role .role-body{padding:0 0 30px;max-width:66ch}
.role-apply{margin-top:6px}
.role-alt{font-size:14px;color:var(--muted);margin:14px 0 0}
.role-rate{font-weight:550;color:var(--blue);margin:0 0 18px}
.roles-note{margin:26px 0 0;font-size:15px;color:var(--muted)}
.role-filters{margin:26px 0 0}
.roles{margin-top:26px}

/* How hiring works: four steps, numbered, because the promise of the section is that
   there are no other steps hiding behind it. */
.hiring-steps{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(4,1fr);gap:40px 34px}
.hiring-steps li{border-top:2px solid var(--orange);padding-top:20px}
.hiring-steps .step-no{display:block;font-size:13px;color:var(--muted);margin-bottom:10px}
.hiring-steps h3{margin:0 0 10px;font-size:20px;line-height:1.25}
.hiring-steps p{margin:0;color:var(--text);font-size:16px;line-height:1.65}

@media(max-width:1000px){.hiring-steps{grid-template-columns:repeat(2,1fr);gap:34px 28px}}
@media(max-width:700px){
  .hiring-steps{grid-template-columns:1fr;gap:26px}
  .hiring-steps li{padding-top:16px}
  .role summary{padding:19px 0}
  /* The list is the reason for the page, so it starts as high as the heading allows. */
  #roles{padding-top:6px}
  .role summary h3{font-size:23px}
  .role .role-body{padding-bottom:26px}
  /* The primary action fills the column on a phone, as it does in the hero and the form. */
  .role-apply{display:flex;width:100%}
}

/* ---- The footage strip on a project page. Fionnan Mór is the reason it exists: the
   company supplied one photograph and forty-two drone clips, so the film is the project's
   only real record of itself. Six of the clips are cut to fifteen seconds. */
.film-strip{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px}
.clip{position:relative;aspect-ratio:16/9;background:#dce2eb;overflow:hidden}
.clip video{width:100%;height:100%;object-fit:cover;display:block}
/* The button covers the clip: the whole poster is the tap target, and the ring reads as
   a play affordance rather than a floating control. */
.clip-play{position:absolute;inset:0;width:100%;border:0;background:none;display:grid;place-items:center;cursor:pointer}
.clip-play:after{content:'';width:54px;height:54px;border-radius:50%;border:1px solid #ffffffb3;background:#12243866}
.clip-play:before{content:'';position:absolute;width:0;height:0;border-left:15px solid #fff;border-top:9px solid transparent;border-bottom:9px solid transparent;margin-left:4px;z-index:1}
.clip.playing .clip-play{display:none}
@media(prefers-reduced-motion:no-preference){.clip-play:after{transition:transform .35s var(--out),background-color .25s ease}.clip-play:hover:after{transform:scale(1.08);background:#122438a6}}
@media(max-width:1000px){.film-strip{grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}}
@media(max-width:700px){
  /* Two across at 393px leaves a 170px clip, too small to read: one column, full width. */
  .film-strip{grid-template-columns:1fr;gap:13px}
}

/* ---- Careers: the vacancies read as cards, not as a flat sheet.
   The page was white from the heading down through the whole list of roles — twelve
   hundred pixels of it — and the roles themselves were separated by hairlines, so the one
   part of the page with a job to do looked like an unbroken field. The list now sits on
   the tinted plate the site already uses for a section it wants to set apart, and each
   role is a white card on it. Nothing else about them changes: still <details>, still
   closed on arrival, still working with scripts off. */
#roles .roles{border-top:0;margin-top:22px;display:grid;gap:10px}
#roles .role{background:#fff;border-bottom:0;padding:0 24px}
#roles .role summary{padding:20px 0}
#roles .role .role-body{padding:0 0 26px;max-width:66ch}
@media(prefers-reduced-motion:no-preference){#roles .role{transition:box-shadow .3s var(--out)}}
/* The open role carries a little more weight than the closed ones around it. */
#roles .role[open]{box-shadow:0 10px 30px #1826380f}
#roles .roles-note{margin-top:22px}
@media(max-width:700px){
  #roles .role{padding:0 18px}
  #roles .role summary{padding:17px 0}
  #roles .roles{gap:8px}
}

/* ---- One rhythm for the whole site, not only the careers page.
   Measured plate by plate: /expertise ran 2,191px of unbroken white, a sector page 7,143px,
   /privacy 2,386px, and the closing block was white on every page. Text rows and form
   fields on white separated only by hairlines are what "everything blends" means, so the
   parts made of rows or fields move onto the tinted plate the site already uses, and the
   rows become white sheets on it. Photographs need no help and keep their white ground. */

/* The seven sectors, the same treatment the vacancies got. */
.sector-plate .sector-list{border-top:0;display:grid;gap:10px}
.sector-plate .sector-row{background:#fff;border-bottom:0;padding:26px 26px}
@media(prefers-reduced-motion:no-preference){.sector-plate .sector-row{transition:box-shadow .3s var(--out),color .25s ease}}
.sector-plate .sector-row:hover{box-shadow:0 10px 30px #1826380f}

/* The find-a-project panel is a set of controls, so it reads as one: tinted plate, white
   field and chips on it. The grid of photographs below keeps its white ground. */
.find-panel .search{background:#fff}
.find-panel .filter{background:#fff}
.find-panel .filter.active{background:var(--blue)}
.find-panel .project-count{margin-bottom:0}

/* The legal pages were the plainest walls of white on the site. The text is a sheet. */
.policy-plate{padding-top:0}
.policy-plate .wrap.policy{background:#fff;padding:54px 56px;max-width:1000px;margin:0 auto}
.policy-plate .wrap.policy>*{max-width:760px}
@media(max-width:700px){
  .policy-plate .wrap.policy{padding:30px 22px;width:90%}
  .sector-plate .sector-row{padding:20px 18px}
  .sector-plate .sector-list{gap:8px}
}

/* ---- Colour accents, in the palette that already exists.
   Taking stock first: the stat figures, the dashes before section labels, the rules over
   the hiring steps and the arrows on project cards were already orange. What had stayed
   grey were the three places a reader's eye goes when scanning — the sector icons, the
   kind of role on each vacancy, and the three ways to reach the company. Those move to
   orange; the navy icons in "How we build" stay navy, because two accents doing the same
   job in one section is noise rather than emphasis.
   Which orange matters: #f3641e is 3.16:1 on white, enough for a graphic but not for
   text, so anything with words in it takes --orange-deep at 4.73:1. */
.sector-row .icon{color:var(--orange)}
.sector-row:hover .icon,.sector-row:focus-visible .icon{color:var(--blue)}
.role-chip{color:var(--orange-deep)}
/* These two sit on the tinted plate rather than on a white card, and #f3641e is 2.89:1
   against it — under the 3:1 a graphic needs. The deeper orange is 4.4:1 there. */
.contact-details .icon{color:var(--orange-deep)}
/* The company timeline had a hairline the colour of the page; the hiring steps on the
   careers page use a 2px orange rule for the same idea, and now both read as one device.
   Its own rule lives in full.css, which loads after this file — changed there rather than
   fought with specificity from here. */
