/* ============================================================
   v3.css · "Low Anchor" · Swiss / International Typographic Style
   Built from the Northlit direction swiss-low-anchor (run mb_mu8kmutc_bai7ua).
   Contract: DESIGN.md at repo root · tokens at design/tokens.json.
   Monochrome; grotesque display (Helvetica Neue / Arial) + Inter + JetBrains
   Mono; strict 12-col / 24px-gutter grid on an 8pt scale; zero radius on
   structure; the orange accent appears only on CTA hover, active section
   numbers and list markers. No entry choreography.
   ============================================================ */
/* registered color tokens, animatable (the scroll theme interpolates them) */
@property --bg{syntax:'<color>';inherits:true;initial-value:#f5f5f7}
@property --surface{syntax:'<color>';inherits:true;initial-value:#ffffff}
@property --surface-raised{syntax:'<color>';inherits:true;initial-value:#ebebee}
@property --fg{syntax:'<color>';inherits:true;initial-value:#0b0b0e}
@property --fg-mid{syntax:'<color>';inherits:true;initial-value:#4a4b52}
@property --muted{syntax:'<color>';inherits:true;initial-value:#e2e2e6}
@property --border{syntax:'<color>';inherits:true;initial-value:#c4c5cb}
@property --border-soft{syntax:'<color>';inherits:true;initial-value:#d6d7dc}
@property --grid-dot{syntax:'<color>';inherits:true;initial-value:#a5a6ad}
@property --accent{syntax:'<color>';inherits:true;initial-value:#ff6a3d}
@property --on-fg{syntax:'<color>';inherits:true;initial-value:#ffffff}
:root{
  /* color, light (Northlit tokens, oklch) */
  --bg:oklch(98.6% 0.003 264);
  --surface:oklch(100% 0 0);
  --surface-raised:oklch(95% 0.004 264);
  --fg:oklch(7% 0.008 264);
  --fg-mid:oklch(36% 0.01 264);
  --muted:oklch(91% 0.005 264);
  --border:oklch(84% 0.006 264);
  --border-soft:oklch(91% 0.005 264);
  --grid-dot:oklch(68% 0.006 264);
  --accent:oklch(62.5% 0.195 33);--logo-blue:oklch(58% 0.13 258);
  --on-fg:oklch(100% 0 0);
  --shadow-sm:0 1px 3px rgba(10,15,28,.06),0 2px 6px rgba(10,15,28,.10);
  --shadow-md:0 2px 6px rgba(10,15,28,.06),0 6px 16px rgba(10,15,28,.10);
  /* type */
  --font-display:"Helvetica Neue",Helvetica,Arial,sans-serif;
  --font-body:Inter,system-ui,-apple-system,sans-serif;
  --font-mono:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; /* labels are sans now: one family, tracked small caps */
  /* Type scale. Six steps, so a size difference always reads as a change of
     level rather than as drift. Before this the page ran nine sizes inside an
     8px band, which is not a hierarchy, it is noise. Display sizes above t6
     stay bespoke per page: there is only ever one on screen, so it is a
     statement rather than a step. Tracking is a property of the step, not of
     the component, which is what kept twenty near-identical values alive. */
  --t1:12px;                      /* micro: mono labels, status, captions */
  --t2:14px;                      /* small: UI, navigation, secondary */
  --t3:16px;                      /* body */
  --t4:clamp(19px,1.7vw,23px);    /* lede */
  --t5:clamp(24px,2.6vw,34px);    /* subhead */
  --t6:clamp(34px,4vw,52px);      /* head */
  --tr-tight:-.04em;              /* display */
  --tr-head:-.02em;               /* heads */
  --tr-text:-.01em;               /* text */
  --tr-caps:.12em;                /* tracked small caps */
  --tr-wide:.06em;                /* mono links and eyebrows */
  /* 8pt scale */
  --s1:8px;--s2:16px;--s3:24px;--s4:32px;--s5:48px;--s6:64px;--s7:96px;--s8:160px;
  --gutter:24px;
  --pad-x:clamp(24px,7vw,120px);
  --maxw:1536px;
  --radius:6px;
  /* filled text fields: a quiet surface at rest that lifts when it takes
     focus. Mixed from the foreground so one pair of steps reads on both
     themes instead of two hand-picked greys. */
  --fld:color-mix(in oklab,var(--fg) 5%,transparent);
  --fld-hi:color-mix(in oklab,var(--fg) 9%,transparent);
  --fld-line:color-mix(in oklab,var(--fg) 11%,transparent);
  --fld-line-hi:color-mix(in oklab,var(--fg) 26%,transparent);
  --r-btn:10px;
  --mark-ground:#fff;   /* the ground a lone brand mark sits on */
  --shot-ground:#eef1f6;--shot-grid:rgba(20,30,50,.07);   /* the ground a framed product shot sits on */
  /* motion (the only permitted transitions) */
  --ease:cubic-bezier(.16,1,.3,1);
  --t-hover:160ms var(--ease);
  --t-line:160ms var(--ease);
  --d-reveal:480ms;
  --d-hero:800ms;
}
html[data-theme="dark"],html[data-theme="auto"].scrolled{
  --bg:oklch(10.8% 0.018 264);
  --surface:oklch(13.2% 0.02 264);
  --surface-raised:oklch(17.4% 0.022 264);
  --fg:oklch(94.8% 0.01 264);
  --fg-mid:oklch(65% 0.016 264);
  --muted:oklch(19.5% 0.02 264);
  --border:oklch(28% 0.016 264);
  --border-soft:oklch(22% 0.016 264);
  --grid-dot:oklch(32% 0.016 264);
  --accent:oklch(65% 0.195 33);--logo-blue:oklch(62% 0.13 258);
  --on-fg:oklch(10.8% 0.018 264);
  --mark-ground:#000;
  --shot-ground:oklch(14.5% 0.02 264);--shot-grid:rgba(255,255,255,.06);
  --shadow-sm:0 1px 3px rgba(0,0,0,.4),0 2px 6px rgba(0,0,0,.5);
  --shadow-md:0 2px 6px rgba(0,0,0,.4),0 6px 16px rgba(0,0,0,.5);
}

*{box-sizing:border-box;margin:0;padding:0}
/* the page colour is on the root as well as the body, so the canvas behind an
   overscroll, and behind the body while a landing fades in, is the page colour
   rather than the browser's white */
html{background:var(--bg);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-rendering:optimizeLegibility;font-feature-settings:"tnum" 1,"lnum" 1}
body{background:var(--bg);color:var(--fg);font-family:var(--font-body);font-size:var(--t2);line-height:1.5;
  transition:background-color var(--t-hover),color var(--t-hover)}
img{display:block;max-width:100%}
a{color:inherit;text-decoration:none}
h1,h2,h3{font-family:var(--font-display);font-weight:900;letter-spacing:var(--tr-head)}
p{margin:0}

/* ── LANDING ───────────────────────────────────────────────────────────────
   A load that is going somewhere other than the top, a link with a fragment
   or a step back, is held for the frame or two it takes to get there, then
   faded in already in place. Without it the browser paints the top of the
   page and only scrolls a few hundred milliseconds later, which reads as a
   flash of the hero and a jump. This stylesheet blocks rendering, so the rule
   is in force before the first paint; the head script that sets the class
   always clears it, on a timer if nothing else, so the page cannot be left
   hidden. The background stays, so what is held back is the content, not the
   window. See the LAND BEFORE PAINT script in the page head. */
/* A fragment jump has to clear the sticky bar, the same clearance the section
   glide uses, or the browser parks the heading underneath it. It also keeps
   the browser's own fragment scroll and the head script's landing on the same
   offset, so a load with a fragment settles once rather than twice. */
main[id],section[id],.arch-cell[id]{scroll-margin-top:90px}
@media(max-width:760px){main[id],section[id],.arch-cell[id]{scroll-margin-top:74px}}
/* transitions are off while it is held: the auto theme is settled during this
   window, and a crossfade started here would still be running, light on its
   way to dark, when the page is revealed */
.pinning body{opacity:0;transition:none}
.pinned body{animation:landIn .3s var(--ease) both}
@keyframes landIn{from{opacity:0}to{opacity:1}}
@media(prefers-reduced-motion:reduce){.pinned body{animation:none}}

/* focus: one ring everywhere, in the accent, rounded to match the radius
   language and offset so it never crowds the element it marks */
:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
a:focus-visible,button:focus-visible{border-radius:6px}
.glass:focus-visible,.ibtn:focus-visible{border-radius:999px;outline-offset:4px}
.skip-link{position:absolute;left:0;top:-120px;background:var(--fg);color:var(--on-fg);padding:8px 16px;z-index:100}
.skip-link:focus{top:0}
.wrap{max-width:var(--maxw);margin:0 auto;padding:0 var(--pad-x);width:100%}
/* monochrome photography */
.bw{filter:none}

/* ── MASTHEAD: plain, not sticky, no blur ── */
/* The blur is re-sampled every scrolled frame, and its cost scales with the
   radius: at 14px the homepage's p95 frame was 27.8ms, at 8px it is 16.8ms,
   which is the whole tail. The frosted look survives the smaller radius. */
.masthead{padding:var(--s2) 0;position:sticky;top:0;z-index:60;background:color-mix(in oklab,var(--bg) 82%,transparent);-webkit-backdrop-filter:blur(8px) saturate(1.2);backdrop-filter:blur(8px) saturate(1.2)}
html.chat-open .masthead{visibility:hidden}
.mast-inner{display:flex;align-items:center;justify-content:space-between;gap:var(--s3)}
.brand{display:inline-flex;align-items:center;gap:12px;font-family:var(--font-display);font-weight:900;font-size:var(--t2);letter-spacing:var(--tr-text)}
.brand img{width:26px;height:26px}
.mast-right{display:flex;align-items:center;gap:var(--s2)}
.nav-links{display:flex;gap:clamp(4px,1.2vw,14px);list-style:none}
.nav-links a{display:inline-block;font-size:var(--t2);font-weight:500;padding:12px 10px 10px;border-bottom:1px solid transparent;transition:border-color var(--t-line)}
.nav-links a:hover{border-bottom-color:var(--fg)}
/* the page you are on is marked as well as announced: every item carries the
   same hairline, the current one carries it at full strength */
.nav-links a[aria-current="page"]{color:var(--fg)}
.ibtn{width:44px;height:44px;display:grid;place-items:center;background:transparent;border:0;border-radius:var(--r-btn);color:var(--fg);cursor:pointer;transition:color var(--t-hover),background-color var(--t-hover)}
.ibtn:hover{background:var(--muted)}
.ibtn:hover{color:var(--accent)}
.ibtn .ic-moon,.ibtn .ic-auto{display:none}.ibtn .ic-sun{display:block}
html[data-theme="dark"] .ibtn .ic-sun{display:none}
html[data-theme="dark"] .ibtn .ic-moon{display:block}
html[data-theme="auto"] .ibtn .ic-sun{display:none}
html[data-theme="auto"] .ibtn .ic-auto{display:block}
.nav-toggle{display:none}

/* ── HOMEPAGE: Low Anchor variation var_mu8pf03d_ekjd ──
   Name high-left, a tall B&W column image with one accent diagonal,
   data annotated in the right margin, sections separated by air. */
.cta{display:inline-flex;align-items:center;gap:20px;font-size:var(--t2);font-weight:500;padding:10px 0 8px;border-bottom:1px solid var(--fg);
  transition:gap 180ms ease-out,border-color var(--t-hover),color var(--t-hover);min-height:44px}
.cta .arw{width:24px;height:9px;flex:none}
.cta:hover{gap:28px;color:var(--accent);border-bottom-color:var(--accent)}
.cta-sm{display:inline-flex;align-items:center;gap:14px;font-size:var(--t2);font-weight:500;padding:8px 0 6px;border-bottom:1px solid var(--fg);transition:gap 180ms ease-out,color var(--t-hover),border-color var(--t-hover);min-height:40px}
.cta-sm .arw{width:20px;height:8px;flex:none}
.cta-sm:hover{gap:20px;color:var(--accent);border-bottom-color:var(--accent)}
.g12{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));column-gap:var(--gutter)}

/* hero */
/* isolate so the cover can sit at -1, under the grid lines and the particle
   canvas — both paint before it and at the same level otherwise */
.hero{position:relative;isolation:isolate;padding:clamp(16px,2vw,32px) 0 clamp(140px,15vw,240px)}
/* particle field behind the hero, masked so it fades out under the type */
.pfield{position:absolute;inset:0;z-index:0;pointer-events:none;display:block;
  -webkit-mask-image:linear-gradient(180deg,transparent 0,#000 16%,#000 90%,transparent 100%);
  mask-image:linear-gradient(180deg,transparent 0,#000 16%,#000 90%,transparent 100%)}
/* in the footer the terrain clears out above the legal line */
.footer .pfield{translate:0 -20%;-webkit-mask-image:linear-gradient(180deg,transparent 0,#000 20%,#000 62%,transparent 92%);
  mask-image:linear-gradient(180deg,transparent 0,#000 20%,#000 62%,transparent 92%)}
@media(prefers-reduced-motion:reduce){.pfield{opacity:.6}}
@media(prefers-reduced-motion:reduce){.hero-bg span{transform:none;transition:none}}
.hero::before{content:"";position:absolute;inset:0;pointer-events:none;opacity:.18;
  background-image:repeating-linear-gradient(90deg,var(--grid-dot) 0 1px,transparent 1px calc(100%/12))}
/* ---- THE REEL ON A TIMER --------------------------------------------
   The hero wears the active slice's cover as a backdrop. Two layers
   ping-pong so one can fade up while the other fades out; the image is
   the same file the slice itself is showing, so it is already in cache
   and costs no second download. The scrim is built from --bg, which is
   what keeps it a backdrop in both themes rather than a photograph in
   one and a fog in the other: heaviest under the type on the left,
   lightest under the reel on the right, and gone at top and bottom so
   the band has no edges. */
.hero-bg{position:absolute;inset:0;z-index:-1;overflow:hidden;pointer-events:none}
/* The grade. Photography only — the renders and UI captures are already on
   brand and would be double-treated. A blue lift on its own throws neutral
   shadows magenta, so green lifts with it: a true mid grey drifts by 2/255. */
.tone,.slice[data-k="My Story"] img,.b-portrait img,.about-portrait img{
  filter:contrast(1.08) saturate(1.04) url(#tone)}

.hero-bg span{position:absolute;inset:0;background-size:cover;background-position:center;
  opacity:0;transform:scale(1.05);
  transition:opacity 1.1s var(--ease-lux),transform 7s linear}
.hero-bg span.on{opacity:1;transform:scale(1)}
.hero-bg::after{content:"";position:absolute;inset:0;background:
  linear-gradient(90deg,var(--bg) 0 16%,color-mix(in oklab,var(--bg) 88%,transparent) 52%,
    color-mix(in oklab,var(--bg) 72%,transparent) 100%),
  linear-gradient(180deg,var(--bg) 0,transparent 26%,transparent 66%,var(--bg) 100%)}
.hero-grid{position:relative;grid-template-rows:1fr auto auto auto 1fr;align-items:start}
.hero-name{grid-column:1/6;grid-row:2;font-weight:900;font-size:clamp(56px,8.6vw,136px);line-height:.9;letter-spacing:var(--tr-tight);margin-top:-.04em;position:relative;z-index:1}
.hero-sub{grid-column:1/5;grid-row:3;font-weight:600;font-size:var(--t4);line-height:1.32;letter-spacing:var(--tr-text);margin-top:clamp(24px,3vw,48px);position:relative;z-index:1}
/* two calls now, so they sit as a row and wrap rather than butting together */
.hero-cta{grid-column:1/5;grid-row:4;align-self:start;position:relative;z-index:1;
  margin-top:clamp(32px,4vw,56px);display:flex;flex-wrap:wrap;gap:0 clamp(24px,3vw,44px)}
.hero-image{grid-column:6/9;grid-row:1/5;position:relative;z-index:2;aspect-ratio:3/7;overflow:hidden;border-radius:var(--radius);background:var(--surface-raised);margin-top:clamp(24px,3vw,48px);box-shadow:var(--shadow-md)}
.hero-image img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
.hero-image::after{content:"";position:absolute;left:-10%;top:38%;width:120%;height:9px;background:var(--accent);transform:rotate(-58deg);transform-origin:center}
.hero-ghost{grid-column:9/11;grid-row:4;align-self:end;justify-self:start;font-family:var(--font-display);font-weight:900;font-size:clamp(48px,6vw,96px);line-height:.8;letter-spacing:var(--tr-tight);color:var(--muted);user-select:none;margin-left:-.35em}
.hero-dots{grid-column:12/13;grid-row:1/4;align-self:center;justify-self:end;list-style:none;display:flex;flex-direction:column;gap:8px;padding-top:clamp(80px,12vw,200px)}
.hero-dots a{display:grid;place-items:center;width:24px;height:24px}
.hero-dots a::before{content:"";width:5px;height:5px;background:var(--fg);border-radius:50%;transition:transform var(--t-hover),background var(--t-hover)}
.hero-dots a:hover::before{background:var(--accent);transform:scale(1.6)}


/* proof band: ruled grid on the 12 columns (no cards): gutters are cell padding
   so the hairlines run continuous; first/last columns sit flush to the wrap */
.bento-sec{padding-top:clamp(8px,1vw,16px)}
.bento{--bp:clamp(16px,2vw,28px);column-gap:0;row-gap:0;grid-auto-rows:minmax(148px,auto);border-top:1px solid var(--border-soft);border-bottom:1px solid var(--border-soft)}
.bento-cell{position:relative;padding:var(--s3) var(--bp);display:flex;flex-direction:column;justify-content:space-between;gap:var(--s2);min-width:0;color:inherit;border-left:1px solid var(--border-soft)}
.bento-cell.first{border-left:0;padding-left:0}
.bento-cell.last{padding-right:0}
.bento-cell.row2,.bento-cell.row3{border-top:1px solid var(--border-soft)}
.bento-k{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;color:var(--fg-mid)}
.bento-about{font-family:var(--font-body);font-weight:600;font-size:var(--t5);line-height:1.22;letter-spacing:var(--tr-head);max-width:22ch}
.bento-h{display:block;font-family:var(--font-body);font-weight:600;font-size:var(--t4);line-height:1.25;letter-spacing:var(--tr-head)}
.bento-line{display:block;font-size:var(--t3);font-weight:500;line-height:1.5;color:var(--fg-mid);margin-top:6px}
.bento-cell .arw{width:24px;height:9px;flex:none;transition:transform 180ms ease-out,color var(--t-hover)}
a.bento-cell:hover .arw{transform:translateX(6px);color:var(--accent)}
a.bento-cell:hover .bento-h{color:var(--accent);transition:color var(--t-hover)}
.bento-foot{display:flex;align-items:center;justify-content:space-between;gap:var(--s2)}
/* the two long reads hang off the summary, so About is one concept with two
   depths rather than three peers in the nav */
.bento-links{display:flex;gap:var(--s3);font-size:var(--t2);font-weight:500}
/* Now and Building lead on the left, the statement answers on the right, with
   the portrait between them. Only the columns move: the document order stays
   About, portrait, Now, Building, so the reading order still opens with the
   summary, and the three focusable cells still run straight down the left. */
.b-now{grid-column:1/4;grid-row:1}
.b-build{grid-column:1/4;grid-row:2}
.b-portrait{grid-column:4/7;grid-row:1/3;display:block;overflow:hidden;padding:clamp(24px,3vw,44px)}
.b-about{grid-column:7/13;grid-row:1/3}
.b-portrait img{width:100%;height:100%;object-fit:cover;object-position:22% 52%;border-radius:var(--radius);display:block}
.b-ask{grid-column:1/7;grid-row:3;cursor:pointer;text-align:left;font:inherit;background:none;border-right:0;border-bottom:0}
.b-ask .fake{display:flex;align-items:center;justify-content:space-between;gap:var(--s2);
  background:var(--fld);border:1px solid var(--fld-line);border-radius:14px;
  padding:5px 5px 5px 16px;min-height:46px;font-size:var(--t3);color:var(--fg-mid);
  transition:background-color 220ms var(--ease),border-color 220ms var(--ease),box-shadow 220ms var(--ease),color 220ms var(--ease)}
.b-ask:hover .fake{background:var(--fld-hi);color:var(--fg)}
.b-ask:focus-visible{outline:none}
.b-ask:focus-visible .fake{background:var(--fld-hi);border-color:var(--fld-line-hi);
  box-shadow:0 0 0 3px color-mix(in oklab,var(--fg) 7%,transparent)}
.b-ask .fake .send{width:32px;height:32px;display:grid;place-items:center;background:var(--fg);color:var(--on-fg);border-radius:8px;flex:none}
.b-stats{grid-column:7/13;grid-row:3;flex-direction:row;align-items:stretch;padding:0 0 0 0;gap:0}
.b-stat{flex:1;padding:var(--s3) var(--bp);display:flex;flex-direction:column;justify-content:space-between;gap:var(--s2);min-width:0}
.b-stat+.b-stat{border-left:1px solid var(--border-soft)}
.b-stat:last-child{padding-right:0}
.b-stat .n{font-family:var(--font-display);font-weight:900;font-size:var(--t6);line-height:.95;letter-spacing:var(--tr-head)}


/* ── MOTION: reveals structure once, never performs ──
   one easing, three durations; hero only on load; scroll-driven layer
   is position-bound (no observers); everything collapses under
   prefers-reduced-motion. */
.hero-name .ln{display:block;overflow:hidden;padding-bottom:.06em;margin-bottom:-.06em}
.hero-name .ln>span{display:block;transform:translateY(110%);animation:rise var(--d-hero) var(--ease) forwards}
.hero-name .ln:nth-child(2)>span{animation-delay:70ms}
.hero-sub,.hero-cta,.hero-dots,.hero-ghost{opacity:0;animation:fadeUp var(--d-hero) var(--ease) forwards}
.hero-sub{animation-delay:320ms}
.hero-dots{animation-delay:520ms}
.hero-cta{animation-delay:520ms}
.hero-ghost{animation-delay:600ms}
.hero-reel{opacity:0;animation:fadeIn 600ms var(--ease) 160ms forwards}
.hero-reel .slice{opacity:0;animation:fadeUp 700ms var(--ease) forwards}
.hero-reel .slice:nth-child(1){animation-delay:200ms}.hero-reel .slice:nth-child(2){animation-delay:260ms}.hero-reel .slice:nth-child(3){animation-delay:320ms}.hero-reel .slice:nth-child(4){animation-delay:380ms}.hero-reel .slice:nth-child(5){animation-delay:440ms}.hero-reel .slice:nth-child(6){animation-delay:500ms}.hero-reel .slice:nth-child(7){animation-delay:560ms}
@keyframes rise{to{transform:translateY(0)}}
@keyframes fadeUp{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
@keyframes fadeIn{to{opacity:1}}
@keyframes draw{to{clip-path:inset(0 0 0 0)}}
/* scroll-driven layer: progressive enhancement, no JS */
@supports (animation-timeline:scroll()){
  .hero-reel{animation:fadeIn 600ms var(--ease) 160ms forwards,heroDrift linear both;animation-timeline:auto,scroll(root);animation-range:normal,0 100vh}
  .hero-ghost{animation:fadeUp var(--d-hero) var(--ease) 600ms forwards,ghostDrift linear both;animation-timeline:auto,scroll(root);animation-range:normal,0 100vh}
  .writing-shot{view-timeline:--wshot}
  .writing-shot img{animation:imgDrift linear both;animation-timeline:--wshot;animation-range:entry 0% exit 100%}
}
@keyframes heroDrift{to{transform:translateY(40px)}}
@keyframes ghostDrift{to{transform:translateY(-48px)}}
@keyframes imgDrift{from{transform:translateY(-4%) scale(1.06)}to{transform:translateY(4%) scale(1.06)}}


/* ── UNDERLINE-DRAW: the one hover motion, applied everywhere ──
   the 1px rule is a pseudo that scales from the left in 160ms; lines
   that are always on redraw themselves; arrows slide 6px. */
.cta,.cta-sm,.proj-link,.clink,.nav-links a,.case-back,.case-cta,.card-btn,.case-next a{position:relative;border-bottom:0;transition:color var(--t-hover)}
.cta::after,.cta-sm::after,.proj-link::after,.clink::after,.nav-links a::after,.case-back::after,.case-cta::after,.card-btn::after,.case-next a::after{content:"";position:absolute;left:0;right:0;bottom:0;height:1px;background:currentColor;opacity:.38;transform-origin:left center;transform:scaleX(1);transition:transform 160ms var(--ease),opacity 160ms var(--ease)}
.cta:hover::after,.cta-sm:hover::after,.proj-link:hover::after,.clink:hover::after,.nav-links a:hover::after,.nav-links a[aria-current="page"]::after,.case-back:hover::after,.case-cta:hover::after,.card-btn:hover::after,.case-next a:hover::after,.work-cell:hover .proj-link::after{opacity:1}
/* links inside case sections size to their text instead of the column */
.case-section>.case-cta,.case-section>.card-btn,.case-section>.cta-sm,.case-section>.case-back{justify-self:start;width:fit-content}
.case-next a{width:fit-content}
.nav-links a::after{left:10px;right:10px}
.nav-links a::after,.clink::after,.proj-link::after,.case-next a:not(.next)::after{transform:scaleX(0)}
.nav-links a:hover::after,.clink:hover::after,.work-cell:hover .proj-link::after,.case-next a:not(.next):hover::after{transform:scaleX(1)}
.cta:hover::after,.cta-sm:hover::after,.case-back:hover::after,.case-cta:hover::after,.card-btn:hover::after,.case-next a.next:hover::after{animation:redraw 480ms var(--ease)}
@keyframes redraw{0%{transform:scaleX(1);transform-origin:right center}45%{transform:scaleX(0);transform-origin:right center}50%{transform:scaleX(0);transform-origin:left center}100%{transform:scaleX(1);transform-origin:left center}}
.cta:hover{gap:20px}.cta-sm:hover{gap:14px}.clink:hover{gap:14px}.work-cell:hover .proj-link{gap:12px}
.arw{transition:transform 160ms var(--ease)}
.cta:hover .arw,.cta-sm:hover .arw,.clink:hover .arw,.work-cell:hover .proj-link .arw,.case-cta:hover .arw,.w-row:hover .arw,.case-next a:hover .arw{transform:translateX(6px)}
.b-stat .n,.signal .sn,.metric__n{font-variant-numeric:tabular-nums}


/* ── SCROLL THEME, data-theme="auto": light while scrolling up, dark while
   scrolling down (direction, not position; JS toggles .scrolled). Surfaces
   cross in 240ms and the ink flips at the midpoint so there is no grey
   in-between state. ── */
html[data-theme="auto"]{transition:--bg 220ms linear,--surface 220ms linear,--surface-raised 220ms linear,--muted 220ms linear,--border 220ms linear,--border-soft 220ms linear,--grid-dot 220ms linear,--accent 220ms linear,
  --fg 0s linear 110ms,--fg-mid 0s linear 110ms,--on-fg 0s linear 110ms}
html[data-theme="auto"] body{transition:none}


/* ── LOGO TILES · MOSAIC · LIGHTBOX ── */
@keyframes logoOut{0%,49.5%{opacity:1}50.5%,100%{opacity:0}}
@keyframes logoIn{0%,49.5%{opacity:0}50.5%,100%{opacity:1}}
/* case-page logo pair (light / dark artwork) */
.logo-pair{position:relative;height:44px;margin:clamp(32px,4vw,56px) 0 0;max-width:260px}
/* brand pages: the logo IS the title, so the h1 stays in the DOM for crawlers,
   screen readers and answer engines, but is clipped from view */
.logo-pair.title{height:clamp(56px,7vw,96px);max-width:min(560px,100%);margin-top:clamp(40px,5vw,64px)}
.logo-pair.title.square{height:clamp(96px,12vw,160px)}
/* the HangarX wordmark is the widest of the marks; a fifth down sits it
   with the rest without touching LunarCrush or Northlit */
.logo-pair.title.sm{height:clamp(45px,5.6vw,77px)}
/* Every lockup is sized by height, so its drawn width falls out of its aspect
   ratio: at 96px the 4.2:1 marks run about 410px and the Digital Pool one,
   led by a tall circle, only 226px, which reads as half the logo. A mark-led
   lockup is given the height that puts it at the same drawn width. */
.logo-pair.title.mark-led{height:clamp(112px,12vw,173px)}
.vh{position:absolute!important;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.logo-pair.title+.vh+.case-sub{margin-top:clamp(28px,3.2vw,40px)}
.logo-pair img{position:absolute;left:0;top:0;height:100%;width:auto;max-width:100%;object-fit:contain;transition:opacity 200ms var(--ease)}
.logo-pair .on-dark{opacity:0}
html[data-theme="dark"] .logo-pair .on-light,html[data-theme="auto"].scrolled .logo-pair .on-light{opacity:0}
html[data-theme="dark"] .logo-pair .on-dark,html[data-theme="auto"].scrolled .logo-pair .on-dark{opacity:1}
/* mosaic: tight 12-column grid, rows sized from the container width so
   every cell keeps a 16:9-derived proportion; dense packing fills holes */
.mosaic-wrap{container-type:inline-size;margin-top:var(--s3)}
.mosaic{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));grid-auto-rows:calc((100cqw - 132px)/12*1.125);gap:12px;list-style:none}
.mosaic li{margin:0}
.mosaic a{display:block;position:relative;height:100%;overflow:hidden;border-radius:calc(var(--radius) - 2px);background:var(--surface-raised)}
.mosaic img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;transition:transform 480ms var(--ease)}
.mosaic a:hover img{transform:scale(1.03)}
.mosaic a::after{content:"";position:absolute;inset:0;border-radius:inherit;box-shadow:inset 0 0 0 1px rgba(128,128,136,.22);pointer-events:none}
.mosaic .m-hero{grid-column:1/-1;grid-row:span 6;display:flex;flex-direction:column;gap:10px}
.mosaic .m-hero a{flex:1;min-height:0}
.m-cap{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;color:var(--fg-mid)}
.mosaic .m-wide{grid-column:span 6;grid-row:span 3}
.mosaic li:not(.m-hero):not(.m-wide){grid-column:span 4;grid-row:span 2}
/* lightbox */
.lb{position:fixed;inset:0;z-index:200;background:rgba(8,9,12,.94);display:flex;align-items:center;justify-content:center;opacity:0;visibility:hidden;transition:opacity 200ms var(--ease),visibility 200ms}
.lb.open{opacity:1;visibility:visible}
.lb figure{margin:0;max-width:min(94vw,1600px);max-height:100vh;display:flex;flex-direction:column;align-items:center;gap:12px;padding:56px 64px;touch-action:pan-y;user-select:none}
.lb img{max-width:100%;max-height:calc(100vh - 140px);width:auto;height:auto;object-fit:contain;border-radius:var(--radius);box-shadow:0 20px 60px rgba(0,0,0,.5);transition:opacity 160ms var(--ease),transform 160ms var(--ease)}
.lb.swap img{opacity:0;transform:translateX(var(--dir,0))}
.lb figcaption{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;color:rgba(255,255,255,.7);display:flex;gap:16px;align-items:baseline}
.lb figcaption .n{color:rgba(255,255,255,.45)}
.lb button{position:absolute;width:48px;height:48px;display:grid;place-items:center;border:1px solid rgba(255,255,255,.28);border-radius:var(--r-btn);background:rgba(0,0,0,.35);color:#fff;cursor:pointer;transition:border-color var(--t-hover),background-color var(--t-hover)}
.lb button:hover{border-color:#fff;background:rgba(0,0,0,.6)}
.lb .lb-x{top:16px;right:16px}
.lb .lb-prev{left:16px;top:50%;transform:translateY(-50%)}
.lb .lb-next{right:16px;top:50%;transform:translateY(-50%)}
body.lb-lock{overflow:hidden}


/* project thumbnail row (HangarX): six tiles on the imagery width */
.proj-row{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:var(--s2);list-style:none;margin-top:var(--s2)}
.proj-row li{min-width:0}
.proj-row .pr-media{display:block;position:relative;aspect-ratio:16/10;overflow:hidden;border-radius:var(--radius);background:var(--surface-raised)}
.proj-row .pr-media img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;transition:transform 480ms var(--ease)}
.proj-row .pr-media::after{content:"";position:absolute;inset:0;border-radius:inherit;box-shadow:inset 0 0 0 1px rgba(128,128,136,.22);pointer-events:none}
.proj-row li:hover .pr-media img{transform:scale(1.03)}
.proj-row h3{font-family:var(--font-body);font-weight:600;font-size:var(--t2);letter-spacing:var(--tr-text);margin-top:14px}
.proj-row p{font-size:var(--t1)!important;line-height:1.5!important;color:var(--fg-mid);margin-top:4px;max-width:none}
.proj-row .pr-links{display:flex;flex-wrap:wrap;gap:4px 14px;margin-top:10px}
.proj-row .pr-links a{white-space:nowrap;font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;padding:6px 0 4px;border-bottom:1px solid transparent;transition:color var(--t-hover),border-color var(--t-hover)}
.proj-row .pr-links a:hover{color:var(--accent);border-bottom-color:var(--accent)}


/* ── GLASS BUTTONS: every button is a frosted, fully rounded control ──
   translucent surface + backdrop blur + hairline edge + inner highlight */
.glass{border-radius:999px;background:color-mix(in oklab,var(--surface) 62%,transparent);border:1px solid color-mix(in oklab,var(--fg) 14%,transparent);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.35),0 1px 2px rgba(10,15,28,.06),0 6px 20px rgba(10,15,28,.06);
  -webkit-backdrop-filter:blur(16px) saturate(1.4);backdrop-filter:blur(16px) saturate(1.4);
  transition:background-color var(--t-hover),border-color var(--t-hover),box-shadow var(--t-hover),transform var(--t-hover),color var(--t-hover)}
.glass:hover{background:color-mix(in oklab,var(--surface) 82%,transparent);border-color:color-mix(in oklab,var(--fg) 28%,transparent);transform:translateY(-1px)}
.glass:active{transform:translateY(0)}
html[data-theme="dark"] .glass,html[data-theme="auto"].scrolled .glass{box-shadow:inset 0 1px 0 rgba(255,255,255,.10),0 1px 2px rgba(0,0,0,.3),0 6px 20px rgba(0,0,0,.35)}
.ibtn.glass:hover{background:color-mix(in oklab,var(--surface) 82%,transparent)}
#toTop.glass{border-radius:999px;border-color:color-mix(in oklab,var(--fg) 14%,transparent);background:color-mix(in oklab,var(--surface) 62%,transparent)}
#toTop.glass:hover{color:var(--accent);border-color:color-mix(in oklab,var(--fg) 28%,transparent)}
.carousel__nav.glass{border-radius:999px;border-color:color-mix(in oklab,var(--fg) 14%,transparent);background:color-mix(in oklab,var(--surface) 62%,transparent)}
.lb button.glass{border-radius:999px;border-color:rgba(255,255,255,.22);background:rgba(20,20,26,.45);color:#fff;box-shadow:inset 0 1px 0 rgba(255,255,255,.12)}
.lb button.glass:hover{background:rgba(20,20,26,.7);border-color:rgba(255,255,255,.5)}
.b-ask .fake .send.glass{border-radius:999px;background:color-mix(in oklab,var(--surface) 62%,transparent);color:var(--fg)}
.chat-send.glass,.chat-x.glass{border-radius:999px}
/* mobile: no double-tap zoom; page never accidentally resizes */
html,body{touch-action:manipulation}


/* ── HERO REEL: vertical slices that breathe on hover (flex-grow accordion),
   the lead frame opens larger at rest; click morphs the frame to full screen ── */
:root{--ease-lux:cubic-bezier(.22,1,.36,1)}
.hero-reel{grid-column:6/13;grid-row:1/6;position:relative;z-index:2;display:flex;gap:6px;height:clamp(440px,66vh,720px);margin-top:clamp(8px,1vw,16px);min-width:0}
.slice{position:relative;flex:1 1 0%;min-width:0;overflow:hidden;border-radius:2px;background:var(--surface-raised);cursor:pointer;padding:0;border:0;text-align:left;color:#fff;
  transition:flex-grow .72s var(--ease-lux),border-radius .72s var(--ease-lux),transform .55s var(--ease-lux);will-change:flex-grow}
/* A spine reads better nearly square, and the radius opens with the card.
   It follows whichever slice is actually open, not the timer's — otherwise a
   hovered reel shows two cards carrying the open radius. */
.hero-reel .slice:hover,.hero-reel .slice:focus-visible{border-radius:var(--radius)}
.hero-reel:not(:hover):not(:has(.slice:focus-visible)) .slice.is-on{border-radius:var(--radius)}
.slice:first-child{flex-grow:3}
/* The timer's slice opens like a hovered one. :has() gates it so that
   with no slice active — reduced motion, or before the first tick —
   the reel keeps its resting widths instead of collapsing to .62. The
   hover rules below are the more specific pair, so a pointer always
   takes the reel back. */
.hero-reel .slice.is-on{flex-grow:6}
.hero-reel:has(.slice.is-on):not(:hover) .slice:not(.is-on){flex-grow:.62}
.hero-reel:hover .slice:not(:hover){flex-grow:.62}
.hero-reel:hover .slice:hover{flex-grow:6}
/* Keyboard focus expands a slice the same way a hover does, but the test has to
   be that a slice is actually :focus-visible, not that the reel holds focus at
   all. Closing the sheet with the mouse hands focus back to the slice without
   :focus-visible, and a bare :focus-within then shrank every slice to .62 and,
   sitting later in the file at equal specificity, outranked :hover, so no slice
   could grow again and its caption bunched up inside the narrow column.
   Kept as their own rules, not merged into the list above: a browser without
   :has() must drop these and keep hover working.
   The :not(:hover) hands the reel back to the pointer the moment it returns. */
.hero-reel:has(.slice:focus-visible):not(:hover) .slice:not(:focus-visible){flex-grow:.62}
.hero-reel:has(.slice:focus-visible):not(:hover) .slice:focus-visible{flex-grow:6}
.slice:focus-visible{outline:2px solid var(--accent);outline-offset:-4px;border-radius:var(--radius)}
.slice img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;transform:scale(1.04);transition:transform 1.15s var(--ease-lux)}
/* theme-aware slice: summer by day, aurora in dark mode */
.slice img.on-light,.slice img.on-dark{transition:opacity 220ms linear,transform 1.15s var(--ease-lux)}
.slice img.on-dark{opacity:0}
html[data-theme="dark"] .slice img.on-light,html[data-theme="auto"].scrolled .slice img.on-light{opacity:0}
html[data-theme="dark"] .slice img.on-dark,html[data-theme="auto"].scrolled .slice img.on-dark{opacity:1}
.slice:hover img{transform:scale(1.1)}
.slice::before{content:"";position:absolute;inset:0;z-index:1;pointer-events:none;background:linear-gradient(to top,rgba(0,0,0,.72) 0%,rgba(0,0,0,.34) 30%,rgba(0,0,0,0) 62%);opacity:.9;transition:opacity .55s ease}
.slice::after{content:"";position:absolute;inset:0;border-radius:inherit;box-shadow:inset 0 0 0 1px rgba(255,255,255,.12);pointer-events:none;z-index:3}
.slice .spine{position:absolute;left:0;right:0;bottom:18px;z-index:2;display:flex;justify-content:center;pointer-events:none;text-shadow:0 1px 8px rgba(0,0,0,.55);transition:opacity .35s ease}
.slice .spine .n{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-caps)}
/* the project title rides the left edge, reading bottom to top. On hover
   it swings flat and grows out of the same corner as the slice widens. */
/* The title reads bottom to top, centred in the strip, sitting just above
   the index. Rotating about the top-left corner makes the rotated text grow
   upward from a known point, so left/top can be animated alongside the
   rotation: on hover it swings flat, moves to the corner and grows, and the
   description follows it in. */
.slice .t{position:absolute;left:calc(50% - 7px);top:calc(100% - 54px);z-index:2;
  transform-origin:0 0;transform:rotate(-90deg);
  font-family:var(--font-body);font-weight:600;font-size:var(--t2);line-height:1;letter-spacing:var(--tr-text);white-space:nowrap;
  text-shadow:0 2px 14px rgba(0,0,0,.65);
  transition:left .66s var(--ease-lux),top .66s var(--ease-lux),transform .66s var(--ease-lux),font-size .66s var(--ease-lux),letter-spacing .66s var(--ease-lux)}
.slice:hover .t,.slice:focus-visible .t,.hero-reel:not(:hover) .slice.is-on .t{left:20px;top:calc(100% - 158px);transform:rotate(0deg);
  font-size:var(--t5);letter-spacing:var(--tr-head)}
.slice .d{position:absolute;left:20px;right:20px;bottom:52px;z-index:2;
  font-size:var(--t2);line-height:1.45;color:rgba(255,255,255,.82);text-shadow:0 1px 10px rgba(0,0,0,.7);
  opacity:0;transform:translateY(10px);pointer-events:none;
  transition:opacity .3s ease,transform .5s var(--ease-lux)}
.slice:hover .d,.slice:focus-visible .d,.hero-reel:not(:hover) .slice.is-on .d{opacity:1;transform:none;transition:opacity .45s ease .22s,transform .6s var(--ease-lux) .22s}
.slice .sgo{position:absolute;left:20px;bottom:22px;z-index:2;width:26px;height:9px;color:#fff;opacity:0;transform:translateY(10px);pointer-events:none;
  filter:drop-shadow(0 1px 10px rgba(0,0,0,.7));transition:opacity .3s ease,transform .5s var(--ease-lux)}
.slice:hover .sgo,.slice:focus-visible .sgo,.hero-reel:not(:hover) .slice.is-on .sgo{opacity:1;transform:none;transition:opacity .4s ease .34s,transform .55s var(--ease-lux) .34s}
.slice:hover .spine,.slice:focus-visible .spine,.hero-reel:not(:hover) .slice.is-on .spine{opacity:0}
.hero-reel.opening .slice .t,.hero-reel.opening .slice .spine,.hero-reel.opening .slice .d,.hero-reel.opening .slice .sgo{opacity:0!important;transition:opacity .18s ease}
/* TOUCH. There is no hover here, so the timer is the only thing that opens a
   slice: the active one widens and the rest fall back to spines. Everything
   carries !important because the resting rules it has to beat do, and the
   collapse excludes .is-on so the two rules cannot fight over it. */
@media(hover:none){
  .hero-reel .slice{flex-grow:1!important}
  .hero-reel .slice:first-child{flex-grow:2.2!important}
  .hero-reel:has(.slice.is-on) .slice:not(.is-on){flex-grow:.5!important}
  .hero-reel .slice.is-on{flex-grow:6!important}
  /* no numerals on a 17px spine, and the name a step down to match */
  .slice .spine{display:none!important}
  .slice .t{transform:rotate(-90deg)!important;font-size:var(--t1)!important;left:calc(50% - 5.5px)!important;top:calc(100% - 24px)!important}
  .slice .d,.slice .sgo{display:none}
  /* the open one reads as a card rather than a spine */
  .hero-reel .slice.is-on .t{transform:rotate(0deg)!important;font-size:var(--t4)!important;
    left:16px!important;right:16px!important;top:auto!important;bottom:78px!important;white-space:normal!important}
  /* two lines, so the blurb cannot grow up into the title above it */
  .hero-reel .slice.is-on .d{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;
    overflow:hidden;opacity:1;transform:none;left:16px;right:16px;bottom:22px;font-size:var(--t1);line-height:1.4}
  .hero-reel .slice.is-on .spine{opacity:0!important}
}
/* full-screen detail (FLIP morph from the slice) */
.detail{position:fixed;inset:0;z-index:300;visibility:hidden;pointer-events:none;overflow-y:auto;overflow-x:hidden;overscroll-behavior:contain;-webkit-overflow-scrolling:touch}
.detail.on{visibility:visible;pointer-events:auto}
/* The sheet stays on screen while the figure flies back to its slice, so the
   close is the open run backwards rather than a cut. Two pieces fade in on a
   long delay when the sheet opens and would still be sitting there when the
   figure lands, so on the way out they are dropped at once instead. */
.detail.closing{visibility:visible;pointer-events:none}
.detail.closing .detail-more,.detail.closing .dm-hint{opacity:0;transition:opacity .2s ease}
/* transform joins the flight only here: while the sheet is open it carries the
   scroll parallax, which must not be eased a frame behind the scroll */
.detail.closing .detail-fig{transition:top .66s var(--ease-lux),left .66s var(--ease-lux),
  width .66s var(--ease-lux),height .66s var(--ease-lux),border-radius .66s var(--ease-lux),
  transform .66s var(--ease-lux)}
/* below the pinned hero, a preview of the case page slides up: kicker,
   one line, a six-image mosaic and the link */
.detail-scroll{position:relative;z-index:4}
.detail-space{height:100vh;height:100dvh;pointer-events:none}
.detail-more{position:relative;background:var(--bg);color:var(--fg);padding:clamp(48px,7vw,96px) 0 clamp(64px,8vw,120px);border-top:1px solid var(--border-soft);opacity:0;transform:translateY(24px);transition:opacity .5s var(--ease) .5s,transform .7s var(--ease-lux) .5s}
.detail.on .detail-more{opacity:1;transform:none}
.dm-k{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;color:var(--fg-mid);display:block}
/* the head is the same twelve columns: name on the left at display scale, the
   line about it set small in the right-hand columns at the name's foot. The
   name sits clear of the mosaic — at .84 the line box is tighter than the ink,
   so the gap below has to clear the descenders, not just the box. */
.detail-more .wrap{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));
  column-gap:clamp(14px,1.8vw,30px)}
.dm-k{grid-column:1/13}
.dm-t{grid-column:1/13;font-family:var(--font-display);font-weight:900;
  font-size:clamp(52px,9.5vw,152px);line-height:.84;letter-spacing:var(--tr-tight);
  margin:10px 0 0;position:relative}
/* under the name, on the reading column: the summary as a bold lede, then the
   facts table on the first six columns, then the button */
.dm-side{grid-column:1/9;display:grid;grid-template-columns:subgrid;column-gap:inherit;row-gap:0;margin-top:clamp(28px,3.4vw,48px)}
.dm-h{grid-column:1/-1;font-weight:600;font-size:var(--t5);line-height:1.3;letter-spacing:var(--tr-head);margin:0 0 clamp(28px,3.4vw,44px);color:var(--fg);text-wrap:balance}
/* the facts are a ruled table on the page's own columns: the number on the
   first of the side's four columns, the label on the other three */
.dm-facts{grid-column:1/7;display:grid;grid-template-columns:subgrid;column-gap:inherit;row-gap:0;margin:0;border-top:1px solid var(--border-soft)}
.dm-facts[hidden]{display:none}
.dm-facts div{grid-column:1/-1;display:grid;grid-template-columns:subgrid;column-gap:inherit;align-items:baseline;padding:14px 0;border-bottom:1px solid var(--border-soft)}
.dm-facts dt{grid-column:1/3;font-family:var(--font-display);font-weight:900;font-size:var(--t3);letter-spacing:var(--tr-tight);line-height:1;color:var(--fg);white-space:nowrap}
.dm-facts dd{grid-column:3/-1;margin:0;font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);color:var(--fg-mid);line-height:1.4}
.dm-go{grid-column:1/4;display:flex;align-items:center;justify-content:space-between;gap:12px;min-width:max-content;white-space:nowrap;min-height:48px;padding:0 18px;margin-top:clamp(28px,3.4vw,40px);font-size:var(--t2);font-weight:600;color:var(--fg)}
.dm-go.glass{border-radius:var(--r-btn)}
.dm-go .arw{width:26px;height:9px;transition:transform var(--t-hover)}
.dm-go:hover .arw{transform:translateX(6px)}
@supports not (grid-template-columns:subgrid){
  .dm-side{grid-template-columns:repeat(8,minmax(0,1fr))}
  .dm-facts{grid-template-columns:repeat(6,minmax(0,1fr))}
  .dm-facts div{grid-template-columns:1fr 5fr}
}
@media(max-width:900px){
  .dm-t{grid-column:1/13}
  .dm-side{grid-column:1/13;grid-template-columns:1fr;margin:var(--s3) 0 0}
  .dm-h{font-size:var(--t2);font-weight:500;line-height:1.55;letter-spacing:var(--tr-text);color:var(--fg-mid);margin-bottom:var(--s3)}
  .dm-facts{grid-column:1;grid-template-columns:1fr}
  .dm-go{grid-column:1;width:100%}
  .dm-facts div{grid-template-columns:max-content 1fr;column-gap:20px}
  .dm-facts dd{grid-column:2}
}

/* on a phone the sheet stacks on the site's spacing steps: the summary a step
   below the name, then each fact on its own ruled row, number above label,
   then the button */
@media(max-width:640px){
  .dm-side{gap:0;margin-top:var(--s3)}
  .dm-h{margin-bottom:var(--s3)}
  .dm-facts{display:block;padding-top:0;border-top:0}
  .dm-facts div{display:block;padding:var(--s2) 0;border-top:1px solid var(--border-soft)}
  .dm-facts div:last-child{border-bottom:1px solid var(--border-soft)}
  .dm-facts dt{font-size:var(--t4);margin-bottom:6px;white-space:normal}
  .dm-facts dd{font-size:12px;letter-spacing:.08em}
  .dm-go{width:100%;justify-content:center;margin-top:var(--s3)}
}
.dm-hint{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);z-index:5;color:#fff;font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-caps);text-transform:uppercase;opacity:0;transition:opacity .5s ease 1.2s;pointer-events:none;padding-bottom:18px}
.dm-hint::after{content:"";position:absolute;left:50%;bottom:0;width:1px;height:12px;background:#fff;animation:hintBob 1.8s ease-in-out infinite}
.detail.on .dm-hint{opacity:.7}
.detail.on.scrolled .dm-hint{opacity:0;transition-delay:0s}
@keyframes hintBob{0%,100%{transform:translateY(0)}50%{transform:translateY(5px)}}
.detail-bg{position:absolute;inset:0;background:#0b0b0e;opacity:0;transition:opacity .6s var(--ease)}
.detail.on .detail-bg{opacity:.96}
.detail-fig{position:fixed;overflow:hidden;border-radius:var(--radius);margin:0;z-index:2;background:#0b0b0e;will-change:transform}
.detail-fig.animating{transition:top .66s var(--ease-lux),left .66s var(--ease-lux),width .66s var(--ease-lux),height .66s var(--ease-lux),border-radius .66s var(--ease-lux)}
.detail-fig img{width:100%;height:100%;object-fit:cover;display:block;transition:opacity .34s var(--ease),transform .6s var(--ease-lux)}
.detail.swapping .detail-fig img{opacity:0;transform:scale(1.02)}
.detail-grad{position:fixed;inset:0;z-index:3;pointer-events:none;background:linear-gradient(to top,rgba(0,0,0,.74) 0%,rgba(0,0,0,.3) 32%,rgba(0,0,0,0) 60%);opacity:0;transition:opacity .6s var(--ease)}
.detail.on .detail-grad{opacity:1}
.detail-content{pointer-events:none;--t0:.42s;position:fixed;left:clamp(20px,6vw,96px);bottom:clamp(28px,6vh,72px);z-index:5;color:#fff;max-width:min(640px,84vw)}
.detail-content .k,.detail-content .h,.detail-content .d{display:block}
/* kicker: a rule draws in, then the label uncovers left→right */
.detail-content .k{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;opacity:.85;display:flex;align-items:center;gap:10px}
.detail-content .k::before{content:"";width:16px;height:1px;background:#fff;flex:none;transform:scaleX(0);transform-origin:left center;transition:transform .3s var(--ease)}
.detail-content .k span{display:inline-block;clip-path:inset(0 100% 0 0);transition:clip-path .3s var(--ease)}
.detail.on .k::before{transform:none;transition:transform .5s var(--ease) var(--t0)}
.detail.on .k span{clip-path:inset(0 -2% 0 0);transition:clip-path .55s var(--ease) calc(var(--t0) + .12s)}
/* headline: each word rises out of its own mask, staggered */
.detail-content .h{font-family:var(--font-display);font-weight:900;font-size:clamp(34px,6vw,84px);line-height:.94;letter-spacing:var(--tr-tight);margin-top:10px}
.detail-content .h .w{display:inline-block;overflow:hidden;vertical-align:top;padding:0 .06em .08em 0;margin:0 0 -.08em 0}
.detail-content .h .w>span{display:inline-block;transform:translateY(112%);transition:transform .32s var(--ease) calc(var(--i)*12ms)}
.detail.on .h .w>span{transform:none;transition:transform .9s var(--ease) calc(var(--t0) + .16s + var(--i)*45ms)}
/* description and link settle in after the headline */
.detail-content .d{font-size:var(--t3);line-height:1.55;opacity:0;margin-top:14px;max-width:48ch;transform:translateY(8px);transition:opacity .25s ease,transform .25s ease}
.detail.on .d{opacity:.88;transform:none;transition:opacity .6s var(--ease) calc(var(--t0) + .5s),transform .7s var(--ease) calc(var(--t0) + .5s)}
.detail-content .cta{pointer-events:auto;display:inline-flex;color:#fff;margin-top:20px;opacity:0;transform:translateY(8px);transition:opacity .25s ease,transform .25s ease}
.detail-content .cta::after{background:#fff;transform:scaleX(0);transition:transform .25s var(--ease)}
.detail.on .cta{opacity:1;transform:none;transition:opacity .5s var(--ease) calc(var(--t0) + .62s),transform .6s var(--ease) calc(var(--t0) + .62s)}
.detail.on .cta::after{transform:scaleX(1);transition:transform .6s var(--ease) calc(var(--t0) + .72s)}
.detail.on .cta:hover::after{animation:redraw 480ms var(--ease)}
/* a swap plays the exit (fast, base rules) then the entry from a short --t0 */
.detail.on.swapping .k::before{transform:scaleX(0);transition:transform .25s var(--ease)}
.detail.on.swapping .k span{clip-path:inset(0 100% 0 0);transition:clip-path .25s var(--ease)}
.detail.on.swapping .h .w>span{transform:translateY(112%);transition:transform .32s var(--ease) calc(var(--i)*12ms)}
.detail.on.swapping .d,.detail.on.swapping .cta{opacity:0;transform:translateY(8px);transition:opacity .22s ease,transform .22s ease}
.detail.on.swapping .cta::after{transform:scaleX(0);transition:transform .22s var(--ease)}
.detail.swapping .detail-fig img{opacity:0;transform:scale(1.03) translateX(var(--dx,0))}
.detail-counter{position:fixed;left:clamp(20px,6vw,96px);top:clamp(20px,3vw,36px);z-index:5;color:#fff;font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-caps);opacity:0;transition:opacity .5s ease .15s}
.detail.on .detail-counter{opacity:1}
.detail-x,.detail-nav{position:fixed;z-index:6;width:48px;height:48px;display:grid;place-items:center;color:#fff;cursor:pointer;opacity:0;transition:opacity .4s ease .1s,background-color var(--t-hover),border-color var(--t-hover),transform var(--t-hover)}
.detail.on .detail-x,.detail.on .detail-nav{opacity:1}
.detail.on.scrolled .detail-nav,.detail.on.scrolled .detail-counter{opacity:0;pointer-events:none;transition-delay:0s}
.detail.on.scrolled .detail-content,.detail.on.scrolled .detail-content .cta{pointer-events:none}
.detail-x{top:clamp(16px,2.4vw,30px);right:clamp(16px,2.4vw,30px)}
/* the dialog takes focus on open, which a pointer should not be shown a ring
   for; the class comes off on blur, so the next Tab gets it back */
.detail-x.quiet:focus,.detail-x.quiet:focus-visible{outline:none}
.detail-nav{top:50%;transform:translateY(-50%)}
.detail-nav.prev{left:clamp(12px,2vw,28px)}.detail-nav.next{right:clamp(12px,2vw,28px)}
.detail-nav:hover{transform:translateY(-50%)}
.detail .glass{border-color:rgba(255,255,255,.22);background:rgba(20,20,26,.45);box-shadow:inset 0 1px 0 rgba(255,255,255,.12)}
.detail .glass:hover{background:rgba(20,20,26,.7);border-color:rgba(255,255,255,.5)}
/* The sheet's CTA sits on the page, not over the photo, so the smoked glass
   above read as a grey slab in light mode. Light takes a solid ink button;
   on hover the label turns accent like every other CTA (4.9:1 on ink, where
   white on an accent fill would be 3.9:1). Dark keeps the glass. */
.detail .dm-go.glass{background:var(--fg);color:var(--on-fg);border-color:var(--fg);box-shadow:none;-webkit-backdrop-filter:none;backdrop-filter:none}
.detail .dm-go.glass:hover{background:var(--fg);border-color:var(--fg);color:var(--accent)}
html[data-theme="dark"] .detail .dm-go.glass,html[data-theme="auto"].scrolled .detail .dm-go.glass{background:rgba(20,20,26,.45);color:var(--fg);border-color:rgba(255,255,255,.22);box-shadow:inset 0 1px 0 rgba(255,255,255,.12);-webkit-backdrop-filter:blur(16px) saturate(1.4);backdrop-filter:blur(16px) saturate(1.4)}
html[data-theme="dark"] .detail .dm-go.glass:hover,html[data-theme="auto"].scrolled .detail .dm-go.glass:hover{background:rgba(20,20,26,.7);border-color:rgba(255,255,255,.5);color:var(--fg)}
body.detail-lock{overflow:hidden}
@media(max-width:640px){.detail-content{bottom:calc(46px + env(safe-area-inset-bottom));max-width:calc(100vw - 40px)}}
@media(prefers-reduced-motion:reduce){.slice,.slice img,.detail-fig.animating,.detail-fig img,.detail-bg,.detail-content,.detail-content *,.detail-content .k::before,.detail-content .cta::after,.detail-grad{transition:none!important}.hero-reel .slice{animation:none!important;opacity:1!important}}


/* ── ARCHIVE: ruled grid of project tiles; each tile is a one-image mosaic
   whose hidden siblings feed the lightbox ── */
/* forty-four tiles at one size read as a template, so the grid runs a
   repeating 2 / 3 / 4 rhythm across the twelve columns, and the type scales
   with the tile it sits under */
.arch-grid{--wgap:clamp(24px,2.8vw,44px);display:grid;grid-template-columns:repeat(12,minmax(0,1fr));
  column-gap:var(--wgap);row-gap:clamp(44px,4.6vw,76px);margin-top:clamp(40px,5vw,64px)}
.arch-cell{position:relative;min-width:0;grid-column:span 3;--tt:15px;cursor:pointer}
.arch-cell:nth-child(9n+1),.arch-cell:nth-child(9n+2){grid-column:span 6;--tt:23px}
.arch-cell:nth-child(9n+3),.arch-cell:nth-child(9n+4),.arch-cell:nth-child(9n+5){grid-column:span 4;--tt:18px}
/* pin gallery: masonry columns, every image at its own aspect ratio,
   ordered so similar tones sit together */
.mosaic.pin{display:block;columns:4;column-gap:12px;grid:none}
.case-section>.mosaic-wrap:has(.mosaic.pin),.case-section>.mosaic-wrap:has(.mosaic.full),.case-section.brand-sys>.mosaic-wrap{grid-column:1/-1;margin-top:var(--s2);width:100vw;margin-left:calc(50% - 50vw);padding:0 12px;box-sizing:border-box}
@media(max-width:1280px){.mosaic.pin{columns:3}}
.mosaic.pin li{break-inside:avoid;margin:0 0 12px;display:block}
.mosaic.pin a{height:auto}
.mosaic.pin img{position:static;width:100%;height:auto;display:block;object-fit:unset;aspect-ratio:auto}
@media(max-width:1024px){.mosaic.pin{columns:2}}
@media(max-width:640px){.mosaic.pin{columns:1;column-gap:0}.mosaic.pin li{margin-bottom:10px}}
.case-article .case-figure img,.case-article .carousel__slide{cursor:zoom-in}
/* the frames stack in place: the first one shows, the rest stay display:none
   until a hover asks for them, so their files are never fetched on a tap-only
   device and the lightbox set is unchanged */
.mosaic.arch{display:block;margin:0;position:relative;aspect-ratio:16/10}
.mosaic.arch li{display:none;position:absolute;inset:0;opacity:0;transition:opacity .5s var(--ease)}
.mosaic.arch li:first-child{display:block;opacity:1}
.mosaic.arch li.lit{display:block}
.mosaic.arch li.on{opacity:1;z-index:1}
.mosaic.arch.cy li:first-child{opacity:0}
.mosaic.arch.cy li:first-child.on{opacity:1}
.mosaic.arch a{border-radius:var(--radius)}
/* a page screenshot is tall; the tile shows its top, where the site is recognisable, not its middle */
.mosaic.arch img{object-position:center top}
/* these are framed in the middle of their images (a logo mark, a centred device), so crop them evenly */
#bull .mosaic.arch img,#game-boy-micro .mosaic.arch img{object-position:center}
.case-intro{border-bottom:0}
.case-intro p{font-weight:400}
/* a process as two lanes: the agent's step over the person's call */
.flow{list-style:none;margin:clamp(20px,2.4vw,32px) 0 0;padding:0;display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:10px}
.flow-step{display:grid;grid-template-rows:auto 1fr auto;gap:8px}
.flow.f4{grid-template-columns:repeat(4,minmax(0,1fr))}
@supports (grid-template-rows:subgrid){.flow{grid-template-rows:auto 1fr auto;row-gap:8px}.flow-step{grid-row:span 3;grid-template-rows:subgrid}}
.flow-n{font-family:var(--font-mono);font-size:var(--t1);color:var(--fg-mid)}
.flow-ai,.flow-you{border:1px solid var(--border-soft);border-radius:var(--radius);padding:12px;font-size:var(--t2);line-height:1.4}
.flow-ai{background:color-mix(in oklab,var(--fg) 4%,transparent)}
.flow-you{border-style:dashed}
.flow-tool{display:block;font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;color:var(--accent);margin-bottom:6px}
.flow-you .flow-tool{color:var(--fg-mid)}
@media(max-width:900px){.flow,.flow.f4{grid-template-columns:1fr;grid-template-rows:none}.flow-step{grid-row:auto;grid-template-columns:auto 1fr 1fr;grid-template-rows:auto;align-items:stretch}}
@media(max-width:560px){.flow-step{grid-template-columns:1fr}}
/* AI case-study blocks: how the workflow changes (.shift) and who does what (.roles) */
.shift,.roles{display:grid;grid-template-columns:1fr 1fr;gap:clamp(16px,2vw,28px);margin-top:clamp(20px,2.4vw,32px)}
.shift>div,.roles>div{border-top:1px solid var(--border-soft);padding-top:14px}
.shift h3,.roles h3{font-family:var(--font-mono);font-weight:400;font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;color:var(--fg-mid);margin:0 0 10px}
.shift .now h3,.roles .ai h3{color:var(--accent)}
.shift ol,.roles ul{list-style:none;margin:0;padding:0;display:grid;gap:8px}
.shift li,.roles li{font-size:var(--t2);line-height:1.5;color:var(--fg)}
.shift li{position:relative;padding-left:22px}
.shift li::before{content:"→";position:absolute;left:0;color:var(--fg-mid)}
.shift li:first-child::before{content:"•"}
.shift .was li{color:var(--fg-mid)}
.roles li{padding-left:14px;position:relative}
.roles li::before{content:"";position:absolute;left:0;top:.65em;width:5px;height:5px;border-radius:50%;background:var(--fg-mid)}
.roles .ai li::before{background:var(--accent)}
.hard-call{margin-top:clamp(20px,2.4vw,32px)}
@media(max-width:640px){.shift,.roles{grid-template-columns:1fr}}
/* a project page's sizzle reel plays over the cover: on hover with a pointer, in view on touch (see v3.js) */
.mosaic.arch li.sz{display:block;opacity:1;z-index:2;pointer-events:none}
.arch-sizzle{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;border-radius:var(--radius);opacity:0;transition:opacity .45s var(--ease)}
.arch-sizzle.on{opacity:1}
/* caption sits under its own rule, which an accent line sweeps on hover */
.arch-meta{position:relative;margin-top:15px;padding-top:12px;display:grid;grid-template-columns:1fr auto;
  column-gap:14px;border-top:1px solid var(--border-soft)}
.arch-meta::after{content:"";position:absolute;left:0;right:0;top:-1px;height:1px;background:var(--accent);
  transform:scaleX(0);transform-origin:0 50%;transition:transform .55s var(--ease-lux)}
.arch-cell:hover .arch-meta::after{transform:scaleX(1)}
.arch-n,.arch-count{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-caps);text-transform:uppercase;
  color:var(--fg-mid);grid-row:1;line-height:1;transition:color 240ms var(--ease)}
.arch-n{grid-column:1}
.arch-count{grid-column:2;justify-self:end}
.arch-cell:hover .arch-n{color:var(--accent)}
/* a project with its own page says so under the caption */
.arch-go{grid-column:1/-1;justify-self:start;margin-top:10px;font-size:var(--t1);font-weight:600;color:var(--fg);border-bottom:1px solid var(--fg);padding-bottom:2px;transition:color var(--t-hover),border-color var(--t-hover)}
.arch-go:hover{color:var(--accent);border-color:var(--accent)}
.arch-meta h3{grid-column:1/-1;font-family:var(--font-body);font-weight:600;font-size:var(--tt);
  letter-spacing:var(--tr-head);line-height:1.15;margin-top:9px}
/* two lines everywhere, so every caption is the same height and the tiles in
   a row land on one baseline */
.arch-meta p{grid-column:1/-1;font-size:var(--t2)!important;line-height:1.45!important;color:var(--fg-mid);
  margin-top:6px;max-width:52ch;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}


/* positioning line under the hero tagline */
.hero-pos{display:block;font-size:var(--t2);font-weight:500;line-height:1.5;color:var(--fg-mid);margin-top:14px;max-width:34ch;letter-spacing:0}
/* how I lead: four ruled cells on the grid */
.lead-grid{--wgap:clamp(24px,2.8vw,44px);grid-template-columns:repeat(4,minmax(0,1fr));column-gap:var(--wgap);border-top:1px solid var(--border-soft);border-bottom:1px solid var(--border-soft)}
.lead-more{margin-top:var(--s5)}
.lead-cell{position:relative;padding:var(--s4) 0;min-width:0}
.lead-cell+.lead-cell::before{content:"";position:absolute;top:0;bottom:0;left:calc(var(--wgap)/-2);border-left:1px solid var(--border-soft)}
.lead-n{display:block;font-family:var(--font-display);font-weight:900;font-size:var(--t6);line-height:.95;letter-spacing:var(--tr-head);font-variant-numeric:tabular-nums}
.lead-cell h3{font-family:var(--font-body);font-weight:600;font-size:var(--t3);letter-spacing:var(--tr-text);margin-top:var(--s3)}
.lead-cell p{font-size:var(--t3);line-height:1.55;color:var(--fg-mid);margin-top:8px}

/* section chrome */
.sec{padding-top:clamp(64px,8vw,112px)}
.sec-head{display:flex;align-items:baseline;justify-content:space-between;gap:var(--s3);margin-bottom:clamp(32px,4vw,56px)}
.sec-title{font-family:var(--font-body);font-weight:600;font-size:var(--t3);letter-spacing:var(--tr-text);display:inline-flex;align-items:center}
.sec-note{font-size:var(--t3);font-weight:500;line-height:1.5;max-width:22ch;color:var(--fg)}

/* work */
/* work, editorial case rows: image seven columns, text five, alternating,
   with an oversized numeral sitting behind the copy */
.case-rows{list-style:none;border-top:1px solid var(--border-soft);margin-top:var(--s2)}
.case-row{border-bottom:1px solid var(--border-soft)}
.cr{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));column-gap:var(--gutter);align-items:center;padding:clamp(56px,6.4vw,104px) 0;color:inherit}
/* each row's media is a six-tile mosaic on a 6x4 cell grid; layout a and
   its mirror b alternate down the page */
/* justified rows: each tile's width is proportional to its aspect ratio
   (--ar), so a row fills the width at one shared height and nothing is
   cropped. Two rows, grouped by tone, the brand tile closing its row. */
/* Six tiles justified into two equal rows made every image the same height,
   which is exactly what removes emphasis: the largest was 1.43x the smallest,
   so nothing led and a dense screenshot at 232px wide could not be read. The
   composition now has a dominant: one tile across four columns and two rows,
   two stacked beside it, three along the bottom on a shorter row. That is a
   little over five times the area between largest and smallest, which is what
   makes a group of images read as a picture rather than a contact sheet.
   The whole block carries the aspect ratio, so the cells are shapes and the
   pictures fill them; their own ratios no longer have to agree. */
.cr-media{grid-column:1/8;grid-row:1;display:grid;
  grid-template-columns:repeat(6,minmax(0,1fr));
  grid-template-rows:1fr 1fr .74fr;gap:10px;aspect-ratio:16/9.6}
.cr-media .t{position:relative;min-width:0;overflow:hidden;border-radius:var(--radius);
  background:var(--surface-raised)}
.cr-media .t:nth-child(1){grid-column:1/5;grid-row:1/3}
.cr-media .t:nth-child(2){grid-column:5/7;grid-row:1}
.cr-media .t:nth-child(3){grid-column:5/7;grid-row:2}
.cr-media .t:nth-child(4){grid-column:1/3;grid-row:3}
.cr-media .t:nth-child(5){grid-column:3/5;grid-row:3}
.cr-media .t:nth-child(6){grid-column:5/7;grid-row:3}
/* mirrored on the alternating rows, so the hero always sits on the outer edge
   and the two compositions are not the same picture twice */
.case-row:nth-child(even) .cr-media .t:nth-child(1){grid-column:3/7}
.case-row:nth-child(even) .cr-media .t:nth-child(2),
.case-row:nth-child(even) .cr-media .t:nth-child(3){grid-column:1/3}
.case-row:nth-child(even) .cr-media .t:nth-child(4){grid-column:1/3}
.case-row:nth-child(even) .cr-media .t:nth-child(5){grid-column:3/5}
.case-row:nth-child(even) .cr-media .t:nth-child(6){grid-column:5/7}
.cr-media .t img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;transform:scale(1.02);transition:transform 1.1s var(--ease-lux)}
/* a square mark in a landscape cell: cover would cut the ring off top and
   bottom, so it is contained on the ground it was drawn against */
.cr-media .t.fit{background:#000}
.cr-media .t.fit img{object-fit:contain;transform:scale(.88)}
.cr:hover .cr-media .t.fit img{transform:scale(.93)}
/* one mark on white, filling the whole media cell */
.cr-media.solo .t,.case-row:nth-child(even) .cr-media.solo .t{grid-column:1/-1;grid-row:1/-1;background:var(--mark-ground)}
.cr-media.solo .t img{object-fit:contain;transform:scale(.56)}
.cr:hover .cr-media.solo .t img{transform:scale(.6)}
.cr-media.solo .t .mk-dk{display:none}
html[data-theme="dark"] .cr-media.solo .t .mk-lt,html[data-theme="auto"].scrolled .cr-media.solo .t .mk-lt{display:none}
html[data-theme="dark"] .cr-media.solo .t .mk-dk,html[data-theme="auto"].scrolled .cr-media.solo .t .mk-dk{display:block}
.cr:hover .cr-media .t img{transform:scale(1.08)}
.cr-media .t:nth-child(2) img{transition-delay:40ms}.cr-media .t:nth-child(3) img{transition-delay:80ms}.cr-media .t:nth-child(4) img{transition-delay:120ms}.cr-media .t:nth-child(5) img{transition-delay:160ms}.cr-media .t:nth-child(6) img{transition-delay:200ms}
@supports (animation-timeline:scroll()){
  .cr-media .t{animation:rowIn linear both;animation-timeline:view();animation-range:entry 0% entry 30%}
  .cr-media .t:nth-child(2){animation-range:entry 5% entry 35%}.cr-media .t:nth-child(3){animation-range:entry 10% entry 40%}
  .cr-media .t:nth-child(4){animation-range:entry 15% entry 45%}.cr-media .t:nth-child(5){animation-range:entry 20% entry 50%}.cr-media .t:nth-child(6){animation-range:entry 25% entry 55%}
}
.cr-body{grid-column:8/13;grid-row:1;position:relative;isolation:isolate;display:block;padding-left:var(--s2)}
.case-row:nth-child(even) .cr-media{grid-column:6/13}
.case-row:nth-child(even) .cr-body{grid-column:1/6;padding-left:0;padding-right:var(--s2)}
.cr-num{position:absolute;right:0;top:-.5em;z-index:-1;font-family:var(--font-display);font-weight:900;font-size:clamp(96px,11vw,168px);line-height:1;letter-spacing:var(--tr-tight);color:var(--muted);font-variant-numeric:tabular-nums;pointer-events:none;user-select:none;transition:color 220ms linear}
.cr-k{display:block;font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;color:var(--fg-mid)}
.cr-title{display:block;font-weight:600;font-size:var(--t6);letter-spacing:var(--tr-head);line-height:1.05;margin-top:12px}
.cr-desc{display:block;font-size:var(--t3);line-height:1.55;color:var(--fg-mid);margin-top:14px;max-width:34ch}
.cr-go{display:inline-flex;align-items:center;gap:12px;margin-top:var(--s3);font-size:var(--t2);font-weight:500;color:var(--fg);transition:gap 180ms ease-out,color var(--t-hover)}
.cr-go .arw{width:24px;height:9px;flex:none;transition:transform 180ms ease-out}
.cr:hover .cr-go{gap:18px;color:var(--accent)}
/* oversized section words, set on the grid: title and note in the first
   four columns, the word right-aligned to column twelve with its foot on
   the rule below */
.sec-head--giant{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));column-gap:var(--gutter);align-items:start;margin-bottom:0}
.sec-head--giant .sec-title{grid-column:1/5;grid-row:1}
.sec-head--giant .sec-note{grid-column:1/5;grid-row:2;align-self:end;margin-top:var(--s3);padding-bottom:clamp(20px,2.4vw,32px)}
.giant{grid-column:5/13;grid-row:1/3;justify-self:end;align-self:end;font-family:var(--font-display);font-weight:900;font-size:clamp(96px,14vw,200px);line-height:.74;letter-spacing:var(--tr-tight);color:var(--muted);pointer-events:none;user-select:none;transition:color 220ms linear}
.sec-statement{font-weight:600;font-size:var(--t5);line-height:1.2;letter-spacing:var(--tr-head);max-width:22ch;text-align:left}
/* running line in the left margin: the label sits at the foot with a
   hairline rising from it, stopping short of the top of the screen */
.margin-line{position:fixed;left:14px;top:0;bottom:36px;z-index:5;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;gap:22px;pointer-events:none}
/* a dot every 8px, on the spacing scale. --grid-dot rather than --border
   because the rule now covers an eighth of its length and needs the weight
   back; it is also the token that exists for exactly this */
.margin-line::before{content:"";flex:1 1 auto;min-height:0;width:1px;margin-top:50px;
  background-image:repeating-linear-gradient(180deg,var(--grid-dot) 0 1px,transparent 1px 8px)}
.ml-t{flex:none;writing-mode:vertical-rl;transform:rotate(180deg);font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-caps);text-transform:uppercase;color:var(--fg-mid);opacity:.7}
@media(max-width:1279px){.margin-line{display:none}}

/* about */
.about-portrait{grid-column:6/9;position:relative;aspect-ratio:1/1;overflow:hidden;border-radius:var(--radius);background:var(--surface-raised)}
.about-portrait img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center top}

/* archive orbit: a ring of thumbnails around a centre line */
.orbit{--ow:clamp(48px,5.2vw,74px);position:relative;height:clamp(440px,58vw,700px);display:grid;place-items:center;overflow:hidden;border-top:1px solid var(--border-soft);border-bottom:1px solid var(--border-soft)}
.orbit-core{position:relative;z-index:60;text-align:center;max-width:min(470px,82vw);padding:var(--s2)}
.orbit-core::before{content:"";position:absolute;inset:-18% -22%;z-index:-1;background:radial-gradient(closest-side,var(--bg) 58%,transparent);transition:background 220ms linear}
.orbit-k{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;color:var(--fg-mid)}
/* the measure belongs to the heading, in the heading's own characters. It used
   to be a 30ch cap on the parent, which resolves at the parent's 16px while
   this renders at 34px, so the line ran to six on a desktop and three on a
   phone. In ch it is the same nineteen characters at every size. */
.orbit-h{font-weight:600;font-size:var(--t5);line-height:1.2;letter-spacing:var(--tr-head);
  margin:10px auto 0;max-width:19ch;text-wrap:balance}
.orbit-core .cta-sm{margin-top:var(--s2)}
.orbit-ring{position:absolute;inset:0;pointer-events:none}
.orb{position:absolute;left:50%;top:50%;width:var(--ow);height:calc(var(--ow)*.75);margin:calc(var(--ow)*-.375) 0 0 calc(var(--ow)*-.5);border-radius:var(--radius);pointer-events:auto;display:block;overflow:hidden;background:var(--surface-raised);box-shadow:0 0 0 1px color-mix(in oklab,var(--fg) 8%,transparent),0 16px 40px -16px rgba(0,0,0,.45);will-change:transform,opacity}
.orb img{width:100%;height:100%;object-fit:cover;display:block}

/* experience, a ruled ledger: big start year, role, outcome, arrow */
.ledger{list-style:none;border-top:1px solid var(--border-soft)}
.ledger li{border-bottom:1px solid var(--border-soft)}
.lrow{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));column-gap:var(--gutter);align-items:start;padding:clamp(20px,2.4vw,32px) 0;color:inherit}
.l-yr{grid-column:1/4;display:flex;align-items:baseline;gap:12px;flex-wrap:wrap}
.l-n{font-family:var(--font-display);font-weight:900;font-size:var(--t6);line-height:.9;letter-spacing:var(--tr-tight);font-variant-numeric:tabular-nums;transition:color var(--t-hover)}
.l-to{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;color:var(--fg-mid);display:inline-flex;align-items:center;gap:8px}
.l-to::before{content:"";width:12px;height:1px;background:var(--fg-mid)}
.live{width:6px;height:6px;border-radius:50%;background:var(--accent);animation:livePulse 2.4s ease-out infinite}
@keyframes livePulse{0%{box-shadow:0 0 0 0 color-mix(in oklab,var(--accent) 45%,transparent)}70%{box-shadow:0 0 0 8px transparent}100%{box-shadow:0 0 0 0 transparent}}
.l-main{grid-column:4/9;min-width:0}
.l-role{display:block;font-size:var(--t3);font-weight:600;letter-spacing:var(--tr-text);line-height:1.3;transition:color var(--t-hover)}
.l-org{display:block;font-size:var(--t2);font-weight:500;color:var(--fg-mid);margin-top:4px}
.l-out{grid-column:9/12;font-size:var(--t3);line-height:1.5;color:var(--fg-mid);padding-top:2px}
.lrow .arw{grid-column:12/13;justify-self:end;width:24px;height:9px;margin-top:8px;transition:transform 180ms ease-out,color var(--t-hover)}
a.lrow:hover .l-n,a.lrow:hover .l-role,a.lrow:hover .arw{color:var(--accent)}
a.lrow:hover .arw{transform:translateX(6px)}
.ledger+.cta-sm{margin-top:var(--s3)}
@keyframes rowIn{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
@supports (animation-timeline:scroll()){.ledger li{animation:rowIn linear both;animation-timeline:view();animation-range:entry 0% entry 35%}}

/* writing, featured essay on the 12-column grid */
.writing-feat{align-items:start}
.writing-shot{grid-column:1/7;display:block;position:relative;aspect-ratio:16/9;overflow:hidden;border-radius:var(--radius);background:var(--surface-raised);transition:transform 260ms var(--ease)}
.writing-shot img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.writing-shot:hover{transform:scale(1.02)}
.writing-body{grid-column:8/13;display:flex;flex-direction:column;gap:var(--s3);padding-top:var(--s1)}
.w-kicker{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;color:var(--fg-mid);margin-bottom:10px}
.w-lead{font-family:var(--font-body);font-weight:600;font-size:var(--t5);line-height:1.15;letter-spacing:var(--tr-head)}
.w-note{font-size:var(--t3);font-weight:500;line-height:1.55;color:var(--fg-mid);max-width:38ch}
.writing-body .cta-sm{margin-top:var(--s2)}
.w-list{list-style:none;margin-top:clamp(32px,4vw,56px);border-top:1px solid var(--border-soft)}
.w-list li{border-bottom:1px solid var(--border-soft)}
.w-row{display:grid;grid-template-columns:32px minmax(0,1fr) auto;gap:var(--s2);align-items:baseline;padding:14px 0;color:inherit;transition:color var(--t-hover)}
a.w-row:hover{color:var(--accent)}
.w-num{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);color:var(--fg-mid)}
.w-title{font-size:var(--t3);font-weight:600;letter-spacing:var(--tr-text);display:block}
.w-meta{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;color:var(--fg-mid);display:block;margin-top:3px}
.w-status{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;color:var(--fg-mid);white-space:nowrap}
.w-row .arw{width:20px;height:8px}
/* an unwritten entry keeps the row's rhythm but takes the arrow's place with
   its status, so nothing in the list looks like a broken link */
.w-soon{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;
  color:var(--fg-mid);border:1px solid var(--border-soft);border-radius:999px;padding:3px 9px;white-space:nowrap}

/* contact */
.contact-body{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:var(--s4);align-items:start}
.contact-h{font-weight:900;font-size:clamp(40px,5vw,76px);line-height:.98;letter-spacing:var(--tr-tight)}
/* contact mark: the monogram inside a hairline circle. The geometry never
   moves, so the facets stay seamless; light sweeps across the five
   diamonds in a wave (a white overlay on the lit facet, a black one on the
   shaded facet) while the whole mark floats. Strokes matching each fill
   cover anti-aliasing seams between facets. */
/* the mark sizes the svg as a fraction of the circle rather than padding it:
   a percentage padding resolves against the parent, which collapsed the svg
   and knocked the dotted ring off centre */
.contact-mark{width:clamp(170px,17vw,250px);aspect-ratio:1;flex:none;margin-top:8px;justify-self:end;display:grid;place-items:center;border-radius:50%;border:1px solid var(--border-soft);transition:border-color 220ms linear}
.contact-mark svg{width:48%;height:auto;display:block;overflow:visible}
.contact-mark polygon{stroke-width:.6;stroke-linejoin:round;paint-order:stroke}
/* the five diamonds flip in a wave, like prisms turning */
.contact-mark .dm{transform-box:fill-box;transform-origin:center;animation:markFlip 5.2s cubic-bezier(.65,0,.35,1) infinite}
.contact-mark .d2{animation-delay:.18s}.contact-mark .d3{animation-delay:.36s}.contact-mark .d4{animation-delay:.54s}.contact-mark .d5{animation-delay:.72s}
@keyframes markFlip{0%,18%{transform:scaleX(1)}50%,68%{transform:scaleX(-1)}100%{transform:scaleX(1)}}
/* a dotted ring turns slowly around the mark */
.contact-mark .ring{fill:none;stroke:currentColor;transform-box:fill-box;transform-origin:center;stroke-width:1.2;stroke-linecap:round;stroke-dasharray:0 9.16;opacity:.35;animation:markSpin 80s linear infinite}
@keyframes markSpin{to{transform:rotate(360deg)}}
.contact-links{display:flex;flex-wrap:wrap;gap:clamp(16px,3vw,48px);margin-top:clamp(24px,3vw,40px)}
.clink{display:inline-flex;align-items:center;gap:14px;font-size:var(--t2);font-weight:500;padding:10px 0 8px;border-bottom:1px solid transparent;min-height:40px;transition:gap 180ms ease-out,color var(--t-hover),border-color var(--t-hover)}
.clink .arw{width:20px;height:8px;flex:none}
.clink:hover{gap:20px;color:var(--accent);border-bottom-color:var(--accent)}

/* testimonials (hidden until the array is filled) */
.tgrid{display:grid;grid-template-columns:repeat(3,1fr)}
.tcard{padding:var(--s3) clamp(14px,1.8vw,28px) var(--s4);border-left:1px solid var(--border);margin:0}
.tcard:first-child{border-left:0;padding-left:0}
.tcard__quote{font-size:var(--t3);font-weight:500;line-height:1.45;letter-spacing:var(--tr-text)}
.tcard__who{margin-top:var(--s2);border-top:1px solid var(--border-soft);padding-top:12px}
.tcard__name{font-size:var(--t1);font-weight:600;display:block}
.tcard__role{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;color:var(--fg-mid)}

/* footer */
.footer{position:relative;isolation:isolate;margin-top:clamp(64px,8vw,112px);padding:clamp(150px,16vw,250px) 0 var(--s3);overflow:hidden}
.footer .wrap{position:relative;z-index:1}
.footer .wrap{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px}
.footer .fmid{margin-left:var(--s2)}
.footer .fmade{margin-left:auto}
.footer .foot__social{margin-left:var(--s4)}
.footer p{font-size:var(--t2);font-weight:500}
.footer .fmark img{width:24px;height:24px;display:block}
.footer .fmid{display:flex;gap:var(--s4);flex-wrap:wrap}
.heart{color:var(--accent)}
.flink{border-bottom:1px solid color-mix(in oklab,currentColor 38%,transparent);padding-bottom:2px;transition:color var(--t-hover),border-color var(--t-hover)}
.flink:hover{color:var(--accent);border-bottom-color:var(--accent)}

/* ── SCROLL-TO-TOP (square, hairline) ── z-index above the project dock (90): v3.js moves the button onto
   the body beside the dock, so the dock's faded end cannot take its clicks */
#toTop{position:fixed;right:clamp(16px,3vw,28px);bottom:clamp(16px,3vw,28px);z-index:95;width:44px;height:44px;display:grid;place-items:center;cursor:pointer;border-radius:var(--r-btn);
  color:var(--fg);background:var(--bg);border:1px solid var(--fg);opacity:0;visibility:hidden;transition:opacity var(--t-hover),visibility var(--t-hover),color var(--t-hover),border-color var(--t-hover)}
#toTop.show{opacity:1;visibility:visible}
#toTop:hover{color:var(--accent);border-color:var(--accent)}

/* ── PROJECT DOCK ── every project on one bottom-edge carousel, macOS-dock
   style: forty-nine same-sized, evenly spaced tiles on a track that lies
   flat until the pointer is over it; then the tile under the pointer
   magnifies (its nearest neighbours proportionally less), each grown tile
   shoving the rest outward by half its growth so the tiles beyond keep
   their spacing. v3.js builds it on every page, reveals it as the cursor
   nears the bottom edge, turns the track toward whichever side of centre the
   cursor sits on (faster the further out, still in a dead zone at the
   centre) and writes each tile's --x/--y/--s; the transforms live here. Fine
   pointers only; :focus-within reveals it for keyboard users and centres the
   focused tile. Tiles are .glass, so surface and theme come from there. */
/* the top pad holds the tallest tile (magnified and lifted) and its name, since overflow clips. clip, not
   hidden: hidden is still a scroll container, and focusing an off-screen tile would scroll it. --rim is how
   far the reel's edge rises above the bottom at the centre; the tiles sit centred in that band */
#dock{position:fixed;left:0;right:0;bottom:0;z-index:90;display:none;overflow:clip;--rim:86px;--reel-r:4600px;padding:176px 0 19px;
  opacity:0;pointer-events:none;transform:translateY(calc(100% + 20px));
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 14%,#000 86%,transparent);mask-image:linear-gradient(90deg,transparent,#000 14%,#000 86%,transparent);
  /* leaving: a smooth slide down below the fold, opaque the whole way, then off. The transition here is the
     exit's; the one on .show is the entrance's */
  transition:transform 460ms cubic-bezier(.45,.05,.55,.95),opacity 1ms linear 460ms}
/* the reel: a disc so large (v3.js sets --reel-r from the viewport) that only its rim shows, rising from
   below the page like a View-Master reel edge-on. The tiles sit on it like the reel's windows, and steering
   the track turns it. Paper in the light theme, ink in the dark; a soft shadow lifts it off the page and
   the side mask fades its ends with the tiles */
#dock::before{content:"";position:absolute;left:50%;top:calc(100% - var(--rim));width:calc(var(--reel-r) * 2);height:calc(var(--reel-r) * 2);
  border-radius:50%;transform:translateX(-50%);pointer-events:none;
  background:rgba(13,14,19,.8);box-shadow:inset 0 1px 0 rgba(255,255,255,.14),0 -10px 40px rgba(0,0,0,.38)}
html[data-theme="light"] #dock::before,html[data-theme="auto"]:not(.scrolled) #dock::before{
  background:rgba(243,242,239,.88);box-shadow:inset 0 1px 0 #fff,0 -10px 40px rgba(10,15,28,.16)}
@media(hover:hover) and (pointer:fine){#dock{display:block}}
#dock.show,#dock:focus-within{opacity:1;pointer-events:auto;transform:none;transition:opacity 240ms var(--ease),transform 480ms var(--ease)}
/* the analytics prompt owns the bottom edge while it is up: on a narrow
   window the dock would rise under its buttons as the pointer reaches for them */
html:has(.consent.on) #dock{opacity:0;pointer-events:none}
.dock-track{position:relative;display:flex;align-items:flex-end;gap:10px;width:max-content;will-change:transform}
/* a tile is one of the reel's windows: a rounded square with a thin film-frame line over its content
   (the ::after, so it also rims the covers). --r is its tilt on the reel's rim */
#dock .dock-item{position:relative;flex:none;display:grid;place-items:center;width:48px;height:48px;border:0;border-radius:14px;text-decoration:none;color:var(--fg);
  font-family:var(--font-display);font-weight:900;font-size:var(--t2);letter-spacing:var(--tr-head);line-height:1;
  transform-origin:50% 100%;will-change:transform;transform:translate(var(--x,0px),var(--y,0px)) rotate(var(--r,0deg)) scale(var(--s,1));
  /* no transform transition: v3.js springs --x/--y/--s itself, every frame */
  transition:background-color var(--t-hover),border-color var(--t-hover),color var(--t-hover);
  /* a soft drop shadow seats the window on the rim; the ::after adds a faint edge and a hairline top light over
     the content, so the corners read crisp without a hard line */
  box-shadow:0 1px 2px rgba(10,15,28,.12),0 6px 16px rgba(10,15,28,.14)}
html[data-theme="dark"] #dock .dock-item,html[data-theme="auto"].scrolled #dock .dock-item{box-shadow:0 1px 2px rgba(0,0,0,.45),0 8px 20px rgba(0,0,0,.4)}
#dock .dock-item:focus-visible{border-radius:14px}
#dock .dock-item::after{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;
  box-shadow:inset 0 0 0 1px color-mix(in oklab,var(--fg) 16%,transparent),inset 0 1px 0 rgba(255,255,255,.3)}
#dock .dock-item:hover{color:var(--accent)}
/* the tile for the page being viewed, the only one highlighted: an accent border and a blurred accent glow,
   with no inner edge line so the border reads. The html prefix outranks the dark-theme shadow above */
html #dock .dock-item[aria-current="page"]{border:1px solid var(--accent);
  box-shadow:0 0 0 1px color-mix(in oklab,var(--accent) 35%,transparent),0 0 22px color-mix(in oklab,var(--accent) 65%,transparent),0 6px 16px rgba(10,15,28,.14)}
#dock .dock-item[aria-current="page"]::after{box-shadow:none}
/* a mark sits inside the glass; a cover fills the circle (an icon or a
   screenshot cropped round). Monochrome marks carry a light and a dark file. */
#dock .dock-item img{display:block;pointer-events:none}
#dock .dock-item .mark{width:64%;height:64%;object-fit:contain}
#dock .dock-item .cover{width:100%;height:100%;object-fit:cover;border-radius:inherit}
#dock .dock-item .dk{display:none}
html[data-theme="dark"] #dock .lt,html[data-theme="auto"].scrolled #dock .lt{display:none}
html[data-theme="dark"] #dock .dk,html[data-theme="auto"].scrolled #dock .dk{display:block}
/* the one name, above whichever tile is hovered or focused; v3.js places it each frame. It is the
   dock's last child, so it paints over every tile, and the dock sits over the page */
.dock-name{position:absolute;left:0;top:0;transform:translate(-50%,-100%);white-space:nowrap;pointer-events:none;
  font-family:var(--font-mono);font-weight:500;font-size:var(--t1);letter-spacing:var(--tr-caps);text-transform:uppercase;
  color:var(--fg);padding:5px 9px;border-radius:999px;background:color-mix(in oklab,var(--surface) 82%,transparent);border:1px solid color-mix(in oklab,var(--fg) 14%,transparent);
  opacity:0;transition:opacity var(--t-hover)}
.dock-name.on{opacity:1}

/* ============================================================
   CASE STUDIES & ESSAY. Editorial two-column: section label left,
   content right; hairlines only, no boxes; air between sections.
   ============================================================ */
.case-article{padding-top:clamp(16px,2vw,32px);padding-bottom:0}

/* ---- THE CASE HERO BACKDROP -----------------------------------------
   The same device as the homepage hero: the reel's cover image, full
   bleed behind the head of the page, under a scrim built from --bg so it
   reads as a backdrop in both themes rather than a photograph in one and
   a fog in the other. The wrapper is what the backdrop is sized to, so
   it ends with the signals rule at every width and needs no magic
   height. It runs up past the masthead and a little below the rule, and
   the gradient closes on --bg at both ends, so the band has no edges.
   The horizontal ramp is flatter than the homepage's: the content here
   spans the full measure instead of sitting in the left columns. */
.case-hero{position:relative;isolation:isolate}
.case-bg{position:absolute;left:calc(50% - 50vw);width:100vw;
  top:calc(-1 * clamp(104px,14vh,184px));
  bottom:calc(-1 * clamp(18px,2.4vw,40px));
  z-index:-1;overflow:hidden;pointer-events:none}
.case-bg img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center}
.case-bg::after{content:"";position:absolute;inset:0;background:
  linear-gradient(90deg,var(--bg) 0 10%,color-mix(in oklab,var(--bg) 88%,transparent) 48%,
    color-mix(in oklab,var(--bg) 78%,transparent) 100%),
  linear-gradient(180deg,var(--bg) 0,transparent 24%,transparent 70%,var(--bg) 100%)}
/* narrow: the content runs the full measure, so the ramp has nothing to stay
   out of the way of, and the crop bites into brighter parts of the picture */
@media(max-width:900px){
  .case-bg::after{background:
    linear-gradient(90deg,var(--bg) 0 6%,color-mix(in oklab,var(--bg) 88%,transparent) 40%,
      color-mix(in oklab,var(--bg) 85%,transparent) 100%),
    linear-gradient(180deg,var(--bg) 0,transparent 20%,transparent 72%,var(--bg) 100%)}
}

.case-back{display:inline-flex;align-items:center;gap:10px;font-size:var(--t2);font-weight:500;padding:10px 0 8px;min-height:40px;border-bottom:1px solid var(--fg);transition:color var(--t-hover),border-color var(--t-hover)}
.case-back:hover{color:var(--accent);border-bottom-color:var(--accent)}
/* a nested page shows its place: All work / Archive / Konneckt. The current
   page is the last crumb, in the mid tone and not a link */
.crumbs{display:block;width:fit-content;padding:10px 0 8px;min-height:40px}
.crumbs ol{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;align-items:center;gap:0 10px}
.crumbs li{display:flex;align-items:center;gap:10px;font-size:var(--t2);font-weight:500;line-height:1.4}
.crumbs li+li::before{content:"/";color:var(--fg-mid);font-weight:400}
.crumbs a{position:relative;color:var(--fg);transition:color var(--t-hover)}
.crumbs a::after{content:"";position:absolute;left:0;right:0;bottom:-2px;height:1px;background:currentColor;opacity:.38;transition:opacity 160ms var(--ease)}
.crumbs a:hover{color:var(--accent)}
.crumbs a:hover::after{opacity:1}
.crumbs [aria-current="page"]{color:var(--fg-mid)}
.case-kicker{display:flex;align-items:center;margin-top:clamp(32px,4vw,56px);font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;color:var(--fg-mid)}
.case-logo{height:32px;width:auto;display:block;margin:clamp(32px,4vw,56px) 0 0;max-width:260px;object-fit:contain}
.invert-on-dark{filter:none}
.case-title{font-weight:900;font-size:clamp(52px,8.6vw,136px);line-height:.9;letter-spacing:var(--tr-tight);margin-top:var(--s4);max-width:11ch}
.case-sub{font-weight:600;font-size:var(--t4);line-height:1.28;letter-spacing:var(--tr-head);margin-top:clamp(28px,3.2vw,40px);max-width:40ch}
/* at-a-glance row: same rhythm as the Work grid */
.signals,.metrics{--wgap:clamp(28px,3.6vw,56px);display:grid;grid-template-columns:repeat(4,minmax(0,1fr));column-gap:var(--wgap);border-top:1px solid var(--border-soft);border-bottom:1px solid var(--border-soft)}
.signals{margin-top:clamp(48px,6vw,64px);border-top:1px solid var(--border);border-bottom:1px solid var(--border)}
.signal,.metric{position:relative;padding:var(--s4) 0;display:flex;flex-direction:column;gap:10px;min-width:0}
.signal+.signal::before,.metric+.metric::before{content:"";position:absolute;top:0;bottom:0;left:calc(var(--wgap)/-2);border-left:1px solid var(--border-soft)}
.signal .sn,.metric__n{font-family:var(--font-body);font-weight:600;font-size:var(--t5);line-height:1;letter-spacing:var(--tr-head)}
.signal .sl,.metric__l{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;color:var(--fg-mid);line-height:1.5}
.signal .sl a{border-bottom:1px solid color-mix(in oklab,currentColor 45%,transparent)}
.signal .sl a:hover{color:var(--accent);border-bottom-color:var(--accent)}
/* showcase carousel */
.showcase{margin-top:clamp(48px,6vw,80px);position:relative}
.carousel{position:relative;overflow:hidden;border-radius:var(--radius);background:var(--surface-raised)}
.carousel__track{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;scrollbar-width:none}
.carousel__track::-webkit-scrollbar{display:none}
.carousel__slide{flex:0 0 100%;width:100%;scroll-snap-align:center;display:block;object-fit:cover;aspect-ratio:16/9}
.carousel__nav{position:absolute;top:50%;transform:translateY(-50%);width:44px;height:44px;border:1px solid var(--fg);border-radius:var(--r-btn);background:var(--bg);color:var(--fg);display:none;place-items:center;cursor:pointer;z-index:3;transition:color var(--t-hover),border-color var(--t-hover)}
.carousel__nav:hover{color:var(--accent);border-color:var(--accent)}
.carousel__nav.glass:hover{transform:translateY(-50%)}
.carousel__nav.prev{left:12px}.carousel__nav.next{right:12px}
@media(hover:hover) and (pointer:fine){.carousel__nav{display:grid}}
.showcase__pills{display:flex;gap:8px;margin-top:12px}
.showcase__pill{width:8px;height:8px;border:1px solid var(--fg);border-radius:3px;background:transparent;cursor:pointer;padding:0;transition:background var(--t-hover)}
.showcase__pill.active{background:var(--fg)}
/* body */
.case-body{margin-top:clamp(48px,6vw,80px)}
.case-result{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));column-gap:var(--gutter);row-gap:var(--s2);padding:var(--s5) 0;border-top:1px solid var(--border-soft);border-bottom:1px solid var(--border-soft)}
.case-result .rk{grid-column:1/4;font-family:var(--font-body);font-weight:600;font-size:var(--t2);letter-spacing:var(--tr-text);display:inline-flex;align-items:center;align-self:start;padding-top:4px}
.case-result p{grid-column:4/11;font-size:var(--t3);line-height:1.5;font-weight:500}
.case-result p strong{font-weight:600}
.case-section{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));column-gap:var(--gutter);row-gap:var(--s2);padding-top:clamp(64px,8vw,96px)}
.case-section.brand-sys,.case-section.brand-sys+.case-section{padding-top:clamp(80px,10vw,128px)}
.case-section__label{grid-column:1/4;font-family:var(--font-body);font-weight:600;font-size:var(--t2);letter-spacing:var(--tr-text);display:inline-flex;align-items:center;align-self:start;padding-top:4px;color:var(--fg)}
.case-section>:not(.case-section__label){grid-column:4/11;min-width:0}
.case-section>.mosaic-wrap,.case-section>.highlights,.case-section>.projects,.case-section>.proj-row,.case-section>.case-gallery,.case-section>.metrics,.case-section>.brandbar{grid-column:4/12}
.case-section p{font-size:var(--t3);line-height:1.65;max-width:64ch}
.case-section p strong{font-weight:600}
.case-section p a,.link{border-bottom:1px solid color-mix(in oklab,currentColor 38%,transparent);transition:color var(--t-hover),border-color var(--t-hover)}
.case-section p a:hover,.link:hover{color:var(--accent);border-bottom-color:var(--accent)}
.case-lede{font-weight:600;font-size:var(--t5)!important;line-height:1.3!important;letter-spacing:var(--tr-head);max-width:44ch!important;margin-bottom:8px}
.case-note{font-family:var(--font-mono);font-size:var(--t1)!important;line-height:1.6!important;color:var(--fg-mid)!important}
.highlights{--wgap:clamp(28px,3.6vw,56px);display:grid;grid-template-columns:1fr 1fr;column-gap:var(--wgap);margin-top:var(--s2);border-top:1px solid var(--border-soft);border-bottom:1px solid var(--border-soft)}
.hl{position:relative;padding:var(--s4) 0}
.hl:nth-child(2n)::before{content:"";position:absolute;top:0;bottom:0;left:calc(var(--wgap)/-2);border-left:1px solid var(--border-soft)}
.hl:nth-child(n+3){border-top:1px solid var(--border-soft)}
.hl h3{font-family:var(--font-body);font-weight:600;font-size:var(--t3);letter-spacing:var(--tr-text);margin-bottom:8px}
.hl p{font-size:var(--t3)!important;line-height:1.55!important;color:var(--fg-mid)}
.highlights.figs{grid-template-columns:1fr}
.highlights.figs .hl{padding:var(--s5) 0}
.highlights.figs .hl::before{display:none}
.highlights.figs .hl:nth-child(n+2){border-top:1px solid var(--border-soft)}
.highlights.figs .case-figure{margin:var(--s4) 0 0}
.highlights.figs .hl p{max-width:68ch}
.metrics{margin-top:var(--s2)}
.caps{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));column-gap:var(--gutter);row-gap:6px;list-style:none;margin-top:var(--s1)}
.caps li{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);line-height:1.6;position:relative;padding-left:14px}
.caps li::before{content:"";position:absolute;left:0;top:.62em;width:4px;height:4px;background:var(--accent);border-radius:1px}
.cv{list-style:none;border-top:1px solid var(--border-soft)}
.cv__row{display:flex;align-items:baseline;justify-content:space-between;gap:var(--s2);flex-wrap:wrap;padding:12px 0;border-bottom:1px solid var(--border-soft)}
.cv__main{display:flex;flex-direction:column;gap:2px}
.cv__co{font-weight:600;font-size:var(--t3)}
.cv__role{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;color:var(--fg-mid)}
.cv__note{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);color:var(--fg-mid);white-space:nowrap}
/* résumé page: roles are full-width ledger rows, like Experience on the
   homepage. Big start year, range beneath, role and organisation in the
   middle, the outcome in plain sentences on the right. */
.rs .rl{grid-column:1/-1;list-style:none;margin-top:var(--s3);border-top:1px solid var(--border-soft)}
.rs .case-lede{margin-bottom:0}
.rl-row{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));column-gap:var(--gutter);align-items:start;padding:clamp(24px,2.8vw,36px) 0;border-bottom:1px solid var(--border-soft)}
/* The bullets are capped at a proper 56ch measure, which is right, and which
   used to leave the last two columns empty for the whole length of the page:
   213px of nothing, seventeen per cent of the width. The dates move there as a
   right rail, so the composition closes on the grid and the left column loses
   the third line that was making it feel like a thin ribbon beside a slab. */
/* all three sit on row one explicitly: the date comes before the bullets in
   the source, which reads correctly, but sparse auto-placement will not step
   back past a column it has already filled and was dropping the bullets to a
   second row */
.rl-row>*{grid-row:1}
.rl-lead{grid-column:1/5;display:flex;flex-direction:column;min-width:0;padding-right:var(--s2)}
.rl-co{font-size:var(--t4);font-weight:600;letter-spacing:var(--tr-head);line-height:1.15}
.rl-title{font-size:var(--t3);line-height:1.45;color:var(--fg);margin-top:8px}
.rl-rng{grid-column:11/13;justify-self:end;text-align:right;font-size:var(--t2);
  line-height:1.4;color:var(--fg-mid);font-variant-numeric:tabular-nums;
  padding-top:.32em;white-space:nowrap}   /* set to the org name's first line */
.rl-pts{grid-column:5/11;list-style:none;max-width:56ch}
.rl-pts li{font-size:var(--t3);line-height:1.55;color:var(--fg-mid)}
.rl-pts li:first-child{padding-top:2px}
.rl-pts li+li{margin-top:14px}
.resume .caps{row-gap:8px}
.resume .caps li{font-family:var(--font-body);font-size:var(--t2);letter-spacing:0;line-height:1.5;padding-left:0}
.resume .caps li::before{display:none}
.case-ctas{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s1) var(--s4)}
.brandbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s2) var(--s4);margin:var(--s2) 0 var(--s2)}
.brandbar img{height:22px;width:auto}
.project-group{margin-top:var(--s3)}
.project-group:first-child{margin-top:0}
.project-group__title{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;color:var(--fg-mid);margin-bottom:8px}
.projects{--wgap:clamp(28px,3.6vw,56px);display:grid;grid-template-columns:1fr 1fr;column-gap:var(--wgap);margin-top:var(--s2)}
.project{position:relative;display:flex;flex-direction:column;padding:0 0 var(--s4);min-width:0}
.project:nth-child(2n)::before{content:"";position:absolute;top:0;bottom:0;left:calc(var(--wgap)/-2);border-left:1px solid var(--border-soft)}
.project:nth-child(n+3){border-top:1px solid var(--border-soft);padding-top:var(--s4)}
.project__media{position:relative;aspect-ratio:16/10;overflow:hidden;border-radius:var(--radius);background:var(--surface-raised)}
.project__media img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
.project__body{padding:var(--s3) 0 0;display:flex;flex-direction:column;gap:10px;flex:1}
.project__body h3{font-family:var(--font-body);font-weight:600;font-size:var(--t3);letter-spacing:var(--tr-text)}
.project__body p{font-size:var(--t2)!important;line-height:1.55!important;color:var(--fg-mid)}
.project__links{display:flex;flex-wrap:wrap;align-items:center;gap:14px;padding-top:4px}
.project__tag{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;color:var(--fg-mid)}
.card-btn{display:inline-flex;align-items:center;gap:12px;align-self:flex-start;margin-top:4px;padding:8px 0 6px;min-height:40px;border-bottom:1px solid var(--fg);color:var(--fg);font-size:var(--t2);font-weight:500;transition:color var(--t-hover),border-color var(--t-hover)}
.card-btn:hover{color:var(--accent);border-bottom-color:var(--accent)}
/* pull quote, one per case page, set in the display face */
.case-quote{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));column-gap:var(--gutter);margin-top:clamp(64px,8vw,96px);padding:clamp(40px,5vw,72px) 0;border-top:1px solid var(--border-soft);border-bottom:1px solid var(--border-soft)}
.case-quote p{grid-column:2/12;font-family:var(--font-display);font-weight:900;font-size:var(--t6);line-height:1;letter-spacing:var(--tr-tight);text-wrap:balance}
.case-quote cite{grid-column:2/12;font-style:normal;font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;color:var(--fg-mid);margin-top:var(--s2)}
/* cross-document view transitions: a work row's image morphs into the case hero */
@view-transition{navigation:auto}
::view-transition-old(root),::view-transition-new(root){animation-duration:.32s}
.case-gallery{display:grid;grid-template-columns:1fr 1fr;gap:var(--s3);margin-top:var(--s1)}
.case-figure{overflow:hidden;margin:0}
.case-figure.wide{grid-column:1/-1}
/* a figure that leaves the reading column entirely: full width of the window,
   with its caption brought back to the page's left margin so it lines up with
   the tier labels rather than floating under the middle of the picture */
.case-figure.bleed{width:100vw;margin-left:calc(50% - 50vw);margin-block:var(--s7)}
.case-figure.bleed>figcaption{max-width:var(--maxw);margin:0 auto;padding:12px var(--pad-x) 0}
/* height:auto matters: an <img height="…"> attribute is a presentational hint
   that would otherwise win over aspect-ratio and draw the picture at its own
   pixel height, which is exactly what happened when the case images gained
   their dimensions for layout stability */
.case-figure img{width:100%;height:auto;display:block;aspect-ratio:16/9;object-fit:cover;border-radius:var(--radius);background:var(--surface-raised)}
/* a cinematic band for a picture that is wide by nature, a quarter shorter than 16:9 */
.case-figure.band img{aspect-ratio:21/9}
/* public product screenshots keep their own proportions: a console cropped to
   16/9 loses the chrome that makes it read as a real product. They are
   light-UI, so they sit on a white plate in both themes rather than fighting
   the dark one. */
.case-figure.shot img{aspect-ratio:auto;height:auto;object-fit:contain;background:#fff;border:1px solid var(--border-soft)}
/* paired shots share one plate ratio so the two captions sit on the same line */
.case-gallery .case-figure.shot:not(.wide) img{aspect-ratio:16/10}
/* A console screenshot on a bare plate reads as a pasted image. Google Cloud
   frames its own product shots in dark rounded window chrome; the convention is
   generic, so the frame here is ours, in our neutrals, and the caption stays
   outside it. */
.shotwin{background:#1c1d21;border-radius:var(--radius);padding:28px 8px 8px;position:relative;overflow:hidden}
.shotwin::before{content:"";position:absolute;top:10px;left:14px;width:8px;height:8px;border-radius:50%;
  background:#44464e;box-shadow:13px 0 0 #44464e,26px 0 0 #44464e}
.case-figure.shot .shotwin img{border:0;border-radius:4px}
/* product screenshots baked as a dark window (chrome, address pill, the shot) with
   transparent corners; the ground around the window is the page's, so it follows
   the theme, and each window is as tall as its own screenshot */
.case-figure.win{background:var(--shot-ground);background-image:repeating-linear-gradient(90deg,var(--shot-grid) 0 1px,transparent 1px calc(100%/12));border-radius:var(--radius);padding:clamp(12px,1.8vw,24px)}
.case-figure.win img{aspect-ratio:auto;height:auto;object-fit:contain;background:transparent;border:0;border-radius:0}
.case-figure.win figcaption{padding-top:10px}
.prod .case-gallery{align-items:start}
/* an animation cannot be baked into a window, so for those the chrome is drawn
   here, matched to the baked frames: dark bar, three dots, the address pill */
.case-figure.win .shotwin{background:#14171e;border:1px solid rgba(255,255,255,.14);border-radius:14px;padding:27px 0 0;position:relative;overflow:hidden}
.case-figure.win .shotwin::before{content:"";position:absolute;top:11px;left:11px;width:6px;height:6px;border-radius:50%;background:#3a3f4a;box-shadow:9px 0 0 #3a3f4a,18px 0 0 #3a3f4a}
.case-figure.win .shotwin::after{content:"https:// " attr(data-url);position:absolute;top:6px;left:42px;right:8px;height:16px;line-height:16px;padding:0 8px;border-radius:8px;background:#0f1218;color:#9aa0a6;font-family:var(--font-mono);font-size:8px;letter-spacing:.04em;white-space:nowrap;overflow:hidden}
.case-figure.win .shotwin img{display:block;width:100%;height:auto;border-radius:0}
/* the products list: one block per product, its links beside the title and its
   screenshots underneath, grouped under an era heading */
/* the products list spans the section grid; each product is itself a 12-column
   row: icon and name in the label column on the left, description, links and
   screenshots in the content columns on the right, like the rest of the page */
.case-section>.prods{grid-column:1/-1}
.prod-era{grid-column:1/-1;font-family:var(--font-mono);font-weight:500;font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;color:var(--fg-mid);margin-top:var(--s6);padding-bottom:var(--s2);border-bottom:1px solid var(--border)}
.prod-era:first-child{margin-top:var(--s2)}
.prod{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));column-gap:var(--gutter);row-gap:var(--s2);align-items:start;padding:var(--s4) 0 var(--s3);border-bottom:1px solid var(--border-soft)}
.prod__head{display:contents}
/* the official product icon sits inline before the name; the name hangs to the
   right of it so a wrapped second line aligns with the first */
.prod__icon{grid-column:1/4;grid-row:1;width:28px;height:28px;display:block}
.prod__icon img{width:28px;height:28px;display:block}
.prod__head h3{grid-column:1/4;grid-row:1;padding-left:40px;font-family:var(--font-body);font-weight:600;font-size:var(--t4);letter-spacing:var(--tr-text);line-height:1.2}
.prod__head p{grid-column:4/11;grid-row:1;margin:-3px 0 0;font-size:var(--t3)!important;line-height:1.55!important;color:var(--fg-mid);max-width:60ch}
.prod__links{grid-column:4/13;grid-row:2;display:flex;flex-wrap:wrap;gap:var(--s2)}
.prod__link{font-size:var(--t2);font-weight:500;white-space:nowrap;border-bottom:1px solid color-mix(in oklab,currentColor 38%,transparent);transition:color var(--t-hover),border-color var(--t-hover)}
.prod__link:hover{color:var(--accent);border-bottom-color:var(--accent)}
.prod .case-gallery{grid-column:4/13;grid-row:3;margin-top:var(--s1)}
@media(max-width:900px){
  .prod{grid-template-columns:1fr}
  .prod__icon{grid-column:1;grid-row:1;width:24px;height:24px;margin-top:-1px}
  .prod__icon img{width:24px;height:24px}
  .prod__head h3{grid-column:1;grid-row:1;padding-left:34px}
  .prod__head p{grid-column:1;grid-row:2;margin-top:0}
  .prod__links{grid-column:1;grid-row:3}
  .prod .case-gallery{grid-column:1;grid-row:4}
}
@media(max-width:640px){.shotwin{padding:22px 5px 5px}
  .shotwin::before{top:8px;left:10px;width:6px;height:6px;box-shadow:10px 0 0 #44464e,20px 0 0 #44464e}}
.case-figure figcaption{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);color:var(--fg-mid);padding:10px 0 0}
.case-cta{display:inline-flex;align-items:center;gap:14px;margin-top:4px;font-size:var(--t2);font-weight:500;padding:10px 0 8px;min-height:44px;border-bottom:1px solid var(--fg);transition:color var(--t-hover),border-color var(--t-hover)}
.case-cta:hover{color:var(--accent);border-bottom-color:var(--accent)}
.case-next{display:flex;align-items:center;justify-content:space-between;gap:var(--s2);flex-wrap:wrap;margin-top:clamp(56px,7vw,96px);padding-top:var(--s3);border-top:1px solid var(--border-soft);font-size:var(--t2);font-weight:500}
.case-next a{padding:10px 0 8px;min-height:40px;display:inline-flex;align-items:center;border-bottom:1px solid transparent;transition:color var(--t-hover),border-color var(--t-hover)}
.case-next a:hover{color:var(--accent);border-bottom-color:var(--accent)}
.case-next a.next{border-bottom-color:var(--fg)}
/* the drawn ::after line is the only underline; no border beneath it */
.case-back,.case-cta,.card-btn,.case-next a,.case-next a.next,.cta,.cta-sm,.clink{border-bottom:0}
/* essay */
.article__title{font-weight:900;font-size:clamp(44px,7.4vw,120px);line-height:.92;letter-spacing:var(--tr-tight);margin-top:var(--s3);max-width:14ch;text-wrap:balance}
.article__standfirst{font-weight:600;font-size:var(--t4);line-height:1.3;letter-spacing:var(--tr-head);margin-top:clamp(24px,3vw,40px);max-width:44ch}
.article__tldr{font-size:var(--t3);line-height:1.55;color:var(--fg-mid);margin-top:var(--s3);padding:var(--s2) 0;border-top:1px solid var(--border-soft);border-bottom:1px solid var(--border-soft);max-width:60ch}
.article__tldr strong{color:var(--fg);font-weight:600}
.article__byline{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);color:var(--fg-mid);margin-top:var(--s3)}
.prose{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));column-gap:var(--gutter);row-gap:var(--s3);margin-top:clamp(40px,5vw,64px);padding-top:clamp(40px,5vw,64px);border-top:1px solid var(--border-soft)}
.prose>*{grid-column:4/11;min-width:0}
.prose p{font-size:var(--t3);line-height:1.7;max-width:68ch}
.prose p strong{font-weight:600}
.prose h2{grid-column:1/4;font-family:var(--font-body);font-weight:600;font-size:var(--t3);letter-spacing:var(--tr-text);line-height:1.4;display:flex;align-items:flex-start;padding-top:4px}
.prose h2,.prose h2+p,.prose h2+ol{margin-top:clamp(24px,3vw,40px)}
.prose ul,.prose ol{padding-left:20px}
.prose li::marker{color:var(--accent)}
.prose li{font-size:var(--t3);line-height:1.7;max-width:68ch}
.prose li+li{margin-top:.5em}
.article__note{font-family:var(--font-mono);font-size:var(--t2)!important;line-height:1.7!important;color:var(--fg-mid)!important;margin-top:var(--s4)!important;padding-top:var(--s3);border-top:1px solid var(--border-soft)}
/* footer social row (sub-pages) */
.foot__social{display:flex;gap:4px;align-items:center}
.foot__social a{width:44px;height:44px;display:grid;place-items:center;color:var(--fg);transition:color var(--t-hover)}
.foot__social a:hover{color:var(--accent)}
.foot__social svg{width:18px;height:18px;display:block}

/* ============================================================
   CHAT: a ruled transcript in a side sheet (Swiss, not chat-app)
   ============================================================ */
.chat-panel{position:fixed;z-index:120;top:0;right:0;height:100dvh;width:min(460px,100vw);display:flex;flex-direction:column;overflow:hidden;will-change:transform;
  background:var(--bg);border-left:1px solid var(--border-soft);box-shadow:-24px 0 64px rgba(10,15,28,.10);
  transform:translateX(100%);visibility:hidden;transition:transform 420ms var(--ease),visibility 420ms}
.chat-panel.open{transform:none;visibility:visible}
html[data-theme="dark"] .chat-panel,html[data-theme="auto"].scrolled .chat-panel{box-shadow:-24px 0 64px rgba(0,0,0,.5)}
.chat-head{display:flex;align-items:center;gap:12px;padding:calc(18px + env(safe-area-inset-top)) 16px 18px 24px;border-bottom:1px solid var(--border-soft);flex:none}
.chat-head img{width:28px;height:28px}
.chat-head strong{font-weight:600;font-size:var(--t3);letter-spacing:var(--tr-text)}
.chat-head .live{width:6px;height:6px;border-radius:50%;background:var(--accent);flex:none}
.chat-x{margin-left:auto;width:44px;height:44px;color:var(--fg);cursor:pointer;display:grid;place-items:center}
.chat-log{flex:1;min-height:0;overflow-y:auto;padding:8px 24px 24px;overscroll-behavior:contain;display:flex;flex-direction:column}
.chat-log>*{flex:none}
/* empty state */
/* the opening state reads from the top; pinning it to the floor left the
   panel opening on a screenful of nothing */
.chat-empty{padding:10px 0 4px;transition:opacity 480ms var(--ease)}
/* action chips under an answer */
.chat-acts{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0 2px}
.chat-acts .chip{display:inline-flex;align-items:center;min-height:36px;padding:0 14px;border:1px solid var(--border);border-radius:999px;background:var(--surface);color:var(--fg);font-size:var(--t1);font-weight:500;letter-spacing:var(--tr-text);transition:color var(--t-hover),border-color var(--t-hover)}
.chat-acts .chip:hover{color:var(--accent);border-color:var(--accent)}
.chat-empty.collapsed{display:none}
.chat-empty .lead{font-size:var(--t3);line-height:1.5;letter-spacing:var(--tr-text);color:var(--fg);max-width:34ch}
.chat-empty .sug{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;color:var(--fg-mid);margin-top:26px}
.prompts{list-style:none;margin-top:10px;border-top:1px solid var(--border-soft)}
.prompts li{border-bottom:1px solid var(--border-soft)}
.prompts button,.prompts a{display:grid;grid-template-columns:32px 1fr 20px;align-items:center;gap:12px;width:100%;padding:14px 0;background:none;border:0;text-align:left;font:inherit;color:var(--fg);cursor:pointer;transition:color var(--t-hover)}
.prompts .k{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);color:var(--fg-mid)}
.prompts .q{font-size:var(--t2);font-weight:500;line-height:1.35}
.prompts .arw{width:20px;height:8px;transition:transform var(--t-hover)}
.prompts button:hover,.prompts a:hover{color:var(--accent)}
.prompts button:hover .arw,.prompts a:hover .arw{transform:translateX(6px)}
.prompts .book .k{display:grid;place-items:center;justify-items:start;color:var(--accent)}
.prompts .book .k svg{width:15px;height:15px;display:block}
.prompts .book .q{font-weight:600}
/* turns */
.turn{display:grid;grid-template-columns:72px 1fr;gap:16px;padding:18px 0;border-bottom:1px solid var(--border-soft);animation:turnIn var(--d-reveal) var(--ease) both}
@keyframes turnIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.turn .who{display:flex;flex-direction:column;gap:4px;font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;color:var(--fg-mid);padding-top:3px}
.turn .who .n{color:var(--grid-dot)}
.turn.bot .who span:first-child{color:var(--fg)}
.turn .body{font-size:var(--t3);line-height:1.6;min-width:0;white-space:pre-wrap}
.turn.bot .body{white-space:normal}
.turn.user .body{color:var(--fg-mid)}
.turn .body p{margin:0 0 10px}.turn .body>:last-child{margin-bottom:0}
.turn .body strong{font-weight:600}
.turn .body a{border-bottom:1px solid color-mix(in oklab,currentColor 38%,transparent);word-break:break-word;transition:color var(--t-hover),border-color var(--t-hover)}
.turn .body a:hover{color:var(--accent);border-bottom-color:var(--accent)}
.turn .body ul,.turn .body ol{margin:8px 0;padding-left:18px}
.turn .body li{margin:5px 0}
.turn .body li::marker{color:var(--accent)}
.turn .body code{font-family:var(--font-mono);font-size:.88em;background:var(--muted);padding:1px 5px;border-radius:4px}
.turn .body h4,.turn .body h5{font-family:var(--font-body);font-weight:600;margin:14px 0 6px;line-height:1.25}
.turn .body h4{font-size:var(--t3)}.turn .body h5{font-size:var(--t2)}
.turn .thinking{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;color:var(--fg-mid)}
.turn.streaming .body::after{content:"";display:inline-block;width:8px;height:1em;margin-left:4px;vertical-align:-2px;background:var(--fg);animation:blink 1s steps(2,start) infinite}
@keyframes blink{to{visibility:hidden}}
/* composer */
/* the next-question stack under the latest answer, same rows as the intro list */
.chat-next{padding:6px 0 4px}
.chat-next .sug{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);text-transform:uppercase;color:var(--fg-mid);margin:0 0 4px}
.chat-next .prompts button,.chat-next .prompts a{padding:11px 0}
.chat-form{flex:none;padding:12px 24px calc(14px + env(safe-area-inset-bottom));border-top:1px solid var(--border-soft);background:var(--bg)}
.cf-row{display:flex;align-items:center;gap:8px;
  background:var(--fld);border:1px solid var(--fld-line);border-radius:14px;
  padding:5px 5px 5px 16px;
  transition:background-color 220ms var(--ease),border-color 220ms var(--ease),box-shadow 220ms var(--ease)}
.cf-row:hover{background:var(--fld-hi)}
/* the field itself is the focus indicator, which is why the input drops its
   own ring, because a 2px outline inside a filled box reads as a mistake */
.cf-row:focus-within{background:var(--fld-hi);border-color:var(--fld-line-hi);
  box-shadow:0 0 0 3px color-mix(in oklab,var(--fg) 7%,transparent)}
.chat-form input{flex:1;min-width:0;height:44px;font-family:var(--font-body);font-size:var(--t3);color:var(--fg);background:transparent;border:0;padding:0;border-radius:0}
.chat-form input::placeholder{color:var(--fg-mid)}
.chat-form input:focus,.chat-form input:focus-visible{outline:none}
.chat-send{width:44px;height:44px;flex:none;color:var(--fg);cursor:pointer;display:grid;place-items:center}
.chat-send:disabled{opacity:.5}
/* a whisper under the field: centred, below the type scale, and mixed back
   toward the background so it never competes with the placeholder */
.cf-hint{display:flex;justify-content:center;align-items:center;gap:12px;margin-top:10px;font-family:var(--font-mono);font-size:10px;letter-spacing:var(--tr-wide);text-transform:uppercase;color:color-mix(in oklab,var(--fg-mid) 62%,var(--bg))}
.cf-hint a{display:inline-flex;align-items:center;gap:6px;color:var(--fg);border-bottom:1px solid color-mix(in oklab,currentColor 38%,transparent);padding-bottom:2px;transition:color var(--t-hover),border-color var(--t-hover)}
.cf-hint a:hover{color:var(--accent);border-bottom-color:var(--accent)}
/* frosted scrim behind the open sheet, click to close */
.chat-scrim{position:fixed;inset:0;z-index:119;background:color-mix(in oklab,var(--bg) 55%,transparent);-webkit-backdrop-filter:blur(10px) saturate(1.2);backdrop-filter:blur(10px) saturate(1.2);opacity:0;visibility:hidden;transition:opacity 420ms var(--ease),visibility 420ms;cursor:pointer}
.chat-scrim.on{opacity:1;visibility:visible}
html[data-theme="dark"] .chat-scrim,html[data-theme="auto"].scrolled .chat-scrim{background:color-mix(in oklab,var(--bg) 62%,transparent)}
@media(max-width:540px){.chat-scrim{display:none}}
/* while the sheet is open the page underneath is frozen and its floating
   controls are hidden, so nothing peeks through or scrolls behind it */
/* lock the page under the chat without moving it: the body is pinned in
   place at the current scroll offset (set inline by v3.js), so the scrim
   blurs whatever was on screen */
html.chat-open{overflow:hidden;overscroll-behavior:none}
html.chat-open body{position:fixed;left:0;right:0;width:100%;overflow:hidden}
html.chat-open #toTop,html.chat-open .masthead{visibility:hidden}
.chat-log{-webkit-overflow-scrolling:touch}
@media(max-width:540px){.chat-panel{width:100vw;border-left:0;transition:transform 360ms var(--ease),visibility 360ms}.chat-log{padding:8px 20px 20px}.chat-head{padding-left:20px}.chat-form{padding-left:20px;padding-right:20px}.turn{grid-template-columns:1fr;gap:8px}.turn .who{flex-direction:row;gap:10px}}

/* ============================================================
   RESPONSIVE
   ============================================================ */
@media(max-width:1024px){
  .hero-grid{grid-template-rows:auto}
  .hero-name{grid-column:1/-1;grid-row:1}
  .hero-sub{grid-column:1/7;grid-row:2;margin-top:var(--s3)}
  .hero-reel{grid-column:1/-1;grid-row:3;height:clamp(320px,48vh,460px);margin-top:var(--s4)}
  .hero-dots{grid-column:12/13;grid-row:3;align-self:start;padding-top:var(--s5)}
  .hero-ghost{grid-column:7/11;grid-row:3;align-self:end;margin-left:0}
  .hero-cta{grid-column:1/7;grid-row:4;margin-top:var(--s4)}
  .bento{grid-auto-rows:minmax(132px,auto)}
  .b-portrait{grid-column:1/6;grid-row:1/3;border-left:0;padding:var(--s3) var(--s3) var(--s3) 0}
  .b-about{grid-column:6/13;grid-row:1/3}
  .b-now{grid-column:1/7;grid-row:3;border-left:0;padding-left:0;border-top:1px solid var(--border-soft)}
  /* back on the right of row three here, so it needs the edge .first removes */
  .b-build{grid-column:7/13;grid-row:3;padding-right:0;
    border-left:1px solid var(--border-soft);padding-left:var(--bp)}
  .b-ask{grid-column:1/-1;grid-row:4;padding-right:0}
  .b-stats{grid-column:1/-1;grid-row:5;border-left:0}
  .b-stat:first-child{padding-left:0}
  .lead-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
  .lead-cell:nth-child(2n+1)::before{display:none}
  .lead-cell:nth-child(n+3){border-top:1px solid var(--border-soft)}
  .cr{align-items:start}
  .cr-media,.case-row:nth-child(even) .cr-media{grid-column:1/-1;grid-row:1}
  .cr-body,.case-row:nth-child(even) .cr-body{grid-column:1/-1;grid-row:2;padding:var(--s3) 0 0}
  .cr-num{top:-.2em;right:0}
  .sec-head--giant .sec-title,.sec-head--giant .sec-note{grid-column:1/-1}
  .sec-head--giant .sec-note{padding-bottom:0}
  .giant{grid-column:1/-1;grid-row:3;font-size:clamp(72px,20vw,160px);margin-top:var(--s2)}
  .l-yr{grid-column:1/3}
  .l-main{grid-column:3/8}
  .l-out{grid-column:8/12}
  /* Two columns is not enough for "Feb 2022 to Jul 2023" once the page
     narrows, and the rail is nowrap and right-aligned, so it grew leftward
     straight over the bullets. Below this width the date goes back under the
     org and title, and the bullets take the rest of the row. */
  .rl-lead{grid-column:1/5;grid-row:1}
  .rl-rng{grid-column:1/5;grid-row:2;justify-self:start;text-align:left;
    white-space:normal;padding-top:6px}
  .rl-pts{grid-column:5/13;grid-row:1/3}
  .writing-shot{grid-column:1/-1}
  .writing-body{grid-column:1/-1;padding-top:0;margin-top:var(--s3)}
  .proj-row{grid-template-columns:repeat(3,minmax(0,1fr))}
  /* a shorter 2 / 3 rhythm; four across would be unreadable at this width */
  .arch-cell,.arch-cell:nth-child(9n+1),.arch-cell:nth-child(9n+2),
  .arch-cell:nth-child(9n+3),.arch-cell:nth-child(9n+4),.arch-cell:nth-child(9n+5){grid-column:span 4;--tt:17px}
  .arch-cell:nth-child(5n+1),.arch-cell:nth-child(5n+2){grid-column:span 6;--tt:20px}
  .tgrid{grid-template-columns:repeat(2,minmax(0,1fr))}
  .tcard{border-left:0;border-top:1px solid var(--border);padding-left:0;padding-right:0}
  .tcard:nth-child(2){padding-left:clamp(14px,2vw,26px);border-left:1px solid var(--border)}
  .tcard:first-child,.tcard:nth-child(2){border-top:0}
  .signals,.metrics{grid-template-columns:repeat(2,minmax(0,1fr))}
  .signal:nth-child(2n+1)::before,.metric:nth-child(2n+1)::before{display:none}
  .signal:nth-child(n+3),.metric:nth-child(n+3){border-top:1px solid var(--border-soft)}
}
/* Six items, three buttons and a wordmark stop fitting well before the phone
   rules take over: at 768 the bar ran 32px past the page. The menu takes over
   at 900 instead. The JS gate that lets the pane open reads the same number,
   or the button would be there with nothing behind it. */
/* seven items stop fitting beside the wordmark just under 940px */
@media(max-width:950px){
  .nav-links{display:none}
  .nav-toggle{display:grid}
}
@media(max-width:760px){
  .masthead{position:fixed;top:0;left:0;right:0;z-index:60;background:var(--bg);padding:var(--s1) 0}
  html[data-theme="auto"] .masthead{transition:none}
  main{padding-top:64px}
  /* two across, evenly, before the phone index list takes over at 640.
     Matching the rhythm rules' specificity so the later source order wins */
  .arch-grid .arch-cell{grid-column:span 6;--tt:18px}
  /* footer on phones: brand and social share the top row, then a rule,
     then the legal line and the sign-off, each on its own full-width row */
  .footer{padding-top:clamp(150px,30vw,190px)}
  /* the canvas already ends above the legal line, so the mask only softens
     its own two edges instead of clearing the lower half */
  .footer .pfield{translate:0 0;-webkit-mask-image:linear-gradient(180deg,transparent 0,#000 20%,#000 78%,transparent 100%);
    mask-image:linear-gradient(180deg,transparent 0,#000 20%,#000 78%,transparent 100%)}
  .footer .wrap{justify-content:space-between;align-items:center;gap:0}
  .footer .fmark{order:1}
  .footer .foot__social{order:2;margin-left:0;margin-right:-13px}
  .footer .fmid{order:3;flex-basis:100%;margin-left:0;justify-content:space-between;align-items:baseline;gap:var(--s2);
    border-top:1px solid var(--border-soft);margin-top:var(--s2);padding-top:var(--s2)}
  .footer .fmade{order:4;flex-basis:100%;margin-left:0;margin-top:10px;color:var(--fg-mid)}
  .nav-links{display:none}
  .nav-toggle{display:grid}
  .case-section__label,.case-section>:not(.case-section__label),.case-section>.mosaic-wrap,.case-section>.highlights,.case-section>.projects,.case-section>.proj-row,.case-section>.case-gallery,.case-section>.metrics,.case-section>.brandbar,.case-result .rk,.case-result p,.prose>*,.prose h2{grid-column:1/-1}
  .prose h2+p,.prose h2+ol{margin-top:0}
}
@media(max-width:640px){
  .hero-name{font-size:clamp(52px,17vw,76px)}
  .hero-sub{grid-column:1/-1}
  .hero-reel{grid-column:1/-1;grid-row:3;height:clamp(300px,52vh,420px);margin-top:var(--s3)}
  .slice .spine{bottom:12px}
  .slice .spine .t{font-size:var(--t1)}
  .hero-dots{display:none}
  .hero-ghost{grid-column:9/13;font-size:var(--t6)}
  .hero-cta{grid-column:1/-1}
  .bento{grid-auto-rows:auto}
  .b-about,.b-portrait,.b-now,.b-build,.b-ask,.b-stats{grid-column:1/-1;grid-row:auto;border-left:0;padding-left:0;padding-right:0;border-top:1px solid var(--border-soft)}
  .b-about{border-top:0}
  .b-portrait{aspect-ratio:4/3;min-height:0;padding:var(--s3) 0}
  .b-stats{flex-direction:row}
  .b-stat{padding:var(--s2) var(--s2)}
  .b-stat:first-child{padding-left:0}
  .sec-head{flex-direction:column;align-items:flex-start;gap:var(--s2)}
  .lead-grid{grid-template-columns:1fr}
  .lead-cell::before{display:none!important}
  .lead-cell+.lead-cell{border-top:1px solid var(--border-soft)}
  .tgrid{grid-template-columns:1fr}
  .cr{padding:var(--s3) 0}
  /* six cells in a 336px column put the smallest tile at 108x59, which is a
     smudge. The hero takes the full width and the rest run two up, so the
     smallest is 164x102 and every picture is still legible. */
  .cr-media{gap:8px;aspect-ratio:auto;grid-template-columns:repeat(2,minmax(0,1fr));
    grid-template-rows:none;grid-auto-rows:auto}
  /* :nth-child(n) matches everything and is only here to carry the weight: the
     mirrored desktop rules are five classes deep, so a plain reset loses to
     them and the hero collapses to a few pixels */
  .cr-media .t:nth-child(n),
  .case-row:nth-child(even) .cr-media .t:nth-child(n){grid-column:auto;grid-row:auto;aspect-ratio:16/10}
  .cr-media .t:nth-child(1),
  .case-row:nth-child(even) .cr-media .t:nth-child(1){grid-column:1/3}
  .cr-num{font-size:72px;top:-.15em}
  .cr-title{font-size:var(--t5)}
  .cr-desc{font-size:var(--t3)}
  .tcard{grid-column:1/-1;border-left:0;padding-left:0;padding-right:0;border-top:1px solid var(--border-soft)}
  .tcard:first-child{border-top:0}
  .tcard:nth-child(2){padding-left:0;border-left:0}
  .contact-body{grid-template-columns:1fr}
  .orbit{--ow:clamp(38px,12vw,54px);height:clamp(400px,110vw,520px)}
  .orbit-core{max-width:22ch;padding:var(--s1)}
  .orbit-h{font-size:var(--t3)}
  .lrow{padding:18px 0}
  .rl-row{padding:20px 0;row-gap:10px}
  /* .rl-row>* is pinned to row one on wide screens so the date rail and the
     bullets sit beside each other; stacked, that pin lays all three on top of
     one another, so it comes off here */
  .rl-row>*{grid-row:auto}
  .rl-lead,.rl-pts,.rl-rng{grid-column:1/-1;padding-right:0;max-width:none}
  .rl-rng{justify-self:start;text-align:left;padding-top:0;margin-top:4px}
  .l-yr{grid-column:1/11}
  .l-main{grid-column:1/-1;margin-top:10px}
  .l-out{grid-column:1/-1;margin-top:8px}
  .lrow .arw{grid-column:11/13;grid-row:1;margin-top:14px}
  .contact-mark{width:min(64vw,220px);justify-self:center;margin-top:var(--s3)}
  /* brand mosaics match the galleries on phones: one column at the
     images' own aspect ratios, nothing cropped */
  .mosaic:not(.pin):not(.arch){display:flex;flex-direction:column;gap:10px}
  .mosaic:not(.pin):not(.arch) li{grid-column:auto;grid-row:auto}
  .mosaic:not(.pin):not(.arch) a{height:auto}
  .mosaic:not(.pin):not(.arch) img{position:static;width:100%;height:auto;object-fit:unset}
  .lb figure{padding:64px 8px 24px}
  .lb .lb-prev{left:8px;top:auto;bottom:12px;transform:none}
  .lb .lb-next{right:8px;top:auto;bottom:12px;transform:none}
  .lb img{max-height:calc(100vh - 150px)}
  .proj-row{grid-template-columns:repeat(2,minmax(0,1fr))}
  /* an index list, because a 4:3 screenshot two to a phone screen is mush;
     the imagery lives in the lightbox a tap away */
  .arch-grid{display:block;margin-top:clamp(28px,7vw,40px);border-top:1px solid var(--border-soft)}
  .arch-cell{display:grid;grid-template-columns:104px 1fr;column-gap:16px;align-items:start;
    padding:16px 0;border-bottom:1px solid var(--border-soft);--tt:16px}
  .mosaic.arch{aspect-ratio:4/3}
  .mosaic.arch a,.mosaic.arch .arch-sizzle{border-radius:calc(var(--radius) - 2px)}
  .arch-meta{margin-top:0;padding-top:0;border-top:0;align-content:start}
  .arch-meta::after{display:none}
  .arch-meta h3{margin-top:5px}
  .highlights,.projects,.case-gallery,.caps{grid-template-columns:1fr}
  .cv__row{flex-direction:column;align-items:flex-start;gap:4px}
  .cv__note{white-space:normal}
  .case-figure.wide{grid-column:auto}
  .hl::before,.project::before{display:none}
  .hl:nth-child(n+2){border-top:1px solid var(--border-soft)}
  .project:nth-child(n+2){border-top:1px solid var(--border-soft);padding-top:var(--s4)}
}
@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition-duration:.01ms!important;opacity:1;transform:none}.hero-name .ln>span{transform:none}.hero-reel,.hero-sub,.hero-cta,.hero-dots,.hero-ghost,.hero-image{opacity:1!important;transform:none!important}.hero-image::after{clip-path:none}}

/* ============================================================
   SCROLL REVEALS
   One vocabulary, applied by weight: headings travel furthest,
   body copy less, meta least, so a section resolves in order of
   importance rather than arriving all at once. Everything is a
   scroll-driven CSS animation on opacity and translate only, so it
   runs on the compositor and costs no script. Elements that carry a
   hover transform are faded rather than moved, so the reveal never
   clobbers their resting transform. Wrapped in @supports, so where
   scroll timelines are unavailable content simply renders.
   ============================================================ */
@keyframes revL{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:none}}
@keyframes revM{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
@keyframes revS{from{opacity:0;transform:translateY(7px)}to{opacity:1;transform:none}}
@keyframes revFade{from{opacity:0}to{opacity:1}}
@keyframes revGiant{from{opacity:0;transform:translateY(30px)}to{opacity:1;transform:none}}

@supports (animation-timeline:view()){
  .rv-l,.sec-title,.case-title:not(.vh),.contact-h,.orbit-h,.w-lead,.case-quote p,.article__title,.rl .case-lede{
    animation:revL linear both;animation-timeline:view();animation-range:cover 0% cover 24%}
  .rv-m,.sec-note,.sec-statement,.case-sub,.logo-pair.title,.case-result,.case-section>p,.case-section>.caps,
  .case-section>.project-group,.article__standfirst,.article__tldr,.prose>p,.prose>h2,.orbit-core .cta-sm,
  .writing-body>div,.contact-links,.case-next,.case-quote cite,.orbit-k{
    animation:revM linear both;animation-timeline:view();animation-range:cover 0% cover 25%}
  .rv-s,.dm-k{animation:revS linear both;animation-timeline:view();animation-range:cover 0% cover 23%}

  /* the oversized section word drifts up a little further and later, so it
     settles behind the heading rather than with it */
  .giant{animation:revGiant linear both;animation-timeline:view();animation-range:cover 0% cover 30%}

  /* groups stagger by index; a repeating three-step wave for long lists so
     late items never feel delayed */
  .bento-cell,.lead-cell,.signal,.metric,.hl,.cv__row,.rl-row,.arch-cell,.w-list li,
  .mosaic li,.proj-row li,.case-figure,.contact-links a{
    animation:rowIn linear both;animation-timeline:view();animation-range:cover 0% cover 24%}
  .bento-cell:nth-child(2),.lead-cell:nth-child(2),.signal:nth-child(2),.metric:nth-child(2),
  .hl:nth-child(2),.contact-links a:nth-child(2),.mosaic li:nth-child(3n+2),.proj-row li:nth-child(3n+2),
  .arch-cell:nth-child(3n+2),.case-figure:nth-child(3n+2),.cv__row:nth-child(3n+2),.rl-row:nth-child(3n+2),
  .w-list li:nth-child(2){animation-range:cover 3% cover 25%}
  .bento-cell:nth-child(3),.lead-cell:nth-child(3),.signal:nth-child(3),.metric:nth-child(3),
  .hl:nth-child(3),.contact-links a:nth-child(3),.mosaic li:nth-child(3n),.proj-row li:nth-child(3n),
  .arch-cell:nth-child(3n),.case-figure:nth-child(3n),.cv__row:nth-child(3n),.rl-row:nth-child(3n),
  .w-list li:nth-child(3){animation-range:cover 6% cover 27%}
  .bento-cell:nth-child(4),.lead-cell:nth-child(4),.signal:nth-child(4),.metric:nth-child(4),
  .hl:nth-child(4),.contact-links a:nth-child(4){animation-range:cover 9% cover 28%}
  .bento-cell:nth-child(n+5),.lead-cell:nth-child(n+5),.hl:nth-child(n+5),
  .contact-links a:nth-child(n+5){animation-range:cover 12% cover 30%}

  /* the case row copy resolves kicker, title, description, arrow */
  .cr-k{animation:revS linear both;animation-timeline:view();animation-range:cover 0% cover 23%}
  .cr-title{animation:revL linear both;animation-timeline:view();animation-range:cover 2% cover 25%}
  .cr-desc{animation:revM linear both;animation-timeline:view();animation-range:cover 5% cover 27%}
  .cr-go{animation:revS linear both;animation-timeline:view();animation-range:cover 8% cover 27%}
  .cr-num{animation:revFade linear both;animation-timeline:view();animation-range:cover 0% cover 31%}

  /* these carry a hover transform, so they only fade */
  .writing-shot,.contact-mark,.orbit{animation:revFade linear both;animation-timeline:view();animation-range:cover 0% cover 24%}

  /* nothing inside the full-screen slice sheet: it has its own choreography */
  .detail *{animation-timeline:auto}

  /* the archive tile reveals as a whole cell; a reveal on its stacked frames
     would pin them all to opacity 1 and kill the hover crossfade */
  .mosaic.arch li{animation:none}
}


/* ── SHELL NAVIGATION ───────────────────────────────────────────────────────
   The menu is not a layer that arrives on top. It is always there, fixed
   behind the page, and opening it moves the page off it: the whole document
   is one surface that shifts left and scales down as a single transform, so
   the masthead, the content and the buttons travel together rather than each
   animating on their own.

   Three layers, painted bottom to top:
     .sidenav    the menu, fixed, behind everything
     .shell      the page itself, opaque, covering the menu when closed
     .nav-catch  an invisible sheet over the moved page, so a tap on it closes
                 the menu instead of falling through to a link underneath

   The shell must have NO transform at rest. A transform, even an identity
   one, makes it the containing block for its position:fixed descendants,
   which would strand the masthead and the back-to-top button mid-document. */

/* a damped spring, sampled: it arrives, overshoots two percent, and settles.
   A cubic-bezier cannot express the settle, which is the part that reads as
   weight rather than as a slide */
:root{
  /* how far the page travels, and how far it shrinks. The strip it uncovers is
     wider than the travel, because scaling pulls the right edge in as well:
     strip = --nav-w + 50vw * (1 - --nav-scale) */
  --nav-w:min(58vw,280px);
  --nav-scale:.9;
  --nav-gap:22px;
  --nav-r:26px;
  --nav-spring:linear(0, 0.0093 1.7%, 0.0344 3.4%, 0.0719 5.1%, 0.1185 6.8%, 0.1717 8.5%,
    0.4692 16.9%, 0.5801 20.3%, 0.6309 22%, 0.6783 23.7%, 0.7221 25.4%, 0.7622 27.1%,
    0.7986 28.8%, 0.8314 30.5%, 0.8608 32.2%, 0.8868 33.9%, 0.9297 37.3%, 0.9618 40.7%,
    0.9849 44.1%, 1.0064 49.2%, 1.0184 55.9%, 1);
  --nav-ease:cubic-bezier(.32,.72,0,1);
  --nav-t:560ms;
}
@supports (transition-timing-function:linear(0,1)){:root{--nav-ease:var(--nav-spring)}}

.shell{position:relative;z-index:1;background:var(--bg);min-height:100svh;
  transition:transform var(--nav-t) var(--nav-ease),border-radius var(--nav-t) var(--nav-ease)}

/* While the menu is out the page stops being a tall document and becomes a
   pane the size of the screen, showing the same place through a negative
   margin. That is what gives the slab real corners and a real bottom edge: a
   document-tall element carries its rounded corners thousands of pixels off
   screen, so all you see is a square, full-bleed edge with a rounded dim
   layer floating over it. It also bounds the composited surface to one
   screenful instead of the whole page.

   The margin is layout, not a transform, so position:fixed children still
   resolve against the shell, which is now exactly the viewport box, and the
   masthead pins to the top of the slab with nothing to correct.

   .nav-closing holds all of this in place for the flight back, because
   letting the shell drop into flow mid-transition would move the pivot to the
   middle of the document and the page would lurch. */
html.nav-open .shell,html.nav-closing .shell{
  position:fixed;inset:0;min-height:0;overflow:hidden;
  transform-origin:50% 50%;border-radius:var(--nav-r)}
html.nav-open .shell-view,html.nav-closing .shell-view{margin-top:calc(-1 * var(--sy))}
html.nav-closing .shell{border-radius:0}
html.nav-open .shell{transform:translateX(calc(-1 * var(--nav-w))) scale(var(--nav-scale));
  box-shadow:0 30px 70px -22px rgba(0,0,0,.55),
    0 0 0 1px color-mix(in oklab,var(--fg) 20%,transparent)}
html.nav-open,html.nav-closing{overflow:hidden;touch-action:none;overscroll-behavior:none}

/* The page's own bar goes with the page. Leaving the close mark on it put the
   one control you still need underneath the dim layer, which washed it out.
   The mark now lives in that layer instead, above the scrim. */
html.nav-open .masthead,html.nav-closing .masthead,
html.nav-open #toTop,html.nav-closing #toTop,html.nav-open #dock,html.nav-closing #dock,
html.nav-open .margin-line,html.nav-closing .margin-line{visibility:hidden}

/* The bar gets out of the way (v3.js): reading down slides it up out of view,
   any scroll back up slides it back. Only transform animates, so the phone
   rule above that keeps the auto theme's background swap instant still holds;
   this sits after it to win at equal specificity. */
html[data-theme] .masthead{transition:transform 380ms var(--ease)}
.masthead.tucked{transform:translateY(-100%)}
@media(prefers-reduced-motion:reduce){html[data-theme] .masthead{transition:none}}

.sidenav{position:fixed;inset:0;z-index:0;display:flex;flex-direction:column;justify-content:center;
  background:var(--bg);color:var(--fg);visibility:hidden}
html.nav-open .sidenav{visibility:visible}
/* sized to the strip the page uncovers, less a gap at each end, so the type
   never runs under the page's edge however the travel is tuned */
.sidenav-in{margin-left:auto;margin-right:var(--nav-gap);
  width:calc(var(--nav-w) + (50vw * (1 - var(--nav-scale))) - (var(--nav-gap) * 2))}
.sidenav h2{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-caps);text-transform:uppercase;
  color:var(--fg-mid);font-weight:500;margin-bottom:var(--s3)}
.sidenav ul{list-style:none}
.sidenav li+li{margin-top:2px}
.sidenav a{display:block;padding:9px 0;font-size:var(--t4);font-weight:600;letter-spacing:var(--tr-head);
  color:var(--fg);border-bottom:0;transition:color 240ms var(--ease)}
/* An outline around a 29px word is loud and sits oddly on a list this open.
   The item in play is the one at full strength while the rest step back, which
   carries the focus state too, a strong enough change to stand in for a ring.
   The stepping-back is a variable on the list rather than a rule on the links,
   because :has() borrows the specificity of its argument and would otherwise
   outrank the very rule meant to lift one item back out of it. */
.sidenav ul{--ink:var(--fg)}
.sidenav ul:has(a:hover),.sidenav ul:has(a:focus){--ink:color-mix(in oklab,var(--fg) 44%,transparent)}
.sidenav ul a{color:var(--ink)}
.sidenav ul a:hover,.sidenav ul a:focus,.sidenav ul a:active{color:var(--fg);outline:none}
.sidenav-meta a:hover,.sidenav-meta a:focus{outline:none;color:var(--fg)}
.sidenav-meta{margin-top:var(--s4);display:flex;flex-wrap:wrap;gap:6px 18px}
.sidenav-meta a{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-caps);text-transform:uppercase;
  font-weight:500;color:var(--fg-mid);padding:0;transition:color 240ms var(--ease)}
/* the links rise as the page uncovers them, a touch behind the page itself */
.sidenav li,.sidenav-meta{opacity:0;transform:translateY(14px);
  transition:opacity 260ms var(--ease),transform 420ms var(--nav-ease)}
html.nav-open .sidenav li,html.nav-open .sidenav-meta{opacity:1;transform:none}
html.nav-open .sidenav li:nth-child(1){transition-delay:130ms}
html.nav-open .sidenav li:nth-child(2){transition-delay:170ms}
html.nav-open .sidenav li:nth-child(3){transition-delay:210ms}
html.nav-open .sidenav li:nth-child(4){transition-delay:250ms}
html.nav-open .sidenav li:nth-child(n+5){transition-delay:290ms}
html.nav-open .sidenav-meta{transition-delay:330ms}

/* Rides the same transform as the page, so it covers exactly the moved surface
   and never the menu beside it. It absorbs the tap that would otherwise fall
   through to a link the visitor cannot see, dims the page to show it is out of
   play, and is itself the way back. */
.nav-catch{position:fixed;inset:0;z-index:2;cursor:pointer;background:rgba(6,8,14,.26);
  opacity:0;pointer-events:none;border-radius:var(--nav-r);
  transform:translateX(calc(-1 * var(--nav-w))) scale(var(--nav-scale));transform-origin:50% 50%;
  transition:opacity var(--nav-t) var(--nav-ease),transform var(--nav-t) var(--nav-ease)}
html.nav-open .nav-catch{opacity:1;pointer-events:auto}
html:not(.nav-open) .nav-catch{transform:none;border-radius:0}
.nav-x{position:absolute;top:22px;right:22px;width:48px;height:48px;display:grid;place-items:center;
  padding:0;cursor:pointer;border-radius:999px;color:var(--fg);
  background:color-mix(in oklab,var(--bg) 88%,var(--fg));
  border:1px solid color-mix(in oklab,var(--fg) 18%,transparent);
  box-shadow:0 2px 12px rgba(0,0,0,.2);
  opacity:0;transform:scale(.7) rotate(-60deg);
  transition:opacity 220ms var(--ease),transform 420ms var(--nav-ease)}
html.nav-open .nav-x{opacity:1;transform:none;transition-delay:140ms}
.nav-x:active{transform:scale(.94)}
/* the ring belongs to the keyboard; script moving focus back here is not the
   visitor asking for it (see the modality tracking in v3.js) */
.nav-toggle.nav-quiet:focus-visible,.nav-toggle.nav-quiet:focus{outline:none}

/* the hamburger becomes a close mark, on the same clock as everything else.
   The bars are drawn at y=12, 6 and 18 in a 24-unit view box, so the outer two
   are carried to the middle before they turn, or they cross off-centre */
.nav-toggle svg path{transition:transform var(--nav-t) var(--nav-ease),opacity 200ms var(--ease);
  transform-box:view-box;transform-origin:12px 12px}
html.nav-open .nav-toggle svg path:nth-child(1){opacity:0}
html.nav-open .nav-toggle svg path:nth-child(2){transform:translateY(6px) rotate(45deg)}
html.nav-open .nav-toggle svg path:nth-child(3){transform:translateY(-6px) rotate(-45deg)}

/* The blanket reduced-motion rule flattens durations but not delays, and a
   staggered entrance is exactly what it is there to remove. */
@media(prefers-reduced-motion:reduce){
  html.nav-open .sidenav li,html.nav-open .sidenav-meta{transition-delay:0s!important}
}


/* ── ROLLING LABELS ─────────────────────────────────────────────────────────
   A label stacked twice inside a clipped box: hovering slides the second copy
   up into the first one's place. Only a transform moves, so it stays on the
   compositor and never touches the line it sits in.

   The roll inherits the link's own line-height rather than inventing one. That
   matters twice over: the glyphs land exactly where they did before the text
   was wrapped, and the box stays tall enough that the clip does not shear the
   descenders off. Both copies are the same word, so a roll caught mid-state,
   a focus left behind or a transition interrupted, is indistinguishable from
   rest, and there is no wrong state to land in. */
.roll{display:inline-block;overflow:hidden;vertical-align:bottom;height:1lh}
@supports not (height:1lh){.roll{height:1.5em}}
.roll-i{display:block;transition:transform .6s var(--ease-lux)}
.roll-l{display:block;white-space:nowrap}
/* the incoming copy is generated content, so it never joins the document's
   text and a crawler reads the label once */
.roll-l[data-t]::before{content:attr(data-t)}
:is(a,button):hover>.roll>.roll-i,
:is(a,button):focus-visible>.roll>.roll-i{transform:translateY(-50%)}
/* the second copy is decoration; it must never be read out or found in-page */
.roll-l[aria-hidden]{user-select:none}


/* ── VIDEO FACADE ───────────────────────────────────────────────────────────
   A poster and a play button standing in for an embed. The iframe is only
   built on the click, so a page carrying a video costs nothing until someone
   asks for it. A YouTube embed is hundreds of kilobytes and a pile of
   third-party requests, which is not a fair price for a frame most visitors
   scroll past. nocookie, and no request of any kind reaches Google until the
   visitor chooses to play. */
.case-video{position:relative;display:block;width:100%;aspect-ratio:16/9;overflow:hidden;
  border-radius:var(--radius);background:var(--surface-raised);cursor:pointer}
.case-video img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  border-radius:0;aspect-ratio:auto;transition:transform 620ms var(--ease),filter 380ms var(--ease)}
.case-video:hover img,.case-video:focus-within img{transform:scale(1.025);filter:brightness(.78)}
.cv-play{position:absolute;left:50%;top:50%;z-index:1;width:76px;height:76px;padding:0;
  display:grid;place-items:center;cursor:pointer;border-radius:999px;color:#fff;
  background:color-mix(in oklab,#0b0e15 52%,transparent);
  border:1px solid color-mix(in oklab,#fff 34%,transparent);
  -webkit-backdrop-filter:blur(14px) saturate(1.3);backdrop-filter:blur(14px) saturate(1.3);
  box-shadow:0 10px 34px -12px rgba(0,0,0,.7);
  transform:translate(-50%,-50%);
  transition:transform 420ms var(--ease-lux),background-color 260ms var(--ease),border-color 260ms var(--ease)}
.case-video:hover .cv-play{transform:translate(-50%,-50%) scale(1.07);
  background:color-mix(in oklab,#0b0e15 38%,transparent);border-color:color-mix(in oklab,#fff 54%,transparent)}
.cv-play:focus-visible{outline:2px solid #fff;outline-offset:4px}
.case-video iframe,.case-video video{position:absolute;inset:0;width:100%;height:100%;border:0;z-index:2}
.case-video video{background:#000;object-fit:contain}
.case-video.on img,.case-video.on .cv-play{opacity:0;pointer-events:none;transition:opacity 220ms var(--ease)}
@media(max-width:640px){.cv-play{width:62px;height:62px}.cv-play svg{width:22px;height:22px}}


/* the line under the contact headline: it narrows who should get in touch,
   so it reads as considered prose rather than another label */
.contact-note{max-width:44ch;margin-top:var(--s3);font-size:var(--t3);line-height:1.6;color:var(--fg-mid)}
@media(max-width:640px){.contact-note{font-size:var(--t3);margin-top:var(--s2)}}


/* the claim under the About statement, at the hero's positioning-line weight:
   three sentences of display type turned the cell into a wall */
.bento-pos{display:block;font-family:var(--font-body);font-size:var(--t3);font-weight:500;
  line-height:1.6;letter-spacing:0;color:var(--fg-mid);margin-top:20px;max-width:42ch}
@media(max-width:640px){.bento-pos{font-size:var(--t2);margin-top:16px}}


/* ── PANNING BAND ───────────────────────────────────────────────────────────
   A short full-bleed window onto a tall image, which pans as the band crosses
   the viewport so the rest of the picture arrives with the scroll.

   Driven by a view() timeline rather than background-attachment:fixed. Fixed
   attachment is ignored outright on iOS Safari, and where it does work it
   repaints the whole band every frame; a transform on the compositor costs
   nothing and it is the same mechanism the rest of this file already uses.

   The image is sized as a multiple of the window rather than by its own
   aspect, so the travel is the same at every viewport width: 260% tall, so
   160% of the window scrolls past. */
.pan{position:relative;width:100vw;margin-left:calc(50% - 50vw);
  height:clamp(150px,17vw,200px);overflow:hidden;margin-top:var(--s5);
  background:var(--surface-raised)}
.pan img{position:absolute;inset:0 0 auto 0;width:100%;height:260%;
  object-fit:cover;object-position:center;display:block;
  /* the resting frame is the middle of the picture, which is also what a
     browser without scroll timelines and a reader who wants no motion get */
  transform:translateY(-30.7%)}
@keyframes panBand{from{transform:translateY(0)}to{transform:translateY(-61.5%)}}
@supports (animation-timeline:view()){
  /* the band clips, which makes it a scroll container in its own right, so an
     anonymous view() on the image would resolve against that box and never
     advance. The timeline is declared on the band, whose own scrollport is the
     page, and the image reads it by name. */
  .pan{view-timeline:--panv block}
  .pan img{animation:panBand linear both;animation-timeline:--panv;
    animation-range:cover 0% cover 100%}
}
@media (prefers-reduced-motion:reduce){
  .pan img{animation:none}
}

/* ── THE LOOP ───────────────────────────────────────────────────────────────
   A pinned, scrubbed sequence on nothing but scroll timelines. The reader's
   own scroll is the playhead: no library, no rAF loop, no scroll listener,
   and the work runs off the main thread.

   The track is four viewports tall and the stage sticks to the top of it, so
   the section holds still for the length of the track less the stage. That
   window is exactly what a view() timeline calls "contain", which makes the
   pin and the scrub the same range by construction instead of by arithmetic.
   Each beat then takes a slice of it, overlapping its neighbours so one fades
   up as the last fades down.

   Everything that pins or hides is inside the @supports and the motion query,
   so a browser without scroll timelines, and a reader who has asked for less
   motion, get the four beats as an ordinary list with nothing hidden. */
.story{width:100vw;margin-left:calc(50% - 50vw);margin-block:var(--s6);
  background:var(--surface-raised);border-block:1px solid var(--border-soft)}
.story-stage{padding-block:var(--s6)}
.story-grid{display:grid;gap:clamp(28px,5vw,64px);align-items:center}
.story-ring{display:none}
.story-k{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-caps);text-transform:uppercase;
  color:var(--fg-mid);margin:0 0 var(--s3)}
.story-copy{display:block}
.story-steps{list-style:none;margin:0;padding:0;display:grid;gap:var(--s5)}
.ss-n{display:block;font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-caps);color:var(--fg-mid)}
.story-step h3{font-size:var(--t6);line-height:1.04;letter-spacing:var(--tr-head);margin:12px 0 0}
.story-step p{margin-top:14px;max-width:36ch;color:var(--fg-mid);
  font-size:var(--t3);line-height:1.62}
.story-rail{display:none}

@supports (animation-timeline:view()){
 @media (prefers-reduced-motion:no-preference){
  /* the slice is short and the beat lands early inside it, so the swap reads
     as a step rather than a dissolve */
  .story{margin-block:var(--s7);--snap:cubic-bezier(.2,.9,.25,1)}
  .story-track{height:300svh;view-timeline:--loop block}
  .story-stage{position:sticky;top:0;height:100svh;padding:0;
    display:grid;align-content:center;overflow:hidden}
  .story-grid{grid-template-columns:minmax(0,.85fr) minmax(0,1fr)}
  .story-ring{display:block;justify-self:center;width:min(38vh,320px)}
  .story-ring svg{display:block;width:100%;height:auto;overflow:visible}

  .sr-bed{fill:none;stroke:var(--border-soft);stroke-width:1.5}
  /* the arc is drawn from twelve o'clock, so only it is rotated, not the nodes */
  .sr-arc{fill:none;stroke:var(--accent);stroke-width:2.5;stroke-linecap:round;
    stroke-dasharray:528;transform:rotate(-90deg);transform-origin:110px 110px;
    animation:arcDraw linear both;animation-timeline:--loop;
    animation-range:contain 0% contain 100%}
  @keyframes arcDraw{from{stroke-dashoffset:528}to{stroke-dashoffset:0}}

  /* scale rather than r, which is a much newer thing to animate in CSS */
  /* every station stays readable, so the ring reads as a loop of four rather
     than one travelling dot */
  .sr-n{fill:color-mix(in oklab,var(--fg) 26%,transparent);stroke:none;
    transform-box:fill-box;transform-origin:center;
    animation:nodeOn linear both;animation-timeline:--loop;
    animation-range:contain calc(var(--i)*24%) contain calc(var(--i)*24% + 28%)}
  @keyframes nodeOn{
    0%{fill:color-mix(in oklab,var(--fg) 26%,transparent);transform:scale(1);animation-timing-function:var(--snap)}
    9%,87%{fill:var(--accent);transform:scale(1.6)}
    100%{fill:color-mix(in oklab,var(--fg) 26%,transparent);transform:scale(1)}}

  /* the beats share one cell so they cross over each other in place */
  .story-steps{display:grid;gap:0}
  .story-step{grid-area:1/1;animation:beat linear both;animation-timeline:--loop;
    animation-range:contain calc(var(--i)*24%) contain calc(var(--i)*24% + 28%)}
  @keyframes beat{
    0%{opacity:0;transform:translateY(16px);filter:blur(4px);animation-timing-function:var(--snap)}
    9%,87%{opacity:1;transform:none;filter:blur(0)}
    100%{opacity:0;transform:translateY(-14px);filter:blur(4px)}}
  /* the first beat is already up as the section arrives and the last one stays
     up as it leaves, so the stage is never empty at either end */
  .story-step:first-child{animation-name:beatFirst}
  .story-step:last-child{animation-name:beatLast}
  @keyframes beatFirst{
    0%,87%{opacity:1;transform:none;filter:blur(0)}
    100%{opacity:0;transform:translateY(-14px);filter:blur(4px)}}
  @keyframes beatLast{
    0%{opacity:0;transform:translateY(16px);filter:blur(4px);animation-timing-function:var(--snap)}
    9%,100%{opacity:1;transform:none;filter:blur(0)}}
  /* the end stations get the same treatment, but on their own keyframes: the
     prose ones carry an opacity and a slide that would blank a node out */
  .sr-first{animation-name:nodeFirst}
  .sr-last{animation-name:nodeLast}
  @keyframes nodeFirst{
    0%,87%{fill:var(--accent);transform:scale(1.6)}
    100%{fill:color-mix(in oklab,var(--fg) 26%,transparent);transform:scale(1)}}
  @keyframes nodeLast{
    0%{fill:color-mix(in oklab,var(--fg) 26%,transparent);transform:scale(1);animation-timing-function:var(--snap)}
    9%,100%{fill:var(--accent);transform:scale(1.6)}}

  .story-rail{display:block;position:absolute;left:0;right:0;bottom:0;height:2px;
    background:var(--border-soft)}
  .story-fill{display:block;height:100%;background:var(--accent);
    transform-origin:0 50%;animation:railFill linear both;animation-timeline:--loop;
    animation-range:contain 0% contain 100%}
  @keyframes railFill{from{transform:scaleX(0)}to{transform:scaleX(1)}}

  @media(max-width:860px){
    .story-track{height:250svh}
    .story-grid{grid-template-columns:1fr;gap:clamp(20px,4vw,32px);justify-items:start}
    .story-ring{width:min(22vh,170px);justify-self:start}
  }
 }
}

/* ── ROSTER ─────────────────────────────────────────────────────────────────
   A tiered catalogue, not a strip of cards. The six-across .proj-row above is
   built for exactly six uniform thumbnails at 12px; used for groups of one or
   two it leaves most of the measure empty and crushes the copy into a column
   three words wide. These are rows on the twelve, so the columns land in the
   same places whether or not an entry has a picture, and the copy always has
   a readable measure. Entries without art simply leave the media columns
   empty instead of ragging the row. */
.case-section>.tier-k,.case-section>.roster{grid-column:1/-1}
.tier-k{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-caps);
  text-transform:uppercase;color:var(--fg-mid);font-weight:500;
  margin:var(--s6) 0 0;padding-bottom:var(--s2);border-bottom:1px solid var(--border-soft)}
.tier-k+.case-lede,.tier-k+p{margin-top:var(--s4)}
.tier-k+p+.roster{margin-top:var(--s4)}
/* one tier opens with a lead paragraph instead of the rule. The space that
   separates tiers moves up onto that paragraph, so the rule can close on
   the list it heads the way it does in every other tier. */
.case-section>p:not(.case-lede):has(+.tier-k){margin-top:var(--s6)}
.case-section>p:not(.case-lede)+.tier-k{margin-top:var(--s3)}
.roster{list-style:none;margin:0;padding:0}
/* the same twelve the section itself is on, so an entry's copy begins in the
   column every other paragraph on the page begins in */
.roster>li{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));
  column-gap:var(--gutter);align-items:start;
  padding:var(--s4) 0;border-bottom:1px solid var(--border-soft)}
.tier-k+.roster>li:first-child{padding-top:var(--s4)}
.ro-m{grid-column:1/4;display:block;position:relative;aspect-ratio:16/10;overflow:hidden;
  border-radius:var(--radius);background:var(--surface-raised)}
.ro-m img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  transition:transform 520ms var(--ease)}
.ro-m::after{content:"";position:absolute;inset:0;border-radius:inherit;
  box-shadow:inset 0 0 0 1px rgba(128,128,136,.22);pointer-events:none}
.roster>li:hover .ro-m img{transform:scale(1.03)}
/* the copy starts in the same column with or without a picture */
.ro-b{grid-column:4/11}
.ro-b h4{font-family:var(--font-body);font-weight:600;letter-spacing:var(--tr-text);
  font-size:var(--t4);margin:0}
.ro-b p{font-size:var(--t2);line-height:1.62;color:var(--fg-mid);
  margin-top:8px;max-width:54ch}
.ro-a{grid-column:11/13;display:flex;flex-direction:column;align-items:flex-end;
  gap:12px;text-align:right}
.ro-st{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-caps);
  text-transform:uppercase;color:var(--fg-mid);white-space:nowrap}
.ro-l{display:flex;flex-direction:column;align-items:flex-end;gap:8px}
.ro-l a{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);
  text-transform:uppercase;white-space:nowrap;border-bottom:1px solid transparent;
  padding-bottom:2px;transition:color var(--t-hover),border-color var(--t-hover)}
.ro-l a:hover{color:var(--accent);border-bottom-color:var(--accent)}
@media(max-width:860px){
  .roster>li{grid-template-columns:repeat(6,minmax(0,1fr));row-gap:var(--s3);padding:var(--s3) 0}
  .ro-m{grid-column:1/4}
  .ro-b{grid-column:1/-1}
  .ro-a{grid-column:1/-1;flex-direction:row;align-items:center;justify-content:flex-start;
    gap:var(--s3);text-align:left}
  .ro-l{flex-direction:row;align-items:center;gap:var(--s3)}
}

@supports (animation-timeline:view()){
 @media (prefers-reduced-motion:no-preference){
 }
}

/* ── RESUME ─────────────────────────────────────────────────────────────────
   Two things a recruiter does in the first eight seconds: skim the top third
   and look for numbers. So the competencies sit above the history rather than
   below it, the PDF is a button rather than a word in a strip, and every
   figure in the history carries weight instead of sitting flat in a sentence. */
/* The standfirst only uses the left half, so the two actions take the empty
   right of the same twelve and end on the last column. They are set to the
   first line of that standfirst rather than to its box: a line box is taller
   than its glyphs, so matching box tops would leave the buttons sitting high
   of the text they are meant to align with. */
.case-head{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));
  column-gap:var(--gutter);align-items:start}
.case-head .case-sub{grid-column:1/8}
.case-head .res-act{grid-column:9/13;justify-content:flex-end;
  margin-top:calc(clamp(28px,3.2vw,40px) + (1.28em - 1cap) / 2)}
@supports not (height:1cap){.case-head .res-act{margin-top:calc(clamp(28px,3.2vw,40px) + .2em)}}
.res-act{display:flex;flex-wrap:wrap;gap:var(--s2);margin-top:var(--s4)}
@media(max-width:900px){
  .case-head{display:block}
  .case-head .res-act{margin-top:var(--s4);justify-content:flex-start}
}
.btn-solid,.btn-line{display:inline-flex;align-items:center;gap:10px;
  font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-caps);text-transform:uppercase;
  padding:13px 22px;border-radius:var(--r-btn,999px);border:1px solid var(--fg);
  transition:background-color var(--t-hover),color var(--t-hover),border-color var(--t-hover)}
.btn-solid{background:var(--fg);color:var(--on-fg)}
.btn-solid:hover{background:var(--accent);border-color:var(--accent);color:#fff}
.btn-line:hover{border-color:var(--accent);color:var(--accent)}

.comp{display:grid;grid-template-columns:auto minmax(0,1fr);column-gap:var(--s3);
  row-gap:14px;margin:0;align-items:baseline}
.comp dt{font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-caps);
  text-transform:uppercase;color:var(--fg-mid);white-space:nowrap}
.comp dd{margin:0;font-size:var(--t3);line-height:1.6;max-width:72ch}
@media(max-width:640px){
  .comp{grid-template-columns:1fr;row-gap:4px}
  .comp dd{margin-bottom:12px}
}

/* a figure is the thing the eye is hunting for, so it is given the page's
   full ink while the sentence around it stays at reading weight */
.rs strong,.case-result strong{font-weight:600;color:var(--fg)}


/* ============================================================
   PRINT
   A twelve-column grid, and nothing else. No rules, no tinted
   panels: hierarchy comes from the columns and the space between
   them, which is the only way it survives being photocopied.

   Every row is subgrid, so the date rail, the section names and
   the body all sit on the same twelve columns from the masthead
   to the last line. Columns 1–3 carry meta, 4–13 carry content.

   The reveals have to be undone first — they park content at
   opacity 0 waiting on a scroll timeline that never runs on paper.
   ============================================================ */
.print-head{display:none}
@media print{
  *,*::before,*::after{animation:none!important;transition:none!important;
    opacity:1!important;transform:none!important;filter:none!important;
    box-shadow:none!important;background:transparent!important;
    border-color:transparent!important;text-shadow:none!important}
  @page{size:Letter;margin:16mm}
  html,body{background:#fff!important;color:#000!important;font-size:10pt}
  .masthead,.footer,#chatBtn,#themeToggle,#toTop,.chat-panel,.chat-scrim,
  .skip-link,.case-back,.crumbs,.case-next,.res-act,.grid-lines,.particles,
  .case-title,.case-head,.case-ctas,.hero-ghost,.case-lede,.case-bg{display:none!important}
  main,.wrap,.case-article,.case-hero{max-width:none;padding:0;margin:0;display:block}
  a{color:#000;text-decoration:none}
  p,li,dd{font-size:9pt;line-height:1.45;color:#000;max-width:none;text-wrap:pretty}
  img,figure,svg{display:none!important}
  h2,h3{break-after:avoid}

  /* ---- the module -------------------------------------------------
     Twelve columns across the Letter text block, and one vertical
     module, stated once so every rule below lands on the same lines.
     Every space in the document is a multiple of --m; the two-page cut
     halves --m rather than inventing its own numbers.

     The text block is eight of the twelve columns, not nine. Nine gave
     a 74-character measure, which is past the point where the eye
     finds the return reliably; eight gives 66 and leaves column twelve
     as a deliberate margin.

     Only the two blocks that can never straddle a page break are real
     grids — Chromium will not fragment a grid row, so a tall grid
     section leaves half a page blank rather than spilling. Everything
     that can run long is block flow inset to the column-four line,
     with its meta pulled back into columns one to three. -------- */
  html{--pw:183.9mm;--pgap:4mm;--m:4mm;
    --pcol:calc((var(--pw) - 11*var(--pgap))/12);
    --pin:calc(3*var(--pcol) + 3*var(--pgap));
    --band:calc(3*var(--pcol) + 2*var(--pgap));
    --pcut:calc(var(--pcol) + var(--pgap));
    /* one line of band text: 7pt at line-height 1.5. The dates sit on the
       next band baseline after the section name, so this is a measurement
       of the type, not of the spacing — it does not halve with --m. */
    --bandline:10.5pt}

  /* three values of ink, and no more: black for anything read as
     content, a single mid grey for the header's second line, and one
     light grey for every piece of metadata */
  .case-body{display:block}
  .print-head,.signals{
    display:grid!important;grid-template-columns:repeat(12,1fr);column-gap:var(--pgap)}

  /* the name hangs from the top margin; bottom-aligning it to the
     contact block left dead air above the largest thing on the page */
  .print-head{align-items:start;margin:0 0 calc(4*var(--m));padding:0}
  .ph-id{grid-column:1/8}
  .ph-name{font-family:var(--font-display);font-weight:700;font-size:27pt;line-height:.95;
    letter-spacing:-.03em;margin:0 0 calc(var(--m)/2);color:#000}
  .ph-role{font-size:9.5pt;line-height:1.4;margin:0;color:#555}
  .ph-contact{grid-column:9/13;display:grid;grid-template-columns:auto 1fr;
    column-gap:var(--pgap);row-gap:calc(var(--m)/4);
    margin:0;font-family:var(--font-mono);font-size:7pt;letter-spacing:.02em}
  /* scoped to the header so the two-page cut's body type scale, which
     also matches `dd`, cannot reach in and resize half of a pair */
  .print-head .ph-contact dt{font-size:7pt;line-height:1.5;color:#999;
    text-transform:uppercase;letter-spacing:.12em}
  .print-head .ph-contact dd{margin:0;font-size:7pt;line-height:1.5;color:#000;text-align:right}

  .signals{margin:0 0 calc(3*var(--m))}
  .signal{grid-column:span 3;padding:0;display:block}
  .signal+.signal::before{display:none}
  .signal .sn{font-size:17pt;line-height:1;color:#000;letter-spacing:-.03em;
    margin:0 0 calc(var(--m)/2)}
  .signal .sl{font-size:6.5pt;color:#999;letter-spacing:.1em}

  /* Section name in the meta band, body beside it. The label floats so
     it still counts toward the section's height — an absolute label on
     a one-line section would hang off the bottom. */
  .case-section,.case-result{display:flow-root;margin:0 0 calc(3*var(--m));padding:0;break-inside:auto}
  /* one closing line is not worth a page of its own */
  .case-section:last-child{break-before:avoid}
  /* Everything in the band — section name, dates, group names — is 7pt
     mono at line-height 1.5, so the half-leading is identical and the
     baselines actually agree. Matching the box tops is not the same
     thing, and was where this drifted. */
  .case-section__label,.case-result .rk{display:block;float:left;width:var(--band);
    font-family:var(--font-mono);font-size:7pt;line-height:1.5;font-weight:500;letter-spacing:.14em;
    text-transform:uppercase;color:#000;padding:0;margin:0;border:0;break-after:avoid}
  .case-section>:not(.case-section__label),
  .case-result>:not(.rk){margin:0 var(--pcut) 0 var(--pin);max-width:none}

  /* Roles: block flow in columns four to eleven, each range pulled back
     into the meta band. Absolute, not a grid cell — a row that has to
     split over a page break has to stay block flow to do it. */
  .rl{display:block;padding:0;list-style:none}
  /* clears the section label above it — needs the same weight as the
     blanket child rule that sets the column-four inset */
  .case-section>.rl{margin:var(--bandline) var(--pcut) 0 var(--pin)}
  .rl-row{display:block;position:relative;padding:0 0 calc(2*var(--m));margin:0;border:0;break-inside:auto}
  /* the space between entries belongs between them — on the last one it
     just doubles up with the section's own margin */
  .rl-row:last-child{padding-bottom:0}
  /* company and title travel together, and stay with the section label */
  .rl-row:first-child{break-before:avoid}
  .rl-rng{position:absolute;left:calc(-1 * var(--pin));top:0;width:var(--band);
    text-align:left;white-space:normal;
    font-family:var(--font-mono);font-size:7pt;line-height:1.5;letter-spacing:.03em;
    color:#999;padding:0;margin:0}
  .rl-lead{display:block;padding:0;max-width:none;break-inside:avoid;break-after:avoid}
  .rl-co{font-family:var(--font-mono);font-size:7pt;line-height:1.5;letter-spacing:.12em;color:#999;
    text-transform:uppercase;display:block;margin:0 0 calc(var(--m)/4)}
  .rl-title{font-size:10.5pt;line-height:1.25;font-weight:700;display:block;
    letter-spacing:-.015em;color:#000}
  .rl-pts{margin:calc(var(--m)/2) 0 0;padding:0;max-width:none;list-style:none;orphans:2;widows:2}
  /* the screen rules for these two are more specific than `p,li,dd`
     above, so without naming them the body copy prints grey */
  .rl-pts li{margin:0 0 calc(var(--m)/2);break-inside:avoid;color:#000}
  .rl-pts li:last-child{margin-bottom:0}
  .rl-pts li::before,.caps li::before{display:none}

  /* competencies and grouped lists on the same columns */
  .comp{display:grid;grid-template-columns:repeat(8,1fr);column-gap:var(--pgap);
    row-gap:calc(var(--m)/2)}
  .comp dt{grid-column:1/3;font-family:var(--font-mono);font-size:6.5pt;line-height:1.5;
    letter-spacing:.12em;color:#999;text-transform:uppercase}
  .comp dd{grid-column:3/9;font-size:8.5pt;margin:0;max-width:none;color:#000}
  /* a flowed two-column list that straddles a page break fills its two
     columns to different heights, so the next group's label ends up
     pointing at the previous group's overflow. Keep each list whole. */
  .project-group,.caps{break-inside:avoid}
  .project-group{position:relative;margin:0 0 var(--m)}
  .project-group:first-of-type{margin-top:var(--bandline)}
  .project-group__title{position:absolute;left:calc(-1 * var(--pin));top:0;width:var(--band);
    font-family:var(--font-mono);font-size:7pt;line-height:1.5;
    letter-spacing:.12em;text-transform:uppercase;color:#999;margin:0}
  /* two columns of four, because the text block is eight — three never
     divided it, and at the narrower measure it wrapped short phrases
     onto three lines each */
  .caps{display:block!important;columns:2;column-gap:var(--pgap);
    list-style:none;padding:0}
  .caps li{font-size:8.5pt;break-inside:avoid;margin:0 0 calc(var(--m)/4);
    border:0;padding:0;color:#000}
  .project-group .caps{margin:0}
}

/* ------------------------------------------------------------
   The two-page cut. Same markup, same stylesheet: the generator
   adds .resume-short before printing, so the long and short PDFs
   can never drift apart. What goes is depth, not history — the
   earlier roles keep their titles and dates and lose their
   bullets, so twenty-seven years still reads as twenty-seven
   years on two pages.
   ------------------------------------------------------------ */
@media print{
  /* two bullets a role, and only where the role is recent enough to matter */
  .resume-short .rl-pts li:nth-child(n+3){display:none}
  .resume-short [data-sec="google"] .rl-row:nth-child(n+5) .rl-pts,
  .resume-short [data-sec="ventures"] .rl-row:nth-child(n+3) .rl-pts,
  .resume-short [data-sec="earlier"] .rl-pts,
  /* Core competencies already says what Skills says, at a fifth the height */
  .resume-short [data-sec="skills"],
  .resume-short [data-sec="clients"],
  .resume-short [data-sec="next"]{display:none!important}
  /* 1999 to 2014 is a list of names on a two-page cut, not twenty entries */
  .resume-short [data-sec="earlier"] .rl-row{display:inline!important;border:0;padding:0;margin:0}
  .resume-short [data-sec="earlier"] .rl-row::after{content:" · ";color:#999}
  .resume-short [data-sec="earlier"] .rl-row:last-child::after{content:""}
  .resume-short [data-sec="earlier"] .rl-rng{display:none}
  .resume-short [data-sec="earlier"] .rl-lead{display:inline}
  .resume-short [data-sec="earlier"] .rl-co{display:inline;font-family:var(--font-body);
    font-weight:600;font-size:8.5pt;letter-spacing:0;text-transform:none;color:#000;margin:0}
  .resume-short [data-sec="earlier"] .rl-co::after{content:", "}
  .resume-short [data-sec="earlier"] .rl-title{display:inline;font-size:8.5pt;font-weight:400;color:#555}
  /* no ranges left in the band, so the run starts level with the label */
  .resume-short [data-sec="earlier"] .rl{margin-top:0;line-height:1.5}
  /* on its own line under the section name, the way every other date in
     the band sits — as one run it wrapped and orphaned the year */
  .resume-short [data-sec="earlier"] .case-section__label::after{
    content:"1999–2014";display:block;font-weight:400;color:#999}
  /* skills as dense runs rather than a tall stack */
  .resume-short [data-sec="skills"] .caps{columns:4}
  .resume-short [data-sec="skills"] .caps li{font-size:7.5pt}
  /* Halving the module is the whole of the two-page cut's spacing. Every
     space below is still a multiple of it, so the dense version is the
     same document at half the air rather than a different set of numbers. */
  .resume-short{--m:2mm}
  .resume-short .comp dd{font-size:8pt}
  .resume-short p,.resume-short li,.resume-short dd{line-height:1.28;font-size:8.7pt}
  .resume-short .rl-title{font-size:9.5pt}
  /* a short résumé says where the long one is */
  .resume-short .ph-more{display:block}
}
.ph-more{display:none}


/* ══ MY STORY ════════════════════════════════════════════════════════
   The left of the page is deliberately empty. Everything — the label
   column, the pictures, the prose — is pushed into the right-hand
   columns, so the margin does the work the reference does with it. The
   text inside that block stays left-aligned; ranged-right body copy is
   a poster trick and this is something to read. */
/* ---- LONG-FORM PAGES -------------------------------------------------
   The shared frame behind My Story and How I work. A chapter is a short mono
   label out in the left columns and a display heading over the body in the
   right ones, with columns 4 to 6 left empty - that gap is what gives these
   pages their air, and it is the thing .prose does not do.

   It takes .wrap's measure like every other page rather than running full
   bleed. Full bleed measured the twelve columns against the viewport, which
   matches the masthead only until the viewport passes --maxw; past that the
   whole page slid left of the logo, 82px out at 1700 and 232px at 2000. */
.longform{margin-block:var(--s6);padding-bottom:clamp(80px,10vw,160px)}
.longform .st-head,.longform .chapter,.longform .st-rule,.longform .st-fig,.longform .st-next{
  display:grid;grid-template-columns:repeat(12,minmax(0,1fr));column-gap:var(--gutter)}

.st-k{grid-column:1/4;font-family:var(--font-mono);font-size:var(--t1);line-height:1.6;
  letter-spacing:var(--tr-wide);text-transform:uppercase;color:var(--fg-mid);
  margin-top:clamp(24px,4vw,56px)}
.st-hero{grid-column:5/13;margin:clamp(40px,5vw,84px) 0 0;position:relative}
.st-hero .fr{display:block;border-radius:var(--radius);overflow:clip}
.st-hero img{display:block;width:100%;height:auto}
/* the place name set vertically down the picture's left edge */
.st-vert{position:absolute;left:-.62em;top:6%;z-index:2;writing-mode:vertical-rl;
  font-family:var(--font-display);font-weight:900;font-size:clamp(28px,3.4vw,54px);
  line-height:1;letter-spacing:.06em;text-transform:uppercase;color:#fff;
  mix-blend-mode:difference}

.st-rule{grid-column:1/13;margin-top:clamp(18px,2.4vw,34px);
  padding-top:14px;border-top:1px solid var(--border-soft);
  font-family:var(--font-mono);font-size:var(--t1);letter-spacing:var(--tr-wide);
  text-transform:uppercase;color:var(--fg-mid)}
.st-rule span:first-child{grid-column:1/7}
.st-rule span:last-child{grid-column:10/13;text-align:right}
.st-rule.end{margin-top:clamp(48px,7vw,104px)}

.st-title{grid-column:6/13;font-family:var(--font-display);font-weight:900;
  font-size:clamp(64px,11vw,168px);line-height:.84;letter-spacing:var(--tr-tight);
  margin:clamp(10px,1.4vw,22px) 0 0}
.st-lede{grid-column:6/12;font-size:var(--t4);line-height:1.5;letter-spacing:var(--tr-text);
  margin:clamp(22px,2.6vw,36px) 0 0;max-width:46ch}

.longform .chapter{margin-top:clamp(56px,8vw,128px);align-items:start}
.ch-k{grid-column:1/4;font-family:var(--font-mono);font-size:var(--t1);
  letter-spacing:var(--tr-wide);text-transform:uppercase;color:var(--fg-mid);
  padding-top:.5em}
/* 52ch was cramped once the chapters carried real argument rather than short
   narrative paragraphs; 68ch is the measure the prose layout used before these
   pages shared a component, and the six-column span caps it anyway at the
   widest sizes. */
.ch-b{grid-column:6/12;max-width:68ch}
.ch-b h2{font-family:var(--font-display);font-weight:900;font-size:var(--t6);
  line-height:1.08;letter-spacing:var(--tr-head);margin:0 0 clamp(16px,1.8vw,24px)}
.ch-b p{font-size:var(--t3);line-height:1.62;color:var(--fg-mid);margin:0 0 1em}
.ch-b p:last-child{margin-bottom:0}

.st-fig{margin:clamp(44px,6vw,92px) 0 0}
.st-fig .fr{display:block;border-radius:var(--radius);overflow:clip}
.st-fig img{display:block;width:100%;height:auto}
.st-fig figcaption{font-family:var(--font-display);font-size:var(--t1);letter-spacing:.06em;
  text-transform:uppercase;color:var(--fg-mid);line-height:1.5;padding-top:12px;max-width:40ch}
.st-fig.wide .fr{grid-column:5/13}
.st-fig.wide figcaption{grid-column:5/10}
.st-fig.wide.alt .fr{grid-column:4/12}
.st-fig.wide.alt figcaption{grid-column:4/9}
.st-fig.narrow .fr{grid-column:8/13}
.st-fig.narrow figcaption{grid-column:8/13}
.st-fig.pair .fr.mark{grid-column:5/8;align-self:end}
/* the pair's two frames share a row height set by the square mark, so the
   wider photograph has to cover its frame rather than sit in it and leave a
   strip of page showing under it */
.st-fig.pair .fr:not(.mark){grid-column:8/13}
.st-fig.pair .fr:not(.mark) img{height:100%;object-fit:cover}
.st-fig.pair figcaption{grid-column:5/11;max-width:56ch}

.st-next{margin-top:clamp(40px,5vw,72px)}
.st-next .cta{grid-column:span 3}
.st-next .cta:first-child{grid-column:6/9}

/* ---- THE PICTURES DRIFT INSIDE THEIR FRAMES -------------------------
   Nothing moves down the page. Each photograph is scaled up, then slid
   within a frame that clips it, so the frame stays full and the caption
   stays put. --dy is a percentage of the picture's own height, and the
   scale is what buys the room to travel: at 1.26 there is 13% of the
   frame spare at each end, which caps --dy at (S-1)/2S = 10.3% before
   the scale multiplies it. Everything here sits at 58-87% of that cap,
   which is about twice the old travel and still cannot expose a corner.
   Going further means cropping more of every picture for it.
   The mark is artwork with its own margins and would be cropped, so it
   sits still. Without the scale there is nothing to slide, so it is
   declared here with the animation and not in the base rules. The frames
   crop with overflow:clip, not hidden: hidden makes each frame a scrollport
   and view() then measures the picture against its own frame, where it never
   moves. clip crops the same way, keeps the scaled picture out of the
   document's scrollable width, and leaves the timeline on the page. */
@supports (animation-timeline:view()){
  @media (prefers-reduced-motion:no-preference){
    .longform .st-hero .fr img,.longform .st-fig .fr:not(.mark) img{
      scale:1.26;animation:dmDrift linear both;
      animation-timeline:view();animation-range:cover 0% cover 100%;--dy:8%}
    .longform .st-hero .fr img{--dy:6%}
    .longform .st-fig.narrow .fr img{--dy:9%}
    .longform .st-fig.wide.alt .fr img{--dy:7%}
    .longform .st-fig.pair .fr:not(.mark) img{--dy:8.5%}
    /* on a phone the frames are short enough that a 14% crop costs real
       subject, and the effect is barely legible at that size anyway */
    @media(max-width:640px){
      .longform .st-hero .fr img,.longform .st-fig .fr:not(.mark) img{scale:1;animation:none}
    }
  }
}

@media(max-width:900px){
  .st-k{grid-column:1/13;margin-top:0}
  .st-hero{grid-column:1/13;margin-top:clamp(28px,4vw,48px)}
  .st-title,.st-lede,.ch-b{grid-column:1/13}
  .ch-k{grid-column:1/13;padding:0 0 10px}
  .st-rule span:first-child{grid-column:1/8}
  .st-rule span:last-child{grid-column:8/13}
  .st-fig.wide .fr,.st-fig.wide.alt .fr,.st-fig.narrow .fr,
  .st-fig.wide figcaption,.st-fig.wide.alt figcaption,.st-fig.narrow figcaption,
  .st-fig.pair figcaption{grid-column:1/13}
  .st-fig.pair .fr.mark{grid-column:1/6}
  .st-fig.pair .fr:not(.mark){grid-column:6/13}
  .st-next .cta,.st-next .cta:first-child{grid-column:span 6}
  .st-vert{font-size:clamp(22px,5vw,34px)}
}
@media(max-width:640px){
  .st-fig.pair .fr.mark,.st-fig.pair .fr:not(.mark){grid-column:1/13}
  .st-fig.pair .fr.mark{max-width:220px;margin-bottom:18px}
  .st-next .cta,.st-next .cta:first-child{grid-column:1/13}
}
/* analytics consent, a small card bottom-left that asks once */
.consent{position:fixed;left:16px;bottom:16px;z-index:60;max-width:min(360px,calc(100vw - 32px));padding:16px 18px;border-radius:16px;background:color-mix(in oklab,var(--surface) 88%,transparent);backdrop-filter:blur(12px) saturate(1.2);border:1px solid color-mix(in oklab,var(--fg) 14%,transparent);box-shadow:0 12px 40px rgba(0,0,0,.18);opacity:0;transform:translateY(12px);transition:opacity 240ms var(--ease),transform 240ms var(--ease)}
.consent.on{opacity:1;transform:none}
.consent p{margin:0 0 12px;font-size:var(--t2);line-height:1.5;color:var(--fg)}
.consent__acts{display:flex;gap:10px;align-items:center}
.consent__ok{min-height:36px;padding:0 16px;font-size:var(--t2);font-weight:600;color:var(--fg);cursor:pointer}
.consent__no{min-height:36px;padding:0 6px;background:none;border:0;font-size:var(--t2);font-weight:500;color:var(--fg-mid);cursor:pointer;border-bottom:1px solid color-mix(in oklab,currentColor 38%,transparent)}
.consent__no:hover{color:var(--accent);border-bottom-color:var(--accent)}
@media(prefers-reduced-motion:reduce){.consent{transition:none;transform:none}}
