/* ───────────────────────── tokens ───────────────────────── */
:root{
  --paper:#f4f1ea;
  --ink:#1b1a17;
  --body:#4a463d;
  --muted:#8b8578;
  --faint:#a09a8c;
  --rule:#ded7c8;
  --card:#fbf9f4;

  --green:oklch(0.86 0.09 150);
  --coral:oklch(0.78 0.15 30);
  --gold:oklch(0.86 0.11 95);
  --coral-deep:oklch(0.6 0.15 30);

  --serif:'Cormorant Garamond',Georgia,serif;
  --sans:'DM Sans',system-ui,-apple-system,'Segoe UI',sans-serif;
  --mono:'DM Mono',ui-monospace,SFMono-Regular,Menlo,monospace;

  --pad:46px;
  --ease:cubic-bezier(0.2,0.8,0.2,1);

  /* ── shadows ──
     Real shadows are not one blur. A lit object casts a tight, darker contact
     shadow where it meets the surface and a wide, faint ambient one further out,
     with the penumbra softening as it spreads. These stack blur radii roughly
     doubling each step while opacity falls, which is what produces that falloff.
     The colour is a warm brown-grey sampled from the paper, not black, because a black
     shadow on a cream page reads as grey sludge. */
  --sh-rest:
    0 1px 1px rgba(58,48,32,.045),
    0 2px 3px rgba(58,48,32,.045),
    0 5px 8px rgba(58,48,32,.05),
    0 10px 17px rgba(58,48,32,.05),
    0 21px 34px rgba(58,48,32,.05);
  --sh-lift:
    0 2px 2px rgba(58,48,32,.05),
    0 4px 6px rgba(58,48,32,.05),
    0 9px 14px rgba(58,48,32,.055),
    0 18px 30px rgba(58,48,32,.06),
    0 36px 58px rgba(58,48,32,.06),
    0 68px 100px rgba(58,48,32,.05);
  --sh-phone:
    0 1px 1px rgba(58,48,32,.06),
    0 3px 4px rgba(58,48,32,.06),
    0 7px 10px rgba(58,48,32,.06),
    0 14px 22px rgba(58,48,32,.06),
    0 28px 44px rgba(58,48,32,.055);
  --sh-recede:
    0 2px 4px rgba(58,48,32,.05),
    0 6px 12px rgba(58,48,32,.05),
    0 14px 26px rgba(58,48,32,.045);
}

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

html{scroll-behavior:smooth;}
html,body{margin:0;padding:0;background:var(--paper);}
/* `clip` contains the carousel's side cards and the drifting hero shapes without
   making the root a scroll container the way `hidden` would */
html{overflow-x:hidden;overflow-x:clip;}
body{
  color:var(--ink);
  font-family:var(--sans);
  min-height:100vh;
  position:relative;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}

a{color:var(--ink);text-decoration:none;}
a:hover{color:var(--coral-deep);}

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

.skip{
  position:absolute;left:-9999px;top:0;z-index:99;
  background:var(--ink);color:var(--paper);padding:12px 18px;border-radius:0 0 10px 0;
  font-family:var(--mono);font-size:12px;
}
.skip:focus{left:0;color:var(--paper);}

/* ───────────────────────── motion ───────────────────────── */
@keyframes rise{from{opacity:0;transform:translateY(26px);}to{opacity:1;transform:none;}}
@keyframes drift{from{transform:translateX(0);}to{transform:translateX(-50%);}}
@keyframes float1{0%,100%{transform:translate(0,0) rotate(0deg);}50%{transform:translate(18px,-26px) rotate(8deg);}}
@keyframes float2{0%,100%{transform:translate(0,0) rotate(0deg);}50%{transform:translate(-22px,18px) rotate(-10deg);}}

/* ───────────────────── cursor blob ───────────────────── */
.blob{
  position:fixed;left:0;top:0;width:460px;height:460px;border-radius:50%;
  background:radial-gradient(circle at 50% 50%,oklch(0.86 0.09 150 / 0.5),transparent 68%);
  pointer-events:none;z-index:0;will-change:transform;
  transform:translate3d(-600px,-600px,0);
}

/* ───────────────────────── header ───────────────────────── */
.site-header{
  position:relative;z-index:5;
  display:flex;justify-content:space-between;align-items:center;gap:20px;
  padding:26px var(--pad);
}
/* The masthead. Set in the same serif as the hero headline and project names,
   and capped well below the headline's size so the two read as a hierarchy
   (name = who, headline = what) rather than competing for the same job. It is
   allowed to shrink but the nav is not, so a long title never squeezes the
   links off the row. */
.wordmark{
  margin:0;flex:1 1 auto;min-width:0;
  font-family:var(--serif);font-weight:400;
  font-size:clamp(22px,2.6vw,38px);
  line-height:1.08;letter-spacing:-0.015em;
  color:var(--ink);
}
.wordmark__title{font-weight:300;color:var(--muted);}
.nav{display:flex;gap:8px;flex-wrap:wrap;flex:0 0 auto;}
.pill{
  font-family:var(--mono);font-size:12px;letter-spacing:0.08em;text-transform:uppercase;
  border:1px solid var(--rule);border-radius:999px;padding:8px 16px;
  transition:background .25s var(--ease),transform .25s var(--ease);
  white-space:nowrap;
}
.pill:hover{background:var(--green);transform:rotate(-3deg);}
.nav .pill:nth-child(even):hover{transform:rotate(3deg);}
.pill--solid{border-color:var(--ink);background:var(--ink);color:var(--paper);}
.pill--solid:hover{background:var(--ink);color:var(--paper);transform:rotate(3deg);}

/* ───────────────────────── hero ───────────────────────── */
.hero{position:relative;z-index:3;padding:74px var(--pad) 30px;animation:rise .9s var(--ease) both;}
.shape{position:absolute;pointer-events:none;}
.shape--circle{right:8%;top:40px;width:132px;height:132px;border-radius:50%;background:oklch(0.78 0.15 30 / 0.85);animation:float1 11s ease-in-out infinite;}
.shape--square{right:22%;bottom:10px;width:66px;height:66px;border-radius:20px;background:var(--green);animation:float2 14s ease-in-out infinite;}

.hero h1{
  font-family:var(--serif);font-weight:300;
  font-size:clamp(48px,11vw,178px);line-height:.9;letter-spacing:-0.03em;
  margin:0;max-width:15ch;position:relative;
}
.hero h1 em{font-style:italic;font-weight:400;}
.hero-foot{display:flex;flex-wrap:wrap;align-items:flex-end;gap:46px;margin-top:46px;}
.lede{font-size:19px;line-height:1.55;color:var(--body);max-width:40ch;margin:0;}
.tag{font-family:var(--mono);font-size:12px;letter-spacing:0.1em;text-transform:uppercase;color:var(--muted);margin:0;}

/* ───────────────────────── marquee ───────────────────────── */
.marquee{
  position:relative;z-index:3;margin-top:52px;
  border-top:1px solid var(--rule);border-bottom:1px solid var(--rule);
  padding:14px 0;overflow:hidden;white-space:nowrap;
}
.marquee__track{display:inline-flex;gap:40px;padding-right:40px;animation:drift 32s linear infinite;}
.marquee__item,.marquee__sep{font-family:var(--serif);font-weight:300;font-size:26px;color:var(--faint);}
.marquee__sep{opacity:.55;}

/* At this width the whole set of names fits on screen, so sliding them past
   adds motion without adding information. Freeze the strip, drop the duplicate
   set that only exists to make the loop seamless, and drop the trailing
   separator so the row does not end on a dangling dot. Below this width the
   names genuinely overflow, so the scroll earns its place and stays. */
@media (min-width:1040px){
  .marquee{white-space:normal;}
  .marquee__track{
    display:flex;width:100%;justify-content:center;
    animation:none;padding-right:0;
  }
  .marquee__item--dup,.marquee__sep--dup,.marquee__sep--end{display:none;}
}

/* ───────────────────────── carousel ───────────────────────── */
.work{
  position:relative;z-index:3;padding:74px 0 0;
  overflow-x:clip;   /* contains the side cards; `clip` leaves shadows free vertically */
}
.work__head{
  display:flex;justify-content:space-between;align-items:baseline;gap:24px;
  padding:0 var(--pad);margin-bottom:8px;
}
.work__label{font-family:var(--mono);font-size:12px;letter-spacing:0.14em;text-transform:uppercase;color:var(--muted);margin:0;font-weight:400;}
.work__hint{font-family:var(--mono);font-size:11px;letter-spacing:0.06em;color:var(--faint);margin:0;}

.stage{
  position:relative;
  margin-top:26px;
  min-height:460px;
  perspective:1900px;          /* side cards turn in space rather than just shrink */
  perspective-origin:50% 42%;
}

.card{
  /* Anchored to the bottom of the stage, not the top, so every card's devices
     sit on the same baseline. Head text varies in height (BK D2C carries an
     extra token line) and top-anchoring would drop its frames ~64px lower than
     the rest, making the devices lurch as you page through. Growing the text
     upward instead keeps the one thing your eye tracks perfectly still. */
  position:absolute;bottom:0;left:50%;
  width:min(1060px,71vw);
  transform:translateX(-50%);
  transition:transform .78s var(--ease),opacity .78s var(--ease),filter .78s var(--ease);
  /* deliberately no `will-change` and no `transform-style: preserve-3d`.
     will-change would keep all four cards promoted to their own compositor
     layers permanently, for an animation that runs only when you navigate;
     preserve-3d would force a 3D rendering context for every child including
     the iframes. The card's own rotateY still works via the stage's perspective. */
}
/* active */
.card[data-rel="0"]{z-index:30;opacity:1;}
/* the two flanking it are turned away, dimmed, and slightly out of focus, which is
   what actually sells depth; scale alone reads as a small copy, not a far one */
.card[data-rel="-1"],.card[data-rel="1"]{z-index:20;opacity:.42;filter:blur(2px) saturate(.72);}
.card[data-rel="-1"]{transform:translateX(calc(-50% - 63%)) scale(.8) rotateY(15deg);}
.card[data-rel="1"]{transform:translateX(calc(-50% + 63%)) scale(.8) rotateY(-15deg);}
/* anything further back is parked offstage */
.card[data-rel="-2"],.card[data-rel="2"]{z-index:10;opacity:0;pointer-events:none;filter:blur(4px);}
.card[data-rel="-2"]{transform:translateX(calc(-50% - 96%)) scale(.68) rotateY(22deg);}
.card[data-rel="2"]{transform:translateX(calc(-50% + 96%)) scale(.68) rotateY(-22deg);}

.card:not([data-rel="0"]){cursor:pointer;}
.card:not([data-rel="0"]) .screen__shield{display:none;}
.card:not([data-rel="0"]) a{pointer-events:none;}
/* the flanking cards contribute their devices, not their words. Two sets of
   headings crossing behind the live one just reads as noise. `visibility` keeps
   their layout identical so the frames stay aligned across all three. */
.card:not([data-rel="0"]) .card__head{visibility:hidden;}

/* THE expensive combination to avoid: a live, animating cross-origin document
   sitting under a blur filter. The browser re-rasterises and re-blurs it every
   frame it repaints, times four neighbouring frames. Keep them LOADED so
   arriving is still instant, but stop painting them: `visibility:hidden` leaves
   the document running while the cheap striped placeholder shows through, and
   blurring a static placeholder costs nothing. The frame becomes visible again
   the moment its card takes the stage, already loaded. */
.card:not([data-rel="0"]) .screen iframe{visibility:hidden;}

.card__head{
  display:grid;grid-template-columns:1.15fr 1fr;gap:40px;align-items:end;
  min-height:118px;margin-bottom:28px;
}
.card__meta{font-family:var(--mono);font-size:12px;letter-spacing:0.12em;color:var(--faint);}
.card__name{
  font-family:var(--serif);font-weight:400;
  font-size:clamp(32px,4.4vw,64px);line-height:1.02;letter-spacing:-0.02em;margin:10px 0 0;
}
.card__line{font-size:17px;line-height:1.55;color:var(--body);max-width:40ch;margin:0;}

/* access token for a gated preview. Set apart from the prose so it reads as
   something to copy rather than something to skim past. */
.card__token{
  /* block-level flex sized to its content, so it takes its own line instead of
     running into the "Open site" link, but still hugs the text */
  display:flex;width:fit-content;align-items:center;gap:9px;margin:12px 0 0;
  background:var(--gold);border-radius:999px;padding:7px 8px 7px 14px;
}
.card__token b{
  font-family:var(--mono);font-size:11px;font-weight:400;letter-spacing:0.1em;
  text-transform:uppercase;color:#4a4326;
}
.card__token code{
  font-family:var(--mono);font-size:13px;color:var(--ink);
  background:rgba(255,255,255,.68);border-radius:999px;padding:4px 12px;
  user-select:all;   /* one click selects the whole token */
}
.card__link{
  display:inline-block;margin-top:14px;
  font-family:var(--mono);font-size:12px;letter-spacing:0.1em;text-transform:uppercase;
  border-bottom:1px solid var(--ink);padding-bottom:3px;
}
.card__link:hover{border-color:var(--coral-deep);}

.card__frames{display:flex;gap:26px;align-items:flex-end;}

/* ───────── desktop browser frame ───────── */
.browser{
  position:relative;isolation:isolate;
  flex:1 1 auto;min-width:0;
  border-radius:14px;
  box-shadow:var(--sh-rest);
  transition:transform .5s var(--ease);
}
/* the lift shadow is a separate layer faded with opacity, because transitioning
   box-shadow itself repaints this whole (very large) element every frame */
.browser::before{
  content:"";position:absolute;inset:0;z-index:-2;
  border-radius:14px;pointer-events:none;
  box-shadow:var(--sh-lift);
  opacity:0;transition:opacity .5s var(--ease);
}
/* contact shadow: the dark, tight pool directly beneath where the object meets
   the page. Without this a card looks pasted on rather than resting above */
.browser::after{
  content:"";position:absolute;z-index:-3;pointer-events:none;
  left:5%;right:5%;bottom:-20px;height:40px;
  background:radial-gradient(50% 50% at 50% 50%,rgba(58,48,32,.30),rgba(58,48,32,0) 70%);
  filter:blur(11px);
  opacity:.85;transition:opacity .5s var(--ease),transform .5s var(--ease);
}
.card[data-rel="0"] .browser:hover{transform:translateY(-5px);}
.card[data-rel="0"] .browser:hover::before{opacity:1;}
/* lifting the object spreads and softens its contact shadow, it does not keep
   the same hard pool. This is the tell that makes a hover feel physical */
.card[data-rel="0"] .browser:hover::after{opacity:.6;transform:scaleX(1.04) translateY(5px);}

.browser__chrome{
  border:1px solid #e2dbcc;border-radius:14px;overflow:hidden;background:#fff;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.9);   /* top edge catching the light */
}
.browser__bar{
  display:flex;align-items:center;gap:6px;padding:11px 14px;
  background:linear-gradient(#f3eee4,#eae4d8);border-bottom:1px solid #e0d8c9;
}
.dot{width:9px;height:9px;border-radius:50%;flex:none;}
.dot--r{background:var(--coral);}
.dot--y{background:var(--gold);}
.dot--g{background:oklch(0.84 0.09 150);}
.browser__url{margin-left:10px;font-family:var(--mono);font-size:11px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}

/* ───────── phone frame ─────────
   Width is a share of the row so the phone's height (width × 2, from the 390/780
   screen) lands on the browser frame's height. That frame is 16/10 plus a ~41px
   title bar, and the two share the row minus the 26px gap. Solving those together
   gives ≈23.8% + 14px. In CSS rather than JS because sizing one flex item off
   another's measured height causes a measure→resize→measure loop. */
.phone{
  position:relative;isolation:isolate;
  flex:0 0 calc(23.8% + 14px);
}
.phone__body{
  position:relative;
  border:9px solid #16150f;border-radius:34px;overflow:hidden;
  background:#16150f;
  box-shadow:var(--sh-phone);
}
/* a hard specular sliver down the left bezel, glass catching the same light */
.phone__body::after{
  content:"";position:absolute;inset:0;border-radius:26px;pointer-events:none;z-index:4;
  box-shadow:inset 1px 0 0 rgba(255,255,255,.14),inset -1px 0 0 rgba(255,255,255,.06);
}
.phone::after{  /* its own, smaller contact shadow */
  content:"";position:absolute;z-index:-1;pointer-events:none;
  left:8%;right:8%;bottom:-14px;height:26px;
  background:radial-gradient(50% 50% at 50% 50%,rgba(58,48,32,.32),rgba(58,48,32,0) 70%);
  filter:blur(8px);
}
.phone__notch{
  position:absolute;top:1.4%;left:50%;width:25%;height:2.6%;
  border-radius:999px;background:#16150f;transform:translateX(-50%);z-index:3;
}

/* receded cards sit closer to the page: shallower shadows, no contact pool */
.card:not([data-rel="0"]) .browser{box-shadow:var(--sh-recede);}
.card:not([data-rel="0"]) .phone__body{box-shadow:var(--sh-recede);}
.card:not([data-rel="0"]) .browser::after,
.card:not([data-rel="0"]) .phone::after{opacity:.35;}

/* ───────── the live screen ───────── */
.screen{
  position:relative;overflow:hidden;
  background-image:repeating-linear-gradient(135deg,#f2eee5 0 9px,#e9e4d8 9px 18px);
  --scale:0.32;
  contain:layout paint style;
}
.browser .screen{aspect-ratio:16/10;}
.phone .screen{aspect-ratio:390/780;border-radius:26px;}

.screen__placeholder{
  position:absolute;inset:0;z-index:0;
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:7px;
  text-align:center;padding:0 14px;
}
.screen__placeholder span{font-family:var(--mono);font-size:12px;color:#7d776b;}
.screen__placeholder span + span{font-size:11px;color:var(--faint);}

/* Two of these projects are client-rendered, so they return an near-empty shell
   and show nothing until their JS has run. A static label during those seconds
   reads as broken; a pulse reads as working. Only on the slide you are looking
   at, and only until it loads, so nothing animates needlessly. */
@keyframes pulse{0%,100%{opacity:.4;}50%{opacity:1;}}
.card[data-rel="0"] .screen:not(.is-loaded) .screen__placeholder span{
  animation:pulse 1.4s ease-in-out infinite;
}
@media (prefers-reduced-motion:reduce){
  .card[data-rel="0"] .screen:not(.is-loaded) .screen__placeholder span{animation:none;}
}

/* iOS Safari expands iframes to content height; this wrapper contains it */
.screen__vp{position:absolute;inset:0;z-index:1;overflow:hidden;-webkit-overflow-scrolling:touch;}
.screen iframe{
  position:absolute;top:0;left:0;border:0;display:block;
  transform-origin:top left;transform:scale(var(--scale));
  opacity:0;transition:opacity .45s ease;
  background:#fff;
}
.screen.is-loaded iframe{opacity:1;}

.screen__shield{
  position:absolute;inset:0;z-index:2;display:flex;
  align-items:flex-end;justify-content:center;padding-bottom:10px;
  background:transparent;border:0;width:100%;font:inherit;cursor:pointer;
}
.screen__shield.is-off{display:none;}
.screen__shield span{
  font-family:var(--mono);font-size:10px;letter-spacing:.08em;text-transform:uppercase;
  background:rgba(27,26,23,.72);color:var(--paper);padding:5px 10px;border-radius:999px;
  transition:opacity .25s ease;
}
@media (hover:hover){
  .screen__shield span{opacity:0;}
  .screen:hover .screen__shield span,.screen__shield:focus-visible span{opacity:1;}
}

/* ───────── carousel controls ───────── */
.controls{
  display:flex;align-items:center;justify-content:center;gap:22px;
  margin-top:44px;padding:0 var(--pad);
}
.arrow{
  width:52px;height:52px;flex:none;border-radius:50%;
  border:1px solid var(--rule);background:var(--card);
  display:grid;place-items:center;cursor:pointer;color:var(--ink);
  box-shadow:0 1px 2px rgba(58,48,32,.05),0 4px 10px rgba(58,48,32,.05);
  transition:transform .25s var(--ease),background .25s var(--ease),box-shadow .25s var(--ease);
}
.arrow:hover{
  background:var(--green);transform:translateY(-2px);
  box-shadow:0 2px 4px rgba(58,48,32,.06),0 8px 20px rgba(58,48,32,.08);
}
.arrow:active{transform:translateY(0);box-shadow:0 1px 2px rgba(58,48,32,.06);}
.arrow svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:1.6;}

.dots{display:flex;gap:9px;align-items:center;}
.dot-btn{
  width:9px;height:9px;padding:0;border:0;border-radius:50%;cursor:pointer;
  background:#d3ccbc;transition:background .3s var(--ease),transform .3s var(--ease),width .3s var(--ease);
}
.dot-btn[aria-current="true"]{background:var(--ink);width:26px;border-radius:999px;}

.counter{
  font-family:var(--mono);font-size:12px;letter-spacing:.12em;color:var(--muted);
  min-width:64px;text-align:center;
}

/* ───────────────────────── about / stack ───────────────────────── */
.strip{position:relative;z-index:3;padding:20px var(--pad) 0;}
.strip--about{padding-top:104px;}
.strip--stack{padding-top:78px;}
.strip__inner{
  border-top:1px solid var(--rule);padding-top:30px;
  display:grid;grid-template-columns:200px 1fr;gap:46px;
}
.strip__label{
  font-family:var(--mono);font-size:12px;letter-spacing:0.14em;text-transform:uppercase;
  color:var(--muted);margin:8px 0 0;font-weight:400;
}
.about__statement{
  font-family:var(--serif);font-weight:300;
  font-size:clamp(24px,3.4vw,50px);line-height:1.22;letter-spacing:-0.015em;
  margin:0;max-width:40ch;
}
.roles{display:flex;gap:16px;margin-top:34px;flex-wrap:wrap;}
.role{font-family:var(--mono);font-size:12px;letter-spacing:0.08em;text-transform:uppercase;border-radius:999px;padding:9px 16px;}
.role--1{background:var(--green);}
.role--2{background:var(--gold);}
.role--3{background:oklch(0.82 0.1 30);}
/* fourth title needs its own tint; a soft blue reads as distinct from the
   green/gold/coral without fighting the warm paper */
.role--4{background:oklch(0.85 0.06 250);}

.chips{display:flex;flex-wrap:wrap;gap:10px;}
.chip{
  font-family:var(--mono);font-size:13px;color:#3c3931;
  border:1px solid var(--rule);border-radius:999px;padding:9px 15px;background:var(--card);
  transition:transform .25s var(--ease);
}
.chip:hover{transform:translateY(-3px) rotate(-2deg);}

/* ───────────────────────── contact ───────────────────────── */
.contact{
  position:relative;z-index:3;
  margin:100px var(--pad) 60px;
  background:var(--ink);color:var(--paper);border-radius:28px;
  padding:64px 56px;
  display:grid;grid-template-columns:1fr 1fr;gap:64px;align-items:start;
  overflow:hidden;
  box-shadow:var(--sh-lift);
}
.contact__shape{
  position:absolute;right:-30px;top:-20px;width:88px;height:88px;border-radius:50%;
  background:oklch(0.78 0.15 30 / .9);animation:float2 12s ease-in-out infinite;
}
.contact h2{
  font-family:var(--serif);font-weight:300;
  font-size:clamp(38px,5.6vw,84px);line-height:.96;letter-spacing:-0.025em;margin:0;position:relative;
}
.contact h2 em{font-style:italic;font-weight:400;}
.contact__lede{font-size:17px;line-height:1.6;color:#b3ada1;max-width:30ch;margin:24px 0 0;}

.details{display:grid;gap:2px;align-content:start;}
.detail{display:grid;grid-template-columns:88px 1fr;gap:18px;align-items:baseline;padding:18px 0;border-bottom:1px solid #302d27;}
.detail:first-child{border-top:1px solid #302d27;}
.detail__key{font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:#8d877b;}
.detail__val{font-size:19px;color:var(--paper);word-break:break-word;}
a.detail__val{border-bottom:1px solid #4a463d;padding-bottom:2px;transition:border-color .25s ease,color .25s ease;}
a.detail__val:hover{color:var(--green);border-color:var(--green);}
.cta{
  display:inline-block;margin-top:30px;
  background:var(--green);border-radius:999px;padding:15px 26px;
  font-family:var(--mono);font-size:13px;letter-spacing:0.1em;text-transform:uppercase;
  color:var(--ink);justify-self:start;transition:transform .25s var(--ease);
}
.cta:hover{transform:rotate(-2deg) scale(1.03);color:var(--ink);}

/* ── closing call to action ──
   The same block at the foot of every page that is not the form itself. A hairline
   above it separates it from whatever content it follows without adding a heavy
   band, so it reads as the page's last line rather than an advertisement bolted on. */
.endcta{
  margin-top:96px;padding-top:38px;border-top:1px solid var(--rule);
  display:grid;justify-items:start;gap:20px;
}
.endcta__line{
  margin:0;font-family:var(--serif);font-size:clamp(26px,3.4vw,40px);
  line-height:1.15;letter-spacing:-0.01em;color:var(--ink);max-width:20ch;
}
.endcta .cta{margin-top:0;}

@media (max-width:620px){
  .endcta{margin-top:66px;padding-top:30px;}
}

/* ── quote form ──
   Same paper, rules and mono labels as the rest of the site. Inputs sit on the
   card colour so a long form still reads as a stack of fields rather than a
   wall, and the focus state uses the deep coral so it is visible against cream
   without introducing a colour the site does not already use. */
.form{max-width:660px;}
.form__row{display:grid;grid-template-columns:1fr 1fr;gap:0 22px;}

.field{margin-bottom:26px;}
.field__label{
  display:block;margin-bottom:9px;
  font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;
  color:var(--muted);
}
.field__req{color:var(--coral-deep);}
.field__opt{text-transform:none;letter-spacing:.04em;color:var(--faint);}

.field__input{
  display:block;width:100%;box-sizing:border-box;
  background:var(--card);border:1px solid var(--rule);border-radius:9px;
  padding:13px 15px;
  /* 16px keeps iOS Safari from zooming the page on focus */
  font-family:var(--sans);font-size:16px;line-height:1.5;color:var(--ink);
  transition:border-color .2s ease,box-shadow .2s ease;
}
.field__input::placeholder{color:var(--faint);}
.field__input:hover{border-color:var(--muted);}
.field__input:focus{
  outline:none;border-color:var(--coral-deep);
  box-shadow:0 0 0 3px color-mix(in oklch,var(--coral-deep) 18%,transparent);
}
.field__input--area{resize:vertical;min-height:150px;}

/* native select arrow varies wildly across platforms, so draw our own */
.field__input--select{
  appearance:none;-webkit-appearance:none;
  padding-right:40px;cursor:pointer;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' fill='none' stroke='%238b8578' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 15px center;background-size:12px 8px;
}

.field__hint{margin:8px 0 0;font-size:13.5px;line-height:1.5;color:var(--muted);}

/* invalid only after a submit attempt, so the form does not scold as you type */
.form--checked .field__input:invalid{border-color:var(--coral-deep);}

.cta--submit{border:none;cursor:pointer;font-family:var(--mono);margin-top:6px;}
.form__foot{margin:22px 0 0;font-size:15px;color:var(--muted);}
.form__foot a{border-bottom:1px solid var(--rule);padding-bottom:1px;color:var(--body);}
.form__foot a:hover{color:var(--coral-deep);border-color:var(--coral-deep);}

.formnote{
  margin:0 0 30px;padding:14px 18px;border-radius:9px;
  border:1px solid var(--coral-deep);
  background:color-mix(in oklch,var(--coral) 22%,var(--card));
  font-size:15.5px;line-height:1.5;color:var(--ink);max-width:660px;
}

/* Honeypot. Kept in the layout and merely moved off screen: display:none is a
   pattern bots recognise and skip, which defeats the point. */
.pot{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden;}

@media (max-width:620px){
  .form__row{grid-template-columns:1fr;gap:0;}
}

.site-footer{
  position:relative;z-index:3;padding:0 var(--pad) 44px;
  display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap;
  font-family:var(--mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--faint);
}

/* ───────────────────────── reveal ───────────────────────── */
.reveal{opacity:0;transform:translateY(26px);transition:opacity .8s var(--ease),transform .8s var(--ease);}
.reveal.is-in{opacity:1;transform:none;}

/* ───────────────────────── responsive ───────────────────────── */
@media (max-width:1080px){
  :root{--pad:32px;}
  .card{width:78vw;}
  .card__head{grid-template-columns:1fr;gap:16px;min-height:0;}
  .card__line{max-width:56ch;}
}

@media (max-width:860px){
  :root{--pad:22px;}
  .site-header{flex-direction:column;align-items:flex-start;gap:14px;padding-top:20px;}
  .wordmark{font-size:clamp(21px,6vw,32px);}
  /* let it break after the name rather than orphaning a word off the title */
  .wordmark__title{display:block;}
  .nav{gap:6px;}
  .pill{padding:7px 13px;font-size:11px;}
  .hero{padding-top:44px;}
  .shape--circle{width:84px;height:84px;top:auto;bottom:-10px;right:4%;}
  .shape--square{display:none;}
  .hero-foot{gap:22px;}
  .lede{font-size:17px;}
  .marquee__item{font-size:22px;}
  .work{padding-top:52px;}
  .work__hint{display:none;}
  .card{width:84vw;}
  /* one preview per card, because a 1440px viewport scaled this small is unreadable,
     and two live iframes on a phone connection is punishing */
  .card__frames .browser{display:none;}
  /* except where the browser frame is the only frame there is: a desktop-only
     project (a Power BI canvas) has no phone view to fall back to, so hiding it
     would leave an empty card */
  .frames--desktop .browser{display:block;}
  .card__frames{justify-content:center;}
  .phone{flex:0 0 min(300px,70vw);}
  .card[data-rel="-1"]{transform:translateX(calc(-50% - 72%)) scale(.82) rotateY(12deg);}
  .card[data-rel="1"]{transform:translateX(calc(-50% + 72%)) scale(.82) rotateY(-12deg);}
  .controls{gap:14px;margin-top:34px;}
  .arrow{width:46px;height:46px;}
  .strip--about{padding-top:74px;}
  .strip__inner{grid-template-columns:1fr;gap:18px;}
  .contact{grid-template-columns:1fr;gap:36px;padding:44px 28px;margin-top:70px;border-radius:22px;}
  .contact__shape{display:none;}
  .detail{grid-template-columns:1fr;gap:6px;padding:14px 0;}
  .detail__val{font-size:17px;}
}

@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto;}
  *,*::before,*::after{animation-duration:.001ms !important;animation-iteration-count:1 !important;transition-duration:.001ms !important;}
  .reveal{opacity:1;transform:none;}
  .blob{display:none;}
}

/* ═══════════════════ multi-page additions ═══════════════════ */

/* masthead is now a link home */
.wordmark a{color:inherit;}
.wordmark a:hover .wordmark__name{color:var(--coral-deep);}
.pill--on{background:var(--green);border-color:transparent;}

.footer-nav{display:flex;gap:18px;flex-wrap:wrap;}
.footer-nav a{color:var(--faint);}
.footer-nav a:hover{color:var(--coral-deep);}

/* ── shared interior page frame ── */
.page{position:relative;z-index:3;padding:40px var(--pad) 96px;}
/* No `ch` cap on these wrappers. `ch` resolves against the WRAPPER's 16px font,
   not the 100px headline inside it, which squeezed the title to ~570px and broke
   it across three lines. Each child gets its own limit, in its own type size. */
.page__head{margin-bottom:44px;}
.page__eyebrow{
  font-family:var(--mono);font-size:12px;letter-spacing:0.14em;text-transform:uppercase;
  color:var(--muted);margin:0 0 18px;
}
.page__title{
  max-width:15ch;
  font-family:var(--serif);font-weight:300;
  font-size:clamp(40px,7vw,104px);line-height:.96;letter-spacing:-0.03em;margin:0;
}
.page__title em{font-style:italic;font-weight:400;}
.page__lede{font-size:19px;line-height:1.55;color:var(--body);max-width:44ch;margin:24px 0 0;}
.page__next{margin:64px 0 0;}
.page__next a,.strip__more a{
  font-family:var(--mono);font-size:12px;letter-spacing:0.1em;text-transform:uppercase;
  border-bottom:1px solid var(--ink);padding-bottom:3px;
}
.page__next a:hover,.strip__more a:hover{border-color:var(--coral-deep);}
.strip__more{margin:28px 0 0;}
/* same `ch` trap as the page headers: the statement inside is display type and
   already carries its own 40ch limit, measured in ITS size, not this wrapper's */
.prose{max-width:none;}

/* ── /work/ listing ── */
.worklist{list-style:none;margin:0;padding:0;border-top:1px solid var(--rule);}
.worklist__link{
  display:grid;grid-template-columns:44px 1fr auto;gap:20px;align-items:center;
  padding:30px 6px 30px 0;border-bottom:1px solid var(--rule);
  transition:padding-left .28s var(--ease),background .28s var(--ease);
}
.worklist__link:hover{padding-left:16px;background:var(--card);}
.worklist__num{font-family:var(--mono);font-size:12px;color:var(--faint);}
.worklist__name{
  display:block;font-family:var(--serif);font-weight:400;
  font-size:clamp(26px,3.4vw,42px);line-height:1.1;letter-spacing:-0.02em;
}
.worklist__line{display:block;font-size:16px;color:var(--body);margin-top:6px;max-width:52ch;}
.worklist__kind{
  font-family:var(--mono);font-size:11px;letter-spacing:0.1em;text-transform:uppercase;
  color:var(--faint);
  /* three titles will not fit on one line in this column, so let it wrap and
     stay right-aligned rather than forcing the row wider */
  text-align:right;max-width:16ch;line-height:1.5;
}

/* ── project page ── */
.crumb{
  font-family:var(--mono);font-size:12px;letter-spacing:0.08em;color:var(--faint);
  margin:0 0 34px;display:flex;gap:8px;
}
.crumb a{color:var(--muted);}
.crumb a:hover{color:var(--coral-deep);}
.project__head{margin-bottom:44px;}
.project__title{
  max-width:14ch;
  font-family:var(--serif);font-weight:400;
  font-size:clamp(38px,6.4vw,88px);line-height:1;letter-spacing:-0.025em;margin:10px 0 0;
}
.project__line{font-size:19px;line-height:1.55;color:var(--body);max-width:46ch;margin:22px 0 0;}
.project__detail{font-size:17px;line-height:1.65;color:var(--body);max-width:60ch;margin:18px 0 0;}
.project__links{margin:22px 0 0;}
.project-page .card__token{margin-top:20px;}

/* the frames sit in normal flow here, no carousel stage around them */
.project__frames{align-items:flex-end;margin-bottom:72px;}
.project__frames .browser{transform:none;}

/* ── prev / next ── */
.pager{
  display:grid;grid-template-columns:1fr 1fr;gap:20px;
  border-top:1px solid var(--rule);padding-top:22px;margin-bottom:20px;
}
.pager__side{display:grid;gap:6px;padding:16px 0;}
.pager__side--next{text-align:right;}
.pager__dir{
  font-family:var(--mono);font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:var(--faint);
}
.pager__name{font-family:var(--serif);font-size:clamp(22px,2.6vw,32px);line-height:1.1;}

.details--page{max-width:640px;margin-bottom:36px;}
.details--page .detail{border-color:var(--rule);}
.details--page .detail__key{color:var(--muted);}
.details--page .detail__val{color:var(--ink);}
.details--page a.detail__val{border-bottom-color:var(--rule);}
.details--page a.detail__val:hover{color:var(--coral-deep);border-color:var(--coral-deep);}
.page .cta{margin-top:0;}

@media (max-width:860px){
  .page{padding:20px var(--pad) 64px;}
  .page__head{margin-bottom:32px;}
  .worklist__link{grid-template-columns:32px 1fr;gap:14px;padding:22px 0;}
  .worklist__kind{grid-column:2;font-size:10px;}
  .project__frames{margin-bottom:48px;}
  .pager{grid-template-columns:1fr;gap:0;}
  .pager__side--next{text-align:left;border-top:1px solid var(--rule);}
}

.card__links{display:flex;gap:22px;align-items:center;flex-wrap:wrap;margin-top:14px;}
.card__links .card__link{margin-top:0;}
.card__link--out{color:var(--muted);border-bottom-color:var(--rule);}
.card__link--out:hover{color:var(--coral-deep);border-color:var(--coral-deep);}

/* the roles line under the hero headline */
.hero__roles{
  font-size:clamp(17px,1.9vw,23px);
  line-height:1.45;
  color:var(--body);
  letter-spacing:-0.005em;
  /* no cap: the whole line fits on one row under the headline. When a narrow
     screen does force a wrap, `balance` splits it evenly instead of stranding
     "Engineer, & Designer." on its own. */
  max-width:none;
  text-wrap:balance;
  margin:38px 0 0;
  /* the decorative shapes are absolutely positioned and would otherwise paint
     over this line on a narrow screen */
  position:relative;z-index:1;
}
@media (max-width:860px){
  .hero__roles{margin-top:26px;max-width:30ch;}
}



/* ── about page hero photo ──
   Treated like every other object on this site: a rounded frame resting on the
   paper, with the same layered shadow language as the browser and phone frames.
   Before this it was the one square-edged, shadowless element on the page and it
   read as pasted on rather than part of the set.

   The figure is full bleed (the page wrapper carries `--pad` on both sides, so
   pulling out by that amount reaches the viewport edges without `100vw`, which
   would include the scrollbar). Inside it, a 25 / 65 / 10 grid: text in the left
   quarter, photo in the middle, right tenth deliberately empty. */
.page--about{padding-top:0;}
.about-hero{
  margin:0 calc(var(--pad) * -1) 58px;
  display:grid;
  grid-template-columns:25% 65% 10%;
  align-items:center;
}

/* Both children are pinned to row 1. Setting only grid-column is not enough:
   auto-placement had already advanced past row 1 after the image, so the caption
   landed in a new row underneath instead of beside it. */
.about-hero__shot{
  grid-column:2;grid-row:1;
  position:relative;isolation:isolate;
}
.about-hero__shot img{
  display:block;width:100%;
  height:clamp(300px,38vw,560px);
  object-fit:cover;
  /* biased down from centre: the court and the player sit below the midline,
     and a straight centre crop starts eating them on short viewports */
  object-position:50% 56%;
  border-radius:14px;                 /* same radius as the browser frame */
  background:#e9e4d8;
  box-shadow:
    var(--sh-rest),
    inset 0 0 0 1px rgba(58,48,32,.07);   /* hairline, so the edge reads against cream */
}
/* the tight, darker pool where the object meets the page: what makes it sit
   above the paper instead of being printed onto it */
.about-hero__shot::after{
  content:"";position:absolute;z-index:-1;pointer-events:none;
  left:4%;right:4%;bottom:-20px;height:42px;
  background:radial-gradient(50% 50% at 50% 50%,rgba(58,48,32,.28),rgba(58,48,32,0) 70%);
  filter:blur(12px);
}

.about-hero__cap{
  grid-column:1;grid-row:1;
  margin:0;
  padding:0 26px 0 var(--pad);   /* line up with the page gutter on the left */
  font-family:var(--serif);font-weight:300;
  font-size:clamp(16px,1.35vw,21px);line-height:1.42;letter-spacing:-0.008em;
  color:var(--ink);
  text-wrap:pretty;
}

/* photo missing: lose the frame, keep the sentence, it is real content */
.about-hero--nopic{grid-template-columns:1fr;}
.about-hero--nopic .about-hero__shot{display:none;}

@media (max-width:860px){
  /* too narrow to hold a column of text beside a photo: stack them. Also drop
     the full bleed, because rounded corners running off the viewport edge would
     defeat the point of rounding them. */
  .about-hero{grid-template-columns:1fr;margin:0 0 36px;}
  .about-hero__shot{grid-column:1;grid-row:1;}
  .about-hero__shot img{height:clamp(240px,58vw,340px);}
  .about-hero__cap{
    grid-column:1;grid-row:2;
    padding:30px 0 0;max-width:none;
    font-size:clamp(18px,4.6vw,22px);
  }
}

/* ── native embed (Power BI) ──
   Fills its container at real size instead of being rendered large and scaled
   down, so the report's own text stays legible. The aspect ratio comes from the
   report itself and is set inline per project. */
.screen--native{aspect-ratio:auto;}
.browser .screen--native{aspect-ratio:auto;}
.screen--native iframe{
  position:absolute;inset:0;
  width:100%;height:100%;
  transform:none;
}
/* the embed is capped and centred rather than stretching the full page width */
.frames--desktop .browser{max-width:1100px;margin-left:auto;margin-right:auto;}

/* ── case study ──
   Uses the same label-left / content-right grid as the About and Stack strips,
   so a long read still belongs to the same page. The document's own structure is
   kept: a short summary line under each label, then the prose. */
.case{margin:78px 0 0;border-top:1px solid var(--rule);padding-top:34px;}
.case__head{margin-bottom:52px;}
.case__eyebrow{
  font-family:var(--mono);font-size:12px;letter-spacing:0.14em;text-transform:uppercase;
  color:var(--muted);margin:0 0 18px;
}
.case__deck{
  font-family:var(--serif);font-weight:300;
  font-size:clamp(24px,3vw,40px);line-height:1.22;letter-spacing:-0.02em;
  color:var(--ink);margin:0;max-width:26ch;
}
.case__byline{
  font-family:var(--mono);font-size:12px;letter-spacing:0.06em;color:var(--faint);
  margin:22px 0 0;
}

.case__sec{
  display:grid;grid-template-columns:200px 1fr;gap:46px;
  padding:30px 0;border-top:1px solid var(--rule);
}
.case__h{
  font-family:var(--mono);font-size:12px;letter-spacing:0.14em;text-transform:uppercase;
  color:var(--muted);margin:4px 0 0;font-weight:400;
}
/* `min-width:0` is load-bearing. A grid item defaults to min-width:auto, which
   lets it expand to fit its widest child, so the 420px-min table stretched this
   column past its track and got clipped by the page instead of scrolling inside
   its own box. With this, the wrapper's overflow-x:auto actually engages. */
.case__body{max-width:68ch;min-width:0;}
.case__lead{
  font-family:var(--serif);font-weight:300;
  font-size:clamp(20px,2.2vw,29px);line-height:1.3;letter-spacing:-0.015em;
  color:var(--ink);margin:0 0 22px;
}
.case__body p{font-size:17px;line-height:1.65;color:var(--body);margin:0 0 18px;}
.case__body p:last-child{margin-bottom:0;}

.case__srcs{list-style:none;margin:0 0 20px;padding:0;}
.case__srcs li{font-size:17px;line-height:1.6;color:var(--body);padding:12px 0;border-bottom:1px solid var(--rule);}
.case__srcs li:first-child{border-top:1px solid var(--rule);}
.case__srcs b{font-weight:500;color:var(--ink);}

.case__list{margin:0;padding-left:1.3em;}
.case__list li{font-size:17px;line-height:1.65;color:var(--body);margin-bottom:16px;padding-left:6px;}
.case__list li::marker{font-family:var(--mono);font-size:13px;color:var(--faint);}

/* the numbers are the point of this table, so they get the mono face */
/* code excerpts: real source, so they scroll in their own box rather than
   widening the 68ch reading column or wrapping mid-expression */
.case__codewrap{margin:26px 0;}
.case__code{
  overflow-x:auto;margin:0;padding:18px 20px;
  background:var(--card);border:1px solid var(--rule);border-radius:8px;
  font-family:var(--mono);font-size:13.5px;line-height:1.62;color:var(--ink);
  -webkit-overflow-scrolling:touch;
}
.case__code code{font:inherit;color:inherit;white-space:pre;}
.case__codewrap figcaption{
  font-family:var(--mono);font-size:11.5px;letter-spacing:0.08em;
  color:var(--faint);margin-top:9px;
}
.case__tablewrap{overflow-x:auto;margin:26px 0;}
.case__table{border-collapse:collapse;width:100%;min-width:420px;}
.case__table th,.case__table td{text-align:left;padding:11px 14px;border-bottom:1px solid var(--rule);}
.case__table th{
  font-family:var(--mono);font-size:11px;letter-spacing:0.1em;text-transform:uppercase;
  color:var(--muted);font-weight:400;border-bottom-color:var(--ink);
}
.case__table td{font-size:16px;color:var(--body);}
.case__table td:nth-child(2),.case__table td:nth-child(3){font-family:var(--mono);font-size:14px;color:var(--ink);}
.case__table tbody tr:hover{background:var(--card);}

.case__foot{
  font-size:13px;line-height:1.6;color:var(--faint);
  margin:34px 0 0;padding-top:22px;border-top:1px solid var(--rule);max-width:80ch;
}

@media (max-width:860px){
  .case{margin-top:52px;}
  .case__head{margin-bottom:34px;}
  .case__sec{grid-template-columns:1fr;gap:14px;padding:24px 0;}
  .case__body{max-width:none;}
  .case__code{font-size:12.5px;padding:14px 15px;}
  .case__deck{max-width:none;}
}
