/* brand-voice: skip -- CSS only; `text-transform` trips the prohibited-word check */
:root{
  --paper:#eae6de;
  --paper-rgb:234,230,222;
  --ink:#11100c;
  --ink-2:#18150f;
  --ink-3:#2a251c;
  --rule:#c0b8a4;
  --rule-strong:#8d8674;
  --key-face:linear-gradient(#f7f4ee,#e4dfd2);
  --key-edge:#b3ab97;
  --key-lip:#fffdf8;
  /* The side padding. Everything that has to line up with the content column
     is derived from this rather than repeating the number, so the passage legs
     and their gaps follow it on their own. */
  --gutter:92px;
  /* The shell width. Above this the content stops growing and centres, so on a
     wide monitor this is the number that decides how much margin you see, not
     the gutter. Body copy is capped separately at 62ch, so widening the shell
     widens the full width things (the passage, the services grid, the photos)
     without stretching any line of text past a readable measure. */
  --max:1680px;
}
*{box-sizing:border-box}
/* Page to page transitions.

   The content swipes out, the navigation happens, and the next page is uncovered
   by a wash of paper sliding off it. Driven by swipe.js: native View Transitions
   were here, their CSS was correct, and they never actually ran.

   THE WASH IS A TRANSFORM, NOT A MASK. It used to animate mask-position on the
   whole of main, which is honest but slow: a mask position change repaints the
   entire element every frame, on the main thread, and main is a long page with
   photographs in it. Frames got dropped and it read as stutter rather than as
   ink. What moves now is a sheet of paper, fixed to the viewport, that starts
   covering the content and slides off it. Transform only, so the compositor
   carries it and the page underneath is never repainted.

   It sits at z-index 45, below the header at 50, so the header stays clear and
   holds still while everything under it is uncovered.

   DIRECTION is set by swipe.js from the two pages involved. The four top level
   pages sit side by side in nav order, so moving between them travels sideways.
   Anything underneath one of them is below it, so opening it rises from the
   bottom and coming back drops from the top. Nothing is random, so going back
   always undoes the way you came.

   The ramp is ten stops on an ease curve falling off over most of a screen. A
   three stop gradient banded, and banding across text reads as a moving block. */

/* Geometry per direction.

   The sheet is solid for one viewport, ramps over one, and is clear for one and
   two fifths. It travels two and two fifths.

   The extra four tenths at the end is the point. Before it, the left edge of the
   screen finished clearing at exactly the moment the animation ended, so the tail
   of the easing curve was the last thing you saw, and an ease out decelerating
   into its final value reads as a surge and then a stop. Now the wash is visibly
   over at about 83 per cent of the run and the remaining travel happens beyond
   the edge of the screen, where whatever the curve does cannot be seen.

   The other half of the same problem was the curve itself. An ease out moves
   fast early and slow late, so the right of the screen cleared almost at once
   while the left crawled: the same wash arriving at two different speeds
   depending on where you were looking. Even a flattened curve still varied the
   speed of the front by about two to one across the part you can see.

   So it is linear. That is usually the wrong answer for motion, because a thing
   that starts and stops at full speed looks mechanical. It is the right answer
   here because you never see it start or stop: at nought the ramp is exactly at
   the right hand edge of the screen, and by six tenths it has left the left hand
   edge, so the whole of the visible wash is the middle of the travel. A wipe
   crossing at one steady speed is what a hand pulling a sheet actually does. */
html{
  --sheet-l:0;      --sheet-t:0;
  --sheet-w:340vw;  --sheet-h:100vh;
  --sheet-grad:to right;
  --sheet-x:-240vw; --sheet-y:0;
  --ox:-6%; --oy:0;
}
html[data-dir="r"]{--sheet-l:0;--sheet-t:0;--sheet-w:340vw;--sheet-h:100vh;
  --sheet-grad:to right; --sheet-x:-240vw;--sheet-y:0;  --ox:-6%;--oy:0}
html[data-dir="l"]{--sheet-l:-240vw;--sheet-t:0;--sheet-w:340vw;--sheet-h:100vh;
  --sheet-grad:to left;  --sheet-x:240vw; --sheet-y:0;  --ox:6%; --oy:0}
html[data-dir="b"]{--sheet-l:0;--sheet-t:0;--sheet-w:100vw;--sheet-h:340vh;
  --sheet-grad:to bottom;--sheet-x:0;--sheet-y:-240vh;  --ox:0;--oy:-5%}
html[data-dir="t"]{--sheet-l:0;--sheet-t:-240vh;--sheet-w:100vw;--sheet-h:340vh;
  --sheet-grad:to top;   --sheet-x:0;--sheet-y:240vh;   --ox:0;--oy:5%}

/* leaving */
html[data-leaving] main,
html[data-leaving] footer{animation:swipe-out .24s cubic-bezier(.5,0,.4,1) both}
@keyframes swipe-out{
  from{transform:translate(0,0);                 opacity:1}
  to  {transform:translate(var(--ox),var(--oy)); opacity:0}
}

/* Arriving: the sheet slides off, and the content itself does not move at all.

   It used to settle in by a few percent, which looked right on its own and was
   quietly wrong. The section reveals run on a view() timeline, so their progress
   is computed from where each section sits in the scrollport. A transform on
   main moves every section it contains, so the reveals were being recalculated
   for the whole of the settle and then snapped to their true progress the
   instant the transform hit zero. That snap is the opacity hitch that landed
   right at the end of the transition, and it gets worse the further the content
   travels, not better.

   The sheet is fixed to the viewport and is nobody's ancestor, so it can move as
   much as it likes. The content stays put and is simply uncovered, which is what
   ink on paper does anyway. */

html[data-entering] body::after{
  content:"";position:fixed;pointer-events:none;z-index:45;
  left:var(--sheet-l);  top:var(--sheet-t);
  width:var(--sheet-w); height:var(--sheet-h);
  background:linear-gradient(var(--sheet-grad),
    rgb(var(--paper-rgb)) 0 29.41%,
    rgba(var(--paper-rgb),.97) 32.6%,
    rgba(var(--paper-rgb),.90) 35.6%,
    rgba(var(--paper-rgb),.78) 38.7%,
    rgba(var(--paper-rgb),.61) 41.8%,
    rgba(var(--paper-rgb),.43) 44.9%,
    rgba(var(--paper-rgb),.26) 48.0%,
    rgba(var(--paper-rgb),.13) 51.4%,
    rgba(var(--paper-rgb),.04) 54.8%,
    rgba(var(--paper-rgb),0)   58.82%,
    rgba(var(--paper-rgb),0)   100%);
  animation:wash .72s linear both;
  will-change:transform;
}
@keyframes wash{
  from{transform:translate(0,0)}
  to  {transform:translate(var(--sheet-x),var(--sheet-y))}
}

@media (prefers-reduced-motion:reduce){
  html[data-leaving] main, html[data-leaving] footer,
  html[data-entering] main, html[data-entering] footer{animation:none}
  html[data-entering] body::after{display:none}
}

html{-webkit-text-size-adjust:100%;scroll-behavior:smooth;overflow-x:clip;scroll-padding-top:88px}
@media (prefers-reduced-motion:reduce){ html{scroll-behavior:auto} }
body{
  margin:0;
  background-color:var(--paper);
  background-image:url("/paper-tile.jpg");
  background-size:512px 512px;
  color:var(--ink);
  font-family:Helvetica,"Helvetica Neue",Arial,sans-serif;
  font-synthesis:none;
  -webkit-font-smoothing:antialiased;
  line-height:1.5;
}
.shell{max-width:var(--max);margin:0 auto;padding:0 var(--gutter)}

/* ink */
.ink{text-shadow:0 0 .55px currentColor, 0 0 1.7px rgba(17,16,12,.30)}
.displaced{filter:url(#inkbleed)}
@media (max-width:768px){ .displaced{filter:none} }
@media (prefers-reduced-motion:reduce){ .displaced{filter:none} }

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

/* nav */
/* The header pins so navigation is always one reach away. Its height never
   changes, so pinning it costs no layout shift. The paper ground behind it
   fades in only once the page has moved, keeping the top of the page open. */
header{position:sticky;top:0;z-index:50;
  display:flex;align-items:baseline;justify-content:space-between;padding:48px 0 18px}
/* The paper ground is always painted, so content can never show through the
   pinned bar even if the script never runs. At the top of the page it is
   invisible: same stock, same tile origin as the body behind it. Only the
   hairline is scroll-gated, and losing it is harmless. */
header::before{content:"";position:absolute;top:0;bottom:0;left:calc(50% - 50vw);
  width:100vw;z-index:-1;
  background-color:var(--paper);background-image:url("/paper-tile.jpg");background-size:512px 512px}
/* No line under the header. B: we do not need extra lines everywhere. The
   header still has its own paper behind it, so content passing underneath is
   covered; it just is not underscored while it happens. */
.wordmark{font-weight:700;font-size:15px;letter-spacing:-.02em;text-decoration:none}
nav{display:flex;gap:34px}
nav a{font-weight:700;font-size:10px;letter-spacing:.16em;text-transform:uppercase;text-decoration:none}
nav a:hover,nav a[aria-current]{text-decoration:underline;text-underline-offset:4px}

/* headings */
h1{font-weight:300;font-size:clamp(44px,6.4vw,92px);line-height:1.04;letter-spacing:-.03em;margin:0;text-wrap:balance}
h2{font-weight:300;font-size:clamp(28px,3.4vw,44px);line-height:1.1;letter-spacing:-.03em;margin:0}
.lede{font-weight:400;font-size:clamp(19px,2.3vw,30px);letter-spacing:-.01em;margin:34px 0 0}
.sub{font-size:17px;line-height:1.7;margin:clamp(22px,2.4vw,34px) 0 0;max-width:62ch}
.measure{max-width:64ch}

/* rules + section heads */
/* The way down from the first screen.

   A hairline with a stroke of ink that runs down it and is then drawn off the
   bottom, and an arrow that moves with it. The ink arrives from the top and
   leaves from the top, so it always travels the way it is asking you to go: a
   stroke that grew and then shrank back would read as breathing rather than as
   a direction.

   It is a real link to the section below, so it works when clicked and a
   keyboard can reach it. Under reduced motion the ink simply sits there,
   pointing, which is still the whole message. */
/* flex, not inline-flex: the call to action above it is an inline-block, so an
   inline cue sat down on the same line beside it. */
.scroll-cue{display:flex;flex-direction:column;align-items:center;gap:7px;
  width:18px;margin:clamp(54px,9vh,110px) 0 0;text-decoration:none}
.sc-rail{display:block;width:1px;height:24px;background:var(--rule-strong);
  position:relative;overflow:hidden}
.sc-rail::after{content:"";position:absolute;left:0;top:0;width:1px;height:100%;
  background:var(--ink);transform:scaleY(0);transform-origin:50% 0;
  animation:cue-run 2.6s cubic-bezier(.45,0,.25,1) infinite}
@keyframes cue-run{
  0%   {transform:scaleY(0); transform-origin:50% 0}
  42%  {transform:scaleY(1); transform-origin:50% 0}
  50%  {transform:scaleY(1); transform-origin:50% 100%}
  92%,100%{transform:scaleY(0); transform-origin:50% 100%}
}
.sc-arrow{display:block;width:18px;height:10px;overflow:visible;
  animation:cue-bob 2.6s cubic-bezier(.45,0,.25,1) infinite}
.sc-arrow path{fill:none;stroke:var(--ink);stroke-width:1.4;
  stroke-linecap:round;stroke-linejoin:round;opacity:.5;transition:opacity .28s ease}
.scroll-cue:hover .sc-arrow path,
.scroll-cue:focus-visible .sc-arrow path{opacity:1}
.scroll-cue:focus-visible{outline:2px solid var(--ink);outline-offset:8px}
@keyframes cue-bob{
  0%,100%{transform:translateY(0)}
  42%    {transform:translateY(3px)}
  70%    {transform:translateY(0)}
}
@media (prefers-reduced-motion:reduce){
  .sc-rail::after{animation:none;transform:scaleY(1)}
  .sc-arrow{animation:none}
}

.sechead{display:flex;justify-content:space-between;align-items:baseline;gap:20px;
  padding:26px 0 0;font-weight:700;font-size:10px;letter-spacing:.16em;text-transform:uppercase}
.sechead a{text-decoration:none}
.sechead a:hover{text-decoration:underline;text-underline-offset:4px}
.eyebrow{font-weight:700;font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-3)}

/* One gap between major blocks, everywhere. It used to be three values,
   132 for a section, 104 for the ticker and 110 for the footer, which is three
   answers to one question and reads as drift rather than as rhythm. B: use the
   smallest. 104 was already the tightest of them, so the page closes up rather
   than opening out, which suits a site that has just had every dividing line
   taken out of it. */
section{margin:clamp(54px,6vw,76px) 0 0}
/* The headline is the first thing on a page now the page name above it has
   gone, so the hero owns its spacing here rather than in five generated files. */
/* 190px of paper between the header and the headline was most of a screen on
   a laptop before a word was read. B, 2026-08-23: the spacing on all the
   pages is way too big. A clamp so it breathes on a desktop and does not
   swallow a short screen. */
.hero{margin:0;padding:clamp(72px,8.5vh,132px) 0 0}
.hero h1{margin:0}

/* plates */
.plate-wrap{position:relative;isolation:isolate;display:block}
.plate{display:block;width:100%;height:auto;object-fit:cover;filter:url(#plate)}
.plate-wrap::after{content:"";position:absolute;inset:0;pointer-events:none;
  background-image:url("/paper-tile.jpg");background-size:512px 512px;mix-blend-mode:overlay;opacity:.30}

/* work grid */
.grid{display:grid;grid-template-columns:repeat(2,1fr);gap:118px 40px;margin:44px 0 0;padding:0;list-style:none}
figure{margin:0}
.grid .plate{aspect-ratio:1/.62}
figcaption{margin:22px 0 0;display:grid;grid-template-columns:1fr auto;gap:0 16px}
.title{font-weight:700;font-size:22px;letter-spacing:-.02em}
.year{font-size:14px;color:var(--ink-3);font-variant-numeric:tabular-nums}
.disc{grid-column:1/-1;font-size:14px;color:var(--ink-2);margin:8px 0 0}
.grid a{text-decoration:none;display:block}
.grid a:hover .title{text-decoration:underline;text-underline-offset:5px}

/* plain rows: work index, capabilities, credits */
.rows{list-style:none;margin:0;padding:0}
.rows li{border-bottom:1px solid var(--rule)}
.rows a,.rows .row{display:grid;grid-template-columns:76px 1fr 1fr 110px;gap:24px;align-items:baseline;
  padding:26px 0;text-decoration:none}
.rows .yr{font-size:13px;color:var(--ink-3);font-variant-numeric:tabular-nums}
.rows .nm{font-weight:700;font-size:26px;letter-spacing:-.02em}
.rows .dt{font-size:14px;color:var(--ink-2)}
.rows .tag{font-weight:700;font-size:10px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--ink-3);text-align:right}
.rows a:hover .nm{text-decoration:underline;text-underline-offset:6px}
/* What we are responsible for: the list on the left, the photograph on the
   right, standing with it. B: the photo belongs on the right of the services
   actually listed. Sticky, so it keeps pace with the list instead of scrolling
   off at the first heading. Below 900 the column folds and the photo simply
   sits after the list, where a narrow screen wants it. */
/* The only two lists on the site with no rules of their own, so they came out
   with the browser's discs and a 40px indent while every other list had been
   reset years ago. */
/* One line under each capability, carrying the terms the hero deliberately
   does not. It sits between the name and the bullets. */
/* The service pages: prose sections rather than a bullet list. Two up on a
   wide screen so a page of five does not become a column of five screens. */
.svc-sections{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));
  gap:clamp(34px,4vw,64px) clamp(40px,5vw,88px);margin:36px 0 0}
.svc-sec h2{font-weight:300;font-size:clamp(23px,2vw,30px);letter-spacing:-.02em;
  line-height:1.12;margin:0}
.svc-sec p{font-size:15px;line-height:1.72;color:var(--ink-2);margin:12px 0 0;max-width:52ch}
@media (max-width:900px){ .svc-sections{grid-template-columns:1fr} }

/* Where cleared proof still has to go. Deliberately conspicuous: this must not
   reach enhaus.io unresolved, and the site is noindex until it does. */
.capline{margin:10px 0 0;font-size:15px;line-height:1.65;color:var(--ink-2);max-width:54ch}
.capitems{list-style:none;margin:14px 0 0;padding:0}
.capitems li{font-size:15px;line-height:1.75;color:var(--ink-2)}
.capitems li + li{margin-top:2px}
.wide-items li{font-size:16px}
.capwrap,
/* A journal section that carries a photograph: the read on the left, the
   picture in the column beside it. Same grid as Services, because it is the
   same arrangement and it should stay the same shape at every width. */
.prose.postwrap{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,.8fr);
  gap:clamp(38px,5vw,84px);align-items:start;margin:36px 0 0}
.capgrid{display:grid;gap:clamp(30px,3vw,44px)}
.pagephoto.beside{position:sticky;top:118px;margin:0;width:auto}
/* Both photographs in the column beside the list. Sharing one column is what
   makes them the same size at every width: the arrangement before this set one
   as a fraction of the column and the other as min(52%,600px), which agreed at
   1728 and disagreed everywhere else. Sticky, so they stay with the list while
   it is being read rather than sliding away at the first heading. */
.photostack{position:sticky;top:118px;align-self:start;
  display:grid;gap:clamp(12px,1.4vw,20px)}
.pagephoto.in-stack{display:block;margin:0;width:auto}
@media (max-width:900px){ .photostack{position:static} }
@media (max-width:900px){
  .capwrap,
  .prose.postwrap{grid-template-columns:1fr;gap:40px}
  .pagephoto.beside{position:static}
}
/* And one gap between a section label and whatever it introduces. This was
   38, 44, 44, 36 and 38 across five blocks doing the identical job. Smallest
   again, so 36. */
.caps-list{list-style:none;margin:36px 0 0;padding:0}
.caps-list li{font-weight:300;font-size:26px;letter-spacing:-.02em;padding:13px 0}
.caps-list a{text-decoration:none}
.caps-list a:hover{text-decoration:underline;text-underline-offset:6px}

/* ---------- case study ---------- */
.still{margin:44px 0 0}
.still .plate{aspect-ratio:16/7}
.artist{font-weight:300;font-size:clamp(60px,11vw,150px);letter-spacing:-.04em;line-height:1;margin:0}
.meta{display:grid;grid-template-columns:repeat(4,1fr);gap:32px;margin:44px 0 0;
  padding:34px 0;border-top:2px solid var(--ink);border-bottom:1px solid var(--rule)}
.meta .big{font-weight:300;font-size:clamp(38px,4.6vw,64px);letter-spacing:-.04em;line-height:1}
.meta .lab{margin:12px 0 0}
.meta .note{font-size:14px;color:var(--ink-2);margin:6px 0 0}
.constraint{font-weight:300;font-size:clamp(26px,3.4vw,44px);line-height:1.28;letter-spacing:-.02em;
  margin:38px 0 0;max-width:24ch}
.gallery{display:grid;gap:20px;margin:44px 0 0}
.gallery .wide .plate{aspect-ratio:16/7}
.pair{display:grid;grid-template-columns:1fr 1fr;gap:20px}
.pair .plate{aspect-ratio:3/2}
.trio{display:grid;grid-template-columns:repeat(3,1fr);gap:20px}
.trio .plate{aspect-ratio:3/4}
.cap{font-weight:700;font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);margin:12px 0 0}
.did{list-style:none;margin:44px 0 0;padding:0;border-top:2px solid var(--ink)}
.did li{display:grid;grid-template-columns:56px 1fr 1.1fr;gap:28px;padding:26px 0;border-bottom:1px solid var(--rule)}
.did .n{font-weight:700;font-size:12px;letter-spacing:.1em;color:var(--ink-3)}
.did h2{font-weight:700;font-size:24px;letter-spacing:-.02em;margin:0}
.did p{margin:0;font-size:15px;line-height:1.65;color:var(--ink-2)}

/* the labelled divider: everything below is how it was built */
.divider{margin:150px 0 0;border-top:3px solid var(--ink);padding:34px 0 0;
  display:grid;grid-template-columns:1fr 1.4fr;gap:40px}
.divider .lab{font-weight:700;font-size:13px;letter-spacing:.22em;text-transform:uppercase}
.divider p{margin:0;font-size:16px;line-height:1.6;color:var(--ink-2)}

/* the document surface: a spec sheet on the same stock, tighter and cooler */
.sheet{margin:44px 0 0;padding:56px 60px;background:rgba(255,253,248,.62);
  border:1px solid var(--rule-strong);box-shadow:0 1px 0 rgba(255,255,255,.5) inset}
.sheet .head{font-weight:700;font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-3)}
.sheet h2{margin:14px 0 0}
.notes{list-style:none;margin:34px 0 0;padding:0;border-top:2px solid var(--ink)}
.notes li{display:grid;grid-template-columns:180px 1fr;gap:26px;padding:20px 0;border-bottom:1px solid var(--rule)}
.notes .k{font-weight:700;font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3)}
.notes .v{font-size:15px;line-height:1.68}
.creditgrid{display:grid;grid-template-columns:1fr 1fr;gap:6px 60px;margin:22px 0 0;
  border-top:2px solid var(--ink);padding:22px 0 0}
.creditgrid div{padding:9px 0}
.creditgrid .k{font-weight:700;font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3)}
.creditgrid .v{font-weight:700;font-size:16px;letter-spacing:-.01em}
.creditgrid .tbc{color:var(--ink-3);font-weight:400}

.closing{margin:150px 0 0}
.closing .plate{aspect-ratio:16/6}
.why{font-weight:300;font-size:clamp(26px,3.2vw,42px);line-height:1.3;letter-spacing:-.02em;margin:44px 0 0;max-width:28ch}
.nextup{margin:132px 0 0;border-top:2px solid var(--ink);padding:34px 0 0}
.nextup a{text-decoration:none;display:block}
.nextup .n{font-weight:300;font-size:clamp(38px,5.4vw,76px);letter-spacing:-.04em;line-height:1;margin:14px 0 0}
.nextup a:hover .n{text-decoration:underline;text-underline-offset:8px}

/* forms */
.branches{display:grid;grid-template-columns:repeat(3,1fr);gap:40px;margin:44px 0 0}
/* The three ways in on the contact page. Same three-across idiom as .branches,
   because it is the same kind of choice: parallel routes, none of them the
   default. Each route is a flex column so the CTA can be pushed to the bottom
   with margin-top:auto, which lands all three links on one baseline however
   uneven the paragraphs above them are. */
.routes{display:grid;grid-template-columns:repeat(3,1fr);gap:40px;margin:44px 0 0}
.route{display:flex;flex-direction:column;border-top:2px solid var(--ink);padding:24px 0 0}
.route .n{font-weight:700;font-size:11px;letter-spacing:.18em;color:var(--ink-3);
  display:block;margin:0 0 12px}
.route h2{font-weight:300;font-size:clamp(26px,2.3vw,34px);letter-spacing:-.025em;
  line-height:1.08;margin:0}
.route p{font-size:15px;line-height:1.65;color:var(--ink-2);margin:14px 0 0}
/* The rule under a call to action is drawn, not switched on.

   Two lines sit on top of each other: a resting hairline so the thing reads as
   a link before anybody touches it, and an ink line above it that is scaled to
   nothing until the pointer arrives and then draws itself across. It is the same
   motif as the mark drawing itself and the wash crossing the page, and it is a
   transform, so it costs nothing to run. */
/* Any call to action, not just the ones in the contact lanes. */
.go{display:inline-block;align-self:start;padding-top:26px;padding-bottom:7px;
  position:relative;text-decoration:none;border-bottom:0;
  transition:color .28s ease}
/* in a lane it is pushed to the bottom so all three sit on one line */
.route .go{margin-top:auto}
.go::before,
.go::after{content:"";position:absolute;left:0;right:0;bottom:0;height:1px}
.go::before{background:var(--rule-strong)}
.go::after{background:var(--ink);transform:scaleX(0);transform-origin:0 50%;
  transition:transform .42s cubic-bezier(.2,.8,.2,1)}
.go:hover,.go:focus-visible{color:var(--ink)}
.go:hover::after,.go:focus-visible::after{transform:scaleX(1)}
.go:focus-visible{outline:2px solid var(--ink);outline-offset:5px}

/* the address in the footer of the contact page gets the same treatment */
body.one .footrow .wordmark{position:relative;border-bottom:0;padding-bottom:5px}
body.one .footrow .wordmark::before,
body.one .footrow .wordmark::after{content:"";position:absolute;left:0;right:0;bottom:0;height:1px}
body.one .footrow .wordmark::before{background:var(--rule-strong)}
body.one .footrow .wordmark::after{background:var(--ink);transform:scaleX(0);transform-origin:0 50%;
  transition:transform .42s cubic-bezier(.2,.8,.2,1)}
body.one .footrow .wordmark:hover::after,
body.one .footrow .wordmark:focus-visible::after{transform:scaleX(1)}

@media (prefers-reduced-motion:reduce){
  .go::after, body.one .footrow .wordmark::after{transition:none}
}

/* the note that answers the real objection: nobody wants to compose an email */
/* Sits straight under the headline, because the objection it answers is the
   one somebody has before they have read anything else: not wanting to compose
   an email standing up. One line, no heading, out of the way. */
.prewritten{margin:20px 0 0;font-size:14px;line-height:1.6;color:var(--ink-3);max-width:62ch}

.branch{border-top:2px solid var(--ink);padding:24px 0 0}
.branch h3{font-weight:700;font-size:22px;letter-spacing:-.02em;margin:0}
.branch p{font-size:15px;line-height:1.65;color:var(--ink-2);margin:12px 0 0}
.branch a{font-weight:700;font-size:10px;letter-spacing:.16em;text-transform:uppercase;
  display:inline-block;margin:18px 0 0;text-decoration:none}
.branch a:hover{text-decoration:underline;text-underline-offset:4px}

/* footer */
/* The 120px used to sit under the house mark with nothing after it, which
   read as the page failing to end rather than as breathing room. */
footer{margin:clamp(54px,6vw,76px) 0 0;padding:0 0 40px}
/* The 34px was clearance under a horizontal rule that has since been taken
   out, so it was holding a gap open against nothing. */
.footrow{display:flex;justify-content:space-between;gap:24px;flex-wrap:wrap;padding:0;font-size:13px}
.footrow .left{display:flex;gap:70px}
/* Contact is one screen. B: no scroll, real clean, we know why you are here.

   Nothing is achieved by clipping the page, so nothing is clipped: the shell is
   at least a screen tall and main takes up the slack and centres itself in it.
   On a viewport too short to hold the three lanes it scrolls, which is better
   than cutting a call to action in half to keep a promise about scrollbars.

   The house mark is hidden rather than cut, so the step that copies it across
   from index.html still finds one to update. */
/* svh, not dvh. dvh is the CURRENT viewport, so it changes the moment browser
   chrome hides or shows, and the whole column relays out underneath you. That
   is the footer B saw render with dead space under it and then re render to
   take it away. svh is the SMALLEST the viewport gets, chrome showing, so the
   page is laid out once for the worst case and never moves again. It also
   means one screen really is one screen: with dvh the page fits only while the
   chrome is hidden. 100vh first as a fallback for anything without svh. */
body.one .shell{min-height:100vh;min-height:100svh;display:flex;flex-direction:column}
body.one main{flex:1 0 auto;display:flex;flex-direction:column;justify-content:center}
body.one .hero{padding:0}
body.one section{margin:0}
/* The headline is clamp(44px, 6.4vw, 92px) everywhere else, which is width
   only. On a wide but short screen, a 1440 by 720 laptop, that is a 92px line
   on a 720px page and it is the single thing that pushes contact over one
   screen. Here it answers to whichever of the two is tighter. */
body.one h1{margin:0;font-size:clamp(32px,min(6.4vw,10.4vh),92px)}
body.one .sechead{margin:0 0 4px}
body.one .routes{margin:36px 0 0;gap:44px}
body.one .route p{font-size:14px;line-height:1.62}
body.one footer{margin:0;padding:0 0 30px}
/* The file's "details" are facts, not a section: the address, the city, the
   reach and who we build on. On a page that is one screen they belong on the
   line that is already there rather than in a block that would push it into
   scrolling. The build on line comes back underneath, which is the only part
   that costs any height. */
/* A short screen gives back proportionally, so the promise of one screen holds
   on a laptop and not only on a desktop. */
@media (max-height:820px){
  body.one .routes{margin:clamp(10px,2.2vh,36px) 0 0;gap:clamp(14px,2.6vh,44px)}
  body.one .route{padding:clamp(12px,2vh,24px) 0 0}
  body.one .route p{font-size:13.5px;line-height:1.55;margin:clamp(8px,1.4vh,14px) 0 0}
  body.one footer{padding:0 0 clamp(14px,2.4vh,30px)}
}
/* A phone stacks the three routes, so the same page is three times taller and
   every pixel of furniture counts. Everything here is spacing and type size.
   No copy is cut: what a route says on a phone is what it says on a desktop. */
@media (max-width:560px){
  body.one .routes{gap:15px;margin:14px 0 0}
  body.one .route{padding:11px 0 0;border-top-width:1px}
  body.one .route p{font-size:13px;line-height:1.5;margin:7px 0 0}
  body.one .sechead{margin:0 0 2px}
  body.one .hero p{margin:12px 0 0}
  body.one footer{padding:0 0 12px}
}
/* Below 680 the clamps have already bottomed out, so the last few pixels come
   from the furniture rather than the words. */
@media (max-height:680px){
  body.one .routes{margin:8px 0 0;gap:12px}
  body.one .route{padding:9px 0 0}
  body.one .route p{font-size:13px;line-height:1.5;margin:6px 0 0}
  body.one footer{padding:0 0 10px}
}
body.one .markwrap{display:none}
body.one .footrow .left{display:flex;align-items:baseline;gap:22px;flex-wrap:wrap}
body.one .footrow .wordmark{text-decoration:none}

.haus{display:block;margin:70px 0 0;width:88px;opacity:.34}

@media (max-width:900px){
  :root{--gutter:40px}
  .grid,.pair,.trio,.branches{grid-template-columns:1fr}
  .grid{gap:74px}
  .meta{grid-template-columns:1fr 1fr;gap:26px}
  .rows a,.rows .row{grid-template-columns:1fr;gap:6px;padding:22px 0}
  .rows .tag{text-align:left}
  .did li{grid-template-columns:1fr;gap:10px}
  .divider{grid-template-columns:1fr}
  .sheet{padding:32px 24px}
  .notes li,.creditgrid{grid-template-columns:1fr}
  .hero{padding:120px 0 0}
  section{margin:96px 0 0}
  nav{gap:20px}
}
@media (max-width:560px){
  :root{--gutter:20px}
  /* the header stacks rather than dropping links: a phone still needs the whole site */
  header{padding:18px 0 12px;flex-direction:column;align-items:flex-start;gap:10px}
  nav{gap:0 18px;flex-wrap:wrap}
  nav a{font-size:11px;letter-spacing:.13em;padding:3px 0}
  .hero{padding:90px 0 0}
  /* Stacked, not squeezed. The wordmark and the legal line were sharing one
     row on a phone, which read as a single congested block rather than two
     separate things. */
  .footrow{flex-direction:column;align-items:flex-start;gap:16px}
  .footrow .left{flex-direction:column;gap:5px}
  /* No margin here. Contact is meant to be one screen on a phone too, and
     56px of air above the footer was enough to push it over on its own. The
     spacing comes from the routes instead, which give it back when there is
     room. */
  body.one footer{margin:0}
  .meta{gap:22px}
  .mode{padding:34px 0}
}

/* ---------- the service: three modes, the spine of the page ---------- */
.service{border-top:2px solid var(--ink);margin:44px 0 0}
.mode{display:grid;grid-template-columns:110px 1fr 1.15fr;gap:44px;
  padding:52px 0;border-bottom:1px solid var(--rule);align-items:start}
.mode .n{font-weight:700;font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-3);padding-top:10px}
.mode h3{font-weight:300;font-size:clamp(32px,3.6vw,50px);letter-spacing:-.03em;line-height:1.05;margin:0}
.mode .body{font-size:16px;line-height:1.72}
.mode .body p{margin:0}
.mode .body ul{list-style:none;margin:20px 0 0;padding:0;
  font-weight:700;font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);
  display:flex;flex-wrap:wrap;gap:6px 18px}
.mode .shot{margin:26px 0 0}
.mode .shot .plate{aspect-ratio:16/9}

/* ---------- trust ticker: names, not a logo wall ---------- */
.ticker{margin:clamp(54px,6vw,76px) 0 0;border-top:1px solid var(--rule);border-bottom:1px solid var(--rule);
  padding:20px 0;overflow:hidden;position:relative;
  /* break out of the content column: a ticker has to run edge to edge */
  width:100vw;margin-left:calc(50% - 50vw);margin-right:calc(50% - 50vw)}
.ticker-track{display:flex;align-items:center;gap:0;width:max-content;animation:slide 68s linear infinite}
.ticker-track>*{flex:0 0 auto;margin:0 34px;white-space:nowrap;display:flex;align-items:center;
  min-height:44px}
/* full strength at all times: these are marks, not decoration */
.ticker .tick{text-decoration:none;opacity:1}
.ticker .brk{display:none}
.ticker .tick:focus-visible{outline:2px solid var(--ink);outline-offset:6px}
/* logos are masked to ink so a row of different brands still reads as one line */
/* Every mark carries its own --w and --h, set where it sits in the markup so
   each is sized to its own drawing. One factor here takes the whole row down
   without going near those, and keeps their sizes relative to each other. */
.ticker{--logo-scale:.82}
.ticker .logo{width:calc(var(--w) * var(--logo-scale));
  height:calc(var(--h,26px) * var(--logo-scale));background-color:var(--ink);
  -webkit-mask:var(--src) no-repeat center/contain;mask:var(--src) no-repeat center/contain}
@keyframes slide{from{transform:translateX(0)}to{transform:translateX(-50%)}}
/* Reduce Motion: this stops being a ticker and becomes what it always was, a
   client roster, set as a centred block on the page. Nothing moves and nothing
   is asked of the visitor. Rows centre themselves, so any number of names sits
   evenly and a short last row reads as deliberate rather than orphaned. The
   duplicate set exists only to make the loop seamless, so it goes away. */
@media (prefers-reduced-motion:reduce){
  /* The column count is set explicitly, not left to whatever happens to fit.
     13 is prime, so a width-driven grid always strands one mark on its own
     row. Seven across gives 7+6, five across gives 5+5+3. Both are even.
     The half-pixel trim keeps sub-pixel rounding from wrapping a column. */
  .ticker{padding:46px 0}
  .ticker-track{--cols:7;--colgap:18px;
    animation:none;flex-wrap:wrap;justify-content:center;
    width:auto;max-width:1080px;margin:0 auto;padding:0 20px;
    gap:38px var(--colgap)}
  .ticker-track>*{margin:0;min-width:0;justify-content:center;height:44px;
    flex:0 0 calc((100% - (var(--cols) - 1) * var(--colgap)) / var(--cols) - .5px)}
  .ticker .logo{max-width:100%;max-height:100%}
  .ticker .dup{display:none}
  @media (max-width:900px){
    /* Fourteen marks used to fall into ragged groups of 3, 5, 5 and 1, because
       the .brk spacers force a line break and a phone has room for far fewer
       across than a desktop does. One mark ended up stranded on its own row.

       The breaks go and the whole row is scaled down instead, so the marks pack
       themselves into two rows and each keeps its own width. A fixed column
       grid was the other way to guarantee two rows, and it would have put a
       wide wordmark and a stacked three line mark in identically sized cells,
       which flatters neither. B: fine if they get smaller to fit into 2 rows. */
    .ticker{padding:30px 0;--logo-scale:.34}
    /* The 34px each side is marquee spacing and has no business in a wrapped
       block: fourteen marks were spending over 900px on margins alone, which is
       why they came out three to a row. Gap does the job here. */
    .ticker-track>*{margin:0;min-height:0;height:auto}
    .ticker-track{gap:18px 10px;padding:0 12px;justify-content:center}
    .ticker .brk{display:none}
  }
}

@media (max-width:900px){
  .mode{grid-template-columns:1fr;gap:20px;padding:40px 0}
  .mode .n{padding-top:0}
}

/* ---------- the wipe: sections are uncovered from a side, never dropped in ----------
   clip-path only, plus a short counter-slide. No layout property is ever animated,
   so nothing shifts and CLS stays at zero. Gated on .js so the page is fully
   readable if the script never runs. */
/* Travel is in pixels, not percent. A percentage is of the element's own
   width, so on a wide section it throws the box far past the gutter and the
   page genuinely scrolls sideways during the wipe. Each value here sits
   inside the gutter at that breakpoint: 110, 40 and 20. */
.js [data-reveal]{
  --travel:88px;
  opacity:0;
  will-change:clip-path,transform,opacity;
  transition:clip-path 1.05s cubic-bezier(.16,.84,.28,1),
             transform 1.05s cubic-bezier(.16,.84,.28,1),
             opacity .62s ease-out;
}
.js [data-reveal="left"] {clip-path:inset(0 100% 0 0);transform:translateX(calc(-1 * var(--travel)))}
.js [data-reveal="right"]{clip-path:inset(0 0 0 100%);transform:translateX(var(--travel))}
.js [data-reveal="up"]   {clip-path:inset(34% 0 0 0);transform:translateY(20px)}
.js [data-reveal].in{
  clip-path:inset(0 0 0 0);
  transform:none;
  opacity:1;
  will-change:auto;
}
/* the wipe travels less on a phone, but it still comes from a side */
@media (max-width:900px){ .js [data-reveal]{--travel:30px} }
@media (max-width:560px){ .js [data-reveal]{--travel:15px} }
@media (prefers-reduced-motion:reduce){
  .js [data-reveal]{transition:none;opacity:1;clip-path:none;transform:none}
}

/* ---------- scroll driven: the wipe is tied to scroll position ----------
   Where the browser supports it, the reveal runs on a view() timeline
   instead of a class toggle. It is driven by the compositor, needs no
   script, and the wipe tracks the scroll rather than firing once. The JS
   path below stands down when this is available. */
@supports (animation-timeline:view()){
  @media (prefers-reduced-motion:no-preference){
    .js [data-reveal]{
      opacity:1;transition:none;will-change:auto;
      animation:wipe-up linear both;
      animation-timeline:view();
      /* B: the reveal on Services fires early and looks odd. It did:
         entry 2% is the instant the top edge crosses the fold, so a tall
         section began animating while almost all of it was still below
         the screen and was finished before it was readable. Starting at
         entry 25% means a quarter of the block is genuinely in view
         before anything moves. */
      animation-range:entry 25% entry 65%;
    }
    .js [data-reveal="left"] {animation-name:wipe-left}
    .js [data-reveal="right"]{animation-name:wipe-right}
    .js [data-reveal="up"]   {animation-name:wipe-up}
  }
}
@keyframes wipe-left{
  from{opacity:0;clip-path:inset(0 100% 0 0);transform:translateX(calc(-1 * var(--travel)))}
  to  {opacity:1;clip-path:inset(0 0 0 0);transform:none}
}
@keyframes wipe-right{
  from{opacity:0;clip-path:inset(0 0 0 100%);transform:translateX(var(--travel))}
  to  {opacity:1;clip-path:inset(0 0 0 0);transform:none}
}
@keyframes wipe-up{
  from{opacity:0;clip-path:inset(34% 0 0 0);transform:translateY(20px)}
  to  {opacity:1;clip-path:inset(0 0 0 0);transform:none}
}

   Scroll-linked, so it only ever moves under the reader's own hand. */
@keyframes read-progress{from{transform:scaleX(0)}to{transform:scaleX(1)}}

/* ---------- the album: one stamp per tour ----------
   Typographic stamps, not photographs. We only hold verified frames for some
   of these runs, and putting one tour's photography under another tour's name
   would be a false claim. Type carries the entry; the glass reads the rest. */
.stamps{list-style:none;margin:52px 0 0;padding:0;
  display:grid;grid-template-columns:repeat(3,1fr);grid-auto-rows:1fr;gap:44px 34px}
.stamps>li{margin:0;display:flex}
.stamp{display:block;text-decoration:none;position:relative;width:100%}
.face{display:block;position:relative;height:100%}
.stamp .face{padding:22px 20px 20px;
  background:rgba(255,253,248,.62);box-shadow:0 1px 0 rgba(255,255,255,.5) inset;
  /* perforated edge, punched on all four sides */
  --s:7px;
  -webkit-mask:
    radial-gradient(circle var(--s) at 50% 0,#0000 98%,#000) 0 0/calc(var(--s)*2.6) 100%,
    radial-gradient(circle var(--s) at 50% 100%,#0000 98%,#000) 0 0/calc(var(--s)*2.6) 100%,
    radial-gradient(circle var(--s) at 0 50%,#0000 98%,#000) 0 0/100% calc(var(--s)*2.6),
    radial-gradient(circle var(--s) at 100% 50%,#0000 98%,#000) 0 0/100% calc(var(--s)*2.6);
  -webkit-mask-composite:source-in;
  mask:
    radial-gradient(circle var(--s) at 50% 0,#0000 98%,#000) 0 0/calc(var(--s)*2.6) 100%,
    radial-gradient(circle var(--s) at 50% 100%,#0000 98%,#000) 0 0/calc(var(--s)*2.6) 100%,
    radial-gradient(circle var(--s) at 0 50%,#0000 98%,#000) 0 0/100% calc(var(--s)*2.6),
    radial-gradient(circle var(--s) at 100% 50%,#0000 98%,#000) 0 0/100% calc(var(--s)*2.6);
  mask-composite:intersect}
.stamp .perf{position:absolute;inset:10px;border:1px solid var(--rule);pointer-events:none}
.stamp .denom{position:absolute;top:20px;right:22px;font-weight:700;font-size:11px;
  letter-spacing:.14em;font-variant-numeric:tabular-nums}
.stampshot{margin:0 0 18px;display:block}
.stampshot .plate{aspect-ratio:16/9}
.cname{display:block;font-weight:300;font-size:27px;letter-spacing:-.03em;line-height:1.06;
  margin:14px 0 0;padding-right:52px}
.crole{display:block;font-weight:700;font-size:9px;letter-spacing:.13em;text-transform:uppercase;
  color:var(--ink-3);margin:12px 0 0;line-height:1.7}
/* the small print: set to be read through the glass, not with the naked eye */
.micro{display:block;margin:14px 0 0;font-size:3.4px;line-height:1.62;color:var(--ink-2);
  letter-spacing:.002em;text-align:justify;hyphens:auto}
.stamp[href]:hover .cname{text-decoration:underline;text-underline-offset:6px}
.stamp[aria-disabled] {cursor:default}

/* any card that carries the glass must be its positioning context, or the
   loupe resolves against some distant ancestor and flies off the card */
[data-glass]{position:relative}

/* the glass */
.loupe{position:absolute;width:230px;height:230px;border-radius:50%;overflow:hidden;
  clip-path:circle(50% at 50% 50%);
  pointer-events:none;opacity:0;transition:opacity .16s ease;z-index:40;
  left:0;top:0;transform:translate(-50%,-50%);
  background-color:var(--paper);background-image:url("/paper-tile.jpg");background-size:512px 512px;
  box-shadow:0 0 0 1px var(--rule-strong), 0 0 0 6px rgba(255,253,248,.5),
             0 18px 40px rgba(17,16,12,.22)}
.loupe.on{opacity:1}
.loupe .face{position:absolute;transform-origin:0 0;background:none;box-shadow:none;
  -webkit-mask:none;mask:none}
.loupe .perf{display:none}
@media (prefers-reduced-motion:reduce){ .loupe{transition:none} }
/* no glass without a pointer: touch gets the small print set at a readable size */
@media (hover:none),(pointer:coarse){
  .loupe{display:none}
  .micro{font-size:12px;line-height:1.65;text-align:left}
}
@media (max-width:1100px){ .stamps{grid-template-columns:repeat(2,1fr)} }
@media (max-width:640px){ .stamps{grid-template-columns:1fr;gap:34px} }

/* ---------- the mark sets itself in ink ----------
   The house is decomposed into modules and each one lands on its own slice of
   the scroll, so the mark assembles out of the paper as the footer arrives
   rather than simply appearing. */
.markwrap{display:block;margin:38px 0 0;width:84px;
  view-timeline-name:--markline;view-timeline-axis:block}
.mark{display:block;width:100%;height:auto;overflow:visible}
/* The mark is B's own drawing, traced. It used to be a 56 by 31 grid of rects,
   which is a bitmap: at 120px wide each module was about 2px and it read as
   pixelated, which is what B said. This is the same drawing as vector, so it is
   as sharp as the screen allows at any size. */
.mark .art{fill:var(--ink);opacity:.68}
/* The mark draws itself, with a pen that follows the lines B actually drew.

   The visible artwork is his path, untouched. What moves is a mask: his drawing
   thinned to centrelines, chained into one continuous line, and stroked wide
   enough to cover every part of the drawing. A dash offset run along that line
   uncovers the artwork in the order a hand would lay it down.

   One line, not many. SVG restarts the dash pattern at every subpath, so a mask
   made of 204 separate strokes reveals all 204 at once whatever the offset is.
   Chaining them with travel moves between makes it a single subpath; the travel
   passes over blank paper, where a mask reveals nothing, so it costs nothing.

   The base offset is 0, fully drawn. If the animation never runs, the mark is
   simply there. It must not be possible for a decoration to hide the mark. */
.pen{fill:none;stroke:#fff;stroke-width:8.0;stroke-linecap:round;stroke-linejoin:round;
  stroke-dasharray:1000;stroke-dashoffset:0}
@keyframes pen{
  0%   {stroke-dashoffset:1000}
  48%  {stroke-dashoffset:0}
  74%  {stroke-dashoffset:0}
  100% {stroke-dashoffset:-1000}
}
@media (prefers-reduced-motion:no-preference){
  .pen{animation:pen 7s linear infinite}
}

/* the QR sets itself in ink, same mechanic as the house mark */
.qr{display:block;width:190px;height:190px}
.qr rect{fill:var(--ink)}
.qrwrap{view-timeline-name:--qrline;view-timeline-axis:block}
@supports (animation-timeline:view()){
  @media (prefers-reduced-motion:no-preference){
    .qr rect{opacity:0;transform-box:fill-box;transform-origin:50% 50%;
      animation:ink-set-full linear both;animation-timeline:--qrline;
      animation-range:entry calc(var(--i) * 40%) entry calc(42% + var(--i) * 40%)}
  }
}
@keyframes ink-set-full{
  from{opacity:0;transform:scale(.1) rotate(-16deg)}
  to  {opacity:1;transform:scale(1) rotate(0)}
}

/* ---------- 404: the ink lifts off the page ---------- */
.four-hero{padding-bottom:0}
.fourstage{margin:30px 0 0;max-width:820px}
.four{display:block;width:100%;height:auto;overflow:visible}
.four rect{fill:var(--ink);transform-box:fill-box;transform-origin:50% 50%}
.doors{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(4,1fr)}
.doors a{display:block;text-decoration:none;padding:26px 24px 26px 0}
.doors .dk{display:block;font-weight:300;font-size:22px;letter-spacing:-.02em;line-height:1.12}
.doors .dv{display:block;font-size:13px;line-height:1.58;color:var(--ink-3);margin:10px 0 0}
.doors a:hover .dk{text-decoration:underline;text-underline-offset:6px}

@media (max-width:900px){
  .ygroup{grid-template-columns:1fr;gap:16px}
  .ygroup .yr{font-size:40px}
  .routes{grid-template-columns:1fr;gap:34px}
  .details{grid-template-columns:1fr;gap:40px}
  .doors{grid-template-columns:1fr 1fr}
}
@media (max-width:560px){
  .doors{grid-template-columns:1fr}
  .dets dd{font-size:17px}
}

/* ---------- the souvenir sheet: an annotated frame ----------
   The LANY issue runs double width so the photograph is big enough to be
   read as a document. Notes sit on the image itself, set far too small to
   read with the naked eye. They are catalogue notes about the engagement,
   not callouts claiming to identify objects in the frame. */
.sheetstamp{grid-column:span 2}
.stampshot{position:relative}
.annots{position:absolute;inset:0;pointer-events:none;z-index:2}
/* .plate-wrap is positioned, so notes that sit beside it in the DOM need to
   be lifted or the photograph paints straight over them */
.annot{position:absolute;left:var(--x);top:var(--y);
  display:flex;align-items:flex-start;gap:2px;
  width:33%;max-width:33%;
  transform:translateY(-2px)}
.annot .ai,.annot .at{
  background:rgba(238,234,225,.42);
  color:var(--ink);
  font-size:2.6px;line-height:1.5;letter-spacing:.002em;
  padding:1px 1.6px;
  opacity:0;
  box-shadow:0 0 0 .25px rgba(17,16,12,.18)}
/* under the glass they come up to full strength: the reveal is the point */
.loupe .annot .ai,.loupe .annot .at{
  background:rgba(240,236,228,.94);
  opacity:1;
  box-shadow:0 0 0 .25px rgba(17,16,12,.4)}
.loupe .annot::before{background:rgba(17,16,12,.75);opacity:1}
.annot .ai{font-weight:700;letter-spacing:.06em;flex:0 0 auto}
.annot .at{font-weight:400;text-align:left}
/* a hairline tick, so each note reads as pinned to the frame */
.annot::before{content:"";position:absolute;left:-4px;top:2px;width:3.4px;height:.3px;
  background:rgba(17,16,12,.25);opacity:0}
/* touch has no glass, so the notes would be unreadable clutter */
@media (hover:none),(pointer:coarse){ .annots{display:none} }
@media (max-width:1100px){ .sheetstamp{grid-column:span 2} }
@media (max-width:640px){ .sheetstamp{grid-column:span 1} }

/* ================= INTERNAL: album option review ================= */
.opt{margin:110px 0 0}
.opt-head{display:grid;grid-template-columns:44px 260px 1fr;gap:24px;align-items:baseline;padding:22px 0 0}
.opt-key{font-weight:700;font-size:11px;letter-spacing:.18em}
.opt-name{font-weight:300;font-size:32px;letter-spacing:-.03em;line-height:1}
.opt-note{font-size:14px;line-height:1.6;color:var(--ink-2);max-width:62ch}
.opt-grid{display:grid;grid-template-columns:1.35fr 1fr;gap:40px;margin:40px 0 0;align-items:start}
.noframe{display:flex;align-items:center;justify-content:center;height:100%;
  font-weight:700;font-size:10px;letter-spacing:.16em;color:var(--rule-strong)}

/* --- B. contact sheet --- */
.csheet{position:relative;display:block}
.csheet .face{display:block;background:rgba(28,26,22,.94);padding:0 0 16px;position:relative}
.sprock{display:block;height:13px;
  background:
    repeating-linear-gradient(90deg,
      transparent 0 7px, rgba(238,234,225,.92) 7px 15px, transparent 15px 22px)}
.cs-img{position:relative;display:block;margin:0 10px}
.cs-img .plate{aspect-ratio:16/9}
.cs-blank{height:170px;background:rgba(238,234,225,.06);display:block}
.cs-blank .noframe{color:rgba(238,234,225,.4)}
.china{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}
.china circle{fill:none;stroke:rgba(226,90,54,.85);stroke-width:.8}
.fnum{position:absolute;left:6px;bottom:5px;font-weight:700;font-size:8px;letter-spacing:.14em;
  color:rgba(238,234,225,.85)}
.cs-cap{display:block;padding:14px 12px 0}
.csheet .cname{color:#efe9dd;font-weight:300;font-size:24px;letter-spacing:-.03em;display:block}
.csheet .crole{color:rgba(239,233,221,.62);display:block;margin:8px 0 0}
.csheet .micro{color:rgba(239,233,221,.75);display:block;margin:10px 0 0}
.csheet .noframe{color:rgba(238,234,225,.4)}

/* --- C. drawing sheet --- */
.tblock .face{display:block;border:1px solid var(--ink);padding:7px;background:rgba(255,253,248,.5)}
.tb-area{display:block;border:1px solid var(--rule);padding:7px;
  /* the find stays inside the frame: no text may spill past the photograph */
  overflow:hidden}
.tb-area .plate{aspect-ratio:16/9}
.tb-empty{height:180px;display:block}
.tb-grid{display:grid;grid-template-columns:repeat(4,1fr);margin:7px 0 0;
  border:1px solid var(--ink);border-bottom:0}
.tbf{display:block;padding:8px 10px;border-bottom:1px solid var(--ink);border-right:1px solid var(--rule)}
.tbf:nth-child(4n){border-right:0}
.tbf.wide{grid-column:span 3}
.tbf.full{grid-column:1/-1}
.tbf b{display:block;font-weight:700;font-size:8px;letter-spacing:.16em;color:var(--ink-3)}
.tbf i{display:block;font-style:normal;font-size:14px;margin:4px 0 0;letter-spacing:-.01em}
.tbf i.micro{font-size:3.4px;line-height:1.6}

/* --- D. press proof --- */
.proof .face{position:relative;display:block;padding:26px 24px 20px;background:rgba(255,253,248,.42)}
.reg{position:absolute;width:15px;height:15px;pointer-events:none;
  background:
    linear-gradient(var(--rule-strong),var(--rule-strong)) center/1px 100% no-repeat,
    linear-gradient(var(--rule-strong),var(--rule-strong)) center/100% 1px no-repeat}
.reg::after{content:"";position:absolute;inset:4px;border:1px solid var(--rule-strong);border-radius:50%}
.reg.tl{top:5px;left:5px}.reg.tr{top:5px;right:5px}.reg.bl{bottom:5px;left:5px}.reg.br{bottom:5px;right:5px}
.proof-img{display:block}
.proof-img .plate{aspect-ratio:16/9}
.proof-empty{height:180px;display:block;background:rgba(17,16,12,.04)}
.bars{display:flex;gap:0;margin:8px 0 0;height:7px}
.bars i{flex:1}
.bars i:nth-child(1){background:#11100c}.bars i:nth-child(2){background:#3f3a30}
.bars i:nth-child(3){background:#6e6759}.bars i:nth-child(4){background:#9c9483}
.bars i:nth-child(5){background:#c4bdab}.bars i:nth-child(6){background:#e2dccd}
.proof-cap{display:grid;grid-template-columns:1fr auto;gap:0 16px;margin:16px 0 0}
.proof .cname{font-weight:300;font-size:26px;letter-spacing:-.03em;line-height:1}
.proof .pyear{font-weight:700;font-size:11px;letter-spacing:.16em;text-align:right}
.proof .crole{grid-column:1/-1;font-weight:700;font-size:9px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--ink-3);margin:10px 0 0}
.proof .micro{grid-column:1/-1;margin:10px 0 0}

/* --- E. no card --- */
.bare{list-style:none;margin:40px 0 0;padding:0;border-top:2px solid var(--ink)}
.bare-row{display:grid;grid-template-columns:90px 1.1fr 1.3fr 1fr;gap:24px;align-items:baseline;
  padding:26px 0;border-bottom:1px solid var(--rule);cursor:default}
.byr{font-weight:700;font-size:11px;letter-spacing:.16em;font-variant-numeric:tabular-nums}
.bnm{font-weight:300;font-size:34px;letter-spacing:-.03em;line-height:1}
.brole{font-weight:700;font-size:9px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3)}
.bctx{font-size:14px;color:var(--ink-2)}
.peek{position:fixed;width:290px;pointer-events:none;z-index:65;opacity:0;
  transform:translate(-50%,-50%) scale(.96);transition:opacity .2s ease,transform .35s cubic-bezier(.16,.84,.28,1);
  box-shadow:0 22px 50px rgba(17,16,12,.28)}
.peek.on{opacity:1;transform:translate(-50%,-50%) scale(1)}
.peek img{display:block;width:100%;height:auto}
@media (hover:none),(pointer:coarse){ .peek{display:none} }
@media (prefers-reduced-motion:reduce){ .peek{transition:opacity .2s ease} }

@media (max-width:1000px){
  .opt-head{grid-template-columns:1fr;gap:10px}
  .opt-grid{grid-template-columns:1fr;gap:34px}
  .tb-grid{grid-template-columns:repeat(2,1fr)}
  .tbf:nth-child(4n){border-right:1px solid var(--rule)}
  .tbf:nth-child(2n){border-right:0}
  .tbf.wide{grid-column:span 2}
  .bare-row{grid-template-columns:1fr;gap:8px}
}

/* ---------- drawing-sheet callouts ----------
   Balloons with leaders, the way a detail is referenced on an issued
   drawing. Faint on the sheet, legible under the glass. */
.annots.dwg .annot{width:31%;max-width:31%;gap:3px;align-items:flex-start}
.annots.dwg .annot .ai{
  flex:0 0 auto;width:6px;height:6px;padding:0;border-radius:50%;
  display:flex;align-items:center;justify-content:center;
  font-size:3.1px;font-weight:700;line-height:1;letter-spacing:0;
  background:rgba(240,236,228,.88);color:var(--ink);
  box-shadow:0 0 0 .3px rgba(17,16,12,.7)}
.annots.dwg .annot .at{
  background:rgba(240,236,228,.30);
  box-shadow:none;
  /* flex children do not shrink below content by default, which pushed the
     longer notes past the frame and got them clipped mid-sentence */
  min-width:0;overflow-wrap:anywhere;
  border-bottom:.25px solid rgba(17,16,12,.28)}
.annots.dwg .annot::before{left:-5px;top:3px;width:4.4px;background:rgba(17,16,12,.4);opacity:0}
.loupe .annots.dwg .annot .ai{background:rgba(243,240,233,.97);box-shadow:0 0 0 .3px rgba(17,16,12,.9)}
.loupe .annots.dwg .annot .at{background:rgba(243,240,233,.93);border-bottom-color:rgba(17,16,12,.55)}
.loupe .annots.dwg .annot::before{opacity:1}

/* ---------- F · the drawer ----------
   Sheets stack like folders in a drawer. Each sticks as it reaches the top,
   so everything already passed stays visible as a tab and the catalogue is
   navigable at any length. Sticky works here because html uses overflow-x:
   clip rather than hidden. */
.drawer{list-style:none;margin:44px 0 0;padding:0 0 40vh}
.folder{position:sticky;top:calc(88px + var(--i) * 30px);margin:0 0 26px}
.folder .face{display:block;background:#fbf9f4;/* fully opaque: a folder in a stack must occlude the one behind it */
  border:1px solid var(--ink);
  box-shadow:0 -1px 0 rgba(255,255,255,.7) inset, 0 12px 26px rgba(17,16,12,.10)}
/* the tab: steps across so a stack reads like a real drawer */
.tab{display:flex;align-items:baseline;gap:14px;
  padding:9px 16px;border-bottom:1px solid var(--ink);
  background:rgba(238,234,225,.9)}
.tab b{font-weight:700;font-size:13px;letter-spacing:-.01em}
.tab i{font-style:normal;font-weight:700;font-size:10px;letter-spacing:.16em;
  font-variant-numeric:tabular-nums;color:var(--ink-3)}
.tab em{font-style:normal;font-weight:700;font-size:9px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--ink-3);margin-left:auto}
.dw-body{display:block;padding:12px}
.dw-area{position:relative;display:block;border:1px solid var(--rule);overflow:hidden}
.dw-area .plate{aspect-ratio:21/9}
.dw-empty{height:150px;display:block;background:#f2efe8}
.dw-block{display:grid;grid-template-columns:repeat(4,1fr);margin:12px 0 0;
  border:1px solid var(--ink);border-bottom:0}
.dwf{display:block;padding:8px 11px;border-bottom:1px solid var(--ink);border-right:1px solid var(--rule)}
.dwf:nth-child(4n){border-right:0}
.dwf.wide{grid-column:span 2}
.dwf.wide2{grid-column:span 2;border-right:0}
.dwf.full{grid-column:1/-1}
.dwf b{display:block;font-weight:700;font-size:8px;letter-spacing:.16em;color:var(--ink-3)}
.dwf i{display:block;font-style:normal;font-size:13px;margin:4px 0 0;letter-spacing:-.01em;line-height:1.4}
.dwf i.micro{font-size:3.4px;line-height:1.6}
@media (max-width:900px){
  .folder{position:static;top:auto}
  .dw-block{grid-template-columns:repeat(2,1fr)}
  .dwf:nth-child(4n){border-right:1px solid var(--rule)}
  .dwf:nth-child(2n){border-right:0}
  .dwf.wide,.dwf.wide2{grid-column:span 2}
}

/* the whole sheet is the link where a case study exists */
.dw-link{display:block;text-decoration:none}
.dw-link:hover .tab b{text-decoration:underline;text-underline-offset:4px}
.dwf b{text-transform:uppercase}

/* ---------- the title block printed on the drawing ----------
   Where a sheet has a frame, its whole record is printed onto it at a size
   nobody reads with the naked eye. Client, year, discipline, scope: all of it
   is in the picture, and the glass is the only way to read it.
   Sheets with no frame keep a plain block, because there is nothing to
   print on and the record must still exist. */
/* The glass is a 230px aperture at 3.6x, so it only ever shows about 64px of
   page. A block parked in one corner covered 0.45% of the frame: findable only
   by luck, which is a needle, not an easter egg. The record now sits wherever
   the lens is, clamped inside the frame, so any part of the drawing rewards
   looking. It is still only ever visible through the glass. */
/* The record lives in the DOM on the drawing, so crawlers and screen readers
   reach it, but it is never painted on the frame: the lens carries the visible
   copy. A block printed at one spot covered 0.45% of the photograph and could
   only be found by luck. */
.embed{position:absolute;right:3%;bottom:6%;width:58px;
  display:grid;grid-template-columns:1fr 1fr;gap:0;
  opacity:0;pointer-events:none;z-index:2}
.embed .er{display:block;padding:1.1px 1.5px;
  background:rgba(240,236,228,.30);
  box-shadow:inset 0 0 0 .18px rgba(17,16,12,.3)}
.embed .er.wide{grid-column:1/-1}
.embed b{display:block;font-weight:700;font-size:1.6px;letter-spacing:.09em;
  text-transform:uppercase;color:rgba(17,16,12,.8)}
.embed i{display:block;font-style:normal;font-size:2.7px;line-height:1.4;
  color:var(--ink);margin:.4px 0 0;overflow-wrap:anywhere}
/* full strength only inside the glass */
.loupe .embed{opacity:1}
.loupe .embed .er{background:rgba(243,240,233,.95);
  box-shadow:inset 0 0 0 .25px rgba(17,16,12,.85)}
/* no glass on touch, so the record is set plainly instead */
@media (hover:none),(pointer:coarse){
  .embed{display:none}
  .folder .dw-plain{display:grid}
}
.dw-plain{display:none;grid-template-columns:1fr 1fr;margin:12px 0 0;
  border:1px solid var(--ink);border-bottom:0}
.dw-plain span{display:block;padding:8px 11px;border-bottom:1px solid var(--ink);
  border-right:1px solid var(--rule)}
.dw-plain span:nth-child(2n){border-right:0}
.dw-plain b{display:block;font-weight:700;font-size:8px;letter-spacing:.16em;
  text-transform:uppercase;color:var(--ink-3)}
.dw-plain i{display:block;font-style:normal;font-size:13px;margin:4px 0 0;letter-spacing:-.01em}
/* the block is printed over the frame, so it has to sit above it */
.embed{z-index:2}

/* the record as the lens renders it: centred in the circle, always legible */
.grecord{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  width:172px;display:grid;grid-template-columns:1fr 1fr;gap:0;
  pointer-events:none;z-index:3;
  box-shadow:0 0 0 .5px rgba(17,16,12,.55)}
.grecord .er{display:block;padding:3px 5px;
  background:rgba(243,240,233,.95);
  box-shadow:inset 0 0 0 .5px rgba(17,16,12,.5)}
.grecord .er.wide{grid-column:1/-1}
.grecord b{display:block;font-weight:700;font-size:5px;letter-spacing:.1em;
  text-transform:uppercase;color:rgba(17,16,12,.7)}
.grecord i{display:block;font-style:normal;font-size:8.5px;line-height:1.35;
  color:var(--ink);margin:1px 0 0;letter-spacing:-.01em}



/* ---------- the eraser ----------
   Two frames from the same show, one on top of the other. The cursor rubs a
   hole in the top one and the frame underneath comes through. No magnifier,
   no invented document: both images are real photographs of that engagement.
   Nothing on the sheet says the second frame is there. */
.dw-area.erase{position:relative}
.dw-area.erase .plate-wrap{display:block}
.dw-area.erase .under{position:absolute;inset:0}
.dw-area.erase .under .plate{width:100%;height:auto}
.dw-area.erase .over{position:relative;z-index:1;filter:var(--warp,none)}
.dw-area.erase .over .plate{
  -webkit-mask-image:var(--rub,none);mask-image:var(--rub,none);
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}
@media (hover:none),(pointer:coarse){
  .dw-area.erase .over{filter:none}
  .dw-area.erase .over .plate{-webkit-mask-image:none;mask-image:none}
}
@media (prefers-reduced-motion:reduce){
  .dw-area.erase .over{filter:none}
  .dw-area.erase .over .plate{-webkit-mask-image:none;mask-image:none}
}

/* ---------- scope: the punchline, then the fold ----------
   The first sentence always shows, so it has to earn its place. The rest is
   folded away on its top edge and drops open like a letter coming out of an
   envelope: the height opens the space, the rotation swings the paper down
   into it, and a crease shadow sits along the fold until it lies flat. The
   text is in the document either way, so a crawler, an on page search and a
   reader without JavaScript all get the whole thing. */
.dwf .scope{display:block;perspective:900px}
.dwf .scope-lede{display:block;line-height:1.45}
.dwf .scope-rest{display:block;height:0;overflow:hidden;
  transform-origin:top center;transform:rotateX(-88deg);
  opacity:0;backface-visibility:hidden;
  transition:height .5s cubic-bezier(.2,.82,.24,1),
             transform .5s cubic-bezier(.2,.82,.24,1),
             opacity .3s linear .06s}
.dwf .scope-rest .fold{display:block;position:relative;line-height:1.45;padding:3px 0 0}
/* the crease: darkest while the paper is still standing up, gone once flat */
.dwf .scope-rest .fold::before{content:"";position:absolute;left:0;right:0;top:0;height:14px;
  background:linear-gradient(rgba(17,16,12,.20),rgba(17,16,12,0));
  opacity:1;transition:opacity .5s ease;pointer-events:none}
.dwf .scope.open .scope-rest{transform:rotateX(0);opacity:1}
.dwf .scope.open .scope-rest .fold::before{opacity:0}
.dwf .more{display:inline-block;margin:8px 0 0;padding:0;
  background:none;border:0;border-bottom:1px solid var(--rule-strong);
  font:inherit;font-size:12px;color:var(--ink-3);cursor:pointer;letter-spacing:.01em}
.dwf .more[hidden]{display:none}
.dwf .more:hover{color:var(--ink);border-bottom-color:var(--ink)}
.dwf .more:focus-visible{outline:2px solid var(--ink);outline-offset:3px}
@media (prefers-reduced-motion:reduce){
  .dwf .scope-rest{transition:none;transform:none;opacity:1;height:auto}
  .dwf .scope-rest .fold::before{display:none}
  .dwf .more{display:none}
}

/* the client name carries the link now, not the whole sheet */
.tab b .tablink{text-decoration:none}
.tab b .tablink:hover{text-decoration:underline;text-underline-offset:4px}

/* ---------- the passage ----------
   Design it, build it, run it is a sequence, so it moves sideways rather than
   stacking. Each leg fills the viewport, so you are only ever looking at one
   of them and the next is genuinely out of sight until it arrives. The section
   is tall, the frame inside pins, and the legs are drawn across as you scroll.

   Widths have to agree with the keyframe: track = gutter + 3 legs + 2 gaps,
   which is 300vw - 3g, so the pan ends at -(200vw - 3g). */
/* One measurement, used by the leg width and by the travel, so the two cannot
   disagree. They did: the leg was sized against 100vw while the pin is capped
   at --max by the shell, so on any viewport wider than the shell every leg was
   too wide and the next one bled into view. And the travel was written as a
   separate expression that worked out 110px short of two full steps, which is
   why the third leg never fully arrived. Both now derive from --legw. */
.journey{position:relative;height:620vh;margin:36px 0 0;
  /* A leg is the width of the pin, and the pin is the content column: the
     shell is max-width --max with a --gutter of padding each side, so the
     column is --max less two gutters. This used to say min(100vw, --max),
     which is the shell's outer width and 220px wider than the column it has
     to fit inside. The pin clips what overflows, so every leg was losing its
     right hand 220px, which on the third leg is most of the photograph. */
  --legw:calc(min(100vw, var(--max)) - var(--gutter) * 2);
  /* The gap is two gutters, so a leg is fully clear of the pin before its
     neighbour arrives. Anything less and the previous leg's photograph shows
     as a sliver at the edge. */
  --leggap:calc(var(--gutter) * 2);
  --legstep:calc(var(--legw) + var(--leggap))}
.journey-pin{position:sticky;top:0;height:100vh;overflow:hidden;
  display:flex;align-items:center;
  background-color:var(--paper);background-image:url("/paper-tile.jpg");
  background-size:512px 512px;background-repeat:repeat}
.journey-track{list-style:none;margin:0;padding:0;
  display:flex;gap:var(--leggap);width:max-content;align-items:center}
.leg{flex:0 0 var(--legw);padding:0;
  display:grid;grid-template-columns:minmax(0,.92fr) minmax(0,1.08fr);
  /* start, not centre: the number, the heading and the top edge of the
     photograph sit on one line, and they sit on the same line in all three
     legs, so nothing shifts vertically as the passage moves sideways. */
  gap:clamp(30px,4vw,72px);align-items:start}
.leg-words{min-width:0}
.leg-n{display:block;font-weight:700;font-size:11px;letter-spacing:.18em;
  text-transform:uppercase;color:var(--ink-3)}
.leg h2{font-weight:300;font-size:clamp(40px,4.6vw,68px);letter-spacing:-.03em;
  line-height:1.03;margin:14px 0 0;
  /* Optical alignment. The box edges of the number, the heading and the
     paragraph all sit on 116 exactly, but a D at 68px light carries 5.5px of
     left side bearing where an 0 at 11px carries 0.34px, so the heading read
     as indented against everything above and below it. Measured, not guessed:
     5.51 / 68 is .081em. Expressed in em so it holds as the clamp changes the
     size. Design it, Build it and Run it all start on a flat stem, so one
     value is right for all three. */
  text-indent:-.081em}
.leg p{font-size:17px;line-height:1.72;margin:26px 0 0;max-width:46ch}
.leg ul{list-style:none;margin:24px 0 0;padding:0;display:flex;flex-wrap:wrap;
  gap:7px 20px;font-weight:700;font-size:10px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--ink-3)}
/* ---------- the passage photographs ----------
   Four frames per leg, stacked and crossfading, rather than one still. B asked
   for them to be transitional rather than a highlight: something the eye catches
   changing while it is reading the words beside it, not a thing to stop at.

   The first frame is the floor and never animates, so there is no moment where
   the box is empty and nothing to see while the others load. The other three
   fade in over it on a stagger and fade back out, which means the resting state
   is always a real photograph rather than a gap.

   One aspect ratio for the whole stack, set on the container, so a 16:9 frame
   and a 3:2 frame do not make the layout jump as they swap. */
.leg-shot{display:block;margin:0}
/* The frame clips and holds the ratio. The credit sits outside it, because a
   child of an overflow:hidden box with a fixed ratio is a child that gets cut
   off, which is what happened the first time. */
.shot-frame{display:block;position:relative;aspect-ratio:3/2;overflow:hidden}
.shot-frame picture{position:absolute;inset:0;display:block}
.shot-frame img{width:100%;height:100%;object-fit:cover;display:block}
.shot-frame picture:not(:first-child){opacity:0;animation:legfade 24s linear infinite}
.shot-frame picture:nth-child(2){animation-delay:6s}
.shot-frame picture:nth-child(3){animation-delay:12s}
.shot-frame picture:nth-child(4){animation-delay:18s}
@keyframes legfade{
  0%{opacity:0} 4%{opacity:1} 21%{opacity:1} 25%{opacity:0} 100%{opacity:0}
}
/* Reduced motion gets the first frame and nothing moving. It is a photograph
   either way, so nothing is lost but the change. */
@media (prefers-reduced-motion:reduce){
  .shot-frame picture:not(:first-child){animation:none;opacity:0}
}
/* a rule that draws itself across as you go, so progress is legible */
.journey-rule{position:absolute;left:0;bottom:7vh;height:1px;width:100%;
  background:var(--rule);transform-origin:0 50%;transform:scaleX(0)}

@supports (animation-timeline:view()){
  @media (prefers-reduced-motion:no-preference){
    .journey{view-timeline-name:--passage;view-timeline-axis:block}
    .journey-track{animation:pass linear both;animation-timeline:--passage;
      animation-range:contain 0% contain 100%}
    .journey-rule{animation:draw linear both;animation-timeline:--passage;
      animation-range:contain 0% contain 100%}
    /* The grain travels sideways with the legs rather than downward against
       them. Same timeline, same range, so it cannot drift out of step. The
       legs travel 2880px and the grain travels 720px, a quarter of it, which
       reads as depth rather than as a second surface sliding past. This was first written against view() on the pin itself,
       which never progresses: a sticky element that stays put in the viewport
       has almost no view progress to report. */
    .journey-pin{animation:papershift linear both;animation-timeline:--passage;
      animation-range:contain 0% contain 100%}
  }
}
@keyframes papershift{
  0%,  26% {background-position:0 0}
  46%, 60% {background-position:calc(-0.25 * var(--legstep)) 0}
  80%,100% {background-position:calc(-0.5 * var(--legstep)) 0}
}
@keyframes pass{
  /* Hold, move, hold, move, hold.

     Three goes at this failed for the same reason, which was that all three
     were tuning the wrong number. A leg travels 1672px sideways. What decides
     whether that reads as smooth or as a snap is not the easing curve, it is
     how much SCROLL that 1672px is spread over.

     At 8% of the passage it was about 210px of scroll: content moving eight
     times faster than the wheel. At 15% it was about 510px, still more than
     three times. Easing cannot rescue a ratio like that, it only decides which
     part of the move is the fast part.

     A move is now 32% of the passage and the passage is 620vh, so 1672px of
     travel is spread over roughly 1600px of scroll. About one to one: the legs
     track the wheel instead of outrunning it, which is the whole of the fix.

     The easing is mild on purpose now, near enough to linear. At one to one a
     strong curve would put the middle back above the scroll speed, which is
     the thing being removed. All it does here is take the edge off starting
     and stopping so a leg eases out of rest rather than jumping into motion.

     Scroll snapping was tried and removed: it stops a flick, but stopping is
     the opposite of smooth, and B called it exactly that. */
  0%,  12% {transform:translateX(0);
            animation-timing-function:cubic-bezier(.37,0,.63,1)}
  44%, 56% {transform:translateX(calc(-1 * var(--legstep)));
            animation-timing-function:cubic-bezier(.37,0,.63,1)}
  88%,100% {transform:translateX(calc(-2 * var(--legstep)))}
}
@keyframes draw{from{transform:scaleX(0)}to{transform:scaleX(1)}}

/* without a scroll timeline, or with reduced motion, it simply stacks */
@supports not (animation-timeline:view()){
  .journey{height:auto}
  .journey-pin{position:static;height:auto;display:block;overflow:visible}
  .journey-track{flex-direction:column;width:auto;padding-left:0;gap:0}
  .leg{flex:none;padding:0 0 74px;grid-template-columns:1fr;gap:26px}
  .journey-rule{display:none}
}
@media (prefers-reduced-motion:reduce){
  .journey{height:auto}
  .journey-pin{position:static;height:auto;display:block;overflow:visible}
  .journey-track{flex-direction:column;width:auto;padding-left:0;gap:0}
  .leg{flex:none;padding:0 0 74px;grid-template-columns:1fr;gap:26px}
  .journey-rule{display:none}
}
@media (max-width:760px){
  /* A phone keeps the sideways travel. Stacking it turned Design, Build and
     Run into three more things to scroll past, which is not what the sequence
     is for. What changes is the inside of a leg: a 350px column cannot hold a
     heading and a photograph beside each other, so the words sit above the
     picture and the whole leg centres itself in the pin. */
  .leg{grid-template-columns:1fr;gap:18px;align-items:center;align-content:center}
  .leg h2{font-size:clamp(34px,10vw,52px);margin:8px 0 0}
  .leg p{font-size:15px;line-height:1.6;margin:12px 0 0;max-width:none}
  .leg .credit{margin:8px 0 0}
}

/* ---------- the enquiry form ----------
   No boxes. Nothing else on this site has a filled input, a chevron or a
   dropdown, so a form built out of them reads as UI dropped onto the page.
   Hairlines on paper, the answer set at reading size, and four things to
   fill in rather than seven. */
.enq{margin:52px 0 0;max-width:860px}

/* the one that matters, set like body copy so typing feels like writing */
.q{display:block;margin:0 0 54px}
.q-label{display:block;font-weight:300;font-size:clamp(26px,3vw,38px);
  letter-spacing:-.02em;line-height:1.15;margin:0 0 18px}
.q textarea{
  display:block;width:100%;box-sizing:border-box;
  font:inherit;font-size:clamp(17px,1.5vw,21px);line-height:1.7;color:var(--ink);
  background:none;border:0;border-bottom:1px solid var(--rule-strong);
  border-radius:0;padding:4px 0 12px;resize:none;overflow:hidden;
  min-height:calc(2 * 1.7em)}

/* the rest: a label above a rule, nothing more */
.pair{display:grid;grid-template-columns:1fr 1fr;gap:0 46px}
.l{display:block;margin:0 0 40px}
.l>span,.q-label+*{}
.l>span{display:block;font-weight:700;font-size:10px;letter-spacing:.16em;
  text-transform:uppercase;color:var(--ink-3);margin:0 0 8px}
.l input{
  display:block;width:100%;box-sizing:border-box;
  font:inherit;font-size:17px;color:var(--ink);
  background:none;border:0;border-bottom:1px solid var(--rule-strong);
  border-radius:0;padding:4px 0 10px}
.q textarea::placeholder,.l input::placeholder{color:var(--rule-strong)}
.q textarea:focus,.l input:focus{outline:none;
  border-bottom-color:var(--ink);box-shadow:0 2px 0 0 var(--ink)}
.q textarea:focus-visible,.l input:focus-visible{outline:none}
/* The skip link: off screen until it is focused, which is the only time it exists. */
.skip{position:absolute;left:-9999px;top:0;z-index:100;
  background:var(--paper);color:var(--ink);border:2px solid var(--ink);
  padding:12px 18px;font-weight:700;font-size:12px;letter-spacing:.14em;
  text-transform:uppercase;text-decoration:none}
.skip:focus{left:16px;top:16px}
.f-invalid input,.f-invalid textarea{border-bottom-color:#8c3a22}

/* the send is a line of type with a rule under it, like every other action */
.f-actions{display:flex;align-items:baseline;gap:26px;flex-wrap:wrap;margin:6px 0 0}
/* the send is a key, like every other thing you press */
.send{--h:5px}
.f-note{font-size:13px;color:var(--ink-3);margin:0}
.f-status{margin:22px 0 0;font-size:14px;min-height:1.2em}
.f-status.err{color:#8c3a22}
@media (max-width:760px){ .pair{grid-template-columns:1fr;gap:0} }

/* ---------- the key ----------
   A push button built the way a letterpress key is: a face, an edge with real
   depth, and a lip catching the light. Pressing it travels the full height of
   the edge, so it goes down rather than changing colour. */
/* ---------- the key ----------
   B: "i liked the example of a button i gave you only for what it looks like
   when pressed down. it should reveal you are pushing down a button but when
   not you cant tell its a button."

   So at rest it is a line of type on a rule and nothing else: no face, no lip,
   no shadow, nothing announcing itself as a control. The depth exists only
   while it is being pressed. The surface drops and the wall it drops into
   appears above it, which is the whole trick: you find out it was a button by
   pushing it. */
.key{
  --h:4px;
  position:relative;display:inline-flex;align-items:center;gap:9px;
  font:inherit;font-weight:700;font-size:10px;letter-spacing:.16em;
  text-transform:uppercase;color:var(--ink);text-decoration:none;
  background:transparent;border:0;border-bottom:1px solid var(--ink);
  border-radius:0;padding:10px 2px;cursor:pointer;
  box-shadow:none;transform:translateY(0);
  transition:transform .07s ease, box-shadow .07s ease, background-color .16s ease;
}
.key:hover{ background-color:rgba(17,16,12,.045) }
.key:active,.key[aria-pressed="true"]:active{
  transform:translateY(var(--h));
  box-shadow:0 calc(-1 * var(--h)) 0 0 var(--key-edge),
             0 calc(-1 * var(--h)) 9px rgba(17,16,12,.20),
             0 1px 0 rgba(17,16,12,.10) inset;
  background-color:rgba(17,16,12,.06);
}
@media (prefers-reduced-motion:reduce){ .key{transition:none} }
.key:focus-visible{outline:2px solid var(--ink);outline-offset:4px}
@media (prefers-reduced-motion:reduce){ .key{transition:none} }

/* ---------- the first load ----------
   The page sets rather than appears: it comes up from a fraction pressed and
   releases into place, the same motion as a key coming back up. Runs once, on
   first paint, and never again on a page you have already seen this session. */
.js[data-first] body{--set:1}
@media (prefers-reduced-motion:no-preference){
  .js[data-first] .shell>header,
  .js[data-first] .shell>section:first-of-type{
    animation:set-in .62s cubic-bezier(.16,.86,.28,1) both}
  .js[data-first] .shell>section:first-of-type{animation-delay:.06s}
}
@keyframes set-in{
  from{opacity:0;transform:translateY(9px) scale(.995)}
  to  {opacity:1;transform:none}
}

/* ---------- the closing action ----------
   Every page now ends by offering somewhere to go. Home sends you to contact,
   capabilities sends you to contact, contact sends you back to capabilities
   for anyone not ready to write yet. Nothing dead ends into the footer. */
.closer{display:grid;grid-template-columns:1fr auto;gap:40px;align-items:end;
  padding:44px 0 0}
.closer h2{font-weight:300;font-size:clamp(30px,3.6vw,46px);letter-spacing:-.03em;
  line-height:1.1;margin:0}
.closer .sub{margin:22px 0 0;max-width:52ch}
/* the key works as a link as well as a button */
a.key{text-decoration:none}
@media (max-width:760px){
  .closer{grid-template-columns:1fr;gap:28px;align-items:start}
}

/* ---------- about: the through line ----------
   Four years read as one argument, so they are set as a sequence with the year
   holding the left column, the same shape as the credits ledger. */
.throughline{list-style:none;margin:44px 0 0;padding:0;border-top:2px solid var(--ink)}
.throughline li{display:grid;grid-template-columns:150px 1fr;gap:0 34px;
  padding:26px 0;border-bottom:1px solid var(--rule)}
.tl-y{grid-row:span 2;font-weight:300;font-size:40px;letter-spacing:-.04em;line-height:.9}
.tl-t{font-weight:700;font-size:20px;letter-spacing:-.02em}
.tl-b{font-size:15px;line-height:1.66;color:var(--ink-2);margin:8px 0 0;max-width:62ch}

/* what we believe: three, because there are three */
.beliefs{display:grid;grid-template-columns:repeat(3,1fr);gap:40px;margin:36px 0 0}
.beliefs h2{font-weight:300;font-size:clamp(22px,2.3vw,28px);letter-spacing:-.02em;
  line-height:1.14;margin:0}
.beliefs p{font-size:15px;line-height:1.66;color:var(--ink-2);margin:14px 0 0}

/* the facts, as a plain record */
.facts{margin:44px 0 0;padding:0}
.facts>div{display:grid;grid-template-columns:170px 1fr;gap:0 34px;
  padding:0 0 18px;margin:0 0 18px;border-bottom:1px solid var(--rule)}
.facts dt{font-weight:700;font-size:10px;letter-spacing:.16em;text-transform:uppercase;
  color:var(--ink-3);padding-top:5px}
.facts dd{margin:0;font-size:18px;letter-spacing:-.01em;line-height:1.5}
.facts dd a{text-decoration:none;border-bottom:1px solid var(--rule-strong)}
@media (max-width:900px){
  .throughline li{grid-template-columns:1fr;gap:10px}
  .tl-y{grid-row:auto;font-size:32px}
  .beliefs{grid-template-columns:1fr;gap:34px}
  .facts>div{grid-template-columns:1fr;gap:6px}
}

/* ---------- journal ---------- */
.jlist{list-style:none;margin:44px 0 0;padding:0;border-top:2px solid var(--ink)}
.jlist li{border-bottom:1px solid var(--rule)}
.jlist a{display:grid;grid-template-columns:150px 1fr 80px;gap:6px 34px;
  padding:28px 0;text-decoration:none;align-items:baseline}
.jl-date{font-weight:700;font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-3)}
.jl-title{font-weight:300;font-size:clamp(22px,2.4vw,30px);letter-spacing:-.02em;line-height:1.14}
.jl-read{font-weight:700;font-size:10px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--ink-3);text-align:right}
.jl-sum{grid-column:2;font-size:15px;line-height:1.66;color:var(--ink-2);max-width:62ch;margin:4px 0 0}
.jlist a:hover .jl-title{text-decoration:underline;text-underline-offset:6px}

/* the reading column: one measure, nothing else competing */
.prose{max-width:none}
.prose h2{font-weight:300;font-size:clamp(24px,2.8vw,34px);letter-spacing:-.02em;
  line-height:1.16;margin:56px 0 0;max-width:24ch}
.prose h3{font-weight:700;font-size:19px;letter-spacing:-.01em;margin:40px 0 0}
.prose p{font-size:17px;line-height:1.75;margin:22px 0 0;max-width:66ch}
.prose h2+p,.prose h3+p{margin-top:18px}
.prose .pull{margin:48px 0 0;padding:0 0 0 26px;border-left:2px solid var(--ink);
  font-weight:300;font-size:clamp(21px,2.3vw,27px);line-height:1.38;letter-spacing:-.02em;
  max-width:30ch}
.topics{font-weight:700;font-size:10px;letter-spacing:.16em;text-transform:uppercase;
  color:var(--ink-3);margin:26px 0 0}
@media (max-width:900px){
  .jlist a{grid-template-columns:1fr;gap:8px}
  .jl-read{text-align:left}
  .jl-sum{grid-column:1}
}

/* ---------- photographs in the run of the page ----------
   Not a gallery and not a section. A frame sits where it happens to help what
   the page is already saying, and a quiet line underneath says who and where.
   Nothing announces it, nothing links away from it, and there is no heading
   over it: a reader meets the work while reading rather than being taken off
   to look at it.

   The caption is deliberately smaller than body text and set in the same
   micro label as the drawing sheet furniture, so it reads as a note on the
   photograph rather than as another thing to read. */
.frame{display:block;margin:clamp(46px,6vw,84px) 0}
/* No #plate filter here. That filter ends in a gaussian blur, which is fine on
   a 2560px case study plate and visible softness on a 1220px source being asked
   to fill a 1220px column: at 2x the image is already rendering at 1x, and the
   blur compounds it. These frames get the paper grain from the overlay below
   and nothing that touches the pixels. */
.frame img{display:block;width:100%;height:auto}
.frame .plate-wrap::after{content:"";position:absolute;inset:0;pointer-events:none;
  background-image:url("/paper-tile.jpg");background-size:512px 512px;
  mix-blend-mode:overlay;opacity:.18}
.frame .plate-wrap{position:relative;isolation:isolate;display:block}
.frame figcaption{display:flex;align-items:baseline;gap:9px;margin:14px 0 0;
  font-size:11px;letter-spacing:.02em;color:var(--ink-3)}
.frame figcaption b{font-weight:700;letter-spacing:.06em;color:var(--ink);
  text-transform:none}
.frame figcaption i{font-style:normal;color:var(--rule-strong)}
@media (max-width:560px){ .frame{margin:38px 0} }

/* ---------- photographer credit ----------
   B: "photographers are not us, we need to credit the photographers on their
   photos." So every photograph carries the name of whoever took it, set small
   and quiet enough that it reads as a plate on the frame rather than as copy,
   and never so small it cannot be read. It sits with the photograph, not in a
   list at the bottom of the page, because a credit somebody has to go looking
   for is not really a credit. */
.credit{display:block;margin:11px 0 0;font-weight:700;font-size:10px;
  letter-spacing:.14em;text-transform:uppercase;color:var(--rule-strong)}
.frame figcaption .credit{display:inline;margin:0 0 0 auto;padding-left:14px}

/* ---------- questions, as folders ----------
   B: give these the same folder style as the work sheets, opening like a file on
   hover, or on a tap on mobile.

   Built on <details>, so it opens on click and on Enter with no script at all,
   it is announced as a disclosure, and the answer sits in the DOM whether it is
   open or shut. That last part is the one that matters: the FAQ schema and a
   crawler both need to see the answer, and content that only exists after a
   hover is content that never gets indexed. Hovering to open is layered on top
   in polish.js for pointer devices only. */
.qfolders{display:block;margin:36px 0 0;border-top:2px solid var(--ink)}
.qfolder{display:block;border-bottom:1px solid var(--rule);background:transparent;
  transition:background .22s ease}
.qfolder summary{display:flex;align-items:baseline;gap:16px;list-style:none;
  padding:19px 16px 19px 0;cursor:pointer;position:relative}
/* No numbers, so the question and its answer sit on one left edge and there is
   nothing to align to but each other. */
.qfolder summary::-webkit-details-marker{display:none}
.qq{font-weight:600;font-size:17px;letter-spacing:-.01em;color:var(--ink);flex:1 1 auto}
/* the tab mark: a rule that becomes a corner as the folder opens */
.qmark{flex:0 0 14px;height:14px;position:relative;margin-top:4px}
.qmark::before,.qmark::after{content:"";position:absolute;background:var(--ink-3);
  transition:transform .26s cubic-bezier(.2,.82,.24,1),opacity .2s ease}
.qmark::before{left:0;top:6px;width:14px;height:1.5px}
.qmark::after{left:6px;top:0;width:1.5px;height:14px}
.qfolder[open] .qmark::after{transform:scaleY(0);opacity:0}
.qfolder[open]{background:var(--sheet,#f4f1ea)}
.qfolder:hover .qq,.qfolder[open] .qq{color:var(--ink)}
.qfolder:hover{background:rgba(244,241,234,.6)}
/* the answer slides out from under the tab rather than appearing */
.qa{overflow:hidden;padding:0 16px 22px 0}
/* 66ch is the right measure for a paragraph and the wrong one for an answer
   that is meant to be a single line: it capped the column at 551px, which is 77
   characters, so almost every answer wrapped into a block. There is no return
   sweep to protect on one line, so the answer runs the width of its folder. */
.qa p{margin:0;font-size:15px;line-height:1.72;color:var(--ink-2);max-width:none}
.qfolder[open] .qa{animation:qopen .3s cubic-bezier(.2,.82,.24,1) both}
@keyframes qopen{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){
  .qfolder[open] .qa{animation:none}
  .qmark::before,.qmark::after{transition:none}
}
@media (max-width:560px){
  .qq{font-size:16px}
  .qfolder summary{padding:16px 8px 16px 0;gap:12px}
}
/* The handle is a link out to the photographer's own profile. B: "it would be
   awesome if someone clicks the name and it links to that instagram profile."
   Underlined on hover only, so a page of photographs does not become a page of
   links, and it carries noopener because it opens in a new tab. */
/* A photograph in the body of a page. Same 3:2 frame and same quiet credit
   as the passage, so the work turns up while somebody reads rather than being
   presented to them. .right keeps it to a column, which is where the cart sits
   on Services; it goes full width on a narrow screen like everything else. */
.pagephoto{display:block;margin:44px 0 0}
.pagephoto .pf{display:block;position:relative;aspect-ratio:3/2;overflow:hidden}
.pagephoto .pf picture{position:absolute;inset:0;display:block}
.pagephoto .pf img{width:100%;height:100%;object-fit:cover;display:block}
.pagephoto.right{width:min(52%,600px);margin-left:auto}
/* 2.39:1, the cinema ratio, full width of the column and sitting under the
   headline. Every other photograph on the site is 3:2; this one is deliberately
   not, because the page is about cinema and the shape says so before the copy
   does. The section margin comes off: it belongs to the headline above it. */
/* The photograph belongs to the headline above it, so the section it sits in
   gives up the standard 104px and takes a tighter gap. Without this the margin
   on the section wins and the two read as separate blocks. */
section:has(> .pagephoto.wide){margin-top:clamp(26px,3.4vw,44px)}
.pagephoto.wide{margin:0}
.pagephoto.wide .pf{aspect-ratio:3.5}
/* 2.39 was 623px tall on a 1440 screen, which read as a picture rather than a
   break, and it forced a 50% upscale of the source. A band is shorter, sharper
   and does the job B asked of it: break the page and give it colour. */
@media (max-width:700px){ .pagephoto.wide .pf{aspect-ratio:2.4} }
@media (max-width:900px){ .pagephoto.right{width:auto;margin-left:0} }

/* Two credits, one at each end of the photograph. */
.credit.split{display:flex;justify-content:space-between;align-items:baseline;gap:20px}
@media (max-width:560px){ .credit.split{flex-direction:column;gap:3px} }
.credit a{color:inherit;text-decoration:none;border-bottom:1px solid transparent}
.credit a:hover{color:var(--ink);border-bottom-color:var(--ink)}
.credit a:focus-visible{outline:2px solid var(--ink);outline-offset:3px}

/* ---------------------------------------------------- the arrival: a fade ----
   The cover is painted by CSS, not by script, so the page is never seen before
   it lifts. html.arriving is set by a tiny inline script in the head the moment
   the document begins, and only when this session has not arrived before, so a
   returning reader never sees a flash of it.

   The failsafe matters more than the fade: if doorway.js never loads, the
   keyframe below takes the cover away on its own. A loader that can trap a
   reader behind a blank sheet is worse than no loader.

   B, after several passes at the house and the door: make it a nice fade in.
   The whole loader is this now. The door survives at the foot of the home page,
   where it is a drawing at rest rather than an animation that has to land. */
#doorway{position:fixed;inset:0;z-index:120;display:none;
  background-color:var(--paper);background-image:url("/paper-tile.jpg");
  background-size:512px 512px;
  animation:doorwayFailsafe 0s linear 2.6s forwards}
html.arriving #doorway{display:block}
@keyframes doorwayFailsafe{to{display:none;visibility:hidden}}
/* Held a beat so the fonts and the hero photograph have landed, then away.
   Ease out rather than linear: paper lifting slows as it goes. */
#doorway.lifting{animation:doorwayLift .95s cubic-bezier(.33,0,.24,1) .14s both}
@keyframes doorwayLift{from{opacity:1}to{opacity:0}}
@media (prefers-reduced-motion:reduce){ html.arriving #doorway{display:none} }


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

   The photograph is the top of the home page rather than a band under it, and
   it goes back to paper on the way down instead of ending on an edge.

   Three things are happening, and they are separate on purpose.

   1. The bottom of the photograph is masked into paper permanently, so even
      standing still there is no line where the picture stops. The mask is on
      the shot and the veil together, so the dark wash fades out with the
      picture it is darkening rather than surviving it as a grey band.

   2. On the way down the whole hero loses opacity, driven by scroll position
      rather than a script. By the time the next section reaches the top the
      photograph is gone and the page is paper again.

   3. The header sits over a dark picture at rest, so its ink would vanish. It
      starts in paper with no bar behind it and turns back to ink as the bar
      fades in, on the same scroll. Both are pure CSS: if the browser has no
      scroll timelines the header keeps its normal paper bar and the hero
      simply does not fade, which is a plainer page and not a broken one. */

/* The header is in the flow above the hero, so a photograph that starts at the
   hero's own top edge starts 88 pixels down and leaves a paper strip with paper
   coloured navigation on it, which is invisible. The picture is pulled up
   behind the header instead, and the hero gives that height back so the two
   together are still one screen. One number, declared once. */
:root{--headh:88px}
.hero.full{position:relative;min-height:calc(100svh - var(--headh));
  margin:0;padding:0;display:flex;flex-direction:column;justify-content:center}
/* Escaping the shell's gutters: the same full width trick the header uses. */
.hero-shot,.hero-veil{position:absolute;top:calc(-1 * var(--headh) - 2px);bottom:0;
  left:calc(50% - 50vw);width:100vw;pointer-events:none}
.hero-shot{overflow:hidden;z-index:-2}
.hero-veil{z-index:-1}
.hero-shot img{width:100%;height:100%;object-fit:cover;display:block;
  /* Four pixels. Seven was off the focal plane but it also took the screens
     with it, and B could see the picture had gone further than intended. At
     four the wall still reads as a wall and the headline still owns the page.
     Scaled past the frame because a blur samples beyond its own edge and would
     otherwise leave a soft border all the way round. */
  filter:blur(4px);transform:scale(1.06)}
.hero-veil{background:linear-gradient(100deg,
  rgba(17,16,12,.84) 0%,rgba(17,16,12,.72) 42%,
  rgba(17,16,12,.44) 70%,rgba(17,16,12,.26) 100%)}
/* The dissolve into paper. Held solid for the first two thirds so the words
   never sit on a fading ground, then away over the last third. */
.hero-shot,.hero-veil{
  -webkit-mask-image:linear-gradient(to bottom,#000 0%,#000 64%,
    rgba(0,0,0,.72) 79%,rgba(0,0,0,.28) 91%,rgba(0,0,0,0) 100%);
  mask-image:linear-gradient(to bottom,#000 0%,#000 64%,
    rgba(0,0,0,.72) 79%,rgba(0,0,0,.28) 91%,rgba(0,0,0,0) 100%)}

/* On the photograph the words are paper, not ink, so .ink is overridden here
   rather than removed from the markup: every other page keeps those classes. */
.hero.full .hero-words .ink{color:var(--paper)}
.hero.full .go::before,.hero.full .go::after{background:var(--paper)}
.hero.full .sc-rail{background:rgba(234,230,222,.55)}
.hero.full .sc-arrow path{stroke:var(--paper)}
.hero-credit{position:absolute;right:0;bottom:26px;z-index:1;
  font-weight:700;font-size:10px;letter-spacing:.16em;text-transform:uppercase}
.hero-credit a{color:rgba(234,230,222,.62);text-decoration:none}
.hero-credit a:hover{color:var(--paper)}

/* The header, while the hero is still behind it. */
/* Deliberately NOT here. See the @supports block below: undressing the header
   is only safe where a scroll timeline exists to dress it again, and iOS Safari
   has none. Left outside the guard, the paper bar never faded in and the header
   was transparent over every section below the hero. */

@keyframes hero-out{to{opacity:0}}
@keyframes hero-bar-in{to{opacity:1}}
@keyframes hero-ink-in{to{color:var(--ink);text-shadow:0 1px 10px rgba(17,16,12,0)}}

@supports (animation-timeline:scroll()){
  /* Only inside the guard: the picture runs behind a header with no ground of
     its own, and the rules further down put the ground back on scroll. Without
     scroll timelines the header simply keeps its normal paper bar and ink type,
     which is a plainer home page and not a broken one. */
  .home-hero header a{color:var(--paper)}
  .home-hero header::before{opacity:0}

  /* Everything below is tied to the document rather than to the hero, so the
     fade is the same whatever the hero's own height turns out to be.

     Nothing is linear. A linear opacity ramp reads as a switch being thrown
     rather than as something dissolving, which is what B saw when the bar
     arrived: it went solid. Two curves do the work. The picture leaves on an
     ease out, quick at first and then long and slow into nothing. The bar and
     the ink arrive on an ease in out, so they start from a standstill instead
     of jumping at the first notch of the wheel.

     The words go before the picture does, and that is what makes the bar safe
     to take its time. The headline used to still be at full strength when it
     slid under a bar that was only part opaque, and it showed through. Faded
     out by half a screen, there is nothing left of it to show through, so the
     bar can spend a whole screen arriving. */
  /* B: the background of the header should be the background of the page.
     That is the rule, and the first attempt at it was wrong. I tried to have
     the bar arrive at exactly one minus the picture's opacity, on the theory
     that the two would sum. They do not: the bar sits ON TOP of the picture
     that is already fading, so above the header line you get picture plus a
     paper wash and below it you get picture alone. That difference is the edge,
     and no curve removes it.

     The header simply has no background of its own while the hero is there.
     Nothing is painted, so there is nothing to see a line at: the header's
     ground is the picture, which is the page. The bar only exists to cover
     content passing underneath a pinned bar, and nothing passes under it until
     the hero is spent, so it waits until then and arrives over paper that is
     already paper.

     The one thing that would pass under it is the headline, so the words leave
     early and quickly, well before they reach it. That is also the better read:
     the words go, then the picture dissolves behind them. */
  .hero-shot,.hero-veil,.hero-credit{
    animation:hero-out cubic-bezier(.33,0,.30,1) both;
    animation-timeline:scroll();
    animation-range:0 96svh}
  /* Gone by about a quarter of a screen, which is where the h1 meets the
     header. Front loaded so it is almost away before it gets there. */
  .hero-words{
    animation:hero-out cubic-bezier(.25,.85,.35,1) both;
    animation-timeline:scroll();
    animation-range:0 32svh}
  /* Starts at 78, by which point the picture is down to a tenth and the bar is
     paper arriving over paper. */
  .home-hero header::before{
    animation:hero-bar-in cubic-bezier(.4,0,.5,1) both;
    animation-timeline:scroll();
    animation-range:78svh 100svh}
  /* The type turns with the ground it stands on, over the picture's whole
     distance, so it is never light on light or dark on dark. The shadow carries
     the letterforms through the crossover, where both are near mid grey, and
     fades out with the picture that made it necessary. */
  .home-hero header a{
    animation:hero-ink-in cubic-bezier(.70,0,.67,1) both;
    animation-timeline:scroll();
    animation-range:0 96svh}
  .home-hero header a{text-shadow:0 1px 10px rgba(17,16,12,.45)}
}

@media (prefers-reduced-motion:reduce){
  .hero-shot img{filter:none;transform:none}
  .hero-shot,.hero-veil,.hero-words,.hero-credit,
  .home-hero header::before,.home-hero header a{animation:none}
}

@media (max-width:900px){
  /* A blur over a full screen photograph is real work for a phone GPU, and the
     picture is smaller anyway, so it comes down with the screen. */
  .hero-shot img{filter:blur(2.5px)}
  .hero.full{min-height:calc(88svh - var(--headh))}
}
/* The phone header stacks the wordmark over the navigation, so it is taller
   and the pull up has to match or the strip comes back. */
@media (max-width:700px){ :root{--headh:104px} }

/* The Instagram link in the footer, next to the wordmark rather than beside the
   legal line: it is somewhere to go, not a notice. Same small caps as the
   navigation, and it keeps its distance from the wordmark so the two do not
   read as one lockup. */
.footrow .left{display:flex;align-items:baseline;gap:26px}
.footig{font-weight:700;font-size:10px;letter-spacing:.16em;text-transform:uppercase;
  text-decoration:none;color:var(--ink-3)}
.footig:hover{text-decoration:underline;text-underline-offset:4px;color:var(--ink)}

/* The photograph beside body copy starts level with the first line of it.
   B: move the first photograph up so it is in line with the copy we have on it.
   The gap was not the photograph sitting low, it was the paragraph sitting 34px
   down: .sub carries a top margin for when it follows a heading, and here it is
   the first thing in its column with nothing above it to clear. Zeroing it in
   that one position aligns the two columns at every width, which matching a
   number on the figure would not. */
.capwrap > div > .sub:first-child{margin-top:0}

/* The hero with a photograph beside it: headline across the top, then prose and
   photograph as one row underneath, so the top of the picture is the top of the
   first line of copy. The gap does the spacing rather than the paragraph's own
   margin, which is what makes the two columns start at the same pixel. */
.hero .capwrap.heroaside{margin-top:clamp(28px,3vw,44px)}
.hero .capwrap.heroaside > div > .sub:first-child{margin-top:0}
/* Boxed into a column the headline was breaking across three lines. Across the
   full width it takes two, which is what it was drawn for. Capped short of the
   shell so it still breaks where it reads best rather than at the paper's edge. */
.hero h1{max-width:19ch}
@media (max-width:900px){ .hero h1{max-width:none} }

/* The services list. B: the font is huge and they do not need underlining.

   These h2s were inheriting the page heading size, which is drawn for one
   headline at the top of a page, not for ten names in a two column list. At
   that size the list shouts and you cannot take it in at a glance. Two thirds
   of it reads as a list of things rather than ten headlines.

   The underline was the browser's default on a bare link, never a decision.
   It comes off, and hover puts it back, which is the same affordance the
   navigation and the section headings already use: nothing decorated at rest,
   the underline appears when you are about to click. */
.capgrid h2{font-size:clamp(21px,2vw,29px);font-weight:400;letter-spacing:-.02em}
.capgrid h2 a{text-decoration:none}
.capgrid h2 a:hover,
.capgrid h2 a:focus-visible{text-decoration:underline;text-underline-offset:5px;
  text-decoration-thickness:1px}

/* The services list sits under the opening paragraph inside the same grid, so
   it needs the paragraph's own spacing above it rather than a section's. */
.heroaside .capgrid{margin-top:clamp(40px,4.4vw,64px)}

/* About: the three steps sit closer to the opening. B: move that section a
   little higher. The margin above it was sized for a section that announced
   itself with a label; with the label gone it was holding a gap open for
   nothing. */
.about-steps{margin-top:clamp(18px,2vw,30px)}

/* ------------------------------------------------------------ the closing ask

   The home page ended on a small caps link under a three item list, which read
   as a footer utility rather than an invitation. B: beef this up so we get
   clicks, make it interesting.

   The first attempt filled the panel with ink. B: the black is an eyesore, and
   it was: the home page already spends its one dark object on the hero
   photograph, and a second black rectangle at the foot of a paper page reads as
   a banner pasted on rather than part of the sheet. So the panel is paper, and
   the interest comes from the drawing and the scale instead of from contrast.

   B, 2026-08-24: drop the door drawing. One line of type carries the panel now,
   full width, nothing to hinge or hover.

   The whole panel is the link rather than the words inside it, so the target is
   about fifteen hundred pixels wide instead of ninety. That, not the styling,
   is most of the reason it will get clicked. */
.doorcta{
  display:block;
  text-decoration:none;color:var(--ink);
  border-top:1px solid var(--rule);
  padding:clamp(38px,4.6vw,66px) clamp(4px,1vw,14px) clamp(30px,3.6vw,52px);
  margin:clamp(54px,6vw,76px) 0 0;
}
.doorcta:focus-visible{outline:2px solid var(--ink);outline-offset:5px}

.dc-words{display:flex;flex-direction:column;align-items:flex-start;gap:.5em}
.dc-eyebrow{font-weight:700;font-size:10px;letter-spacing:.16em;
  text-transform:uppercase;color:var(--ink-3)}
.dc-head{margin:0;font-weight:300;
  font-size:clamp(30px,3.8vw,54px);line-height:1.05;letter-spacing:-.03em}

@media (prefers-reduced-motion:reduce){
  .dc-leaf{transition:none}
  .doorcta:hover .dc-leaf{transform:none}
  .dc-light{transition:none}
}
