/* ============================================================
   BrandForge — branding and web development, Blankenberge
   Cold monochrome, one saturated accent, one type family.
   Dependency-free.
   ============================================================ */

/* ---------- 0. Fonts ---------- */
/* Self-hosted, so the site makes no third-party request at all: no preconnect to
   Google, no visitor IP leaving for a stylesheet, and nothing in the privacy policy
   about a cross-border transfer. It is also faster — one less DNS lookup, TLS
   handshake and render-blocking stylesheet on a third-party origin.

   Both files are the VARIABLE fonts, not instances. Bricolage carries opsz, wdth and
   wght, and this stylesheet drives all three through font-variation-settings; slicing
   it into static weights would silently flatten every headline. CSS has no descriptor
   for an opsz range, which is why only weight and stretch are declared here — the axis
   is in the file and font-variation-settings reaches it regardless.

   Subsets and unicode-range are Google's own latin and latin-ext slices, kept verbatim.
   Dutch needs nothing beyond latin (ë ï é è ü ö ç all sit under U+00FF); latin-ext is
   here for the foreign client and place names that a studio site inevitably sets.

   Licence: both are SIL Open Font License 1.1. Full text sits next to the files in
   public/assets/fonts/, which is what the licence asks for when the font is bundled. */
@font-face{
  font-family:'Bricolage Grotesque';
  font-style:normal;
  font-weight:300 800;
  font-stretch:75% 100%;
  font-display:swap;
  src:url(/assets/fonts/bricolage-grotesque-latin.woff2) format('woff2');
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,
    U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family:'Bricolage Grotesque';
  font-style:normal;
  font-weight:300 800;
  font-stretch:75% 100%;
  font-display:swap;
  src:url(/assets/fonts/bricolage-grotesque-latin-ext.woff2) format('woff2');
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,
    U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,
    U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face{
  font-family:'JetBrains Mono';
  font-style:normal;
  font-weight:100 800;
  font-display:swap;
  src:url(/assets/fonts/jetbrains-mono-latin.woff2) format('woff2');
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,
    U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family:'JetBrains Mono';
  font-style:normal;
  font-weight:100 800;
  font-display:swap;
  src:url(/assets/fonts/jetbrains-mono-latin-ext.woff2) format('woff2');
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,
    U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,
    U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* ---------- 1. Tokens ---------- */
:root{
  /* Resting scale for parallaxed images: the drift happens inside this
     headroom, so an edge can never appear. */
  --img-rest:1.1;

  /* Cool greys, deliberately NOT the warm bone/cream family that every
     generated "premium" page reaches for. */
  --bg:#ededef;
  --bg-2:#e3e4e7;
  --bg-3:#d8d9dd;

  --ink:#111214;      /* 15.9:1 on --bg */
  --ink-2:#4a4d55;    /* 7.2:1  */
  --ink-3:#63666e;    /* 4.9:1  */

  /* One accent, locked across the page. 6.2:1 on --bg, so it is legible
     at body sizes, not only as a shape. */
  --accent:#1b34f5;

  /* The same hue, for ink grounds.
     --accent is a dark blue and lands at 2.55:1 on --ink — it reads as a shape and
     fails AA outright at the 12px the mono labels use. Measured, not eyeballed:
     6.52:1 here. Same hue angle, so it is the accent in a different light rather
     than a second colour in the palette. */
  --accent-dk:#7d90ff;

  --line:rgba(17,18,20,.14);
  --line-2:rgba(17,18,20,.26);

  --font:'Bricolage Grotesque',system-ui,-apple-system,sans-serif;
  --mono:'JetBrains Mono',ui-monospace,'SF Mono',Menlo,monospace;

  --pad:clamp(18px,4.2vw,64px);
  --maxw:1600px;
  --sec:clamp(96px,12vw,190px);
  --gap:clamp(22px,3.4vw,64px);
  --nav-h:70px;
  --r:2px;                       /* one radius, everywhere */

  --e:cubic-bezier(.16,1,.3,1);
  --e-io:cubic-bezier(.76,0,.24,1);

  --z-nav:100;
  --z-menu:120;
  --z-load:300;
}

/* ---------- 2. Base ---------- */
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body,h1,h2,h3,h4,p,figure,blockquote,ul,ol,dl,dd{margin:0}
ul,ol{padding:0;list-style:none}
img,svg{display:block;max-width:100%}
img{height:auto}
button,input,select,textarea{font:inherit;color:inherit}
button{background:none;border:0;cursor:pointer}
a{color:inherit;text-decoration:none}

html{scroll-behavior:smooth}
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}

body{
  background:var(--bg);color:var(--ink);
  font-family:var(--font);
  font-variation-settings:'opsz' 14,'wdth' 100,'wght' 400;
  font-size:16px;line-height:1.55;
  -webkit-font-smoothing:antialiased;
}
html.is-locked,body.is-locked{overflow:hidden}

::selection{background:var(--accent);color:#fff}
:focus-visible{outline:2px solid var(--accent);outline-offset:3px}

.skip-link{
  position:fixed;top:-100px;left:var(--pad);z-index:400;
  background:var(--ink);color:var(--bg);padding:12px 20px;border-radius:var(--r);
  font-family:var(--mono);font-size:.78rem;
}
.skip-link:focus{top:14px}

.wrap{width:100%;max-width:var(--maxw);margin-inline:auto;padding-inline:var(--pad)}
section[id]{scroll-margin-top:var(--nav-h)}

/* ---------- 3. Type ---------- */
/* The display size, for a page whose heading is the first thing on it.
   This did not exist. All three standalone pages asked for .h1 and got the browser
   default — 32px against the homepage hero's 69px — which on pages carrying no
   imagery meant the one device they had was never switched on. */
.h1{
  font-variation-settings:'opsz' 96,'wdth' 100,'wght' 800;
  font-size:clamp(2.9rem,7.6vw,7rem);
  line-height:.9;letter-spacing:-.045em;
}

.h2{
  font-variation-settings:'opsz' 72,'wdth' 100,'wght' 800;
  font-size:clamp(2.2rem,6vw,5.4rem);
  line-height:.92;letter-spacing:-.04em;
}
/* Emphasis stays in the family: wide-black against narrow-light, plus the
   accent. No second typeface injected into a headline. */
em{
  font-style:normal;
  font-variation-settings:'opsz' 72,'wdth' 75,'wght' 300;
  color:var(--accent);
  letter-spacing:-.02em;
}

/* ---------- 4. Motion utilities ---------- */
.w{
  display:inline-block;overflow:hidden;vertical-align:top;
  padding-bottom:.14em;margin-bottom:-.14em;
  padding-inline:.06em;margin-inline:-.06em;
}
.js .wi{display:inline-block;transform:translateY(112%)}
.js .is-in .wi{transform:none;transition:transform .95s var(--e);transition-delay:calc(var(--d,0) * 42ms)}

.c{display:inline-block}
.js .c{transform:translateY(115%)}
.js .is-in .c{transform:none;transition:transform .9s var(--e);transition-delay:calc(var(--d,0) * 17ms)}
.hero__title.is-kinetic .c{transition:none;will-change:transform}

.js [data-anim="fade"]{opacity:0;transform:translateY(26px)}
.js [data-anim="fade"].is-in{
  opacity:1;transform:none;
  transition:opacity .9s var(--e),transform .9s var(--e);
  transition-delay:calc(var(--d,0) * 80ms);
}

.js [data-anim="img"]{clip-path:inset(0 0 100% 0)}
.js [data-anim="img"] img{scale:1.22}
.js [data-anim="img"].is-in{clip-path:inset(0 0 0 0);transition:clip-path 1.1s var(--e)}
.js [data-anim="img"].is-in img{scale:var(--img-rest);transition:scale 1.4s var(--e)}

/* Lede copy resolves word by word rather than as a block. */
.js [data-anim="soft"] .sw{display:inline-block;opacity:0;transform:translateY(9px)}
.js [data-anim="soft"].is-in .sw{
  opacity:1;transform:none;
  transition:opacity .7s var(--e),transform .7s var(--e);
  transition-delay:calc(var(--d,0) * 24ms);
}

/* Pointer tilt on the work cards. JS writes the transform. */
/* .pj__a used to reserve a 3D context and a compositor layer here for a pointer tilt
   that no module ever wrote. A standing will-change on six large cards costs memory
   for nothing, and the preserve-3d put the link in the same 3D context that broke the
   rail's hit-testing. Its display:block lives with the rest of the card, in 13. */

/* ---------- 5. Atmosphere ---------- */
/* Fixed, pointer-events:none, transform-only animation. Never on a
   scrolling container. */
.paper{
  position:fixed;inset:-60px;z-index:60;pointer-events:none;
  opacity:.05;mix-blend-mode:multiply;will-change:transform;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E");
  animation:tooth 1.1s steps(1) infinite;
}
@keyframes tooth{
  0%{transform:translate3d(0,0,0)}    25%{transform:translate3d(-3%,-2%,0)}
  50%{transform:translate3d(2%,3%,0)} 75%{transform:translate3d(-2%,1%,0)}
}
.progress{position:fixed;top:0;left:0;right:0;height:2px;z-index:calc(var(--z-nav) + 1);pointer-events:none}
.progress i{display:block;height:100%;width:100%;background:var(--accent);transform:scaleX(0);transform-origin:left}

/* ---------- 6. Loader ---------- */
/* The showreel's transition panel, reused as the opening. Cobalt, the mark ghosted
   and breathing, leaving upward on the same curve. One move for both, rather than a
   dark loading screen that belongs to nothing else on the page. */
.loader{
  position:fixed;inset:0;z-index:var(--z-load);
  background:var(--accent);
  display:grid;place-items:center;
  transition:transform .52s cubic-bezier(.76,0,.24,1);
}
.loader.is-done{transform:translateY(-100%)}
.loader__mark{
  width:min(13vw,148px);color:#fff;opacity:.18;
  animation:loadBreathe 3.6s ease-in-out infinite;
}
.loader__mark svg{fill:currentColor;width:100%}
@keyframes loadBreathe{
  0%,100%{transform:scale(1) rotate(0)}
  50%{transform:scale(1.07) rotate(-2.5deg)}
}

/* ---------- 7. Nav ---------- */
.nav{
  position:fixed;top:0;left:0;right:0;z-index:var(--z-nav);
  transition:transform .55s var(--e),color .45s,background-color .45s;
}
.nav.is-stuck{background:rgba(237,237,239,.93)}
/* Wrapper exists so the nav aligns to the same max-width as page content
   instead of running edge to edge on wide screens. */
.nav__in{
  width:100%;max-width:var(--maxw);margin-inline:auto;
  height:var(--nav-h);display:flex;align-items:center;gap:22px;
  padding-inline:var(--pad);
}

.brand{display:flex;align-items:center;gap:11px;padding-block:10px;flex-shrink:0}
.brand__mark{position:relative;width:22px;transition:color .3s,transform .5s var(--e)}
.brand__mark svg{fill:currentColor}

/* On the index the bar used to have no logo at all through the whole hero: the
   nav mark was hidden until the travelling one docked into it. Instead it sits
   there as an outline and gets filled in on arrival, which also gives the
   hand-off something to visibly land in.
   vector-effect keeps the stroke at real pixels rather than scaling it by the
   22/170 viewBox ratio, so the weight is predictable at this size. It is declared
   on the path below, not here — see the note there.
   Scoped to .brand--nav, which only the index has; every other page shows the
   finished logo from the start. */
.js .brand--nav .brand__mark svg{
  fill-opacity:var(--nav-fill,0);
  stroke:currentColor;stroke-opacity:var(--nav-stroke,1);
  stroke-width:1.6;stroke-linejoin:round;
}
/* vector-effect has to sit on the shape, not the <svg>: it is one of the few SVG
   presentation properties that does not inherit, so declaring it above did nothing and
   the stroke was being scaled by the viewBox ratio like any other geometry. At 22px
   wide against a 170-unit viewBox that is 1.6 x 0.129 = 0.2 of a pixel — which is why
   the outline was there in the DOM, correct in every computed style, and invisible. */
.js .brand--nav .brand__mark svg path{vector-effect:non-scaling-stroke}
.brand:hover .brand__mark{color:var(--accent);transform:rotate(-8deg)}

/* The mark locking into the bar. One impact pulse and a single ring, the same
   language as the load strike, so the two moments read as one idea rather than
   two unrelated effects. Fires once per dock: scrolling back into the hero
   re-arms it. */
.brand__mark::after{
  content:'';position:absolute;inset:-34%;border-radius:50%;
  border:1.5px solid var(--accent);opacity:0;pointer-events:none;
}
.js .brand--nav .brand__mark.is-docked{animation:dockPop 620ms var(--e) both}
.js .brand--nav .brand__mark.is-docked::after{animation:dockRing 560ms cubic-bezier(.12,.85,.25,1) both}

@keyframes dockPop{
  0%{transform:scale(.7) rotate(-14deg);color:var(--accent)}
  36%{transform:scale(1.17) rotate(3deg);color:var(--accent)}
  60%{transform:scale(.96) rotate(-1.5deg)}
  100%{transform:none;color:inherit}
}
@keyframes dockRing{
  0%{opacity:.85;transform:scale(.45)}
  70%{opacity:0;transform:scale(1.95)}
  100%{opacity:0;transform:scale(1.95)}
}
/* opsz tracks the rendered size rather than being left where it was: Bricolage has a
   real optical-size axis, and a face cut for 20px set at 21 is subtly wrong in the
   direction of too delicate. Width is not a constraint here — at 1010px, the tightest
   viewport that still shows the links and the CTA, this leaves 239px of slack. */
.brand__word{font-variation-settings:'opsz' 24,'wdth' 100,'wght' 800;font-size:1.3rem;letter-spacing:-.04em;white-space:nowrap}
.brand--lg .brand__mark{width:38px}
.brand--lg .brand__word{font-size:2.1rem}

.nav__links{display:flex;gap:clamp(16px,2.2vw,34px);margin-left:auto}
.nav__links a{
  display:block;overflow:hidden;height:1.45em;
  /* Bricolage, not mono. The mono had spread to the nav, the buttons, every eyebrow
     and every label, which made the chrome read as a developer tool rather than a
     studio. Mono is now reserved for metadata: years, weeks, indices, the tags. */
  font-family:var(--font);font-size:.92rem;letter-spacing:-.015em;color:var(--ink-2);
  font-variation-settings:'opsz' 14,'wdth' 100,'wght' 500;
}
.nav__links a span{display:block;position:relative;transition:transform .5s var(--e)}
.nav__links a span::after{content:attr(data-txt);position:absolute;left:0;top:100%;color:var(--accent)}
.nav__links a:hover span,.nav__links a:focus-visible span{transform:translateY(-100%)}
/* aria-current, not a class, so the state a screen reader announces and the state you
   can see are the same one — and an attribute-presence selector, because there are now
   two sources for it: navSpy sets "location" while you scroll past a section on the
   homepage, and the nav sets "page" on whichever entry you are actually on.
   Set by navSpy as aria-current, not a class, so the state a screen reader
   announces and the state you can see are the same attribute. */
.nav__links a[aria-current]{color:var(--ink)}

.nav__end{display:flex;align-items:center;gap:10px;margin-left:clamp(12px,2vw,28px)}
/* The hero already carries this button. Two identical black rectangles within a few
   hundred pixels of each other made the first screen look like a template, so the
   bar's copy waits until the hero has scrolled out — which is the point at which it
   becomes useful. Every other page has is-stuck from the start, so it is simply there.
   visibility as well as opacity, so it is not a tab stop while invisible. */
.nav__end .btn{transition:color .35s var(--e),opacity .35s var(--e),visibility .35s}
.nav:not(.is-stuck) .nav__end .btn{opacity:0;visibility:hidden}

/* Language switcher. Deliberately outside .nav__links, so it survives the
   900px breakpoint that hides the links and the CTA: the other language has to
   be reachable on a phone too. */
.lang{display:flex;align-items:center;gap:2px;font-family:var(--mono);font-size:.72rem;letter-spacing:.06em}
.lang a{color:var(--ink-3);padding:4px 5px;transition:color .3s}
.lang a:hover,.lang a[aria-current="true"]{color:var(--ink)}
.lang i{width:1px;height:11px;background:var(--line-2)}
.nav--invert .lang a{color:rgba(237,237,239,.6)}
.nav--invert .lang a:hover,.nav--invert .lang a[aria-current="true"]{color:var(--bg)}
.nav--invert .lang i{background:rgba(237,237,239,.3)}
@media (max-width:1000px){
  .nav__links,.nav__end .btn{display:none}
  .nav__end{margin-left:auto}
}

/* inverted while a dark or cobalt register sits behind the bar */
.nav--invert{color:var(--bg)}
.nav--accent.is-stuck{background:rgba(27,52,245,.92)}
.nav--accent .nav__links a span::after{color:#fff}
.nav--accent .brand:hover .brand__mark{color:#fff}
.nav--invert .nav__links a{color:rgba(237,237,239,.7)}
.nav--invert .nav__links a[aria-current]{color:var(--bg)}
.nav--invert .nav__links a span::after{color:#6e86ff}
.nav--invert.is-stuck{background:rgba(17,18,20,.92)}
.nav--invert .burger{border-color:rgba(237,237,239,.36)}
.nav--invert .burger span{background:var(--bg)}
.nav--invert .btn--fill{background:var(--bg);color:var(--ink);border-color:var(--bg)}

.burger{
  display:none;width:44px;height:44px;border:1px solid var(--line-2);border-radius:var(--r);
  align-items:center;justify-content:center;flex-direction:column;gap:5px;
}
.burger span{display:block;width:16px;height:1.5px;background:var(--ink);transition:transform .4s var(--e)}
.burger.is-open span:first-child{transform:translateY(3.25px) rotate(45deg)}
.burger.is-open span:last-child{transform:translateY(-3.25px) rotate(-45deg)}
@media (max-width:1000px){.burger{display:flex}}

/* ---------- 8. Menu ---------- */
.menu{
  position:fixed;inset:0;z-index:var(--z-menu);background:var(--ink);color:var(--bg);
  display:flex;flex-direction:column;justify-content:center;
  padding:calc(var(--nav-h) + 36px) var(--pad) 52px;
  clip-path:inset(0 0 100% 0);transition:clip-path .8s var(--e-io);
}
.menu[hidden]{display:none}
.menu.is-open{clip-path:inset(0 0 0 0)}
.menu__nav{display:flex;flex-direction:column}
.menu__nav a{
  padding:12px 0;border-bottom:1px solid rgba(237,237,239,.18);
  font-variation-settings:'opsz' 60,'wdth' 100,'wght' 800;
  font-size:clamp(2rem,10vw,3.6rem);letter-spacing:-.045em;line-height:1.1;
  transition:color .3s,padding-left .4s var(--e);
}
.menu__nav a span{display:block;overflow:hidden}
.menu__nav a span > *{display:block;transform:translateY(110%);transition:transform .7s var(--e);transition-delay:calc(var(--i,0) * 60ms)}
.menu.is-open .menu__nav a span > *{transform:none}
.menu__nav a:hover{color:#6e86ff;padding-left:12px}
/* It was one email address under five links, with the rest of the panel empty. The
   things a phone visitor might actually want are here now: the other language, the
   two direct lines, the time at the studio, and the mark. */
.menu__foot{
  margin-top:clamp(28px,6vh,48px);
  display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px 24px;align-items:end;
  font-family:var(--mono);font-size:.78rem;color:rgba(237,237,239,.68);
}
.menu__foot a{color:inherit;transition:color .3s}
.menu__foot a:hover{color:var(--bg)}
.menu__lines{display:flex;flex-direction:column;gap:8px}
.menu__foot .lang{font-size:.78rem}
.menu__foot .lang a{color:rgba(237,237,239,.5)}
.menu__foot .lang a[aria-current="true"]{color:var(--bg)}
.menu__foot .lang i{background:rgba(237,237,239,.3)}
.menu__time{display:flex;flex-direction:column;gap:4px;font-family:var(--mono);font-size:.72rem}
.menu__time time{
  font-family:var(--font);font-variation-settings:'opsz' 48,'wdth' 100,'wght' 700;
  font-size:2rem;letter-spacing:-.04em;line-height:1;color:var(--bg);
  min-width:4ch;display:inline-block;
}
.menu__mark{width:34px;color:#6e86ff;grid-column:1 / -1}
.menu__mark svg{fill:currentColor;width:100%}

/* ---------- 9. Buttons ---------- */
.btn{
  position:relative;display:inline-flex;align-items:center;justify-content:center;
  min-height:48px;padding:14px 26px;overflow:hidden;isolation:isolate;
  border:1px solid currentColor;border-radius:var(--r);
  font-family:var(--font);font-size:.9rem;letter-spacing:-.015em;
  font-variation-settings:'opsz' 14,'wdth' 100,'wght' 600;
  white-space:nowrap;
  transition:color .35s var(--e),transform .5s var(--e);
  will-change:transform;
}
.btn span{position:relative;z-index:2}
.btn::before{
  content:"";position:absolute;inset:0;z-index:1;background:var(--accent);
  transform:translateY(101%);transition:transform .5s var(--e);
}
.btn:hover::before,.btn:focus-visible::before{transform:translateY(0)}
.btn:hover,.btn:focus-visible{color:#fff}
/* The magnetic lean arrives as --mx/--my rather than as an inline transform, so the
   stylesheet keeps control of composition and :active can still add its scale. An
   inline transform would have overridden it and lost the press. */
.btn{transform:translate3d(var(--mx,0),var(--my,0),0)}
.btn:active{transform:translate3d(var(--mx,0),var(--my,0),0) scale(.98)}
/* JS eases the lean on the shared frame loop, so a transition on top of it would
   double-smooth and lag. The transition stays for the spring back to zero, which is
   also driven by the loop — .35s is the colour, not the movement. */
[data-magnetic]{transition:color .35s var(--e)}
.ft__up{transform:translate3d(var(--mx,0),var(--my,0),0)}
.btn--fill{background:var(--ink);color:var(--bg);border-color:var(--ink)}
.btn--block{width:100%}
.nav .btn{min-height:38px;padding:10px 18px;font-size:.86rem}

/* ---------- 10. Hero — asymmetric split ---------- */
.hero{
  position:relative;min-height:min(100svh,1000px);
  display:flex;flex-direction:column;
  /* nav height + 26 = 96px, the skill's pt-24 cap. The old +40 put it at 110px,
     and the rule's reason was exactly the symptom this hero had: content
     floating mid-viewport reading as a layout bug rather than intent. */
  --hero-pb:clamp(52px,8vh,92px);
  padding:calc(var(--nav-h) + 26px) 0 var(--hero-pb);
  overflow:hidden;
}
/* Fixed layer, so the mark survives .hero's overflow clip and can travel
   into the nav. JS owns its transform; nothing else writes to it. */
.markfx{
  /* Behind the hero content, not over it. It is a background element: at 5.5% ink
     that never mattered, but a cobalt trace painted on top of the headline reads as
     an artifact crossing the type. Behind it, the type occludes the trace and the
     whole thing reads as depth. The cost is that the last 6% of the travel passes
     under the nav bar, which is 93% opaque, so the crossfade just resolves a hair
     earlier than it used to. */
  position:fixed;z-index:1;pointer-events:none;
  /* Rests on the LEFT, behind the type. The right half belongs to the hero
     image now, and a 5.5% ghost over a photograph is neither visible nor clean.
     Side effect worth having: the travel to the nav lockup is shorter and more
     direct, since the lockup is top-left too. */
  /* Hero-scale, and drawn rather than ghosted. At 32vw and 8% ink it was a
     texture nobody saw, which left the middle of the viewport empty between the
     headline and the reel. At this size it is the composition: an outlined cobalt
     form the type sits on top of, bleeding off the left edge, with the trace
     running its outline. Still behind everything, still a fixed layer, still the
     thing the strike lands and the hand-off carries into the nav. */
  top:54%;left:calc(var(--pad) * -.55);
  width:min(660px,46vw);
  translate:0 -50%;
  will-change:transform;
}
/* .085, up from .055. At .055 the mark was invisible against --bg, which meant the
   load strike — the site's opening gesture — landed on nothing anyone could see. */
.hero__stamp{display:block;position:relative;color:var(--mark-ink,rgba(17,18,20,.05))}
.hero__stamp svg{fill:currentColor;width:100%;overflow:visible}
/* The outline, in cobalt, so the form reads at rest and not only while the trace
   passes. Non-scaling, so it stays a hairline as the mark shrinks into the nav. */
.hero__stamp svg path:not(.hero__trace){
  stroke:var(--accent);stroke-opacity:.34;stroke-width:1.5;stroke-linejoin:round;
  vector-effect:non-scaling-stroke;
}

/* The travelling highlight. Dash values are in the normalised 1000 units set by
   pathLength on the element, not user units, so replacing the path does not break
   the timing. Stroke width is in user units deliberately: it scales down with the
   mark as it docks, where a non-scaling stroke would end up thick enough to swamp a
   22px logo. */
.hero__trace{
  fill:none;
  stroke:var(--accent);stroke-opacity:.95;
  stroke-width:2.6;stroke-linejoin:round;stroke-linecap:round;
  stroke-dasharray:150 850;
  animation:markTrace 5.6s linear infinite;
}
@keyframes markTrace{to{stroke-dashoffset:-1000}}
/* Animating a dash offset repaints the mark's whole box, which is up to 420px
   across. Nothing below the hero can see it, so stop once it has docked. */
.markfx.is-idle .hero__trace{animation-play-state:paused}
@media (max-width:900px){.markfx{display:none}}

/* The type is the image. The headline runs the full width across the top and the
   reel sits bottom-right beside the lede and the buttons, so the composition spans
   the viewport instead of floating in the middle third of it with a band of empty
   grey above and below — which is what the old centred 60/40 split did at every
   width from 1440 up. flex:1 is what lets the grid take the whole hero. */
.hero__grid{
  position:relative;z-index:2;flex:1;
  display:flex;flex-direction:column;gap:clamp(30px,5vh,60px);
}

/* From 900px: headline across both columns, then the foot and the reel share the
   second row with their bottoms aligned. Below that it stacks and the reel becomes a
   full-bleed band under the CTAs. */
@media (min-width:900px){
  .hero__grid{
    display:grid;grid-template-columns:minmax(0,1fr) minmax(0,38%);
    grid-template-rows:auto 1fr;
    column-gap:clamp(30px,4vw,72px);row-gap:clamp(28px,5vh,64px);
    align-items:end;
  }
  .hero__title{grid-column:1 / -1;align-self:start}
  .hero__foot{grid-column:1}
  .reel{grid-column:2;grid-row:2}
}

.hero__title{
  font-variation-settings:'opsz' 96,'wdth' 100,'wght' 800;
  /* Type and reel are scaled against each other, not independently. "We make
     brands that" measures 8.665x the font size, so the largest font keeping it
     on one line beside a 40% reel is ~5.4vw, capped at 5.5rem because the column
     stops growing once --maxw caps the container. Solved 900-2560px wide;
     tightest margin 35px. The reel needs 40% rather than 30% because a website
     has to be shown in landscape to read as a website at all.
     17ch, not 15ch, so the column is the single governing constraint rather
     than the two fighting near the top of the range. */
  /* Sized to the full measure now that nothing sits beside it. 7.4vw puts "Word de
     logische keuze" at ~1070px on a 1440 screen and the cap keeps the longest of the
     two headlines inside --maxw on a 1920 one. The old 5.5rem cap made it read as a
     section heading. */
  font-size:clamp(2.8rem,9.2vw,9.2rem);
  line-height:.84;letter-spacing:-.055em;
}
.hero__title em{font-variation-settings:'opsz' 96,'wdth' 75,'wght' 300}
/* "pick a side" always starts its own line on wide screens, so the accent phrase
   reads as the payoff rather than landing wherever the measure happens to break.
   A <br> would have to be hidden below the breakpoint and would sit inside the
   per-character split; a block-level em is the same result with nothing to undo,
   and the splitter preserves the em element itself. */
@media (min-width:1024px){
  .hero__title em{display:block}
}

.hero__foot{
  display:grid;grid-template-columns:minmax(0,1fr) auto;
  gap:clamp(20px,4vw,60px);align-items:end;
  padding-top:22px;border-top:1px solid var(--line);
}
.hero__lead{max-width:40ch;color:var(--ink-2);font-size:clamp(.98rem,1.15vw,1.12rem)}
/* Showreel of shipped sites. Deliberately NO fake browser chrome: a window built
   from divs with traffic lights and a URL bar is the single most recognisable
   generated-design tell. The screenshot is the evidence; a drawn frame round it
   adds nothing.

   Shown in true colour, unlike the duotone rail. The whole point is the design
   work, and tinting it cobalt would hide exactly what the visitor came to see. */
.reel{position:relative;margin:0;display:flex;flex-direction:column;gap:10px}
.reel__frame{
  position:relative;overflow:hidden;
  border-radius:var(--r);background:var(--bg-3);
  /* No shadow. It was the only one on a flat site, and it made the reel read as a
     card sitting on the page rather than a window into the work. */
  aspect-ratio:16 / 10;
}
.reel__shot{
  position:absolute;inset:0;overflow:hidden;
  opacity:0;transition:opacity .9s var(--e);
}
.reel__shot.is-on{opacity:1}
/* Taller than the frame on purpose, so the drift below has somewhere to go. */
.reel__shot img{width:100%;height:auto;display:block}

/* The frame pans down the page while a shot is on screen, which is what makes it
   read as a site being browsed rather than a screenshot sitting there. Paused by
   a class rather than removed, so the position is kept. */
.js .reel__shot.is-on img{animation:reelDrift 9s linear both}
.reel.is-held .reel__shot.is-on img{animation-play-state:paused}
@keyframes reelDrift{from{transform:translate3d(0,0,0)}to{transform:translate3d(0,-24%,0)}}

/* The video layer, when a recording is available. Sits over the shots rather than
   replacing them, so the first shot stays the LCP frame and remains the fallback
   if autoplay is refused, the file is missing, or the visitor asked for less data. */
.reel__vid{
  position:absolute;inset:0;z-index:1;pointer-events:none;
  width:100%;height:100%;object-fit:cover;
  opacity:0;transition:opacity .9s var(--e);
}
.reel.has-video .reel__vid{opacity:1}
/* Nothing to pan once the video is on top. */
.reel.has-video .reel__shot.is-on img{animation:none}

/* Names what is on screen, so the reel is attributed evidence and not decoration.
   aria-hidden because each shot's alt already names it; announcing both would
   double up. */
.reel__tag{
  position:absolute;left:10px;bottom:10px;z-index:2;
  font-family:var(--mono);font-size:.68rem;letter-spacing:.04em;
  padding:5px 9px;border-radius:var(--r);
  background:rgba(17,18,20,.82);color:var(--bg);
  backdrop-filter:none;
}
/* No JS means nothing advances, so there is nothing to pause. */
.reel__btn{display:none}
/* Above the frame and flush right, so the frame itself can sit on the bottom edge
   of the hero with nothing under it. */
.js .reel__btn{
  display:inline-flex;align-items:center;gap:7px;align-self:flex-end;order:-1;
  font-family:var(--mono);font-size:.7rem;letter-spacing:.05em;
  color:var(--ink-3);padding:3px 0;transition:color .3s;
}
.js .reel__btn:hover{color:var(--ink)}
.reel__btn i{
  width:9px;height:9px;flex:none;border-left:2.5px solid currentColor;
  border-right:2.5px solid currentColor;
}
.reel.is-paused .reel__btn i{
  border:0;width:0;height:0;
  border-left:8px solid currentColor;border-top:5px solid transparent;
  border-bottom:5px solid transparent;
}

@media (min-width:900px){
  /* Anchored: the frame bleeds off the right edge and down onto the hero's bottom
     edge, where the marquee's rule meets it. A window into the work, not a card
     floating in the corner. */
  .reel{margin-right:calc(var(--pad) * -1);margin-bottom:calc(var(--hero-pb) * -1)}
  .reel__frame{border-radius:var(--r) 0 0 0}
  /* The frame bleeds; the control stays inside the measure. */
  .js .reel__btn{margin-right:var(--pad)}
}
@media (max-width:899px){
  /* Stacked: full-bleed band under the CTAs. The negative margin takes the frame
     to both edges, so the control has to be pushed back in or it sits flush
     against the viewport edge. */
  .reel{margin-inline:calc(var(--pad) * -1)}
  .reel__frame{border-radius:0}
  .js .reel__btn{margin-inline:var(--pad)}
}

.hero__cta{display:flex;gap:8px;flex-wrap:nowrap}
@media (max-width:860px){
  .hero__foot{grid-template-columns:1fr;align-items:start}
  .hero__cta{flex-wrap:wrap}
  .hero__cta .btn{flex:1 1 auto}
}

/* ---------- 11. Marquee ---------- */
.mq{border-block:1px solid var(--ink);padding-block:18px;overflow:hidden;background:var(--bg-2)}
.mq__t{display:flex;width:max-content;will-change:transform}
.mq__g{display:flex;align-items:center;flex-shrink:0}
.mq__g span{
  font-variation-settings:'opsz' 40,'wdth' 100,'wght' 700;
  font-size:clamp(1.3rem,3.2vw,2.7rem);letter-spacing:-.04em;
  padding-inline:clamp(16px,2.4vw,38px);white-space:nowrap;
}
/*
 * Sized against the type it sits between rather than fixed, so it holds its proportion
 * as the marquee scales — the words run from 1.3rem to 2.7rem.
 *
 * Scoped under .mq__g and with the word rule's spacing explicitly cleared, because the
 * mark is a span and .mq__g span therefore matches it too. That rule sets 30.72px of
 * inline padding a side, and under border-box that is more than this element's whole
 * width — the content box collapsed to zero and the svg inside rendered at 0x0.
 */
.mq__g .mq__m{
  width:clamp(11px,1.3vw,20px);flex:none;
  padding-inline:0;margin-inline:0;
  /* The marquee's own font-size is inherited otherwise, and line-height off a 41px
     font would push the mark off the words' baseline. */
  font-size:0;line-height:0;
}
.mq__g .mq__m svg{width:100%;height:auto}
.mq__g .mq__m path{fill:var(--accent)}

/* ---------- 12. Statement ---------- */
/* The one cobalt register on the page, and the full measure. As a grey column on a
   grey page it used half the width and left the rest empty for nearly two viewports;
   as a band it is the beat between the hero and the dark rail, and the tokens are
   redefined the same way section 13 does it for the dark one. */
.stmt{
  --ink:#fff;
  --ink-2:rgba(255,255,255,.78);
  --line:rgba(255,255,255,.22);
  padding-block:clamp(90px,13vw,200px);
  background:var(--accent);color:var(--ink);
}
/* Held while the copy resolves, so the position reads as a moment rather
   than something you scroll past. Pin is added by JS on wide screens only. */
.stmt.is-pinned{padding-block:0}
.stmt.is-pinned .stmt__hold{
  position:sticky;top:0;height:100svh;
  display:flex;align-items:center;
}
.stmt__t{
  font-variation-settings:'opsz' 72,'wdth' 100,'wght' 500;
  font-size:clamp(1.6rem,4.4vw,4rem);
  line-height:1.06;letter-spacing:-.04em;max-width:30ch;
}
.stmt__t .k{color:rgba(255,255,255,.5);transition:color .5s var(--e)}
.stmt__t .k.on{color:var(--ink)}

/* The mark signing the statement. Defaults to the FINISHED state on purpose: with
   no JS the signature is simply there, rather than an invisible outline that never
   gets drawn. The frame task sets it back to zero before animating. */
.stmt__sign{display:block;width:clamp(50px,5vw,74px);margin-top:clamp(26px,4vh,50px)}
.stmt__sign svg{
  width:100%;overflow:visible;
  fill:#fff;fill-opacity:var(--sign-fill,1);
  stroke:#fff;stroke-width:3;stroke-linejoin:round;
  stroke-dasharray:var(--sign-len,none);
  stroke-dashoffset:var(--sign-off,0);
}

/* ---------- 13. Work — pinned rail, the one dark register ---------- */
.work{
  --ink:#ededef;
  --ink-2:#9a9da6;
  --ink-3:#8b8e97;
  --accent:#6e86ff;                /* 5.8:1 on the dark ground */
  --line:rgba(237,237,239,.18);
  --line-2:rgba(237,237,239,.3);
  background:#111214;color:var(--ink);
}
.work__pin{padding-block:var(--sec)}
.work__head{margin-bottom:clamp(30px,4.5vw,58px)}

.rail{overflow-x:auto;scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;scrollbar-width:none}
.rail::-webkit-scrollbar{display:none}
.rail__t{display:flex;gap:clamp(14px,1.8vw,30px);width:max-content;padding-inline:var(--pad)}

.pj{flex:0 0 auto;width:min(600px,82vw);scroll-snap-align:center}
.pj__a{display:block}
.pj__img{
  display:block;position:relative;overflow:hidden;
  aspect-ratio:5/4;background:#0b0c0e;border-radius:var(--r);
}
.pj__img img{
  width:100%;height:100%;object-fit:cover;scale:var(--img-rest);
  filter:grayscale(1) contrast(1.08);will-change:transform;
  transition:filter .7s var(--e),scale 1.4s var(--e);
}
/* Colour-blend plate over a desaturated photograph: the work reads as one
   art-directed set rather than six unrelated stock shots. */
.pj__img::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  background:var(--accent);mix-blend-mode:color;opacity:.9;
  transition:opacity .7s var(--e);
}
.pj__a:hover .pj__img::after,.pj__a:focus-visible .pj__img::after{opacity:0}
.pj__a:hover .pj__img img,.pj__a:focus-visible .pj__img img{filter:none;scale:1.16}

.pj__row{
  display:flex;align-items:baseline;gap:12px;margin-top:16px;
  padding-bottom:8px;border-bottom:1px solid var(--line);
}
.pj__n{
  flex:1;font-variation-settings:'opsz' 36,'wdth' 100,'wght' 700;
  font-size:clamp(1.25rem,2.1vw,1.9rem);letter-spacing:-.04em;transition:color .3s;
}
.pj__a:hover .pj__n{color:var(--accent)}
.pj__row i{font-family:var(--mono);font-style:normal;font-size:.8rem;color:var(--ink-3)}
.pj__k{display:block;margin-top:8px;font-family:var(--mono);font-size:.8rem;color:var(--ink-3)}

.work__bar{margin-top:clamp(24px,3.5vw,44px)}
.work__line{display:block;height:1px;background:var(--line);position:relative}
.work__line i{position:absolute;inset:0;width:18%;background:var(--accent);transform-origin:left}

.work.is-pinned .work__pin{
  position:sticky;top:0;height:100svh;
  display:flex;flex-direction:column;justify-content:center;
  padding-block:calc(var(--nav-h) + 20px) 44px;overflow:hidden;
}
/* The perspective belongs on the track, not on .rail, and that is a correctness fix
   rather than a preference.
 
   perspective applies to an element's DIRECT children. On .rail that is the track, so
   the cards — grandchildren — could only join the 3D space through transform-style:
   preserve-3d on the track. Inside that preserve-3d context Chrome hit-tested the
   cards against geometry that had nothing to do with where they painted:
   elementFromPoint at a card's own measured centre returned the track, so every card
   in the pinned rail was unclickable. Below 1024px the rail does not pin, no card is
   transformed, and clicking worked — which is why it looked intermittent.
 
   With the perspective on the track the cards are its direct children and need no
   preserve-3d. perspective-origin is written per frame to follow the viewport centre
   through the track's own translation, which reproduces .rail's fixed, centred origin
   exactly: card widths and positions measured pixel-identical before and after. */
.work.is-pinned .rail{overflow:hidden;scroll-snap-type:none}
.work.is-pinned .rail__t{will-change:transform;perspective:1500px}
.work.is-pinned .pj{width:min(540px,40vw);will-change:transform}

/* ---------- 14. Services — index with cursor-tracked preview ---------- */
.svc{padding-block:var(--sec)}
.svc__h{margin-bottom:clamp(30px,4.5vw,58px)}
.idx{border-top:1px solid var(--ink)}
.idx__row{
  position:relative;
  border-bottom:1px solid var(--line);
  transition:opacity .45s var(--e),padding-left .55s var(--e);
}
/* The grid moved off the row and onto the link inside it. These rows are now links to
   the service pages, and an anchor wrapping the cells would otherwise become the row's
   single grid item and collapse the whole layout into one column. */
.idx__a{
  display:grid;
  grid-template-columns:minmax(0,8fr) minmax(0,4fr);
  gap:var(--gap);align-items:baseline;
  padding:clamp(24px,3vw,42px) 0;
  color:inherit;
}
@media (hover:hover) and (pointer:fine){
  .idx:hover .idx__row{opacity:.3}
  .idx:hover .idx__row:has(.idx__a:hover){opacity:1;padding-left:clamp(8px,1.4vw,24px)}
}
.idx__row::after{
  content:"";position:absolute;left:0;bottom:-1px;height:1px;width:100%;
  background:var(--accent);transform:scaleX(0);transform-origin:right;
  transition:transform .6s var(--e);
}
.idx__row:has(.idx__a:hover)::after,
.idx__row:has(.idx__a:focus-visible)::after{transform:scaleX(1);transform-origin:left}
.idx__t{
  font-variation-settings:'opsz' 48,'wdth' 100,'wght' 700;
  font-size:clamp(1.4rem,3.2vw,2.8rem);letter-spacing:-.045em;line-height:1;
  transition:color .4s var(--e);
}
.idx__a:hover .idx__t,.idx__a:focus-visible .idx__t{color:var(--accent)}
/* These two were crossed. The prose rule — 54ch, ink-3, a top margin — was written
   for the description, but the port put the description in .idx__b and a two-digit
   index in .idx__d, so the number inherited a 54ch measure and the description got no
   styling at all. */
.idx__b{color:var(--ink-3);font-size:.96rem;max-width:54ch;margin-top:12px}
.idx__d{font-family:var(--mono);font-size:.78rem;color:var(--ink-3);text-align:right}
.idx__k{font-family:var(--mono);font-size:.8rem;color:var(--ink-3);text-align:right}
@media (max-width:900px){
  .idx__a{grid-template-columns:minmax(0,1fr)}
  .idx__k,.idx__d{text-align:left}
}

.peek{
  position:fixed;top:0;left:0;z-index:90;pointer-events:none;
  width:clamp(200px,18vw,320px);aspect-ratio:4/3;overflow:hidden;border-radius:var(--r);
  opacity:0;transition:opacity .4s var(--e);will-change:transform;background:var(--accent);
}
.peek.is-on{opacity:1}
.peek img{width:100%;height:100%;object-fit:cover;filter:grayscale(1) contrast(1.08);mix-blend-mode:luminosity}
@media (hover:none),(pointer:coarse){.peek{display:none}}

/* The pointer label. Not a custom cursor — the native one stays — but a small tag
   that says what a click will do over the two things on the page that open or play:
   the work cards and the reel. Text and target come from data-cursor; position is
   written by motion/cursor.js. Fine pointers only, like the peek. */
.cur{
  position:fixed;left:0;top:0;z-index:95;pointer-events:none;display:none;
  translate:-50% -50%;scale:.8;opacity:0;
  transition:opacity .25s var(--e),scale .3s var(--e);will-change:transform;
  font-family:var(--mono);font-size:.68rem;letter-spacing:.05em;white-space:nowrap;
  padding:8px 11px;border-radius:var(--r);background:var(--accent);color:#fff;
}
.cur.is-on{opacity:1;scale:1}
@media (hover:hover) and (pointer:fine){.cur{display:block}}

/* ---------- 15. Process — sequential timeline ---------- */
/* Steps are ordered, so they get a line and a playhead rather than a grid.
   A bento says "these are parallel"; a process is not. */
.proc{padding-block:var(--sec);background:var(--bg-2);border-block:1px solid var(--line)}
.proc__grid{display:grid;grid-template-columns:minmax(0,4fr) minmax(0,7fr);gap:var(--gap);align-items:start}
.proc__side{position:sticky;top:calc(var(--nav-h) + 44px);display:flex;flex-direction:column;gap:24px}
.proc__lead{color:var(--ink-2);font-size:.98rem;max-width:34ch}
/* The sticky column used to hold a placeholder photograph. Now it holds the number
   of the step the playhead has reached, at display size, counted up by steps() in
   motion/home.js as you scroll — something that changes per step, and true. */
.proc__count{
  display:flex;align-items:baseline;gap:.12em;margin-top:clamp(10px,2vw,26px);
  font-variation-settings:'opsz' 96,'wdth' 100,'wght' 800;
  font-size:clamp(5rem,13vw,12rem);line-height:.8;letter-spacing:-.06em;
  font-variant-numeric:tabular-nums;
}
.proc__count b{font-weight:inherit;color:var(--accent);min-width:2ch}
.proc__count small{font-family:var(--mono);font-size:.8rem;letter-spacing:.04em;color:var(--ink-3)}
@media (max-width:900px){
  .proc__grid{grid-template-columns:1fr}
  .proc__side{position:static}
  .proc__count{display:none}
}

.steps{position:relative;padding-left:66px}
.steps__rail{
  position:absolute;left:17px;top:14px;bottom:14px;width:1px;
  background:var(--line-2);
}
.steps__rail i{
  display:block;width:100%;height:100%;background:var(--accent);
  transform:scaleY(0);transform-origin:top;will-change:transform;
}
.step{position:relative;padding-block:clamp(22px,3vw,40px)}
.step:first-child{padding-top:0}
.step__n{
  position:absolute;left:-66px;top:clamp(22px,3vw,40px);
  /* var(--r), not 50%: these were the only circles on a site that commits to one
     radius in its opening comment. */
  width:36px;height:36px;display:grid;place-items:center;border-radius:var(--r);
  background:var(--bg-2);border:1px solid var(--line-2);
  font-family:var(--mono);font-size:.72rem;color:var(--ink-3);
  transition:color .45s var(--e),border-color .45s var(--e),background-color .45s var(--e);
}
.step:first-child .step__n{top:0}
.step.is-on .step__n{color:#fff;background:var(--accent);border-color:var(--accent)}
.step__b{transition:transform .55s var(--e);transform:translateY(9px)}
.step.is-on .step__b{transform:none}
.step h3{
  font-variation-settings:'opsz' 40,'wdth' 100,'wght' 700;
  font-size:clamp(1.4rem,2.8vw,2.3rem);letter-spacing:-.04em;margin-bottom:10px;
}
.step p{color:var(--ink-2);font-size:.98rem;max-width:50ch}
.step__w{
  display:inline-block;margin-top:14px;
  font-family:var(--mono);font-size:.76rem;color:var(--ink-3);
}
@media (max-width:620px){
  .steps{padding-left:50px}
  .steps__rail{left:13px}
  .step__n{left:-50px;width:28px;height:28px;font-size:.7rem}
}

/* ---------- 16. Stats — full-bleed accent field ---------- */
.stats{
  --ink:#fff;
  --line:rgba(255,255,255,.42);
  background:var(--accent);color:var(--ink);
  padding-block:clamp(56px,7vw,104px);
}
.stats__g{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:clamp(16px,2.6vw,40px)}
.stat{display:flex;flex-direction:column;gap:8px;padding-top:18px;border-top:2px solid var(--ink)}
.stat__n{
  font-variation-settings:'opsz' 96,'wdth' 100,'wght' 800;
  font-size:clamp(2.6rem,6.4vw,5.6rem);line-height:.86;letter-spacing:-.055em;
  font-variant-numeric:tabular-nums;
}
.stat__l{font-family:var(--mono);font-size:.8rem;color:var(--ink)}
@media (max-width:760px){.stats__g{grid-template-columns:repeat(2,minmax(0,1fr))}}

/* ---------- 17. Studio — image + copy split ---------- */
.studio{padding-block:var(--sec)}
.studio__grid{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:var(--gap);align-items:start}
.studio__fig{overflow:hidden;border-radius:var(--r);background:var(--bg-3);aspect-ratio:4/5}
.studio__fig img{width:100%;height:100%;object-fit:cover;scale:var(--img-rest);filter:grayscale(1) contrast(1.05);will-change:transform}
.studio__b{display:flex;flex-direction:column;align-items:flex-start;gap:26px}
.studio__lead{color:var(--ink-2);max-width:48ch;font-size:1rem}
.tenets{display:flex;flex-direction:column;border-top:1px solid var(--line)}
.tenets div{padding:18px 0;border-bottom:1px solid var(--line)}
.tenets dt{
  font-variation-settings:'opsz' 24,'wdth' 100,'wght' 700;
  font-size:1.06rem;letter-spacing:-.03em;margin-bottom:5px;
}
.tenets dd{color:var(--ink-3);font-size:.94rem;max-width:52ch}
@media (max-width:900px){.studio__grid{grid-template-columns:1fr}}

/* ---------- 18. Clients — one large quote ---------- */
.says{padding-block:var(--sec);background:var(--bg-2);border-block:1px solid var(--line)}
/* The reserved height stops the controls jumping as quotes of different lengths
   rotate through. With only one quote nothing rotates, so reserving space for a
   longer one that will never arrive is just a hole in the page. */
.says__stage{position:relative;min-height:clamp(210px,26vw,290px)}
.says__stage--one{min-height:0}
.say{position:absolute;inset:0;opacity:0;visibility:hidden;transform:translateY(20px)}
.say.is-active{opacity:1;visibility:visible;transform:none;transition:opacity .7s var(--e),transform .7s var(--e)}
.say p{
  font-variation-settings:'opsz' 72,'wdth' 100,'wght' 500;
  font-size:clamp(1.4rem,3.4vw,2.9rem);line-height:1.1;letter-spacing:-.04em;
  max-width:20ch;
}
.say footer{margin-top:clamp(22px,3vw,38px)}
.say cite{font-style:normal;display:flex;flex-direction:column;font-variation-settings:'wght' 700;font-size:.98rem}
.say cite i{font-family:var(--mono);font-style:normal;font-size:.8rem;color:var(--ink-3);font-variation-settings:'wght' 400}
/* A quote about a project that links to it. The most persuasive thing on the page
   should be one click from the evidence behind it. */
.say__link{
  display:inline-block;margin-top:14px;
  font-family:var(--mono);font-size:.78rem;color:var(--ink-2);
  text-decoration:underline;text-underline-offset:4px;transition:color .3s;
}
.say__link:hover{color:var(--accent)}
/* The stage stacks absolutely positioned slides, so a hidden one must not be
   reachable by keyboard while it is invisible. */
.say:not(.is-active){pointer-events:none}

.says__nav{display:flex;align-items:center;gap:12px;margin-top:clamp(20px,3vw,36px)}
.rbtn{
  width:46px;height:46px;display:grid;place-items:center;border-radius:var(--r);
  border:1px solid var(--line-2);color:var(--ink-2);
  transition:background-color .3s,color .3s,border-color .3s;
}
.rbtn svg{width:18px;height:18px}
.rbtn:hover{background:var(--ink);color:var(--bg);border-color:var(--ink)}
.says__dots{display:flex;gap:2px}
.says__dots button{width:44px;height:44px;display:grid;place-items:center}
.says__dots button::before{content:"";width:22px;height:2px;background:var(--line-2);transition:background-color .3s}
.says__dots button[aria-current="true"]::before{background:var(--accent)}

/* ---------- 19. Contact — split form ---------- */
.ct{padding-block:var(--sec)}
.ct__grid{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:var(--gap);align-items:start}
.ct__side{display:flex;flex-direction:column;gap:26px}
.ct__t{
  font-variation-settings:'opsz' 80,'wdth' 100,'wght' 800;
  font-size:clamp(2.2rem,5.6vw,4.8rem);line-height:.9;letter-spacing:-.05em;
}
.ct__t em{font-variation-settings:'opsz' 80,'wdth' 75,'wght' 300}
.ct__lead{color:var(--ink-2);font-size:.98rem;max-width:44ch}
.ct__direct{display:flex;flex-direction:column;border-top:1px solid var(--line)}
.ct__direct div{display:grid;grid-template-columns:80px minmax(0,1fr);gap:12px;padding:12px 0;border-bottom:1px solid var(--line);align-items:baseline}
.ct__direct dt{font-family:var(--mono);font-size:.76rem;color:var(--ink-3)}
.ct__direct dd{font-size:.95rem}
.ct__direct a{border-bottom:1px solid var(--line-2);transition:color .3s,border-color .3s}
.ct__direct a:hover{color:var(--accent);border-color:var(--accent)}
@media (max-width:900px){.ct__grid{grid-template-columns:1fr}}

.form{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}
@media (max-width:560px){.form{grid-template-columns:1fr}}
.field{display:flex;flex-direction:column;gap:6px}
.field--full{grid-column:1 / -1}
.field label{font-family:var(--mono);font-size:.76rem;color:var(--ink-2)}
.field label i{color:var(--accent);font-style:normal}
.field input,.field select,.field textarea{
  width:100%;min-height:48px;padding:12px 14px;
  background:var(--bg-2);color:var(--ink);
  border:1px solid var(--line-2);border-radius:var(--r);
  font-size:.95rem;font-family:inherit;transition:border-color .3s,background-color .3s;
}
.field textarea{min-height:112px;resize:vertical;line-height:1.5}
.field select{
  appearance:none;cursor:pointer;padding-right:38px;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2363666e' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 13px center;
}
/* The honeypot. Taken out of the layout and out of the accessibility tree, but
   NOT display:none or hidden: some bots skip fields a real browser would never
   render, and a few screen readers still reach display:none content. Off-canvas
   plus aria-hidden plus tabindex="-1" is the combination that a human never
   meets and a naive bot fills in. */
.trap{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}

.field input::placeholder,.field textarea::placeholder{color:var(--ink-3)}
.field input:focus,.field select:focus,.field textarea:focus{border-color:var(--accent);background:#fff;outline:none}
/* The rule above drops the outline so the pointer focus reads as a clean border
   tint. Keyboard focus still needs a real indicator, so put the ring back. */
.field input:focus-visible,.field select:focus-visible,.field textarea:focus-visible{
  outline:2px solid var(--accent);outline-offset:2px;
}
.field__e{font-size:.8rem;color:var(--accent);display:none}
/* One accent is locked across the page, so an invalid field cannot be told from
   a focused one by hue. It is told apart by weight instead. */
.field.has-error input,.field.has-error select,.field.has-error textarea{
  border-color:var(--accent);box-shadow:inset 3px 0 0 var(--accent);
}
.field.has-error .field__e{display:block}
.form__foot{grid-column:1 / -1;display:flex;flex-direction:column;gap:10px}
/* Quieter than the status line and not in mono: it is a sentence to read once, not
   a system message. --ink-3 is 4.9:1, so it still clears AA at this size. */
.form__p{font-size:.82rem;line-height:1.5;color:var(--ink-3)}
.form__p a{color:var(--ink-2);text-decoration:underline;text-underline-offset:3px;transition:color .3s}
.form__p a:hover{color:var(--accent)}
.form__s{font-family:var(--mono);font-size:.8rem;color:var(--ink-2);min-height:1.4em}
.form__s.is-ok{color:var(--ink)}
.form__s.is-err{color:var(--accent)}
.btn.is-busy{pointer-events:none;opacity:.6}

/* ---------- 19b. The sentence form ---------- */
/* The form is the sentence the headline asks for, with the five values as blanks in
   it. Set as prose at display size rather than as labels over boxes: the words around
   a blank are its label, and the blank is an underline the way a blank in a sentence
   has always been drawn. */
.form--sentence{display:flex;flex-direction:column;gap:clamp(22px,3vw,34px)}
.sen{
  font-variation-settings:'opsz' 32,'wdth' 100,'wght' 500;
  font-size:clamp(1.3rem,2.3vw,2rem);line-height:1.45;letter-spacing:-.025em;
  color:var(--ink-2);max-width:30ch;
}
/* The blank. An inline grid with the control and a hidden copy of its text in the
   one cell, so the cell is as wide as the text and the control fills it — sizing by
   layout, with nothing measured. Same font as the prose, inherited, so the mirror and
   the control cannot disagree about width. */
.blank{
  display:inline-grid;vertical-align:baseline;
  max-width:100%;
  border-bottom:2px solid var(--line-2);
  transition:border-color .3s var(--e);
}
.blank__m,.blank input,.blank select,.blank textarea{
  grid-area:1 / 1;
  font:inherit;font-variation-settings:inherit;letter-spacing:inherit;line-height:inherit;
  padding:0 .12em;margin:0;min-width:0;
}
.blank__m{visibility:hidden;white-space:pre}
/* width:0 with min-width:100%, not width:100%. A percentage width resolves to auto
   while the grid is sizing its track, so the input contributed its own intrinsic
   width — the browser's twenty-character default — and every empty blank was three
   words wider than its placeholder. At zero it contributes nothing and the mirror
   alone sets the cell; the min-width then stretches the control to fill it. */
.blank input,.blank select,.blank textarea{
  width:0;min-width:100%;background:none;border:0;border-radius:0;color:var(--accent);
  outline:none;
}
.blank input::placeholder,.blank textarea::placeholder{color:var(--ink-3);opacity:1}
.blank select{appearance:none;cursor:pointer;color:var(--ink-3)}
.blank.is-filled select{color:var(--accent)}
/* Native option lists do not inherit the page's type sizes, and at 2rem they would
   be unusable on the platforms that do. */
.blank select option{font-size:1rem;color:var(--ink)}
/* The project blank is its own line, because a paragraph of description cannot wrap
   around the words beside it. The mirror wraps like the text does, so the textarea is
   always exactly as tall as what has been typed. */
.blank--block{display:grid;width:100%;margin-block:.2em .1em}
.blank--block .blank__m{white-space:pre-wrap;overflow-wrap:anywhere}
.blank--block textarea{resize:none;overflow:hidden;overflow-wrap:anywhere}
.blank:focus-within{border-color:var(--accent)}
/* A text control always matches :focus-visible, so a ring on the whole blank would
   appear on every click. The caret and the accent underline are the indicator for
   those; the select has neither, so it keeps a ring for keyboard focus. */
.blank--select:has(:focus-visible){outline:2px solid var(--accent);outline-offset:4px;border-radius:var(--r)}
/* One accent is locked across the page, so an invalid blank cannot be told from a
   focused one by hue. It is told apart by weight, and by its message below. */
.blank.has-error{border-bottom-width:3px;border-color:var(--accent)}
.sen__errors{
  display:flex;flex-direction:column;gap:6px;
  padding-left:14px;border-left:2px solid var(--accent);
  font-size:.86rem;line-height:1.4;color:var(--ink);
}
@media (max-width:560px){
  .sen{font-size:1.2rem;letter-spacing:-.02em}
}

/* ---------- 20. Footer ---------- */
.ft{padding-block:clamp(48px,6vw,80px) 24px;border-top:1px solid var(--ink)}
.ft__top{display:flex;flex-wrap:wrap;gap:20px 40px;justify-content:space-between;align-items:flex-end;padding-bottom:clamp(30px,4vw,54px)}
.ft__blurb{color:var(--ink-3);font-size:.94rem;text-align:right}
@media (max-width:640px){.ft__blurb{text-align:left}}
.ft__cols{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--gap);padding-block:clamp(28px,3.5vw,46px);border-top:1px solid var(--line)}
.ft__cols > div{display:flex;flex-direction:column;gap:8px}
.ft__cols h3{font-family:var(--mono);font-size:.76rem;color:var(--accent);font-weight:400;font-variation-settings:'wght' 400;margin-bottom:4px}
.ft__cols a{color:var(--ink-2);font-size:.94rem;width:fit-content;padding-block:3px;transition:color .3s}
.ft__cols a:hover{color:var(--accent)}
.ft__time{font-variation-settings:'wdth' 100,'wght' 800;font-size:1.6rem;font-variant-numeric:tabular-nums;letter-spacing:-.04em}
@media (max-width:700px){.ft__cols{grid-template-columns:repeat(2,minmax(0,1fr))}}
.ft__bar{
  display:flex;flex-wrap:wrap;gap:12px 24px;justify-content:space-between;align-items:center;
  padding-top:20px;border-top:1px solid var(--line);
  font-family:var(--mono);font-size:.8rem;color:var(--ink-2);
}
/* Case pages keep their own way back to the rail. It sits above the shared footer
   rather than replacing it, so leaving a case study no longer means losing the
   address, the contact details and every section link. */
.ft__ret{padding-block:clamp(26px,3.4vw,44px);border-top:1px solid var(--line);font-family:var(--mono);font-size:.8rem}
.ft__bar small{font-size:inherit}
/* Sits between the copyright and "back to top". margin-right:auto rather than a
   third flex child with space-between, so the legal links stay next to the
   copyright they belong with instead of being pushed to the centre of the bar. */
.ft__legal{display:flex;flex-wrap:wrap;gap:2px 18px;margin-right:auto}
/*
 * Padded to a 24px target.
 *
 * These were inline with no padding and measured 54x17 — under WCAG 2.2's 24x24
 * minimum for a pointer target. The exception for links inline in a sentence does not
 * cover them: they are a list of three sitting on their own in the footer bar, which
 * is exactly the case the criterion is about. The gap drops to 2px so the row does not
 * grow now that each link carries its own height.
 */
.ft__legal a{
  display:inline-block;padding-block:4px;
  color:var(--ink-3);transition:color .3s;
}
.ft__legal a:hover{color:var(--accent)}
.ft__up{display:inline-flex;align-items:center;gap:7px;color:var(--ink-2);transition:color .3s}
.ft__up svg{width:14px;height:14px;transition:transform .4s var(--e)}
.ft__up:hover{color:var(--accent)}
.ft__up:hover svg{transform:translateY(-3px)}
/* Case pages leave the page rather than scrolling up, so the arrow points back
   and the hover has to follow it. Same rule, different axis. */
.ft__up--back:hover svg{transform:translateX(-3px)}

/* ---------- 20b. Footer reveal ---------- */
/* The footer does not scroll up into view — it is already there, fixed at the bottom
   of the window, and the page lifts off it.

   .page is opaque and sits one layer above, so the footer is hidden behind the content
   for the whole scroll. .ft__gap is a transparent hole in the flow exactly as tall as
   the footer, so the last stretch of scrolling slides the page off and uncovers it. No
   per-frame work: the only thing measured is the footer's height, and CSS does the rest
   from the scroll position the browser already knows.

   The full-bleed accent field is deliberate. One accent is locked across this site, so
   spending it whole on the last thing you see reads as arrival rather than decoration. */
.page{position:relative;z-index:1;background:var(--bg)}
.ft__gap{height:var(--ft-h,0px);pointer-events:none}

.ft{
  position:fixed;inset:auto 0 0 0;z-index:0;
  /* The dark register does the same thing in section 13: redefine the tokens rather
     than override every rule that consumes them. */
  --ink:#fff;
  --ink-2:rgba(255,255,255,.82);
  /* .72 measured 4.45:1 on this blue, which misses AA by a hair. .78 clears it. */
  --ink-3:rgba(255,255,255,.78);
  --accent:#fff;
  --line:rgba(255,255,255,.22);
  --line-2:rgba(255,255,255,.36);
  /* The literal blue, not var(--accent): --accent is redefined to white just above
     so the footer's hovers land on white, and a custom property cannot reference the
     value it is shadowing. Section 13 hardcodes its own ground for the same reason. */
  background:#1b34f5;color:var(--ink);
}
/* h3 took its colour from --accent, which is now white and would flatten against the
   body text. The hierarchy moves to weight and opacity instead. */
.ft .ft__cols h3{color:var(--ink-3)}
.ft .ft__legal a{color:var(--ink-3)}
.ft .ft__legal a:hover,.ft .ft__cols a:hover,.ft .ft__up:hover{color:var(--ink)}
.ft .brand__mark svg{fill:currentColor}

/* A short page cannot lift far enough to uncover a tall footer, so on those the footer
   returns to the flow and simply sits at the end. Nothing to reveal, nothing to fake. */
.ft--static{position:static;z-index:auto}
/* No rule needed to collapse the gap: footerReveal clears --ft-h whenever it sets this
   class, and the gap's height is var(--ft-h,0px). The first attempt here used a
   next-sibling selector, which cannot work — the gap comes before the footer. */

/* ---------- 21. The load strike ---------- */
.js .markfx.is-armed .hero__stamp{opacity:0}
.js .markfx.is-stamped .hero__stamp{animation:stampDrop 720ms both, stampInk 1150ms both}

@keyframes stampDrop{
  0%{
    opacity:0;
    transform:translate3d(0,-124vh,0) scale(1.3) scaleY(1.26) rotate(-11deg);
    animation-timing-function:cubic-bezier(.6,.02,.86,.06);
  }
  6%{opacity:1}
  42%{
    opacity:1;
    transform:translate3d(0,0,0) scale(1) scaleY(1.08) rotate(0deg);
    animation-timing-function:steps(1,end);
  }
  46%{transform:translate3d(0,0,0) scaleX(1.075) scaleY(.87) rotate(0deg);animation-timing-function:cubic-bezier(.16,1,.3,1)}
  50%{transform:translate3d(0,0,0) scaleX(1.075) scaleY(.87) rotate(0deg)}
  64%{transform:translate3d(0,-8px,0) scaleX(.985) scaleY(1.04) rotate(.4deg)}
  80%{transform:translate3d(0,0,0) scaleX(1.008) scaleY(.99) rotate(0deg)}
  100%{transform:translate3d(0,0,0) scale(1) rotate(0deg)}
}
@keyframes stampInk{
  0%{color:rgba(17,18,20,.8)}
  25%{color:rgba(17,18,20,.86)}
  26%{color:rgba(17,18,20,1)}
  32%{color:rgba(17,18,20,1);animation-timing-function:cubic-bezier(.2,.7,.3,1)}
  100%{color:var(--mark-ink,rgba(17,18,20,.055))}
}
.hero__ring{
  position:absolute;left:50%;top:50%;width:100%;aspect-ratio:1;
  translate:-50% -50%;border-radius:50%;
  border:2px solid var(--accent);opacity:0;
}
.js .markfx.is-stamped .hero__ring{animation:ring 540ms 302ms cubic-bezier(.12,.85,.25,1) forwards}
.js .markfx.is-stamped .hero__ring--2{animation-delay:368ms;animation-duration:720ms;border-width:1px}
@keyframes ring{
  0%{opacity:.7;transform:scale(.12)}
  60%{opacity:.15}
  100%{opacity:0;transform:scale(2.1)}
}
.js .hero.is-stamped .hero__grid{animation:jolt 400ms 302ms cubic-bezier(.16,1,.3,1) both}
@keyframes jolt{
  0%{transform:translate3d(0,0,0)}
  12%{transform:translate3d(0,13px,0)}
  38%{transform:translate3d(0,-4px,0)}
  66%{transform:translate3d(0,1px,0)}
  100%{transform:translate3d(0,0,0)}
}

/* ---------- 21b. Case studies ---------- */
/* Cross-document transitions, for when navigation really is cross-document: with
   JavaScript off, or SSR on and hydration not yet done, links are ordinary
   navigations and the browser runs this itself. Under Inertia the visit is
   client-side and this rule never applies — MorphLink drives the same transition by
   hand there. Kept because it costs nothing and covers the case Inertia does not. */
@view-transition{navigation:auto}

/* ---------- 21b-i. Opening a case study ---------- */
/* The card does not link to the case study so much as become it. The image keeps its
   identity across the navigation, and the rest of the case page arrives over the
   homepage rather than replacing it, so the two read as one surface.

   Named elements are lifted out of the root snapshot and animated separately, which
   is why the hero can travel while everything else cross-fades underneath it. */
.case__hero{view-transition-name:case-hero}
/* A service opens out of its own name in the homepage index: the row's title is what
   continues onto the page, the way a case study's hero image does. Only ever one per
   page, so it can be named in CSS rather than assigned on the way out. */
.svcp__t{view-transition-name:service-title}

/* Every part of the transition on one clock. The root group keeps the UA's 250ms
   otherwise, which finishes while the pages are still crossing — measured, and it
   makes the whole move feel like two overlapping animations rather than one. */
::view-transition-group(*){
  animation-duration:.62s;
  animation-timing-function:var(--e-io);
}
::view-transition-group(case-hero){
  animation-duration:.62s;
  animation-timing-function:var(--e-io);
}
/* The two images are different crops of the same picture, so cross-fading them while
   the group travels hides the change in aspect. Without this the old one holds its
   shape and pops at the end. */
::view-transition-old(case-hero),
::view-transition-new(case-hero){
  animation-duration:.62s;
  animation-timing-function:var(--e-io);
  height:100%;
  object-fit:cover;
}

/* The preview is a 4:3 panel under the pointer and the header figure is a wider box
   in the corner of the page. Different crops of one drawing, so they cross-fade while
   the group travels — the same reason case-hero does, and without it the old one holds
   its shape and pops at the end. */
::view-transition-old(service-art),
::view-transition-new(service-art){
  animation-duration:.62s;
  animation-timing-function:var(--e-io);
  height:100%;
  object-fit:cover;
}

/* The opening curtain, reused as the page transition. The outgoing page wipes up
   off a cobalt ground and the incoming one wipes up onto it a beat later, on the
   same curve the loader leaves on — so the site has one way of moving between
   things rather than a curtain on load and a crossfade everywhere after.

   The ground is painted on the transition root itself: the snapshots sit above it,
   so the gap between the two wipes shows cobalt rather than whatever the live DOM
   underneath happens to be. Named groups (the case hero, the service title) are
   above both and travel across the band unclipped. */
::view-transition,
::view-transition-group(root){background:var(--accent)}
::view-transition-old(root){
  animation:pageWipeOut .5s var(--e-io) both;
}
::view-transition-new(root){
  animation:pageWipeIn .55s var(--e-io) .2s both;
  z-index:2;
}
@keyframes pageWipeOut{
  to{clip-path:inset(0 0 100% 0)}
}
@keyframes pageWipeIn{
  from{clip-path:inset(100% 0 0 0)}
}

.is-case .nav{background:rgba(237,237,239,.93)}
.is-case .brand__mark{opacity:1}          /* no hand-off on this page */

.case{padding-top:calc(var(--nav-h) + clamp(40px,7vw,96px))}
.case__head{display:flex;flex-direction:column;gap:22px;padding-bottom:clamp(32px,5vw,64px)}
.case__t{
  font-variation-settings:'opsz' 96,'wdth' 100,'wght' 800;
  font-size:clamp(2.6rem,8vw,7rem);line-height:.88;letter-spacing:-.05em;
}
.case__lede{
  font-variation-settings:'wdth' 100,'wght' 500;
  font-size:clamp(1.1rem,2.2vw,1.7rem);line-height:1.25;letter-spacing:-.025em;
  color:var(--ink-2);max-width:38ch;
}
.case__meta{display:flex;flex-wrap:wrap;gap:10px 48px;padding-top:20px;border-top:1px solid var(--line)}
.case__meta dt{font-family:var(--mono);font-size:.7rem;color:var(--ink-3);margin-bottom:3px}
.case__meta dd{font-size:.98rem}

.case__hero{margin:0;background:var(--bg-3);overflow:hidden}
.case__hero img{width:100%;height:auto;display:block;scale:var(--img-rest);filter:grayscale(1) contrast(1.06);will-change:transform}
/* A drawn cover is already in the site's own two colours; desaturating it would turn
   the cobalt ground grey for no reason. Photographs keep the filter. */
.case__hero img[data-cover],.case__figs img[data-cover]{filter:none}

.case__body{
  display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--gap);
  padding-block:clamp(48px,7vw,110px);
}
/*
 * The three acts, numbered.
 *
 * A case study is problem, what we did, where it landed — and all three headings were
 * 12px mono, the smallest type on the page, so the structure the whole page is built
 * on was invisible. A counter rather than numbers in the markup, because the acts are
 * in two different containers and the count should follow document order rather than
 * be maintained by hand in both.
 */
.case{counter-reset:act}
.case__body h2,.case__out h2{
  font-family:var(--mono);font-size:.76rem;color:var(--accent);
  font-weight:400;font-variation-settings:'wght' 400;margin-bottom:16px;
  display:flex;align-items:center;gap:10px;
  counter-increment:act;
}
.case__body h2::before,.case__out h2::before{
  content:counter(act,decimal-leading-zero);
  color:var(--ink-3);
}
/* A rule after the number, so the label reads as a chapter mark and not a caption. */
.case__body h2::after,.case__out h2::after{
  content:"";flex:1;height:1px;background:var(--line);max-width:60px;
}
.case__body p{font-size:1.06rem;line-height:1.62;color:var(--ink-2);max-width:52ch}
@media (max-width:820px){.case__body{grid-template-columns:1fr}}

.case__figs{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:clamp(10px,1.4vw,20px)}
.case__figs figure{margin:0;overflow:hidden;background:var(--bg-3);aspect-ratio:4/5}
.case__figs img{width:100%;height:100%;object-fit:cover;scale:var(--img-rest);filter:grayscale(1) contrast(1.06);will-change:transform}
@media (max-width:620px){.case__figs{grid-template-columns:1fr}}

/*
 * Where it landed, as the page's one moment of colour.
 *
 * Full width because the section is full width, with the measure restored by the
 * .wrap inside — never a pseudo bled out with -100vw, which doubles the document.
 * Ink here rather than at the end: this is the emphasis in the middle of the story,
 * and the page still has its next-project block and the cobalt footer to close on.
 */
.case__out{
  background:var(--ink);color:var(--bg);
  padding-block:clamp(64px,10vw,150px);
  margin-top:clamp(40px,6vw,90px);
}
.case__out h2{color:var(--accent-dk)}
.case__out h2::before{color:rgba(237,237,239,.5)}
.case__out h2::after{background:rgba(237,237,239,.22)}
.case__out p{
  font-variation-settings:'wdth' 100,'wght' 500;
  /* Pull-quote scale. This is the result, and the result is what sells the next one. */
  font-size:clamp(1.5rem,3.4vw,2.6rem);line-height:1.18;letter-spacing:-.03em;
  color:var(--bg);max-width:22ch;
}
/* Sits after the ink band, on the light ground it was designed for. The quotation
   marks are drawn rather than typed, so the text stays a clean string for anyone
   copying it and the mark itself can be set at a size the sentence never dictates. */
.case__quote{
  margin-top:clamp(44px,6vw,84px);padding-top:30px;border-top:1px solid var(--line);
  position:relative;
}
.case__quote::before{
  content:"\201C";position:absolute;top:8px;left:calc(var(--pad) - .12em);
  font-size:4rem;line-height:1;color:var(--accent);opacity:.35;
}
.case__quote p{
  font-variation-settings:'opsz' 72,'wdth' 100,'wght' 500;
  font-size:clamp(1.3rem,3vw,2.3rem);line-height:1.12;letter-spacing:-.035em;color:var(--ink);
  max-width:24ch;
}
.case__quote footer{margin-top:20px}
.case__quote cite{font-style:normal;display:flex;flex-direction:column;font-variation-settings:'wght' 700;font-size:.98rem}
.case__quote cite i{font-family:var(--mono);font-style:normal;font-size:.72rem;color:var(--ink-3);font-variation-settings:'wght' 400}

.nextcase{border-top:1px solid var(--ink);background:var(--bg-2)}
.nextcase__a{display:flex;flex-direction:column;gap:10px;padding-block:clamp(44px,7vw,96px);transition:background-color .4s}
.nextcase__a:hover{background:var(--ink);color:var(--bg)}
.nextcase__l{font-family:var(--mono);font-size:.76rem;color:var(--accent)}
/* The block inverts to ink on hover and the label kept its dark blue, which is the
   same 2.55:1 failure — pre-dating the case study band, found looking for it. */
.nextcase__a:hover .nextcase__l,
.nextcase__a:focus-visible .nextcase__l{color:var(--accent-dk)}
.nextcase__n{
  font-variation-settings:'opsz' 96,'wdth' 100,'wght' 800;
  font-size:clamp(2rem,6.5vw,5rem);line-height:.9;letter-spacing:-.05em;
}

/* ---------- 21b-ii. Service pages ---------- */
/* A page that has to answer a search query, so the prose is set at a measure and the
   furniture stays out of its way. Same register as a case study — this is the site,
   not a landing page bolted onto it. */
.svcp{padding-top:calc(var(--nav-h) + clamp(40px,7vw,96px))}
/* Two columns above the breakpoint: the words on the left, the service's own drawing
   filling the room the taller header opened up. That room was the complaint — a 60vh
   header with nothing in the top half of it. */
.svcp__head{
  display:grid;gap:clamp(28px,4vw,64px);align-items:end;
  grid-template-columns:minmax(0,1fr);
  /* Given room, like /services above it. The index page opened on 78vh of header and
     this one opened on about 30%, so walking from the hub into the page it points at
     was a step down — and this is the page a search result actually lands on. Less
     than the index takes, because there is a page of prose under it that the reader
     came for. */
  min-height:min(60vh,620px);
  padding-bottom:clamp(34px,5vw,64px);border-bottom:1px solid var(--ink);
}
.svcp__htext{display:flex;flex-direction:column;justify-content:flex-end;gap:18px;min-width:0}
.svcp__art{
  /* Named for the transition: the preview trailing the pointer on /services becomes
     this. Set in CSS rather than assigned on the way in, because there is only ever
     one of these per page. */
  view-transition-name:service-art;
  margin:0;border-radius:var(--r);overflow:hidden;aspect-ratio:4/3;
  /* The same treatment .peek gives it — cobalt behind the drawing, the drawing itself
     dropped to luminosity. The two are the same object seen twice, and without this
     the preview was cobalt and the header was white, so the morph changed colour
     halfway across. It is also the only saturated block on the page, which is the
     other half of why this header read as empty. */
  background:var(--accent);
}
.svcp__art img{
  width:100%;height:100%;object-fit:cover;
  filter:grayscale(1) contrast(1.08);mix-blend-mode:luminosity;
}
@media (min-width:900px){
  .svcp__head{grid-template-columns:minmax(0,1.05fr) minmax(0,.75fr)}
}
@media (max-width:899px){
  /* Above the words on a phone, where the header has room at the top and the text is
     what the reader came for. */
  .svcp__head{grid-template-rows:auto auto}
  .svcp__art{order:-1;max-height:34vh}
}
.svcp__k{font-family:var(--mono);font-size:.76rem;color:var(--accent)}
.svcp__t{
  font-variation-settings:'opsz' 96,'wdth' 100,'wght' 800;
  font-size:clamp(2.3rem,6vw,4.8rem);line-height:.92;letter-spacing:-.05em;max-width:18ch;
}
.svcp__lede{
  font-variation-settings:'wdth' 100,'wght' 500;
  font-size:clamp(1.05rem,2vw,1.5rem);line-height:1.3;letter-spacing:-.02em;
  color:var(--ink-2);max-width:44ch;
}

/* minmax(0,...) on both tracks. A bare 1fr floors at min-content, which is what put a
   706px index inside a 352px column on the legal pages. */
.svcp__grid{
  display:grid;grid-template-columns:minmax(0,1fr) minmax(0,19rem);
  gap:var(--gap);padding-block:clamp(40px,6vw,90px);align-items:start;
}
.svcp__body{max-width:66ch}
.svcp__body p{font-size:1.05rem;line-height:1.62;color:var(--ink-2)}
.svcp__body p + p{margin-top:18px}
/* The opening paragraph carries the answer, so it is set to be read first. */
.svcp__body p:first-child{font-size:1.15rem;color:var(--ink)}

.svcp__aside{
  position:sticky;top:calc(var(--nav-h) + 22px);
  background:var(--bg-2);border:1px solid var(--line);padding:clamp(20px,2.4vw,30px);
}
.svcp__aside h2{font-family:var(--mono);font-size:.72rem;color:var(--ink-3);font-weight:400;font-variation-settings:'wght' 400;margin-bottom:14px}
.svcp__aside ul{display:flex;flex-direction:column;gap:11px}
.svcp__aside li{position:relative;padding-left:20px;font-size:.95rem;line-height:1.5;color:var(--ink-2)}
.svcp__aside li::before{content:'';position:absolute;left:2px;top:.62em;width:7px;height:1px;background:var(--line-2)}

/* What we did here — the way out of a case study and into the offer.
   Built as .svcp__more is: cells divided by hairlines rather than pills. The pill
   version was the only shape in this stylesheet that broke the one radius the top of
   the file commits to, and it read as a widget dropped onto the page. */
.case__did{margin-top:clamp(34px,5vw,64px);padding-top:26px;border-top:1px solid var(--line)}
.case__did h2{
  font-family:var(--mono);font-size:.76rem;color:var(--ink-3);
  font-weight:400;font-variation-settings:'wght' 400;margin-bottom:14px;
}
.case__did > div{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(min(15rem,100%),1fr));
  gap:1px;background:var(--line);
}
.case__dida{
  background:var(--bg);padding:clamp(16px,1.9vw,24px);
  font-variation-settings:'wdth' 100,'wght' 700;font-size:1.02rem;letter-spacing:-.025em;
  transition:background-color .35s,color .35s;
}
.case__dida:hover{background:var(--ink);color:var(--bg)}

.svcp__faq{margin-top:clamp(30px,4vw,56px);padding-top:26px;border-top:1px solid var(--line)}
.svcp__faq h2{font-family:var(--mono);font-size:.76rem;color:var(--ink-3);font-weight:400;font-variation-settings:'wght' 400;margin-bottom:10px}
.svcp__faq details{border-bottom:1px solid var(--line)}
.svcp__faq summary{
  list-style:none;cursor:pointer;padding:16px 30px 16px 0;position:relative;
  font-variation-settings:'wdth' 100,'wght' 700;
  font-size:clamp(1rem,1.8vw,1.2rem);letter-spacing:-.02em;
  transition:color .3s;
}
.svcp__faq summary::-webkit-details-marker{display:none}
.svcp__faq summary:hover{color:var(--accent)}
/* The marker is drawn rather than a glyph, so it can rotate on open. */
.svcp__faq summary::after{
  content:'';position:absolute;right:4px;top:50%;width:11px;height:11px;
  border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;
  transform:translateY(-70%) rotate(45deg);transition:transform .35s var(--e);
}
.svcp__faq details[open] summary::after{transform:translateY(-30%) rotate(225deg)}
.svcp__faq details p{padding:0 clamp(30px,6vw,64px) 20px 0;font-size:1rem;line-height:1.6;color:var(--ink-2);max-width:62ch}

.svcp__work,.svcp__more{padding-top:26px;border-top:1px solid var(--line)}
.svcp__work h2,.svcp__more h2{font-family:var(--mono);font-size:.76rem;color:var(--ink-3);font-weight:400;font-variation-settings:'wght' 400;margin-bottom:6px}
.svcp__work ul{display:flex;flex-direction:column}
.svcp__work a{
  display:flex;justify-content:space-between;align-items:baseline;gap:20px;
  padding:15px 0;border-bottom:1px solid var(--line);
  font-variation-settings:'wdth' 100,'wght' 700;
  font-size:clamp(1.05rem,2.2vw,1.5rem);letter-spacing:-.03em;
  transition:color .3s,padding-left .45s var(--e);
}
.svcp__work a:hover{color:var(--accent);padding-left:10px}
.svcp__work a i{font-family:var(--mono);font-style:normal;font-size:.78rem;color:var(--ink-3);flex:none}

.svcp__more{margin-top:clamp(40px,6vw,80px)}
.svcp__more > div{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(15rem,100%),1fr));gap:1px;background:var(--line);margin-top:14px}
.svcp__morea{
  display:flex;flex-direction:column;gap:6px;background:var(--bg);
  padding:clamp(18px,2vw,26px);
  transition:background-color .35s,color .35s;
}
.svcp__morea:hover{background:var(--ink);color:var(--bg)}
.svcp__morea span{font-variation-settings:'wdth' 100,'wght' 700;font-size:1.02rem;letter-spacing:-.025em}
.svcp__morea i{font-family:var(--mono);font-style:normal;font-size:.74rem;color:var(--ink-3)}
.svcp__morea:hover i{color:rgba(237,237,239,.7)}

/* The same close the index pages use. Full width because the section is full width,
   with the measure restored by the .wrap inside it — never a pseudo-element bled out
   with -100vw, which doubles the document width. */
.svcp__cta{
  margin-top:clamp(56px,8vw,110px);padding-block:clamp(60px,9vw,130px);
  background:var(--ink);color:var(--bg);
}
.svcp__cta .wrap{display:flex;flex-direction:column;gap:16px;align-items:flex-start}
.svcp__cta h2{
  font-variation-settings:'opsz' 72,'wdth' 100,'wght' 800;
  font-size:clamp(2rem,4.6vw,3.6rem);line-height:1;letter-spacing:-.045em;max-width:18ch;
}
.svcp__cta p{font-size:1.04rem;line-height:1.6;color:rgba(237,237,239,.68);max-width:52ch}
.svcp__cta .btn{margin-top:10px}
.svcp__cta .btn--fill{background:var(--bg);color:var(--ink);border-color:var(--bg)}
.svcp__cta .btn--fill:hover{background:var(--accent);border-color:var(--accent);color:var(--bg)}

@media (max-width:900px){
  .svcp__grid{grid-template-columns:minmax(0,1fr)}
  .svcp__aside{position:static}
}

/* ---------- 21c. Legal documents ---------- */
/* Same asymmetric split the rest of the site uses, with the index in the narrow
   column. Prose is set to a measure, not to the container: a privacy policy is the
   one page here somebody actually has to read end to end. */
.lg{padding-top:calc(var(--nav-h) + clamp(40px,7vw,96px))}
.lg__head{
  display:flex;flex-direction:column;gap:20px;
  padding-bottom:clamp(30px,4.5vw,56px);border-bottom:1px solid var(--ink);
}
.lg__t{
  font-variation-settings:'opsz' 96,'wdth' 100,'wght' 800;
  font-size:clamp(2.3rem,6.5vw,5.4rem);line-height:.9;letter-spacing:-.05em;
}
.lg__lede{
  font-variation-settings:'wdth' 100,'wght' 500;
  font-size:clamp(1.05rem,1.9vw,1.45rem);line-height:1.3;letter-spacing:-.02em;
  color:var(--ink-2);max-width:46ch;
}
.lg__meta{font-family:var(--mono);font-size:.76rem;color:var(--ink-3)}

.lg__grid{
  display:grid;grid-template-columns:minmax(0,15rem) minmax(0,1fr);
  gap:var(--gap);padding-block:clamp(40px,6vw,90px);align-items:start;
}

/* The index. Sticky so it stays available through a long document, and scrolling
   inside its own box so a fifteen-clause list cannot outgrow the viewport. */
.lg__toc{
  position:sticky;top:calc(var(--nav-h) + 22px);
  max-height:calc(100vh - var(--nav-h) - 60px);overflow:auto;
  font-family:var(--mono);font-size:.78rem;line-height:1.45;
  padding-right:10px;
}
.lg__toc h2{font-size:.7rem;color:var(--ink-3);font-weight:400;font-variation-settings:'wght' 400;margin-bottom:12px}
.lg__toc ol{counter-reset:toc;display:flex;flex-direction:column;gap:9px}
.lg__toc li{counter-increment:toc;display:flex;gap:9px}
.lg__toc li::before{content:counter(toc,decimal-leading-zero);color:var(--ink-3);flex:none}
.lg__toc a{color:var(--ink-2);transition:color .25s}
.lg__toc a:hover{color:var(--ink)}
/* aria-current is set by the scroll spy, so the visual state and the state read out
   by a screen reader are the same attribute and cannot disagree. */
.lg__toc a[aria-current]{color:var(--accent)}

.lg__body{max-width:70ch}
.lg__body > section{padding-bottom:clamp(30px,4vw,52px)}
.lg__body > section + section{padding-top:clamp(30px,4vw,52px);border-top:1px solid var(--line)}
/* An anchored clause has to clear the fixed nav, or the heading you followed the
   link for lands underneath it. */
.lg__body > section{scroll-margin-top:calc(var(--nav-h) + 28px)}
.lg__body h2{
  display:flex;gap:14px;align-items:baseline;
  font-variation-settings:'wdth' 100,'wght' 700;
  font-size:clamp(1.15rem,2.1vw,1.6rem);line-height:1.1;letter-spacing:-.03em;
  margin-bottom:16px;
}
.lg__n{font-family:var(--mono);font-size:.72rem;color:var(--accent);flex:none;font-variation-settings:'wght' 400}
.lg__body p{font-size:1rem;line-height:1.62;color:var(--ink-2)}
.lg__body p + p{margin-top:14px}
.lg__body ul{margin-top:14px;display:flex;flex-direction:column;gap:9px}
.lg__body ul li{
  position:relative;padding-left:20px;
  font-size:1rem;line-height:1.6;color:var(--ink-2);
}
.lg__body ul li::before{
  content:'';position:absolute;left:2px;top:.62em;
  width:7px;height:1px;background:var(--line-2);
}

.lg__defs{margin-top:16px;display:flex;flex-direction:column;gap:10px}
.lg__defs > div{display:grid;grid-template-columns:minmax(0,11rem) minmax(0,1fr);gap:6px 18px}
.lg__defs dt{font-family:var(--mono);font-size:.72rem;color:var(--ink-3);padding-top:.22em}
.lg__defs dd{font-size:.97rem;line-height:1.55;color:var(--ink-2)}
@media (max-width:620px){.lg__defs > div{grid-template-columns:1fr;gap:2px}}

/* Tables scroll in their own box rather than widening the page. The gradient hint on
   the right edge is the only signal that there is more, since a scrollbar does not
   show until you touch it. */
.lg__tw{
  margin-top:18px;overflow-x:auto;
  border:1px solid var(--line);background:var(--bg-2);
}
.lg__tw:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.lg__tw table{border-collapse:collapse;width:100%;min-width:44rem}
.lg__tw th,.lg__tw td{
  text-align:left;vertical-align:top;padding:12px 14px;
  border-bottom:1px solid var(--line);font-size:.86rem;line-height:1.5;
}
.lg__tw thead th{
  font-family:var(--mono);font-size:.68rem;font-weight:400;font-variation-settings:'wght' 400;
  /* --ink-2, not --ink-3. The token comments give ink-3 as 4.9:1, but that is against
     --bg; on the darker --bg-3 this header sits on it measures 4.07 and misses AA.
     ink-2 clears it and the hierarchy is carried by the mono face and the size. */
  color:var(--ink-2);background:var(--bg-3);white-space:nowrap;
  position:sticky;top:0;                     /* survives the internal scroll */
}
.lg__tw tbody th{font-variation-settings:'wght' 600;color:var(--ink);width:26%}
.lg__tw td{color:var(--ink-2)}
.lg__tw tbody tr:last-child th,.lg__tw tbody tr:last-child td{border-bottom:0}

/* A config value nobody has filled in yet. Loud on purpose: this is the difference
   between an unfinished policy that looks unfinished and one that looks binding. */
.lg__todo{
  background:var(--accent);color:#fff;
  font-family:var(--mono);font-size:.7em;letter-spacing:.06em;text-transform:uppercase;
  padding:2px 6px;border-radius:var(--r);white-space:nowrap;
}

.lg__rel{
  display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1px;
  background:var(--line);border-block:1px solid var(--line);
}
.lg__rela{
  display:flex;justify-content:space-between;align-items:center;gap:16px;
  background:var(--bg);padding:clamp(20px,3vw,32px) clamp(16px,2vw,26px);
  font-variation-settings:'wdth' 100,'wght' 700;
  font-size:clamp(1rem,1.7vw,1.3rem);letter-spacing:-.025em;
  transition:background-color .35s,color .35s;
}
.lg__rela:hover{background:var(--ink);color:var(--bg)}
.lg__rela svg{width:18px;height:18px;flex:none;transition:transform .4s var(--e)}
.lg__rela:hover svg{transform:translateX(4px)}
@media (max-width:620px){.lg__rel{grid-template-columns:1fr}}

/* Under the split, the index becomes a plain list at the top of the document rather
   than a sticky rail eating a third of a phone screen. */
@media (max-width:900px){
  /* minmax(0,1fr), not 1fr. A bare 1fr is minmax(auto,1fr), and an auto minimum is
     min-content — so the track could not shrink below the index's own min-content,
     which is 706px once its auto-fill columns are counted. The legal pages therefore
     overflowed horizontally on every phone. Measured: container forced to 352px, the
     index still 706px wide; with a zero floor it becomes 316px and one column. The
     desktop rule above already gets this right. */
  .lg__grid{grid-template-columns:minmax(0,1fr)}
  .lg__toc{
    position:static;max-height:none;overflow:visible;padding-right:0;
    padding-bottom:26px;border-bottom:1px solid var(--line);
  }
  .lg__toc ol{
    display:grid;grid-template-columns:repeat(auto-fill,minmax(13rem,1fr));
    gap:8px 18px;
  }
}

/* ---------- 22. Reduced motion ---------- */
@media (prefers-reduced-motion:reduce){
  @view-transition{navigation:none}
  /* MorphLink bails out before starting one, but a transition begun just as the
     setting changes must not animate either. */
  ::view-transition-group(*),::view-transition-old(*),::view-transition-new(*){
    animation:none !important;
  }
  *,*::before,*::after{
    animation-duration:.01ms !important;animation-iteration-count:1 !important;
    transition-duration:.01ms !important;scroll-behavior:auto !important;
  }
  .js [data-anim="fade"]{opacity:1;transform:none}
  .js .wi,.js .c{transform:none}
  .js [data-anim="soft"] .sw{opacity:1;transform:none}
  /* The zoom headroom exists so the parallax has room to move inside the frame.
     With no parallax it is just a 10% crop, so drop it. This has to match the
     specificity of `.js [data-anim="img"].is-in img` (0,3,1) or it loses. */
  .js [data-anim="img"] img,
  .js [data-anim="img"].is-in img,
  .pj__img img,.proc__fig img,.studio__fig img,.case__hero img,.case__figs img{scale:1}
  .hero__title.is-kinetic .c{transform:none !important}
  .js [data-anim="img"]{clip-path:none}
  .js [data-anim="img"] img{transform:none}
  /* The travelling mark is switched off, which would leave the hero with an
     empty right half. .markfx is a fixed, body-level layer so it cannot just be
     left visible: it would hang over every section below. Hide it and put a
     static mark inside the hero instead, where the overflow clip contains it and
     no JS is involved. */
  .markfx{display:none}
  .hero__ring{display:none}
  .stmt__t .k{color:var(--ink) !important}
  .mq__t{transform:none}
  .peek{display:none}
  .markfx{transform:none !important}
  .menu{clip-path:none}
  .menu__nav a span > *{transform:none}
  .paper{animation:none}
}
/* The static mark, for the two cases where the travelling one cannot appear.
   Contained by .hero's overflow clip, so unlike .markfx it can never leak over
   the sections below, and it needs no JS.

   Left-hand rest position, matching .markfx: the right half of the hero belongs
   to the image now, and a ghost mark sitting under a photograph is neither
   visible nor clean. */
.hero::after{
  content:'';position:absolute;pointer-events:none;display:none;
  top:50%;left:calc(var(--pad) * -.34);translate:0 -50%;
  aspect-ratio:170 / 205;
  background:url(../img/logo-mark.svg) center / contain no-repeat;
  width:min(380px,29vw);opacity:.055;
}
/* No mobile case any more: the stacked hero image is the visual there, and a
   ghost mark behind it as well would be two half-statements instead of one. */
/* Reduced motion on a wide screen: the travelling mark is off, so this stands in
   at the size and ink it would have landed at. */
@media (prefers-reduced-motion:reduce) and (min-width:901px){
  .hero::after{display:block}
}


/* ---------- 23. Print ---------- */
@media print{
  /* .markfx, not .hero__mark — the mark layer was renamed and print kept
     hiding a class that no longer exists, so the ghosted F printed over the
     first page. */
  .nav,.menu,.paper,.loader,.progress,.peek,.markfx{display:none !important}
  body{background:#fff;color:#000}

  /* The legal pages are the ones people actually print, and a signed set of terms
     is often a printed set of terms. Three things break on paper: the sticky index
     tries to stick, the table clips whatever does not fit its scroll box, and the
     top padding that clears the fixed nav leaves a blank first page. */
  .lg{padding-top:0}
  .lg__grid{display:block;padding-block:0}
  .lg__toc{position:static;max-height:none;overflow:visible;padding:0 0 18px}
  .lg__body{max-width:none}
  .lg__body > section{break-inside:avoid;page-break-inside:avoid}
  .lg__body h2{break-after:avoid;page-break-after:avoid}
  .lg__tw{overflow:visible;border-color:#000}
  .lg__tw table{min-width:0}
  .lg__tw thead th{position:static;background:#fff;white-space:normal}
  .lg__body p,.lg__body ul li,.lg__defs dd,.lg__tw td{color:#000}
  /* A cobalt-on-white marker prints as pale grey and stops being a warning. */
  .lg__todo{background:#000;color:#fff;-webkit-print-color-adjust:exact;print-color-adjust:exact}
  /* Navigation is meaningless on paper; the URL behind each is not. */
  .lg__rel,.ft__up{display:none}
  /* Fixed means printed over page one. And the accent field is a full-bleed ink
     dump nobody asked their printer for. */
  .page{z-index:auto}
  .ft{position:static;background:#fff;color:#000;--ink:#000;--ink-2:#000;--ink-3:#333;--line:#ccc}
  .ft__gap{display:none}
  .lg__toc a[href]::after{content:' (' attr(href) ')';color:#555}
}

/* ---------- 21d. Not found ---------- */
/* Same register as a case study: it is a page of the site, not an error dialog. */
.nf{padding-top:calc(var(--nav-h) + clamp(48px,9vw,120px));padding-bottom:var(--sec)}
/* The constraint goes on the lede, not on the .wrap. .wrap carries
   margin-inline:auto, so putting a max-width on the same element centres the whole
   block — which is how this first went out, indented 500px from the left. Every other
   section in this stylesheet measures its child for the same reason. */
.nf__head{display:flex;flex-direction:column;gap:22px}
.nf__t{max-width:22ch}
.nf__code{
  font-family:var(--mono);font-size:.8rem;letter-spacing:.16em;color:var(--accent);
}
.nf__t{
  font-variation-settings:'opsz' 96,'wdth' 100,'wght' 800;
  font-size:clamp(2.4rem,7vw,5.6rem);line-height:.9;letter-spacing:-.05em;
}
.nf__lede{
  font-variation-settings:'wdth' 100,'wght' 500;
  font-size:clamp(1.05rem,2vw,1.45rem);line-height:1.3;letter-spacing:-.02em;
  color:var(--ink-2);max-width:46ch;
}
.nf__mean{
  font-size:1rem;color:var(--ink-2);
  padding:14px 0;border-block:1px solid var(--line);
}
.nf__mean a{color:var(--ink);text-decoration:underline;text-underline-offset:4px;transition:color .3s}
.nf__mean a:hover{color:var(--accent)}
.nf__cta{display:flex;flex-wrap:wrap;gap:12px;padding-top:8px}

.nf__list{margin-top:clamp(48px,7vw,96px);border-top:1px solid var(--ink);padding-top:26px}
.nf__list h2{font-family:var(--mono);font-size:.76rem;color:var(--ink-3);font-weight:400;font-variation-settings:'wght' 400;margin-bottom:6px}
.nf__list ul{display:flex;flex-direction:column}
.nf__list a{
  display:flex;justify-content:space-between;align-items:baseline;gap:20px;
  padding:16px 0;border-bottom:1px solid var(--line);
  font-variation-settings:'wdth' 100,'wght' 700;
  font-size:clamp(1.1rem,2.4vw,1.6rem);letter-spacing:-.03em;
  transition:color .3s,padding-left .45s var(--e);
}
.nf__list a:hover{color:var(--accent);padding-left:10px}
.nf__list a i{font-family:var(--mono);font-style:normal;font-size:.78rem;color:var(--ink-3);flex:none}

/* ---------- 21e. Index pages — work and services ---------- */
/* Two standalone pages that used to be sections of the homepage. Built as lists rather
   than grids of cards: these are ordered, comparable things where the eye should run
   down one column, and a card grid would say "these are parallel and interchangeable"
   about six projects that are not. */

/* The same clearance every other standalone page uses. The nav is fixed, so a page
   that only pads by a clamp runs its first line underneath it. */
.idxp__head{
  position:relative;
  /* Given a screen rather than a strip. These pages opened straight into a list, so
     they had no moment of arrival at all — the homepage gets a full viewport before
     it asks you to read anything and these got 40% of one. */
  min-height:min(78vh,760px);
  display:flex;flex-direction:column;justify-content:flex-end;
  padding-top:calc(var(--nav-h) + clamp(50px,9vw,120px));
  padding-bottom:clamp(34px,5vw,70px);
  /* clip, not hidden: the mark bleeds off the right edge by design and has to be cut
     there, but hidden would also become a scroll container on the y axis and trap the
     word reveals, which animate out of their own overflow. */
  overflow-x:clip;
}
.idxp__eyebrow{
  font-family:var(--mono);font-size:.76rem;color:var(--ink-3);
  margin-bottom:clamp(14px,2vw,22px);
}
.idxp__h{max-width:13ch;position:relative}

/*
 * The accent phrase rakes out as the header scrolls away.
 *
 * --rake is written on the h1 by motion/rake.js; --d is the word's index, set by
 * reveals.js when it splits the line. Multiplying the two here means one style write
 * per frame drives every word, each travelling a little further than the one before —
 * so the emphasis loosens from the line rather than sliding as a block.
 *
 * On .w, the clipping box, and not on .wi inside it: .wi carries the reveal's own
 * transform and a transition to go with it, and two things writing one property is
 * how an entrance animation ends up fighting a scroll effect.
 */
.idxp__h em .w{
  transform:translate3d(calc(var(--rake,0px) * (var(--d,0) + 1)),0,0);
}
@media (min-width:900px){
  /* Keeps the text clear of the mark rather than trusting max-width to do it: the
     mark is sized in vw and the column in ch, and the two cross at some widths. */
  .idxp__eyebrow,.idxp__h,.idxp__lede{max-width:min(60%,var(--col,58ch))}
  .idxp__h{--col:13ch}
  .idxp__lede{--col:46ch}
}
.idxp__lede{
  margin-top:clamp(18px,2.4vw,30px);
  max-width:58ch;font-size:clamp(1rem,1.5vw,1.18rem);line-height:1.55;color:var(--ink-2);
}

/* The drifting mark. Sits behind the words and never in front of them: it is a
   texture, not an illustration, and at this size any contrast at all would fight the
   headline. --rot, --drift and --fade are written by motion/emblem.js. */
/* Cropped hard by the header's own edge and running off it, at a size where it is a
   composition rather than a watermark. At 0.11 it was invisible against --bg; the
   whole point of putting the mark here was for it to be seen. */
.idxp__emblem{
  position:absolute;top:calc(var(--nav-h) + clamp(10px,2vw,34px));right:calc(var(--pad) * -.15);
  /* Big enough to be a composition, small enough to still read as the mark. At 44vw
     it was cropped past legibility and crossed the headline — a stray line rather
     than a logo, which is worse than no logo. */
  width:clamp(200px,30vw,470px);
  z-index:0;pointer-events:none;
  /* Raised from .22, with the base outline dimmed on its own stroke to compensate —
     so the resting outline lands in the same place it always did while leaving room
     above it for the trace to actually read as brighter. */
  opacity:calc(.58 * var(--fade,1) * var(--ink-in,1));
  /* The lean is added to the drift rather than replacing it: one is scroll, the other
     is the pointer, and they are different things happening to the same object. */
  transform:translate3d(var(--lean-x,0px),calc(var(--drift,0px) + var(--lean-y,0px)),0)
            rotate(var(--rot,0deg));
  transform-origin:50% 50%;
}
.idxp__emblem svg{width:100%;height:auto;overflow:visible}

/*
 * The mark draws itself in.
 *
 * The studio is called BrandForge and the header is the first thing on the page, so
 * the mark being *made* on arrival rather than simply being there is the one gesture
 * that is actually about the name. pathLength="1" on the path normalises the outline
 * to a single unit, so this needs no measurement — 1 is the whole stroke whatever the
 * geometry underneath is.
 *
 * Re-runs on every navigation, because Inertia mounts a fresh SVG each time. That is
 * the intent: arriving at /work should draw it again.
 */
@keyframes emblemDraw{
  from{stroke-dashoffset:var(--len,800)}
  to{stroke-dashoffset:0}
}

/*
 * And it draws at full strength before settling back to being texture.
 *
 * At the resting 0.22 the line being drawn was barely visible — the gesture happened
 * and nobody saw it. Registering the multiplier with @property is what makes it
 * animatable at all: a plain custom property is a string to the animation engine and
 * jumps from one value to the other.
 */
@property --ink-in{syntax:'<number>';inherits:false;initial-value:1}
@keyframes emblemInk{
  from{--ink-in:3.4}
  60%{--ink-in:3.4}
  to{--ink-in:1}
}
/* :not(.mark__trace) because the trace carries its own dash pattern, and this rule
   outranks it on specificity — it was overwriting the lit segment with the whole
   outline, so the light had nothing to be. */
.js .idxp__emblem path:not(.mark__trace){
  /* --len is the outline's measured length, written once by motion/emblem.js. The
     fallback is close enough that a failed measurement still draws rather than
     rendering the mark as a dotted line. */
  stroke-dasharray:var(--len,800);
  stroke-dashoffset:var(--len,800);
  /* Slow, and eased out hard, so the last third of the outline lands almost gently.
     Delayed a beat so it starts after the headline's words have begun. */
  animation:emblemDraw 2.6s var(--e) .25s forwards;
}
/* The fade-back runs on the span, because that is where the opacity lives — and it
   outlasts the draw so the mark holds for a moment at full strength before receding. */
.js .idxp__emblem{animation:emblemInk 4.2s var(--e) .25s forwards}
/*
 * Below the breakpoint the copy runs the full width, so the column rule that keeps the
 * two apart on a desktop does not apply and the mark ends up sitting on the headline.
 * The header reserves 78vh with its content pinned to the bottom, so there is empty
 * room above the eyebrow — the mark goes there, smaller, instead of behind the words.
 */
@media (max-width:899px){
  .idxp__emblem{
    top:calc(var(--nav-h) + 8px);right:-9%;
    width:48vw;
    opacity:calc(.16 * var(--fade,1));
  }
}
.idxp__emblem path:not(.mark__trace){
  fill:none;stroke:var(--accent);stroke-width:1.25;
  /* On the path, not the element: the trace overlays this and needs to sit above it
     rather than share one opacity with it. .58 x .33 is the .19 it rendered at before. */
  stroke-opacity:.33;
  vector-effect:non-scaling-stroke;
}

/*
 * A light running the outline, forever.
 *
 * The draw-on is a one-off — it happens on arrival and then the mark is a shape that
 * turns too slowly to notice. This is the part that keeps going: a short lit segment
 * travelling the same path the outline is made of, on a fourteen-second lap.
 *
 * Long and slow on purpose. A fast chase reads as a loading spinner; at this length it
 * reads as light moving over an object, which is the difference between decoration and
 * something that looks made.
 */
@keyframes emblemTrace{
  from{stroke-dashoffset:var(--len,797)}
  to{stroke-dashoffset:0}
}
.js .idxp__emblem .mark__trace,
.idxp__emblem .mark__trace{
  fill:none;stroke:var(--accent);vector-effect:non-scaling-stroke;
  stroke-opacity:1;
  stroke-width:2.6;
  /*
   * Two lit runs, different lengths, unevenly spaced — one long and one short, so the
   * mark is lit twice per lap at an irregular rhythm rather than ticking past like a
   * spinner. The four values sum to exactly --len, which is what makes the loop
   * seamless: shifting the pattern by one full length returns it to the same state,
   * so there is no jump at the wrap.
   *
   *   lit L/6  ·  dark L/3  ·  lit L/12  ·  dark 5L/12   =   L
   */
  stroke-dasharray:
    calc(var(--len,797) / 6)
    calc(var(--len,797) / 3)
    calc(var(--len,797) / 12)
    calc(var(--len,797) * 5 / 12);
  /* Starts as the draw-on finishes, so the mark is completed and then lit rather than
     both happening over each other. */
  animation:emblemTrace 14s linear 2.6s infinite;
}
/* Above the mark, so the outline can never sit on top of a letterform. */
.idxp__head>:not(.idxp__emblem){position:relative;z-index:1}

/* ---- filters ---- */
.idxp__filters{
  display:flex;flex-wrap:wrap;gap:8px;align-items:center;
  padding-bottom:clamp(20px,3vw,34px);
}
.chip{
  font-family:var(--mono);font-size:.78rem;color:var(--ink-2);
  padding:10px 15px;border:1px solid var(--line);border-radius:var(--r);
  transition:color .3s,border-color .3s,background .3s;
}
.chip:hover{color:var(--ink);border-color:var(--line-2)}
.chip.is-on{background:var(--ink);border-color:var(--ink);color:var(--bg)}
.idxp__count{
  font-family:var(--mono);font-size:.74rem;color:var(--ink-3);
  margin-left:auto;
}
.idxp__empty{
  font-family:var(--mono);font-size:.86rem;color:var(--ink-3);
  padding:clamp(40px,8vw,90px) 0;text-align:center;
}

/* ---- shared row chrome for both lists ---- */
.wl,.sl{border-top:1px solid var(--ink)}
.wl__row,.sl__row{
  position:relative;border-bottom:1px solid var(--line);
  transition:opacity .45s var(--e),padding-left .55s var(--e);
}
.wl.is-hover .wl__row.is-dim,.sl.is-hover .sl__row.is-dim{opacity:.32}
.wl__row:has(.wl__a:hover),.sl__row:has(.sl__a:hover){padding-left:10px}
/* The underline sweeps in from the side the pointer is travelling towards, which is
   the same gesture the homepage index uses. One vocabulary, two pages. */
.wl__row::after,.sl__row::after{
  content:"";position:absolute;left:0;bottom:-1px;height:1px;width:100%;
  background:var(--accent);transform:scaleX(0);transform-origin:right;
  transition:transform .6s var(--e);
}
.wl__row:has(.wl__a:hover)::after,.wl__row:has(.wl__a:focus-visible)::after,
.sl__row:has(.sl__a:hover)::after,.sl__row:has(.sl__a:focus-visible)::after{
  transform:scaleX(1);transform-origin:left;
}
.wl__n,.sl__n{
  font-family:var(--mono);font-size:.74rem;color:var(--ink-3);
  padding-top:.5em;
}
.wl__name,.sl__name{
  display:block;
  font-variation-settings:'opsz' 72,'wdth' 100,'wght' 800;
  /* Was 3rem, which made a list of six names read as a table of contents. On a page
     with no photography the names are the only thing with any scale to give. */
  font-size:clamp(2rem,5.6vw,4.6rem);letter-spacing:-.05em;line-height:.96;
  transition:color .4s var(--e);
}
.wl__a:hover .wl__name,.wl__a:focus-visible .wl__name,
.sl__a:hover .sl__name,.sl__a:focus-visible .sl__name{color:var(--accent)}
.wl__lede,.sl__lede{
  display:block;margin-top:10px;max-width:52ch;
  font-size:.98rem;line-height:1.55;color:var(--ink-2);
}
.wl__k,.sl__k,.wl__y,.sl__p{font-family:var(--mono);font-size:.76rem;color:var(--ink-3)}

/* ---- work list ---- */
.wl__a{
  display:grid;gap:6px 20px;padding:clamp(30px,4.4vw,58px) 0;
  grid-template-columns:auto 1fr;
  grid-template-areas:'n main' '. meta' '. shot';
  align-items:start;
}
.wl__n{grid-area:n}
.wl__main{grid-area:main}
.wl__meta{grid-area:meta;display:flex;flex-wrap:wrap;gap:6px 16px;margin-top:14px}
.wl__shot{grid-area:shot;display:block;margin-top:18px;overflow:hidden;border-radius:var(--r);background:var(--bg-3)}
.wl__shot img{width:100%;height:auto;aspect-ratio:8/5;object-fit:cover}

@media (min-width:900px){
  .wl__a{
    grid-template-columns:auto minmax(0,1fr) auto;
    grid-template-areas:'n main meta';
    gap:0 clamp(20px,3vw,52px);align-items:center;
  }
  .wl__meta{margin-top:0;flex-direction:column;align-items:flex-end;text-align:right;gap:5px}
  /* The pointer already gets a preview trailing behind it up here, so the inline
     thumbnail would be the same picture twice. */
  .wl__shot{display:none}
}

/* ---- services list ---- */
.sl__a{
  display:grid;gap:6px 20px;padding:clamp(30px,4.4vw,58px) 0;
  grid-template-columns:auto 1fr;
  grid-template-areas:'n main' '. meta';
  align-items:start;
}
.sl__n{grid-area:n}
.sl__main{grid-area:main}
.sl__meta{grid-area:meta;display:flex;flex-wrap:wrap;gap:6px 16px;margin-top:14px;align-items:baseline}
.sl__inc{
  display:inline-block;margin-top:16px;
  font-family:var(--mono);font-size:.74rem;color:var(--ink-3);
}
.sl__p{color:var(--accent)}

@media (min-width:900px){
  .sl__a{
    grid-template-columns:auto minmax(0,1fr) minmax(0,22ch);
    grid-template-areas:'n main meta';
    gap:0 clamp(20px,3vw,52px);align-items:start;
  }
  .sl__meta{margin-top:.55em;flex-direction:column;align-items:flex-end;text-align:right;gap:8px}
}

/* ---- the ticker ---- */
/* Continuous by construction: one transform animation, owned by the compositor, so it
   costs nothing per frame and does not care what JavaScript is doing. */
.tick{
  overflow:hidden;background:var(--bg-2);border-block:1px solid var(--line);
  padding-block:clamp(18px,2.6vw,30px);margin-bottom:clamp(30px,5vw,70px);
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 8%,#000 92%,transparent);
  mask-image:linear-gradient(90deg,transparent,#000 8%,#000 92%,transparent);
}
.tick__t{display:flex;width:max-content;animation:tick 46s linear infinite}
.tick__set{display:flex;flex:none}
.tick__i{
  display:flex;align-items:center;gap:clamp(22px,3vw,44px);
  padding-right:clamp(22px,3vw,44px);
  font-variation-settings:'wdth' 100,'wght' 600;
  font-size:clamp(1.1rem,2.2vw,1.7rem);letter-spacing:-.03em;color:var(--ink-2);
  white-space:nowrap;
}
.tick__m{width:clamp(13px,1.4vw,19px);flex:none}
.tick__m svg{width:100%;height:auto}
.tick__m path{fill:var(--accent)}
/* Two identical sets, so the second is exactly where the first was when it wraps. */
@keyframes tick{from{transform:translate3d(0,0,0)}to{transform:translate3d(-50%,0,0)}}
.tick__t:hover{animation-play-state:paused}

/* On /services the ticker is the opening rather than a strip under it: the five
   names at display size, moving, are a better first screen for a page about the
   five things than the same eyebrow-headline-emblem header every index page had. */
.tick--hero{
  margin:var(--nav-h) 0 0;padding-block:clamp(40px,9vh,110px) clamp(30px,6vh,70px);
  background:var(--bg);border-top:0;
}
.tick--hero .tick__t{animation-duration:80s}
.tick--hero .tick__i{
  font-variation-settings:'opsz' 96,'wdth' 100,'wght' 800;
  font-size:clamp(2.6rem,7.6vw,7.6rem);letter-spacing:-.05em;line-height:1;color:var(--ink);
  gap:clamp(24px,3.4vw,54px);padding-right:clamp(24px,3.4vw,54px);
}
.tick--hero .tick__m{width:clamp(28px,3.6vw,56px)}
/* A header that follows an opening rather than being one: no reserved screen. */
.idxp__head--after{min-height:0;padding-top:clamp(20px,4vw,56px)}

/* The studio's opening is the time at the studio, where the other pages put the
   emblem. It is the one continuous detail an about page can honestly carry, and at
   this size it is the composition rather than a footnote to it. Digits arrive after
   mount; the label is there from the first paint. */
.idxp__clock{
  display:none;position:absolute;right:var(--pad);bottom:clamp(34px,5vw,70px);
  flex-direction:column;align-items:flex-end;gap:6px;text-align:right;
  font-family:var(--mono);font-size:.76rem;color:var(--ink-3);
}
@media (min-width:900px){.idxp__clock{display:flex}}
.idxp__clock time{
  display:inline-block;min-width:4ch;
  font-family:var(--font);font-variation-settings:'opsz' 96,'wdth' 100,'wght' 800;
  font-size:clamp(4rem,9vw,9rem);line-height:.86;letter-spacing:-.06em;color:var(--ink);
  font-variant-numeric:tabular-nums;
}

/* ---- closing call to action ---- */
.idxp__cta{
  position:relative;
  padding-block:clamp(70px,11vw,160px);
  margin-top:clamp(50px,8vw,120px);
  background:var(--ink);color:var(--bg);
}
/*
 * Full width because the section is full width, with the measure restored by a .wrap
 * inside it. The first version bled a pseudo-element out with inset:0 -100vw, which
 * spans the viewport visually and also makes the document 2560px wide on a 1280px
 * screen — measured, and the kind of overflow nothing looks wrong about until someone
 * on a phone can swipe the whole page sideways.
 */
.idxp__cta h2{max-width:16ch}
.idxp__cta p{margin-top:20px;max-width:52ch;font-size:1.06rem;line-height:1.6;color:rgba(237,237,239,.68)}
.idxp__cta .btn{margin-top:clamp(26px,3.4vw,40px)}
/* The filled button is ink-on-bg by default, which is invisible on ink. */
.idxp__cta .btn--fill{background:var(--bg);color:var(--ink);border-color:var(--bg)}
.idxp__cta .btn--fill:hover{background:var(--accent);border-color:var(--accent);color:var(--bg)}
.idxp__cta em{color:var(--accent-dk)}

@media (prefers-reduced-motion:reduce){
  /* rake() returns early under reduced motion so --rake is never written, but the
     resting value is stated here too rather than left to a fallback. */
  .idxp__h em .w{transform:none}
  /* The ticker is the only thing here that moves without being scrolled, so it is the
     only thing that has to stop. The mark is handled in JS, where it is parked at a
     fixed angle rather than removed — and its outline is simply drawn, rather than
     drawing itself. */
  .js .idxp__emblem path:not(.mark__trace){animation:none;stroke-dashoffset:0}
  .js .idxp__emblem{animation:none}
  /* Removed rather than frozen: a lit segment parked somewhere on the outline reads as
     a rendering fault, not as a mark with a highlight. */
  .idxp__emblem .mark__trace{display:none}
  .tick__t{animation:none;transform:none}
  .wl__row,.sl__row,.wl__row::after,.sl__row::after,.chip{transition:none}
}

/* The way out of a homepage section and into the page that holds all of it. */
.sec__bar{display:flex;flex-wrap:wrap;align-items:baseline;justify-content:space-between;gap:14px}
.sec__all{
  font-family:var(--mono);font-size:.78rem;color:var(--ink-3);
  padding-bottom:2px;border-bottom:1px solid var(--line);
  transition:color .3s,border-color .3s;white-space:nowrap;
}
.sec__all::after{content:" \2192"}
.sec__all:hover{color:var(--accent);border-color:var(--accent)}
.work__head{display:flex;flex-wrap:wrap;align-items:baseline;justify-content:space-between;gap:14px}

/* ---------- 21f. Studio ---------- */
/* The page where someone decides whether to send the enquiry. Set wider than a case
   study and narrower than a landing page: it is read, not scanned, but it is not a
   document either. */

/* Who "me" is. On the homepage it links to the studio page; on the studio page it is
   already there, so it is just a signature. One class, because two would drift. */
.byline{
  display:flex;flex-wrap:wrap;align-items:baseline;gap:6px 14px;
  margin-top:clamp(16px,2vw,24px);
  font-family:var(--mono);font-size:.82rem;color:var(--ink-3);
}
.byline::before{content:"—";flex:none}
.byline a{
  color:var(--ink);padding-bottom:2px;
  border-bottom:1px solid var(--line-2);transition:color .3s,border-color .3s;
}
.byline a:hover{color:var(--accent);border-color:var(--accent)}
.byline span{font-size:.76rem}
.byline span::before{content:"· "}
.byline--plain::before{content:"— "}

.st__lbl{
  font-family:var(--mono);font-size:.76rem;color:var(--ink-3);
  font-weight:400;font-variation-settings:'wght' 400;
  margin-bottom:clamp(16px,2.4vw,28px);
}

/* ---- what that means in practice ---- */
.st__practice{padding-block:clamp(36px,6vw,80px);border-top:1px solid var(--line)}
.st__practice h2{
  font-family:var(--mono);font-size:.76rem;color:var(--ink-3);
  font-weight:400;font-variation-settings:'wght' 400;margin-bottom:clamp(20px,3vw,34px);
}
.st__practice ul{display:grid;gap:clamp(16px,2.2vw,26px);max-width:64ch}
.st__practice li{
  position:relative;padding-left:26px;
  font-size:clamp(1.02rem,1.7vw,1.24rem);line-height:1.55;color:var(--ink-2);
}
/* A rule rather than a bullet: these are statements, not a shopping list. */
.st__practice li::before{
  content:"";position:absolute;left:0;top:.72em;
  width:14px;height:1px;background:var(--accent);
}

/* ---- how we work ---- */
.st__tenets{
  padding-block:var(--sec);
  background:var(--bg-2);border-block:1px solid var(--line);
}
.st__list{display:grid;gap:clamp(28px,4vw,54px)}
@media (min-width:860px){.st__list{grid-template-columns:repeat(3,minmax(0,1fr));gap:clamp(24px,3vw,48px)}}
.st__list li{position:relative;padding-top:22px;border-top:1px solid var(--line-2)}
.st__n{
  display:block;font-family:var(--mono);font-size:.74rem;color:var(--accent);
  margin-bottom:14px;
}
.st__list h3{
  font-variation-settings:'wdth' 100,'wght' 700;
  font-size:clamp(1.18rem,2.1vw,1.5rem);letter-spacing:-.03em;line-height:1.12;
  margin-bottom:12px;
}
.st__list p{font-size:.99rem;line-height:1.6;color:var(--ink-2);max-width:44ch}

/* ---- where ---- */
.st__where{
  padding-block:var(--sec);
  display:grid;gap:clamp(30px,5vw,64px);align-items:start;
}
@media (min-width:900px){.st__where{grid-template-columns:minmax(0,1fr) minmax(0,.72fr)}}
.st__wbody{max-width:52ch;font-size:clamp(1.02rem,1.6vw,1.2rem);line-height:1.6;color:var(--ink-2)}
.st__clock{
  display:flex;align-items:baseline;gap:12px;
  margin-top:clamp(24px,3.4vw,40px);padding-top:18px;border-top:1px solid var(--line);
  font-family:var(--mono);font-size:.8rem;color:var(--ink-3);
}
.st__clock time{
  font-size:clamp(1.5rem,3.4vw,2.4rem);letter-spacing:-.03em;color:var(--ink);
  font-variation-settings:'wght' 500;
  /* Reserved, so the line does not jump when the digits arrive after mount. */
  min-width:4ch;display:inline-block;
}
.st__fig{overflow:hidden;border-radius:var(--r);background:var(--bg-3);aspect-ratio:4/5}
.st__fig img{width:100%;height:100%;object-fit:cover;scale:var(--img-rest);will-change:transform}

/* ---- the two ways out ---- */
.st__out{
  display:grid;gap:1px;background:var(--line);
  border:1px solid var(--line);border-radius:var(--r);overflow:hidden;
}
@media (min-width:620px){.st__out{grid-template-columns:1fr 1fr}}
.st__card{
  display:flex;flex-direction:column;gap:8px;
  padding:clamp(26px,4vw,44px);background:var(--bg);
  transition:background .35s var(--e),color .35s var(--e);
}
.st__card:hover{background:var(--ink);color:var(--bg)}
.st__ck{
  font-variation-settings:'wdth' 100,'wght' 700;
  font-size:clamp(2rem,5vw,3.4rem);letter-spacing:-.045em;line-height:1;
}
.st__card:hover .st__ck{color:var(--bg)}
.st__cl{font-family:var(--mono);font-size:.8rem;color:var(--ink-3)}
.st__card:hover .st__cl{color:var(--bg)}

/* ---------- 21g. Studio — the small movements ---------- */
/* Everything here rides the reveal the section already does. No new observers, no new
   frame tasks: a transition that starts when .is-in lands costs nothing per frame and
   cannot fall out of step with the block it belongs to. */

/* The accent rule beside each practice line draws itself in, staggered down the list.
   width rather than transform, because a scaled 1px line renders soft. */
.st__practice li::before{width:0;transition:width .7s var(--e);transition-delay:calc(var(--d,0) * 80ms + 120ms)}
.st__practice li.is-in::before{width:14px}

/* Each tenet's rule sweeps in from the left as the card arrives. The border stays put
   as the resting state; this is a second line drawn on top of it in accent. */
.st__list li{position:relative}
.st__list li::before{
  content:"";position:absolute;left:0;top:-1px;height:1px;width:100%;
  background:var(--accent);transform:scaleX(0);transform-origin:left;
  transition:transform .9s var(--e);
}
.st__list li.is-in::before{transform:scaleX(1)}
.st__list li:nth-child(2)::before{transition-delay:.1s}
.st__list li:nth-child(3)::before{transition-delay:.2s}

/* The two counters settle into place rather than appearing at their value. The digits
   are tabular so the box does not jitter as it counts. */
.st__ck{font-variant-numeric:tabular-nums}

/* The clock's colon pulses. It is the smallest possible way of saying this number is
   live rather than printed, and at two seconds it is a heartbeat, not a blink. */
.st__tick{font-style:normal;animation:sttick 2s var(--e-io) infinite}
@keyframes sttick{0%,45%{opacity:1}60%,100%{opacity:.28}}

/* The two cards lift very slightly under the pointer, on top of the fill they already
   take. Enough to feel pressable; not enough to move the layout. */
.st__card{transition:background .35s var(--e),color .35s var(--e),transform .45s var(--e)}
@media (hover:hover){.st__card:hover{transform:translateY(-3px)}}
.st__card:active{transform:translateY(0)}

@media (prefers-reduced-motion:reduce){
  /* The rules are still drawn, just already drawn. The pulse stops outright — a
     repeating animation is the thing this setting is most often asked to stop. */
  .st__practice li::before{width:14px;transition:none}
  .st__list li::before{transform:scaleX(1);transition:none}
  .st__tick{animation:none;opacity:1}
  .st__card{transition:none}
  @media (hover:hover){.st__card:hover{transform:none}}
}

/* ---------- 21h. Index lists — rhythm ---------- */
/*
 * Six names in one flush column read as a table of contents no matter how large the
 * type is: the eye runs straight down the left edge and every row arrives the same
 * way. These break that, and each break has a reason rather than being asymmetry for
 * its own sake.
 *
 * The offset is margin, not transform. transform was the first attempt and it moves
 * the row without resizing it, so the right-aligned meta column was pushed off the
 * container and "Urban transport, Identity, web, 2025" rendered as "Urban tran".
 * Margin makes the grid recompute, which is the entire point. It is also why the
 * offset itself is not animated — that would be a layout pass per frame, and this
 * site holds an 8.3ms budget.
 *
 * Desktop only. Below the breakpoint there is no horizontal room to give away, and an
 * indent eating a third of a phone screen is not rhythm, it is a broken layout.
 */
@media (min-width:900px){

  /* ---- work: a zigzag ---- */
  /* Alternate rows step in, so the eye has to travel to find each name instead of
     falling down a single edge. That is what makes six of them land separately. */
  /* nth-child rather than an index passed from React: the filter removes rows from
     the DOM, so the browser recomputes which are even and the zigzag re-forms on the
     rows that are actually showing instead of keeping gaps where the hidden ones were. */
  .wl__row{--step:0;--off:calc(var(--step) * clamp(24px,7vw,120px))}
  .wl__row:nth-child(even){--step:1}
  .wl__a{margin-left:var(--off);transition:transform .5s var(--e)}

  /* The underline belongs to the row, so it starts where the row does. */
  .wl__row::after{left:var(--off);width:calc(100% - var(--off))}

  /* A small pull toward the reader on hover. Transform, so it is compositor-only and
     cannot reflow — the earlier version squared the row up to the page edge, which
     looked better and cost a layout pass on every frame of the transition. */
  .wl__row:has(.wl__a:hover) .wl__a,
  .wl__row:has(.wl__a:focus-visible) .wl__a{transform:translateX(-10px)}
  .wl__row:has(.wl__a:hover){padding-left:0}

  /* The first row is the one the studio would show first — sort order is a real
     editorial decision, so it gets real weight. :first-child rather than a fixed
     index, so it survives filtering. */
  .wl__row:first-child .wl__name{font-size:clamp(2.6rem,7.4vw,6.2rem)}
  .wl__row:first-child .wl__lede{font-size:1.14rem;max-width:44ch}
  .wl__row:first-child .wl__a{padding-block:clamp(40px,6vw,84px)}

  /* ---- services: a staircase ---- */
  /* These are numbered 01 to 05 and they are also the order a project runs in:
     strategy, identity, build, motion, launch. Each step in is one step through a
     project, so the shape says what the numbers already say. Small per step, because
     five accumulating indents run away from the page fast. */
  .sl__row{--off:calc(var(--i,0) * clamp(8px,1.8vw,30px))}
  .sl__a{margin-left:var(--off);transition:transform .5s var(--e)}
  .sl__row::after{left:var(--off);width:calc(100% - var(--off))}
  .sl__row:has(.sl__a:hover) .sl__a,
  .sl__row:has(.sl__a:focus-visible) .sl__a{transform:translateX(-10px)}
  .sl__row:has(.sl__a:hover){padding-left:0}
}

@media (prefers-reduced-motion:reduce){
  /* The offsets stay — they are layout, not motion. Only the travel stops. */
  .wl__a,.sl__a{transition:none}
  .wl__row:has(.wl__a:hover) .wl__a,.sl__row:has(.sl__a:hover) .sl__a{transform:none}
}

/* ---------- 21i. Contact ---------- */
/* The form beside the answers, not under them: the two things a reader wants here are
   "how do I reach you" and "what happens if I do", and stacking them puts the second
   below the fold on the page whose only job is the first. */
.cp__grid{
  display:grid;gap:clamp(34px,5vw,72px);align-items:start;
  padding-bottom:var(--sec);
}
@media (min-width:900px){.cp__grid{grid-template-columns:minmax(0,.85fr) minmax(0,1fr)}}
.cp__side{display:flex;flex-direction:column;gap:clamp(34px,5vw,64px)}
.cp__answer ol{counter-reset:step;display:grid;gap:16px;max-width:46ch}
.cp__answer li{
  position:relative;padding-left:34px;counter-increment:step;
  font-size:1rem;line-height:1.6;color:var(--ink-2);
}
.cp__answer li::before{
  content:counter(step,decimal-leading-zero);
  position:absolute;left:0;top:.15em;
  font-family:var(--mono);font-size:.74rem;color:var(--accent);
}
