/* ═══════════════════════════════════════════════════════════════════════════
   case.css — the shared system for the interior project pages.

   ONE stylesheet for all five case studies (01 ARES, 02 Forestry, 03 PACE,
   04 Yuba, 05 Aray) so the pages cannot drift apart. PACE and Aray swapped
   places 2026-07-30; the components below are named for the page they were
   BUILT for, so "added for 03 Aray" still points at the right build history.
   It is a SUBSET of
   hero-concept/site.html's own furniture — same tokens, same divider, same
   topbar, same .deck, same .biglink, same footer marquee — plus the handful
   of components a case study needs that the home page has no use for
   (.backrow, .casehead, .prose, .screens, .seealso).

   Deliberately NOT carried over from site.html:
   - the dither shader and its canvas. Jesse, 2026-07-29: "No, the internal
     pages will not get the dither effect across them." The effect is the home
     page's ARGUMENT; on five interior pages it becomes wallpaper, and it would
     cost five more WebGL contexts on a phone.
   - `body.scrolled`. That class exists only to swap the header backing from
     hero-black to page-navy at the hero handoff. There is no near-black hero
     panel here, so the backing is the ground colour from the first pixel.
   - the vertical grid lines the LIVE case-study pages carry behind the content.
     Jesse rejected background grid lines for this direction and that reasoning
     still holds — they would fight the dot matrix below.

   The dot-matrix ground DOES carry over. It is page furniture rather than the
   dither effect, and without it the interior pages would be flat navy while
   the home page has texture.
   ═══════════════════════════════════════════════════════════════════════════ */

:root{
  --ground:#050F1C;      /* page ground */
  --panel:#010609;       /* deck / frame fill */
  --rule:#0d2135;        /* hairline */
  --blue:#0E8FFF; --blue-soft:#3D9BFF; --hot:#FFFFFF;
  --text:#dce9f8; --muted:#6f90b8; --lede:#b9cee6;

  --container:1334px;
  --gutter:clamp(20px, 4.2vw, 80px);

  --sans:"Spline Sans", -apple-system, "Helvetica Neue", sans-serif;
  /* 2026-09-05: Spline Sans -> Familjen Grotesk for DISPLAY only, to match
     site.html, which swapped on 2026-09-04. Body copy stays Spline Sans in both.
     Familjen sets about 6.5% narrower at the same size, which is why the display
     tracking on this page is the same -0.022em site.html uses for .sectionhead. */
  --display:"Familjen Grotesk", var(--sans);
  --mono:"IBM Plex Mono", ui-monospace, monospace;

  /* THE LINK ARROW, DRAWN (2026-07-30) — identical value and reasoning to
     site.html's, kept in step by hand because these two stylesheets are
     deliberately separate. It was the character &#8599;, which Spline Sans does
     not contain, so browsers substituted a system glyph: unreachable by
     font-weight, stems about 1 CSS px landing unevenly on the pixel grid
     (measured 21% and 34% partial coverage on the two arms at 2x DPR), and a
     different drawing per OS. As a mask both arms carry the SAME stroke-width
     by construction — at ANY weight, with no hinting to make one arm heavier,
     so a hairline costs nothing in evenness.
     SHAPE: butt caps, miter join, LONG TAIL (head arms ~30% of the shaft).
     v1's round caps at 1.75 were rejected for reading as a UI icon rather than
     a drafted mark. v3 lengthened the shaft +18% and the head arms +1.06 CSS px
     on Jesse's call, WITHOUT touching the weight — stroke rendering depends on
     stroke-width and the viewBox→box scale only, never on how far the path
     travels. ONE KNOB: the stroke-width; Jesse picked 1.15. */
  --arw:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%23000' stroke-width='1.15' stroke-linecap='butt' stroke-linejoin='miter'%3E%3Cpath d='M0.8 11.2 11.2 0.8'/%3E%3Cpath d='M6.8 0.8h4.4v4.4'/%3E%3C/svg%3E");
}
/* ⚠️ SCOPED, never a blanket `.arw`: `.backlink .arw` on these pages is a LEFT
   arrow (&#8592;) that shares the class name, and `.marqtrack i` is a period.
   Masking either would erase it. Only the two ↗ contexts are listed. */
.navpanel a i, .biglink i{
  background-color:currentColor;
  -webkit-mask:var(--arw) no-repeat center / contain;
          mask:var(--arw) no-repeat center / contain;
}
* { margin:0; padding:0; box-sizing:border-box; }
html{ scroll-behavior:smooth; }
body{
  background:var(--ground); color:var(--text);
  font-family:var(--sans); -webkit-font-smoothing:antialiased;
  overflow-x:hidden;
}
/* Values copied verbatim from site.html, including the calibration: the grain
   has to stay quieter than the divider hairline or the registration marks
   dissolve into the wallpaper. */
body::before{
  content:""; position:fixed; inset:0; z-index:0; pointer-events:none;
  background-image:
    radial-gradient(rgba(125,182,255,0.075) 1px, transparent 1px),
    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='0.82' numOctaves='3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.07'/%3E%3C/svg%3E");
  background-size:34px 34px, 160px 160px;
  opacity:0.55;
  -webkit-mask-image:radial-gradient(125% 95% at 50% 38%, #000 30%, transparent 100%);
          mask-image:radial-gradient(125% 95% at 50% 38%, #000 30%, transparent 100%);
}
section{ position:relative; z-index:1; }
.wrap{ width:min(var(--container), calc(100% - 2*var(--gutter))); margin-inline:auto; }

/* ── the divider ─────────────────────────────────────────────────────────
   A rule running edge to edge with a 1px vertical tick at each end — the
   registration mark from site.html, drawn the same way for the same reasons
   (the tick is DRAWN, not a typeset "+", so its weight matches the rule by
   construction; overflow:visible defeats Chrome's `hr{overflow:hidden}`). */
.divider{
  position:relative; border:0; height:1px; margin:0;
  overflow:visible; background:var(--rule);
}
.divider::before, .divider::after{
  content:""; position:absolute; top:-5px;
  width:1px; height:11px; background:var(--rule);
}
.divider::before{ left:0;  transform:translateX(-50%); }
.divider::after { right:0; transform:translateX( 50%); }

/* ── top bar ─────────────────────────────────────────────────────────────
   Identical to site.html's, with two changes forced by the context:
   1. the backing is --ground, not --panel. There is no black hero here.
   2. the nav links are absolute paths back to the home page's sections.
   The long eased tail is kept — its second job is holding the nav legible
   over content passing beneath it, and a short hard ramp is what reads as a
   "band". */
/* ── THE NAV IS A FLOATING FROSTED ISLAND ────────────────────────────────
   Ported from site.html 2026-09-05, verbatim, so a visitor moving from the home
   page into a case study does not walk into a different site. It was a
   full-width gradient band here until now — the treatment site.html replaced on
   2026-09-03. The markup already matches (.topbar > .wrap > .brand + .nav), so
   this is a stylesheet change only.

   Jesse's reasoning for the island, kept with the code it governs: the ground is
   one continuous field, and a full-width bar draws a horizontal line straight
   across it. An island lets the field run edge to edge behind it, gives the
   frosted glass a SHAPE (blur with no boundary reads as a wash; blur with an
   edge reads as a pane in front of something), and joins the family the "Let's
   Talk" pill already started. */
.topbar{
  position:fixed; top:0; left:0; right:0; z-index:6;
  --float:clamp(8px, 1.1vh, 12px);
  /* the island overhangs the text column by ~7% at wide widths — chrome belongs
     to the window, not to the document. Below ~1430px it just follows the gutter. */
  --navwide:min(calc(var(--container) + 96px), calc(100vw - 2 * var(--gutter)));
  padding-top:var(--float);
  padding-bottom:0;
  background:transparent;
}
/* ⚠️ THE PANE LIVES ON .topbar::before, NOT ON .wrap — .wrap is positioned with a
   z-index, so it forms its own stacking context and backdrop-filter samples the
   backdrop of the nearest such ancestor, which is then empty. */
.topbar .wrap{
  position:relative; z-index:1;
  width:min(var(--navwide), calc(100% - 2 * var(--gutter)));
  display:flex; align-items:center; justify-content:space-between; gap:20px;
  /* Asymmetric on purpose: the left holds a TEXT wordmark, whose glyphs have
     their own side bearing; the right holds a bordered PILL, whose edge is a
     hard line that reads further out than it measures. */
  padding:9px 20px 9px 30px;
}
.topbar::before{
  content:""; position:absolute; pointer-events:none;
  top:var(--float); bottom:0;
  left:max(var(--gutter), calc((100vw - var(--navwide)) / 2));
  right:max(var(--gutter), calc((100vw - var(--navwide)) / 2));
  border-radius:999px;
  background:
    linear-gradient(rgba(255,255,255,0.055), rgba(255,255,255,0.014)),
    rgba(4,12,22,0.44);
  -webkit-backdrop-filter:blur(30px) saturate(1.45);
          backdrop-filter:blur(30px) saturate(1.45);
  border:1px solid rgba(255,255,255,0.055);
  box-shadow:inset 0 1px 0 rgba(255,255,255,0.05), 0 18px 44px rgba(0,0,0,0.34);
  opacity:0; transition:opacity .35s linear;
}
body.navscrim .topbar::before{ opacity:1; }
/* Once the cover has scrolled past, the near-black tint sits on the navy page and
   reads as a dark object rather than glass, so it switches to the page's colour. */
body.scrolled .topbar::before{
  background:
    linear-gradient(rgba(255,255,255,0.05), rgba(255,255,255,0.012)),
    rgba(8,20,34,0.42);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))){
  .topbar::before{ background:rgba(1,6,9,0.92); }
  body.scrolled .topbar::before{ background:rgba(5,15,28,0.92); }
}
.brand{
  font-size:clamp(17px, 1.3vw, 24px); font-weight:400; letter-spacing:-0.012em;
  color:var(--text); text-decoration:none; white-space:nowrap; transition:color .25s;
}
.brand b{ font-weight:700; color:var(--hot); }
.brand .dot{ color:var(--blue); }
.brand:hover{ color:var(--hot); }
.nav{ display:flex; align-items:center; gap:clamp(16px, 2.1vw, 36px); }
.nav a{
  font-family:var(--mono); font-size:11.5px; font-weight:600; letter-spacing:0.16em;
  text-transform:uppercase; color:var(--text); text-decoration:none; transition:color .25s;
  white-space:nowrap;
}
.nav a:hover{ color:var(--blue); }
.nav .btn{
  color:var(--hot); border:1px solid rgba(14,143,255,0.45); border-radius:999px;
  padding:9px 17px; transition:background .25s, border-color .25s, color .25s;
}
.nav .btn:hover{ background:var(--blue); border-color:var(--blue); color:#02080f; }

/* MENU — the word plus a two-rule mark. The word carries the meaning, the
   mark carries the affordance and the state. TWO rules, not three: three IS
   the hamburger, and three cannot fold into an X without hiding the middle
   one. (Locked 2026-07-29, same as site.html.) */
.navtoggle{
  display:none; align-items:center; gap:9px;
  background:none; border:0; padding:9px 2px; cursor:pointer;
  font-family:var(--mono); font-size:11.5px; font-weight:600; letter-spacing:0.16em;
  text-transform:uppercase; color:var(--text);
}
.navtoggle:hover{ color:var(--blue); }
.navmark{ position:relative; width:17px; height:11px; flex:0 0 auto; }
.navmark i{
  position:absolute; left:0; right:0; height:1.5px; border-radius:1px;
  background:currentColor; transition:transform .28s cubic-bezier(.2,.6,.2,1);
}
.navmark i:first-child{ top:1px; }
.navmark i:last-child{ bottom:1px; }
.navtoggle[aria-expanded="true"] .navmark i:first-child{ transform:translateY(3.75px) rotate(45deg); }
.navtoggle[aria-expanded="true"] .navmark i:last-child{ transform:translateY(-3.75px) rotate(-45deg); }
.navpanel{
  /* z-index 5: BELOW the header, above every section. At 60 the panel covered
     its own Close button and a phone had no way out of an open menu. */
  position:fixed; inset:0; z-index:5;
  background:var(--ground);
  display:flex; flex-direction:column; justify-content:center;
  padding:0 var(--gutter);
  /* visibility flips at 0s on open (a hidden element cannot take focus) and is
     delayed .3s on close so the fade still completes. */
  opacity:0; visibility:hidden; transition:opacity .3s ease, visibility 0s .3s;
}
.navpanel[data-open]{ opacity:1; visibility:visible; transition:opacity .3s ease, visibility 0s; }
.navpanel a{
  display:flex; align-items:center; justify-content:space-between; gap:20px;
  padding:clamp(16px, 2.4vh, 26px) 2px;
  border-bottom:1px solid var(--rule); text-decoration:none; color:var(--text);
  font-family:var(--display); font-weight:700; letter-spacing:-0.022em; line-height:1.05;
  font-size:clamp(2rem, 11vw, 3.4rem);
}
.navpanel a:first-of-type{ border-top:1px solid var(--rule); }
.navpanel a i{ color:var(--muted); width:.38em; height:.38em; display:inline-block;
  vertical-align:.06em; }
.navpanel a:last-of-type{ color:var(--hot); }
.navpanel a:last-of-type i{ color:var(--blue); }

/* ── shared type ─────────────────────────────────────────────────────────── */
.eyebrow{
  font-family:var(--mono); font-size:clamp(11px, 1.05vw, 20px); font-weight:600;
  letter-spacing:0.18em; text-transform:uppercase; color:var(--blue);
  margin-bottom:clamp(10px, 1.2vw, 22px);
}
.band{ padding-block:clamp(72px, 12vh, 150px); }
.sectionhead{
  font-family:var(--display); font-weight:700; color:var(--hot);
  font-size:clamp(2.2rem, 4.5vw, 86px); line-height:0.95; letter-spacing:-0.028em;
}
.tags{
  display:flex; flex-wrap:wrap; gap:8px 20px;
  font-family:var(--mono); font-size:10.5px; letter-spacing:0.16em;
  text-transform:uppercase; color:var(--muted);
}

/* ── back row ────────────────────────────────────────────────────────────
   The live case-study pages open with a bar: back-link left, CTA right. The
   header above already carries the CTA, so only the BACK half survives —
   repeating "Let's Talk" 40px under the pill that says it would be the third
   ask before any work has been shown.

   It sits inside the topbar's fade rather than under it, so the first thing
   below the fixed header is an exit rather than a headline crashing into the
   nav. */
.casetop{ padding-top:clamp(96px, 16vh, 158px); }
.backrow{ display:flex; align-items:center; }
.backlink{
  display:inline-flex; align-items:center; gap:11px;
  font-family:var(--mono); font-size:11.5px; font-weight:600; letter-spacing:0.16em;
  text-transform:uppercase; color:var(--muted); text-decoration:none;
  transition:color .25s;
}
.backlink .arw{
  display:inline-block; color:var(--blue);
  transition:transform .3s cubic-bezier(.2,.6,.2,1);
}
.backlink:hover{ color:var(--hot); }
.backlink:hover .arw{ transform:translateX(-5px); }

/* ── the title block ─────────────────────────────────────────────────────
   Two columns: title + description left, tags right. Same left/right split as
   .casemeta on the home page, so a visitor arriving from a project block
   recognises the shape they clicked.

   The tags are bottom-aligned to the description's last line. That is the ONE
   place bottom-alignment is safe here: it is a 2-item list against a fixed
   3-line paragraph, not the six variable-length blocks that made align-self:end
   fail on the home page. */
.casehead{ margin-top:clamp(34px, 5vh, 62px); }
.caseidx{
  font-family:var(--mono); font-size:11.5px; letter-spacing:0.14em; color:var(--muted);
  display:block; margin-bottom:clamp(14px, 1.8vw, 24px);
}
.casetitle{
  font-family:var(--display); font-weight:700; color:var(--hot);
  /* 96px, not the home page's 90: this is the page's H1 with nothing competing
     for the space, and a 28-character title over two lines wants the scale. */
  font-size:clamp(2.4rem, 6vw, 96px); line-height:0.94; letter-spacing:-0.03em;
  text-wrap:balance;
}
.casesub{
  display:grid; grid-template-columns:minmax(0,1.1fr) minmax(0,0.62fr);
  column-gap:clamp(24px, 4.4vw, 78px); align-items:end;
  margin-top:clamp(26px, 3.4vw, 48px);
}
.caselede{
  max-width:52ch;
  font-size:clamp(14px, 1.05vw, 20px); line-height:1.55; color:var(--lede);
}
/* Outlined pills, per the live page. This is the ONE place .tags are boxed —
   as the page's own subject label they are a statement of fact rather than a
   footnote to a block above them, and the outline is what separates them from
   the muted mono captions used everywhere else. */
.casesub .tags{ justify-content:flex-end; gap:9px; padding-bottom:0.3em; }
.casesub .tags span{
  border:1px solid var(--rule); border-radius:999px; padding:7px 14px;
  color:var(--lede);
}

/* ── the cover ───────────────────────────────────────────────────────────── */
.cover{ margin-top:clamp(40px, 6vh, 84px); }

/* ── decks ───────────────────────────────────────────────────────────────
   Verbatim from site.html. Every artefact on this project sits in a 16:9
   frame — the exports are 2600x1463, so they fill it with no crop. */
.deck{
  position:relative; overflow:hidden; border-radius:clamp(10px, 1vw, 18px);
  border:1px solid var(--rule); background:var(--panel); aspect-ratio:16/9;
}
.deck::after{
  content:""; position:absolute; inset:0; pointer-events:none; border-radius:inherit;
  box-shadow:inset 0 0 0 1px rgba(255,255,255,0.04);
}
.deck img, .deck video{ display:block; width:100%; height:100%; object-fit:cover; }
/* a full-page export overflows its frame on purpose — JS drives the travel */
.deck[data-scrub] img{ height:auto; will-change:transform; }

/* ── the write-up ────────────────────────────────────────────────────────
   Head left, copy right, on the same column split as the title block. The
   live page stacks these full-width; splitting them gives the section a left
   edge of blue heads reading down the page and keeps the body copy at a
   sane measure instead of a 1334px line. */
.prose{ display:grid; gap:clamp(30px, 5vh, 64px); margin-top:clamp(24px, 3.4vh, 44px); }
/* MEASURED, not guessed. The first pass split this 0.72fr / 1fr, copying the
   home page's .casemeta. Rendered at 1440 that gave the head column 530 px to
   hold the word "Problem", which sets at ~200 px — so 330 px of void opened in
   the middle of the section, and because the copy was capped at 56ch another
   ~300 px sat empty on the right. A band of nothing across the widest part of
   the page.
   The head is a LABEL column, so it should be sized to the label: 0.42fr is
   ~390 px at this container, which fits "Solution" with air and no more. The
   copy column gets the width back, and the measures below are widened to use
   it — a claim that runs 40ch and a paragraph that runs 62ch fill an 860 px
   column instead of floating in it. */
.proseblock{
  display:grid; grid-template-columns:minmax(0,0.42fr) minmax(0,1fr);
  column-gap:clamp(24px, 4.4vw, 78px); align-items:start;
}
.proseblock h2{
  font-family:var(--display); font-weight:700; color:var(--blue);
  font-size:clamp(1.9rem, 3.6vw, 58px); line-height:0.98; letter-spacing:-0.026em;
}
/* the bold one-line statement — the claim, before the paragraph that backs it */
.stmt{
  font-family:var(--display); font-weight:600; color:var(--hot);
  font-size:clamp(17px, 1.6vw, 27px); line-height:1.32; letter-spacing:-0.014em;
  max-width:40ch;
}
.proseblock p + p{
  margin-top:clamp(16px, 1.8vw, 26px);
  font-size:clamp(14px, 1vw, 18px); line-height:1.62; color:var(--lede);
  max-width:62ch; font-family:var(--sans); font-weight:400; letter-spacing:0;
}

/* ── the spec row ────────────────────────────────────────────────────────
   Client / Stack / Timeline / Year. Same mono + blue-bold language as the
   .spec row on the home page's rocket beat, so the facts read as a
   measurement rather than a table. */
.spec{
  margin-top:clamp(40px, 6vh, 78px);
  display:grid; grid-template-columns:repeat(4, minmax(0,1fr));
  gap:clamp(18px, 2.6vw, 44px);
  border-top:1px solid var(--rule); padding-top:clamp(22px, 3vh, 34px);
  font-family:var(--mono); font-size:10.5px; letter-spacing:0.16em; text-transform:uppercase;
}
.spec dt{ color:var(--muted); }
.spec dd{ margin-top:10px; color:var(--blue-soft); font-weight:600; line-height:1.9; }

/* ── the note ────────────────────────────────────────────────────────────
   Jesse's own disclosure that this is an independent redesign concept. It is
   set as a marked aside — a blue left rule and mono lead-in — because burying
   it in body copy is how a disclosure stops working. */
.note{
  margin-top:clamp(34px, 5vh, 60px);
  border-left:1px solid rgba(14,143,255,0.4); padding-left:clamp(16px, 2vw, 26px);
  display:grid; gap:12px; max-width:74ch;
  font-size:clamp(13px, 0.95vw, 16px); line-height:1.6; color:var(--muted);
}
.note b{
  font-family:var(--mono); font-size:10.5px; letter-spacing:0.16em;
  text-transform:uppercase; color:var(--blue-soft); font-weight:600;
}
.note a{ color:var(--lede); text-decoration:none; border-bottom:1px solid rgba(14,143,255,0.5); }
.note a:hover{ color:var(--hot); }

/* ── the sheets ──────────────────────────────────────────────────────────
   REBUILT 2026-07-29 to Jesse's own structure, replacing a 2-up grid of seven
   device mockups. His objection, and he was right: every one of those mockups
   put a different screen inside the SAME forest-floor photograph at the same
   device angle, so the picture never changed even though the content did — it
   read as wallpaper — and it showed none of the mobile design at all.

   A .sheet is a full-height export standing at container width: the whole page
   design, top to bottom. Optional full-size links are beside the labels. The live site
   does and it is the right call here.

   NOT a .deck. .deck is aspect-ratio:16/9 with overflow:hidden, which would
   crop a 7810 px export to its first 750 px. NOT [data-scrub] either — the
   scrub caps travel at span x 0.85, so it would show roughly a fifth of one of
   these and stop. Jesse described scrolling THROUGH the full page, and the only
   honest way to do that is to let it stand at full height.

   COST, stated plainly: four sheets at container width come to ~17,500 px of
   imagery, so this page is about 20,000 px long. That is the price of showing
   complete page designs rather than crops, and it is what his live site
   already asks of a visitor. */
.sheets{ display:grid; gap:clamp(44px, 8vh, 104px); margin-top:clamp(30px, 4.4vh, 56px); }
.sheet{
  position:relative; overflow:hidden; border-radius:clamp(10px, 1vw, 18px);
  border:1px solid var(--rule); background:var(--panel);
}
.sheet::after{   /* same seating as .deck — keeps the artefact in the page */
  content:""; position:absolute; inset:0; pointer-events:none; border-radius:inherit;
  box-shadow:inset 0 0 0 1px rgba(255,255,255,0.04);
}
.sheet img{ display:block; width:100%; height:auto; }

/* ── THE ROLL (2026-09-05) ───────────────────────────────────────────────
   ⚠️ THE SHEET IS IN FLOW AT ITS FULL NATURAL HEIGHT. All 5,764px of the
   homepage screenshot, scrolling 1:1 with the page like any other picture.
   There is no track, no stage, no window and no internal scroll. Four builds
   were rejected for putting it in a fixed-height sticky box and scrolling the
   image through it — Jesse, three times: "it's just bulged out and scrolls
   within a frame" / "why is this in a black frame... it should not be in an
   iframe" / "the whole page should be visible immediately as one single image.
   You have the bottom chopped off."

   The canvas is sticky and one viewport tall ONLY so the GPU has somewhere to
   draw the slice that is currently on screen; margin-bottom cancels the height
   it would otherwise add, so the sheet measures exactly as tall as the image.
   With the warp at rest the result is pixel-identical to the flat <img>.

   Inert until roll.js adds .is-rolling, so a phone, a reduced-motion reader, a
   browser without WebGL2, or a GPU whose MAX_TEXTURE_SIZE cannot hold a 7810px
   image all keep the plain picture. */
.sheet--roll{
  /* no frame and no clip: the strip tapers as it leans, and a rounded rectangle
     around it would leave bright gaps at the corners and re-assert the very box
     this exists to dissolve. */
  border:0; background:transparent; overflow:visible; position:relative;
}
.sheet--roll::after{ content:none; }
/* visibility, NEVER display:none — the <img> is what gives the sheet its
   height, and collapsing it takes the whole page's length with it. */
.sheet--roll.is-rolling img{ visibility:hidden; }
.rollcanvas{
  display:block; height:100vh;
  position:sticky; top:0;
  margin-bottom:-100vh;              /* costs the sheet no height */
  pointer-events:none;
  /* WIDER THAN THE IMAGE, BY DESIGN. The ripple brings material toward the
     camera, which makes the strip grow past its resting width — and a canvas
     that ended exactly at the image's edge clipped that to a straight vertical
     line, so only the inward half of the ripple was ever visible. 4% each side
     is more than the warp needs and still fits inside .wrap's gutter at every
     width; wider than the gutter would hand the document a horizontal
     scrollbar, and overflow-x cannot fix it here without making a scroll
     container and breaking the canvas's own stickiness. */
  width:108%; margin-left:-4%;
}
@media (max-width:899px), (prefers-reduced-motion: reduce){
  .rollcanvas{ display:none; }
  .sheet--roll.is-rolling img{ visibility:visible; }
}

figure.shotfig{ display:block; }
/* The label sits ABOVE its sheet, not below. On a 6000 px tall image a caption
   underneath is a screen and a half away from the thing it names, which is the
   same proximity bug that moved the home page's meta above its artefact. */
.sheetlabel{
  display:flex; align-items:baseline; gap:clamp(10px, 1.4vw, 18px); flex-wrap:wrap;
  margin-bottom:clamp(14px, 1.8vh, 22px);
}
.sheetlabel h3{
  font-family:var(--display); font-weight:700; letter-spacing:-0.02em; line-height:1.05;
  font-size:clamp(1.2rem, 2vw, 34px); color:var(--text);
}
.sheetlabel .kind{
  font-family:var(--mono); font-size:10.5px; letter-spacing:0.16em;
  text-transform:uppercase; color:var(--muted);
}
/* Archive viewing uses the existing sheet system; native disclosures keep
   additional long layouts optional and keyboard accessible without script. */
.artifact-link{ color:var(--blue-soft); font-size:14px; line-height:1.5; text-underline-offset:4px; }
.artifact-link:hover{ color:var(--hot); }
.artifact-link:focus-visible,.archive-page summary:focus-visible{ outline:2px solid var(--blue-soft); outline-offset:5px; }
.sheetlabel .artifact-link{ margin-inline-start:auto; }
.archive-pages{ border-top:1px solid var(--rule); padding-top:32px; }
.archive-pages > h3{ font-family:var(--display); font-size:clamp(1.5rem,2.4vw,2rem); line-height:1.15; color:var(--text); }
.archive-intro{ max-width:68ch; margin:16px 0 28px; font-size:16px; line-height:1.6; color:var(--lede); }
.archive-page{ border-top:1px solid var(--rule); }
.archive-page:last-child{ border-bottom:1px solid var(--rule); }
.archive-page summary{ padding:22px 4px; color:var(--text); font-size:18px; font-weight:600; line-height:1.4; cursor:pointer; }
.archive-page summary::marker{ color:var(--blue-soft); }
.archive-page-content{ padding-bottom:32px; }
.artifact-actions{ margin-bottom:18px; }

/* The per-artefact captions were CUT from the home page because they described
   the mechanic ("plays through as you scroll") — narration of something the
   page already demonstrates. These are different: they NAME the page and the
   viewport, which is the only way a visitor knows whether they are looking at
   the homepage or the Forest Technician page, desktop or mobile. Information,
   not narration. */

/* ── pairs (added for 03 Aray) ───────────────────────────────────────────
   Two artefacts side by side, EQUAL HEIGHT BY CONSTRUCTION. Jesse has killed a
   height stagger twice, so this does not leave it to luck in two places:

   1. THE SHEETS. Each pair sets one shared --ar on the row and both frames take
      it, with object-fit:cover inside. The two assets in a pair are chosen to
      be within ~2% of each other, so cover trims a couple of pixels of mockup
      background rather than re-cropping anything (measured per pair in the
      page's markup).
   2. THE LABELS. `subgrid` puts both .sheetlabels in ONE shared row and both
      .sheets in another, so even if one label wrapped to two lines the two
      frames would still start at the same y. Without it a longer label on the
      left would push its own sheet down and re-create the stagger one level up
      from where it was fixed. align-self:end sits a short label on the same
      baseline as a tall one. row-gap:0 because .sheetlabel's own margin-bottom
      is already the spacing — the parent's gap would double it. */
.pairs{
  display:grid; grid-template-columns:repeat(2, minmax(0,1fr));
  grid-template-rows:auto auto;
  gap:clamp(18px, 2.6vw, 38px);
}
.pairs .shotfig{ display:grid; grid-row:1 / -1; grid-template-rows:subgrid; row-gap:0; }
.pairs .sheetlabel{ align-self:end; }
.pairs .sheet{ aspect-ratio:var(--ar); }
.pairs .sheet img{ height:100%; object-fit:cover; }

/* ── the phone grid (added for 03 Aray) ──────────────────────────────────
   Twelve mobile page sections as TWELVE SEPARATE FILES, all exactly 700x1456.
   Forestry's mobile artwork arrived pre-composed as grids, so nothing had to be
   decided; this needed a real layout call.

   FOUR across, three rows. At the 1334 container that is ~314 px a screen —
   81% of a real phone, which is where the headings, the orange sub-heads and
   the icon rows still read. Six across drops to ~200 and dies; three across
   makes each screen LARGER than a phone, which misrepresents the design.

   No device bezels. Adding a phone frame to a flat export is inventing chrome,
   and it is the exact move that made Forestry's mockup grid read as wallpaper.
   Nothing repeats here anyway — the twelve screens run dark purple, light,
   product photography and the open MENU overlay, so the artwork carries its own
   variety. No aspect-ratio and no cover needed either: identical source
   dimensions means height:auto already gives every row the same height. */
.phones{
  display:grid; grid-template-columns:repeat(4, minmax(0,1fr));
  gap:clamp(12px, 1.8vw, 26px);
}

/* ── see also ────────────────────────────────────────────────────────────── */
.seealso{ display:grid; grid-template-columns:repeat(4, minmax(0,1fr)); gap:clamp(20px, 2.4vw, 38px);
  margin-top:clamp(30px, 4.4vh, 56px); }
.seecard{ text-decoration:none; color:inherit; display:block; }
.seecard .deck{ aspect-ratio:4/3; }
/* object-position:top so the tall full-page exports show their header rather
   than a crop of the middle of somebody's homepage. */
.seecard .deck img{ object-position:top; transition:transform .6s cubic-bezier(.2,.6,.2,1); }
.seecard:hover .deck img{ transform:scale(1.03); }
.seecard h3{
  margin-top:16px;
  font-family:var(--display); font-weight:700; letter-spacing:-0.018em; line-height:1.1;
  font-size:clamp(1rem, 1.35vw, 22px); color:var(--text); transition:color .25s;
}
.seecard:hover h3{ color:var(--hot); }
.seecard p{
  margin-top:9px; font-size:clamp(12px, 0.85vw, 14.5px); line-height:1.55; color:var(--muted);
}

/* ── contact + footer ────────────────────────────────────────────────────
   Verbatim from site.html: the work keeps moving past while the ask stays
   still. Same markup, same two-copies-of-five track. */
.contactgrid{
  margin-top:clamp(44px, 7vh, 88px);
  display:grid; grid-template-columns:minmax(0,0.42fr) minmax(0,1fr);
  gap:clamp(28px, 5vw, 92px); align-items:center;
}
.showcol{
  height:clamp(360px, 52vh, 560px); overflow:hidden;
  -webkit-mask-image:linear-gradient(180deg, transparent, #000 14%, #000 86%, transparent);
          mask-image:linear-gradient(180deg, transparent, #000 14%, #000 86%, transparent);
}
.showtrack{ display:flex; flex-direction:column; animation:showScroll 50s linear infinite; will-change:transform; }
.showtrack img{ width:100%; display:block; border-radius:10px; border:1px solid var(--rule); margin-bottom:1.2rem; }
@keyframes showScroll{ to{ transform:translateY(calc(-100% / 3)); } }   /* three copies of the set, 2026-10-23 */
@keyframes showScrollX{ to{ transform:translateX(calc(-100% / 3)); } }

.biglinks{ display:flex; flex-direction:column; }
.biglink{
  display:flex; align-items:center; justify-content:space-between; gap:20px;
  padding:clamp(20px, 2.6vh, 36px) 4px;
  border-bottom:1px solid var(--rule); text-decoration:none; color:var(--text);
  font-family:var(--display); font-weight:700; letter-spacing:-0.022em; line-height:1.05;
  font-size:clamp(1.9rem, 4.2vw, 68px); transition:color .25s;
}
.biglink:first-child{ border-top:1px solid var(--rule); }
.biglink i{ color:var(--muted); width:.44em; height:.44em; display:inline-block;
  vertical-align:.04em; transition:transform .35s cubic-bezier(.2,.6,.2,1), color .25s; }
.biglink:hover{ color:var(--hot); }
.biglink:hover i{ color:var(--blue); transform:translate(6px,-6px); }

.footmarquee{ overflow:hidden; padding-block:clamp(18px, 3vh, 40px); position:relative; z-index:1; }
.marqtrack{
  display:flex; width:max-content; white-space:nowrap;
  font-family:var(--display); font-weight:700;
  font-size:clamp(3rem, 8vw, 6.5rem); letter-spacing:-0.03em;
  color:rgba(255,255,255,0.06);
  /* SIX identical spans and a -50% travel: the loop is seamless only because
     the track is exactly two copies of the repeating unit. Change the span
     count and this breaks. */
  animation:marq 30s linear infinite;
}
.marqtrack i{ font-style:normal; color:rgba(14,143,255,0.35); }
@keyframes marq{ to{ transform:translateX(-50%); } }
footer{
  padding-block:clamp(32px, 5vh, 60px); border-top:1px solid var(--rule);
  display:flex; justify-content:space-between; gap:18px; flex-wrap:wrap;
  font-family:var(--mono); font-size:10.5px; letter-spacing:0.16em;
  text-transform:uppercase; color:var(--muted);
}
footer a{ color:var(--muted); text-decoration:none; transition:color .25s; }
footer a:hover{ color:var(--blue); }

/* ── reveal ──────────────────────────────────────────────────────────────
   14px, not 40: the page is a column of large frames, and a long rise makes
   an image look like it is loading rather than arriving. */
.reveal{ opacity:0; transform:translateY(14px); transition:opacity .7s ease, transform .7s cubic-bezier(.2,.6,.2,1); }
.reveal.in{ opacity:1; transform:none; }

/* ── narrow ─────────────────────────────────────────────────────────────── */
@media (max-width:900px){
  .casesub, .proseblock, .contactgrid{ grid-template-columns:minmax(0,1fr); }
  .casesub{ gap:clamp(18px, 3vh, 28px); align-items:start; }
  .casesub .tags{ justify-content:flex-start; padding-bottom:0; }
  .proseblock{ gap:clamp(16px, 2.4vh, 24px); }
  .stmt{ max-width:none; }
  .spec{ grid-template-columns:repeat(2, minmax(0,1fr)); gap:clamp(20px,4vw,30px) clamp(18px,3vw,28px); }
  .seealso{ grid-template-columns:repeat(2, minmax(0,1fr)); }
  /* A pair stacks. The subgrid rows go with it — one column means one artefact
     per row, so there is no second height to match and no stagger to prevent. */
  .pairs{ grid-template-columns:minmax(0,1fr); grid-template-rows:none; }
  .pairs .shotfig{ display:block; grid-row:auto; }
  .phones{ grid-template-columns:repeat(3, minmax(0,1fr)); }
  /* The tall column becomes a horizontal filmstrip — a restacked column
     repeats the page's own geometry and pushes the three links a screen down.
     Same markup, same images, only the axis changes. --strip is one length
     driving both the frame and the images so the row cannot half-resize. */
  .showcol{
    --strip:clamp(104px, 15vh, 132px);
    height:var(--strip);
    -webkit-mask-image:linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
            mask-image:linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
  }
  .showtrack{ flex-direction:row; animation-name:showScrollX; animation-duration:34s; width:max-content; }
  .showtrack img{ height:var(--strip); width:auto; flex:0 0 auto; margin-bottom:0; margin-right:1.2rem; }
}
@media (max-width:720px){
  .nav a:not(.btn){ display:none; }
  .navtoggle{ display:inline-flex; }   /* flex, not block — the mark sits beside the label */
  .nav .btn{ padding:8px 14px; }
}
@media (max-width:560px){
  /* TWO across on a phone, not one. At 390 that is ~161 px a screen, which is a
     legible INDEX of the mobile system rather than a readable page — but one
     across would put twelve full-size phone screens (~8,400 px) into the page,
     making a secondary artefact longer than the rest of the case study put
     together. Same trade-off Forestry's desktop sheets carry, and the same fix
     is available if Jesse wants it: let the row pan horizontally instead of
     shrinking. NOT built — that changes how the page behaves and it is his
     call. */
  .phones{ grid-template-columns:repeat(2, minmax(0,1fr)); }
}

a:focus-visible{ outline:1px solid var(--blue); outline-offset:4px; }

@media (prefers-reduced-motion: reduce){
  *{ transition:none !important; }
  html{ scroll-behavior:auto; }
  .showtrack, .marqtrack{ animation:none; }
  .navmark i{ transition:none; }
  .navpanel{ transition:none; }
  .reveal{ opacity:1; transform:none; }
  /* nothing travels — drop the viewport and let any export stand full height */
  .deck[data-scrub]{ aspect-ratio:auto; }
  .deck[data-scrub] img{ transform:none !important; }
}

  /* 2026-09-28: a named view-transition element is a backdrop root. Blur on
     its child pseudo-element cannot sample the page. Make the header itself
     the same floating island and apply the filter there. Keep vt-topbar and
     matching case-page geometry so the approved navigation transition holds. */
  .topbar{
    top:var(--float);
    left:max(var(--gutter), calc((100vw - var(--navwide)) / 2));
    right:max(var(--gutter), calc((100vw - var(--navwide)) / 2));
    padding-top:0;border-radius:999px;
    -webkit-backdrop-filter:blur(0px) saturate(1);
            backdrop-filter:blur(0px) saturate(1);
    transition:backdrop-filter .35s linear, -webkit-backdrop-filter .35s linear;
  }
  .topbar .wrap{width:100%;}
  .topbar::before{inset:0;-webkit-backdrop-filter:none;backdrop-filter:none;}
  body.navscrim .topbar{
    -webkit-backdrop-filter:blur(30px) saturate(1.45);
            backdrop-filter:blur(30px) saturate(1.45);
  }
  @media(prefers-reduced-motion:reduce){.topbar{transition:none;}}

/* ── Desktop pages shown side by side (2026-10-23, Shasta) ───────────────
   Full-page exports are ~4.7x taller than wide; four across turns ~25,000px
   of scrolling into one ~1,450px row, with each page one click from full size. */
.pagegrid{ display:grid; grid-template-columns:repeat(4, minmax(0,1fr)); gap:clamp(16px, 2vw, 28px); align-items:start; }
.pagecol-head{ display:flex; justify-content:space-between; align-items:baseline; gap:12px; margin-bottom:12px; }
.pagecol-head h4{ margin:0; font-family:var(--display); font-weight:600; font-size:15px; color:var(--text); }
.pagecol .sheet{ display:block; }
.pagecol .sheet img{ display:block; width:100%; height:auto; }
@media (max-width:1100px){ .pagegrid{ grid-template-columns:repeat(2, minmax(0,1fr)); } }
@media (max-width:600px){ .pagegrid{ grid-template-columns:minmax(0,1fr); } }

/* ── Live-site link-out on a case page (2026-10-23, KIN) ─────────────────────
   A prominent pill up with the title, and the cover frame itself is a link. */
.livebtn{ display:inline-flex; align-items:center; gap:10px; justify-self:start; margin-top:clamp(18px,2.4vh,28px);
  padding:12px 22px; border:1px solid rgba(14,143,255,.6); border-radius:999px; background:rgba(14,143,255,.10);
  color:var(--hot); font-family:var(--mono); font-size:12px; letter-spacing:.16em; text-transform:uppercase; text-decoration:none;
  transition:background .25s, border-color .25s, color .25s; }
.livebtn:hover{ background:var(--blue); border-color:var(--blue); color:#02080f; }
.livebtn:focus-visible{ outline:2px solid var(--blue); outline-offset:4px; }
.deck a.deckhit{ position:absolute; inset:0; z-index:1; border-radius:inherit; }

/* 2026-10-23, phone pass: 44px tap targets for the header and the back link on phones/tablets. The wordmark's
   hit area grows through an invisible ::after, so no layout moves. */
@media (max-width:900px), (pointer:coarse){
  .nav .btn, .navtoggle{ min-height:44px; display:inline-flex; align-items:center; box-sizing:border-box; }
  .brand{ position:relative; }
  .brand::after{ content:""; position:absolute; inset:-12px -4px; }
  .backlink{ position:relative; }
  .backlink::after{ content:""; position:absolute; inset:-12px -4px; }
}

/* Three recommendations fill the row when the current project is omitted. */
@media (min-width:901px){
  .seealso:has(> .seecard:nth-child(3):last-child){ grid-template-columns:repeat(3,minmax(0,1fr)); }
}
