/* 21 Portfolios — shared design system.
   Three colours, nothing else: ink, paper, red. */

/* Self hosted so there is no third party request on first paint, and no
   outside service seeing who visits. Both are variable fonts: Archivo carries
   the width axis the headings animate, which the old Google request pinned to
   100% because it only asked for weight. Latin and latin-ext only.

   A font is fetched under CORS, so a page opened straight off disk cannot read
   even its own local copy and falls back to the system sans. Chromium treats
   that as terminal rather than trying another src, so there is no fallback to
   offer here. Preview over http, not file://. */
@font-face{
  font-family:'Archivo';
  src:url('fonts/archivo-latin.woff2') format('woff2');
  font-weight:400 900; font-stretch:62% 125%; font-style:normal; font-display:swap;
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family:'Archivo';
  src:url('fonts/archivo-latin-ext.woff2') format('woff2');
  font-weight:400 900; font-stretch:62% 125%; font-style:normal; font-display:swap;
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face{
  font-family:'Inter';
  src:url('fonts/inter-latin.woff2') format('woff2');
  font-weight:400 600; font-style:normal; font-display:swap;
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family:'Inter';
  src:url('fonts/inter-latin-ext.woff2') format('woff2');
  font-weight:400 600; font-style:normal; font-display:swap;
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

:root{
  --ink:#0A0807;
  --paper:#F2EBE1;
  --red:#FF2244;

  --display:'Archivo', 'Helvetica Neue', sans-serif;
  --body:'Inter', system-ui, sans-serif;

  --ease:cubic-bezier(0.16,1,0.3,1);
  --ease-io:cubic-bezier(0.76,0,0.24,1);


  --fg:var(--paper);
  /* What reads against a block of --fg, for anything that fills with it. */
  --on-fg:var(--ink);
  --dim:rgba(242,235,225,0.6);
  --rule:rgba(242,235,225,0.18);
  --gutter:clamp(20px,4vw,64px);
  /* Content edge for full bleed fixed elements, so they line up with .shell
     past its 1440 cap instead of running to the viewport. */
  --edge:max(var(--gutter), calc((100% - 1440px) / 2 + var(--gutter)));
}

/* Cross document view transitions. Navigation between pages is a scripted
   fade rather than a white flash. Browsers without support just navigate
   normally, so there is nothing to fall back to. */
@view-transition{ navigation:auto; }
::view-transition-old(root){ animation:vtOut .3s var(--ease-io) both; }
::view-transition-new(root){ animation:vtIn .44s var(--ease) both; }
@keyframes vtOut{ to{ opacity:0; transform:scale(.992); } }
@keyframes vtIn{ from{ opacity:0; transform:translateY(16px); } }

/* The header is the one thing that should hold still across a navigation.
   It gets its own group and the browser's own crossfade, kept short: two
   identical headers crossfade to exactly the same picture, so an ordinary
   link shows no change at all, and when they differ (a menu link, leaving
   the open menu's X for the next page's closed lines) one dissolves into the
   other. Freezing both, as this used to, stacked the X and the lines on top
   of each other for the length of the transition. */
.site-head{ view-transition-name:site-head; }
::view-transition-group(site-head){ animation-duration:.26s; }

/* A project carrying over into its case study. The tile's picture and the
   case study's opening section share a name (vt-shot-<page>), so the picture
   grows into the section, title and all, and the section comes up through
   it; going back, the section shrinks into the tile. For that pair the page
   behind does not crossfade (the head snippet marks the trip as case-open or
   case-close): the page arriving sits still underneath and the page leaving
   fades off the top (z-index, or the arriving page simply covers it), so
   there is no grey wash between a paper page and an ink one. On any other trip only one side has the name, and that side just
   fades with the page (the :only-child rules), so nothing lingers. */
/* Named only for a trip marked case-open or case-close, so Home to Work,
   the menu and every other link never move the picture. */
.vt-shot{ view-transition-class:vt-shot; view-transition-name:none; }
html:active-view-transition-type(case-open, case-close) .vt-shot{ view-transition-name:var(--vt); }
/* Moving from the first frame: a curve that eases in sat the picture still while the page around it was already gone. */
::view-transition-group(*.vt-shot){ animation-duration:.6s; animation-timing-function:cubic-bezier(.22,.9,.24,1); }
::view-transition-image-pair(*.vt-shot){ overflow:hidden; }
::view-transition-old(*.vt-shot),
::view-transition-new(*.vt-shot){ height:100%; object-fit:cover; object-position:left top; }   /* the title is on the left: never cropped */
::view-transition-old(*.vt-shot){ animation:vtShotOut .34s ease .16s both; }
::view-transition-new(*.vt-shot){ animation:vtShotIn .3s ease .08s both; }
@keyframes vtShotOut{ to{ opacity:0; } }
@keyframes vtShotIn{ from{ opacity:0; } }
::view-transition-old(*.vt-shot):only-child{ animation:vtOut .3s var(--ease-io) both; }
::view-transition-new(*.vt-shot):only-child{ animation:vtIn .44s var(--ease) both; }
html:active-view-transition-type(case-open, case-close)::view-transition-new(root){ animation:none; }
html:active-view-transition-type(case-open, case-close)::view-transition-old(root){ animation:vtFade .3s ease both; mix-blend-mode:normal; z-index:1; }
@keyframes vtFade{ to{ opacity:0; } }
html.paused::view-transition-group(*),
html.paused::view-transition-old(*),
html.paused::view-transition-new(*){ animation:none !important; }
/* Arriving through a page transition the opening section is placed, not
   revealed: the transition is its entrance. Left to the reveals it slid up a
   second time, a beat after the page had already arrived. */
.vt-arrive .p-hero .rv > *{ transform:none; transition:none; }
.vt-arrive .p-hero .fade{ opacity:1; transform:none; transition:none; }

*{ box-sizing:border-box; margin:0; padding:0; }
html{ background:var(--ink);
  /* Locking the page for the menu removes a Windows or Linux scrollbar, and
     the header jumped sideways by its width under the cursor. Its room is
     kept instead. Overlay scrollbars (Mac, phones) are unaffected. */
  scrollbar-gutter:stable; }
html.lenis,html.lenis body{ height:auto; }
.lenis.lenis-smooth{ scroll-behavior:auto !important; }

body{
  background:var(--ink); color:var(--paper);
  font-family:var(--body); font-size:16px; line-height:1.5;
  -webkit-font-smoothing:antialiased; overflow-x:clip;
}

a{ color:inherit; text-decoration:none; }
button{ font:inherit; color:inherit; background:none; border:none; cursor:pointer; }
img{ display:block; max-width:100%; }
::selection{ background:var(--red); color:var(--paper); }
:focus-visible{ outline:2px solid var(--red); outline-offset:4px; }

/* Each section paints its own colour. Nothing cross-fades, so nothing flickers. */
.s{ position:relative; background:var(--ink); color:var(--paper); }
.s-light{ background:var(--paper); color:var(--ink); --fg:var(--ink); --on-fg:var(--paper); --dim:rgba(10,8,7,0.62); --rule:rgba(10,8,7,0.16); }
.s-red{ background:var(--red); color:var(--ink); --fg:var(--ink); --on-fg:var(--paper); --dim:rgba(10,8,7,0.72); --rule:rgba(10,8,7,0.2); }

/* The overscan only has to cover the drift, and the drift is at most 3% of
   this element's own size. At inset -120% the element was three and a half
   viewports across in each direction, so every step of the animation repainted
   and re-blended about eleven screens' worth of pixels, forever, on every page.
   At -6% it covers exactly the same ground for a fraction of the work. */
/* No mix-blend-mode, deliberately. A blended layer has to be re-composited
   against everything beneath it whenever any of that changes, and this one
   is full screen and animated, so every step of its drift forced a full
   screen re-blend. Measured: the homepage ran at 30fps with it and 60 with
   it off, and slowing everything underneath down did not help at all
   because the cost was never in the drawing. Against ink and paper at three
   percent the two blend modes differ by a mean of 1.3 of 255, which is
   nothing, so the grain keeps its drift and the page keeps its frame rate. */
.grain{
  position:fixed; inset:-6%; z-index:400; pointer-events:none;
  contain:strict;
  opacity:.035;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23n)'/%3E%3C/svg%3E");
  animation:grainShift 700ms steps(2) infinite;
}
@keyframes grainShift{
  0%{ transform:translate(0,0); } 25%{ transform:translate(-3%,2%); }
  50%{ transform:translate(2%,-3%); } 75%{ transform:translate(-2%,-2%); }
  100%{ transform:translate(3%,1%); }
}

/* Topographic contour field, drawn to canvas in site.js. Lines are painted in
   the section's own text colour, so it inverts with the section and never
   introduces a fourth colour. */
/* The contour lines are stroked in the host's own colour at the host's own
   opacity, so a theme sets both from here. Red on the dark and light panels;
   on the red one red would sink into its own background, so they stay ink.
   Red carries far less contrast than cream did against either, hence the
   heavier weighting on the first two. */
.bg{ position:absolute; inset:0; z-index:0; pointer-events:none; overflow:hidden; color:var(--red); opacity:.38; }
.s-light .bg{ opacity:.24; }
.s-red .bg{ color:var(--ink); opacity:.13; }
.bg canvas{ position:absolute; inset:0; width:100%; height:100%; display:block; }

.s > .shell,.s > .container{ position:relative; z-index:2; }

/* Type */
.display{
  font-family:var(--display); font-weight:800;
  font-variation-settings:'wght' 800,'wdth' 112;
  letter-spacing:-0.035em; line-height:0.86; text-transform:uppercase;
}
/* A display heading beside body copy needs its cap height dropped onto the
   copy's first baseline, or the two read as misaligned even though the boxes
   line up. line-height 0.86 against 1.65 is most of the gap. */
.align-copy{ padding-top:0.16em; }
/* Sized by how many characters it has to fit, not by a single vw figure that
   happens to suit a four letter word. At 16.5vw flat, Work and Start sat
   nicely and Questions ran 160px past the shell at 1100 wide, clipped rather
   than wrapped, at every width below 1440. The constant is calibrated from
   Archivo at this weight and tracking: the ink of a word is about 0.74 of its
   font size per character, and the shell is about 0.92 of the viewport, so
   118vw over the character count fills the measure without passing it.
   --chars is set per heading and means width in average characters rather
   than a literal count, because letters are not the same width: WORK is four
   characters of W and O and needs the room of four and a half. */
/* Sized from the width the heading actually has (the shell, capped at 1440,
   less its gutters), not the whole window: measured against the window, a
   long word ran past the edge and was cut, the K of WORK on a small phone,
   the S of QUESTIONS on a wide screen. */
.huge{ font-size:clamp(2.2rem, calc((min(100vw, 1440px) - 2 * var(--gutter)) * 1.18 / var(--chars, 6)), 12.5rem); }
.big{ font-size:clamp(2.4rem,7.5vw,6rem); }
.mid{ font-size:clamp(1.7rem,4vw,3rem); }
.sm{ font-size:clamp(1.3rem,2.4vw,1.9rem); }
.lede{ font-size:clamp(1.05rem,1.5vw,1.3rem); line-height:1.6; color:var(--dim); max-width:46ch; }
.meta{ font-size:0.86rem; color:var(--dim); }

/* ---- phones: room to breathe -------------------------------------------
   One layer for every page. Copy stayed centred (that was a choice), but it
   ran within 20px of both edges, display lines sat on top of each other at
   0.86, headings ran edge to edge and their text started straight under
   them. Wider margins, a shorter measure, lines that clear each other and
   longer pauses between things. */
@media (max-width:760px){
  :root{ --gutter:28px; }
  .display{ line-height:0.94; }
  .big{ font-size:clamp(2.1rem,10.5vw,3rem); }
  .mid{ font-size:clamp(1.55rem,7vw,2rem); }
  .sm{ font-size:clamp(1.15rem,5.2vw,1.4rem); line-height:1.02; }
  /* the one word page titles fill a little less of the width */
  .huge{ font-size:clamp(2.2rem, calc((100vw - 2 * var(--gutter)) * 1.02 / var(--chars, 6)), 7rem); }
  .lede{ font-size:1.04rem; line-height:1.7; max-width:34ch; }
  .pad{ padding:104px 0; }
  main p{ line-height:1.7; }
  main p, main dd, main li{ text-wrap:pretty; }
  main h1, main h2, main h3{ text-wrap:balance; }
  main h2 + p, main h3 + p{ margin-top:14px; }
}

.shell{ width:100%; max-width:1440px; margin:0 auto; padding:0 var(--gutter); }
.pad{ padding:clamp(70px,10vw,150px) 0; }

/* Reveal */
.rv{ display:block; overflow:hidden; }
.rv > *{
  display:block; transform:translateY(105%);
  transition:transform 1.05s var(--ease); transition-delay:var(--d,0ms);
}
.rv.in > *{ transform:translateY(0); }
.fade{ opacity:0; transform:translateY(22px); transition:opacity .9s var(--ease),transform .9s var(--ease); transition-delay:var(--d,0ms); }
.fade.in{ opacity:1; transform:none; }
/* Already where it belongs, with no journey to make. Set on whatever is on
   screen when a page is arrived at through a view transition: the transition
   is the entrance, and a second one underneath it reads as a stutter. */
.rv.placed > *{ transition:none; transform:translateY(0); }
.fade.placed{ transition:none; opacity:1; transform:none; }

/* Header */
.site-head{
  position:fixed; top:0; left:0; right:0; z-index:300;
  display:flex; align-items:center; justify-content:space-between;
  padding:22px var(--edge); color:var(--paper);
  transition:color .35s ease;
}
.site-head.on-light{ color:var(--ink); }
@media (max-width:760px){
  .site-head{ transition:color .35s ease, background-color .35s ease, padding .35s ease; }
  .site-head.scrolled{ background:rgba(10,8,7,.86); -webkit-backdrop-filter:blur(12px); backdrop-filter:blur(12px); padding-block:16px; }
  .site-head.scrolled.on-light{ background:rgba(242,235,225,.88); }
  .site-head.scrolled.on-red{ background:rgba(255,34,68,.9); }
  .site-head.menu-open{ background:none !important; -webkit-backdrop-filter:none; backdrop-filter:none; }
}
.brand{ font-family:var(--display); font-weight:800; font-size:1rem; letter-spacing:-0.02em; text-transform:uppercase; }
.head-right{ display:flex; align-items:center; gap:clamp(14px,2vw,30px); }
.head-link{ font-size:0.86rem; }
@media (hover:hover){ .head-link:hover{ color:var(--red); } }
@media (max-width:760px){ .head-link.hide-sm{ display:none; } }

.menu-btn{ width:34px; height:20px; position:relative; }
/* The lines stay the size they are; the tap area around them does not have
   to. 20px tall was a hard target on a phone. */
.menu-btn::before{ content:''; position:absolute; inset:-14px -10px; }
.brand{ padding-block:10px; margin-block:-10px; }
.menu-btn i{ position:absolute; left:0; width:100%; height:2px; background:currentColor; transition:transform .5s var(--ease-io); }
.menu-btn i:first-child{ top:3px; }
.menu-btn i:last-child{ bottom:3px; }
.menu-btn.open i:first-child{ transform:translateY(6px) rotate(45deg); }
.menu-btn.open i:last-child{ transform:translateY(-6px) rotate(-45deg); }
.site-head.menu-open{ color:var(--paper); }
/* Set by site.js to change state with no animation (leaving through a menu
   link, coming back to a page that was left with the menu open). */
.menu-btn.snap i, .menu.snap, .menu.snap *{ transition:none !important; }

/* Menu: a bento of destinations. The headline names whatever is under the
   cursor, so the cards can stay as icons without becoming a guessing game. */
.menu{
  position:fixed; inset:0; z-index:290;   /* inset alone: 100svh left a strip of page under it once a phone's toolbar had hidden */
  background:var(--ink); color:var(--paper); overflow:hidden;
  display:flex; flex-direction:column; align-items:center;
  padding:clamp(86px,11vh,124px) var(--edge) clamp(22px,3.4vh,40px);
  clip-path:inset(0 0 100% 0); pointer-events:none;
  /* The wipe only runs on the compositor if this has a layer of its own, and
     asking for one as the transition starts is already too late. site.js sets
     will-change the moment the button is reachable and drops it after. */
  /* clip-path hides it but leaves every link in the tab order, so tabbing on
     a closed page walks the cursor through an invisible menu. visibility
     takes them out, delayed on close so it still plays the wipe. */
  visibility:hidden;
  transition:clip-path .9s var(--ease-io), visibility 0s linear .9s;
}
.menu.open{
  clip-path:inset(0 0 0 0); pointer-events:auto;
  visibility:visible; transition:clip-path .9s var(--ease-io), visibility 0s;
}
.menu > *{ width:100%; max-width:1440px; }
/* A phone on its side has no room for the cards: the menu scrolls instead of
   cutting the last of them off. */
@media (max-height:520px){ .menu{ overflow-y:auto; overscroll-behavior:contain; } }

.menu-top{
  flex:none; display:flex; justify-content:space-between; align-items:center;
  font-size:0.86rem; color:var(--dim);
}
.menu-top b{ font-family:var(--body); font-weight:500; color:var(--paper); }
.menu-clock{ display:flex; align-items:center; gap:9px; font-variant-numeric:tabular-nums; }
.menu-clock::before{ content:''; flex:none; width:6px; height:6px; border-radius:50%; background:var(--red); }

.menu-head{
  flex:none; margin:clamp(10px,2vh,24px) 0;
  font-family:var(--display); font-weight:900; text-transform:uppercase;
  font-size:clamp(2rem,6.2vw,4.6rem); line-height:0.94; letter-spacing:-0.03em;
}
.menu-head .word{ display:inline-block; white-space:nowrap; }
.menu-head .letter{ display:inline-block; }
/* The wave runs on a delay per letter, so it reads left to right rather than
   flashing the whole word at once. */
@keyframes menuPulse{ 0%,15%,100%{ color:var(--paper); } 50%{ color:var(--red); } }
.menu-head.pulse .letter{ animation:menuPulse 2.6s ease-in-out infinite; }

.bento{
  flex:1; min-height:0;
  display:grid; grid-template-columns:0.8fr 1.6fr 0.7fr; grid-template-rows:1fr 1fr;
  gap:clamp(10px,1vw,16px);
}
.bento-card{
  position:relative; min-height:0;
  display:flex; align-items:center; justify-content:center; gap:clamp(9px,1.2vw,16px);
  padding:clamp(14px,1.8vw,22px);
  border:1px solid var(--rule); border-radius:16px;
  background:color-mix(in srgb, currentColor 5%, transparent);
  color:var(--paper); text-align:center;
  opacity:0; transform:translateY(26px);
  transition:opacity .7s var(--ease), transform .7s var(--ease),
             border-color .3s ease, background-color .3s ease;
}
.menu.open .bento-card{ opacity:1; transform:none; transition-delay:var(--d,0ms); }
@media (hover:hover){ a.bento-card:hover{ border-color:var(--red); background:color-mix(in srgb, currentColor 10%, transparent); } }
.bento-icon{
  flex:none; width:clamp(28px,3.2vw,44px); height:clamp(28px,3.2vw,44px);
  transition:color .3s ease, transform .3s var(--ease);
}
@media (hover:hover){ a.bento-card:hover .bento-icon{ color:var(--red); transform:scale(1.08); } }

.b-home{ grid-column:1; grid-row:1; }
.b-work{ grid-column:2 / 4; grid-row:1; }
.b-pricing{ grid-column:1; grid-row:2; }
.bento-worked{ grid-column:2; grid-row:2; flex-direction:column; }
.bento-worked .worked-label{ padding-right:0; }
.bento-worked .worked-label::after{ display:none; }
.bento-worked .brandmark{ opacity:1; }
.bento-col3{ grid-column:3; grid-row:2; display:grid; grid-template-rows:1fr 1fr; gap:clamp(10px,1vw,16px); min-height:0; }

/* Without hover there is nothing to drive the headline, so the cards say what
   they are instead of leaving a touch user to read four icons. */
.bento-label{ display:none; font-family:var(--display); font-weight:800; text-transform:uppercase; font-size:0.78rem; letter-spacing:0.02em; }
@media (hover: none){
  .bento-card[data-headline]{ flex-direction:column; gap:8px; padding:10px; }
  .bento-label{ display:block; }
}

.menu-foot{
  flex:none; margin-top:clamp(12px,1.8vh,20px); padding-top:clamp(14px,2vh,24px);
  display:flex; flex-wrap:wrap; gap:8px 34px;
  border-top:1px solid var(--rule); font-size:0.9rem; color:var(--dim);
}
@media (hover:hover){ .menu-foot a:hover{ color:var(--paper); } }

@media (max-width:760px){
  /* Four destinations need a row each here, so the column of two splits and
     lets its cards place themselves. Start a project takes a full row rather
     than the corner it can afford on a wide screen. */
  .bento{ grid-template-columns:1fr 1fr; grid-template-rows:repeat(5, minmax(58px, 1fr)); }
  .b-home{ grid-column:1 / 3; grid-row:1; }
  .b-work{ grid-column:1 / 3; grid-row:2; }
  .b-pricing{ grid-column:1 / 3; grid-row:3; }
  .b-propose{ grid-column:1 / 3; grid-row:4; }
  .bento-col3{ display:contents; }
  .bento-worked{ grid-column:1; grid-row:5; }
  .b-faq{ grid-column:2; grid-row:5; }
  .bento-worked .mark-swap{
    height:clamp(24px,7vw,40px);
    min-width:calc(clamp(24px,7vw,40px) * 2.203);
  }
  .menu-head{ font-size:clamp(1.7rem,7vw,2.6rem); }
}
body.locked{ overflow:hidden; }

/* Buttons */
.btn{
  position:relative; display:inline-flex; align-items:center;
  padding:19px 34px; overflow:hidden; isolation:isolate;
  font-family:var(--display); font-weight:700; font-size:0.95rem;
  letter-spacing:0.01em; text-transform:uppercase;
  background:var(--red); color:var(--paper); border-radius:8px;
}
.btn::after{ content:''; position:absolute; inset:0; z-index:-1; background:var(--fg); transform:translateY(101%); transition:transform .6s var(--ease-io); }
@media (hover:hover){ .btn:hover::after{ transform:translateY(0); } }
/* The sweep fills with --fg, so the label has to switch to whatever reads
   against it. Hard coding paper here left the label invisible on the dark
   panels, where --fg is paper too. */
@media (hover:hover){ .btn:hover{ color:var(--on-fg); } }
.btn-line{
  display:inline-block; font-family:var(--display); font-weight:700; font-size:0.95rem;
  text-transform:uppercase; padding-bottom:5px;
  background-image:linear-gradient(var(--red),var(--red));
  background-size:0% 2px; background-repeat:no-repeat; background-position:left bottom;
  transition:background-size .5s var(--ease),color .3s;
}
@media (hover:hover){ .btn-line:hover{ background-size:100% 2px; color:var(--red); } }
/* Red on red is invisible, so on the red sections the accent becomes ink */
.s-red .btn-line{ background-image:linear-gradient(var(--ink),var(--ink)); }
@media (hover:hover){ .s-red .btn-line:hover{ color:var(--ink); } }

/* Media */
.media{
  overflow:hidden; border-radius:16px;
  background:rgba(10,8,7,.14);
  background:color-mix(in srgb, currentColor 13%, transparent);
}
.media img{ width:100%; height:100%; object-fit:cover; will-change:transform; }
.media.shot img{ object-fit:contain; }

/* A scaled live frame used where a screenshot would otherwise sit. The page
   renders at desktop width and is scaled down, so it reads as the real site
   rather than a squashed mobile layout. */
.mini-live{ position:relative; background:var(--ink); }
.mini-live iframe{
  position:absolute; top:0; left:0; border:0;
  transform-origin:top left; pointer-events:none;
  opacity:0; transition:opacity .7s var(--ease);
}
.mini-live iframe.on{ opacity:1; }
.frame-slot{ display:none; }
/* Narrowing a desktop window turns a frame into the phone shape: the desktop
   site loaded in it would sit cropped over the phone still, so it goes. */
@media (max-width:820px){ .mini-live iframe{ display:none; } }
/* The still under the frame, cropped the way the frame is: desktop width,
   pinned top left. On a desktop the running site fades over it; on a phone
   it is all there is. */
.mini-live .poster{
  position:absolute; inset:0; width:100%; height:100%;
  object-fit:cover; object-position:top left; will-change:auto;
}
.mini-live .live-veil{ border-radius:inherit; }

/* Full screen for a project's phone still, see site.js. The button shows on
   phones only; on a desktop the frame is the running site. */
.expand-btn{ display:none; }
@media (max-width:820px), (hover:none), (pointer:coarse){   /* the screens STILL serves, tablets included */
  [data-expand]{ cursor:zoom-in; }
  .expand-btn{
    display:flex; align-items:center; gap:9px; width:fit-content;
    margin:14px auto 0; padding:11px 16px; border-radius:10px; cursor:pointer;
    background:none; color:inherit; border:1px solid currentColor;
    font-family:var(--display); font-weight:700; font-size:0.72rem;
    letter-spacing:0.12em; text-transform:uppercase; line-height:1;
  }
  .live-frame .expand-btn{ margin:14px auto; color:var(--paper); }
  .expand-btn svg{ width:13px; height:13px; fill:none; stroke:currentColor; stroke-width:1.6; }
}
.viewer{
  position:fixed; inset:0; z-index:500; display:flex; flex-direction:column;
  background:var(--ink); color:var(--paper);
}
.viewer[hidden]{ display:none; }
.viewer-bar{
  flex:none; display:flex; justify-content:space-between; align-items:center; gap:12px;
  padding:calc(10px + env(safe-area-inset-top)) 16px 10px;
  border-bottom:1px solid rgba(242,235,225,.12);
}
.viewer-bar a, .viewer-bar button{
  font-family:var(--display); font-weight:700; font-size:0.74rem;
  letter-spacing:0.12em; text-transform:uppercase; color:var(--paper);
}
.viewer-bar a{ text-decoration:underline; text-underline-offset:4px; }
.viewer-bar a[hidden]{ display:none; }
.viewer-close{
  margin-left:auto; padding:10px 14px; border-radius:10px; cursor:pointer;
  background:none; border:1px solid rgba(242,235,225,.3);
}
.viewer-scroll{ flex:1; overflow-y:auto; -webkit-overflow-scrolling:touch; overscroll-behavior:contain; }
/* The still at the phone's own width: 1:1 on a 390px phone, never blown up
   past it on a bigger one. */
.viewer-scroll img{ display:block; width:min(100%,390px); height:auto; margin:0 auto; }

/* Client marks. The logo is used as a mask and filled with currentColor, so
   the wordmark takes the section's own text colour instead of shipping a
   light and a dark file. Needs a transparent PNG, WebP or SVG. The image
   itself is set inline on the element: a url() inside a custom property
   resolves against the stylesheet that uses it, which would look for the
   logo inside assets/ rather than beside the page. */
.worked{ display:flex; align-items:center; gap:clamp(12px,1.4vw,20px); flex-wrap:wrap; }
.worked-label{
  font-size:0.72rem; letter-spacing:0.14em; text-transform:uppercase; color:var(--dim);
  position:relative; padding-right:clamp(12px,1.4vw,20px);
}
.worked-label::after{
  content:''; position:absolute; right:0; top:50%; width:1px; height:14px;
  background:var(--rule); transform:translateY(-50%);
}
.worked-marks{ display:flex; align-items:center; gap:clamp(16px,2vw,30px); }

/* ---- the client marks, one at a time ------------------------------------
   A window one mark tall with the marks stacked inside it, so the one going
   out and the one coming in move together and neither can be seen outside the
   window. The height is the mark's own height, so nothing here changes the
   line the label sits on. */
.mark-swap{
  position:relative; overflow:hidden;
  height:clamp(24px,2.6vw,34px);
  /* The widest mark's own width, so the row beside it does not resize on
     every swap. Abinci is 401 by 182, so 2.203 times its height. */
  min-width:calc(clamp(24px,2.6vw,34px) * 2.203);
}
.mark-swap .brandmark{
  position:absolute; left:0; top:0; height:100%;
  transition:transform .62s var(--ease-io), opacity .62s var(--ease-io);
}
/* Above the window, in the window, below it. */
.mark-swap .brandmark[data-at="out"]{ transform:translateY(-118%); opacity:0; }
.mark-swap .brandmark[data-at="in"] { transform:translateY(0);      opacity:.9; }
.mark-swap .brandmark[data-at="next"]{ transform:translateY(118%);  opacity:0; }
/* In the stacked menu card the label sits above the window and the window is
   as wide as the widest mark, so a narrower mark pinned to its left edge
   drifts off the label's centre line. Abinci fills the window and looks right
   by accident; Prunt is half its width and sat eighteen pixels to the left.
   Centring in the window puts every mark on the label's axis. Not applied to
   the inline row, where the label is to the left and the marks read better
   starting flush against the divider. */
.bento-worked .mark-swap .brandmark{ left:0; right:0; margin-inline:auto; }
/* The window sizes the mark inside it, so the menu card's own larger size
   has to be set here rather than on the mark, where it never applied. */
.bento-worked .mark-swap{
  height:clamp(28px,4.4vw,60px);
  min-width:calc(clamp(28px,4.4vw,60px) * 2.203);
}

/* Nothing is cycling, so every client is simply shown, side by side. */
html.paused .mark-swap,
.mark-swap.no-cycle{
  position:static; overflow:visible; height:auto; min-width:0;
  display:flex; align-items:center; gap:clamp(14px,1.8vw,24px);
}
html.paused .mark-swap .brandmark,
.mark-swap.no-cycle .brandmark{
  position:static; transform:none; opacity:.9; transition:none;
  height:clamp(24px,2.6vw,34px);
}
@media (prefers-reduced-motion:reduce){
  .mark-swap{
    position:static; overflow:visible; height:auto; min-width:0;
    display:flex; align-items:center; gap:clamp(14px,1.8vw,24px);
  }
  .mark-swap .brandmark{
    position:static; transform:none; opacity:.9; transition:none;
    height:clamp(24px,2.6vw,34px);
  }
}

.brandmark{
  /* A client mark, recoloured by whatever section it sits in. The artwork is a
     mask and currentColor is the fill, so one file serves both the dark and the
     light sections instead of shipping a pair and switching between them.
     The text stays in the DOM for screen readers and for the high contrast
     fallback below, collapsed rather than made transparent: currentColor is
     what fills the mask, so recolouring the text would erase the mark with it. */
  display:block; opacity:.9;
  /* Somebody else's mark, so it is reproduced in white rather than tinted to
     this site's cream. Shown rather than offered: these are not links, so
     there is no hover state and nothing to click. */
  color:#fff;
  height:clamp(24px,2.6vw,34px); width:auto;
  font-family:var(--display); font-weight:900; text-transform:uppercase;
  font-size:0; letter-spacing:0.01em;
  background-color:#fff;
  -webkit-mask-repeat:no-repeat; -webkit-mask-position:center; -webkit-mask-size:contain;
          mask-repeat:no-repeat;         mask-position:center;         mask-size:contain;
  transition:opacity .3s ease;
}

/* Each mark carries its own proportions so it ends flush with its own edges and
   can be aligned against like any other box. The artwork is inlined because a
   CSS mask is fetched under CORS, unlike an img, and a linked file fails that
   check when the page is opened straight off disk. A failed mask clips the
   element to nothing, which is silent, so there is nothing to fall back to.
   Source artwork: abinci-logo.webp at the repo root, trimmed to the ink and
   base64'd in here. Re-encode from that file if the mark ever changes. */
.brandmark--abinci{
  aspect-ratio:401 / 182;
  /* Declared once and referenced twice. A relative url() in a custom property
     would resolve against this stylesheet rather than the page, but a data URI
     has nothing to resolve, so it is safe to hold in a variable. */
  --mark:url("data:image/webp;base64,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");
  -webkit-mask-image:var(--mark);
          mask-image:var(--mark);
}

/* Traced from the artwork they supplied at 2000 by 1634. It carries no alpha
   channel, despite looking like it does: the transparency checkerboard is
   baked into it as real pixels, which is harmless here because the mark is
   253 to 255 white and the checker is 67 to 98 grey, so they part cleanly.
   Resampled to 600 wide first, which antialiases the edge so a threshold at
   the halfway point lands sub-pixel accurate, then marching squares and
   Douglas-Peucker at a tolerance that keeps the hairline slits, which are
   most of the shape's character. Agrees with the source on 99.89 percent of
   its pixels and is resolution independent.

   Inlined as a data URI for the same reason as the mark above: a linked mask
   is fetched under CORS, and a page opened straight off disk fails that
   check silently, which clips the element to nothing with no way to tell. */
.brandmark--prunt{
  aspect-ratio:387.0 / 344.0;
  --mark:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 387 344'%3E%3Cpath d='M43.0,0.5 L317.5,0.0 L318.5,1.0 L329.5,2.0 L342.5,6.0 L357.5,13.0 L363.5,17.0 L378.0,32.5 L384.0,45.5 L385.0,52.5 L386.0,53.5 L386.0,61.5 L387.0,62.5 L386.0,77.5 L385.0,78.5 L383.0,88.5 L379.0,96.5 L374.0,103.5 L365.5,112.0 L355.5,119.0 L334.5,130.0 L319.5,135.0 L299.5,144.0 L295.0,149.5 L299.5,151.0 L300.5,150.0 L306.5,150.0 L307.5,149.0 L321.5,147.0 L363.5,135.0 L375.5,135.0 L378.5,136.0 L381.5,138.0 L386.0,143.5 L387.0,146.5 L387.0,292.5 L386.0,293.5 L386.0,298.5 L385.0,299.5 L383.0,309.5 L375.0,323.5 L366.5,332.0 L357.5,338.0 L343.5,343.0 L339.5,343.0 L338.5,344.0 L315.5,344.0 L314.5,343.0 L309.5,343.0 L304.5,341.0 L297.5,340.0 L274.5,331.0 L255.5,318.0 L244.0,306.5 L234.0,292.5 L226.0,278.5 L219.5,273.0 L219.0,280.5 L220.0,281.5 L220.0,284.5 L231.0,313.5 L233.0,320.5 L233.0,325.5 L234.0,326.5 L232.0,335.5 L230.0,338.5 L224.5,343.0 L221.5,344.0 L59.5,344.0 L53.5,341.0 L48.0,334.5 L46.0,327.5 L46.0,196.5 L47.0,195.5 L47.0,183.5 L48.0,182.5 L48.0,178.5 L55.0,162.5 L68.5,148.0 L83.5,139.0 L85.5,139.0 L91.5,136.0 L94.5,136.0 L99.5,134.0 L146.5,123.0 L150.5,121.0 L153.5,121.0 L167.5,115.0 L170.0,112.5 L168.5,111.0 L153.5,111.0 L152.5,112.0 L146.5,112.0 L145.5,113.0 L140.5,113.0 L139.5,114.0 L134.5,114.0 L128.5,116.0 L113.5,118.0 L108.5,120.0 L99.5,121.0 L98.5,122.0 L93.5,122.0 L92.5,123.0 L81.5,124.0 L80.5,125.0 L75.5,125.0 L74.5,126.0 L67.5,126.0 L66.5,127.0 L55.5,127.0 L54.5,128.0 L44.5,128.0 L43.5,127.0 L33.5,126.0 L20.5,120.0 L16.5,117.0 L7.0,105.5 L4.0,99.5 L1.0,89.5 L1.0,83.5 L0.0,82.5 L0.0,44.5 L1.0,43.5 L1.0,39.5 L4.0,29.5 L7.0,23.5 L20.5,9.0 L32.5,3.0 L43.0,0.5 Z' fill='%23000'/%3E%3C/svg%3E");
  -webkit-mask-image:var(--mark);
          mask-image:var(--mark);
}

/* A mask has no colour of its own to force, so high contrast mode would show
   nothing at all. Fall back to the text underneath. */
@media (forced-colors: active){
  .brandmark{
    -webkit-mask-image:none; mask-image:none;
    aspect-ratio:auto; height:auto; width:auto;
    background:none; color:CanvasText; opacity:1;
    font-size:clamp(0.95rem,1.4vw,1.15rem); line-height:1.2;
  }
}

/* Live site embed. The frame loads itself on approach and fades over the
   poster. A cross origin iframe fires load even when the host refuses to be
   framed and there is no way to tell the two apart from this side, so the
   open-in-a-tab link is always present as the way out. */
.live-frame{ border:1px solid var(--rule); border-radius:16px; overflow:hidden; background:var(--ink); }
.live-chrome{
  display:flex; align-items:center; gap:7px;
  padding:11px 16px; border-bottom:1px solid var(--rule); background:rgba(242,235,225,.05);
}
.live-chrome i{ width:9px; height:9px; border-radius:50%; background:var(--rule); display:block; }
.live-chrome em{
  font-style:normal; margin-left:10px; font-size:0.76rem; letter-spacing:0.04em;
  color:var(--dim); font-family:var(--body);
}
.live-view{ position:relative; aspect-ratio:16/10; background:var(--ink); }
.live-view img{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; object-position:top center; }
.live-view iframe{
  position:absolute; inset:0; width:100%; height:100%; border:0;
  opacity:0; transition:opacity .7s var(--ease);
}
.live-view iframe.on{ opacity:1; }
.live-veil{
  position:absolute; inset:0; z-index:3; display:grid; place-items:center;
  background:linear-gradient(to top, rgba(10,8,7,.55), rgba(10,8,7,.2));
  transition:opacity .6s ease; pointer-events:none;
}
.live-veil span{
  font-family:var(--display); font-weight:700; font-size:0.78rem;
  letter-spacing:0.14em; text-transform:uppercase; color:var(--paper); opacity:.85;
}
/* The veil lifts on whichever kind of embed finished. This was keyed to
   .live-frame alone, which meant the scaled .mini-live panels sat under a
   Loading label forever, long after the site inside them had painted. */
.loaded .live-veil{ opacity:0; }
.stalled .live-veil span{ opacity:1; color:var(--paper); }
.live-foot{ display:flex; justify-content:space-between; align-items:center; gap:16px; flex-wrap:wrap; padding:14px 16px; border-top:1px solid var(--rule); }
.live-note{ font-size:0.84rem; color:var(--dim); }
@media (max-width:760px){
  .live-view{ aspect-ratio:3/4; }
  .live-foot{ justify-content:center; text-align:center; }
}

/* Inner page hero */
.p-hero{ padding:clamp(140px,20vh,220px) 0 clamp(50px,7vw,90px); }
.p-hero .lede{ margin-top:24px; }

/* Footer */
.site-foot{ background:var(--ink); color:var(--paper); padding:clamp(60px,9vw,120px) 0 36px; }
.foot-top{ display:flex; flex-wrap:wrap; justify-content:space-between; gap:40px; }
.foot-nav{ display:flex; gap:clamp(30px,6vw,90px); }
.foot-col h4{ font-size:0.72rem; letter-spacing:0.14em; text-transform:uppercase; color:rgba(242,235,225,0.44); margin-bottom:16px; font-weight:500; }
.foot-col a{ display:block; padding:6px 0; font-size:0.98rem; }
@media (hover:hover){ .foot-col a:hover{ color:var(--red); } }
/* The monogram took the tagline's place in the footer, so it keeps the
   tagline's column and its weight: held back from the paper the links are set
   in, and lifting to full strength on hover like everything else here. It is
   decorative and the link already has a label, so the alt text is empty
   rather than repeating the name a screen reader just read. */
.foot-mark{ display:block; line-height:0; }
.foot-mark img{
  display:block; width:auto; height:clamp(40px,5.2vw,64px);
  opacity:.5; transition:opacity .3s ease;
}
.foot-mark:focus-visible img{ opacity:.92; }
@media (hover:hover){ .foot-mark:hover img{ opacity:.92; } }
.foot-bottom{
  margin-top:clamp(50px,8vw,90px); padding-top:24px; border-top:1px solid rgba(242,235,225,0.14);
  display:flex; flex-wrap:wrap; justify-content:space-between; gap:12px;
  font-size:0.82rem; color:rgba(242,235,225,0.42);
}
@media (hover:hover){ .foot-bottom a:hover{ color:var(--paper); } }
@media (max-width:700px){
  .foot-top{ flex-direction:column; align-items:center; text-align:center; }
  .foot-nav{ flex-direction:column; gap:28px; }
  .foot-bottom{ flex-direction:column; align-items:center; text-align:center; }
}

@media (prefers-reduced-motion:reduce){
  @view-transition{ navigation:none; }
  *{ animation:none !important; transition-duration:.01ms !important; }
  .rv > *{ transform:none; }
  .fade{ opacity:1; transform:none; }
  .grain,.mesh{ display:none; }
}

/* ---- motion switch ------------------------------------------------------
   Built by site.js so every page gets it without ten copies of the markup.
   Bottom left, quiet until you go near it: somebody whose laptop is
   struggling should be able to find it, and nobody else should have to look
   at it. */
/* Out of the way while the menu or a full screen view is up: it sat over
   the menu's email link and took its clicks. */
body.locked .motion-btn{ opacity:0; pointer-events:none; }
.motion-btn{
  position:fixed; left:14px; bottom:14px; z-index:420;
  display:inline-flex; align-items:center; gap:8px;
  padding:8px 13px 8px 10px; border-radius:999px;
  border:1px solid rgba(242,235,225,.22);
  background:rgba(10,8,7,.62); backdrop-filter:blur(8px);
  color:var(--paper); font-family:var(--body); font-size:0.74rem;
  letter-spacing:0.06em; text-transform:uppercase;
  opacity:.34; transition:opacity .25s ease, border-color .25s ease, background-color .25s ease;
}
.motion-btn:focus-visible{ opacity:1; border-color:var(--red); }
@media (hover:hover){ .motion-btn:hover{ opacity:1; border-color:var(--red); } }
.motion-btn .motion-ico{ display:flex; gap:3px; align-items:center; width:10px; height:12px; }
.motion-btn .motion-ico i{ flex:1; height:100%; background:currentColor; border-radius:1px; transition:clip-path .2s ease; }
/* Two bars for pause; the same two bars clipped into one triangle for play,
   so the icon changes without swapping any nodes. */
.motion-btn.is-paused .motion-ico{ gap:0; }
.motion-btn.is-paused .motion-ico i:first-child{ clip-path:polygon(0 0, 100% 22%, 100% 78%, 0 100%); }
.motion-btn.is-paused .motion-ico i:last-child{ clip-path:polygon(0 22%, 100% 50%, 0 78%); }
@media (max-width:560px){ .motion-btn .motion-txt{ display:none; } .motion-btn{ padding:9px; } }

/* One switch, everything decorative answers to it. Transitions are left
   alone: they are short and user initiated, and killing them makes the site
   feel broken rather than calm. */
html.paused *,
html.paused *::before,
html.paused *::after{ animation-play-state:paused !important; }

/* ---- custom cursor ------------------------------------------------------
   One mark on a spring, no trailing ring. Over the figure it opens into a
   pill with a hand that waves at you. The native cursor is only hidden over
   the hero: a portfolio is still a page of links and text everywhere else. */
.cursor{
  position:fixed; left:0; top:0; z-index:430; pointer-events:none;
  opacity:0; transition:opacity .3s ease;
}
.cursor.ready.on{ opacity:1; }
.cur-dot{
  position:absolute; left:0; top:0; will-change:transform;
  display:flex; align-items:center; justify-content:center; gap:5px;
  width:9px; height:9px; padding:0; border-radius:50%;
  background:var(--red); box-shadow:0 0 12px rgba(255,34,68,.85);
  overflow:hidden; white-space:nowrap;
  transition:width .34s var(--ease), height .34s var(--ease),
             border-radius .34s var(--ease), padding .34s var(--ease);
}
.cur-dot span{
  font-family:var(--body); font-size:0.6rem; font-weight:600;
  letter-spacing:0.14em; text-transform:uppercase; color:var(--paper);
  opacity:0; transition:opacity .18s ease .06s;
}
.cur-wave{
  width:0; height:15px; flex:none; overflow:visible;
  stroke:var(--paper); stroke-width:1.7; stroke-linecap:round; stroke-linejoin:round;
  opacity:0; transition:width .34s var(--ease), opacity .18s ease .06s;
  /* The wrist, so it pivots where a hand pivots rather than spinning about
     its own middle. */
  transform-box:fill-box; transform-origin:52% 92%;
}
/* Over the figure: the mark opens, the label arrives, the hand waves. */
.cursor.fig .cur-dot{ width:86px; height:28px; border-radius:999px; padding:0 11px; }
.cursor.fig .cur-dot span{ opacity:1; }
.cursor.fig .cur-wave{ width:15px; opacity:1; animation:handwave .62s var(--ease-io) infinite; }
@keyframes handwave{
  0%,100%{ transform:rotate(-16deg); }
  25%    { transform:rotate(14deg); }
  50%    { transform:rotate(-10deg); }
  75%    { transform:rotate(18deg); }
}
/* Only while the replacement is actually on screen. */
html.cursor-on .hero[data-hero]{ cursor:none; }
@media (hover:none),(pointer:coarse){ .cursor{ display:none; } }
@media (prefers-reduced-motion:reduce){ .cursor{ display:none; } }
/* The cursor is an animation like any other, so it goes when they do. */
html.paused .cursor{ display:none; }
