/* ============================================================
   Redbridge Development Partners - an earlier version
   Design system: "plan set"
   Headline face : Archivo, width axis pushed to expanded (civic signage)
   Text face     : Source Serif 4 (institutional, sober, screen-tuned)
   Ground        : cured-concrete #E6E9E6 - not neutral grey, not cream
   One loud thing: the brand red. Everything else stays quiet.
   ============================================================ */

/* ------------------------------------------------------------------
   Fonts, self-hosted.

   They used to come from fonts.googleapis.com. That sent every visitor's
   IP address to Google for no benefit: modern browsers partition their
   cache per site, so the old "everyone already has it cached" argument
   stopped being true years ago.

   Both families are variable and licensed under the SIL Open Font
   License 1.1. The licence text sits beside the files in assets/fonts/,
   which the OFL requires.

   The italic cut of Source Serif was downloaded and then deleted: nothing
   on this page renders italic. The <em> tags in the coverage chart are
   set to font-style:normal and the <i> tags are empty rules. That saved
   127 KB. If italic copy is ever added, fetch it back from Google Fonts
   rather than letting the browser synthesise a slanted fake.

   font-display:swap shows the text immediately in a fallback face and
   repaints when the webfont lands. Never invisible text.
   ------------------------------------------------------------------ */

@font-face{
  font-family:'Archivo';
  src:url('assets/fonts/archivo-latin-var.woff2') format('woff2-variations');
  font-weight:400 800;
  font-stretch:62% 125%;
  font-style:normal;
  font-display:swap;
}

@font-face{
  font-family:'Source Serif 4';
  src:url('assets/fonts/source-serif-4-latin-var.woff2') format('woff2-variations');
  font-weight:400 700;
  font-style:normal;
  font-display:swap;
}


/* ------------------------------------------------------------------
   Skip link. Added an earlier version.

   Off-screen until focused, then it drops into the top-left corner. A
   keyboard user presses Tab once on arrival and can jump straight past
   the navigation. Hiding it with display:none would remove it from the
   tab order and defeat the point.
   ------------------------------------------------------------------ */
.skip-link{
  position:absolute;
  left:-9999px;
  top:0;
  z-index:200;
  background:var(--red);
  color:#fff;
  font-family:var(--disp);
  font-weight:600;
  font-size:.9rem;
  padding:12px 18px;
  text-decoration:none;
}
.skip-link:focus{left:0}

/* The skip link needs somewhere to land. <main> is not focusable by
   default, so in Safari and older Firefox the page scrolls but keyboard
   focus stays on the link - the next Tab returns you to the navigation
   you just skipped. tabindex="-1" makes it a focus target without
   putting it in the tab order. The outline is removed because a ring
   around the entire page is noise, not information; every real control
   inside keeps its own. */
main:focus{outline:none}

/* Text for screen readers only. Not display:none and not
   visibility:hidden - both of those remove it from the accessibility
   tree, which is the opposite of the point. */
.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;
}

:root{
  --slate:#16222C;
  --slate-2:#22323F;
  --slate-3:#3A4B55;
  --concrete:#E6E9E6;
  --concrete-2:#DCE0DB;
  --paper:#ffffff;
  --red:#E02117;
  /* Coverage-chart bars. Chosen by the client 2026-10-20 (option B of
     four). Two blues stepping darker, then red for the lead bar, so the
     colour itself says the service gets bigger each row. The blues are
     the same family as the tint on the partner logo strip. Both carry
     white 10.9px text at 4.7:1 and 9.1:1 - above the 4.5:1 minimum. Do
     not lighten them without re-checking that. */
  --bar-finance:#5A788D;
  --bar-advisory:#2F4C5C;
  --red-dp:#B0170F;
  --rule:#C2C9C3;
  --rule-lt:rgba(255,255,255,.30);
  --muted:#5A6663;
  --muted-lt:rgba(255,255,255,.72);

  --maxw:1240px;
  --pad:clamp(20px,5vw,72px);

  --disp:"Archivo","Archivo Expanded",system-ui,-apple-system,"Segoe UI",sans-serif;
  --text:"Source Serif 4",Charter,Georgia,"Times New Roman",serif;

  --ease:cubic-bezier(.22,.61,.36,1);
}

*,*::before,*::after{box-sizing:border-box}

html{scroll-behavior:smooth}

body{
  margin:0;
  background:var(--concrete);
  color:var(--slate);
  font-family:var(--text);
  font-size:17px;
  line-height:1.68;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}

img{max-width:100%;display:block}

::selection{background:var(--red);color:#fff}

a{color:inherit}

:focus-visible{
  outline:2px solid var(--red);
  outline-offset:3px;
}

/* ---------- type scale ---------- */

h1,h2,h3,h4{
  font-family:var(--disp);
  font-variation-settings:"wdth" 118;
  font-weight:700;
  margin:0;
  letter-spacing:-.025em;
  text-wrap:balance;
}

h1{
  font-size:clamp(2.5rem,6.2vw,5rem);
  line-height:.98;
  font-variation-settings:"wdth" 120;
  max-width:16ch;
}

h2{
  font-size:clamp(1.9rem,3.6vw,3rem);
  line-height:1.03;
  max-width:18ch;
}

h3{
  font-size:clamp(1.15rem,1.8vw,1.45rem);
  line-height:1.12;
  font-variation-settings:"wdth" 112;
  letter-spacing:-.018em;
}

p{margin:0}

/* Small technical label. Used only where it carries data, never as a
   decorative eyebrow sitting above a heading. */
.tech{
  font-family:var(--disp);
  font-variation-settings:"wdth" 104;
  font-weight:600;
  font-size:.72rem;
  letter-spacing:.045em;
  font-variant-numeric:tabular-nums;
  color:var(--muted);
  margin:0;
}

.prose{
  max-width:66ch;
  color:var(--slate-2);
}
.prose p + p{margin-top:1.05em}

.lede{
  font-size:clamp(1.1rem,1.65vw,1.3rem);
  line-height:1.55;
  max-width:56ch;
  color:var(--slate-2);
}

/* ---------- layout shells ---------- */

.wrap{
  max-width:var(--maxw);
  margin:0 auto;
  padding-left:var(--pad);
  padding-right:var(--pad);
  width:100%;
}

.section{padding:clamp(76px,10vw,132px) 0}
/* The header is fixed at 88px, so anchor jumps have to stop short of it or
   the section heading lands underneath. */
section[id]{scroll-margin-top:104px}
.section.is-tight{padding-top:0}

/* ---------- buttons and links ---------- */

.btn{
  display:inline-flex;
  align-items:center;
  justify-content:center;
  font-family:var(--disp);
  font-variation-settings:"wdth" 108;
  font-weight:600;
  font-size:.94rem;
  letter-spacing:-.005em;
  padding:16px 30px;
  border:1px solid var(--red);
  background:var(--red);
  color:#fff;
  text-decoration:none;
  border-radius:0;
  transition:background .18s var(--ease),border-color .18s var(--ease);
}
.btn:hover{background:var(--red-dp);border-color:var(--red-dp)}

.btn.is-sm{padding:10px 20px;font-size:.85rem}
.btn.is-lg{padding:19px 38px;font-size:1rem}

.btn.is-ghost{
  background:transparent;
  border-color:currentColor;
  color:inherit;
}
.btn.is-ghost:hover{background:rgba(255,255,255,.10);border-color:currentColor}

.tlink{
  display:inline-block;
  font-family:var(--disp);
  font-variation-settings:"wdth" 108;
  font-weight:600;
  font-size:.94rem;
  color:inherit;
  text-decoration:none;
  border-bottom:1.5px solid var(--red);
  padding-bottom:3px;
  transition:border-bottom-width .15s var(--ease);
}
.tlink:hover{border-bottom-width:4px}

.cta-row{
  display:flex;
  flex-wrap:wrap;
  align-items:center;
  gap:16px 30px;
  margin-top:clamp(22px,4vh,36px);
}

/* ---------- header ---------- */

.site-header{
  position:fixed;
  inset:0 0 auto;
  z-index:60;
  transition:background .3s var(--ease),border-color .3s var(--ease);
  border-bottom:1px solid transparent;
}
.site-header.scrolled{
  background:var(--concrete);
  border-bottom-color:var(--rule);
}

.bar{
  height:88px;
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:24px;
}

.brand{display:block;line-height:0}
.brand .logo{height:50px;width:auto}
.brand .logo-dark{display:none}
.site-header.scrolled .brand .logo-light{display:none}
.site-header.scrolled .brand .logo-dark{display:block}

.nav{
  display:flex;
  align-items:center;
  gap:clamp(18px,2.4vw,34px);
}
.nav a{
  font-family:var(--disp);
  font-variation-settings:"wdth" 104;
  font-weight:500;
  font-size:.92rem;
  color:#fff;
  text-decoration:none;
  padding:6px 0;
  border-bottom:1.5px solid transparent;
  transition:border-color .15s var(--ease);
}
.nav a:hover{border-bottom-color:var(--red)}
.site-header.scrolled .nav a{color:var(--slate)}

/* The CTA keeps its own colours and its own box in every header state.
   .nav a is more specific than .btn, so the button's border and colour have
   to be restated here or the pill loses its bottom edge. */
.site-header .nav a.header-cta,
.site-header.scrolled .nav a.header-cta{
  color:#fff;
  border:1px solid var(--red);
}
.site-header .nav a.header-cta:hover,
.site-header.scrolled .nav a.header-cta:hover{
  color:#fff;
  border-color:var(--red-dp);
}

.menu-toggle{
  display:none;
  background:none;
  border:1px solid var(--rule-lt);
  color:#fff;
  padding:9px;
  cursor:pointer;
  line-height:0;
}
.site-header.scrolled .menu-toggle{color:var(--slate);border-color:var(--rule)}

/* ---------- hero ---------- */

.hero{
  position:relative;
  min-height:min(94vh,940px);
  min-height:min(94svh,940px);
  display:flex;
  align-items:flex-end;
  overflow:hidden;
  background:var(--slate);
}
.hero-media{position:absolute;inset:0}
.hero-media img{
  width:100%;height:100%;
  object-fit:cover;
  object-position:22% 50%;
}

/* HERO DRIFT.
   This is the original animation from the NewCo preview, which the
   an earlier version redesign dropped and nobody noticed until it was noticed missing. Same numbers as the old file.

   It never scales below 1.06, so the 1.6% pan can never pull an edge
   of the photograph into frame.

   Gated on .anim, which the small script in <head> adds. No script, no
   movement, and the hero still looks right. */
.anim .hero-media img{
  animation:heroDrift 22s cubic-bezier(.45,0,.55,1) infinite;
  transform-origin:center 42%;
  will-change:transform;
}
@keyframes heroDrift{
  0%{transform:scale(1.06) translate(0,0)}
  50%{transform:scale(1.16) translate(-1.6%,-1.1%)}
  100%{transform:scale(1.06) translate(0,0)}
}
/* The building carries its own signage on the wall. The left side is pushed
   dark enough that the headline never competes with it. */
.hero-veil{
  position:absolute;inset:0;
  background:
    linear-gradient(90deg,rgba(9,15,20,.86) 0%,rgba(9,15,20,.74) 34%,rgba(9,15,20,.34) 68%,rgba(9,15,20,.06) 100%),
    linear-gradient(180deg,rgba(9,15,20,.58) 0%,rgba(9,15,20,.14) 32%,rgba(9,15,20,.34) 62%,rgba(9,15,20,.88) 100%);
}
/* Vertical rhythm is tied to viewport height as well as width, so the primary
   call to action stays above the fold on a short laptop screen. */
.hero-inner{
  position:relative;
  z-index:2;
  padding-top:clamp(118px,15vh,186px);
  padding-bottom:clamp(40px,6vh,88px);
  color:#fff;
}
.hero h1{
  color:#fff;
  font-size:clamp(2.3rem,min(6.2vw,8.4vh),5rem);
}
.hero .lede{color:var(--muted-lt);margin-top:clamp(16px,2.8vh,26px)}

/* The one drawn moment: a dimension line under the headline, measuring the
   three things Redbridge delivers under a single agreement. */
.span{margin-top:clamp(22px,4vh,40px);max-width:min(620px,100%)}
.span-note{
  font-family:var(--disp);
  font-variation-settings:"wdth" 104;
  font-weight:600;
  font-size:.74rem;
  letter-spacing:.045em;
  color:#fff;
  margin:0 0 13px;
}
.span-rule{
  position:relative;
  height:1px;
  background:rgba(255,255,255,.85);
  transform-origin:left center;
}
.span-rule i{
  position:absolute;
  top:-6px;
  width:1px;
  height:13px;
  background:rgba(255,255,255,.85);
}
.span-rule i:nth-child(1){left:0}
.span-rule i:nth-child(2){left:33.333%}
.span-rule i:nth-child(3){left:66.666%}
.span-rule i:nth-child(4){right:0}
.span-labels{
  list-style:none;
  margin:14px 0 0;
  padding:0;
  display:grid;
  grid-template-columns:repeat(3,1fr);
}
.span-labels li{
  font-family:var(--disp);
  font-variation-settings:"wdth" 112;
  font-weight:700;
  font-size:clamp(.95rem,1.5vw,1.15rem);
  letter-spacing:-.015em;
  color:#fff;
}

/* THE HIDDEN STATE IS GATED ON .anim, deliberately. an earlier version.

   These four elements - the "Full service, turn-key" note, the rule, its
   tick marks and the Design / Build / Finance labels - used to be hidden
   in their base rules and revealed only by the animations below. With no
   JavaScript the .anim class is never added, nothing animates, and the
   hero's centrepiece rendered as an empty 73px gap.

   That is the same fault the gallery reveal took five rounds to get
   right, sitting two sections above it. The rule is the same: never hide
   content in CSS and depend on script to un-hide it. Hidden is now
   something .anim opts into, and the animations put it back. */
.anim .span-note{opacity:0}
.anim .span-rule{transform:scaleX(0)}
.anim .span-rule i{opacity:0}
.anim .span-labels li{opacity:0}

.anim .span-note{animation:fadein .5s var(--ease) .35s forwards}
.anim .span-rule{animation:draw .85s var(--ease) .5s forwards}
.anim .span-rule i{animation:fadein .3s linear forwards}
.anim .span-rule i:nth-child(1){animation-delay:.55s}
.anim .span-rule i:nth-child(2){animation-delay:.85s}
.anim .span-rule i:nth-child(3){animation-delay:1.10s}
.anim .span-rule i:nth-child(4){animation-delay:1.32s}
.anim .span-labels li{animation:fadein .45s var(--ease) forwards}
.anim .span-labels li:nth-child(1){animation-delay:.90s}
.anim .span-labels li:nth-child(2){animation-delay:1.05s}
.anim .span-labels li:nth-child(3){animation-delay:1.20s}

@keyframes draw{to{transform:scaleX(1)}}
@keyframes fadein{to{opacity:1}}

.hero-credit{
  position:absolute;
  right:var(--pad);
  bottom:clamp(56px,7vw,88px);
  z-index:2;
  font-family:var(--disp);
  font-variation-settings:"wdth" 100;
  font-weight:500;
  font-size:.7rem;
  letter-spacing:.04em;
  line-height:1.5;
  color:rgba(255,255,255,.55);
  margin:0;
  text-align:right;
  max-width:22ch;
}

/* ---------- partners ---------- */

.partners{
  background:var(--paper);
  border-top:1px solid var(--rule);
  border-bottom:1px solid var(--rule);
  padding:34px 0;
}
.partners .wrap{display:block}
.partners .tech{margin-bottom:20px}

/* The partner strip scrolls. Pure CSS, no script.

   HOW THE SEAM WORKS. The track holds the logo set TWICE, end to end. The
   animation runs from translateX(-50%) to translateX(0), so at the moment
   it restarts, copy two is sitting exactly where copy one was, and the
   join never shows. The two copies must stay identical or the strip will
   visibly jump once per loop. The build writes both from one list, so
   they cannot drift by hand.

   The mask fades both edges, so logos enter and leave instead of being
   chopped off at the border.

   Each logo carries its own inline height, computed by the build from its
   aspect ratio. See _build_v2_5_0.py for why one flat height does not
   work. Do not add a height here - it would override all twelve. */
/* WCAG 2.2.2: anything that moves by itself for more than five seconds
   needs a way to stop it. Hover was not that - it does nothing for a
   keyboard or a touch screen, and it cannot be found. This is a real
   button. */
.partners-head{
  display:flex;
  align-items:baseline;
  justify-content:space-between;
  gap:16px;
  margin-bottom:22px;
}
.marquee-toggle{
  font-family:var(--disp);
  font-variation-settings:"wdth" 104;
  font-weight:600;
  font-size:.72rem;
  letter-spacing:.045em;
  color:var(--muted);
  background:none;
  border:1px solid var(--rule);
  border-radius:2px;
  padding:4px 11px;
  cursor:pointer;
  transition:color .2s,border-color .2s;
}
.marquee-toggle:hover{color:var(--slate);border-color:var(--slate-3)}
.marquee-toggle:focus-visible{
  outline:2px solid var(--red);
  outline-offset:2px;
}
.marquee.is-paused .marquee-track{animation-play-state:paused}

.marquee{
  position:relative;
  overflow:hidden;
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 6%,#000 94%,transparent);
  mask-image:linear-gradient(90deg,transparent,#000 6%,#000 94%,transparent);
}
.marquee-track{
  display:flex;
  align-items:center;
  /* NOT `gap`. Twenty-four logos have twenty-THREE gaps between them, so
     half the track came out 32px short of one repeat period and the
     strip jumped by that much every 48 seconds. Spacing on the items
     gives every logo its own trailing margin, twenty-four of them, so
     half the track is exactly one period and the seam lands true.
     The 820px query below must change with this value. */
  gap:0;
  width:max-content;
  animation:marquee-scroll 48s linear infinite;
}
.marquee:hover .marquee-track{animation-play-state:paused}
/* Logos are tinted rather than greyscale.

   WHY THE FILTER CHAIN LOOKS ODD. sepia() maps BRIGHTNESS to a gold
   ramp, so it treats the twelve logos very differently: the five box
   logos (CBRE, Corgan, McCarthy, Kutak Rock, First American) are mostly
   mid-to-dark and came out rich bronze, while the thin wordmarks
   (Peakstone, Corgan's lettering) went so pale they nearly vanished on
   the white band. The strip looked lopsided.

   contrast(.68) is the fix. Pulling both ends toward the middle brings
   the boxes down and the wordmarks up, so all twelve read as one tone.
   It is the single value doing the real work here - change it last and
   check the whole strip, not one logo.

   Hover still restores the true brand colours. That matters: a partner
   wall that only ever shows recoloured logos is a brand-guideline
   problem for firms the size of CBRE.

   SLATE BLUE. hue-rotate carries sepia's gold round to the hue of the
   brand slate #16222C, so the strip belongs to the same family as the
   header and footer rather than introducing a new colour. */
.marquee img{
  width:auto;
  margin-right:64px;
  max-width:200px;
  object-fit:contain;
  opacity:1;
  filter:grayscale(1) sepia(1) saturate(2.6) hue-rotate(183deg) brightness(.95) contrast(.68);
  transition:opacity .25s,filter .25s;
}
.marquee img:hover{opacity:1;filter:none}
@keyframes marquee-scroll{
  from{transform:translateX(-50%)}
  to{transform:translateX(0)}
}
/* REMOVED: .partner-row .wordmark and .hours.
   Nothing has used .wordmark since the marquee replaced the static
   partner row at an earlier version, and the hours block came off the page at
   an earlier version. Left here as a note so nobody
   wonders where they went.
   The rule below is the last of it and is being deleted. */
/* ---------- company ---------- */

.two-col{
  display:grid;
  grid-template-columns:minmax(0,1fr) minmax(0,1.15fr);
  gap:clamp(36px,6vw,90px);
  align-items:start;
}

/* Fact panel under the company prose. Reuses the .specs field list so the
   page has one way of showing structured facts, not two. */
.specs.is-facts{margin-top:46px}

/* ---------- coverage chart (replaces the four capability cards) ---------- */

.cover{margin-top:clamp(40px,5vw,66px)}

.cover-head{
  display:grid;
  grid-template-columns:minmax(220px,300px) minmax(0,1fr);
  gap:clamp(20px,3vw,44px);
  align-items:end;
  padding-bottom:12px;
}
.cover-phases{
  list-style:none;
  margin:0;padding:0;
  display:grid;
  grid-template-columns:repeat(5,1fr);
}
.cover-phases li{
  font-family:var(--disp);
  font-variation-settings:"wdth" 100;
  font-weight:600;
  font-size:.72rem;
  letter-spacing:.05em;
  color:var(--muted);
  padding-left:9px;
  border-left:1px solid var(--rule);
  line-height:1.25;
}

.cover-row{
  display:grid;
  grid-template-columns:minmax(220px,300px) minmax(0,1fr);
  gap:clamp(20px,3vw,44px);
  align-items:center;
  border-top:1px solid var(--rule);
  padding:26px 0;
}
.cover-row:last-child{border-bottom:1px solid var(--rule)}

.cover-name h3{margin:0}
.cover-name p{
  margin-top:10px;
  font-size:.94rem;
  line-height:1.5;
  color:var(--muted);
  max-width:38ch;
}

.cover-track{
  position:relative;
  display:grid;
  grid-template-columns:repeat(5,1fr);
  min-height:42px;
  align-items:center;
  /* the repeating gradient draws the five phase divisions; this closes the
     right-hand end so the chart reads as a bounded grid */
  border-right:1px solid var(--rule);
}
.cover-track::before{
  content:"";
  position:absolute;
  inset:0;
  pointer-events:none;
  background:repeating-linear-gradient(90deg,var(--rule) 0 1px,transparent 1px 20%);
}
.cover-bar{
  position:relative;
  grid-column:var(--a) / var(--b);
  height:26px;
  background:var(--slate-3);
  display:flex;
  align-items:center;
  padding-left:12px;
  overflow:hidden;
}
/* The three bars are named, not numbered, so re-ordering the rows cannot
   silently re-colour them. var(--slate-3) above stays as the fallback for
   any bar added later without a name. */
.cover-bar.is-finance{background:var(--bar-finance)}
.cover-bar.is-advisory{background:var(--bar-advisory)}
.cover-bar.is-lead{background:var(--red);height:38px}
.cover-bar em{
  font-family:var(--disp);
  font-variation-settings:"wdth" 102;
  font-style:normal;
  font-weight:600;
  font-size:.68rem;
  letter-spacing:.05em;
  color:#fff;
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}

/* ---------- our work ---------- */

.head-split{
  display:grid;
  grid-template-columns:minmax(0,1.25fr) minmax(0,1fr);
  gap:clamp(24px,5vw,72px);
  align-items:end;
}

.work-list{
  display:flex;
  flex-direction:column;
  gap:clamp(56px,8vw,104px);
  margin-top:clamp(44px,6vw,76px);
}
.work{
  display:grid;
  /* 1.5fr, not 1.2fr. The photograph renders about 516px and the text
     column about 344px, roughly 50 characters, which is still a
     comfortable measure. The next step up tested at 564px and squeezed
     the paragraphs to about 40 characters, which reads cramped.
     .is-alt below MUST mirror these tracks. */
  grid-template-columns:minmax(0,1.5fr) minmax(0,1fr);
  gap:clamp(28px,4.5vw,64px);
  align-items:center;
}
/* Alternating rows flip the photograph to the right. The columns are
   deliberately unequal - the photograph gets more room than the text -
   so the flip has to swap the COLUMN WIDTHS too, not just the order.
   Without the line below, every alternate photograph landed in the
   narrow column and rendered 391px against its neighbours' 469px. */
.work.is-alt{grid-template-columns:minmax(0,1fr) minmax(0,1.5fr)}
.work.is-alt .work-shot{order:2}
.work-shot{
  margin:0;
  /* position:relative is LOAD-BEARING, added an earlier version.
     The scan-reveal ::before is position:absolute. Without a positioned
     ancestor it resolves against the viewport and the red rule sweeps
     across the whole screen instead of across this photograph.
     overflow:hidden does NOT save you here - overflow does not create a
     containing block for absolutely positioned descendants. */
  position:relative;
  aspect-ratio:4/3;
  overflow:hidden;
  background:var(--slate);
}
.work-shot img{width:100%;height:100%;object-fit:cover}

/* GALLERY SCAN REVEAL. Rewritten an earlier version.

   A red rule sweeps across each photograph and the image appears behind
   it, borrowing the construction-drawing device the coverage chart uses.

   THE IMPORTANT PART: there is NO hidden base state. The photograph is
   visible by default and the clip lives ONLY inside the keyframes, with
   no fill-mode. So:

     animation runs      -> you see the sweep
     animation never runs-> the photograph is simply there

   It is impossible for a script failure, a throttled tab, a browser
   quirk or a timing race to leave a photograph hidden, because nothing
   outside the running animation ever hides it.

   Three earlier versions all hid the image with a base rule and relied
   on something later to un-hide it. Every one of them stranded the
   photographs at least once. Do not reintroduce a hidden base state. */
.work-shot.sweeping img{
  animation:scanClip 1.1s cubic-bezier(.76,0,.24,1);
}
@keyframes scanClip{
  from{clip-path:inset(0 100% 0 0)}
  to{clip-path:inset(0 0 0 0)}
}

.work-shot.sweeping::before{
  content:"";
  position:absolute;
  top:0;bottom:0;left:0;
  width:2px;
  background:var(--red);
  z-index:2;
  pointer-events:none;
  /* opacity:0 is the RESTING state and is load-bearing. There is no
     fill-mode on the animation, by design - see the note above about
     never letting a fill-mode persist a state. So whatever is written
     here is what shows before and after the sweep. Without this line
     the rule parks at left:0 and sits on the photograph permanently,
     which is exactly what shipped in an earlier version.
     The keyframes below set opacity:1 for the sweep itself. */
  opacity:0;
  animation:scanLine 1.1s cubic-bezier(.76,0,.24,1);
}
@keyframes scanLine{
  0%{left:0;opacity:1}
  99%{left:100%;opacity:1}
  100%{left:100%;opacity:0}
}

.work-copy h3{font-size:clamp(1.5rem,2.6vw,2.05rem);font-variation-settings:"wdth" 116}
.work-copy .body{
  margin-top:17px;
  color:var(--slate-2);
  max-width:46ch;
}

.specs{
  margin:28px 0 0;
  border-top:1px solid var(--rule);
}
.specs > div{
  display:grid;
  grid-template-columns:minmax(92px,26%) minmax(0,1fr);
  gap:16px;
  padding:11px 0;
  border-bottom:1px solid var(--rule);
}
.specs dt{
  font-family:var(--disp);
  font-variation-settings:"wdth" 100;
  font-weight:600;
  font-size:.72rem;
  letter-spacing:.05em;
  color:var(--muted);
  padding-top:4px;
}
.specs dd{margin:0;font-size:.96rem;line-height:1.45}

/* ---------- team roster ---------- */

.roster{
  list-style:none;
  margin:clamp(40px,5vw,66px) 0 0;
  padding:0;
  border-top:1px solid var(--rule);
}
.person{
  display:grid;
  grid-template-columns:132px minmax(0,1fr);
  gap:clamp(22px,3.5vw,52px);
  padding:30px 0;
  border-bottom:1px solid var(--rule);
  align-items:start;
}
.person-photo{
  aspect-ratio:1;
  overflow:hidden;
  background:var(--concrete-2);
  display:flex;
  align-items:center;
  justify-content:center;
}
.person-photo img{width:100%;height:100%;object-fit:cover}
.person-photo b{
  font-family:var(--disp);
  font-variation-settings:"wdth" 114;
  font-weight:700;
  font-size:1.9rem;
  letter-spacing:.02em;
  color:var(--muted);
}
.person-body h3{font-size:clamp(1.3rem,2.1vw,1.7rem);font-variation-settings:"wdth" 116}
.person-role{
  margin-top:8px;
  font-family:var(--disp);
  font-variation-settings:"wdth" 102;
  font-weight:600;
  font-size:.74rem;
  letter-spacing:.05em;
  color:var(--red-dp);
}
.person-bio{
  margin-top:15px;
  color:var(--slate-2);
  font-size:1rem;
  max-width:70ch;
}

/* ---------- contact ---------- */

.contact{
  background:var(--slate);
  color:#fff;
}
.contact h2{color:#fff;max-width:14ch}
.contact .lede{color:var(--muted-lt);margin-top:24px}
.contact .btn.is-ghost{color:#fff}
.mailto{
  margin-top:34px;
  font-size:.95rem;
  color:var(--muted-lt);
}
.mailto a{color:#fff;text-decoration-thickness:1px;text-underline-offset:4px}

/* ---------- footer ---------- */

footer{
  background:var(--slate-2);
  color:#fff;
  padding:clamp(48px,6vw,72px) 0 34px;
}
.foot-top{
  display:flex;
  justify-content:space-between;
  align-items:center;
  flex-wrap:wrap;
  gap:26px 44px;
  padding-bottom:30px;
  border-bottom:1px solid rgba(255,255,255,.16);
}
/* 42px before. The "Development Partners" lockup puts most
   of its height into the bridge mark, so the wordmark reads small.
   64px here because the footer has room for it. The header is deliberately still 50px - do
   not "fix" the difference, it is intended. */
.foot-top .logo{height:64px;width:auto}
.foot-nav{display:flex;flex-wrap:wrap;gap:14px 28px}
.foot-nav a{
  font-family:var(--disp);
  font-variation-settings:"wdth" 102;
  font-weight:500;
  font-size:.88rem;
  color:rgba(255,255,255,.78);
  text-decoration:none;
  border-bottom:1.5px solid transparent;
}
.foot-nav a:hover{color:#fff;border-bottom-color:var(--red)}
.foot-bottom{
  margin-top:26px;
  display:flex;
  justify-content:space-between;
  flex-wrap:wrap;
  gap:10px 40px;
  font-size:.8rem;
  color:rgba(255,255,255,.55);
}

/* ---------- responsive ---------- */

@media (max-width:1000px){
  .two-col,.head-split{grid-template-columns:minmax(0,1fr)}
  h1,h2{max-width:20ch}
}

@media (max-width:900px){
  .work,.work.is-alt{grid-template-columns:minmax(0,1fr)}
  .work.is-alt .work-shot{order:0}
  .cover-row{grid-template-columns:minmax(0,1fr);gap:18px;align-items:stretch}
  .cover-row.is-first{padding-bottom:48px}
  .cover-track{min-height:36px}
  /* The real phase header STAYS below 900px. It was hidden and replaced
     by a slash-separated string under row one only, in 9.6px type,
     spanning about 40% of a track whose divisions run the full width -
     so a reader could not tell where any bar actually stopped.
     .cover-head is already a five-column grid aligned to the tracks, so
     the honest fix is to keep it and shrink it. At 375px each column is
     about 67px and the longest label, Construction, measures under 55px
     at this size. */
  .cover-head{
    display:grid;
    /* The rows below collapse to ONE column at this width, so the header
       must too. Left as a two-column grid it kept the desktop name-column
       offset and the phase labels floated to the right of the bars they
       label. The empty spacer span is the name column; with one column it
       has nothing to space, so it goes. */
    grid-template-columns:minmax(0,1fr);
    gap:0;
    /* .cover-track carries a 1px right border; without the same border
       here the five header cells are 1px wider than the five track cells
       and the last label drifts. */
    border-right:1px solid transparent;
  }
  .cover-head>span{display:none}
  .cover-phases li{font-size:.56rem;letter-spacing:.01em}
}

@media (max-width:820px){
  .menu-toggle{display:block}
  .nav{
    position:fixed;
    inset:88px 0 auto;
    flex-direction:column;
    align-items:stretch;
    gap:0;
    background:var(--concrete);
    border-bottom:1px solid var(--rule);
    padding:8px var(--pad) 26px;
    display:none;
    /* In landscape on a phone the panel runs past the bottom of the
       screen - measured at 667x375, the Contact us button sat 14px below
       the fold. The panel is position:fixed, so page scrolling does not
       move it and those items could not be reached at all. */
    max-height:calc(100dvh - 88px);
    overflow-y:auto;
    overscroll-behavior:contain;
  }
  .nav.open{display:flex}
  .nav a{color:var(--slate);padding:15px 0;border-bottom:1px solid var(--rule)}
  .site-header .nav a.header-cta{margin-top:18px;padding:15px 20px;justify-content:center;border-bottom:0}
  .hero-inner{padding-top:150px}
  .hero-credit{display:none}
  .person{grid-template-columns:88px minmax(0,1fr);gap:20px}
  .marquee img{margin-right:44px}
}

@media (max-width:520px){
  .span-labels li{font-size:.9rem}
  /* The bar labels STAY on a phone. Hiding them left three unlabelled
     blocks and no way to tell what any of them meant - and one of them
     is "Concept to completion", the client's own phrase, deliberately
     put there to name the lead service. Shrink instead. */
  .cover-bar em{font-size:.62rem;letter-spacing:.02em;padding-right:8px}
  .foot-bottom{font-size:.75rem}
  .specs > div{grid-template-columns:minmax(0,1fr);gap:2px}
}

@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{
    animation-duration:.001ms !important;
    animation-delay:0ms !important;
    transition-duration:.001ms !important;
  }
  .span-rule{transform:scaleX(1)}
  .span-note,.span-rule i,.span-labels li{opacity:1}
  /* Hero sits still, gallery photographs are simply there. */
  .anim .hero-media img{animation:none !important;transform:none !important}
  /* Nothing to un-hide - the photographs are never hidden. Just stop
     the sweep and the rule. */
  .work-shot.sweeping img{animation:none !important}
  .work-shot.sweeping::before{display:none !important}
  .marquee{-webkit-mask-image:none;mask-image:none}
  /* Nothing is moving, so a pause button would be a lie. */
  .marquee-toggle{display:none}
  /* The duplicate set exists only to make the scroll loop seamless.
     With the animation off it wraps into a static block and every
     partner appears twice. */
  .marquee-track img[aria-hidden="true"]{display:none}
  .marquee-track{
    animation:none !important;
    transform:none !important;
    width:auto;
    flex-wrap:wrap;
    justify-content:center;
    gap:28px 48px;
  }
}

/* The review scaffolding is gone. Two devices lived here until
   2026-10-20: .needs, an inline flag for unconfirmed copy, and
   .is-empty, a placeholder for a photo slot with no approved image.
   Every flag was cleared and all three photographs are real, so both
   were dead weight. Nothing in the markup referenced either. */

