/* ══ WBJ Solutions — single committed light theme, no OS dark mode ══ */
:root{
  --white:#FFFFFF;
  --off:#FAF7F3;
  --ink:#111111;
  /* headings are near-black carrying a trace of the brand hue, so
     they read as black but feel warm rather than printer-default */
  --heading:#241A12;
  --charcoal:#252524;
  --grey:#707070;
  --grey-2:#9A9590;
  --line:#E7E3DD;
  /* ONE brand colour, pulled from the W. Three candidates, switchable
     on the swatch control bottom-right. Each defines four roles:
       --brand      fills, bars, panel rules
       --hl         a tint of the brand that passes AA with black over it
       --swipe      the highlighter. The brand itself, drawn at 45%
       --brand-deep links and white-text buttons (must pass AA on white)
       --brand-lift the same hue lifted, for the charcoal footer          */
  --brand:#D97D39; --hl:#D97D39; --brand-deep:#9E5419; --brand-lift:#E9A470;
  --accent:var(--brand-deep);
  --accent-soft:#FAEEE3;
  /* Errors do not follow the brand, and deliberately so. The old error
     colour was a burnt orange six degrees off the brand hue, which meant a
     form that had gone wrong and a form that had gone right were the same
     colour. This is a red, far enough round the wheel to be unmistakable
     and dark enough to pass AA on white at label size. */
  --bad:#B4342A;
  --bad-soft:#FCEDEA;

  --f:"Outfit",system-ui,-apple-system,"Segoe UI",sans-serif;

  --measure:1125px;
  --gutter:clamp(22px,10vw,150px);

  --h1:clamp(2.6rem,5.4vw,4.6rem);
  --h2:clamp(2.05rem,4.2vw,3.75rem);
  --h3:clamp(1.45rem,2.4vw,2rem);
  --h4:clamp(1.25rem,1.9vw,1.5rem);
}
*,*::before,*::after{box-sizing:border-box}
html{color-scheme:light}
body{
  margin:0; background:var(--white); color:var(--ink);
  font-family:var(--f); font-size:20px; font-weight:400; line-height:1.5;
  -webkit-font-smoothing:antialiased; overflow-x:hidden;
}
/* height:auto matters: the width/height attributes on each img act as
   presentational hints, so without this any image given a CSS width
   gets squashed to its attribute height. */
img{max-width:100%; height:auto; display:block}
a{color:inherit}
:focus-visible{outline:3px solid var(--accent); outline-offset:3px}
@media (prefers-reduced-motion: reduce){*{animation:none !important; transition:none !important}}

h1,h2,h3,h4{margin:0; color:var(--heading); font-weight:800; line-height:1.1; letter-spacing:-.02em; text-wrap:balance}
.sec--dark h1,.sec--dark h2,.sec--dark h3,.sec--dark h4{color:var(--white)}
h1{font-size:var(--h1); letter-spacing:-.034em; line-height:1.04}
h2{font-size:var(--h2); letter-spacing:-.028em}
h3{font-size:var(--h3)}
h4{font-size:var(--h4); font-weight:700}
p{margin:0}

.wrap{width:100%; max-width:calc(var(--measure) + var(--gutter)*2); margin-inline:auto; padding-inline:var(--gutter)}
.sec{padding-block:clamp(64px,9vw,130px); position:relative}
.sec--off{background:var(--off)}
.sec--dark{background:var(--charcoal); color:var(--white)}
.sec--dark .body{color:#C9C5C0}
.body{color:var(--grey); font-weight:400; max-width:32em}
.body p + p{margin-top:1em}
/* Links written as [label](url) inside a prose block. Underlined rather than
   colour alone, because colour on its own is not a link to somebody who
   cannot see the colour. */
.body a, .lede a{
  color:var(--brand-deep); text-decoration:underline;
  text-underline-offset:.16em; text-decoration-thickness:1.5px;
}
.body a:hover, .lede a:hover{text-decoration-thickness:2.5px}
.sec--dark .body a, .sec--dark .lede a{color:var(--brand-lift)}
.lede{font-size:clamp(1.05rem,1.5vw,1.375rem); color:var(--grey); line-height:1.5; max-width:28em}
.sec--dark .lede{color:#C9C5C0}

/* ── section edge ─────────────────────────────────────────
   Switchable on the Edge control, bottom-left. Default is
   none: sections simply meet on a colour change.            */
.zag{display:block; width:100%; height:clamp(26px,3.4vw,50px); line-height:0; margin-bottom:-1px}
.zag svg{display:block; width:100%; height:calc(100% + 1px)}
:root[data-edge="none"] .zag{height:0; overflow:hidden}

/* ── highlighter ──────────────────────────────────────────
   A translucent marker band drawn as a background on the text
   itself, so it wraps correctly line by line and the words sit
   over it. Tall enough to read as a highlight, low enough never
   to look like a strike-through.

   45% rather than 32%. At the lower figure it was pale enough to
   read as a wash behind the words rather than a mark drawn over
   them, which is a matter of strength and was for a while mistaken
   for a matter of hue. The colour is the brand's own: a highlighter
   in any other colour reads as a second system rather than as the
   brand marking its own words.                                  */
.hl{
  display:inline; padding-inline:.06em;
  background-image:linear-gradient(
    color-mix(in srgb, var(--swipe) 45%, transparent),
    color-mix(in srgb, var(--swipe) 45%, transparent));
  background-repeat:no-repeat;
  background-position:0 88%;
  background-size:0% .58em;
  -webkit-box-decoration-break:clone; box-decoration-break:clone;
  transition:background-size .7s cubic-bezier(.22,.75,.3,1);
}
.hl.on{background-size:100% .58em}

/* ── masthead ─────────────────────────────────────────── */
/* Floating header: fixed, no bar, no border. It hides on scroll
   down and returns compact on scroll up, and flips to white
   whenever it is sitting over a dark section.                  */
.mast{
  position:fixed; top:0; left:0; right:0; z-index:50; pointer-events:none;
  color:var(--heading); transition:transform .3s ease;
}
.mast--light{color:var(--white)}
.mast__in{display:flex; align-items:flex-start; justify-content:space-between; gap:20px; padding-block:34px; transition:padding .25s ease}
.mast--compact .mast__in{padding-block:14px}
/* Compact: the header leaves the measure and spreads to the page edges, so
   the logo and the nav sit in the margins rather than on top of the column
   of text. It cannot clear it entirely at every width, since the nav is
   wider than the margin is, but it takes the worst of the overlap away and
   on a wide screen it clears completely. */
.mast--compact .mast__in{
  max-width:none;
  padding-inline:clamp(18px,2.4vw,44px);
}
.brand{display:block; flex:0 0 auto; text-decoration:none; pointer-events:auto; transition:padding .25s ease, background .25s ease, box-shadow .25s ease}
.brand img{height:54px; width:auto; transition:height .25s ease}
.b-light,.b-mark{display:none}
.mast--light .b-dark{display:none}
.mast--light .b-light{display:block}
.navbar{display:flex; align-items:center; gap:30px; padding-top:10px; pointer-events:auto; transition:gap .25s ease}

/* Scrolled: each item gets its own white chip, so everything stays
   readable over any background without a full-width bar. Dark text
   returns, because the chips are always white.                     */
.mast--compact{color:var(--heading)}
.mast--compact .b-dark{display:block}
.mast--compact .b-light{display:none}
.mast--compact .brand,
.mast--compact .navbar a,
.mast--compact .hamb{
  /* background-color, not the shorthand: the nav links carry a background-image
     highlight and the shorthand would wipe it out */
  background-color:var(--white); border-radius:999px;
  box-shadow:0 2px 5px rgba(17,17,17,.06), 0 10px 26px -14px rgba(17,17,17,.5);
}
.mast--compact .brand{padding:9px 20px}
.mast--compact .brand img{height:34px}
.mast--compact .navbar{gap:9px; padding-top:0}
.mast--compact .navbar a{height:52px; display:flex; align-items:center; padding:0 22px; font-size:18px; background-position:22px 66%; background-size:0 .6em}
.mast--compact .navbar a:hover,
.mast--compact .navbar a[aria-current="page"]{background-size:calc(100% - 44px) .6em}
.mast--compact .hamb{width:52px; height:52px; padding:0 15px; justify-content:center; gap:5px}
.mast--compact .hamb i{height:2.5px}
@media (max-width:560px){
  .brand img{height:38px}
  .mast--compact .brand{padding:7px 14px}
  .mast--compact .brand img{height:28px}
  .mast--compact .hamb{width:44px; height:44px}
  .mast__in{padding-block:22px}
}
.navbar a{
  font-size:20px; font-weight:500; text-decoration:none; color:inherit; padding-inline:.12em;
  background-image:linear-gradient(
    color-mix(in srgb, var(--swipe) 45%, transparent),
    color-mix(in srgb, var(--swipe) 45%, transparent));
  background-repeat:no-repeat; background-position:0 86%; background-size:0% .6em;
  transition:background-size .25s ease;
}
.navbar a:hover,.navbar a[aria-current="page"]{background-size:100% .6em}
/* Four items plus the logo stop fitting a little under 900px, measured
   rather than guessed. Below that the links go and the menu button becomes
   the only way through; above it the button goes instead, because with all
   four links on the bar the overlay holds nothing the page does not already
   offer: home is the logo, and the town, phone and email are in the footer.
   A button that opens a list of things already on screen is furniture. */
@media (max-width:900px){.navbar a:not(.always){display:none}}
@media (min-width:901px){.navbar .hamb{display:none}}   /* .navbar .hamb, not .hamb: the base rule sets display:flex further
      down the file and would otherwise win on source order alone. */

/* Scrolled, the four links become the button, at every width.
   The reason the header spreads to the page edges once you scroll is to get
   it out of the way of the text. Four chips are about 700px of furniture
   floating over whatever is underneath, which undoes most of that; one chip
   is 52px and sits in the margin. The links are still all there unscrolled,
   so somebody arriving still sees the whole navigation without touching
   anything, and by the time it collapses they are reading rather than
   looking for the way around. The overlay it opens says more than the bare
   words did anyway: how many services, the phone number.
   Specificity, not source order, is what beats the two rules above. */
.mast--compact .navbar a:not(.always){display:none}
.mast--compact .navbar .hamb{display:flex}
.hamb{
  width:34px; height:26px; border:0; background:none; padding:0; cursor:pointer;
  display:flex; flex-direction:column; justify-content:space-between; align-items:flex-end;
  transition:width .25s ease, height .25s ease, background .25s ease, box-shadow .25s ease;
}
.hamb{color:inherit}
.hamb i{display:block; height:3px; background:currentColor; border-radius:2px; width:100%}

/* Four looks for the same button, chosen in Settings. Each keeps the 34px
   hit area, so the target never shrinks below what a thumb can find. */

/* three bars, stepped: the familiar hamburger */
.hamb--bars i:nth-child(2){width:70%}
.hamb--bars i:nth-child(3){width:88%}

/* two bars: quieter, and the default */
.hamb--duo{height:16px; justify-content:space-between}
.hamb--duo i{height:3px}
.hamb--duo i:nth-child(2){width:64%}

/* four dots, echoing the rounded shapes in the mark */
.hamb--dots{height:26px; display:grid; grid-template-columns:repeat(2,9px); gap:8px;
  justify-content:end; align-content:center; width:auto}
.hamb--dots i{width:9px; height:9px; border-radius:999px}

/* the word, with a rule under it */
.hamb--word{width:auto; height:auto; flex-direction:column; align-items:stretch; gap:5px}
.hamb__word{font:700 15px/1 var(--f); letter-spacing:.12em; text-transform:uppercase; color:currentColor}
.hamb--word i{height:2px}
.hamb--word i:nth-child(3){width:60%; align-self:flex-end}

/* full-screen menu overlay */
.overlay{position:fixed; inset:0; z-index:90; background:var(--white); display:flex; flex-direction:column; overflow:hidden}
.overlay[hidden]{display:none}
/* the W mark, oversized and muted: the real gradient, just
   turned right down so it reads as a tint rather than a logo */
.overlay::before{
  content:""; position:absolute; z-index:0; pointer-events:none;
  right:-10%; top:50%; translate:0 -50%;
  width:min(80vw,920px); aspect-ratio:2531/1459;
  /* Root-relative, not bare: a url() resolves against the stylesheet, so
     url(mark.webp) would look for it in /assets/css/ and silently find
     nothing. A missing background image throws no error anywhere. */
  background:url(/assets/img/mark.webp) no-repeat center/contain;
  opacity:.16; filter:saturate(.75);
}
.overlay__top,.overlay nav,.overlay__foot{position:relative; z-index:1}
.overlay__top{display:flex; justify-content:space-between; align-items:flex-start; padding:34px var(--gutter)}
.overlay__top img{height:54px}
.close{border:0; background:none; font-size:42px; line-height:1; cursor:pointer; color:var(--ink); font-family:var(--f); font-weight:300}
.overlay nav{flex:1; display:flex; flex-direction:column; justify-content:center; padding:0 var(--gutter) 6vh; gap:0}
.overlay nav a{
  font-size:clamp(2rem,5.6vw,3.6rem); font-weight:800; letter-spacing:-.03em; line-height:1.05;
  text-decoration:none; color:var(--heading);
  display:flex; align-items:baseline; justify-content:space-between; gap:24px;
  padding-block:clamp(10px,1.9vh,20px); border-bottom:1px solid var(--line);
}
.overlay nav a:first-child{border-top:1px solid var(--line)}
.overlay nav em{
  font-style:normal; font-size:17px; font-weight:500; letter-spacing:0; color:var(--grey-2);
  flex:0 0 auto; transition:color .2s;
}
.overlay nav a:hover em{color:var(--brand-deep)}
@media (max-width:620px){.overlay nav em{display:none}}
.overlay nav a .u{position:relative; display:inline-block}
.overlay nav a .u::before{content:""; position:absolute; left:-.05em; right:calc(100% + .05em); bottom:.09em; height:.55em; background:color-mix(in srgb, var(--swipe) 45%, transparent); transition:right .35s ease}
.overlay nav a:hover .u::before{right:-.05em}
.overlay nav a span{position:relative}
.overlay__foot{padding:0 var(--gutter) 34px; color:var(--grey); font-size:17px}

/* ── hero ─────────────────────────────────────────────── */
.hero{padding-top:clamp(150px,17vw,250px); padding-bottom:clamp(70px,10vw,150px)}
.hero--split{display:grid; grid-template-columns:minmax(0,1.22fr) minmax(0,.78fr); gap:clamp(24px,5vw,64px); align-items:center}
@media (max-width:900px){
  .hero--split{grid-template-columns:1fr; gap:34px}
  .hero--split .hero__art{max-width:240px}
}
.hero--split h1{font-size:clamp(2.2rem,4vw,3.5rem)}
.portrait img{max-width:none; width:100%}
.portrait.split__art img{max-width:390px}
@media (max-width:900px){.hero--split .portrait{max-width:340px}}

/* handwritten aside, pointing up at the portrait */
.hero__art .scribble{
  display:flex; align-items:flex-end; justify-content:flex-end; gap:10px;
  margin-top:12px; color:var(--brand-deep);
  text-transform:none; letter-spacing:0; font-weight:400;
}
.scribble span{
  font-family:"Caveat",ui-rounded,var(--f); font-size:clamp(1.3rem,1.8vw,1.7rem); font-weight:600;
  line-height:1.1; text-align:right; max-width:16ch; padding-bottom:2px;
  text-transform:none; letter-spacing:0;          /* the figcaption rules above set both */
}
.scribble svg{
  width:66px; height:54px; flex:0 0 auto; align-self:flex-end; margin-bottom:2px;
  fill:none; stroke:currentColor; stroke-width:2.6; stroke-linecap:round; stroke-linejoin:round;
  animation:nudge 3.4s ease-in-out infinite;
}
@keyframes nudge{
  0%, 55%, 100%{translate:0 0}
  78%{translate:-4px -5px}
}
@media (max-width:900px){.scribble{justify-content:flex-start}.scribble span{text-align:left}}
.hero__art img{width:100%; max-width:340px; margin-inline:auto}
.hero__art figcaption{text-align:center; margin-top:14px; font-size:15px; font-weight:700; letter-spacing:.11em; text-transform:uppercase; color:var(--grey-2)}

.hello{font-size:clamp(1.05rem,1.7vw,1.35rem); font-weight:600; color:var(--brand-deep); margin-bottom:16px}
.trust{margin-top:34px; font-size:16px; color:var(--grey-2); line-height:1.5; max-width:46ch}
.routes{display:flex; flex-wrap:wrap; gap:10px; margin-top:34px}
.route{
  display:inline-flex; align-items:center; gap:11px; padding:14px 22px; text-decoration:none;
  border:2px solid var(--line); border-radius:999px; background:var(--white);
  font-size:18px; font-weight:600; transition:border-color .18s, background .18s;
}
.route:hover{border-color:var(--brand); background:var(--accent-soft)}
.route svg{width:15px; height:15px; fill:none; stroke:currentColor; stroke-width:2.2; stroke-linecap:round; stroke-linejoin:round; opacity:.5}
.route:hover svg{opacity:1}
.hero h1{max-width:18ch}
.hero .l2{display:block; margin-top:.14em}
.hero__sub{margin-top:38px; max-width:30em; font-size:clamp(1.05rem,1.5vw,1.375rem); color:var(--grey)}
.hero__cta{margin-top:38px; display:flex; flex-wrap:wrap; gap:14px}

/* ── footnote marker and its note ─────────────────────────
   The marker is a link to the note, so with the script blocked it jumps to
   a note that is simply there. With the script running the note starts
   collapsed and the marker toggles it.                                    */
.fnmark{
  display:inline-block; vertical-align:super; line-height:1;
  font-size:.62em; font-weight:700; margin-left:.05em; padding:0 .18em;
  color:var(--brand-deep); text-decoration:none; border-radius:3px;
}
.fnmark:hover{background:var(--accent-soft)}
.fnote{
  display:flex; gap:.45em; margin-top:14px; max-width:48ch;
  font-size:15px; line-height:1.55; color:var(--grey-2);
}
.fnote[hidden]{display:none}
.fnote__star{flex:0 0 auto; color:var(--brand-deep); font-weight:700}
.fnmark[aria-expanded="true"]{background:var(--brand-deep); color:var(--white)}

/* The same note, hanging off the asterisk. The script moves it here; the
   block above is what it looks like with the script blocked. Charcoal
   rather than a pale card, because at this size a bubble on a white page
   needs to be clearly a layer over the page and not another paragraph
   of it. */
.fnote--pop{
  position:absolute; z-index:60; display:block; margin:0;
  max-width:min(330px, calc(100vw - 32px));
  background:var(--charcoal); color:rgba(255,255,255,.88);
  border-radius:10px; padding:14px 17px;
  font-size:15.5px; line-height:1.55;
  box-shadow:0 20px 44px -18px rgba(17,17,17,.6), 0 2px 6px rgba(17,17,17,.12);
  opacity:0; transform:translateY(-5px) scale(.97); transform-origin:var(--arrow,20px) 0;
  transition:opacity .16s ease, transform .16s cubic-bezier(.2,.8,.3,1);
}
.fnote--pop.is-open{opacity:1; transform:none}
.fnote--pop .fnote__star{display:none}
.fnote--pop a{color:var(--brand-lift)}
/* The pointer. A rotated square rather than a border triangle, so it takes
   the bubble's own radius and reads as part of it. */
.fnote--pop::before{
  content:""; position:absolute; top:-6px; left:var(--arrow,20px);
  width:14px; height:14px; margin-left:-7px;
  background:var(--charcoal); border-radius:3px; transform:rotate(45deg);
}
@media (prefers-reduced-motion:reduce){
  .fnote--pop{transition:none; opacity:1; transform:none}
}
/* The line under the closing pair of buttons, saying which is which. */
.ctahint{margin-top:20px; font-size:16px; color:var(--grey-2); line-height:1.55; max-width:56ch}

/* ── closing call to action ───────────────────────────────
   The one block on the site that is centred, and deliberately: everything
   else is a column of text with something beside it, while this is a short
   closing statement with two buttons under it and nothing to its right.
   Left-aligned it read as top-heavy, a full-width headline over a narrow
   huddle in the corner. Centred it reads as the end of the page.          */
.cta .wrap{max-width:calc(860px + var(--gutter)*2); text-align:center}
.cta .lede,
.cta .body,
.cta .ctahint{margin-inline:auto}
.cta .hero__cta{justify-content:center}

/* ── buttons ──────────────────────────────────────────── */
.btn{
  display:inline-flex; align-items:center; gap:11px; padding:16px 34px;
  font-family:var(--f); font-size:19px; font-weight:600; text-decoration:none;
  border:2px solid var(--ink); border-radius:999px; background:var(--ink); color:var(--white);
  cursor:pointer; transition:background .2s, color .2s, border-color .2s;
}
.btn:hover{background:var(--accent); border-color:var(--accent)}
.btn--line{background:none; color:var(--ink)}
.btn--line:hover{background:var(--ink); color:var(--white); border-color:var(--ink)}
.sec--dark .btn{background:var(--white); color:var(--ink); border-color:var(--white)}
.sec--dark .btn:hover{background:var(--accent); border-color:var(--accent); color:var(--white)}
.sec--dark .btn--line{background:none; color:var(--white)}
.btn svg{width:17px; height:17px; fill:none; stroke:currentColor; stroke-width:2.2; stroke-linecap:round; stroke-linejoin:round}

.tlink{display:inline-flex; align-items:center; gap:10px; font-weight:600; text-decoration:none; font-size:19px}
.tlink svg{width:16px; height:16px; fill:none; stroke:currentColor; stroke-width:2.2; stroke-linecap:round; stroke-linejoin:round; transition:transform .2s}
.tlink:hover{color:var(--accent)}
.tlink:hover svg{transform:translateX(5px)}

/* ── two-column section ───────────────────────────────── */
.split{display:grid; grid-template-columns:minmax(0,1fr) minmax(0,.82fr); gap:clamp(30px,6vw,90px); align-items:center}
/* Two pages want a wider first column and a top-aligned second one. These
   were inline styles on the element, which is how the contact page ended up
   two columns wide on a phone: an inline grid-template-columns beats the
   media query below, so the collapse never happened and the form ran
   underneath the contact details. Modifiers, so the media query can win. */
/* The case study. Top aligned, not centred: the facts sit beside a column
   of copy that can run to three times their height, and centring floats
   them in the middle of nothing. Sticky so they stay with the reader all
   the way down the brief rather than scrolling away at the first heading.
   100px clears the compact header chips with air to spare. */
.split--text{grid-template-columns:minmax(0,1.5fr) minmax(0,1fr); align-items:start}
.split--text > :last-child{position:sticky; top:100px}
.split--form{grid-template-columns:minmax(0,1.35fr) minmax(0,1fr); align-items:start}
@media (max-width:900px){
  .split,.split--text,.split--form{grid-template-columns:1fr; gap:40px}
  /* One column: nothing to stay beside, and a sticky block in a
     single column just sits on top of what follows it. */
  .split--text > :last-child{position:static}
}
.split__art img{width:100%; max-width:400px; margin-inline:auto}

/* Illustration with handwritten labels drawn over it. The wrapper carries
   the image's own max width so the overlay and the artwork share an edge. */
.annot{position:relative; width:100%; max-width:400px; margin-inline:auto}
.annot img{display:block}
.annot__ink{
  position:absolute; inset:0; width:100%; height:100%; pointer-events:none;
  fill:none; stroke:var(--brand-deep); stroke-width:5;
  stroke-linecap:round; stroke-linejoin:round;
}
.annot__word{
  fill:var(--brand-deep); stroke:none;
  font-family:"Caveat",ui-rounded,var(--f); font-size:52px; font-weight:600;
}

/* ── project list ─────────────────────────────────────────────────────
   Two treatments, not one. A featured project gets a band: its own
   coloured slab with the screenshot floating whole beside the words.
   Everything else gets a row: a thumbnail, a title and a line.

   The reason is that the old single treatment gave seven projects the
   same weight and the same height, which is a lot of scrolling in
   exchange for no signal about which ones are worth reading. The band
   also stops cropping the screenshots, which was the real cost of
   forcing every panel into one shape. */

.proj{display:grid; align-items:center}

/* ── featured: the band ──────────────────────────────────────────── */
.proj--band{
  grid-template-columns:minmax(0,1.25fr) minmax(0,1fr);
  gap:clamp(26px,3.6vw,56px);
  background:var(--charcoal); color:var(--white);
  border-radius:6px; padding:clamp(24px,3vw,44px);
  margin-top:clamp(18px,2.2vw,28px);
}
.proj--band:first-of-type{margin-top:clamp(30px,4vw,48px)}
.proj--band.proj--flip{grid-template-columns:minmax(0,1fr) minmax(0,1.25fr)}
.proj--band.proj--flip .proj__panel{order:2}
/* The picture floats rather than filling a frame. The panel keeps a fixed
   box so the bands stay a consistent height, and the screenshot is fitted
   inside it whole: a landscape one fills the width, a portrait one sits
   centred and smaller. Filling the box instead cut the sides off anything
   wide and the whole bottom off anything tall, which on a page whose job
   is showing the work is the wrong trade. The shadow moves to the picture,
   because it is the picture that is floating, not the box. */
.proj--band .proj__panel{
  aspect-ratio:auto; display:block; border-radius:0; background:transparent;
  padding:0; overflow:visible; box-shadow:none;
}
.proj--band .proj__panel::after{display:none}
.proj--band .proj__panel--shot::before{opacity:0}
/* The picture sets the height of its own half of the band, up to a ceiling,
   rather than being poured into a box of a fixed shape. A wide screenshot
   then runs the full width of the column and a tall one is as tall as the
   ceiling allows, which is a good deal bigger than either would be inside
   one shape that had to suit both. */
.proj--band .proj__panel--shot .proj__shot{
  position:static; inset:auto; width:100%; height:auto;
  max-height:min(48vh, 420px);
  object-fit:contain; object-position:center;
  border-radius:4px;
  filter:drop-shadow(0 24px 30px rgba(0,0,0,.55));
}
.proj--band .proj__body .proj__client{color:rgba(255,255,255,.6)}
.proj--band .proj__body h3{color:var(--white)}
.proj--band .proj__body p{color:rgba(255,255,255,.76)}
.proj--band .proj__tags span{background:rgba(255,255,255,.1); border-color:rgba(255,255,255,.22); color:#fff}
.proj--band .proj__body .tlink{color:#fff}

/* The band follows the site's own light or dark choice, set under
   Settings. Light keeps the same shape and swaps the ink: the screenshot
   still floats on its own ground rather than filling a frame. The modifier
   is on the band itself, so it applies wherever a band is drawn rather than
   only inside the projects page's list. */
.proj--band--light{background:var(--off); color:var(--ink); border:1px solid var(--line)}
.proj--band--light .proj__panel--shot .proj__shot{
  filter:drop-shadow(0 20px 26px rgba(17,17,17,.3));
}
.proj--band--light .proj__body .proj__client{color:var(--grey-2)}
.proj--band--light .proj__body h3{color:var(--heading)}
.proj--band--light .proj__body p{color:var(--grey)}
.proj--band--light .proj__tags span{background:var(--white); border-color:var(--line); color:var(--ink)}
.proj--band--light .proj__body .tlink{color:var(--brand-deep)}

/* ── the rest: index rows ────────────────────────────────────────── */
.proj--row{
  grid-template-columns:200px minmax(0,1fr);
  gap:clamp(20px,2.4vw,34px);
  padding-block:clamp(18px,2.2vw,26px);
  border-top:1px solid var(--line);
  margin-top:0;
}
/* Air between the last band and the first row, so the two groups read as
   two groups. */
.proj--band + .proj--row{margin-top:clamp(44px,6vw,78px)}
/* Same reasoning at thumbnail size. A full page grab shown whole in a
   200px slot is small, but small and complete beats a crop of its top bar. */
.proj--row .proj__panel{
  aspect-ratio:4/3; border-radius:0; padding:0;
  background:transparent; overflow:visible; box-shadow:none;
}
.proj--row .proj__panel::after{display:none}
.proj--row .proj__panel--shot::before{opacity:0}
.proj--row .proj__panel--shot .proj__shot{
  object-fit:contain; object-position:center;
  border-radius:4px;
  box-shadow:0 2px 5px rgba(17,17,17,.10), 0 14px 26px -16px rgba(17,17,17,.5);
}
.proj--row .proj__body{gap:6px}
.proj--row .proj__body h3{font-size:clamp(1.25rem,1.8vw,1.55rem)}
.proj--row .proj__tags{display:none}
.proj--row .proj__body p{font-size:16.5px; line-height:1.5}
.proj__client{font-size:14px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--grey-2)}
#worklist{border-bottom:1px solid var(--line)}

@media (max-width:860px){
  .proj--band,.proj--band.proj--flip{grid-template-columns:1fr; gap:22px}
  .proj--band.proj--flip .proj__panel{order:0}
}
@media (max-width:620px){
  /* Below this a 200px thumbnail leaves about nothing for the words. */
  .proj--row{grid-template-columns:1fr; gap:14px}
}
.proj__panel{
  aspect-ratio:16/9; border-radius:4px; background:var(--charcoal); color:var(--white);
  padding:clamp(22px,3vw,40px); display:flex; flex-direction:column; justify-content:space-between;
  position:relative; overflow:hidden; text-decoration:none;
}
/* z-index keeps the brand bar above the screenshot scrim, which is also a
   positioned layer on a screenshot panel. */
.proj__panel::after{content:""; position:absolute; left:0; right:0; bottom:0; height:5px; background:var(--brand); z-index:3}
/* A screenshot panel. The image is the background layer, the scrim sits over
   it, and the existing panel children stay where they are: nothing about the
   flex layout changes, so a project with a screenshot and one without still
   line up on the same row. */
.proj__panel--shot{background:var(--charcoal); color:var(--white); justify-content:flex-end}
.proj__panel--shot .proj__panel-top{display:block; margin-bottom:8px; opacity:.78}
/* Smaller than on a plain panel, deliberately. The body column beside it
   already carries the project title at full size, and a screenshot of a
   website has its own headline a third of the way down: a display sized
   name here lands on top of it and the two fight. Kept to one line, the
   label sits in the strong part of the scrim and the picture stays legible. */
.proj__panel--shot .proj__panel-name{font-size:clamp(1.15rem,1.7vw,1.45rem); font-weight:700; letter-spacing:-.02em}
/* The selector has to be at least as specific as the ".proj__panel--shot > *"
   rule below, or the image joins the flex flow as a full height item and
   pushes the name off the bottom of the panel. */
.proj__panel--shot .proj__shot{
  position:absolute; inset:0; width:100%; height:100%;
  object-fit:cover; object-position:top center; z-index:0;
}
/* Both the image and the scrim are positioned, so without these the image
   paints over the scrim simply by coming later in the markup. */
.proj__panel--shot::before{
  content:""; position:absolute; inset:0; z-index:1;
  background:linear-gradient(to bottom, rgba(17,20,24,0) 16%, rgba(17,20,24,.5) 38%, rgba(17,20,24,.9) 66%, rgba(17,20,24,.95) 100%);
}
.proj__panel--shot > *{position:relative; z-index:2}
.proj:nth-of-type(even) .proj__panel--shot .chip{background:rgba(255,255,255,.1); border-color:rgba(255,255,255,.2); color:#fff}
.proj__panel-chips{display:block; margin-top:16px}
/* The technologies come off the panel on a phone. The panel is 16:9, so at
   400px wide it is about 225 tall, and three pills plus the client name fill
   it edge to edge: the screenshot the panel exists to show ends up as a
   background behind a wall of labels. They are still on the project's own
   page, at full size, where there is room to read them. */
@media (max-width:700px){.proj__panel-chips{display:none}}
.proj__panel-top{font-size:15px; font-weight:600; letter-spacing:.1em; text-transform:uppercase; opacity:.62}
.proj__panel-name{font-size:clamp(1.5rem,3vw,2.3rem); font-weight:800; letter-spacing:-.03em; line-height:1.05}
.proj__body{display:flex; flex-direction:column; gap:16px; align-items:flex-start}
.proj__tags{display:flex; flex-wrap:wrap; gap:7px 10px; font-size:16px; font-weight:600}
.proj__tags span{border:1px solid var(--line); border-radius:999px; padding:4px 13px; line-height:1.25; color:var(--ink); background:var(--white)}
.proj__body p{color:var(--grey); font-size:19px; line-height:1.55}

/* ── tech chips ───────────────────────────────────────── */
.chips{display:flex; flex-wrap:wrap; gap:7px}
.chip{
  display:inline-flex; align-items:center; gap:8px; padding:5px 14px 5px 5px;
  border:1px solid var(--line); border-radius:999px; background:var(--white); color:var(--ink);
  font-size:15px; font-weight:500; line-height:1; white-space:nowrap;
}
.chip__g{
  width:24px; height:24px; border-radius:7px; display:grid; place-items:center; flex:0 0 auto;
  font-size:9.5px; font-weight:700; color:#fff; background:var(--tint,#888); letter-spacing:0;
}
.proj__panel .chip{background:rgba(255,255,255,.1); border-color:rgba(255,255,255,.2); color:#fff}
.chip--btn{cursor:pointer; font-family:var(--f); transition:border-color .18s, background .18s}
.chip--btn:hover{border-color:var(--ink)}
.chip--btn[aria-pressed="true"]{border-color:var(--accent); background:var(--accent-soft)}
.chip--lg{font-size:17px; padding:7px 17px 7px 7px}
.chip--lg .chip__g{width:29px; height:29px; font-size:11px; border-radius:8px}

/* ── quote band ───────────────────────────────────────── */
.quote{max-width:22ch}
.quote blockquote{margin:0; font-size:clamp(1.5rem,3.4vw,2.6rem); font-weight:700; letter-spacing:-.028em; line-height:1.2; max-width:20ch}
.quote__who{margin-top:30px; font-size:22px; font-weight:600}
.quote__role{font-size:19px; color:#B5B1AC}
.quotemark{font-size:clamp(4rem,9vw,7.5rem); line-height:.6; font-weight:800; color:var(--brand-lift)}

/* ── brand grid ───────────────────────────────────────── */
/* A wall of equal tiles. Every cell is the same height and every logo
   is fitted inside the same inner box, so a tall mark and a long
   wordmark sit at the same visual weight instead of one towering over
   the other. Two columns on a phone rather than one tall stack. */
/* The rules are drawn by each cell rather than by a background showing
   through the gaps. A background grid leaves a grey slab wherever the
   last row is short of a full set, which is most counts; a ring on each
   cell draws nothing where there is no cell, and neighbouring rings
   overlap across the 1px gap into a single line. */
.brands{display:grid; grid-template-columns:repeat(auto-fill,minmax(195px,1fr)); gap:1px;
  margin-top:clamp(30px,4vw,52px); padding:1px}
.brands div{background:var(--off); height:118px; box-sizing:border-box;
  box-shadow:0 0 0 1px var(--line);
  display:grid; place-items:center; padding:18px 22px; text-align:center;
  font-size:18px; font-weight:600; color:var(--grey); line-height:1.25}
.brands div>span{display:grid; place-items:center; width:100%; height:100%}
@media (max-width:640px){
  .brands{grid-template-columns:repeat(2,minmax(0,1fr))}
  .brands div{height:96px; padding:14px; font-size:15px}
}
/* The marks arrive one after another as the wall comes into view, once,
   and then sit still. The cells and their rules never move: a wall that is
   already built and fills up is a different thing from a wall that draws
   itself, and only the first is worth watching.
   is-staged is added by the script, so with it blocked every logo is simply
   there. --i is the cell's place in the order. */
.brands.is-staged div>span{opacity:0; transform:translateY(7px)}
.brands.is-staged.is-in div>span{
  opacity:1; transform:none;
  transition:opacity .55s ease, transform .55s cubic-bezier(.2,.8,.3,1);
  transition-delay:calc(var(--i,0) * 42ms);
}
@media (prefers-reduced-motion:reduce){
  .brands.is-staged div>span{opacity:1; transform:none}
}

/* ── service list ─────────────────────────────────────── */
/* ── service cards ────────────────────────────────────────
   Two columns, not an auto-fill track count. auto-fill always
   builds as many tracks as fit and then leaves the spare ones
   empty, so a group of two in a three-track grid left a hole on
   the right and squeezed the text to about 145px: every bullet
   wrapped after three words. Two columns also gives each card a
   readable measure. A group with an odd number of services ends
   with one card spanning the full width rather than an orphan
   sitting alone in a half-width column.                        */
.svcgrid{display:grid; grid-template-columns:repeat(2,minmax(0,1fr));
  gap:clamp(18px,2.2vw,26px); margin-top:clamp(26px,3vw,40px)}
@media (max-width:860px){.svcgrid{grid-template-columns:1fr}}

.svccard{
  border:1px solid var(--line); border-top:3px solid var(--brand); border-radius:5px;
  padding:clamp(24px,2.4vw,34px); background:var(--white);
  display:flex; flex-direction:column; gap:14px; position:relative;
  transition:border-color .2s ease, box-shadow .2s ease, transform .2s ease;
}
.svccard:hover{box-shadow:0 3px 10px rgba(17,17,17,.05), 0 18px 40px -28px rgba(17,17,17,.5);
  transform:translateY(-2px)}

/* Heading row: name on the left, the count sitting quietly to the
   right rather than stacked above it eating a line of its own. */
.svccard__top{display:flex; align-items:baseline; justify-content:space-between; gap:16px}
.svccard h3{font-size:clamp(1.15rem,1.5vw,1.35rem); letter-spacing:-.015em}
.svccard__no{font-size:13px; font-weight:700; letter-spacing:.14em; color:var(--grey-2);
  font-variant-numeric:tabular-nums; flex:0 0 auto}

.svccard p{color:var(--grey); font-size:17.5px; line-height:1.55; max-width:46ch}

/* A rule between the summary and the list, so the bullets read as
   "what this includes" rather than as more of the same paragraph. */
.svccard ul{margin:0; padding:16px 0 0 1.15em; border-top:1px solid var(--line);
  color:var(--grey); font-size:16.5px; line-height:1.45;
  display:flex; flex-direction:column; gap:9px}
.svccard li::marker{color:var(--accent)}

/* The odd one out goes full width. Its bullets then run in two
   columns, so a wide card does not become a long thin list. */
.svccard--wide{grid-column:1/-1}
@media (min-width:861px){
  .svccard--wide{display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1.1fr);
    column-gap:clamp(28px,3vw,48px); row-gap:12px; align-items:start}
  .svccard--wide .svccard__top{grid-column:1; align-items:baseline}
  .svccard--wide p{grid-column:1; margin:0}
  .svccard--wide ul{grid-column:2; grid-row:1/4; border-top:0; border-left:1px solid var(--line);
    padding:2px 0 2px calc(1.15em + 22px)}
}

/* ── group heading ────────────────────────────────────────
   A rule running to the right edge, so each stage reads as a
   band across the page rather than a label floating above a
   short row of cards.                                        */
.grouphead{display:flex; align-items:baseline; gap:18px; flex-wrap:wrap;
  margin-top:clamp(48px,6vw,86px)}
.grouphead:first-of-type{margin-top:0}
.grouphead h2{font-size:clamp(1.5rem,2.8vw,2.2rem); flex:0 0 auto}
.grouphead span{color:var(--grey); font-size:17px; flex:0 1 auto}
.grouphead::after{content:""; flex:1 1 60px; height:1px; background:var(--line);
  align-self:center; min-width:30px}

/* ── forms ────────────────────────────────────────────── */
.form{display:grid; gap:22px; max-width:620px; margin-top:clamp(28px,4vw,48px)}
.field{display:flex; flex-direction:column; gap:8px}
.field label{font-size:17px; font-weight:600}
.field input,.field select,.field textarea{
  font-family:var(--f); font-size:19px; color:var(--ink); background:var(--white);
  border:2px solid var(--line); border-radius:4px; padding:14px 16px; width:100%;
}
.field textarea{min-height:150px; resize:vertical}
.field input:focus,.field select:focus,.field textarea:focus{border-color:var(--accent); outline:none}
.row2{display:grid; grid-template-columns:1fr 1fr; gap:22px}
@media (max-width:620px){.row2{grid-template-columns:1fr}}
.consent{display:flex; gap:13px; align-items:flex-start; font-size:17px; color:var(--grey); line-height:1.45}
.consent input{margin-top:4px; width:20px; height:20px; accent-color:var(--accent); flex:0 0 auto}
.turnstile{border:2px dashed var(--line); border-radius:4px; padding:18px 20px; display:flex; align-items:center; gap:14px; font-size:15px; font-weight:600; letter-spacing:.08em; text-transform:uppercase; color:var(--grey-2); background:var(--off)}
.turnstile b{width:26px; height:26px; border-radius:7px; background:var(--brand); flex:0 0 auto}
.formnote{font-size:16px; color:var(--grey-2); line-height:1.5; max-width:46em}
.sent{border-left:5px solid var(--accent); background:var(--accent-soft); border-radius:4px; padding:20px 22px; font-size:18px; display:grid; gap:7px; line-height:1.5}
.sent b{font-size:20px}
.sent.bad{border-left-color:var(--bad); background:var(--bad-soft)}

.facts{display:flex; flex-direction:column; border-top:2px solid var(--line)}
.facts div{padding-block:18px; border-bottom:1px solid var(--line)}
.facts dt{font-size:15px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--grey-2); margin-bottom:5px}
.facts dd{margin:0; font-size:20px; font-weight:500}
.facts a{text-decoration:none}
/* Several links in one definition, so they wrap as a sentence rather than
   each taking a line of its own. */
.factlinks{display:block}
.factlinks a{font-weight:500}
.facts a:hover{color:var(--accent)}

/* ── legal ────────────────────────────────────────────── */
.legal{max-width:40em; color:var(--grey); display:flex; flex-direction:column; gap:1.1em; font-size:19px; margin-top:34px}
.legal h2,.legal h3{color:var(--ink); font-size:clamp(1.3rem,2.2vw,1.75rem); margin-top:.7em}
.legal ul{margin:0; padding-left:1.2em; display:flex; flex-direction:column; gap:7px}
.legal li::marker{color:var(--accent)}
.legal table{border-collapse:collapse; width:100%; font-size:17px; min-width:520px}
.legal th,.legal td{text-align:left; padding:12px 14px; border-bottom:1px solid var(--line); vertical-align:top; color:var(--grey)}
.legal th{color:var(--ink); font-weight:700; font-size:15px; letter-spacing:.08em; text-transform:uppercase}
.tablewrap{overflow-x:auto; max-width:100%}

/* ── footer ───────────────────────────────────────────── */
.foot{background:var(--charcoal); color:var(--white); padding-block:clamp(56px,7vw,92px) 30px}
.foot__cols{display:grid; grid-template-columns:minmax(0,1.25fr) repeat(3,minmax(0,1fr)); gap:clamp(28px,4vw,52px)}
@media (max-width:900px){.foot__cols{grid-template-columns:1fr 1fr}.foot__brand{grid-column:1 / -1}}
@media (max-width:520px){.foot__cols{grid-template-columns:1fr}}
.foot__brand img{height:58px; width:auto}
.foot__brand p{color:#B5B1AC; font-size:18px; margin-top:18px; max-width:28ch; line-height:1.5}
/* Named, not drawn. One row of plain links under the blurb, separated by the
   same middot the disclosure line uses, so it reads as part of the footer
   rather than as a widget bolted to it. */
.foot__social{
  list-style:none; margin:20px 0 0; padding:0;
  display:flex; flex-wrap:wrap; align-items:center; gap:0 14px;
  font-size:17px;
}
.foot__social li{display:flex; align-items:center; gap:14px}
.foot__social li + li::before{content:"·"; color:#6E6A65}
.foot__social a{
  color:#D6D2CD; text-decoration:none;
  padding-block:3px; border-bottom:1px solid transparent;
}
.foot__social a:hover{color:#fff; border-bottom-color:var(--brand-lift)}
/* credential strip: light band, because both supplied badges are dark artwork */
.credstrip{background:var(--off); border-top:1px solid var(--line)}
.credstrip__in{display:flex; align-items:center; justify-content:space-between; gap:24px; flex-wrap:wrap; padding-block:26px}
.credstrip img{display:block}
.cred-fsb{height:34px; width:auto}
.cred-green{height:48px; width:auto}
/* A circle reads smaller than a wordmark of the same height, so the disc is
   set a little taller than the FSB mark to sit as its equal rather than as a
   footnote beside it. */
.cred-pb{height:60px; width:auto}
/* The two marks about the business, kept together at one end of the row. */
.credpair{display:flex; align-items:center; gap:clamp(20px,3vw,38px); flex-wrap:wrap}
@media (max-width:560px){.credstrip__in{justify-content:flex-start; gap:20px}.credpair{gap:20px}.cred-fsb{height:28px}.cred-green{height:36px}.cred-pb{height:46px}}
.fcol h3{font-size:22px; font-weight:800; letter-spacing:.04em; margin-bottom:16px; color:var(--brand-lift); width:fit-content}
.fcol ul{list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:11px; font-size:18px}
.fcol a{color:#D6D2CD; text-decoration:none}
.fcol a:hover{color:#fff}
.foot__base{margin-top:clamp(40px,5vw,64px); padding-top:22px; border-top:1px solid rgba(255,255,255,.14); color:#A5A09B; font-size:13px; line-height:1.6}
.foot__base p{margin:0}

/* ── cookie bar ───────────────────────────────────────── */
.ck{position:fixed; left:16px; right:16px; bottom:16px; z-index:80; max-width:680px; margin-inline:auto; background:var(--white); border:2px solid var(--ink); border-radius:4px; padding:24px 26px; display:grid; gap:16px; box-shadow:0 20px 50px -20px rgba(0,0,0,.35)}
.ck[hidden]{display:none}
.ck h3{font-size:22px}
/* The illustration sits beside the words rather than above them, so the box
   does not get taller, and it is the first thing to go when the screen is
   narrow enough that the words need the full width. */
.ck__head{display:flex; gap:20px; align-items:flex-start}
.ck__head > div:first-child{flex:1 1 auto; min-width:0; display:grid; gap:14px}
.ck__art{flex:0 0 auto; width:132px; margin-top:2px}
.ck__art img{width:100%; height:auto}
@media (max-width:620px){.ck__art{display:none}}
.ck p{font-size:17px; color:var(--grey); line-height:1.5}
.ck__btns{display:flex; flex-wrap:wrap; gap:10px}
.ck .btn{padding:11px 22px; font-size:17px}
.ck__prefs{display:grid; gap:13px; border-top:1px solid var(--line); padding-top:16px}
/* display:grid beats the hidden attribute, so the detail was on show from
   the start and "What is set" appeared to do nothing. */
.ck__prefs[hidden]{display:none}
.pref{display:flex; gap:13px; align-items:flex-start; font-size:16px; color:var(--grey); line-height:1.45}
.pref input{margin-top:3px; width:19px; height:19px; accent-color:var(--accent); flex:0 0 auto}
.pref b{display:block; color:var(--ink); font-size:17px}
.ph{border-bottom:2px dotted var(--grey-2)}
.eyebrow{font-size:17px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:var(--grey-2); margin-bottom:18px}
.sec--dark .eyebrow{color:#8E8983}
.sr{position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0)}

/* ══ Phase 2 additions ══════════════════════════════════════════════ */
.skip{position:absolute;left:-9999px;top:0;z-index:200;background:var(--ink);color:#fff;
  padding:12px 20px;font-weight:600;border-radius:0 0 4px 0}
.skip:focus{left:0}

.artcap{text-align:center;font-size:16px;color:var(--grey-2);margin-top:14px;
  font-weight:600;letter-spacing:.1em;text-transform:uppercase}

.filterlab{font-size:16px;font-weight:700;letter-spacing:.13em;text-transform:uppercase;
  color:var(--grey-2);margin-bottom:16px}
.workempty{color:var(--grey);padding-block:40px}

/* Filters are links, not buttons, so the pressed state is aria-current. */
.chip--btn[aria-current="true"]{border-color:var(--ink);background:var(--ink);color:#fff}
.chip--btn[aria-current="true"] .chip__g{background:#fff;color:var(--ink)}
.chip__g--img{background:transparent!important;padding:0;display:grid;place-items:center}
.chip__g--img img{width:100%;height:100%;object-fit:contain}

.casehead{font-size:clamp(1.6rem,2.8vw,2.3rem)}
.casebody{margin-top:20px;font-size:20px}
.caselist{margin-top:20px;padding-left:1.15em;display:flex;flex-direction:column;gap:11px;font-size:20px}

.updated{font-size:16px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--grey-2)}
.asidenote{margin-top:18px;font-size:16px;color:var(--grey-2)}
.asidenote a{color:var(--accent)}

/* The other policies, beside a legal page. A list of four, so it is a list
   and not a panel: a rule down the left says "this is an aside" with less
   furniture than a box would, and the page you are on is stated rather than
   linked back to itself. */
.sidenav{padding-left:22px; border-left:2px solid var(--line)}
.sidenav__head{
  font-size:15px; font-weight:700; letter-spacing:.12em; text-transform:uppercase;
  color:var(--grey-2); margin:0 0 16px;
}
.sidenav ul{list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:12px}
.sidenav a{
  font-size:18px; font-weight:600; color:var(--ink); text-decoration:none;
  border-bottom:2px solid transparent; transition:border-color .18s, color .18s;
}
.sidenav a:hover{color:var(--accent); border-bottom-color:var(--accent)}
.sidenav__here{font-size:18px; font-weight:600; color:var(--grey-2)}
@media (max-width:900px){
  .sidenav{padding-left:0; padding-top:24px; border-left:0; border-top:2px solid var(--line)}
}

/* The uploaded artwork is already centred on a common canvas, so
   constraining the canvas is what makes every logo match. */
.brands img{max-width:100%; max-height:100%; width:auto; height:auto; object-fit:contain}

body.menu-open{overflow:hidden}

/* The compact masthead turns the button into a 52px white pill. Two of the
   four variants need to opt out of that fixed square: the word needs its
   natural width, and the dot grid needs its own centring. */
.mast--compact .hamb--word{width:auto; height:52px; padding:0 20px; gap:4px;
  flex-direction:column; justify-content:center; align-items:stretch}
.mast--compact .hamb--word .hamb__word{font-size:14px}
.mast--compact .hamb--dots{justify-content:center; align-content:center; gap:7px; padding:0}
.mast--compact .hamb--dots i{width:8px; height:8px}

/* ── projects filter ────────────────────────────────────────────────
   Two ways in. Service first, because it is the question most visitors
   arrive with; technology second, capped so twenty-two chips do not
   become a wall across three rows.                                    */
.filterrow + .filterrow{margin-top:clamp(20px,2.4vw,30px)}
.filterlab{font-size:14px; font-weight:700; letter-spacing:.14em; text-transform:uppercase;
  color:var(--grey-2); margin-bottom:12px}

/* A service chip is words, not a monogram: there is no logo to show. */
.chip--plain{padding-inline:18px}
.chip--extra{display:none}
.chips.is-open .chip--extra{display:inline-flex}

.chip--more{
  border:1px dashed var(--grey-2); background:transparent; color:var(--grey);
  font:inherit; font-size:16px; font-weight:600; cursor:pointer; padding-inline:18px;
}
.chip--more:hover{border-color:var(--ink); color:var(--ink)}

.filterclear{margin-top:clamp(18px,2vw,26px); font-size:16px; color:var(--grey)}
.filterclear strong{color:var(--ink)}
.filterclear a{color:var(--accent); margin-left:10px}

/* No JavaScript: nothing is hidden and the toggle is not offered. */
.no-js .chip--extra{display:inline-flex}
.no-js .chip--more{display:none}

/* ── menu opening ───────────────────────────────────────────────────
   The panel wipes up from the bottom and the items follow, each a beat
   behind the last, so the eye is led down the list rather than hit with
   all five at once. Closing plays the same move in reverse.

   [hidden] still means display:none, so the panel has to stay in the
   flow while it animates: site.js clears hidden first, then adds
   .is-open on the next frame, and on close waits for the transition
   before putting hidden back. */
.overlay{
  clip-path:inset(0 0 100% 0);
  transition:clip-path .42s cubic-bezier(.5,0,.2,1);
}
.overlay.is-open{clip-path:inset(0 0 0 0)}

.overlay__top{opacity:0; transform:translateY(-12px);
  transition:opacity .3s ease .12s, transform .3s ease .12s}
.overlay.is-open .overlay__top{opacity:1; transform:none}

.overlay nav a{
  opacity:0; transform:translateY(26px);
  transition:opacity .34s ease, transform .42s cubic-bezier(.22,.75,.3,1);
}
.overlay.is-open nav a{opacity:1; transform:none}
/* Staggered by hand rather than with a counter, because there are five
   fixed items and a nth-child list is clearer than a custom property. */
.overlay.is-open nav a:nth-child(1){transition-delay:.13s}
.overlay.is-open nav a:nth-child(2){transition-delay:.19s}
.overlay.is-open nav a:nth-child(3){transition-delay:.25s}
.overlay.is-open nav a:nth-child(4){transition-delay:.31s}
.overlay.is-open nav a:nth-child(5){transition-delay:.37s}
.overlay.is-open nav a:nth-child(6){transition-delay:.43s}

.overlay__foot{opacity:0; transition:opacity .3s ease .4s}
.overlay.is-open .overlay__foot{opacity:1}

/* The W mark drifts in behind everything, a touch slower than the wipe. */
.overlay::before{opacity:0; transform:translateX(40px) scale(.97);
  transition:opacity .7s ease .1s, transform .9s cubic-bezier(.22,.75,.3,1) .1s}
.overlay.is-open::before{opacity:.16; transform:none}

/* The hamburger becomes a cross while the panel is open. */
.hamb i{transition:transform .32s cubic-bezier(.5,0,.2,1), opacity .2s ease, width .32s ease}
.hamb[aria-expanded="true"] i:nth-child(1){transform:translateY(6px) rotate(45deg); width:100%}
.hamb[aria-expanded="true"] i:nth-child(2){transform:translateY(-6px) rotate(-45deg); width:100%}
.hamb--bars[aria-expanded="true"] i:nth-child(1){transform:translateY(9px) rotate(45deg)}
.hamb--bars[aria-expanded="true"] i:nth-child(2){opacity:0}
.hamb--bars[aria-expanded="true"] i:nth-child(3){transform:translateY(-9px) rotate(-45deg); width:100%}
.hamb--dots[aria-expanded="true"] i{transform:scale(.55)}
.hamb--word[aria-expanded="true"] i{opacity:0}

@media (prefers-reduced-motion: reduce){
  .overlay,.overlay__top,.overlay nav a,.overlay__foot,.overlay::before,.hamb i{transition:none}
  .overlay{clip-path:none}
}

/* ── menu button, hover ─────────────────────────────────────────────
   Each variant gets a move that suits its own shape rather than one
   generic wiggle applied to everything. All of it is transform and
   opacity only, so nothing reflows and nothing jumps under the cursor.
   Suppressed while the panel is open, where the bars are busy being
   a cross.                                                           */
.hamb:not([aria-expanded="true"]):hover i,
.hamb:not([aria-expanded="true"]):focus-visible i{transition:transform .28s cubic-bezier(.22,.75,.3,1), width .28s ease}

/* two and three bars: the short bar runs out to full width, so the
   stack squares up as you approach it */
.hamb--duo:not([aria-expanded="true"]):hover i:nth-child(2),
.hamb--duo:not([aria-expanded="true"]):focus-visible i:nth-child(2){width:100%}
.hamb--bars:not([aria-expanded="true"]):hover i:nth-child(2),
.hamb--bars:not([aria-expanded="true"]):hover i:nth-child(3),
.hamb--bars:not([aria-expanded="true"]):focus-visible i:nth-child(2),
.hamb--bars:not([aria-expanded="true"]):focus-visible i:nth-child(3){width:100%}
/* and they spread very slightly apart */
.hamb--bars:not([aria-expanded="true"]):hover i:nth-child(1){transform:translateY(-1.5px)}
.hamb--bars:not([aria-expanded="true"]):hover i:nth-child(3){transform:translateY(1.5px)}
.hamb--duo:not([aria-expanded="true"]):hover i:nth-child(1){transform:translateY(-1.5px)}
.hamb--duo:not([aria-expanded="true"]):hover i:nth-child(2){transform:translateY(1.5px)}

/* four dots: they fan out from the centre, one corner at a time */
.hamb--dots:not([aria-expanded="true"]):hover i,
.hamb--dots:not([aria-expanded="true"]):focus-visible i{transition:transform .34s cubic-bezier(.34,1.4,.5,1)}
.hamb--dots:not([aria-expanded="true"]):hover i:nth-child(1){transform:translate(-1.5px,-1.5px)}
.hamb--dots:not([aria-expanded="true"]):hover i:nth-child(2){transform:translate(1.5px,-1.5px); transition-delay:.04s}
.hamb--dots:not([aria-expanded="true"]):hover i:nth-child(3){transform:translate(-1.5px,1.5px); transition-delay:.08s}
.hamb--dots:not([aria-expanded="true"]):hover i:nth-child(4){transform:translate(1.5px,1.5px); transition-delay:.12s}

/* word and rule: the rule sweeps out to meet the width of the word */
.hamb--word .hamb__word{transition:letter-spacing .28s ease}
.hamb--word:not([aria-expanded="true"]):hover .hamb__word{letter-spacing:.2em}
.hamb--word:not([aria-expanded="true"]):hover i:nth-child(3){width:100%}

@media (prefers-reduced-motion: reduce){
  .hamb i,.hamb--word .hamb__word{transition:none}
}

/* Chips are links now, not buttons, so the underline has to go: it made
   a row of them read as body copy rather than controls. */
.chip--btn{text-decoration:none}
.chip--btn:hover{border-color:var(--ink)}
.chip--btn:focus-visible{outline:3px solid var(--accent); outline-offset:3px}

/* ── contact form state ─────────────────────────────────────────── */
/* Honeypot: off-screen rather than display:none, because some bots skip
   anything that is display:none and fill everything else. */
.hp{position:absolute; left:-9999px; width:1px; height:1px; overflow:hidden}

.req{color:var(--accent); font-weight:700}
.reqnote{font-size:15px; color:var(--grey-2); margin-top:-6px}

.field.is-bad input,.field.is-bad select,.field.is-bad textarea{border-color:var(--bad)}
.fielderr{color:var(--bad); font-size:15px; font-weight:600}
.consent.is-bad{color:var(--bad)}

.sent{display:grid; gap:7px; padding:20px 24px; border-radius:6px; margin-bottom:26px;
  background:var(--accent-soft); border-left:5px solid var(--brand)}
.sent b{font-size:19px; color:var(--heading)}
.sent span{color:var(--grey); font-size:17px; line-height:1.5}
.sent a{color:var(--accent)}
.sent--bad{background:var(--bad-soft); border-left-color:var(--bad)}

.cf-turnstile{margin-block:4px}
.field select:invalid{color:var(--grey-2)}

/* ── service icons ──────────────────────────────────────────────────
   The built-in set is inline SVG stroked with currentColor, so the only
   thing needed to make it follow the brand is to set a colour here. An
   uploaded image keeps whatever colours it arrived with. */
.svccard__top{gap:12px}
.svcicon{width:34px; height:34px; flex:0 0 auto; color:var(--brand-deep); align-self:center}
.svcicon--img{display:grid; place-items:center; width:38px; height:38px}
.svcicon--img img{width:100%; height:100%; object-fit:contain}
.svccard__top h3{flex:1 1 auto}
@media (max-width:520px){.svcicon{width:28px; height:28px}}

/* ── inner page hero illustration ───────────────────────────────────
   These illustrations are wider than they are tall and carry their own
   soft background shape, so they want more width than the portrait does
   and no panel behind them. */
.hero--art .hero--split{grid-template-columns:minmax(0,1fr) minmax(0,.95fr); align-items:center}
.pagehero{margin:0}
.pagehero img{width:100%; max-width:520px; margin-inline:auto; height:auto}
/* Set beside a heading with no standfirst under it. See page-head.php: the
   square legal artwork at the full 520 rendered 520 tall against the 232 of
   the wide banners elsewhere, which is what made those pages look off. */
.pagehero--tight img{max-width:340px}

@media (max-width:900px){
  /* Below the split, the illustration leads rather than trails: on a phone
     it reads as the page's picture, not as something left over. */
  .hero--art .hero--split{grid-template-columns:1fr; gap:28px}
  .hero--art .hero--split > div{order:2}
  .hero--art .pagehero{order:1}
  .pagehero img{max-width:360px}
}

/* An email address is one unbreakable word. In the narrow facts column on a
   phone, helpdesk@wbj.solutions was wider than the screen and pushed the
   whole page sideways. */
.facts dd, .facts dd a, .asidenote, .foot a{overflow-wrap:anywhere}

/* ── service card illustration ──────────────────────────────────────
   A band across the top of the card, bleeding to its edges. The
   illustrations are transparent and carry their own soft shape, so they
   sit on a wash rather than on the card's white, which stops them
   floating in the corner of a plain box. */
/* Every banner is the same box on every card, whatever shape the
   illustration inside it happens to be. The height is fixed rather
   than an aspect ratio, because a full-width card is twice as wide
   as a half-width one and a ratio would make its banner twice as
   tall. The artwork is fitted inside that box instead of filling it,
   so nothing is cropped and nothing is stretched. */
.svcbanner{margin:calc(clamp(24px,2.4vw,34px) * -1) calc(clamp(24px,2.4vw,34px) * -1) 4px;
  background:var(--accent-soft); border-radius:4px 4px 0 0; overflow:hidden;
  display:grid; place-items:center; padding:14px;
  height:clamp(150px,17vw,200px); box-sizing:border-box}
/* No blend mode: the illustrations are already transparent, and multiply
   was pulling the wash up through their whites and tinting them peach. */
.svcbanner img{max-width:min(340px,100%); max-height:100%; width:auto; height:auto;
  object-fit:contain}

/* The card already has a 3px brand top border; the banner sits under it. */
.svccard--img{padding-top:clamp(24px,2.4vw,34px)}

/* A full-width card is a two-column grid whose bullet list is pinned across
   the rows. Adding a banner pushes everything down one row, so the pinned
   list has to move with it or it sits on top of the banner. Both cases are
   stated explicitly rather than left to implicit placement. */
@media (min-width:861px){
  .svccard--wide .svcbanner{grid-column:1/-1; grid-row:1; margin-bottom:14px}
  .svccard--wide.svccard--img .svccard__top{grid-row:2}
  .svccard--wide.svccard--img p{grid-row:3}
  .svccard--wide.svccard--img ul{grid-column:2; grid-row:2/5}
}
@media (max-width:520px){.svcbanner img{max-width:min(260px,100%)}}

/* ── back to top ──────────────────────────────────────────────────
   Hidden until there is enough page behind you to want it, and out of
   the way of the cookie bar, which sits in the same corner. Sized to
   the 44px minimum touch target with room to spare. */
.totop{position:fixed; right:clamp(16px,2.5vw,28px); bottom:clamp(16px,2.5vw,28px);
  z-index:60; width:50px; height:50px; border-radius:50%;
  display:grid; place-items:center; background:var(--brand); color:var(--white);
  box-shadow:0 2px 6px rgba(17,17,17,.12), 0 14px 30px -18px rgba(17,17,17,.7);
  opacity:0; visibility:hidden; transform:translateY(10px);
  transition:opacity .25s ease, transform .25s ease, visibility .25s, background-color .2s ease}
.totop.on{opacity:1; visibility:visible; transform:none}
.totop:hover{background:var(--deep)}
.totop svg{width:22px; height:22px; fill:none; stroke:currentColor; stroke-width:2.2;
  stroke-linecap:round; stroke-linejoin:round}
.totop:focus-visible{outline:3px solid var(--deep); outline-offset:3px}
/* The cookie bar owns the bottom of the screen until it is answered. */
body:has(.ck:not([hidden])) .totop{bottom:clamp(96px,14vh,150px)}
@media (prefers-reduced-motion: reduce){
  .totop{transition:opacity .25s ease, visibility .25s; transform:none}
}
@media print{.totop{display:none}}

/* ── fact find ─────────────────────────────────────────────────────
   One form, four fieldsets. Without JavaScript every step is simply
   on the page and the form still submits in one go, so the stepping
   below is entirely additive: .ff--stepped is set by the script. */
.ff{max-width:none; gap:0}
.ffstep{display:grid; gap:22px; padding-block:clamp(26px,3vw,38px); border-top:1px solid var(--line)}
.ffstep:first-of-type{border-top:0; padding-top:0}
.ffstep__h{font-size:clamp(1.5rem,2.6vw,2rem); letter-spacing:-.02em}
.ffstep__n{color:var(--grey); font-size:17.5px; line-height:1.5; max-width:52ch; margin-top:-12px}

.ff--stepped .ffstep{display:none; border-top:0; padding-top:0}
.ff--stepped .ffstep.is-on{display:grid}

/* The navigation row only exists when stepping does. */
.ffnav{display:none; justify-content:space-between; align-items:center; gap:14px; flex-wrap:wrap; margin-top:8px}
.ff--stepped .ffnav{display:flex}

/* Progress. A list, because that is what it is: four named stages in
   order, with the current one marked. */
.stepper{display:flex; flex-wrap:wrap; gap:8px; list-style:none; padding:0;
  margin:0 0 clamp(26px,3vw,38px); counter-reset:none}
.stepper li{display:flex; align-items:center; gap:9px; padding:8px 16px 8px 9px;
  border:1px solid var(--line); border-radius:999px; background:var(--white);
  font-size:15.5px; font-weight:600; color:var(--grey-2)}
.stepper li span{width:24px; height:24px; border-radius:50%; display:grid; place-items:center;
  background:var(--off); color:var(--grey-2); font-size:13px; font-variant-numeric:tabular-nums}
.stepper li.is-done{color:var(--grey)}
.stepper li.is-done span{background:var(--accent-soft); color:var(--accent)}
.stepper li[aria-current]{border-color:var(--brand); color:var(--heading); background:var(--white)}
.stepper li[aria-current] span{background:var(--brand); color:var(--white)}

/* Checkbox and radio sets. Both are the same object with a different
   control in it, so they share their sizing and only the marker differs. */
.checkset,.pillset{border:0; padding:0; margin:0; display:grid; gap:12px}
.checkset legend,.pillset legend{padding:0; font-weight:600; font-size:17.5px; color:var(--heading)}
.checkset__note{font-size:16px; color:var(--grey-2); margin-top:-4px}
.checkgrid{display:grid; grid-template-columns:repeat(auto-fill,minmax(240px,1fr)); gap:8px}
.pillrow{display:flex; flex-wrap:wrap; gap:8px}

.checkopt,.pillopt{display:flex; align-items:center; gap:11px; cursor:pointer;
  border:1px solid var(--line); background:var(--white); border-radius:4px;
  padding:12px 15px; font-size:16.5px; color:var(--ink); line-height:1.3;
  transition:border-color .15s ease, background-color .15s ease}
.pillopt{border-radius:999px; padding:10px 18px}
.checkopt:hover,.pillopt:hover{border-color:var(--grey-2)}
.checkopt input,.pillopt input{width:19px; height:19px; accent-color:var(--accent); flex:0 0 auto; margin:0}
.pillopt input{width:16px; height:16px}
/* :has, so the whole tile reacts rather than only the box inside it.
   Browsers without it lose the tint and keep a perfectly usable control. */
.checkopt:has(input:checked),.pillopt:has(input:checked){border-color:var(--brand); background:var(--accent-soft)}
.checkopt:has(input:focus-visible),.pillopt:has(input:focus-visible){outline:3px solid var(--accent); outline-offset:2px}

@media (max-width:620px){
  .checkgrid{grid-template-columns:1fr}
  .stepper li{font-size:14.5px; padding:6px 13px 6px 7px}
  .ffnav{flex-direction:column-reverse; align-items:stretch}
  .ffnav .btn{justify-content:center}
}

/* A landscape illustration in a split column. The portrait rule caps the
   width at 400px, which on a wide drawing leaves it small and marooned in
   the middle of its half; this lets it use the column it is given. */
.split__art--wide img{max-width:min(560px,100%)}

/* Body copy in two columns across the full measure.

   Safe only while the whole block fits in roughly one screen: past that the
   reader has to scroll to the foot of the first column and back up to the
   head of the second, which is worse than a single column of any length. So
   it collapses to one column on anything narrow, and the block it is used on
   should stay short. Paragraphs never split across the gap, which leaves the
   columns ending at different heights: a ragged bottom edge is a far smaller
   price than a sentence broken in half by a column break. */
.body--cols{
  max-width:none; margin-top:clamp(26px,3vw,38px);
  columns:2; column-gap:clamp(34px,4vw,70px);
}
.body--cols p{break-inside:avoid; margin:0 0 1.05em}
.body--cols p + p{margin-top:0}
.body--cols p:last-child{margin-bottom:0}
@media (max-width:900px){.body--cols{columns:1}}

/* An illustration that comes with its own background, rather than a cutout
   sitting on the page. The frame is what stops it reading as a stray panel. */
.framed img{
  width:100%; border-radius:14px;
  box-shadow:0 2px 6px rgba(17,17,17,.07), 0 24px 50px -30px rgba(17,17,17,.6);
}
/* ── project gallery ──────────────────────────────────────────────────
   Every picture sits in a window of one shape. That is what lets the
   gallery step between a full page screen grab and a photograph of a
   touchscreen without the page jumping under the reader's thumb. */
/* Shown whole by default, which is what a browser with the script blocked
   gets: long, but complete. The script caps it and adds the button. */
.shotwin{
  position:relative; overflow:hidden; border-radius:14px;
  background:var(--off);
  box-shadow:0 2px 6px rgba(17,17,17,.07), 0 24px 50px -30px rgba(17,17,17,.6);
}
.framed .shotwin img{
  display:block; width:100%; height:auto; border-radius:0; box-shadow:none;
}

/* Wider than the screen it is being read on.
   A desktop screenshot is landscape. Fitted to a 390px phone it comes out
   about 214 tall, which shows that a screenshot exists without showing what
   is in it. So on a narrow screen the picture is rendered at a width its own
   interface is legible at and the frame becomes something to swipe across,
   with a fade on the right to say there is more.
   Above the breakpoint this does nothing at all: the rail is the full width
   and there is nothing to scroll. */
.shotpan{position:relative}
.shotpan__rail{overflow-x:auto; overflow-y:hidden; border-radius:14px; -webkit-overflow-scrolling:touch}
@media (max-width:700px){
  .shotpan__rail{scrollbar-width:thin}
  .framed .shotpan__rail img{width:900px; max-width:none; border-radius:14px}
  /* The fade sits over the right edge of the frame, and goes as soon as the
     reader has swiped, because by then they know. */
  .shotpan::after{
    content:""; position:absolute; top:0; right:0; bottom:0; width:56px; z-index:2;
    pointer-events:none; border-radius:0 14px 14px 0;
    background:linear-gradient(to right, rgba(255,255,255,0), rgba(255,255,255,.92));
  }
  .shotpan__hint{
    position:absolute; right:12px; bottom:12px; z-index:3; pointer-events:none;
    font-size:12px; font-weight:600; letter-spacing:.08em; text-transform:uppercase;
    color:#fff; background:rgba(17,20,24,.72); border-radius:999px; padding:5px 12px;
    transition:opacity .25s;
  }
  .shotpan.is-panned::after,
  .shotpan.is-panned .shotpan__hint{opacity:0}
}
@media (min-width:701px){.shotpan__hint{display:none}}

/* Taller than anybody wants on one page. The foot fades rather than being
   cut, because a hard edge reads as the end of the picture and a fade reads
   as more of it, and one button lets the rest out. --cap is set by the
   script from the height of the window it is being read in. */
.shotwin--cap{max-height:var(--cap,620px)}
.shotwin--cap::after{
  content:''; position:absolute; left:0; right:0; bottom:0;
  height:min(30%, 180px); pointer-events:none;
  background:linear-gradient(to bottom, rgba(255,255,255,0), var(--white) 88%);
}
.sec--off .shotwin--cap::after{background:linear-gradient(to bottom, rgba(250,247,243,0), var(--off) 88%)}
.shotmore{display:flex; justify-content:center; margin-top:-24px; position:relative; z-index:2}
.shotmore__btn{
  font:inherit; font-size:15px; font-weight:600; cursor:pointer;
  padding:11px 22px; border-radius:999px;
  border:1px solid var(--line); background:var(--white); color:var(--ink);
  box-shadow:0 10px 26px -12px rgba(17,17,17,.45);
  transition:border-color .18s, transform .18s;
}
.shotmore__btn:hover{border-color:var(--ink); transform:translateY(-1px)}

.gal{margin-top:clamp(34px,5vw,56px)}
/* A figure carries the browser's own 40px side margins, which on a phone is
   a quarter of the picture given away. */
.gal__slide{margin:0}
/* Without the script every slide is simply on the page in turn. */
.gal__slide + .gal__slide{margin-top:clamp(26px,4vw,44px)}
.gal.is-slider .gal__slide + .gal__slide{margin-top:0}
.gal.is-slider .gal__slide[hidden]{display:none}
.gal__bar{
  display:flex; align-items:center; justify-content:space-between;
  gap:20px; margin-top:20px; flex-wrap:wrap;
}
/* Thumbnails, not dots. A dot says there is another picture; a thumbnail
   says which one, which is what somebody deciding whether to click wants
   to know. */
.gal__thumbs{display:flex; gap:10px; flex-wrap:wrap}
.gal__thumb{
  width:78px; height:52px; padding:0; overflow:hidden; cursor:pointer;
  border:1px solid var(--line); border-radius:8px; background:var(--off);
  opacity:.55; transition:opacity .18s, border-color .18s, box-shadow .18s;
}
.gal__thumb img{width:100%; height:100%; object-fit:cover; object-position:top center; display:block}
.gal__thumb:hover{opacity:.85; border-color:var(--grey-2)}
.gal__thumb[aria-current="true"]{
  opacity:1; border-color:var(--ink);
  box-shadow:0 0 0 1px var(--ink);
}
.gal__step{display:flex; align-items:center; gap:6px; margin-left:auto}
.gal__btn{
  width:36px; height:36px; flex:0 0 auto; display:grid; place-items:center;
  border:1px solid var(--line); border-radius:999px; background:var(--white);
  color:var(--ink); cursor:pointer; line-height:0;
  transition:border-color .18s, background .18s, opacity .18s;
}
.gal__btn svg{width:19px; height:19px; fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round; stroke-linejoin:round}
.gal__btn:hover{border-color:var(--ink)}
.gal__btn[disabled]{opacity:.3; cursor:default}
.gal__btn[disabled]:hover{border-color:var(--line)}
.gal__count{
  font-size:15px; color:var(--grey); font-variant-numeric:tabular-nums;
  min-width:62px; text-align:center;
}
@media (max-width:560px){
  .gal__thumb{width:58px; height:40px}
  .gal__step{margin-left:0}
}

/* ── project page pictures ──────────────────────────────────────────
   Every picture in one slider under the title, the big one beside a rail of
   thumbnails. The rail rather than a row of dots because the whole set is
   then visible at once, and down the left rather than under the picture
   because it costs 112px of width instead of a whole row of height. */
/* The project lede: the summary on the left, what it was built with on the
   right, separated by a rule. The hero was two thirds empty on its right
   with the chips sitting under the paragraph like a footnote. */
.prjlede{
  display:grid; grid-template-columns:minmax(0,1.15fr) minmax(0,.85fr);
  gap:clamp(28px,4vw,64px); align-items:start;
}
.prjlede .hero__sub{max-width:34em}
.prjtech{margin-top:38px; padding-left:clamp(20px,2.4vw,34px); border-left:1px solid var(--line)}
.prjtech__lab{
  display:block; font-size:14px; font-weight:700; letter-spacing:.1em;
  text-transform:uppercase; color:var(--grey-2); margin-bottom:16px;
}
.prjtech .chips{gap:10px}
/* Below this the two columns are both too narrow, so the technologies go
   back under the paragraph, still labelled, with the rule above instead of
   beside. */
@media (max-width:820px){
  .prjlede{grid-template-columns:minmax(0,1fr); gap:0}
  .prjtech{
    margin-top:30px; padding-left:0; border-left:0;
    padding-top:26px; border-top:1px solid var(--line);
  }
}

.prjgal{margin-top:clamp(34px,5vw,56px)}
.prjgal .gal{margin-top:0}
/* The grid arrangement, kept because it is one class away: swap data-gal on
   the gallery for gal--grid and every picture is on the page at once instead
   of one at a time. The rail is what the project page uses. */
.gal--grid{
  display:grid; grid-template-columns:repeat(2,minmax(0,1fr));
  gap:clamp(20px,2.4vw,34px); align-items:start;
}
/* A grid item will not shrink below its content unless told to, and the pan
   window's rail is deliberately wider than the screen. */
.gal--grid .gal__slide{margin:0; min-width:0}
.gal--grid .gal__slide:first-child,
.gal--grid .gal__slide:last-child:nth-child(even){grid-column:1 / -1}
.gal--grid .gal__slide:not(:first-child) .galcap{font-size:15.5px; margin-top:10px}
@media (max-width:700px){
  .gal--grid{grid-template-columns:minmax(0,1fr)}
  .gal--grid .gal__slide{grid-column:auto}
}
/* No measure on this one. It runs under a picture that is already as wide
   as the page allows, and a 56ch limit was breaking a one line caption onto
   a second line with a single word on it. */
.galcap{margin-top:14px; font-size:16.5px; line-height:1.5; color:var(--grey)}
/* is-slider is added by the script. Without it the pictures are simply on
   the page one under the next and none of this applies. */
.prjgal .gal.is-slider{
  display:grid; grid-template-columns:112px minmax(0,1fr);
  gap:clamp(18px,2vw,28px); align-items:start;
}
.prjgal .gal.is-slider .gal__slide{grid-column:2; grid-row:1; margin:0}
.prjgal .gal.is-slider .gal__bar{
  grid-column:1; grid-row:1; flex-direction:column; align-items:stretch;
  margin:0; gap:14px;
}
.prjgal .gal.is-slider .gal__thumbs{flex-direction:column; flex-wrap:nowrap; gap:10px}
.prjgal .gal.is-slider .gal__thumb{width:112px; height:74px}
.prjgal .gal.is-slider .gal__step{
  justify-content:space-between; margin-left:0;
  padding-top:12px; border-top:1px solid var(--line);
}
.prjgal .gal.is-slider .gal__count{min-width:0; font-size:14px; white-space:nowrap}
@media (max-width:760px){
  .prjgal .gal.is-slider{display:block}
  .prjgal .gal.is-slider .gal__bar{flex-direction:row; align-items:center; margin-top:18px}
  .prjgal .gal.is-slider .gal__thumbs{flex-direction:row; flex-wrap:wrap}
  .prjgal .gal.is-slider .gal__thumb{width:78px; height:52px}
  .prjgal .gal.is-slider .gal__step{margin-left:auto; padding-top:0; border-top:0}
}
@media (max-width:560px){
  .prjgal .gal.is-slider .gal__thumb{width:58px; height:40px}
  .prjgal .gal.is-slider .gal__step{margin-left:0}
}

/* art() wraps an illustration in <picture> so the browser can take the WebP.
   A <picture> is an inline box, which quietly broke every rule written for
   the image itself: the service banners size their artwork with
   max-height:100%, and that started resolving against the picture's own
   auto height rather than the fixed-height banner, so the cards went back to
   varying in height. display:contents removes the picture from the box tree
   entirely, leaving the img as the direct child it was before. The picture
   carries no semantics of its own, so nothing is lost by doing this. */
picture{display:contents}

/* ── rotating last word of a heading ──────────────────────────────
   Only ever styled once the script has taken it over: with JavaScript
   off this is an ordinary word in an ordinary heading, and a caret
   blinking beside a word that never changes would be a lie. */
.rot--live{position:relative; white-space:nowrap}
.rot--live::after{
  content:""; display:inline-block; width:.055em; height:.82em;
  margin-left:.05em; vertical-align:-.06em; background:currentColor;
  animation:caret 1.05s steps(1) infinite;
}
@keyframes caret{0%,45%{opacity:1} 46%,100%{opacity:0}}
@media (prefers-reduced-motion:reduce){.rot--live::after{display:none}}

/* The work delivered under a service, at the foot of its card. Names rather
   than cards: this is a footnote proving the service is real, not a second
   projects page inside the services page. */
.svcwork{margin-top:22px; padding-top:18px; border-top:1px solid var(--line)}
.svcwork__lab{
  display:block; font-size:13.5px; font-weight:700; letter-spacing:.1em;
  text-transform:uppercase; color:var(--grey-2); margin-bottom:10px;
}
/* One per line. The card is about 330px wide and client names run from
   "Avoke" to "Gordons Partnership LLP", so a wrapping row left one name on
   the first line and two on the second whatever the order. */
.svcwork__list{
  list-style:none; margin:0; padding:0;
  display:flex; flex-direction:column; gap:7px; font-size:16.5px;
}
.svcwork__list a{color:var(--brand-deep); text-decoration:none; border-bottom:1px solid transparent}
.svcwork__list a:hover{border-bottom-color:currentColor}
