/* ── Type. Self-hosted, so the page makes no third-party font request. ────── */
@font-face{font-family:Geist;src:url(fonts/geist-sans.woff2) format('woff2-variations');
  font-weight:100 900;font-display:swap}
@font-face{font-family:'Geist Mono';src:url(fonts/geist-mono.woff2) format('woff2-variations');
  font-weight:100 900;font-display:swap}

/* ── Tokens. Dark is locked: cinematic was the brief, and a light variant
      would undo the composition. One accent, used sparingly. ──────────────── */
:root{
  --bg:#0a0b0d; --bg-2:#101216;
  --ink:#f4f5f7; --ink-2:#9aa2ae; --ink-3:#5e6672;
  --line:#1e2228; --line-2:#282d35;
  --accent:#c9f24a;
  --r:14px;          /* media and panels */
  --r-pill:999px;    /* interactive only */
  --shell:min(1320px,100% - 8vw);
}
*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%;scroll-behavior:smooth}
body{margin:0;background:var(--bg);color:var(--ink);
  font-family:Geist,ui-sans-serif,-apple-system,system-ui,sans-serif;
  font-size:17px;line-height:1.6;-webkit-font-smoothing:antialiased;overflow-x:hidden}
img{display:block;max-width:100%;height:auto}
a{color:inherit;text-decoration:none}
.shell{width:var(--shell);margin-inline:auto}
.mono{font-family:'Geist Mono',ui-monospace,monospace}

/* ── Nav. One line, 64px. ────────────────────────────────────────────────── */
.nav{position:fixed;inset:0 0 auto;height:64px;z-index:50;display:flex;align-items:center;
  backdrop-filter:blur(14px);background:rgb(10 11 13 / .72);
  border-bottom:1px solid transparent;transition:border-color .3s}
.nav.on{border-bottom-color:var(--line);height:56px;background:rgb(10 11 13 / .88)}
.nav{transition:border-color .3s,height .3s cubic-bezier(.16,1,.3,1),background .3s}
.nav .shell{display:flex;align-items:center;gap:22px}
.nav b{font-size:15.5px;font-weight:600;letter-spacing:-.01em;margin-right:auto}
.nav .to{font-size:14.5px;color:var(--ink-2);transition:color .2s}
.nav .to:hover,.nav .to[aria-current]{color:var(--accent)}
.btn{display:inline-block;padding:10px 20px;border-radius:var(--r-pill);
  background:var(--accent);color:#0a0b0d;font-size:14.5px;font-weight:650;white-space:nowrap;
  transition:transform .18s cubic-bezier(.16,1,.3,1),filter .18s}
.btn:hover{filter:brightness(1.08)}
.btn:active{transform:scale(.97)}
.btn.ghost{background:transparent;color:var(--ink);border:1px solid var(--line-2)}
.btn.ghost:hover{border-color:var(--accent);color:var(--accent)}

/* ── Hero. Lines rise out of a clipping mask. ───────────────────────────── */
.hero{min-height:100dvh;display:flex;align-items:flex-end;padding:96px 0 7vh;
  position:relative;overflow:hidden}
.hero.short{min-height:auto;padding:22vh 0 8vh}
.glow{position:absolute;z-index:0;width:70vw;aspect-ratio:1;border-radius:50%;
  top:-28vw;right:-18vw;pointer-events:none;filter:blur(30px);
  background:radial-gradient(circle,rgb(201 242 74 / .10),transparent 62%)}
.hero .shell{position:relative;z-index:1}
h1{margin:0 0 30px;font-size:clamp(38px,8.4vw,124px);line-height:1.02;
  letter-spacing:-.038em;font-weight:620}
.hero.short h1{font-size:clamp(34px,6vw,78px);margin-bottom:22px}
.mask{display:block;overflow:hidden;padding-bottom:.06em}
.mask>span{display:block}
.hero .lede{max-width:46ch;font-size:clamp(16.5px,1.7vw,21px);color:var(--ink-2);margin:0 0 32px}
.hero .lede em{font-style:normal;color:var(--ink)}
.hero .acts{display:flex;flex-wrap:wrap;gap:12px}

/* ── Statement. Large editorial type, one line at a time. ───────────────── */
.say{padding:18vh 0}
.say p{font-size:clamp(23px,3.5vw,46px);line-height:1.24;letter-spacing:-.028em;
  font-weight:520;max-width:22ch;margin:0}
.say .line{display:block;overflow:hidden;padding-bottom:.1em}
.say .line>span{display:block}
.say .after{margin-top:44px;display:grid;gap:26px;max-width:900px;
  grid-template-columns:repeat(auto-fit,minmax(230px,1fr))}
.say .after p{font-size:15.5px;line-height:1.65;font-weight:400;color:var(--ink-2);max-width:38ch}
.say .after strong{color:var(--ink);font-weight:600}

/* ── Pinned panels. One case at a time. Heights are in vh on purpose: a
      pinned panel that overflows cannot be scrolled, so it cannot be read. ── */
.stack{position:relative}
.panel{position:sticky;top:0;min-height:100dvh;display:flex;align-items:center;
  background:var(--bg-2);border-top:1px solid var(--line);padding:80px 0 64px;
  border-radius:20px 20px 0 0}
.panel .grid{display:grid;grid-template-columns:minmax(0,.86fr) minmax(0,1.14fr);
  gap:clamp(28px,5vw,72px);align-items:center}
.panel.wide .grid{grid-template-columns:minmax(0,.66fr) minmax(0,1.34fr)}
.panel h2{margin:0 0 12px;font-size:clamp(24px,2.8vw,38px);line-height:1.1;
  letter-spacing:-.028em;font-weight:600;display:flex;align-items:center;gap:14px}
.panel h2 img{width:clamp(30px,3vw,42px);height:auto;flex:none;border-radius:50%}
.panel .role{font-size:13.5px;color:var(--accent);margin:0 0 12px;font-weight:520}
.panel dl{margin:0;display:grid;gap:13px}
.panel dt{font-size:13.5px;color:var(--ink);font-weight:600;margin-bottom:3px}
.panel dd{margin:0;font-size:14.5px;line-height:1.55;color:var(--ink-2)}
.panel dd b{color:var(--ink);font-weight:600}
.panel .tech{margin-top:16px;padding-top:13px;border-top:1px solid var(--line);
  font-size:12px;color:var(--ink-3);letter-spacing:.02em}
/* The frame shrink-wraps its screenshot instead of being a fixed box the image
   sits inside. A fixed height plus object-fit:contain always letterboxes unless
   the box happens to match the image, and with eleven screenshots at eight
   different ratios it never does. Here the image is capped by column width and
   by a vh ceiling — so a pinned panel still fits the viewport — and the border
   lands exactly on the screenshot's edge. Bars become impossible rather than
   tuned against. */
.frame{border-radius:var(--r);overflow:hidden;border:1px solid var(--line-2);
  background:var(--bg);box-shadow:0 40px 90px -40px rgb(0 0 0 / .9);
  width:fit-content;max-width:100%;margin-inline:auto;will-change:transform}
.frame img{display:block;width:auto;height:auto;max-width:100%}
.panel .frame img{max-height:34vh}
.panel .frame.tall img{max-height:40vh}
.panel.wide .frame img{max-height:50vh}
.panel .pair .frame img{max-height:15vh}
.pair{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:12px;align-items:start}
.cap{margin-top:10px;font-size:12px;color:var(--ink-3);line-height:1.5}

.big{font-size:clamp(46px,6vw,84px);line-height:.95;letter-spacing:-.04em;font-weight:600;
  color:var(--accent);margin:0 0 4px}
.big+span{display:block;font-size:13px;color:var(--ink-3);margin-bottom:24px}

/* ── Horizontal pan. Browsing a set, so it moves sideways. ──────────────── */
.pan{position:relative;border-top:1px solid var(--line);overflow:hidden}
.pan .head{padding:9vh 0 0}
.pan h2{margin:0 0 10px;font-size:clamp(26px,3.4vw,44px);letter-spacing:-.028em;font-weight:600}
.pan .head p{margin:0;max-width:52ch;color:var(--ink-2);font-size:16px}
.track{display:flex;gap:clamp(16px,2.4vw,34px);align-items:flex-start;
  padding:4vh 0 6vh;width:max-content}
.slide{width:min(56vw,560px);flex:none}
/* No forced ratio. The three Ellow captures are all exactly 16/10 today, so this
   passed by luck — recapture any one of them at a different size and it would
   have letterboxed silently. Ratios come from the image everywhere on this page. */
.slide h3{margin:16px 0 4px;font-size:19px;font-weight:600;letter-spacing:-.016em}
.slide a{color:var(--accent);font-size:14px;border-bottom:1px solid transparent}
.slide a:hover{border-bottom-color:currentColor}

/* ── Clusters. Grouped, not a hairline list. ────────────────────────────── */
.more{padding:16vh 0;border-top:1px solid var(--line)}
.more h2{margin:0 0 46px;font-size:clamp(26px,3.4vw,44px);letter-spacing:-.028em;font-weight:600}
.clusters{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:38px}
.cluster h4{margin:0 0 16px;font-size:16px;color:var(--ink);font-weight:600;
  letter-spacing:-.014em;padding-bottom:12px;border-bottom:1px solid var(--line)}
.cluster ul{margin:0;padding:0;list-style:none;display:grid;gap:14px}
.cluster li{font-size:15.5px;line-height:1.5}
.cluster li span{display:block;color:var(--ink-3);font-size:14px;margin-top:2px}

/* ── Group heading. The projects page is no longer one flat list: the first
      group is all one client's internal tooling, and saying so is the context
      that makes the rest of it legible. ──────────────────────────────────── */
.band{padding-bottom:34px;margin-bottom:34px;border-bottom:1px solid var(--line);
  display:grid;grid-template-columns:minmax(0,.9fr) minmax(0,1.1fr);
  gap:clamp(20px,4vw,64px);align-items:start}
.band h2{margin:0;font-size:clamp(24px,3.2vw,40px);line-height:1.08;
  letter-spacing:-.03em;font-weight:620}
.band p{margin:0;color:var(--ink-2);font-size:15.5px;line-height:1.62;max-width:62ch}
@media(max-width:900px){.band{grid-template-columns:1fr}}

/* ── Project grid, used on the projects page. ───────────────────────────── */
.grid-work{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,420px),1fr));
  gap:clamp(20px,2.6vw,34px);padding-bottom:4vh}
.work{background:var(--bg-2);border:1px solid var(--line);border-radius:var(--r);
  overflow:hidden;transition:border-color .3s;transform-style:preserve-3d}
.work:hover{border-color:var(--line-2)}
/* No fixed height. A 26vh band was 32–72% wider than every screenshot in it, so
   object-fit:contain pillarboxed all six — the letterboxing was the frame's
   shape, not the images'. Letting each image set its own height makes bars
   impossible by construction instead of tuning one ratio that suits none of
   them. All six are landscape, so the tallest still only reaches width/1.6. */
.work .shot{border-bottom:1px solid var(--line);background:var(--bg);overflow:hidden}
.work .shot img{width:100%;height:auto;display:block}
.work .body{padding:24px 26px 26px}
.work .role{font-size:13px;color:var(--accent);margin:0 0 8px;font-weight:520}
.work h3{margin:0 0 10px;font-size:21px;letter-spacing:-.02em;font-weight:600}
.work p{margin:0 0 12px;font-size:14.5px;line-height:1.58;color:var(--ink-2)}
.work p:last-of-type{margin-bottom:0}
.work b{color:var(--ink);font-weight:600}
.work .tech{margin-top:16px;padding-top:13px;border-top:1px solid var(--line);
  font-size:11.5px;color:var(--ink-3)}
.work a.open{color:var(--accent);font-size:14px;border-bottom:1px solid transparent}
.work a.open:hover{border-bottom-color:currentColor}
/* .work p is more specific than .big, so the accent numbers need restating here
   or they render as ordinary body text. */
.work p.big{font-size:clamp(40px,4.6vw,64px);line-height:.95;letter-spacing:-.04em;
  font-weight:600;color:var(--accent);margin:0 0 4px}
.work .big+span{display:block;font-size:13px;color:var(--ink-3);margin-bottom:20px}

/* ── Cross-page link ───────────────────────────────────────────────────── */
.jump{padding:14vh 0;border-top:1px solid var(--line)}
.jump a{display:flex;align-items:baseline;gap:18px;flex-wrap:wrap}
.jump h2{margin:0;font-size:clamp(28px,4.6vw,64px);line-height:1.06;letter-spacing:-.034em;
  font-weight:620;transition:color .25s}
.jump a:hover h2{color:var(--accent)}
.jump p{margin:14px 0 0;color:var(--ink-2);max-width:52ch}

/* ── Close ─────────────────────────────────────────────────────────────── */
.end{padding:18vh 0 14vh;border-top:1px solid var(--line)}
.end h2{margin:0 0 22px;font-size:clamp(30px,5.6vw,76px);line-height:1.04;
  letter-spacing:-.036em;font-weight:620;max-width:20ch}
.end p{max-width:46ch;color:var(--ink-2);font-size:17px;margin:0 0 34px}
.end .acts{display:flex;align-items:center;gap:22px;flex-wrap:wrap}
.mailto{font-size:15px;color:var(--ink-2);border-bottom:1px solid var(--line-2);
  padding-bottom:2px;transition:color .2s,border-color .2s}
.mailto:hover{color:var(--accent);border-bottom-color:var(--accent)}
.foot a{color:var(--ink-2);border-bottom:1px solid transparent}
.foot a:hover{color:var(--accent);border-bottom-color:currentColor}
.foot{padding:26px 0 40px;border-top:1px solid var(--line);display:flex;flex-wrap:wrap;
  gap:10px 28px;font-size:13.5px;color:var(--ink-3)}

.js .reveal{opacity:0;transform:translateY(26px)}

/* ═══════════════════════════════════════════════════════════════════════════
   CINEMATIC LAYER

   The page is one continuous piece with chapters, not seven stacked sections.
   Four devices carry that, and they are deliberately global rather than
   per-section, because continuity is what stops "lots of animation" from
   reading as a list of tricks:

     grain + vignette   one atmosphere over everything, so sections share air
     spine              always-visible position in the whole, not in a section
     chapter readout    names where you are, changing as you cross a boundary
     shared easing      every motion on the page uses --e, so it feels authored

   There is no WebGL here. Depth is done with CSS perspective and layered
   parallax, which holds 60fps on a mid-range phone — the device this actually
   gets opened on — where a real 3D scene would not.
   ══════════════════════════════════════════════════════════════════════════ */

:root{
  --e:cubic-bezier(.16,1,.3,1);   /* one easing for the whole page */
  --e-io:cubic-bezier(.6,0,.2,1); /* symmetric, for things that reverse */
}

/* ── Atmosphere. A dark page becomes cinematic through grain and falloff, not
      through more colour. Both are fixed, so they sit over the whole journey
      and never scroll with a section. ─────────────────────────────────────── */
.air{position:fixed;inset:0;z-index:40;pointer-events:none}
.air::before{  /* vignette: corners fall away, centre stays clean */
  content:'';position:absolute;inset:0;
  background:radial-gradient(120% 85% at 50% 42%,transparent 42%,rgb(0 0 0 / .55) 100%)}
.air::after{   /* grain, generated not fetched — no image request */
  content:'';position:absolute;inset:-50%;opacity:.032;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  animation:grain 6s steps(4,end) infinite}
@keyframes grain{
  0%{transform:translate(0,0)}    25%{transform:translate(-3%,2%)}
  50%{transform:translate(2%,-3%)} 75%{transform:translate(-2%,-2%)}
  100%{transform:translate(0,0)}}

/* ── Spine. Position in the whole piece, with a tick per chapter. Hidden on a
      phone, where there is no room and the thumb is already the scrollbar. ── */
.spine{position:fixed;left:22px;top:0;bottom:0;width:1px;z-index:45;
  background:var(--line);pointer-events:none;opacity:0;transition:opacity .5s var(--e)}
.spine.on{opacity:1}
.spine i{position:absolute;left:0;top:0;width:1px;height:0;background:var(--accent);
  box-shadow:0 0 10px 1px rgb(201 242 74 / .45)}
.spine b{position:absolute;left:-2px;width:5px;height:5px;border-radius:50%;
  background:var(--line-2);transform:translateY(-50%);transition:background .35s,transform .35s}
.spine b.on{background:var(--accent);transform:translateY(-50%) scale(1.5)}

/* ── Chapter readout in the nav. Swaps with a short vertical slide, so crossing
      a boundary is legible without a label sitting over the content. ──────── */
.chap{font-family:'Geist Mono',ui-monospace,monospace;font-size:11.5px;
  letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);
  margin-right:auto;padding-left:18px;overflow:hidden;height:1.3em;display:none}
.chap span{display:block}
.nav .shell>b+.chap{display:block}
.nav .pbar{position:absolute;left:0;bottom:-1px;height:1px;width:0;background:var(--accent);
  box-shadow:0 0 8px rgb(201 242 74 / .5)}

/* ── Hero. A floor receding to a horizon, in CSS 3D. This is the only place a
      sense of physical space is wanted: it is the title card. ─────────────── */
.floor{position:absolute;left:-25%;right:-25%;bottom:-6vh;height:62vh;z-index:0;
  pointer-events:none;perspective:280px;perspective-origin:50% 0}
.floor i{position:absolute;inset:0;transform:rotateX(72deg);transform-origin:50% 0;
  background:
    repeating-linear-gradient(90deg,rgb(201 242 74 / .13) 0 1px,transparent 1px 62px),
    repeating-linear-gradient(0deg,rgb(255 255 255 / .07) 0 1px,transparent 1px 62px);
  mask-image:linear-gradient(transparent 4%,#000 55%,transparent 96%);
  -webkit-mask-image:linear-gradient(transparent 4%,#000 55%,transparent 96%)}
h1 .mask>span{will-change:transform}
/* Geist is a variable font, so weight is a continuous axis the pointer can ride.
   Costs one custom property per word and no extra asset. */
h1 .w{display:inline-block;font-variation-settings:'wght' var(--w,620);
  will-change:font-variation-settings}
.btn.mag{will-change:transform}
/* Against the hero, not the shell, and on the right — the shell is bottom-aligned
   so anything anchored to it lands on the buttons. */
.scroll-cue{position:absolute;right:max(4vw,calc((100% - 1320px) / 2));bottom:3.4vh;
  z-index:1;display:flex;align-items:center;gap:10px;
  font-family:'Geist Mono',ui-monospace,monospace;font-size:11px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--ink-3)}
.scroll-cue s{width:44px;height:1px;background:var(--line-2);position:relative;
  overflow:hidden;text-decoration:none}
.scroll-cue s::after{content:'';position:absolute;inset:0;background:var(--accent);
  transform:translateX(-100%);animation:cue 2.4s var(--e-io) infinite}
@keyframes cue{0%{transform:translateX(-100%)}60%,100%{transform:translateX(100%)}}

/* ── Statement. The thesis lights up word by word as it is read. Earned here
      because this section *is* the positioning — nowhere else. ───────────── */
.say .w{display:inline-block;opacity:.22}
.say .line>span{display:block}

/* ── Panels. A chapter number, and media that wipes in rather than fades.
      A wipe stays fully opaque, which is what the ghosting fix requires.

      The upward shadow is what makes the stack legible. Mid-transition you
      necessarily see a slice of the outgoing panel above the incoming one's top
      edge — that is how a slide-over stack works — and with only a hairline
      between them it read as a rendering fault rather than as one card passing
      over another.

      A drop shadow was the obvious move and it did nothing: every panel is the
      same colour as the page, so a black shadow on black is invisible. What
      works is the opposite — a lit leading edge, so the arriving panel catches
      light along its top like a card lifting off the one behind. ─────────── */
.panel{position:sticky;top:0;border-top-color:var(--line-2)}
.panel::before{content:'';position:absolute;inset:0 0 auto;height:1px;z-index:0;
  background:linear-gradient(90deg,transparent,rgb(201 242 74 / .55) 16%,
    rgb(201 242 74 / .55) 84%,transparent)}
.panel::after{content:'';position:absolute;inset:1px 0 auto;height:170px;z-index:0;
  border-radius:20px 20px 0 0;pointer-events:none;
  background:linear-gradient(rgb(255 255 255 / .05),transparent)}
/* Content sits above both, or the bloom washes over the type. */
.panel>.shell,.panel>.no{position:relative;z-index:1}
.panel .no{position:absolute;top:84px;right:max(4vw,calc((100% - 1320px) / 2));
  font-family:'Geist Mono',ui-monospace,monospace;font-size:11.5px;letter-spacing:.12em;
  color:var(--ink-3);z-index:2}
.panel .no em{font-style:normal;color:var(--accent)}
/* The rail is a pseudo-element, which GSAP cannot target. It reads --rail
   instead, and the tween animates that property on the dl itself. */
.panel dl{position:relative;padding-left:20px}
.panel dl::before{content:'';position:absolute;left:0;top:5px;bottom:5px;width:1px;
  transform-origin:top;transform:scaleY(var(--rail,1));
  background:linear-gradient(var(--accent),rgb(201 242 74 / .06))}
.js .panel dl{--rail:0}
.js .panel .frame{clip-path:inset(0 100% 0 0)}

/* ── Pan. An index, and a rail that fills as the set moves past. ─────────── */
.pan .meter{display:flex;align-items:center;gap:14px;margin-top:18px;
  font-family:'Geist Mono',ui-monospace,monospace;font-size:11.5px;letter-spacing:.1em;
  color:var(--ink-3)}
.pan .meter s{flex:1;max-width:180px;height:1px;background:var(--line);
  position:relative;text-decoration:none}
.pan .meter s i{position:absolute;inset:0 auto 0 0;width:0;background:var(--accent)}
.slide{transition:opacity .5s var(--e)}
.slide .frame{transition:transform .5s var(--e),border-color .35s}
.slide:hover .frame{transform:translateY(-8px);border-color:rgb(201 242 74 / .4)}

/* ── Jump. A door that shows what is behind it. The arrow travels, the rule
      draws the page's full width, and hovering fans out three of the actual
      screenshots from the page it leads to — the promise is the work itself,
      not a decorative flourish. ──────────────────────────────────────────── */
.jump{position:relative;overflow:hidden}
.jump a{position:relative;z-index:1}
.jump .arw{display:inline-block;color:var(--accent);transition:transform .45s var(--e)}
.jump a:hover .arw{transform:translateX(14px)}
.jump .rule{height:1px;background:var(--accent);width:0;margin-top:30px;opacity:.5}
/* Anchored to the empty bottom-right quadrant, not the vertical centre. Centred,
   the fan opened straight across the headline and both became unreadable. */
.jump .peek{position:absolute;right:1vw;top:63%;width:min(37vw,470px);height:0;
  pointer-events:none;z-index:0}
.jump .peek img{position:absolute;top:0;width:70%;border-radius:10px;
  border:1px solid var(--line-2);box-shadow:0 30px 70px -30px #000;
  opacity:0;transform:translateY(38px) scale(.94);
  transition:opacity .5s var(--e),transform .6s var(--e)}
.jump .peek img:nth-child(1){right:52%;transform-origin:bottom right}
.jump .peek img:nth-child(2){right:26%}
.jump .peek img:nth-child(3){right:0;transform-origin:bottom left}
/* Staggered by delay rather than by JS: this is hover state, so CSS is enough
   and it costs no scroll-time work at all. */
.jump:hover .peek img{opacity:1}
.jump:hover .peek img:nth-child(1){transform:translateY(-58%) rotate(-7deg) scale(1);transition-delay:0s}
.jump:hover .peek img:nth-child(2){transform:translateY(-50%) rotate(-1deg) scale(1);transition-delay:.06s}
.jump:hover .peek img:nth-child(3){transform:translateY(-42%) rotate(6deg) scale(1);transition-delay:.12s}

/* ── Close. One rule draws, then the type arrives, and the hero's glow comes
      back mirrored — the piece ends where it began, which is the last thing
      holding the whole page together as one journey. ─────────────────────── */
.end{position:relative;overflow:hidden}
.end>.shell{position:relative;z-index:1}
.glow.out{top:auto;right:auto;bottom:-34vw;left:-20vw;width:64vw}
.end .rule{height:1px;background:var(--line-2);width:0;margin-bottom:38px}

@media(max-width:900px){
  /* No hover on a phone, so the fan would never open and the images would be
     three unexplained loads. */
  .jump .peek{display:none}
}

@media(max-width:900px){
  .spine{display:none}
  .chap{display:none !important}
  .floor{height:40vh}
  /* Static here, so it needs the shell's own width — otherwise it sits flush
     against the screen edge while everything else is inside the gutter. */
  .panel .no{position:static;width:var(--shell);margin:0 auto 14px}
}
@media(prefers-reduced-motion:reduce){
  .air::after{animation:none}
  .scroll-cue s::after{animation:none;transform:none}
  /* Anything whose resting state is "hidden until animated" must be visible,
     because with motion off nothing will ever animate it in. */
  .js .panel .frame{clip-path:none}
  .js .panel dl{--rail:1}
  .say .w{opacity:1}
  .jump .rule,.end .rule{width:100%}
}

@media(max-width:900px){
  .panel .grid,.panel.wide .grid,.two .grid{grid-template-columns:1fr}
  /* display:block matters. The panel is a flex row, so taking .no out of
     absolute positioning below made it a flex *sibling* of the content — the
     counter and the whole case study became two half-width columns, with the
     text overflowing off the right edge. Nothing here needs flex: min-height is
     auto on a phone, so there is no free space to centre into. */
  .panel{min-height:auto;position:static;padding:12vh 0;display:block}
  /* Nothing is pinned here, so the vh ceilings that keep a panel inside the
     viewport are not needed and only serve to shrink the screenshots. */
  .panel .frame img,.panel.wide .frame img,
  .panel .frame.tall img,.panel .pair .frame img{max-height:none}
  /* Two desktop screenshots side by side inside 359px are illegible. Full width
     each, so a phone gets fewer things at a readable size rather than more at
     no size at all. */
  .pair{grid-template-columns:1fr}
  .slide{width:82vw}
  /* Bottom-aligned reads as composed on a wide screen. On a tall narrow one it
     put roughly a full screen of nothing above the headline, so it centres. */
  .hero{align-items:center;padding:88px 0 12vh}
  .say p{max-width:none}
  .nav .to{display:none}
  /* .work .shot needs no override any more — it has no fixed height to undo,
     and forcing 16/10 here was what pillarboxed four of the six on a phone. */
}
/* Short laptops. A pinned panel that overflows cannot be scrolled, so on a
   720px-tall screen the type and the media both step down rather than the
   content being cropped. */
@media(min-width:901px) and (max-height:820px){
  /* Measured at 1280x720: the text column, not the media, is what pushes a panel
     past the viewport — two panels were over by 7px and 2px. Top padding still
     clears the 64px nav. */
  .panel{padding:66px 0 26px}
  .panel .no{top:66px}
  .panel h2{font-size:clamp(22px,2.4vw,30px);margin-bottom:9px}
  .panel .role{margin-bottom:9px;font-size:12.5px}
  .panel dl{gap:10px}
  .panel dt{font-size:12.5px}
  .panel dd{font-size:13.5px;line-height:1.5}
  .panel .tech{margin-top:12px;padding-top:10px}
  .panel .frame img{max-height:29vh}
  .panel.wide .frame img{max-height:43vh}
  .panel .frame.tall img{max-height:30vh}
  .panel .pair .frame img{max-height:13vh}
  .cap{font-size:11.5px;margin-top:8px}
}

@media(prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  .js .reveal{opacity:1;transform:none}
  *{animation-duration:.01ms !important;transition-duration:.01ms !important}
}
