/* ============================================================
   NEUE HAAS GROTESK DISPLAY
   Self-hosted, subset to Latin and the punctuation this page uses:
   10KB per weight, down from 100KB. Licensed font, so these files
   ship with the site and must not be redistributed on their own.

   The weight RANGE on each static face is deliberate: the design runs
   at 450, and declaring Roman as 400-450 keeps 450 resolving to Roman
   rather than jumping up to Medium. Swap the ranges to shift register.
   ============================================================ */
@font-face{
  font-family:'Neue Haas Grotesk Display';
  src:url('assets/fonts/NeueHaasDisplay-Roman.woff2') format('woff2');
  font-weight:400 450;font-style:normal;font-display:swap;
}
@font-face{
  font-family:'Neue Haas Grotesk Display';
  src:url('assets/fonts/NeueHaasDisplay-Medium.woff2') format('woff2');
  font-weight:451 550;font-style:normal;font-display:swap;
}

/* ============================================================
   profile3 - design system
   Rebuilt against the reference screen recording rather than the
   published token summary. The page is a three-act colour story:

     hero     #989c9e   grey ground, white display type
     body     #ffffff   white ground, near-black type
     contact  #1c1d20   dark ground, white type, iris disc

   The dark is the finale, not the canvas. Do not "fix" this into
   a single theme; the transition is the design.
   ============================================================ */

:root{
  /* --- colour (sampled off the reference video) ----------- */
  --grey:#989c9e;          /* hero ground */
  --white:#ffffff;         /* body ground */
  --obsidian:#1c1d20;      /* contact ground */
  --paper:#ebebeb;         /* the one banded surface, services on about */
  --ink:#1c1d20;           /* type on white */
  --ink-soft:#6f7375;      /* secondary type on white */
  --bone:#ffffff;          /* type on grey and on dark */
  --fog:#999d9e;           /* secondary type on dark */
  --iris:#455ce9;
  --iris-deep:#334bd3;
  /* the second half of the globe disc's colour cycle, sampled off the
     reference recording at #fd4242. It is used by that one animation
     and nowhere else, so the accent budget still reads as one hue. */
  --ember:#fd4242;

  /* --- type ---------------------------------------------- */
  /* Headline: Neue Haas Grotesk Display when it is licensed and served.
     Helvetica Neue covers macOS; on Windows `Helvetica` aliases to Arial,
     which is itself a Helvetica clone and the closest local stand-in. */
  --display:'Neue Haas Grotesk Display Pro','Neue Haas Grotesk Display','Helvetica Neue',Helvetica,'Inter',Arial,sans-serif;
  /* Paragraph: the same family. Neue Haas Grotesk TEXT is the correct
     optical size for this role, but only the Display Pro cut is
     licensed here, so Display carries both. To upgrade later, add the
     Text @font-face and change this one line. */
  --sans:'Neue Haas Grotesk Display Pro','Neue Haas Grotesk Display','Helvetica Neue',Helvetica,'Inter',Arial,sans-serif;

  /* One weight, everywhere. The Pro cut is named first in both stacks
     because it is the one that resolves a full ladder if a second
     weight is ever wanted here — measured on this machine at a 40px
     test string: 300 Light, 400-500 Roman, 550-600 Medium, 650-700
     Bold, 800-900 Black. Plain "Neue Haas Grotesk Display" resolves
     only Roman and Medium. */
  --w:450;                 /* the single weight. never change it. */
  --track:.05em;

  /* Re-measured off a full-resolution frame of the reference hero at
     1920x911, glyph by glyph. The first pass read the display type
     7% small and the whole UI tier about 20% small:

       marquee cap height   206px  ->  a 287px em
       nav / mark           cap 14px  ->  19.5px
       location pill        cap 13px  ->  18px
       role text            ~43px, line pitch 61 (1.42)
       about headline       cap 83px  ->  116px
       body copy            ~18.5px, line pitch 32 (1.7)

     Everything here is set from those numbers. Sizes with no glyph to
     measure against — the mid headings — were lifted 10% to stay in
     proportion with the ones that were measured. */
  --t-display:clamp(3.75rem,14.95vw,17.94rem);
  --t-h1:clamp(2.75rem,7.7vw,5.5rem);
  --t-h2:clamp(2.1rem,4.84vw,3.58rem);
  --t-h3:clamp(1.5rem,2.64vw,2.1rem);
  --t-lg:17px;
  --t-body:16px;
  --t-cap:12px;

  /* --- shape --------------------------------------------- */
  --r-card:10px;
  --r-pill:36.72px;
  --ring:inset 0 0 0 1px rgba(255,255,255,.2);
  --ring-dark:inset 0 0 0 1px rgba(28,29,32,.22);

  /* --- rhythm -------------------------------------------- */
  --gap:12px;
  --pad:22px;
  --section:clamp(88px,11vw,150px);
  --edge:clamp(20px,4.5vw,58px);
  /* the white act sits on a far wider gutter than the hero, and its
     text is indented again inside it: rules at 160, text at 320 on a
     1920 viewport */
  --gutter:clamp(20px,8.33vw,160px);
  --indent:clamp(16px,8.33vw,160px);
  /* the outer rail: what the header row runs on, and now the footer */
  --edge-rail:clamp(20px,2.6vw,50px);

  /* --- motion -------------------------------------------- */
  --ease:cubic-bezier(.16,1,.3,1);
  --ease-io:cubic-bezier(.76,0,.24,1);
  /* Fitted to the reference: the panel edge was tracked frame by frame
     and this curve fits that trace with an RMSE of 0.025, about eleven
     times closer than the ease-in-out it replaces. It is an ease-OUT:
     it commits immediately and spends the rest of the time settling,
     which is what reads as smooth. */
  /* the disc sweep: a gentler deceleration than --ease, whose expo
     start moves almost instantly and reads as a flick at any speed */
  --ease-sweep:cubic-bezier(.33,1,.68,1);
  --ease-nav:cubic-bezier(.46,.55,.25,1);
}

*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}

/* The scrollbar is hidden, not disabled: the page still scrolls with
   the wheel, the keyboard and a drag, and the smooth wrapper still
   reads window.scrollY. It also hands the layout back the ~15px the
   scrollbar used to take, which is why the rails now land on the
   reference's numbers rather than 6px inside them. */
html{
  background:var(--white);color-scheme:light;-webkit-text-size-adjust:100%;
  scrollbar-width:none;-ms-overflow-style:none;
}
html::-webkit-scrollbar,body::-webkit-scrollbar{width:0;height:0;display:none}

body{
  background:var(--white);
  color:var(--ink);
  font-family:var(--sans);
  font-weight:var(--w);
  font-size:var(--t-body);
  line-height:1.6;
  letter-spacing:var(--track);
  font-synthesis-weight:none;
  -webkit-font-smoothing:antialiased;
  overflow-x:hidden;
}

img{display:block;max-width:100%;height:auto}
a{color:inherit;text-decoration:none}
ul{list-style:none}
button{font:inherit;letter-spacing:inherit;color:inherit;background:none;border:0}
h1,h2,h3,strong,b{font-weight:var(--w)}
::selection{background:var(--iris);color:var(--bone)}

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

.visually-hidden{
  position:absolute;width:1px;height:1px;overflow:hidden;
  clip-path:inset(50%);white-space:nowrap;
}

.skip{
  position:fixed;top:12px;left:12px;z-index:120;
  padding:10px 20px;border-radius:var(--r-pill);
  background:var(--obsidian);color:var(--bone);font-size:var(--t-body);
  transform:translateY(-200%);transition:transform .3s var(--ease);
}
.skip:focus-visible{transform:none}

.label{
  font-size:var(--t-cap);text-transform:uppercase;letter-spacing:.18em;
  color:var(--ink-soft);
}

/* ============================================================
   SMOOTH SCROLL WRAPPER
   Static by default. JS opts in, so no-JS still scrolls.
   ============================================================ */
.scroll__content{width:100%}
body.is-smooth .scroll__content{position:fixed;top:0;left:0;will-change:transform}

/* ============================================================
   REVEAL MACHINERY
   ============================================================ */
.line{
  display:inline-block;overflow:hidden;vertical-align:top;
  padding-bottom:.12em;margin-bottom:-.12em;
}
.word{display:inline-block;transform:translateY(105%);transition:transform .9s var(--ease)}
[data-split].is-in .word{transform:none}

[data-reveal]{
  opacity:0;transform:translateY(28px);
  transition:opacity .8s var(--ease),transform .8s var(--ease);
}
[data-reveal].is-in{opacity:1;transform:none}

.hero__group span,
.intro__lead,
.row__title,
.contact__title,
.overlay__list a,
.preloader__word{font-family:var(--display)}

/* ============================================================
   PRELOADER
   ============================================================ */
.preloader{
  position:fixed;inset:0;z-index:110;
  background:var(--obsidian);color:var(--bone);
  display:grid;place-items:center;
}
/* No transition on the word: the greetings hard-cut from one to the
   next, which is both the look asked for and what lets them run at
   150ms apiece. */
/* 64px at 1920, measured off the reference's own curtain: its cap
   height is 46px, which is a 64px em. This was 48. */
.preloader__word{font-size:clamp(2rem,3.33vw,4rem)}
/* the bullet the reference sets before the greeting. Measured off a
   full-resolution frame: an 11px dot at a 67px word, sitting 30px
   clear of it — 0.164em across, 0.45em of air. */
.preloader__word::before{
  content:"";display:inline-block;
  width:.164em;height:.164em;border-radius:50%;background:currentColor;
  vertical-align:.23em;margin-right:.45em;
}
/* The panel leaves on a curved edge, not a straight one. Measured off
   a recording of the reference at 1920: the bottom bows about 69px
   deep as it starts moving — 3.6% of the width — and flattens to
   nothing as it clears the screen, which is what the height animation
   below does. The 220px it used to carry was three times too deep,
   and the path under it was a rectangle anyway. */
.preloader__curve{
  position:absolute;left:0;width:100%;top:100%;
  height:clamp(24px,3.6vw,69px);fill:var(--obsidian);
}
/* .6s, not 1s: the curtain was the slowest part of arriving */
.preloader.is-out{transform:translateY(-100%);transition:transform .6s var(--ease-io)}
.preloader.is-out .preloader__curve{height:0;transition:height .6s var(--ease-io)}
.preloader.is-done{display:none}

/* ============================================================
   HEADER  +  ROUND TOGGLE
   ============================================================ */
.nav{
  position:fixed;top:0;left:0;right:0;z-index:90;
  /* 100px tall so the row centres on y=50, as measured */
  height:clamp(64px,5.2vw,100px);
  display:flex;align-items:center;justify-content:space-between;
  padding:0 var(--edge-rail);
  /* ink, not white: the row sits on the hero grey, where white measures
     2.77:1 and ink 6.1:1 (AA asks 4.5), and on white on every other page */
  color:var(--ink);font-size:clamp(15px,1.02vw,19.5px);
  transition:opacity .5s var(--ease),transform .5s var(--ease);
}
/* the text nav belongs to the hero only */
body.past-hero .nav{opacity:0;transform:translateY(-12px);pointer-events:none}

/* The corner mark swaps its label on hover. Measured off a crop of the
   reference doing the same thing: the © stays put and everything after
   it slides sideways inside a clipped slot — out to the left and the
   new label in from the right, about 0.5s, and the mirror of that on
   the way back out. The two labels are stacked in one grid cell, so
   the slot is as wide as the longer of them and each slides by exactly
   its own width: no seam, no measuring in JS. */
.nav__mark{display:inline-flex;align-items:baseline;gap:.34em}
.mark__slot{display:grid;overflow:hidden}
.mark__slot > span{
  grid-area:1/1;white-space:nowrap;
  transition:transform .5s var(--ease-sweep);
}
.mark__b{transform:translateX(101%)}
.nav__mark:hover .mark__a,
.nav__mark:focus-visible .mark__a{transform:translateX(-101%)}
.nav__mark:hover .mark__b,
.nav__mark:focus-visible .mark__b{transform:translateX(0)}

.nav__links{display:flex;gap:clamp(24px,2.3vw,44px)}
.nav__links a{position:relative;padding:4px 0}
/* A dot under the item, not an underline — the same mark the overlay
   already uses for the section you are on, and what the reference
   does here. Measured off a crop of its nav at a 15px type size: the
   dot is 7px across (0.47em), centred on the word, its top 13px below
   the baseline, and it scales up from nothing in about 0.13s. Kept a
   touch slower here for the same reason the disc sweep is. */
.nav__links a::after{
  content:"";position:absolute;left:50%;top:calc(100% + .35em);
  /* 7px at the 19.5px nav, its top 20px below the baseline. These were
     right when the nav was 15px and drifted when the type grew — em
     units scale, the reference's dot does not. */
  width:.36em;height:.36em;margin-left:-.18em;border-radius:50%;
  background:currentColor;
  transform:scale(0);
  transition:transform .18s var(--ease-sweep);
}
.nav__links a:hover::after,
.nav__links a:focus-visible::after,
.nav__links a[aria-current]::after{transform:scale(1)}

.burger{
  position:fixed;top:clamp(14px,1.8vw,33px);right:clamp(16px,1.72vw,33px);
  z-index:96;
  width:clamp(56px,4.8vw,92px);height:clamp(56px,4.8vw,92px);border-radius:50%;
  background:var(--obsidian);color:var(--bone);
  display:grid;place-items:center;cursor:pointer;
  opacity:0;transform:scale(.4);pointer-events:none;
  transition:opacity .5s var(--ease),transform .5s var(--ease),background-color .4s var(--ease);
}
body.past-hero .burger{opacity:1;transform:none;pointer-events:auto}
/* the round button fills like the discs do — measured off a recording
   of the reference's, where the iris climbs in on the same curved
   edge rather than cross-fading */
.burger{overflow:hidden;--sweep:var(--iris);--cap:27%}
/* while the panel is open the button turns iris and reads as a close */
body.nav-open .burger{
  opacity:1;transform:none;pointer-events:auto;
  background:var(--iris);
}
body.nav-open .burger{--sweep:var(--iris-deep)}
/* one variable drives both the resting gap and the distance each line
   travels to meet in the middle, so the X closes cleanly at any size */
.burger{--bl-gap:clamp(7px,.62vw,11px)}
.burger__inner{
  position:relative;display:block;
  width:clamp(18px,1.6vw,30px);height:var(--bl-gap);
}
.burger__line{
  position:absolute;left:0;width:100%;height:1px;
  background:currentColor;transition:transform .4s var(--ease);
}
.burger__line:nth-child(1){top:0}
.burger__line:nth-child(2){top:var(--bl-gap)}
body.nav-open .burger__line:nth-child(1){transform:translateY(calc(var(--bl-gap) / 2)) rotate(45deg)}
body.nav-open .burger__line:nth-child(2){transform:translateY(calc(var(--bl-gap) / -2)) rotate(-45deg)}

/* ============================================================
   OVERLAY NAV
   ============================================================ */
.overlay{
  position:fixed;top:0;right:0;bottom:0;z-index:94;
  width:min(642px,100vw);background:var(--obsidian);color:var(--bone);
  transform:translateX(100%);
  visibility:hidden;
  /* visibility is delayed on the way out so the panel can finish
     sliding; without this it snaps invisible and there is no exit */
  transition:transform .8s var(--ease-nav),visibility 0s linear .8s;
}
body.nav-open .overlay{
  transform:none;visibility:visible;
  transition:transform .8s var(--ease-nav),visibility 0s linear 0s;
}

/* The leading edge bulges while the panel is travelling and flattens
   once it settles, in both directions. */
.overlay__curve{
  position:absolute;top:0;height:100%;right:100%;
  width:clamp(48px,5vw,96px);
  fill:var(--obsidian);
  transform:scaleX(0);transform-origin:right center;
}
body.nav-open .overlay__curve{animation:curveIn .8s var(--ease-nav) forwards}
body.nav-closing .overlay__curve{animation:curveOut .8s var(--ease-nav) forwards}
/* deepest as it starts moving, gone once it arrives */
@keyframes curveIn{
  from{transform:scaleX(1)}
  to  {transform:scaleX(0)}
}
/* swells as it leaves, then goes with the panel */
@keyframes curveOut{
  0%  {transform:scaleX(0)}
  45% {transform:scaleX(1)}
  100%{transform:scaleX(0)}
}

/* Spacing is explicit rather than distributed. space-between splits the
   leftover height evenly, which drifts the whole stack; these values are
   measured off the reference at a 910 tall panel. */
.overlay__inner{
  height:100%;
  padding:clamp(60px,15vh,137px) clamp(28px,7.4vw,142px) clamp(45px,10.2vh,93px);
  display:flex;flex-direction:column;justify-content:flex-start;overflow-y:auto;
}
.overlay__label{
  font-size:var(--t-cap);text-transform:uppercase;letter-spacing:.18em;
  color:rgba(255,255,255,.45);
}
/* the rule sits under the navigation heading only */
.overlay__inner > .overlay__label{
  padding-bottom:clamp(18px,3.5vh,32px);
  margin-bottom:clamp(24px,4.8vh,44px);
  border-bottom:1px solid rgba(255,255,255,.18);
}
/* pinned to the base, so the gap above it absorbs the spare height */
.overlay__socials{margin-top:auto}
.overlay__socials .overlay__label{margin-bottom:clamp(12px,2.2vh,20px)}

/* The li masks the word's reveal, which also clipped the dot sitting
   out on its left. Padding gives that dot room inside the box and the
   negative margin puts the type back where it was — and the type size
   lives on the li so that padding is in the same em as the dot's
   offset, not the 16px the li would otherwise inherit. */
.overlay__list li{
  overflow:hidden;
  font-size:clamp(2.2rem,3.7vw,4.4rem);
  padding-left:.85em;margin-left:-.85em;
}
.overlay__list a{
  /* inline-block, not block: a full-width row meant the pointer could
     be 200px from the word and still count as hovering it, which both
     lit the item from across the panel and gave the magnetic pull a
     huge lever. Shrink-wrapped, the hit area is the word and the pull
     stays in the reference's 3-10px range. */
  display:inline-block;position:relative;
  font-size:inherit;line-height:1.31;
  transform:translateY(105%);
  transition:transform .7s var(--ease-nav),color .3s var(--ease);
}
body.nav-open .overlay__list a{transform:none}
/* The dot marks the section you are on — and moves to whatever you
   hover, which is what the reference does: measured off a recording of
   its panel, the dot on the current item scales out over about four
   frames while the hovered one scales in, and the type never changes
   colour. `:hover` on the list is what lets the current item yield. */
.overlay__list a::before{
  content:"";position:absolute;top:.59em;left:-.63em;
  /* Re-measured off the recording at 1:1 and at six thresholds, from
     >120 through >250, because antialiasing makes a small white dot on
     near-black easy to under-read: it holds 10-12px against a 68px
     word, so .16em across, not the .104em first taken from it. Its
     centre sits 43px clear of the text and on the cap-height middle. */
  width:.16em;height:.16em;border-radius:50%;background:currentColor;
  transform:scale(0);transition:transform .18s var(--ease-sweep);
}
.overlay__list a[aria-current]::before,
.overlay__list a:hover::before,
.overlay__list a:focus-visible::before{transform:scale(1)}
.overlay__list:hover a[aria-current]:not(:hover)::before{transform:scale(0)}
/* kept from this build rather than the reference, which leaves the
   type white: the violet on hover is the overlay's one accent */
.overlay__list a:hover{color:var(--iris)}
/* The stagger belongs to the entrance only. These are two values, one
   per property in the transition above — the first delays the slide,
   the second keeps the hover colour at zero. As a single value it
   delayed both, so an item lit up 150-330ms after the pointer reached
   it, which read as lag. */
.overlay__list li:nth-child(1) a{transition-delay:.15s,0s}
.overlay__list li:nth-child(2) a{transition-delay:.21s,0s}
.overlay__list li:nth-child(3) a{transition-delay:.27s,0s}
.overlay__list li:nth-child(4) a{transition-delay:.33s,0s}
.overlay__socials ul{display:flex;flex-wrap:wrap;gap:8px 30px;font-size:var(--t-body)}
.overlay__socials a{color:rgba(255,255,255,.75)}
.overlay__socials a:hover,.overlay__socials a:focus-visible{color:var(--bone)}

/* The rule under a hovered link opens from its middle outward and
   closes back into it — measured off a recording of the reference's
   socials, where it grows 158..196 -> 148..204 -> 146..212 around a
   fixed midpoint, and sits about 0.65em under the baseline. */
.overlay__socials a,.foot__links a{position:relative}
.overlay__socials a::after,.foot__links a::after{
  content:"";position:absolute;left:0;right:0;top:calc(100% + .44em);
  height:1px;background:currentColor;
  transform:scaleX(0);transform-origin:center;
  transition:transform .25s var(--ease-sweep);
}
.overlay__socials a:hover::after,.overlay__socials a:focus-visible::after,
.foot__links a:hover::after,.foot__links a:focus-visible::after{transform:scaleX(1)}

/* ============================================================
   PILLS + DISCS
   ============================================================ */
/* the pills carry the same curved sweep as the discs; only the depth
   of the cap differs, and that is measured — see the sweep block */
.pill{
  --sweep:var(--iris);
  --cap:23%;
  display:inline-block;position:relative;overflow:hidden;z-index:0;
  /* 81px tall on the reference, its type a 16px cap -> 22px. This was
     52px on 16px type, which read as a small control next to a 120px
     headline. */
  padding:clamp(12px,1.2vw,23px) clamp(20px,2vw,38px);
  border-radius:var(--r-pill);font-size:clamp(15px,1.15vw,22px);
  box-shadow:var(--ring-dark);transition:color .45s var(--ease);
}
.pill.is-lit,.pill:focus-visible{color:var(--bone)}

.pill--accent{background:var(--iris);color:var(--bone);box-shadow:none}
.pill--accent{--sweep:var(--iris-deep)}
.pill--onDark{box-shadow:var(--ring);color:var(--bone)}
/* project count, as the reference sets it: small and raised */
.pill__count{font-size:.55em;margin-left:.3em;vertical-align:super;line-height:0}

.disc{
  display:grid;place-items:center;position:relative;flex:none;
  width:clamp(140px,10.9vw,210px);aspect-ratio:1;border-radius:50%;
  /* measured off the reference's "Get in touch": 14px of cap height
     inside a 210px disc, so a 19.5px em — not the 16 of body copy */
  font-size:clamp(16px,1.02vw,19.5px);text-align:center;
  overflow:hidden;
  transition:background-color .45s var(--ease);
}
.disc__label{position:relative;z-index:1}
.disc--dark{background:var(--obsidian);color:var(--bone);--sweep:var(--iris)}
.disc--accent{background:var(--iris);color:var(--bone);--sweep:var(--iris-deep)}

/* ---- the disc hover sweep ------------------------------
   The fill rises through the disc on a curved edge instead of the
   colour switching flat. Measured off a recording of the reference's
   "About me" disc at 30fps:

   - it enters from the BOTTOM and crosses in ~0.25s
   - the leading edge is a parabola about 27% of the disc's width deep
     (fitted to the edge profile: depth = 0.00435 * dx², which is 67px
     across a 248px disc)
   - on leave it does NOT retreat the way it came. It carries on
     upward and out of the top in ~0.22s, and the trailing edge curves
     the other way — so the shape is one body with a cap above it and
     a tail below it, translated.

   The shape is a mask rather than a drawn SVG, so the colour still
   comes from a token and no page needs new markup. Its two curves are
   the same quadratic the preloader and contact curves use. */
:root{
  --sweep-shape:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 154' preserveAspectRatio='none'%3E%3Cpath d='M0,27 Q50,-27 100,27 L100,127 Q50,181 0,127 Z' fill='%23000'/%3E%3C/svg%3E");
}
.disc::before,.pill::before,.burger::before{
  content:"";position:absolute;left:0;width:100%;z-index:-1;
  /* the cap sits above the element and the tail below it, so at rest
     the apex is exactly on the bottom edge and nothing shows */
  top:calc(-1 * var(--cap));height:calc(100% + 2 * var(--cap));
  background:var(--sweep);
  -webkit-mask:var(--sweep-shape) center / 100% 100% no-repeat;
          mask:var(--sweep-shape) center / 100% 100% no-repeat;
  transform:translateY(100%);
}
/* Cap depth is per component, because the reference does not keep it
   proportional: on the 248px disc the leading edge is 67px deep, on
   the 328x81 contact pill it is 19px — 27% of the disc's own height
   against 23% of the pill's. Both are measured off their recordings. */
.disc{--cap:27%}
/* the two states are separate so the exit keeps going up rather than
   reversing, and so the reset back to the bottom is instant: dropping
   the class drops the transition with it.

   The reference crosses in 0.25s on `ease-in-out`, which at that
   length reads as a snap. These are deliberately much slower, and on
   an eased-out cubic rather than the project's `--ease`: expo starts
   at almost full speed, which flicks however long you make it. This
   one leaves gently and settles, so the curve is legible the whole
   way across. If you want the reference's exact tempo back, it is
   .25s/.22s on --ease-io — and `main.js` holds the exit class for the
   length of the exit, so retune that with it. */
.disc::before,.pill::before,.burger::before{will-change:transform}
.disc.is-lit::before,.pill.is-lit::before,.burger.is-lit::before{transform:translateY(0);transition:transform .9s var(--ease-sweep)}
.disc.is-out::before,.pill.is-out::before,.burger.is-out::before{transform:translateY(-100%);transition:transform .75s var(--ease-sweep)}
/* the about globe answers a hover with its colour cycle instead */
.ab-globe::before{content:none}

/* ============================================================
   ACT ONE HERO on grey
   ============================================================ */
.hero{
  position:relative;
  min-height:100svh;
  background:var(--grey);
  overflow:hidden;
  color:var(--bone);
}
/* the portrait is anchored to the base and runs nearly the full
   height, so the head clears the top by about 6% */
.hero__media{
  position:absolute;inset:0;overflow:hidden;
  display:flex;align-items:flex-end;justify-content:center;
}
/* flex:none plus an explicit max-height, because a percentage height
   on a replaced element resolves against its own intrinsic size in
   some engines and the portrait ends up taller than the viewport */
.hero__media img{
  flex:none;width:auto;height:94%;max-height:94%;max-width:none;
  object-fit:contain;
  will-change:transform;
}

/* measured off the reference: the pill runs off the left edge,
   so only its right end is rounded */
.hero__where{
  position:absolute;left:0;top:50%;transform:translateY(-50%);
  display:flex;align-items:center;
  gap:clamp(22px,2.6vw,50px);
  height:clamp(84px,5.83vw,112px);
  padding-left:clamp(20px,2.4vw,45px);
  padding-right:clamp(14px,1.4vw,27px);
  border-radius:0 999px 999px 0;
  background:var(--obsidian);color:var(--bone);
  font-size:clamp(14px,.94vw,18px);line-height:1.25;
}
/* The globe sits on a disc of the hero's own grey, not straight on the
   pill. Measured off the reference at 1920: a 78px circle inside a
   112px pill, with a 38px glyph centred on it. */
.hero__ring{
  flex:none;display:grid;place-items:center;
  width:clamp(56px,4.06vw,78px);height:clamp(56px,4.06vw,78px);
  border-radius:50%;background:var(--grey);color:var(--bone);
}
.hero__globe{
  width:48.7%;height:48.7%;flex:none;
  fill:none;stroke:currentColor;stroke-width:1.5;
  vector-effect:non-scaling-stroke;
}
/* the globe turns: each meridian sweeps from full width to edge-on and
   back, the pair a quarter-cycle apart. Animating the `rx` geometry
   property needs Chrome 79+, Firefox 70+ or Safari 16.4+; anywhere
   else the globe simply sits still, which is a fine fallback.

   5.4s is the reference's own figure: correlating the icon against
   itself frame by frame in the recording gives one clean peak at
   5.40s and nothing at any shorter lag, so that is a full turn. It
   puts a meridian edge-on every 1.35s, against every 1.75s before. */
.mer{animation:meridian 5.4s linear infinite}
.mer--b{animation-delay:-1.35s}
@keyframes meridian{
  0%   {rx:18}
  25%  {rx:0.4}
  50%  {rx:18}
  75%  {rx:0.4}
  100% {rx:18}
}

/* the reference centres the TEXT on the same line as the location
   pill and floats the arrow above it, so the arrow is taken out of
   flow rather than counted into the centring */
/* Ink, on the bare grey beside the head (6.1:1). The name marquee stays
   white at every width: it is the signature, hidden from assistive tech,
   and the real h1 says it. */
.hero__role{
  position:absolute;right:clamp(20px,8.6vw,165px);top:50%;
  transform:translateY(-50%);
  font-size:clamp(19px,2.24vw,43px);line-height:1.42;
  color:var(--ink);
}
/* small and underlined: it has to read as a link without competing
   with the role line above it */
.hero__avail{
  display:inline-block;margin-top:.9em;font-size:max(15px,.42em);
  text-decoration:underline;text-underline-offset:.3em;text-decoration-thickness:1px;
}
.hero__avail:hover,.hero__avail:focus-visible{text-decoration-thickness:2px}
/* when the line has to break, it breaks only between the two phrases */
.hero__avail span{white-space:nowrap}
.hero__arrow{
  position:absolute;left:0;
  /* offset in em from the text block's top edge, so it scales with the
     type and needs no percentage containing block */
  top:-2.1em;
  /* 27px so the ink measures the reference's 22px: the path spans 75%
     of its own viewBox, and the 2px stroke adds the rest */
  width:clamp(16px,1.4vw,27px);height:clamp(16px,1.4vw,27px);
  animation:nudge 2.4s var(--ease) infinite;
}
/* The nudge animates `translate`, not `transform`, because the aiming
   in main.js writes `rotate` — they are separate properties, so the
   two compose instead of one overwriting the other. */
@keyframes nudge{0%,100%{translate:0 0}50%{translate:5px 5px}}

/* Every arrow on the site is the same glyph at a different rotation:
   a 45-degree shaft into a right-angle head whose arms run 77% of the
   span. Measured off the reference at 1920: 22px of ink, a 2px stroke
   — so the stroke does not scale with the box, or it would come out
   at 1.1px here and read as a hairline. */
.hero__arrow,.contact__arrow,.ab-lead__arrow{
  fill:none;stroke:currentColor;stroke-linejoin:miter;
  /* 2.6 rather than the 2px measured off the reference: asked for a
     heavier arrow, and at this size the extra weight is the whole
     difference between a mark and a hairline */
  stroke-width:2.6;vector-effect:non-scaling-stroke;
  transition:rotate .25s linear;
}

/* the signature: the name runs across the base of the hero,
   bleeding off both edges, over the portrait */
.hero__marquee{
  position:absolute;left:0;right:0;bottom:clamp(10px,4.4vh,40px);
  overflow:hidden;pointer-events:none;
}
.hero__track{display:flex;width:max-content;will-change:transform}
.hero__group{display:flex;align-items:center;flex:none}
.hero__group span{
  font-size:var(--t-display);line-height:.92;white-space:nowrap;padding:0 .04em;
}
/* stands in for the long dash between repeats, so no dash
   character ever enters the copy */
.dash{
  display:inline-block;flex:none;
  width:clamp(60px,12vw,230px);
  height:clamp(8px,1.15vw,22px);
  background:currentColor;
  margin:0 clamp(20px,2.2vw,42px);
  transform:translateY(.1em);
}

/* ============================================================
   ACT TWO WHITE BODY
   ============================================================ */
.intro{
  background:var(--white);
  /* almost no bottom padding: the work rules start right under the
     disc, as they do in the reference */
  /* The note column ends on the same rail the statement starts on —
     the reference has both at 116px at a 958 viewport, not the text
     rail one side and the gutter the other. */
  padding:var(--section) calc(var(--gutter) + var(--indent)) clamp(14px,1.2vw,22px) var(--gutter);
  display:grid;grid-template-columns:minmax(0,1fr) minmax(0,31%);
  gap:clamp(32px,4vw,60px);align-items:stretch;
}
/* The statement and the work cue share the left column, so the cue
   ends up beside the disc instead of below the whole block. */
.intro__main{
  padding-left:var(--indent);
  display:flex;flex-direction:column;justify-content:space-between;
  gap:clamp(40px,4vw,72px);
}
/* measured: lead at 45px on a 1.42 leading */
.intro__lead{
  font-size:clamp(29px,2.57vw,50px);line-height:1.42;max-width:18.4em;
}
.intro__cue{margin-bottom:clamp(12px,1.6vw,30px)}
.intro__side{display:flex;flex-direction:column;align-items:flex-start;gap:clamp(24px,3vw,57px)}
.intro__side p{font-size:clamp(15px,.97vw,18.5px);line-height:2;color:var(--ink);max-width:16em}
/* left-aligned with the paragraph above it, not centred */
.intro__side .disc{align-self:flex-start}

/* ---- recent work -------------------------------------- */
.work{background:var(--white);padding:0 var(--gutter) var(--section)}
.work__list{position:relative}

/* The rule spans the full gutter width while the title is indented,
   and the meta sits on a fixed column at 73% rather than being pushed
   to the right edge. */
.row{
  position:relative;z-index:1;
  display:grid;grid-template-columns:minmax(0,1fr) minmax(0,27%);
  align-items:baseline;gap:24px;
  padding:clamp(26px,3.2vw,62px) 0;
  border-top:1px solid rgba(28,29,32,.16);
  transition:color .45s var(--ease);
}
.row:last-of-type{border-bottom:1px solid rgba(28,29,32,.16)}
.row__title{
  padding-left:var(--indent);
  font-size:clamp(37px,6.25vw,120px);line-height:1.1;
  transition:transform .55s var(--ease);
}
/* Measured off a recording of the reference's list: the row you are
   on is the one that recedes. Its title goes from rgb(40,39,41) to
   rgb(180,180,181) — near enough the ink at a third — and steps LEFT
   by 25px, which is exactly its own indent, while every other row
   stays put and stays black. This build had it the other way round.
   The step is a transform, not padding: padding re-wrapped a long title
   onto fewer lines, the row shrank out from under the pointer, and the
   hover flipped between two rows. */
.row:hover .row__title{transform:translateX(calc(-1 * var(--indent)))}
.row__meta{font-size:clamp(15px,.97vw,18.5px);color:var(--ink)}
.row:hover{color:rgba(28,29,32,.32)}
.row:hover .row__meta{color:rgba(28,29,32,.32)}
/* touch only: main.js inserts it when there is no fine pointer */
.row__shot{
  grid-column:1 / -1;display:block;width:100%;aspect-ratio:10 / 7;
  object-fit:cover;background:var(--bone);margin-bottom:clamp(14px,3vw,24px);
}

.preview{
  position:absolute;top:0;left:0;z-index:2;
  /* the reference's card measures about 478px across at a 1920
     viewport; this keeps the 10:7 plate and lands near it */
  width:460px;height:322px;border-radius:2px;overflow:visible;
  pointer-events:none;
  /* Leaving is a zoom out, not a dissolve: the card keeps its opacity
     while it shrinks and only cuts at the end. Scale lives on the
     `scale` property because the pointer tracking owns `translate` —
     as `transform` they overwrote each other, which is why the scale
     never showed and it read as a plain fade. */
  opacity:0;scale:.08;
  /* It shrinks the whole way down before it goes, and takes its time:
     an eased-out curve collapses it in the first third, so this runs
     on an ease-in-out over a second, with the opacity held until the
     last tenth. */
  transition:scale 1s cubic-bezier(.45,0,.55,1),opacity .12s linear .88s;
  will-change:translate,scale;
}
/* The frame clips; the two plates slide through it. On a change of
   project the incoming one comes up from below and the outgoing one
   carries on up and out — the card itself never leaves. */
.preview__frame{position:absolute;inset:0;overflow:hidden;border-radius:2px}
.preview__frame img{
  position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  transition:transform .55s var(--ease-sweep);
}
.preview__frame img:nth-child(2){transform:translateY(100%)}
.preview__disc{
  position:absolute;top:50%;left:50%;
  /* 94px on the reference, measured off the same frame */
  width:94px;height:94px;margin:-47px 0 0 -47px;border-radius:50%;
  background:var(--iris);color:var(--bone);
  display:grid;place-items:center;font-size:var(--t-body);
}
.preview.is-on{
  opacity:1;scale:1;
  /* arriving is the other way round: there, then grow */
  transition:scale .5s var(--ease-sweep),opacity .16s var(--ease-sweep);
}

.work__foot{
  margin:clamp(32px,4vw,56px) 0 clamp(48px,6vw,90px);
  display:flex;justify-content:center;
}

/* ---- archive: two counter-scrolling rows ---------------- */
/* Measured off the reference: 528x400 tiles, 48px apart, a 36px
   matte around a 16:10 screenshot. The rows bleed past the gutter to
   both viewport edges, so they break out of the section padding. */
.archive{
  display:flex;flex-direction:column;
  gap:clamp(16px,2.3vw,44px);
  margin-left:calc(var(--gutter) * -1);
  margin-right:calc(var(--gutter) * -1);
}
.archive__row{overflow:hidden}
.archive__track{display:flex;width:max-content;will-change:transform}
.archive__group{display:flex;gap:clamp(16px,2.5vw,48px);flex:none;padding-right:clamp(16px,2.5vw,48px)}

.tile{
  flex:none;
  width:clamp(280px,27.5vw,528px);
  aspect-ratio:528 / 400;
  background:var(--tint,#e8e8e8);
  display:grid;place-items:center;
  padding:0 clamp(16px,1.9vw,36px);
  /* the archive plates are square-cornered in the reference, unlike
     the 10px cards elsewhere in the system */
  border-radius:0;
}
.tile__shot{
  display:block;width:100%;aspect-ratio:16 / 10;
  overflow:hidden;background:var(--white);
}
.tile__shot img{
  width:100%;height:100%;object-fit:cover;
  transition:transform .8s var(--ease);
}
.tile:hover img{transform:scale(1.04)}

/* ============================================================
   ACT THREE DARK CONTACT, arriving under a flattening curve
   ============================================================ */
/* The white act is a layer in front of the dark one. It is opaque and
   it carries the curved edge, so as it scrolls up the curve goes with
   it — by the time you are at the foot of the page there is no white
   left on screen and nothing covering "Let's work together". */
main{position:relative;z-index:1;background:var(--white)}
/* The block that carries the title is a viewport tall PLUS the depth
   of the white edge above it. That extra is what lets the page scroll
   far enough for the curve to clear the top of the screen: at the foot
   of the page the white is gone entirely and the dark stands square,
   with no curve left in the corner of the view.

   Scoped with :has, because the project page's footer is the same
   class with nothing but the footer in it, and stretching that to a
   viewport would leave a hole. */
.contact:has(.contact__title){min-height:calc(100svh + 24px)}
:root{
  --edge-curve:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath d='M0,0 L100,0 Q50,200 0,0 Z' fill='%23000'/%3E%3C/svg%3E");
}
/* The shading under the white edge. Not a drop-shadow on the white:
   it is a band of the dark section's OWN colour laid over the top of
   it, opaque where the edge is and fading out downward — which is why
   the head of "Let's work together" darkens as it passes under. It
   hangs off `main`, so it travels with the edge it belongs to, and it
   paints under `main::after` because ::before comes first in tree
   order: the white curve covers the part of the band that sits above
   the boundary. */
main::before{
  content:"";position:absolute;left:0;right:0;top:100%;
  height:clamp(90px,8.9vw,142px);
  background:linear-gradient(var(--obsidian),rgba(28,29,32,0));
  pointer-events:none;
}
main::after{
  content:"";position:absolute;left:0;right:0;
  /* a pixel of overlap onto main's own white: the mask's top row is
     antialiased, and against the dark behind it that half-covered row
     reads as a hairline across the join */
  top:calc(100% - 1px);
  /* The dip is driven from the loop and runs to nothing: the edge is
     deepest while the dark section is still a viewport away and
     straightens as it arrives. Tracked across the reference's own
     scroll, where it reads 50px early and 5-27px on approach. */
  height:calc(var(--edge-dip,0px) + 1px);
  background:var(--white);
  -webkit-mask:var(--edge-curve) center / 100% 100% no-repeat;
          mask:var(--edge-curve) center / 100% 100% no-repeat;
  /* offset deliberately larger than the blur, so the shadow cannot
     reach back up into the white and haze it */
  filter:drop-shadow(0 22px 20px rgba(0,0,0,.5));
  pointer-events:none;
}

.contact{
  /* behind the white layer, and lifted under it until the page bottom
     brings it home — see the reveal in main.js */
  position:relative;z-index:0;
  background:var(--obsidian);color:var(--bone);
  /* The dark act sits on the white act's TEXT rail, not its rule rail.
     The top figure is all but gone on purpose. Measured off the
     reference at 1920: its section edge is at y=96 and the title's cap
     at y=133 — 37px, which is about what the line box's own leading
     gives you. The head of "Let's work together" is meant to start
     behind the white edge and be dimmed by it. */
  /* 207px so that once the curve has cleared the top of the screen the
     title sits about 190px down, which is where the reference has it
     in the same state */
  /* 245 = the 221 that lands the title where the reference has it at
     the foot of the page, plus the 24 the section is taller than the
     screen so the flattened edge clears the top */
  padding:clamp(98px,12.76vw,245px) calc(var(--gutter) + var(--indent)) clamp(16px,1.6vw,30px);
}
/* the curve and the shade that used to live at the top of this
   section are gone: the white layer above carries its own edge now,
   and it takes the shadow with it when it leaves */
/* The block rides up under the white layer and settles into place as
   the section arrives — a parallax against the page, so "Let's work
   together" comes out from behind the edge rather than simply
   scrolling into view. The offset is written by the loop. */
.contact__inner{position:relative;z-index:1;display:flex;flex-direction:column}

.contact__title{
  position:relative;
  /* Measured off a still of the reference at 1920x910: cap height 86px
     -> a 120px em, and the two lines' caps 126px apart -> 1.05, not
     the 1.21 this carried. */
  font-size:clamp(46px,6.25vw,120px);line-height:1.05;
  /* forces the break after "work" so the second line returns to the
     rail under the avatar, as the reference does */
  max-width:7em;
  margin-bottom:clamp(44px,4.84vw,93px);
}
.contact__avatar{
  display:inline-block;vertical-align:middle;
  width:clamp(48px,5.16vw,99px);aspect-ratio:1;
  border-radius:50%;overflow:hidden;
  margin-right:clamp(12px,1.9vw,37px);
}
.contact__avatar img{width:100%;height:100%;object-fit:cover;object-position:50% 18%}
/* sits out at the right rail, level with the second title line. It is
   a sibling of the title, not a child, because the title carries a 7em
   measure that `right:0` would otherwise resolve against. */
.contact__arrow{
  /* the reference sits it 176px above the rule; 156 from the section's
     top is what puts it there */
  position:absolute;right:0;top:clamp(58px,8.1vw,156px);
  width:clamp(16px,1.4vw,27px);height:clamp(16px,1.4vw,27px);
  stroke:rgba(255,255,255,.7);
}

/* the disc sits on the rule, exactly as in the reference */
.contact__rule{
  position:relative;
  border-top:1px solid rgba(255,255,255,.22);
  margin-bottom:clamp(40px,4.7vw,91px);
}
/* same centring as the about globe, and for the same reason: the
   magnetic pointer writes to `transform`, so the disc cannot depend
   on it to sit on the line */
.contact__rule .disc{
  /* This is the RESTING offset, and the loop adds up to 60px of
     rightward drift on top of it as the disc rises — so the number
     that matters is what is left at the foot of the page, where the
     drift is at its cap:

       140 here - 60 of drift = 80px in from the rule's end

     The reference settles at 104px in. 80 keeps the disc a touch right
     of that, which is the nudge asked for, without pushing it into the
     corner: at 70 the drift was leaving only 10px of rule showing past
     it. */
  position:absolute;right:clamp(0px,7.3vw,140px);
  top:0;margin-top:calc(clamp(140px,10.9vw,210px) / -2);
}

.contact__links{display:flex;flex-wrap:wrap;gap:var(--gap);margin-bottom:clamp(70px,10vw,150px)}

/* ---- footer -------------------------------------------- */
/* The footer lines up with the header, not with the contact block it
   sits inside. Measured off the reference at 1920: its labels start at
   x=58 and the socials end 59 from the right — the same rail the nav
   runs on, so it is pulled back out of the section's text rail. */
.foot{
  display:grid;grid-template-columns:repeat(3,minmax(0,1fr));
  gap:32px;align-items:end;
  /* the section is a viewport tall and the footer belongs at its foot,
     not straight under the pills — the slack goes above it */
  margin-top:auto;
  margin-left:calc(var(--edge-rail) - var(--gutter) - var(--indent));
  margin-right:calc(var(--edge-rail) - var(--gutter) - var(--indent));
}
.foot__label{
  font-size:var(--t-cap);text-transform:uppercase;letter-spacing:.18em;
  /* the reference's label-to-value gap is tighter: its whole footer
     block measures 47px against the 57 this had */
  color:var(--fog);margin-bottom:4px;
}
.foot__value{font-size:var(--t-body)}
.foot__links{display:flex;flex-wrap:wrap;gap:6px 22px;font-size:var(--t-body);justify-content:flex-end}
/* the same hover as the overlay's socials: it lifts to full white and
   opens the rule underneath, rather than turning violet */
.foot__links a{color:var(--fog);transition:color .35s var(--ease)}
.foot__links a:hover,.foot__links a:focus-visible{color:var(--bone)}
.foot__col--end{text-align:right}


/* ============================================================
   INNER PAGES
   Pages without a grey hero: the ground is white from the top.
   The nav still trades places with the round toggle on scroll exactly
   as it does on the home page — the toggle is absent at the top of
   the page and fades in once you are 60% of a viewport down.
   ============================================================ */
body.no-hero .page-top{padding-top:clamp(120px,14vw,220px)}

/* the headline every inner page opens with */
.page-head{
  background:var(--white);
  padding:0 var(--gutter) clamp(48px,6vw,90px);
}
.page-head h1{
  padding-left:var(--indent);
  /* 78px at 1280 and 102px at 1920, both measured off the reference */
  font-size:clamp(46px,6.93vw,116px);line-height:1.08;max-width:11em;
}
/* 404: the headline plus one line and two ways back */
.nf__text{padding-left:var(--indent);margin-top:1.4em;max-width:30em;font-size:clamp(17px,1.4vw,22px);line-height:1.5}
.nf__links{padding-left:var(--indent);margin-top:2em;display:flex;flex-wrap:wrap;gap:12px}


/* ============================================================
   WORK INDEX
   A table, not a card grid: client, location, services and year
   are all readable without hovering anything.
   ============================================================ */
.filters{
  padding:0 var(--gutter) clamp(20px,2.4vw,34px);
  display:flex;flex-wrap:wrap;align-items:center;gap:clamp(16px,2vw,34px);
  padding-bottom:clamp(48px,6vw,110px);
}
.filters__set{padding-left:var(--indent);display:flex;flex-wrap:wrap;gap:clamp(8px,.7vw,12px)}
/* pills, as the reference sets them: outlined at rest, the pressed
   one filled with ink, the count small and raised */
.filter{
  display:inline-flex;align-items:center;
  padding:clamp(14px,1.4vw,24px) clamp(24px,2.4vw,42px);
  border-radius:var(--r-pill);box-shadow:var(--ring-dark);
  font-size:clamp(15px,1.15vw,20px);color:var(--ink);cursor:pointer;
  transition:background-color .35s var(--ease),color .35s var(--ease);
}
.filter sup{font-size:.6em;margin-left:.3em;vertical-align:super;line-height:0}
.filter:hover{background:rgba(28,29,32,.06)}
.filter[aria-pressed="true"]{background:var(--obsidian);color:var(--bone);box-shadow:none}

/* list / grid switch, pushed to the right end of the filter row */
.views{margin-left:auto;display:flex;gap:clamp(8px,.8vw,12px)}
.view{
  display:grid;place-items:center;
  width:clamp(52px,5vw,74px);aspect-ratio:1;border-radius:50%;
  box-shadow:var(--ring-dark);color:var(--ink);cursor:pointer;
  transition:background-color .35s var(--ease),color .35s var(--ease);
}
.view svg{width:36%;fill:none;stroke:currentColor;stroke-width:1.2}
.view:hover{background:rgba(28,29,32,.06)}
.view[aria-pressed="true"]{background:var(--obsidian);color:var(--bone);box-shadow:none}

.wtable{padding:0 var(--gutter) var(--section)}
.wtable__head,
.wrow{
  display:grid;
  grid-template-columns:minmax(0,2fr) minmax(0,1fr) minmax(0,1.4fr) minmax(0,.5fr);
  gap:clamp(12px,2vw,32px);align-items:baseline;
}
.wtable__head{
  padding:0 0 14px;border-bottom:1px solid rgba(28,29,32,.16);
  font-size:var(--t-cap);text-transform:uppercase;letter-spacing:.18em;
  color:var(--ink-soft);
}
.wrow{
  position:relative;z-index:1;
  padding:clamp(20px,2.4vw,34px) 0;
  border-bottom:1px solid rgba(28,29,32,.16);
  transition:color .45s var(--ease);
}
.wrow__client{font-size:clamp(24px,2.64vw,46px);line-height:1.15}
.wrow span:not(.wrow__client){font-size:clamp(15px,.97vw,18.5px);color:var(--ink-soft)}
.wtable__list:hover .wrow:not(:hover){color:rgba(28,29,32,.3)}
.wtable__list:hover .wrow:not(:hover) span{color:rgba(28,29,32,.3)}
.wrow.is-out{display:none}

/* grid view: the same rows as cards. Location drops out, the year
   moves up beside the name. Shots only exist once grid is chosen,
   except on touch, where the list carries them too. */
@media (pointer:fine){.wtable:not(.is-grid) .row__shot{display:none}}
.wtable.is-grid .wtable__head,
.wtable.is-grid .preview{display:none}
.wtable.is-grid .wtable__list{
  display:grid;grid-template-columns:repeat(2,minmax(0,1fr));
  gap:clamp(40px,5vw,90px) clamp(20px,3vw,60px);
}
.wtable.is-grid .wrow{
  grid-template-columns:minmax(0,1fr) auto;row-gap:10px;
  padding:0;border-bottom:0;
}
.wtable.is-grid .wrow > span:nth-of-type(2){display:none}
.wtable.is-grid .wrow > span:nth-of-type(3){grid-column:1 / -1;grid-row:3;padding-top:12px;border-top:1px solid rgba(28,29,32,.16)}
.wtable.is-grid .wrow > span:nth-of-type(4){grid-row:2;grid-column:2}
.wtable.is-grid .wrow__client{grid-row:2}
.wtable.is-grid .wrow.is-out{display:none}
.wtable__foot{
  padding-top:clamp(28px,3vw,48px);
  display:flex;justify-content:center;
}

/* ============================================================
   PROJECT PAGE
   ============================================================ */
.pjhero{background:var(--white);padding:0 var(--gutter) clamp(28px,3vw,48px)}
.pjhero h1{padding-left:var(--indent);font-size:clamp(48px,7.7vw,141px);line-height:1.06}
.pjhero .label{padding-left:var(--indent);margin-top:clamp(20px,2vw,32px)}
/* inside the margins, as the reference sets it: the page rail frames
   the shot rather than the shot running to the glass */
.pjmedia{
  position:relative;margin:0 var(--gutter);
}
.pjmedia img{width:100%;aspect-ratio:16/9;object-fit:cover;background:#efefef}
/* the live link rides the image's top edge, half over it, so the one
   iris moment on the page sits where the eye already is */
.pjlive{
  position:absolute;z-index:2;top:0;right:clamp(24px,6vw,110px);
  translate:0 -50%;
}

/* before the image, three columns, each label on its own rule */
.pjmeta{
  background:var(--white);
  padding:clamp(40px,5vw,80px) calc(var(--gutter) + var(--indent)) clamp(90px,9vw,150px);
  display:grid;grid-template-columns:repeat(3,minmax(0,1fr));
  gap:clamp(24px,3vw,48px);align-items:start;
}
.pjmeta__col{padding-left:0}
.pjmeta dt{
  font-size:var(--t-cap);text-transform:uppercase;letter-spacing:.18em;
  color:var(--ink-soft);margin-bottom:clamp(18px,2vw,34px);
  padding-bottom:clamp(18px,2vw,34px);border-bottom:1px solid rgba(28,29,32,.16);
}
.pjmeta dd{font-size:clamp(15px,.97vw,18.5px);line-height:1.9;margin:0}

/* body media: reserve the box before the image lands */
.pjbody{background:var(--white);padding:0 var(--gutter) var(--section);display:flex;flex-direction:column;gap:var(--gap)}
.pjshot{background:#efefef;overflow:hidden;border-radius:var(--r-card)}
.pjshot img{width:100%;aspect-ratio:16/10;object-fit:cover}
.pjshot--tall img{aspect-ratio:4/5}
.pjpair{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--gap)}
.pjtrio{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--gap);max-width:960px;margin-inline:auto}
.pjshot--phone img{aspect-ratio:390/844}
/* a before/after inside a case body sits centred in the column */
.pjbody .ba{align-self:center}
.pjbody .ba + .ba-sec__note{align-self:center;margin-top:0}
.pjnote{
  padding-left:var(--indent);margin:clamp(24px,3vw,48px) 0;
  font-size:clamp(22px,2.52vw,50px);line-height:1.42;max-width:18.4em;
}

/* next case: a full-width plate, title visible without hover */
.nextcase{
  position:relative;display:block;
  background:var(--obsidian);color:var(--bone);
  padding:clamp(60px,7vw,110px) var(--gutter);
  overflow:hidden;
}
.nextcase__label{
  font-size:var(--t-cap);text-transform:uppercase;letter-spacing:.18em;
  color:var(--fog);margin-bottom:18px;padding-left:var(--indent);
}
.nextcase__title{
  padding-left:var(--indent);
  font-size:clamp(46px,6.93vw,116px);line-height:1.1;
}
.nextcase__media{
  position:absolute;right:var(--gutter);top:50%;transform:translateY(-50%);
  width:clamp(160px,22vw,420px);aspect-ratio:16/10;
  overflow:hidden;border-radius:var(--r-card);opacity:.55;
  transition:opacity .6s var(--ease),transform .6s var(--ease);
}
.nextcase:hover .nextcase__media{opacity:1;transform:translateY(-50%) scale(1.02)}
.nextcase__media img{width:100%;height:100%;object-fit:cover}
/* the way back to the index, on the same dark plate as the next case */
.pjall{
  background:var(--obsidian);text-align:center;
  padding:0 var(--gutter) clamp(60px,7vw,110px);
}

/* ============================================================
   ABOUT
   ============================================================ */

.svc{background:var(--paper);padding:var(--section) var(--gutter)}
.svc__title{font-size:clamp(29px,3.96vw,70px);line-height:1.15;margin-bottom:clamp(28px,4vw,64px)}
/* the reference sets these as columns on the page gutter, not as
   full-width rows on the text rail */
.svc__grid{
  display:grid;
  grid-template-columns:repeat(4,minmax(0,1fr));
  gap:clamp(32px,5.4vw,69px);
}
.svc__no{
  font-size:clamp(13px,1.18vw,15px);letter-spacing:.18em;
  color:var(--ink-soft);
  padding-bottom:clamp(14px,1.6vw,26px);
  margin-bottom:clamp(24px,3.4vw,54px);
  border-bottom:1px solid rgba(28,29,32,.18);
}
.svc__name{font-size:clamp(22px,2.5vw,32px);line-height:1.2;margin-bottom:12px;max-width:14em}
/* the reference runs a 313px column; capped here so a wide two-up
   cell does not stretch the line beyond a comfortable measure */
.svc__body{font-size:clamp(15px,1.44vw,18.5px);line-height:1.75;color:var(--ink-soft);max-width:22em}

/* Before/after. The text sits left and the slider right, so the
   portrait in .cred below (image left, text right) alternates with it. */
.ba-sec{
  background:var(--white);
  padding:var(--section) var(--gutter) 0;
  display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);
  gap:clamp(28px,5vw,90px);align-items:center;
}
.ba-sec__text{padding-left:var(--indent)}
.ba-sec__title{margin-top:clamp(14px,1.4vw,22px);font-size:clamp(26px,3.52vw,64px);line-height:1.15;max-width:9em}
.ba-sec__body{
  margin-top:clamp(20px,2.4vw,40px);
  font-size:clamp(15px,1.44vw,18.5px);line-height:1.75;
  color:var(--ink-soft);max-width:26em;
}
.ba-sec__note{margin-top:1.2em;font-size:clamp(13px,1.1vw,15px);color:var(--ink-soft)}

/* --pos is the share of the original render on show, from the left.
   main.js writes it; 50% is the no-script state. pan-y hands vertical
   swipes back to the page, so the slider never traps a phone scroll. */
.ba{
  --pos:50%;
  position:relative;overflow:hidden;border-radius:var(--r-card);background:#efefef;
  max-width:640px;width:100%;justify-self:end;
  cursor:ew-resize;touch-action:pan-y;user-select:none;-webkit-user-select:none;
}
.ba__img{width:100%;aspect-ratio:4/5;object-fit:cover;pointer-events:none}
.ba__img--before{position:absolute;inset:0;height:100%;clip-path:inset(0 calc(100% - var(--pos)) 0 0)}
/* rgba .72 keeps white text above 4.5:1 even over the brightest sky */
.ba__tag{
  position:absolute;top:14px;padding:6px 12px;border-radius:var(--r-pill);
  background:rgba(28,29,32,.72);color:var(--bone);
  font-size:var(--t-cap);text-transform:uppercase;letter-spacing:.18em;line-height:1.4;
}
.ba__tag--before{left:14px}
.ba__tag--after{right:14px}
.ba__handle{
  position:absolute;top:0;bottom:0;left:var(--pos);width:2px;margin-left:-1px;
  background:var(--bone);box-shadow:0 0 0 1px rgba(28,29,32,.25);
}
/* the grip: an iris disc with a chevron each way. It rides the line
   but stops short of each edge (26px radius + 6px for the focus ring),
   so neither is cut off at 0% or 100%. */
.ba::after{
  content:"";position:absolute;top:50%;left:clamp(32px,var(--pos),calc(100% - 32px));
  width:52px;height:52px;margin:-26px 0 0 -26px;border-radius:50%;
  background:var(--iris) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 52 52' fill='none' stroke='%23fff' stroke-width='1.6'%3E%3Cpolyline points='21,19 14,26 21,33'/%3E%3Cpolyline points='31,19 38,26 31,33'/%3E%3C/svg%3E") center/100% no-repeat;
  box-shadow:0 0 0 2px var(--bone);
}
/* the range input is the keyboard and screen-reader control; its
   focus ring is drawn on the grip, which is what the eye follows */
.ba:has(.ba__range:focus-visible)::after{
  box-shadow:0 0 0 2px var(--bone),0 0 0 5px var(--ink);
}

/* ============================================================
   ABOUT
   Measured off the reference: headline, a rule carrying an iris
   globe, a narrow note column with an arrow and a caption beside a
   large right-hand portrait, then the services banded on paper.
   ============================================================ */
.ab-head{background:var(--white);padding:0 var(--gutter) 0}
.ab-head h1{
  padding-left:var(--indent);
  font-size:clamp(46px,6.93vw,116px);line-height:1.08;max-width:11em;
}
/* the same rule-carries-a-disc pattern as the contact block */
/* The disc now straddles the line, so half of it (5.6vw) rises into
   the space above. The reference leaves about 40px of air between the
   headline and the top of the disc at 1920, which is where the 145px
   comes from: 105 for the half disc, 40 for the air. */
.ab-rule{
  position:relative;margin-left:var(--indent);margin-right:var(--indent);
  margin-top:clamp(60px,7.55vw,145px);
  border-top:1px solid rgba(28,29,32,.16);
  min-height:clamp(60px,6vw,110px);
}
/* Measured off the reference recording at a 1920 viewport: a 210px
   disc whose centre sits ON the rule, not below it, 104px in from the
   rule's right end. Centring with a negative margin instead of
   translateY(-50%) leaves `transform` free for the magnetic pointer,
   which writes to it directly and would otherwise drop the disc by
   half its own height the moment it is hovered. */
.ab-globe{
  --size:clamp(96px,11.2vw,210px);
  position:absolute;right:clamp(16px,5.6vw,108px);
  top:0;margin-top:calc(var(--size) / -2);
  width:var(--size);
}
/* the cycle is a hover state, not an idle loop. In the reference
   recording the pointer sits on the disc for the first 12.8s and the
   colour turns the whole time; it leaves at 12.83s and the disc holds
   iris for the remaining 1.2s, where every earlier cycle had moved on
   to purple within 0.35s. Leaving restores `--iris` through the .disc
   background-color transition. */
.ab-globe:hover,.ab-globe:focus-visible{
  animation:disc-shift 6s cubic-bezier(.45,0,.55,1) infinite;
}
.ab-globe svg{
  width:38%;height:38%;fill:none;stroke:currentColor;stroke-width:1.1;
}
/* The disc breathes between the iris and a red. Measured frame by
   frame off the reference at 30fps: #455be8 holds through 0.70-1.05s,
   #fd4242 through 3.80-4.05s, and the peaks land exactly 6.00s apart,
   so it is one 6s loop with the turn at the halfway mark. The ramp
   between them fits an ease-in-out quad (that bezier) more closely
   than the browser's own `ease-in-out`, which leaves each end holding
   for about a third of a second before the colour moves.

   Because the animation owns background-color, the `.disc--accent`
   hover colour no longer lands on this disc; the magnetic pull is its
   hover feedback, which is also how the reference behaves. */
@keyframes disc-shift{
  0%,100%{background-color:var(--iris)}
  50%    {background-color:var(--ember)}
}

.ab-lead{
  background:var(--white);
  padding:clamp(28px,3.4vw,56px) var(--gutter) clamp(60px,8vw,140px);
  display:grid;grid-template-columns:minmax(0,1fr) minmax(0,66%);
  gap:clamp(24px,3vw,52px);align-items:start;
}
.ab-lead__note{position:relative;padding-left:var(--indent)}
/* the arrow sits out on the page gutter, left of the text rail */
.ab-lead__arrow{
  position:absolute;left:0;top:.35em;
  width:clamp(18px,1.6vw,27px);height:clamp(18px,1.6vw,27px);
}
.ab-lead__note p{
  font-size:clamp(15px,1.44vw,18.5px);line-height:1.75;max-width:14em;
}
.ab-lead__cap{margin-top:1.6em;color:var(--ink-soft)}
.ab-lead__media{background:#efefef;overflow:hidden}
.ab-lead__media img{width:100%;aspect-ratio:5/6;object-fit:cover}


.cred{
  background:var(--white);
  padding:var(--section) var(--gutter);
  display:grid;grid-template-columns:minmax(0,.9fr) minmax(0,1.1fr);
  gap:clamp(28px,5vw,90px);align-items:start;
}
.cred__media{margin-left:var(--indent);background:#efefef;overflow:hidden}
.cred__media img{width:100%;aspect-ratio:4/5;object-fit:cover}
.cred__seal{
  display:block;width:clamp(56px,5.4vw,104px);aspect-ratio:1;
  margin-bottom:clamp(24px,3vw,52px);
  border-radius:50%;box-shadow:var(--ring-dark);
  display:grid;place-items:center;
  font-size:var(--t-cap);letter-spacing:.18em;color:var(--ink-soft);
}
.cred__title{padding-left:0;font-size:clamp(26px,3.52vw,64px);line-height:1.15;max-width:9em}
.cred__body{
  margin-top:clamp(20px,2.4vw,40px);
  font-size:clamp(15px,1.44vw,18.5px);line-height:1.75;
  color:var(--ink-soft);max-width:30em;
}

/* Four cells cannot hold the reference's 313px measure in the space
   three occupy, so they go two-up until the viewport can afford it. */
@media (max-width:1600px){
  .svc__grid{grid-template-columns:repeat(2,minmax(0,1fr))}
}

@media (max-width:820px){
  .wtable__head{display:none}
  .wrow{grid-template-columns:minmax(0,1fr) minmax(0,auto);row-gap:4px}
  .wrow__client{grid-column:1 / -1}
  .pjmeta{grid-template-columns:repeat(2,minmax(0,1fr))}
  .pjpair{grid-template-columns:minmax(0,1fr)}
  .nextcase__media{position:static;transform:none;width:100%;margin-top:28px;opacity:1}
  .nextcase:hover .nextcase__media{transform:none}
  .cred{grid-template-columns:minmax(0,1fr)}
  .cred__media{margin-left:0}
  .ab-lead{grid-template-columns:minmax(0,1fr)}
  .ba-sec{grid-template-columns:minmax(0,1fr)}
  .ba{justify-self:start}
}
@media (max-width:620px){
  .pjmeta{grid-template-columns:minmax(0,1fr)}
  .wtable.is-grid .wtable__list{grid-template-columns:minmax(0,1fr)}
}

/* ============================================================
   RESPONSIVE
   ============================================================ */
@media (max-width:1200px){
  /* the note column tightens before it is given up: the reference
     runs it at about a quarter of the measure at this width */
  .intro{grid-template-columns:minmax(0,1fr) minmax(0,24%)}
}

/* The portrait scales with the height, so below about 7:5, or once the
   width runs short, the shoulders reach the role beside the head. Every
   lower spot straddles grey and overshirt, where neither white nor ink
   holds 4.5:1, so the role moves up into the clear grey band under the
   name mark, in ink. The arrow goes: there is no room above the text
   there, and the line reads straight on from the mark. */
@media (max-width:1000px),(max-aspect-ratio:7/5){
  .hero__role{
    left:var(--edge-rail);right:auto;
    top:calc(clamp(64px,5.2vw,100px) + 4px);bottom:auto;transform:none;
  }
  .hero__arrow{display:none}
  .hero__avail{margin-top:.35em}
  .hero__where{top:auto;bottom:30vh;transform:none}
}

@media (max-width:820px){
  /* The intro keeps its two columns well past 1000px — the reference
     still has the statement left and the note right at 958, with the
     disc under the note rather than beside it. Stacking them that
     early put the cue between the two and turned the note row into a
     line of its own. */
  .intro{grid-template-columns:minmax(0,1fr);gap:44px}
  .intro__side{flex-direction:row;align-items:center;justify-content:space-between;width:100%}
}

@media (max-width:820px){
  .svc__grid{grid-template-columns:minmax(0,1fr)}
  /* one rail: the second indent stops earning its space */
  :root{--indent:0px}
  .nav__links{display:none}
  /* with no text nav in the hero, the round toggle is always on */
  .burger{opacity:1;transform:none;pointer-events:auto}
  /* 60px = the loop's rightward drift at its cap, so the disc settles
     flush with the rule instead of 60px past the viewport */
  .contact__rule .disc{right:60px}
  .contact__arrow{display:none}
  .foot{grid-template-columns:repeat(2,minmax(0,1fr))}
  .foot__col--end{grid-column:span 2;text-align:left}
  .foot__links{justify-content:flex-start}
}

@media (max-width:620px){
  .hero__media img{height:78%}
  /* the pill keeps bleeding off the left edge at every width */
  .hero__where{left:0;bottom:34vh;top:auto;transform:none}
  .intro__side{flex-direction:column;align-items:flex-start}
  /* the reference drops the disc to the right edge once stacked */
  .intro__side .disc{align-self:flex-end}
  .row{grid-template-columns:minmax(0,1fr);row-gap:4px}
  .row__title{padding-left:0}
  .row:hover .row__title{transform:translateX(clamp(10px,3vw,20px))}
  /* the preview is a pointer affordance; it means nothing on touch */
  .preview{display:none}
  /* the reference drops its archive strip on phones; the tiles above
     already carry the imagery there */
  .archive{display:none}
  /* the reference runs the page on its 20px edge here, not 34px */
  :root{--gutter:var(--edge)}
  .foot{grid-template-columns:minmax(0,1fr)}
  .foot__col--end{grid-column:span 1}
}

/* Landscape phones, and any window under 500px tall. The name mark
   scales with the width, so here it takes a third of the height, and
   the left column (role, then pill, then mark) no longer fits: the pill
   ran into the mark, and at 568x320 it covered the role's link. The
   role moves to the top right, under the round toggle, where the grey
   is clear of the head; the pill keeps the left side to itself, centred
   between the nav row and the top of the mark. */
@media (orientation:landscape) and (max-height:500px){
  .hero__role{
    left:auto;right:var(--edge-rail);text-align:right;
    top:calc(clamp(14px,1.8vw,33px) + clamp(56px,4.8vw,92px) + 12px);
    bottom:auto;transform:none;
  }
  .hero__arrow{display:none}
  /* on the narrowest (568 wide) the link would run into the ear, so it
     breaks onto two lines there; from 667 up it stays on one */
  .hero__avail{margin-top:.35em;max-width:40vw}
  /* the mark's top sits its line box (.92 of the display size) above
     its own bottom offset */
  .hero__where{
    top:calc(50% + (clamp(64px,5.2vw,100px) - clamp(10px,4.4vh,40px) - .92 * var(--t-display)) / 2);
    bottom:auto;transform:translateY(-50%);
  }
}

/* ============================================================
   REDUCED MOTION
   ============================================================ */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:.001ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.001ms !important;
    scroll-behavior:auto !important;
  }
  .word{transform:none}
  [data-reveal]{opacity:1;transform:none}
  .preloader{display:none}
  .hero__track,.archive__track{transform:none !important}
  /* one static row of plates instead of two moving ones */
  .archive__row{overflow-x:auto}
  .hero__arrow{animation:none}
  .mer{animation:none}
  .ab-globe:hover{animation:none}
  body.is-smooth .scroll__content{position:static;transform:none !important}
  /* no delayed visibility, so the panel appears and goes at once */
  .overlay{transition:none !important}
  .overlay__curve{display:none}
  /* the round toggle must stay reachable without the scroll hook */
  .burger{opacity:1;transform:none;pointer-events:auto}
  .nav{opacity:1 !important;transform:none !important}
}

/* Hidden until real social profiles exist */
[hidden] { display: none !important; }
/* while socials are hidden, the time column takes their right-edge slot */
@media (min-width:821px){
  .foot__col:has(+ .foot__col--end[hidden]){grid-column:-2/-1;text-align:right}
}
