/* Final responsive and accessibility layer for the supplied AAH build. */
:root{--focus:#9adede;--nav-h:91px}
.skip-link{position:fixed;left:16px;top:12px;z-index:10000;padding:10px 16px;background:#fff;color:#1a1a1a;font-weight:700;transform:translateY(-150%);transition:transform .16s ease-out}.skip-link:focus{transform:none}
/* Hero → products: ONE continuous charcoal field. No section scoop /
   #121110 ledge. Desktop = copy left + rounded photo plate right.
   Plate uses DashQ top arcs; bottom dissolves (mask) into charcoal so
   the products join is not a knife-edge. */
.hero-sec{
  background:var(--charcoal);
  border-bottom-left-radius:0;
  border-bottom-right-radius:0;
}
/* Soft chapter wash — translucent so coral/teal blooms keep reading
   through the join. Opaque charcoal here was flattening the field into
   a hard rectangular ledge. */
.hero-sec::after{
  content:'';
  position:absolute;left:0;right:0;bottom:0;
  height:clamp(56px,10vh,120px);
  z-index:4;pointer-events:none;
  border-radius:0;
  border:none;
  box-shadow:none;
  background:linear-gradient(
    to bottom,
    transparent 0%,
    rgba(27,26,24,.12) 55%,
    rgba(27,26,24,.42) 100%
  );
}

/* CanGrant's real product colors (pulled from the live app's own CSS
   variables), scoped to just its card/pill — --amber is a shared token
   used elsewhere (winter-intern badges, a team avatar) so it stays as-is
   for those; CanGrant gets its own real brand colors instead of the
   generic lavender/amber placeholders. */
.s-int{background:rgba(240,90,40,.12);color:#F5834F;border:1px solid rgba(240,90,40,.28)}
/* ── Hero: responsive photo panel + dark content field ─────────
   Desktop receives the dedicated 3:2 room photograph while phones keep
   the original portrait photograph. Each source is displayed at its own
   natural ratio without distortion or letterboxing. */
/* Copy on the left, photo on the right. row-reverse rather than reordering
   the markup, so the photo stays first in the DOM (it stacks on top on
   mobile, where the column direction below takes over). */
.hero-sec{
  flex-direction:row-reverse;
  align-items:stretch;
  gap:0;
  /* `overflow-x:hidden` + `overflow-y:visible` is not a legal pair: when
     one axis is not visible, the spec computes the other from `visible`
     to `auto`. This box was resolving to `hidden auto`, so the oversized
     bloom pseudo (inset:-18%) inflated scrollHeight to 986 against a
     791 box and the hero grew its OWN 15px scrollbar — the pale vertical
     strip down the right edge, and a hard clip across the photo bottom.
     `clip` is the one value that lets the cross axis stay `visible`. */
  overflow-x:clip;
  overflow-y:visible;
}
.hero-photo-col{
  position:relative;
  /* Stretch with the row; do NOT fill a 100dvh chapter height — that
     re-coupled plate size to OS chrome and moved the hero→products lip. */
  height:auto;
  align-self:stretch;
  /* Transparent — one charcoal field with .hero-sec blooms. No opaque
     fill that reads as a second panel beside the copy. */
  background:transparent;
  /* Flush to the chapter edges. Soft top radius on the image itself. */
  display:flex;align-items:center;justify-content:center;
  padding:0;
  min-width:0;
  /* Width-locked split — NEVER size this column from 100svh/100vh.
     Mac and Windows report different layout heights for the same window
     (chrome, dock/taskbar, overlay scrollbars), so a height-based flex
     basis made the first-page photo↔copy ratio drift between OSes. */
  flex:0 1 54%;
  width:54%;
  max-width:58%;
  border-radius:0;
  overflow:visible;
  border:none;
  box-shadow:none;
  outline:none;
}
/* Desktop: bleed a hair under the copy column + dissolve that edge so
   the photo↔type join is a shared charcoal field, not a vertical knife. */
@media(min-width:1025px){
  .hero-photo-col{
    margin-left:calc(var(--sheet-radius) * -.55);
    padding-left:calc(var(--sheet-radius) * .55);
    box-sizing:border-box;
  }
  .hero-photo-col::before{
    content:'';
    position:absolute;
    left:0;top:0;bottom:0;
    width:clamp(2.25rem,5.5vw,4.25rem);
    z-index:2;
    pointer-events:none;
    border:none;box-shadow:none;
    background:linear-gradient(
      to right,
      var(--charcoal) 0%,
      rgba(27,26,24,.78) 22%,
      rgba(27,26,24,.32) 58%,
      transparent 100%
    );
  }
}
.hero-picture{
  display:block;
  width:100%;height:auto;min-width:0;min-height:0;
  border:none;box-shadow:none;outline:none;
  background:transparent;
}
/* Width-driven plate — never fill a viewport-tall column. */
.hero-photo-col .hero-photo{
  position:static;inset:auto;
  display:block;
  width:100%;height:auto;
  max-width:100%;max-height:none;
  aspect-ratio:1536 / 1024;
  /* Match the charcoal field — light #e8e6e2 used to flash as a wrong
     strip beside the products top radius. */
  background:transparent;
}
.hero-photo-col .hero-bg{display:none}
.hero-photo{
  /* Rounded plate; no bottom mask (later cascade clears any leftovers). */
  border-radius:var(--sheet-radius);
  object-fit:cover;
  object-position:center center;
  box-shadow:none;
  border:none;
  outline:none;
  pointer-events:none;
  -webkit-mask-image:none;
  mask-image:none;
}
.hero-bg::after{display:none}
.hero-content{
  flex:1 1 42%;
  min-width:0;
  /* Transparent so hero-sec blooms + charcoal read as ONE field. */
  background:transparent;
  justify-content:center;
  /* Top clears the fixed nav; no #main-scroll pad (that was a dead ledge).
     Horizontal padding uses % of the content column, not vw — vw still
     tracks the full viewport including the Windows scrollbar gutter. */
  padding:calc(var(--nav-h) + 12px) clamp(28px,4%,64px) 48px;
  border-radius:0;
  border:none;
  box-shadow:none;
  outline:none;
}
.hero-h1,.hero-eyebrow,.hero-sub{color:#fff}
@media(max-width:1024px){
  .hero-sec{flex-direction:column}
  /* Stacked: full-width photo, no mount padding. Cap height so copy
     still fits the first viewport; cover fills the col without gutters. */
  .hero-photo-col{
    width:100%;height:auto;flex:none;max-width:none;
    align-items:stretch;
    padding:0;
    margin-left:0;
  }
  .hero-photo-col::before{content:none;display:none}
  .hero-photo-col .hero-photo{
    width:100%;height:auto;
    max-width:none;max-height:min(480px,55%);
    aspect-ratio:1536 / 1024;
    object-fit:cover;
    border-radius:var(--sheet-radius) var(--sheet-radius) 0 0;
    -webkit-mask-image:none;
    mask-image:none;
  }
  /* Extra soft shelf wash where the plate meets the copy field. */
  .hero-photo-col::after{
    content:'';
    position:absolute;left:0;right:0;bottom:0;
    height:clamp(40px,10%,88px);
    z-index:2;pointer-events:none;
    border:none;box-shadow:none;
    background:linear-gradient(
      to bottom,
      transparent 0%,
      rgba(27,26,24,.35) 45%,
      var(--charcoal) 100%
    );
  }
  .hero-content{flex:none;background:transparent}
}
/* Sized for the left copy column. */
.hero-h1{font-size:clamp(34px,3.1vw,46px);max-width:560px}
/* Socials sit under the paragraph rather than beside it. */
.hero-row{flex-direction:column;align-items:flex-start;gap:26px}
.hero-sub{max-width:490px}
/* "Mainstreet" carries the brand red wherever it appears, matching the
   footer headline. Italic in the H1, upright in the body copy. */
.hero-h1 em{color:var(--coral)}
.hero-sub em{font-style:normal;font-weight:600;color:var(--coral)}

/* ── Team intro: plain copy, single column ──────────────────────
   Was a heading beside a rotated "paper note" card; the note's gone (no
   more literal sticky-note treatment anywhere on the site), so the intro
   is just the heading plus a plain supporting paragraph underneath. */
.team-intro{margin-bottom:48px}
.team-intro .scroll-h{max-width:620px}
.team-intro-p{max-width:620px;margin-top:14px;font-size:17px;color:var(--muted-ink);line-height:1.7}
.team-founder-note{max-width:720px;margin-top:28px;font-size:16px;color:var(--muted-ink);line-height:1.75}
.team-founder-note strong{color:var(--charcoal)}

/* ── Team portraits ────────────────────────────────────────────
   Supplied portraits are normalized to 720px square crops before they
   reach the page; students without a supplied photo keep the initials
   fallback at the identical size and position. Full-bleed square now
   (was a small 72px circle) -- the card's own border-radius handles the
   corners, so this needs none of its own. */
.big-tav{
  overflow:hidden;
  flex:0 0 auto;
  background-clip:padding-box;
  border-radius:0;
}
.big-tav.has-photo img{
  display:block;
  width:100%;
  height:100%;
  object-fit:cover;
  object-position:center;
}
/* <picture> is inline by default, so as the flex item it would size to its
   content and the img's 100% would resolve against nothing. */
.big-tav.has-photo picture{display:block;width:100%;height:100%}
.pd-shot picture{display:block}

/* ── Reveal on scroll ──────────────────────────────────────────
   Content arrives as it enters view instead of being fully painted the
   moment it exists. Deliberately restrained: a short rise and a fade,
   one easing curve, no slides from the side and no scale — the point is
   that the page feels alive, not that the animation is noticed.

   Only applied where JS has confirmed IntersectionObserver support, via
   the .has-reveal class on <html>. Without that the styles never bind,
   so a failed script leaves everything visible rather than blank. */
.has-reveal [data-reveal]{
  opacity:0;
  transform:translateY(14px);
}
.has-reveal [data-reveal].is-in{
  opacity:1;
  transform:none;
  transition:opacity .62s cubic-bezier(.22,1,.36,1),transform .62s cubic-bezier(.22,1,.36,1);
  transition-delay:var(--reveal-delay,0ms);
}
/* The products intro is the section's hand-off from the hero. Give those
   three lines a slightly longer rise than the supporting grids, then let JS
   stagger them in reading order. */
.has-reveal #products-sec [data-reveal-kind="product-intro"]{
  transform:translateY(24px);
}
.has-reveal #products-sec [data-reveal-kind="product-intro"].is-in{
  transform:none;
  transition-duration:.78s;
}
/* Anything that has already been read should never animate again, and a
   reader who asked for less motion gets none of it. */
@media(prefers-reduced-motion:reduce){
  .has-reveal [data-reveal],
  .has-reveal [data-reveal].is-in{opacity:1;transform:none;transition:none}
}

/* ── Product screenshots ───────────────────────────────────────
   Real captures of the running app, shown at a size where the interface
   is actually legible rather than as thumbnails. Each is a link to the
   full-size file, so a reader who wants to inspect the UI can. */
.pd-shots{display:flex;flex-direction:column;gap:34px}
.pd-shot{margin:0}
.pd-shot a{
  display:block;
  border:1px solid var(--border);
  border-radius:var(--r-md);
  /* The captures are light-UI on a dark page, so they need a seam of
     their own rather than bleeding into the background. */
  background:#0F0E0D;
  overflow:hidden;
  /* Same directional elevation as the hero plate: light from above, so a
     rim on the top edge and the cast pulled in underneath. */
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.10),
    0 3px 8px -3px rgba(0,0,0,.55),
    0 22px 40px -26px rgba(0,0,0,.85);
  transition:border-color .2s ease,transform .2s ease;
}
.pd-shot a:hover{border-color:rgba(255,255,255,.28);transform:translateY(-2px)}
.pd-shot img{display:block;width:100%;height:auto}
.pd-shot figcaption{
  margin-top:12px;
  font-size:13px;line-height:1.65;color:var(--muted);
  max-width:62ch;
}
@media(max-width:700px){
  .pd-shots{gap:26px}
  .pd-shot figcaption{font-size:12.5px}
}

/* ── Product carousel ────────────────────────────────────────────
   DashQ-style scroll scrub: a tall rail pins a sticky viewport while
   vertical scroll in the product overlay drives translateX on the
   track — "scrolling but not scrolling." Arrows are secondary; dots
   jump the scroller to a slide's progress point.

   Fallback (reduced motion / narrow): native horizontal scroll-snap. */
.pd-carousel{position:relative}
.pd-carousel-track{
  display:flex;
  overflow-x:auto;
  scroll-snap-type:x mandatory;
  scroll-behavior:smooth;
  border-radius:var(--r-md);
  border:1px solid var(--border);
  background:#15130F;
  -webkit-overflow-scrolling:touch;
  scrollbar-width:none;
}
.pd-carousel-track::-webkit-scrollbar{display:none}
.pd-carousel-track:focus-visible{outline:3px solid var(--focus);outline-offset:2px}
.pd-carousel-slide{
  flex:0 0 100%;min-width:0;
  scroll-snap-align:start;
  scroll-snap-stop:always;
  box-sizing:border-box;
  padding:clamp(12px,2vw,22px);
}
.pd-carousel-slide .pd-shot img{
  border:1px solid var(--border);border-radius:var(--r-md);
  background:#0F0E0D;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.08),
    0 3px 8px -3px rgba(0,0,0,.55),
    0 18px 36px -24px rgba(0,0,0,.8);
}
.pd-carousel-slide img{display:block;width:100%;height:auto}
.pd-carousel-arrow{
  position:absolute;top:calc(50% - 28px);transform:translateY(-50%);
  width:40px;height:40px;border-radius:50%;
  background:rgba(11,11,12,.88);border:1px solid rgba(255,255,255,.42);
  color:#fff;display:flex;align-items:center;justify-content:center;
  cursor:pointer;font-size:19px;line-height:1;z-index:2;
  transition:background .18s ease,border-color .18s ease;
}
.pd-carousel-arrow:hover{background:rgba(11,11,12,.98);border-color:rgba(255,255,255,.28)}
.pd-carousel-arrow.prev{left:10px}
.pd-carousel-arrow.next{right:10px}
.pd-carousel-dots{display:flex;justify-content:center;gap:8px;margin-top:14px}
.pd-carousel-dot{
  width:8px;height:8px;padding:0;border:none;border-radius:50%;
  background:rgba(255,255,255,.42);cursor:pointer;transition:background .18s ease,width .18s ease;
}
.pd-carousel-dot:hover{background:rgba(255,255,255,.62)}
.pd-carousel-dot[aria-current="true"]{background:var(--coral);width:22px;border-radius:4px}
#pd-cangrant .pd-carousel-dot[aria-current="true"]{background:var(--amber)}
#pd-vibeconsult .pd-carousel-dot[aria-current="true"]{background:var(--coral)}
.pd-carousel-progress{
  height:2px;margin-top:14px;border-radius:1px;
  background:rgba(255,255,255,.1);overflow:hidden;
}
.pd-carousel-progress > span{
  display:block;height:100%;width:0%;
  background:var(--coral);
  transform-origin:left center;
}
#pd-cangrant .pd-carousel-progress > span{background:var(--amber)}

/* Scrub mode: sticky pin + transform-driven track (no overflow scroll). */
.pd-scroll-rail{position:relative;width:100%}
.pd-scroll-rail-space{position:relative;height:var(--rail-h, auto)}
.pd-scroll-rail-sticky{
  position:sticky;
  top:var(--pd-nav-h, 58px);
  height:var(--rail-pin-h, min(72vh, 640px));
  display:flex;flex-direction:column;justify-content:center;
  z-index:1;
}
.pd-scroll-rail.is-scrub .pd-carousel{
  overflow:hidden;
  width:100%;
}
.pd-scroll-rail.is-scrub .pd-carousel-track{
  overflow:visible;
  scroll-snap-type:none;
  scroll-behavior:auto;
  width:100%;
  will-change:transform;
  transform:translate3d(0,0,0);
}
.pd-scroll-rail.is-scrub .pd-carousel-arrow{display:none}
.pd-scroll-rail.is-scrub .pd-carousel-slide{
  flex:0 0 100%;
  width:100%;
  max-width:100%;
  scroll-snap-align:none;
}

@media(max-width:700px){
  .pd-carousel-arrow{display:none}
  .pd-scroll-rail-space{height:auto !important}
  .pd-scroll-rail-sticky{position:static;height:auto}
  .pd-scroll-rail.is-scrub .pd-carousel-track{
    overflow-x:auto;
    scroll-snap-type:x mandatory;
    will-change:auto;
    transform:none !important;
  }
  .pd-scroll-rail .pd-carousel-progress{display:none}
}

@media(prefers-reduced-motion:reduce){
  .pd-scroll-rail-space{height:auto !important}
  .pd-scroll-rail-sticky{position:static;height:auto}
  .pd-scroll-rail.is-scrub .pd-carousel-track{
    overflow-x:auto;
    scroll-snap-type:x mandatory;
    will-change:auto;
    transform:none !important;
  }
  .pd-scroll-rail .pd-carousel-progress{display:none}
}

/* ── Embedded demo: the VIBE live process map ──────────────────
   Click-to-load rather than an iframe that mounts with the page. The map
   is a full third-party document that opens WebSocket and S3 telemetry
   connections; nobody should pay for that just by scrolling past the
   Product preview heading, least of all on a phone. */
.pd-demo{
  margin:0;
  border:1px solid var(--border);
  border-radius:var(--r-md);
  overflow:hidden;
  background:#0B0B0C;               /* the map's own ground, so no seam */
}
.pd-demo-load{
  display:flex;flex-direction:column;align-items:flex-start;gap:10px;
  width:100%;border:0;cursor:pointer;text-align:left;
  padding:clamp(24px,4vw,44px);
  min-height:clamp(200px,30vh,260px);
  background:
    radial-gradient(120% 90% at 18% 0%, rgba(61,139,139,.16), transparent 60%),
    #0B0B0C;
  color:#fff;font:inherit;
}
.pd-demo-load:hover{background:
    radial-gradient(120% 90% at 18% 0%, rgba(61,139,139,.24), transparent 60%),
    #0F0F11}
.pd-demo-play{
  width:44px;height:44px;border-radius:50%;
  background:var(--teal);display:grid;place-items:center;flex:none;
  transition:transform .18s ease;
}
/* Triangle drawn with borders so there is no glyph to go missing. */
.pd-demo-play::after{
  content:'';border-style:solid;
  border-width:8px 0 8px 13px;
  border-color:transparent transparent transparent #0B0B0C;
  margin-left:3px;
}
.pd-demo-load:hover .pd-demo-play{transform:scale(1.06)}
.pd-demo-title{font-family:var(--font-display);font-size:17px;font-weight:700}
.pd-demo-sub{font-size:13.5px;line-height:1.65;color:rgba(255,255,255,.72);max-width:52ch}
/* ── The map, at full viewport ─────────────────────────────────
   Measured: the diagram's node labels only resolve at roughly 1200x760.
   Inline in the Product preview column it got 870x560 on a 1440 screen
   and 314px on a phone, so it was never going to be readable there. It
   opens over the whole viewport instead. z-index clears the product
   detail overlay (2000) that it opens from. */
.demo-overlay{
  position:fixed;inset:0;z-index:5000;
  flex-direction:column;
  background:#070708;
}
.demo-overlay[hidden]{display:none}
.demo-overlay:not([hidden]){display:flex}
.demo-overlay-bar{
  display:flex;align-items:center;justify-content:space-between;gap:16px;
  padding:8px 10px 8px 16px;
  border-bottom:1px solid var(--border);
  background:var(--charcoal);color:#fff;flex:none;
}
.demo-overlay-back{
  flex:none;cursor:pointer;
  font-family:var(--font-sans);font-size:13px;font-weight:600;color:#fff;
  background:rgba(255,255,255,.08);
  border:1px solid rgba(255,255,255,.20);
  border-radius:var(--rp);
  padding:0 18px;min-height:44px;display:inline-flex;align-items:center;
  transition:background .18s ease,border-color .18s ease;
}
.demo-overlay-back:hover{background:rgba(255,255,255,.16);border-color:rgba(255,255,255,.34)}
.demo-overlay-title{
  font-family:var(--font-display);font-size:14px;font-weight:700;letter-spacing:-.1px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;
}

.demo-overlay-actions{display:flex;align-items:center;gap:6px}
.demo-overlay-out{
  display:inline-flex;align-items:center;
  min-height:44px;padding:0 14px;
  font-size:13px;font-weight:600;color:var(--teal-on-dark);text-decoration:none;
  white-space:nowrap;border-radius:var(--rp);
}
.demo-overlay-out:hover{background:rgba(255,255,255,.07)}
.demo-overlay-close{
  min-width:44px;min-height:44px;
  display:grid;place-items:center;
  background:none;border:0;cursor:pointer;
  color:#fff;font-size:17px;line-height:1;border-radius:var(--rp);
}
.demo-overlay-close:hover{background:rgba(255,255,255,.1)}
/* On a phone the labelled Back button is the control that matters, so the
   X stops competing with it. Declared after .demo-overlay-close's own
   display:grid, which would otherwise win on source order. */
@media(max-width:700px){
  .demo-overlay-title{font-size:13px}
  .demo-overlay-close{display:none}
}
.demo-overlay-hint{font-size:12px;color:var(--muted);white-space:nowrap}
@media(min-width:900px){.demo-overlay-hint{display:none}}
/* Pannable viewport for the map. */
.demo-overlay-scroll{flex:1;overflow:auto;-webkit-overflow-scrolling:touch;background:#070708}
/* The map's own layout shrinks its diagram to whatever width it is given,
   which is how a phone turned it into a postage stamp. Holding a real
   minimum width means it always renders at a legible size and the narrow
   screen pans instead of scaling.
   1200 is the diagram's own design width (its script sets W = 1200), so
   at or above that it renders 1:1 with no dead margin; below it the
   container pans. The height covers the 710px stage plus the map's live
   status bar, legend and event stream. */
/* Panning one axis is a gesture; panning two is a maze. The frame holds
   1200 across (the diagram's design width, so labels stay legible and the
   phone pans sideways) but takes the available height exactly, so nothing
   ever needs vertical panning. The map runs its own scaler against the
   height it is given, so it simply lays out a little tighter on a short
   screen instead of overflowing. */
.demo-overlay-scroll iframe{
  display:block;border:0;background:#070708;
  width:100%;min-width:1200px;
  height:100%;min-height:520px;
}
/* Phones get the map's own vertical layout instead (see the @media block
   in assets/vibe-process-map.html), so the frame must NOT be held at the
   1200px design width here — that width is exactly what would suppress it.
   Full width, natural height, and the column scrolls with the page. */
@media(max-width:700px){
  .demo-overlay-scroll iframe{min-width:0;height:100%;min-height:0}
  .demo-overlay-scroll{overflow:hidden}
  .demo-overlay-hint{display:none}
}
/* While it is open the page behind must not scroll under it. */
body.demo-open{overflow:hidden}
.pd-demo-note{
  font-size:12.5px;line-height:1.6;color:var(--muted);
  padding:12px 16px;border-top:1px solid var(--border);background:#0B0B0C;
}
.pd-demo-note a{color:var(--teal-on-dark);text-decoration:none;white-space:nowrap}
.pd-demo-note a:hover{text-decoration:underline}
@media(max-width:700px){
  .pd-demo-sub{font-size:13px}
}

/* ── Footer ────────────────────────────────────────────────────
   .footer-link sets display:block, but .footer-link-icon sets
   display:inline-flex and is declared after it at the same specificity,
   so every Connect entry carried both classes and the inline value won.
   The four links ran together and wrapped as one paragraph instead of
   stacking as a list. flex (not inline-flex) restores the rows; the
   fit-content width keeps each hit area to the text rather than the
   whole column. */
.footer-link.footer-link-icon{display:flex;width:fit-content}
/* Set as plain 15px bold white it competed with the two real logos
   beside it, reading as a third lockup rendered in the wrong medium.
   It is a qualifier on the Algonquin mark, so it is set as one. */
.backed-note{font-size:13px;font-weight:600;color:rgba(255,255,255,.62);letter-spacing:.2px;font-family:var(--font-sans)}

/* ── Site navbar shell: fixed frame + cycling brand aura ────────
   Quiet conic ring + soft bloom. Coral ↔ teal ↔ #027f82 ↔ amber
   blend continuously; ~16s/lap. Perimeter intensity matched to the
   quieter bottom edge (no hot top). */
@property --aah-nav-angle{
  syntax:'<angle>';
  inherits:false;
  initial-value:0deg;
}
.hero-nav-shell{
  --aah-nav-angle:0deg;
  position:fixed;
  top:12px;left:12px;right:12px;
  z-index:1000;
  padding:1px;
  border-radius:calc(var(--r-md) + 1px);
  isolation:isolate;
  pointer-events:none; /* shell is chrome; nav re-enables hits */
}
.hero-nav-shell > .hero-nav{pointer-events:auto}
/* Soft outer bloom — perimeter matched to the quieter bottom edge
   (was reading hot on top). Same conic everywhere; lower opacity. */
.hero-nav-shell::before{
  content:'';
  position:absolute;inset:-4px;
  border-radius:inherit;
  z-index:0;pointer-events:none;
  background:conic-gradient(
    from var(--aah-nav-angle),
    #E05A4E 0deg,
    #b86f6a 40deg,
    #3D8B8B 95deg,
    #027f82 155deg,
    #2a9088 195deg,
    #a89a3a 235deg,
    #D4900A 270deg,
    #d8743a 320deg,
    #E05A4E 360deg
  );
  filter:blur(8px);
  opacity:.14;
}
/* Thin edge ring (1px padding reveals this). Bottom-intensity all around. */
.hero-nav-shell::after{
  content:'';
  position:absolute;inset:0;
  border-radius:inherit;
  z-index:0;pointer-events:none;
  background:conic-gradient(
    from var(--aah-nav-angle),
    #E05A4E 0deg,
    #b86f6a 40deg,
    #3D8B8B 95deg,
    #027f82 155deg,
    #2a9088 195deg,
    #a89a3a 235deg,
    #D4900A 270deg,
    #d8743a 320deg,
    #E05A4E 360deg
  );
  opacity:.38;
}
@media(prefers-reduced-motion:no-preference){
  .hero-nav-shell{
    animation:aah-nav-aura 16s linear infinite;
  }
}
@keyframes aah-nav-aura{
  to{--aah-nav-angle:360deg}
}
/* Fallback when @property isn't supported: rotate the bloom layers. */
@supports not (background:conic-gradient(from var(--aah-nav-angle), red, blue)){
  @media(prefers-reduced-motion:no-preference){
    .hero-nav-shell{animation:none}
    .hero-nav-shell::before,
    .hero-nav-shell::after{
      animation:aah-nav-spin 16s linear infinite;
    }
  }
  @keyframes aah-nav-spin{
    to{transform:rotate(360deg)}
  }
}
@media(prefers-reduced-motion:reduce){
  .hero-nav-shell,
  .hero-nav-shell::before,
  .hero-nav-shell::after{animation:none !important}
}

.hero-nav{
  position:relative;
  z-index:1;
  width:100%;
  margin:0;
  top:auto;left:auto;right:auto;
  display:flex;align-items:center;justify-content:space-between;
  gap:12px;
  padding:12px 18px;
  /* Charcoal field — DashQ energy, AAH palette. Survives over photo,
     cream team, and dark products without dissolving. */
  background:var(--charcoal);
  border:1px solid rgba(255,255,255,.08);
  box-shadow:
    0 1px 2px rgba(0,0,0,.2),
    0 10px 28px rgba(0,0,0,.28);
  border-radius:var(--r-md);
}
/* Old top-only hairline retired — the shell aura wraps the full perimeter. */
.hero-nav::before,
.hero-nav::after{content:none;display:none}
.hero-nav-brand{
  display:flex;align-items:center;gap:11px;
  text-decoration:none;color:#fff;
  padding:6px 10px 6px 6px;border-radius:var(--rp);
  transition:background .18s ease;
  position:relative;z-index:2;
}
.hero-nav-brand:hover{background:rgba(255,255,255,.08)}
.hero-nav-brand:focus-visible{outline:3px solid var(--focus);outline-offset:3px}
.hero-nav-brand span{
  font-family:var(--font-display);font-size:16.5px;font-weight:700;
  letter-spacing:-.2px;white-space:nowrap;color:#fff;
}
.hero-nav-brand em{color:var(--coral);font-style:normal}
.hero-nav-brand .wm-hub{color:#52ABAB;font-weight:inherit}

.hero-nav-links{
  display:flex;align-items:center;gap:4px;list-style:none;
  position:relative;z-index:2;
}
.hero-nav-links a{
  font-family:var(--font-sans);
  font-size:14px;font-weight:600;
  color:rgba(255,255,255,.72);
  padding:8px 16px;border-radius:var(--rp);
  cursor:pointer;text-decoration:none;
  -webkit-tap-highlight-color:transparent;
  touch-action:manipulation;
  transition:color .16s ease, background .16s ease;
}
.hero-nav-links a:hover{
  color:#fff;
  background:rgba(61,139,139,.22);
}
/* coral-ink solid + white clears ~6.6:1 — brand active without pale grey. */
.hero-nav-links a.active{
  background:var(--coral-ink);
  color:#fff;
  font-weight:700;
}
.hero-nav-links a:focus-visible{
  outline:3px solid var(--focus);outline-offset:3px;
}

@media(max-width:700px){
  .hero-nav-brand span{font-size:0}
  .hero-nav-brand{padding:6px}
}
/* Nav floats over the hero — no #main-scroll top pad (that was a dead
   charcoal ledge / false “header strip” above the composition). Desktop
   clearance lives on .hero-content; the photo plate sits beside it. */
#main-scroll{padding-top:0}
/* ── Scroll snapping: proximity, not mandatory ────────────────────
   Mandatory snap only works when every section fits the viewport. Two
   do not -- team runs 1112px and products 906px against a 900px
   scrollport -- so with `mandatory` the scroller refuses to rest
   anywhere between two snap points and the bottom 212px of the team
   grid (Ravi's card and the founder paragraph) could not be viewed at
   rest at all. Worse, the footer's snap point sits at 2988 while max
   scrollTop is 2586, so any approach to it clamped straight to the end
   of the page: asking for 2400 landed at 2580, and 1400 snapped back
   to 970. `proximity` keeps the chapter-boundary assist when you come
   to rest near one -- but that is still not enough here. When a snap area
   is TALLER than the scrollport the spec constrains its snap positions to
   the range that keeps the area covering the port, so team's only legal
   rests are 1876-2088 and coming to a stop at 2300, on the team/footer
   boundary, was still dragged back 218px.
   The sections were designed as full-screen chapters and the content has
   since outgrown that: at 900, 906, 1112 and 498 against a 900px port,
   only one of the four still fits. Snapping is now fighting the content
   rather than shaping it, so it comes off. Change this to `y proximity`
   if the chapter feel is wanted back once the sections fit again. */
.main{scroll-snap-type:none;overscroll-behavior-y:contain;background:var(--charcoal)}
/* The navbar is fixed over the top 91px of the scrollport, so a section
   aligned to `start` parks its heading underneath it. scroll-padding
   moves the snapport's own top edge clear -- and applies to
   scrollIntoView() too, which is what the nav links call. */
#main-scroll{scroll-padding-top:calc(var(--nav-h) + 8px)}
/* The footer is the last section and shorter than the scrollport, so a
   `start` alignment asks for a scrollTop past the end of the document
   and always clamps to the maximum. That left a permanent snap point
   sitting on the last pixel of the page: coming to rest 180px short of
   the bottom still got dragged all the way down. It has no boundary
   worth snapping to -- the end of the scroll range already stops you. */
.footer-sec{scroll-snap-align:none}
/* --nav-h: bar bottom edge (top offset + bar box). Defined at :root top. */

/* ── Section seams (DashQ nested-sheet pattern) ───────────────────
   Colour stack (AAH tokens only):
     Hero + products = continuous charcoal (NO nest / scoop / ledge).
       Photo plate radius + bottom mask dissolve live on .hero-photo.
     → Team cream sheet on charcoal host (radius shows)
     → Footer charcoal sheet on cream host (radius shows)
   Nest radius only where host ≠ sheet (team/footer). */
.sheet-host{
  padding:0 !important;
  display:block;
  overflow:visible;
  isolation:auto;
}
.sheet-host--charcoal{background:var(--charcoal)}
.sheet-host--cream{background:var(--cream)}

.sheet{
  position:relative;
  isolation:isolate;
  overflow:hidden;
  border-top-left-radius:var(--sheet-radius);
  border-top-right-radius:var(--sheet-radius);
  padding:64px 48px;
  display:flex;
  flex-direction:column;
  justify-content:center;
  box-sizing:border-box;
}
.sheet--cream{
  background:var(--cream);
  color:var(--charcoal);
  min-height:100dvh;
}
.sheet--charcoal{
  background:var(--charcoal);
  color:#fff;
  min-height:50dvh;
}
/* Products: flush with hero — same token, zero radius, zero host pad. */
#products-sec.sheet-host--charcoal{
  padding-top:0 !important;
  padding-bottom:0 !important;
  background:var(--charcoal);
  border-radius:0;
  box-shadow:none;
}
.sheet--products{
  min-height:100dvh;
  border-radius:0;
  border-top-left-radius:0;
  border-top-right-radius:0;
  background:var(--charcoal);
  box-shadow:none;
  overflow:visible;
}
.sheet--products .section-intro{margin-bottom:40px}
.sheet--products .scroll-sub-inv{margin-bottom:0}
.sheet--products .big-prod-grid{margin-top:8px}

@media(max-width:980px){
  :root{--sheet-radius:2.25rem}
  .sheet{padding:80px 28px;min-height:0}
  .sheet--products{min-height:0}
  #products-sec.sheet-host--charcoal{padding-top:0 !important}
}
@media(max-width:700px){
  :root{--sheet-radius:1.75rem}
  .sheet{padding:72px 20px}
}

#products-sec.scroll-sec,
#team-sec.scroll-sec{background:transparent}
#team-sec.sheet-host--charcoal{background:var(--charcoal)}
#footer-sec.footer-sec{background:var(--cream);padding:0}

/* ── Kalvin: 8-frame walk sprite ──────────────────────────────────
   Sprite strip: 1600×115, 8 frames (200px each), transparent bg.
   Only ONE frame is ever visible: .nav-beaver is the 42×24 window and
   .nav-beaver-sprite is the full 336px strip sliding behind it, so the
   window's width and the strip's background-size have to stay in step
   with the asset. (They did not for one build -- the strip was rebuilt
   at a new cell size while these numbers still described the old one,
   which squeezed all eight frames into the window at once.)

   Displayed at 42×24 per frame -- 42/24 is 1.75 against the strip's own
   200/115 (1.739), so the frame scales rather than squashing. He started
   at 68px tall, which collided with the nav links, then 48px, which
   still read as an illustration parked on the bar rather than a detail
   moving through it. At 24px he sits inside the bar's bottom band.

   Grounding: a tight contact shadow directly under him plus a wider
   soft one. Without them he floats over the glass instead of standing
   on it, which was most of why he looked pasted on.

   --kb-cycle is the single source of truth for gait timing. site.js
   reads it and derives the traverse duration from the bar's measured
   width, so his ground speed always matches his stride (see below). */
.nav-beaver{
  /* Slightly slower cycle = more deliberate gait; JS keeps ground
     speed matched via LENGTHS_PER_CYCLE ≈ one body length. */
  --kb-cycle:.76s;
  position:absolute;left:12px;bottom:2px;
  width:42px;height:24px;
  overflow:visible;
  pointer-events:none;user-select:none;
  z-index:1;
  filter:
    drop-shadow(0 1px 0 rgba(20,18,15,.30))
    drop-shadow(0 3px 5px rgba(20,18,15,.22));
}
.nav-beaver-sprite{
  display:block;
  width:336px;height:24px;
  background:url('kalvin-walk-sprite.png?v=4') left top / 336px 24px no-repeat;
  will-change:background-position,transform;
}
/* The walk is driven from site.js now, not from these keyframes.
   CSS can only animate a frame strip against TIME, and position was a
   separate time-based animation, so the two could hold step only while
   the body moved at exactly one constant speed. That is the whole
   reason the old comment below insists on linear timing and no dwell:
   any acceleration would have moved the body without moving the legs.
   Driving the frame from DISTANCE TRAVELLED instead removes the
   constraint entirely -- feet stay planted at any speed, including
   zero -- which is what lets him accelerate, slow down, stop and turn.
   The keyframes are kept only as the no-JS fallback. */
@media(prefers-reduced-motion:no-preference){
  .nav-beaver:not([data-kb-js]){
    perspective:220px;
    perspective-origin:50% 80%;
    transform-style:preserve-3d;
  }
  .nav-beaver:not([data-kb-js]) .nav-beaver-win{animation:kb-traverse var(--kb-traverse,44s) linear infinite}
  .nav-beaver:not([data-kb-js]) .nav-beaver-sprite{animation:kb-walk-cycle var(--kb-cycle) infinite}
}
@media(prefers-reduced-motion:reduce){
  .nav-beaver:not([data-kb-js]) .nav-beaver-sprite{background-position:0 0;transform:none}
  .nav-beaver:not([data-kb-js]) .nav-beaver-win{transform:none}
}
/* No-JS fallback only. JS drives the real gait (distance → frame) and
   a ~620ms 3D yaw at each end. Facing for the fallback is rotateY
   (0 outbound, 180 inbound) with scaleX(-1) held the whole loop — not
   a second scaleX snap on top of the 180. Conceptual split is walk
   0–45, turn 45–55, walk 55–100; on a 44s loop that would be a 4s stall
   (and the walk-cycle would moonwalk), so the turn is a ~1.4% window
   (~620ms) at each end, with a hold at 90° so the profile reads. */
/* The source art faces LEFT (head left, paddle tail right), so scaleX(1)
   is the left-facing pose and scaleX(-1) is the right-facing one. These
   were the wrong way round: he was drawn facing left while travelling
   right, then facing right while travelling back -- moonwalking in both
   directions. Facing must always oppose the sprite's native direction
   only on the outbound leg. */
@keyframes kb-traverse{
  0%     {left:0; transform:rotateY(0deg) scaleX(-1)}
  49.32% {left:calc(100% - var(--kb-win,92px)); transform:rotateY(0deg) scaleX(-1)}
  49.72% {left:calc(100% - var(--kb-win,92px)); transform:translateZ(24px) rotateY(90deg) rotateX(18deg) rotateZ(-8deg) scaleX(-1)}
  50.08% {left:calc(100% - var(--kb-win,92px)); transform:translateZ(24px) rotateY(90deg) rotateX(18deg) rotateZ(-8deg) scaleX(-1)}
  50.68% {left:calc(100% - var(--kb-win,92px)); transform:rotateY(180deg) scaleX(-1)}
  98.64% {left:0; transform:rotateY(180deg) scaleX(-1)}
  99.04% {left:0; transform:translateZ(24px) rotateY(270deg) rotateX(18deg) rotateZ(8deg) scaleX(-1)}
  99.40% {left:0; transform:translateZ(24px) rotateY(270deg) rotateX(18deg) rotateZ(8deg) scaleX(-1)}
  100%   {left:0; transform:rotateY(360deg) scaleX(-1)}
}
/* 8 discrete frames with plant-weighted holds (stance a hair longer
   than swing). Adjacent stops share the same position so the browser
   never interpolates between cells (that smears the strip). Sub-pixel
   Y settle on plant frames only — no bounce / no lateral waddle. */
@keyframes kb-walk-cycle{
  0%, 13%     {background-position:0 0;      transform:translateY(.4px)}
  13.01%, 24% {background-position:-92px 0;  transform:translateY(.1px)}
  24.01%, 37% {background-position:-184px 0; transform:translateY(.35px)}
  37.01%, 49% {background-position:-276px 0; transform:translateY(0)}
  49.01%, 63% {background-position:-368px 0; transform:translateY(.4px)}
  63.01%, 74% {background-position:-460px 0; transform:translateY(.1px)}
  74.01%, 87% {background-position:-552px 0; transform:translateY(.35px)}
  87.01%, 99.9%{background-position:-644px 0;transform:translateY(0)}
  100%        {background-position:-736px 0; transform:translateY(.4px)}
}
a:focus-visible,button:focus-visible,[role="button"]:focus-visible{outline:3px solid var(--focus);outline-offset:4px}
/* --focus is tuned for charcoal (11.5:1) and drops to 1.4:1 on the cream
   sheet, so light surfaces re-point the token at a dark ink: 8.9:1 on cream,
   9.4:1 on the white team cards. */
.sheet--cream,.big-tc{--focus:#134E4E}
button{font:inherit}.hero-nav-links a{display:block}.big-pc{text-align:left;font:inherit;color:inherit;width:100%}.big-pc:focus-visible{z-index:2}.social-link svg{display:block;flex-shrink:0;pointer-events:none}
@media(max-width:980px){html,body{height:auto;overflow:auto;scrollbar-gutter:stable}.app{display:block;height:auto;width:100%;max-width:100%}.main{display:block;overflow:visible;scroll-snap-type:none;scrollbar-gutter:auto}.hero-sec,.scroll-sec,.footer-sec{min-height:auto;scroll-snap-align:none}.hero-sec{min-height:100dvh}.scroll-sec{padding:80px 28px}.footer-sec{padding:72px 28px}.hero-content{padding:28px 28px 44px;background:transparent}.hero-nav{padding:14px 18px}.big-prod-grid{grid-template-columns:1fr}.big-pc{min-height:0}.big-team-grid{grid-template-columns:repeat(3,1fr)}.pd-hero{grid-template-columns:1fr}.pd-mark{display:none}}
@media(max-width:700px){html,body{overflow-x:hidden}.main,.hero-sec,.hero-content,.hero-row{width:100%;max-width:100%;min-width:0}.hero-h1{font-size:clamp(32px,8.7vw,36px);letter-spacing:-.04em;max-width:100%;overflow-wrap:anywhere}.hero-row{align-items:flex-start;flex-direction:column}.hero-sub{font-size:16px;line-height:1.65;width:100%;max-width:100%}.scroll-sub,.scroll-sub-inv{margin-bottom:32px}.big-team-grid{grid-template-columns:1fr 1fr}.footer-grid{grid-template-columns:1fr}.footer-btm{align-items:flex-start;flex-direction:column;gap:12px}.pd-nav{padding-inline:18px}.pd-hero{padding:72px 22px 44px}.pd-body{padding:22px 22px 0}.pd-section{grid-template-columns:1fr;padding:38px 0}.pd-highlights{grid-template-columns:1fr}.pd-highlight{min-height:0}.pd-cta{align-items:flex-start;flex-direction:column}.pd-cta a{width:100%;text-align:center}}
@media(max-width:430px){.big-team-grid{grid-template-columns:1fr}.hero-socials{gap:8px}.scroll-sec{padding-inline:20px}.footer-sec{padding-inline:20px}}

/* ── Mobile navbar ───────────────────────────────────────────────
   The narrow layout used to hide .hero-nav-links outright and hand the
   bar over to a .mobile-brand + .hero-nav-cta pair. Neither element
   exists in the markup any more, so phones were left with a bar holding
   nothing but the logo -- no way to reach Projects, Team or Contact.
   With only four short destinations there is nothing here worth a
   disclosure menu: tightened type and padding fit all four on one row
   down to ~340px, and the brand keeps its mark-only treatment beside
   them (see the .hero-nav-brand span rule further up).
   Placed after the bulk narrow-width blocks on purpose -- the 980px one
   sets .hero-nav's padding too, and these have the same specificity. */
@media(max-width:700px){
  .hero-nav{padding:10px 12px;gap:10px}
  .hero-nav-shell{top:10px;left:10px;right:10px}
  /* The old width:calc(100% - 24px) / max-width pair is gone with the
     rest: the shell is fixed inset, so the nav fills it. */
  .hero-nav-links{display:flex;flex:0 1 auto;min-width:0}
  .hero-nav-links a{font-size:13px;padding:6px 10px;white-space:nowrap}
}
@media(max-width:380px){
  .hero-nav-links a{font-size:12px;padding:6px 7px}
}

/* ── Phone hero: stacked portrait plate + soft bottom into copy ──
   Portrait crop; soft top arcs + mask dissolve (same model as tablet).
   Nav chrome only here — plate rules inherit from ≤1024. */
@media(max-width:700px){
  /* Shell stays the fixed floater; nav fills it. */
  .hero-nav-shell{top:10px;left:10px;right:10px}
  .hero-nav{position:relative;top:auto;left:auto;right:auto;margin:0;z-index:auto;padding:10px 12px;gap:10px}
  #main-scroll{padding-top:0}
  /* The later sections have to clear the floating bar themselves now, and
     in-page jumps need the same offset. */
  .scroll-sec{padding-top:104px}
  html{scroll-padding-top:88px}

  .hero-sec{
    position:relative;
    display:flex;flex-direction:column;justify-content:flex-start;
    min-height:100dvh;
    padding:0;
  }
  .hero-photo-col{
    position:relative;z-index:0;
    display:flex;padding:0;margin:0;
    width:100%;height:auto;max-width:none;flex:none;
    border-radius:0;overflow:visible;
  }
  .hero-photo-col::before{content:none;display:none}
  .hero-photo-col .hero-photo{
    width:100%;height:auto;
    max-width:none;max-height:min(420px,52%);
    aspect-ratio:1536 / 1024;
    object-fit:cover;
    /* Horizontal anchor: cover crops width; 28% keeps the wall lockup. */
    object-position:28% center;
    border-radius:var(--sheet-radius) var(--sheet-radius) 0 0;
    box-shadow:none;background:transparent;
    -webkit-mask-image:none;
    mask-image:none;
  }
  .hero-photo-col::after{
    content:'';
    position:absolute;left:0;right:0;bottom:0;
    height:clamp(36px,12%,72px);
    z-index:2;pointer-events:none;
    border:none;box-shadow:none;
    background:linear-gradient(
      to bottom,
      transparent 0%,
      rgba(27,26,24,.4) 50%,
      var(--charcoal) 100%
    );
  }
  .hero-content{
    position:relative;z-index:2;
    display:flex;flex-direction:column;
    flex:none;min-height:0;
    max-width:none;
    background:transparent;
    border-radius:0;
    /* Clears the floating navbar; plate sits above so less top pad. */
    padding:24px 24px 40px;
  }
  .hero-sub{max-width:34ch}
}

/* ── Touch ergonomics ─────────────────────────────────────────────
   Everything interactive here was under the 44px minimum on a phone:
   the nav links were 28px tall, the footer links 18px, and the LinkedIn
   icons on the team cards were 13x13 — a target you have to aim at. */
@media(max-width:980px){
  /* 13px of padding on ~18px of line box clears 44 without making the
     bar look chunky. All four still fit one row down to 360px. */
  .hero-nav-links a{padding:14px 12px;line-height:1.3}
  .hero-nav-brand{min-height:44px}
  .hero-socials .social-link{width:44px;height:44px;aspect-ratio:1/1;border-radius:50%;box-sizing:border-box;overflow:visible}
  .btn-footer-p{padding:15px 26px;line-height:1.3}
  /* Inline links in a stacked list: vertical padding, and the negative
     margin keeps the visual rhythm of the list unchanged. */
  .footer-link.footer-link-icon{padding:13px 0;margin:-9px 0}
  /* The icon stays 13px; only the hit area grows, via an overlay rather
     than padding so the name row's spacing does not shift. The name
     beside it is not interactive, so the overlap costs nothing. */
  .team-li-btn{position:relative}
  .team-li-btn::after{
    content:'';position:absolute;top:50%;left:50%;
    transform:translate(-50%,-50%);
    width:44px;height:44px;
  }
}
@media(max-width:380px){
  /* Tighter bar, but never below the 44px target height — the smaller
     font at this width shrinks the line box, so the padding grows. */
  .hero-nav-links a{padding:15px 8px}
}
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto!important}*,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important;scroll-behavior:auto!important}.main{scroll-snap-type:none}}

/* Scroll-linked hero motion is limited to compositor properties. The class
   exists only while the damped animation is catching up to native scroll. */
@media(min-width:981px){
  .hero-motion-active > .hero-photo-col{
    will-change:transform;backface-visibility:hidden;
  }
  .hero-motion-active > .hero-content{
    will-change:transform,opacity;backface-visibility:hidden;
  }
}
@media(prefers-reduced-motion:reduce){
  .hero-photo-col,.hero-content{transform:none!important;opacity:1!important}
}

/* Page-two layers are promoted only while their scroll-linked entrance is
   settling; no grid dimensions or card geometry are animated. */
@media(min-width:981px){
  #products-sec.products-motion-active .scroll-kicker-inv,
  #products-sec.products-motion-active .scroll-h,
  #products-sec.products-motion-active .scroll-sub-inv,
  #products-sec.products-motion-active .big-pc{
    will-change:transform,opacity;backface-visibility:hidden;
  }
}
@media(prefers-reduced-motion:reduce){
  #products-sec .scroll-kicker-inv,
  #products-sec .scroll-h,
  #products-sec .scroll-sub-inv,
  #products-sec .big-pc{transform:none!important;opacity:1!important}
}

/* Tablets and phones use the IntersectionObserver reveal defined above.
   Its .has-reveal guard keeps every project visible if JavaScript is absent,
   while prefers-reduced-motion retains the static presentation. */

/* ══════════════════════════════════════════════════════════════════
   ATMOSPHERIC SECTION BACKGROUNDS
   Same energy as product overlay (.pd::before): layered coral / teal /
   amber / deep-teal blooms on charcoal. Oversized layers drift via
   transform only. Slow + staggered. prefers-reduced-motion respected.
   ══════════════════════════════════════════════════════════════════ */

/* Shared AAH dark-field bloom stack (reference look = .pd). */
.hero-sec::before,
.hero-content::before,
.sheet--products::before,
.sheet--charcoal:not(.sheet--products)::before,
.pd::before{
  transform-origin:50% 50%;
  background:
    radial-gradient(46% 40% at 14% 16%, rgba(224,90,78,.30), transparent 68%),
    radial-gradient(44% 38% at 86% 20%, rgba(61,139,139,.26), transparent 70%),
    radial-gradient(40% 36% at 78% 78%, rgba(212,144,10,.18), transparent 72%),
    radial-gradient(38% 34% at 22% 72%, rgba(2,127,130,.20), transparent 70%),
    radial-gradient(32% 28% at 48% 46%, rgba(224,90,78,.12), transparent 74%);
}

/* ── Hero field (split plate + blooms) ──────────────────────────── */
.hero-sec{isolation:isolate}
.hero-sec::before{
  content:'';position:absolute;inset:-18%;z-index:0;pointer-events:none;
}
.hero-photo-col{z-index:1}
.hero-content{position:relative;z-index:2}

/* ── Hero copy — blooms behind type, NO opaque panel ───────────────
   Transparent field + overflow visible so nothing clips into a box.
   Soft bloom mask so the aura dissolves — no hard panel corners. */
.hero-content{isolation:isolate;overflow:visible;background:transparent}
.hero-content::before{
  content:'';position:absolute;inset:-28%;z-index:0;pointer-events:none;
  border:none;box-shadow:none;border-radius:0;
  -webkit-mask-image:radial-gradient(ellipse 72% 68% at 40% 45%,#000 20%,transparent 78%);
  mask-image:radial-gradient(ellipse 72% 68% at 40% 45%,#000 20%,transparent 78%);
  background:
    radial-gradient(48% 42% at 18% 24%, rgba(224,90,78,.34), transparent 66%),
    radial-gradient(44% 40% at 82% 70%, rgba(61,139,139,.26), transparent 68%),
    radial-gradient(36% 32% at 58% 12%, rgba(212,144,10,.16), transparent 70%),
    radial-gradient(34% 30% at 36% 58%, rgba(2,127,130,.18), transparent 70%),
    radial-gradient(28% 26% at 70% 40%, rgba(61,139,139,.12), transparent 72%);
}
.hero-content > *{position:relative;z-index:1}

/* ── Products (continuous charcoal chapter with hero) ───────────── */
.scroll-sec.dark{position:relative;overflow:visible;isolation:auto;z-index:auto}
.scroll-sec.dark::before{display:none}
.sheet--products{overflow:visible;isolation:isolate}
.sheet--products::before{
  content:'';position:absolute;inset:-22%;z-index:0;pointer-events:none;
  background:
    radial-gradient(48% 42% at 12% 14%, rgba(224,90,78,.30), transparent 66%),
    radial-gradient(46% 40% at 88% 40%, rgba(61,139,139,.26), transparent 68%),
    radial-gradient(40% 36% at 30% 86%, rgba(212,144,10,.18), transparent 70%),
    radial-gradient(36% 32% at 58% 48%, rgba(2,127,130,.18), transparent 72%),
    radial-gradient(30% 28% at 70% 18%, rgba(224,90,78,.12), transparent 74%);
}
.sheet--products > *{position:relative;z-index:1}

/* ── Team sheet (cream) — quieter, static wash only ─────────────── */
.sheet--cream::before{
  content:'';position:absolute;inset:0;z-index:0;pointer-events:none;
  background:
    radial-gradient(42% 36% at 88% 14%, rgba(224,90,78,.07), transparent 68%),
    radial-gradient(40% 34% at 10% 86%, rgba(61,139,139,.07), transparent 70%);
}
.sheet--cream > *{position:relative;z-index:1}

.scroll-sec:not(.dark){
  position:relative;
  overflow:visible;
  background:transparent;
}
.scroll-sec:not(.dark)::before{display:none}
.scroll-sec:not(.dark) > *{position:relative;z-index:auto}

/* ── Footer sheet (dark) ────────────────────────────────────────── */
.sheet--charcoal:not(.sheet--products){overflow:hidden;isolation:isolate}
.sheet--charcoal:not(.sheet--products)::before{
  content:'';position:absolute;inset:-20%;z-index:0;pointer-events:none;
  background:
    radial-gradient(46% 42% at 12% 80%, rgba(61,139,139,.28), transparent 68%),
    radial-gradient(42% 38% at 88% 18%, rgba(224,90,78,.24), transparent 70%),
    radial-gradient(36% 32% at 48% 42%, rgba(212,144,10,.14), transparent 72%),
    radial-gradient(32% 28% at 68% 68%, rgba(2,127,130,.16), transparent 70%);
}
.sheet--charcoal:not(.sheet--products) > *{position:relative;z-index:1}

.footer-sec{position:relative;overflow:visible;isolation:auto}
.footer-sec::before{display:none}
.footer-sec > *{position:relative;z-index:auto}

/* Do NOT set overflow:hidden on .pd — that overrides overflow-y:auto and
   traps product carousels below the fold. Blooms stay inset oversized. */
.pd{isolation:isolate}
.pd::before{
  content:'';position:absolute;inset:-18%;z-index:0;pointer-events:none;
  background:
    radial-gradient(46% 40% at 82% 14%, rgba(61,139,139,.28), transparent 68%),
    radial-gradient(44% 38% at 14% 10%, rgba(224,90,78,.26), transparent 66%),
    radial-gradient(38% 34% at 52% 72%, rgba(212,144,10,.16), transparent 70%),
    radial-gradient(34% 30% at 28% 48%, rgba(2,127,130,.16), transparent 70%),
    radial-gradient(30% 28% at 70% 50%, rgba(224,90,78,.10), transparent 74%);
}
.pd > *{position:relative;z-index:1}

/* Phone hero is photo+scrim — don't stack a second bloom panel over type. */
@media(max-width:700px){
  .hero-sec::before{opacity:.55}
  .hero-content::before{display:none}
}

/* Slow drift — staggered so sections don't pulse in lockstep. */
@media(prefers-reduced-motion:no-preference){
  .hero-sec::before,
  .hero-content::before,
  .sheet--products::before,
  .sheet--charcoal:not(.sheet--products)::before,
  .pd.open::before{will-change:transform}
  .hero-sec::before{animation:aah-bloom-b 36s ease-in-out infinite}
  .hero-content::before{animation:aah-bloom-a 28s ease-in-out infinite}
  .sheet--products::before{animation:aah-bloom-b 34s ease-in-out infinite}
  .sheet--charcoal:not(.sheet--products)::before{animation:aah-bloom-a 32s ease-in-out infinite reverse}
  .pd.open::before{animation:aah-bloom-b 36s ease-in-out infinite}
}
@keyframes aah-bloom-a{
  0%,100%{transform:translate(0,0) scale(1)}
  33%{transform:translate(2.8%,-2%) scale(1.045)}
  66%{transform:translate(-2.2%,2.4%) scale(1.025)}
}
@keyframes aah-bloom-b{
  0%,100%{transform:translate(0,0) scale(1)}
  40%{transform:translate(-2.6%,2%) scale(1.04)}
  70%{transform:translate(2.2%,-2.6%) scale(1.055)}
}

/* ══════════════════════════════════════════════════════════════════
   CHAPTER SEAMS — one continuous field, sheets that rise out of it
   ══════════════════════════════════════════════════════════════════
   The problem this replaces: hero and products each painted their OWN
   charcoal box and their OWN oversized bloom pseudo. Two independently
   lit rectangles butted edge to edge, so the join read as a seam
   between two panels rather than one continuous page — and every
   compensating patch (edge scrims, chapter washes, a vertical gradient
   down the photo's left side) added another rectangle to hide the last.

   The fix is structural, not cosmetic:
     1. ONE fixed atmospheric field behind the whole dark run. Because
        it is viewport-fixed rather than per-section, no section
        boundary can ever fall across it — continuity is guaranteed by
        construction instead of by matching values in two places.
     2. Sections paint nothing. They are transparent windows onto it.
     3. The seam is carried by a SHEET that rises over the field with a
        large top radius, a 1px lit top edge and a shadow cast upward.
        Same colour family throughout, so the background stays unified;
        the boundary reads as depth, not as a colour change.
   ══════════════════════════════════════════════════════════════════ */

:root{--seam-overlap:48px}

/* ── 1. The field ──────────────────────────────────────────────── */
#main-scroll{position:relative;background:var(--charcoal)}
#main-scroll::before{
  content:'';
  /* Fixed, so it is painted once against the viewport and every
     section scrolls over it. Also keeps it out of scrollHeight — the
     old inset:-18% pseudos inflated their section's scroll box, which
     is what grew the stray nested scrollbar. */
  position:fixed;
  inset:0;
  z-index:0;
  pointer-events:none;
  background:
    radial-gradient(52% 44% at 14% 12%, rgba(224,90,78,.30), transparent 68%),
    radial-gradient(48% 42% at 88% 26%, rgba(61,139,139,.26), transparent 70%),
    radial-gradient(44% 38% at 78% 82%, rgba(212,144,10,.16), transparent 72%),
    radial-gradient(42% 36% at 20% 76%, rgba(2,127,130,.20), transparent 70%),
    radial-gradient(34% 30% at 50% 48%, rgba(224,90,78,.10), transparent 74%);
}
#main-scroll > *{position:relative;z-index:1}

/* ── 2. Sections stop painting ─────────────────────────────────── */
.hero-sec,
#products-sec.sheet-host--charcoal,
.sheet--products{background:transparent}

/* Every patch that existed to disguise a hard edge. With the field
   continuous underneath there is no edge left to disguise, and each of
   these was itself drawing one of the rectangles being complained
   about. */
.hero-sec::before,
.hero-sec::after,
.hero-content::before,
.hero-photo-col::before,
.hero-photo-col::after,
.sheet--products::before{content:none;display:none}

/* ── 3. Products rises out of the hero ─────────────────────────── */
.sheet--products{
  border-radius:var(--sheet-radius) var(--sheet-radius) 0 0;
  /* Pulled up so it genuinely overlaps rather than abutting.
     Overlap is px-pinned (--seam-overlap) so Mac/Win rem/root or
     chapter-height drift cannot stretch the lip. */
  margin-top:calc(var(--seam-overlap) * -1);
  padding-top:calc(64px + var(--seam-overlap));
  /* Surface, not colour: a wash that is brightest at the lip and gone
     by a third of the way down. Charcoal stays charcoal. */
  background:linear-gradient(
    to bottom,
    rgba(255,255,255,.04) 0%,
    rgba(255,255,255,.015) 22%,
    rgba(255,255,255,0)    58%
  );
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.08),   /* lit top edge */
    0 -24px 56px -30px rgba(0,0,0,.48);    /* soft upward cast — less “hard border” */
  overflow:visible;
}

/* ── 4. Hero photo: a rounded plate, not a cropped rectangle ────── */
.hero-photo-col{
  align-items:center;
  justify-content:center;
  /* Clears the fixed nav; bottom gap keeps the plate off the seam so
     the products lip passes under it cleanly. Pads use px/% — not vw —
     so Windows scrollbars cannot shift the plate above the lip. */
  padding:calc(var(--nav-h) + 16px) clamp(20px,3%,36px)
          calc(var(--seam-overlap) + 18px) 0;
  margin-left:0;
  box-sizing:border-box;
}
.hero-picture{
  display:flex;align-items:center;justify-content:center;
  width:100%;height:auto;
}
.hero-photo-col .hero-photo{
  /* Width-driven plate. max-height:100% of a viewport-tall column made
     Mac/Windows chrome height change where the curve meets products. */
  width:100%;
  height:auto;
  max-width:100%;
  max-height:none;
  aspect-ratio:1536 / 1024;
  object-fit:cover;
  border-radius:var(--sheet-radius);
  box-shadow:
    0 0 0 1px rgba(255,255,255,.07),
    0 34px 80px -34px rgba(0,0,0,.8);
  /* The bottom-fade mask existed to dissolve a hard edge into charcoal.
     A rounded plate has no hard edge to dissolve. */
  -webkit-mask-image:none;
  mask-image:none;
}

/* Narrow: the plate goes full-bleed-ish above the copy, same rounding. */
@media(max-width:1024px){
  .hero-photo-col{
    padding:calc(var(--nav-h) + 12px) clamp(16px,3%,24px)
            clamp(18px,28px,28px);
    align-items:center;
  }
  .hero-photo-col .hero-photo{
    width:100%;height:auto;
    max-height:min(420px,55%);
    aspect-ratio:1536 / 1024;
    border-radius:var(--sheet-radius);
    -webkit-mask-image:none;mask-image:none;
  }
}

@media(max-width:980px){:root{--seam-overlap:32px}}
@media(max-width:700px){:root{--seam-overlap:24px}}

/* ── 5. The pale strip down the right edge ─────────────────────────
   `.main::-webkit-scrollbar{width:4px}` styled the thumb but never the
   track. In Chromium, styling ANY scrollbar part opts that scrollbar
   out of the dark-aware UA renderer and unstyled parts fall back to the
   WebKit default — a light grey track. That is the 4px light line down
   a charcoal page. Below 980px the document becomes the scroller and
   has no scrollbar rules at all, so phones got the full-width version.
   `color-scheme` was never declared anywhere, which is what makes every
   UA-painted surface default to its light variant. */
:root{color-scheme:dark}
.main::-webkit-scrollbar-track,
.main::-webkit-scrollbar-corner{background:transparent}
.main{scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.18) transparent}
html,body{scrollbar-color:rgba(255,255,255,.18) transparent}
.pd,.demo-overlay-scroll{scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.18) transparent}

/* ── 6. Blooms that were being clipped into rectangles ─────────────
   Both of these sit inside `overflow:hidden` sheets while sized
   `inset:-20%`, so the oversized radial stack was sheared off flat
   against the sheet's border box — a hard-edged rectangle of slightly
   different tone, which is exactly the artefact being complained about.
   Pulling them back to inset:0 and fading the edges with a mask means
   the gradient reaches zero on its own terms and the clip has nothing
   left to cut. */
.sheet--charcoal:not(.sheet--products)::before,
.sheet--cream::before{
  inset:0;
  -webkit-mask-image:radial-gradient(ellipse 88% 82% at 50% 45%,#000 34%,transparent 88%);
  mask-image:radial-gradient(ellipse 88% 82% at 50% 45%,#000 34%,transparent 88%);
}

/* ── 7. One tonal family for every dark surface ────────────────────
   Cards were `#201E1B` against a `#1B1A18` field — a 5/4/3-point
   near-miss that reads as a mismatched grey patch rather than as a
   lifted surface, and it drifts further the moment the field's blooms
   tint the charcoal underneath. Expressing elevation as a translucent
   white wash over the shared field instead means a card is always
   exactly its background plus light, so the two can never disagree. */
.big-pc,.pd-highlight{
  background:rgba(255,255,255,.038);
  border-color:rgba(255,255,255,.10);
}
.big-pc:hover,.pd-highlight:hover{background:rgba(255,255,255,.065)}

/* ── 8. Breathing room around the seam ─────────────────────────────
   DashQ's chapters run 8rem/128px of vertical padding against a 40px
   corner. Ours ran 64px against a 48px corner, so content came within
   about a corner's width of the curve and the seam read as a boundary
   being crowded rather than as space. The curve needs room around it
   to be read as a transition at all — this is most of why theirs feels
   soft and ours felt like a stacked panel. */
:root{--sheet-pad-y:112px}
.sheet{padding:var(--sheet-pad-y) 48px}
.sheet--products{padding-top:calc(var(--sheet-pad-y) + var(--seam-overlap))}
@media(max-width:980px){:root{--sheet-pad-y:80px} .sheet{padding-inline:28px}}
@media(max-width:700px){:root{--sheet-pad-y:64px} .sheet{padding-inline:20px}}

/* ── 9. Shadows carry the page's hue, never black ──────────────────
   Also from DashQ: their shadows are the brand navy at ~15%, or a beige
   token on light chapters — never rgb(0,0,0). A black shadow on a warm
   charcoal field greys the pixels it falls on, and a grey halo against
   a warm ground is read as an edge. Tinting the shadow to the field's
   own hue lets it darken without shifting colour.
   Keep the products cast soft — a heavy upward shadow read as a hard
   Mac/Win-sensitive “border” between hero and products. */
.sheet--products{
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.08),
    0 -24px 56px -30px rgba(12,11,9,.48);
}
.hero-photo-col .hero-photo{
  box-shadow:
    0 0 0 1px rgba(255,255,255,.07),
    0 34px 80px -34px rgba(12,11,9,.85);
}

/* ── 10. Product overlay: the same oversized-bloom trap ────────────
   .pd::before was still sized inset:-18%. Against a 1697px overlay that
   reaches ~305px past each edge, which is exactly the 2004px scrollWidth
   the overlay reported against a 1697px client box -- and .pd carries
   overflow-y:auto, so the excess became a real horizontal scrollbar
   along the bottom of every product page. It survived a bisect that
   hid all four children because a pseudo-element is not in the DOM.
   Same remedy as the section blooms: pull to inset:0 and let a mask
   fade the edges instead of a negative inset. */
.pd::before{
  inset:0;
  -webkit-mask-image:radial-gradient(ellipse 90% 84% at 50% 42%,#000 32%,transparent 90%);
  mask-image:radial-gradient(ellipse 90% 84% at 50% 42%,#000 32%,transparent 90%);
}

/* ── 11. Section kickers: a chapter marker ─────────────────────────
   These started as 12px/600 captions dwarfed by a 52px headline. The
   first replacement was a tinted pill with a leading dot, which is the
   default SaaS badge — it reads as generic on sight, so it went.

   This is a chapter marker instead: an index numeral at display size
   in the section accent, a hairline, then the label. It earns its
   presence from the numeral's scale and the accent, not from a filled
   shape, and it belongs to the editorial register the rest of the page
   is already in (display type, italic emphasis) rather than importing
   a dashboard component.

   Numeral in the accent, label in section ink: two levels of hierarchy
   inside the marker itself, so it holds together as one object without
   shouting over the headline. Identical metrics across both sections;
   only the accent hue and the numeral change. */
.scroll-kicker,
.scroll-kicker-inv{
  display:flex;
  align-items:center;
  gap:16px;
  /* .section-intro is a column flex container — a stretch-aligned child
     would run the full measure. */
  align-self:flex-start;
  width:auto;
  /* Space Grotesk was the old display face and is no longer loaded, so this
     always fell through to Figtree anyway -- now via the token, which also
     picks up the metric-matched fallback. */
  font-family:var(--font-sans);
  font-size:clamp(12px,.95vw,13px);
  font-weight:700;
  line-height:1;
  letter-spacing:.22em;
  text-transform:uppercase;
  background:none;
  border:0;
  padding:0;
  border-radius:0;
  margin-bottom:30px;
}
.kicker-idx{
  display:flex;
  align-items:center;
  gap:16px;
  font-size:clamp(26px,2.4vw,34px);
  font-weight:700;
  letter-spacing:-.03em;
  line-height:1;
  /* Keeps 01 and 02 the same width so the two markers align down the
     page even though the glyphs differ. */
  font-variant-numeric:tabular-nums;
}
.kicker-idx::after{
  content:'';
  width:1px;
  height:1.7em;
  background:currentColor;
  opacity:.32;
}
/* On charcoal */
.scroll-kicker-inv{color:rgba(255,255,255,.74)}
.scroll-kicker-inv .kicker-idx{color:#EC7060}
/* On cream */
.scroll-kicker{color:rgba(27,26,24,.68)}
.scroll-kicker .kicker-idx{color:var(--teal-ink)}

/* ── 14. The navbar belongs on the product pages too ───────────────
   .pd overlays are position:fixed at z-index 2000 against the nav
   shell's 1000, so opening a product hid the site navigation entirely
   and the only way back was the overlay's own "Back to products".
   Lift the bar over the overlay and inset the overlay's content by the
   bar's height, so .pd-nav comes to rest just beneath it rather than
   underneath it. */
.hero-nav-shell{z-index:2500}
.pd{padding-top:var(--nav-h)}
@media(max-width:700px){
  .pd{padding-top:calc(var(--nav-h) - 8px)}
}
/* Below 980px html/body scroll again, so scrolling past the end of an open
   panel used to chain into the page it is covering. */
.pd{overscroll-behavior:contain}
body.pd-open{overflow:hidden}

/* The overlay's own bar is redundant now that the site navbar reaches
   these pages — two bars stacked saying overlapping things. Getting back
   is still covered three ways: "Projects" in the navbar, "Back to all
   products" at the foot of each product, and Escape. */
.pd-nav{display:none}

/* ── 15. Compact single-row phone navbar ──────────────────────────
   Keep all four destinations visible, but use the mark-only brand on
   phones so the bar does not need a second row. Forty-pixel controls
   remain easy to tap while the full shell stays under 50px tall. */
@media(max-width:700px){
  :root{--nav-h:58px}
  .hero-nav-shell{top:8px;left:8px;right:8px}
  /* Wordmark stays. Mark-only was the compact answer, but measuring
     rather than estimating shows the full brand fits one row: brand +
     links come to 357px of the 360 available at 390, and 297 of 300 at
     320. The bar reads as the desktop bar, just smaller. */
  .hero-nav-brand span{display:inline;font-size:12.5px;letter-spacing:-.01em}
  .hero-nav{
    flex-direction:row;
    align-items:center;
    gap:4px;
    padding:3px 6px;
  }
  .hero-nav-brand{
    align-self:auto;
    min-height:40px;
    padding:4px;
  }
  .hero-nav-links{
    display:flex;
    flex:1 1 auto;
    justify-content:space-between;
    width:auto;
    min-width:0;
    gap:1px;
  }
  .hero-nav-links a{
    display:flex;
    align-items:center;
    justify-content:center;
    min-height:40px;
    /* 6px, not 8. The links are white-space:nowrap so they cannot shrink
       below their own content -- with 8px a side the row needed ~393px
       against the 372 a 390px screen actually gives, and the last link
       hung outside the bar. Four links pay this four times over, so the
       2px each is what buys the wordmark its room. */
    padding:10px 6px;
    font-size:12.5px;
    line-height:1;
    white-space:nowrap;
  }
  .nav-beaver{display:none}
}
/* 429, not 380. Measured: at 390 the larger metrics needed 15px more
   than the bar had and the last link hung outside it, while 360 -- which
   did fall under the old step -- fit fine. The breakpoint was below the
   width that actually broke. */
@media(max-width:429px){
  .hero-nav-links a{font-size:11.5px;padding:10px 4px}
  /* Was 13px here -- larger than the 12.5px it steps down from, so the
     narrowest screens were being handed the widest wordmark. */
  .hero-nav-brand span{font-size:11px}
}
/* Below ~345px the wordmark genuinely does not fit beside four links:
   even at the compact metrics the row still wants 40px more than the bar
   has, and the wordmark is 95px of that. It is the one thing here that
   can go without costing a destination or a tap target, so on the very
   smallest phones the brand falls back to its mark. */
@media(max-width:344px){
  .hero-nav-brand span{display:none}
}
.nav-beaver{left:var(--kb-left,12px)}

/* The kicker relies on align-self:flex-start to avoid stretching, but
   that only does anything when the parent is a flex container. On narrow
   widths .section-intro is block, so the marker ran the full 335px
   measure and its hairline stretched with it. fit-content shrinks it to
   its contents whatever the parent's display happens to be. */
.scroll-kicker,
.scroll-kicker-inv{width:fit-content}

/* ── 16. The phone navbar's rainbow ────────────────────────────────
   .hero-nav-shell::after is a conic gradient at .38 opacity. It is
   meant to show only as a 1px gradient hairline: the shell carries
   1px of padding and .hero-nav sits over the rest of it.
   What holds it down is .hero-nav{z-index:1} — the ring is z-index:0,
   and an ::after paints after its parent's children, so with the nav on
   `auto` the ring lands on top of the charcoal instead of behind it and
   floods the whole bar. The narrow block reset it to auto, which is why
   the colours appeared on phones and nowhere else. Geometry was never
   the problem: the nav covers all but 1px on every side at 375px too.
   The backdrop blur then smeared the hero photo through that wash,
   which is the smudge along the bar's bottom edge. */
@media(max-width:700px){
  .hero-nav{z-index:1}
}

/* ── 18. Kalvin walks a lane of his own ────────────────────────────
   At 42x24 on bottom:2px he spanned y 47-71 of a 73px bar while the
   links sit at 22-51 — a 4px overlap, so he kept grazing the text.
   Smaller and pinned to the floor gives him 52-72, clear of the links
   with the bar's height unchanged. The strip is 8 cells, so its width
   and background-size follow him down; site.js reads the frame width
   from the element rather than assuming 42. */
.nav-beaver{
  width:36px;
  height:21px;
  bottom:1px;
}
.nav-beaver-sprite{
  width:288px;
  height:21px;
  background-size:288px 21px;
}

/* Shrinking him alone was not enough: at 21px tall on a 71px bar he
   still started at y49 against a links band ending at y52. Rather than
   keep cutting him down, the bar gives him the strip — 8px of extra
   floor below the links, so he walks a lane instead of grazing the
   bottom of the text. Desktop only; the phone bar has no room for him
   at all. */
@media(min-width:701px){
  .hero-nav{padding-bottom:20px}
}

/* ══════════════════════════════════════════════════════════════════
   MOBILE DENSITY
   The phone page ran 7766px — about twelve screens for three products,
   eight people and a footer. Almost none of that was padding (dead
   space measured 176-236px per section); it was cards sized for a
   desktop column and then given the full width of a phone.
   ══════════════════════════════════════════════════════════════════ */

/* ── Team: a grid of faces, not a stack of posters ─────────────────
   At one column each card was 418px tall on a 325px square portrait,
   so eight people came to 4138px on their own — over half the page,
   and it read as a slideshow you had to scroll through rather than a
   team you could see. Two columns puts the whole team in about 1000px
   and lets you take them in at a glance, which is the point of the
   section. */
@media(max-width:700px){
  .big-team-grid{
    grid-template-columns:1fr 1fr;
    gap:12px;
  }
  .big-tname{font-size:15px;line-height:1.3;min-width:0}
  /* nowrap, not wrap. At 156px a two-word surname pushes the LinkedIn
     mark onto a line of its own, which reads as a stray icon floating
     under the name ("Jemaelle Saint-Brice"). Keeping the row unwrapped
     lets the NAME wrap inside itself while the mark stays beside it. */
  .big-tname-row{gap:5px;flex-wrap:nowrap;align-items:flex-start}
  .team-li-btn{flex:0 0 auto;margin-top:2px}
  .big-trole{font-size:11.5px}
  .big-tbadge{font-size:9.5px;padding:3px 8px;letter-spacing:.06em}
  .team-li-btn svg{width:12px;height:12px}
}

/* ── Product cards: desktop padding on a phone-width card ──────────
   28px of inset a side on a 325px card spends a sixth of the width on
   nothing, and the tech pills were wrapping to three rows because of
   it. Tightened, the three cards lose ~90px each without dropping a
   word of copy. */
@media(max-width:700px){
  .big-prod-grid{gap:14px}
  .big-pc{padding:20px 18px 18px}
  .big-pc-logo{height:56px}
  .big-pc-desc{font-size:15px;line-height:1.6}
  .tech-row{gap:5px;margin:10px 0 2px}
  .tech-pill{font-size:9.5px;padding:3px 8px;gap:4px}
  .tech-pill svg{width:10px;height:10px}
}

/* ── Mobile: stop paying for effects that do not show ──────────────
   Repeatedly froze the renderer while inspecting this page, which is a
   fair proxy for what a mid-range phone would be doing. Three costs,
   none of which buy anything on a small screen:

   1. backdrop-filter on the navbar. The bar's background is fully
      opaque charcoal, so the blur is sampling and blurring a backdrop
      that is then painted over completely — invisible, and recomputed
      every frame the page scrolls under it. This is the expensive one.
   2. The bloom drift. Four oversized multi-stop radial layers
      animating transform forever. At phone size the drift is barely
      perceptible; the gradients themselves stay, so the field looks
      identical.
   3. The navbar aura spin, same reasoning — it animates a conic
      gradient that only shows as a 1px hairline. */
@media(max-width:700px){
  .hero-nav{backdrop-filter:none;-webkit-backdrop-filter:none}
  .hero-sec::before,
  .hero-content::before,
  .sheet--products::before,
  .sheet--charcoal:not(.sheet--products)::before,
  .pd.open::before,
  .hero-nav-shell,
  .hero-nav-shell::before,
  .hero-nav-shell::after{animation:none!important}
  /* Nothing is animating these any more, so stop asking the compositor
     to keep a layer for each of them. */
  .hero-sec::before,
  .hero-content::before,
  .sheet--products::before,
  .sheet--charcoal:not(.sheet--products)::before,
  .pd::before{will-change:auto}
}

/* ── Brand mark: inline vector, not a rasterised image ─────────────
   The rocks logo was <img src="aah-rocks-spin.svg">. Two things in that
   file make Chromium bake it to a bitmap rather than draw it as vector:
   it animates internally (reveal / spin / breathe), and the whole
   artwork sits inside an feDropShadow filter. The cached raster is then
   scaled to fit, so on a 3x phone a 21px mark was an upscaled 1x
   bitmap — soft and muddy, while looking fine on a 1x desktop.

   Inlined, it draws as live vector at whatever the device resolution
   is. The drop shadow is dropped with it: at 21px it computed to a
   0.56px blur, invisible, and it was half the reason for the raster.
   The .svg file stays for the favicon, where a file is required. */
.brand-mark{display:block;flex:none}
.aahr-c1{fill:#f9a55e;opacity:.75}
.aahr-c2{fill:#f0535b;opacity:.99}
.aahr-c3{fill:#027f82;opacity:.83}
.aahr-reveal,.aahr-breathe,.aahr-spin{
  transform-box:view-box;
  transform-origin:51.805px 58.775px;
}
@media(prefers-reduced-motion:no-preference){
  .aahr-reveal{animation:aahr-reveal 1.2s cubic-bezier(.2,.7,.2,1) both}
  .aahr-spin{animation:aahr-spin 18s linear infinite;animation-delay:1.2s}
  .aahr-breathe{animation:aahr-breathe 7s ease-in-out infinite;animation-delay:1.2s}
}
@keyframes aahr-reveal{from{opacity:0;transform:scale(.6)}to{opacity:1;transform:scale(1)}}
@keyframes aahr-spin{to{transform:rotate(360deg)}}
@keyframes aahr-breathe{0%,100%{transform:scale(1)}50%{transform:scale(1.025)}}
/* The mark is decorative here -- the wordmark beside it carries the
   name -- so the link keeps its own accessible name and this is hidden
   from assistive tech, same as the alt="" it replaces. */
/* Mirrors the sizes the old .hero-nav-brand img rules set, which now
   match nothing: 34 on desktop, stepping down on phones. */
.hero-nav-brand .brand-mark{width:34px;height:34px}
@media(max-width:700px){.hero-nav-brand .brand-mark{width:24px;height:24px}}
@media(max-width:429px){.hero-nav-brand .brand-mark{width:21px;height:21px}}

/* ══════════════════════════════════════════════════════════════════
   THE LIGHTS: continuous drift, not oscillation
   ══════════════════════════════════════════════════════════════════
   aah-bloom-a/b bob because of how they are built, not how fast they
   run. Each keyframe list starts and ends on the same transform, so
   the path retraces itself; it has three waypoints, so the turning
   points are visible; and ease-in-out decelerates to a full stop at
   every one of them. A light that stops dead and reverses reads as a
   bob no matter how long you make the duration.

   Fluid motion needs the opposite: constant speed and no turning
   points at all. These trace a CIRCLE — eight waypoints around one
   revolution, linear timing — so the layer is always moving at the
   same rate and never reverses. 0% and 100% are the same point on the
   circle, so the loop is seamless with no seam to notice.

   Two layers drift in opposite directions on periods that do not
   divide into each other (73s / 109s), so the composite takes ~22
   minutes to repeat and never settles into a rhythm you can read.

   Offsets are in vmin, not %. A percentage translate resolves against
   the layer's own box, which is viewport-shaped -- so on a wide screen
   the "circle" came out a stretched ellipse and the light sped up
   along the long axis and slowed at the ends. Measured: radius swung
   17px to 40px and speed varied 51%. vmin is the same length on both
   axes, so the path is a real circle and the speed is genuinely
   constant. */
@keyframes aah-drift-a{
      0%{transform:translate(0.000vmin,-2.600vmin)}
   12.5%{transform:translate(1.838vmin,-1.838vmin)}
     25%{transform:translate(2.600vmin,-0.000vmin)}
   37.5%{transform:translate(1.838vmin,1.838vmin)}
     50%{transform:translate(0.000vmin,2.600vmin)}
   62.5%{transform:translate(-1.838vmin,1.838vmin)}
     75%{transform:translate(-2.600vmin,0.000vmin)}
   87.5%{transform:translate(-1.838vmin,-1.838vmin)}
    100%{transform:translate(-0.000vmin,-2.600vmin)}
}
@keyframes aah-drift-b{
      0%{transform:translate(0.000vmin,-3.200vmin)}
   12.5%{transform:translate(2.263vmin,-2.263vmin)}
     25%{transform:translate(3.200vmin,-0.000vmin)}
   37.5%{transform:translate(2.263vmin,2.263vmin)}
     50%{transform:translate(0.000vmin,3.200vmin)}
   62.5%{transform:translate(-2.263vmin,2.263vmin)}
     75%{transform:translate(-3.200vmin,0.000vmin)}
   87.5%{transform:translate(-2.263vmin,-2.263vmin)}
    100%{transform:translate(-0.000vmin,-3.200vmin)}
}

/* A second light layer over the field, so the two drift across each
   other instead of the whole sky sliding as one rigid sheet. Its
   sources sit where the first layer's are sparse. */
#main-scroll::after{
  content:'';
  position:fixed;
  inset:-6%;
  z-index:0;
  pointer-events:none;
  background:
    radial-gradient(38% 34% at 72% 8%,  rgba(212,144,10,.16), transparent 70%),
    radial-gradient(42% 36% at 8% 52%,  rgba(224,90,78,.16),  transparent 72%),
    radial-gradient(40% 34% at 94% 68%, rgba(2,127,130,.18),  transparent 70%),
    radial-gradient(34% 30% at 40% 94%, rgba(61,139,139,.16), transparent 72%);
}
/* The field itself needs room to move without pulling its edge into
   view — the drift reaches 2.1%, so 6% of slack is plenty. */
#main-scroll::before{inset:-6%}

@media(prefers-reduced-motion:no-preference){
  #main-scroll::before{animation:aah-drift-a 73s linear infinite}
  #main-scroll::after{animation:aah-drift-b 109s linear infinite reverse}
  /* The footer light was the one visibly bobbing: 32s of ease-in-out
     on a three-point path that returned to its own start. Same circle,
     same constant speed, a third period again. */
  .sheet--charcoal:not(.sheet--products)::before{
    animation:aah-drift-a 91s linear infinite;
  }
  .pd.open::before{animation:aah-drift-b 97s linear infinite}
}

/* Phones get the lights too. They were switched off in the mobile
   performance pass, but that pass lumped them in with the genuinely
   expensive things and they are not the same kind of cost: a transform
   animation over a static background rasterises ONCE and is then
   composited on the GPU each frame. The per-frame work is a matrix
   multiply. What actually cost frames there was backdrop-filter, which
   re-samples and re-blurs every frame, and a JS rAF loop writing to a
   hidden element — both still gone.

   The drift is in vmin, so it scales with the screen: ~19px across on
   a desktop, ~10px on a phone. Same motion, same proportion of the
   viewport, so it reads the same on both. */
@media(max-width:700px){
  /* One layer, not two. The gradients are memory, not frame time, and
     two full-viewport stacks at DPR 3 is the one part worth halving —
     the second layer's job is to break up the rigid-sheet feel, which
     matters far less on a screen this size. */
  #main-scroll::after{display:none}
}

/* ══════════════════════════════════════════════════════════════════
   NAVBAR: one fluid bar instead of two stepped ones
   ══════════════════════════════════════════════════════════════════
   The bar was measured at 47px up to 700px and 79px from 701px — a
   32px jump at a single breakpoint, with Kalvin popping into existence
   at the same moment. Every metric was authored twice: a phone set and
   a desktop set, with nothing in between, so a tablet or a resized
   window fell off a cliff.

   These are the same metrics expressed once, with clamp(), so they
   interpolate across the whole range instead of switching. Nothing
   steps: the bar grows from ~48px to ~70px continuously.

   Kalvin's lane is part of that interpolation rather than a fixed
   +20px that appears with him — it opens up gradually as the viewport
   widens, so by the time he is shown the space is already there. */
.hero-nav{
  flex-direction:row;
  align-items:center;
  gap:clamp(6px,1vw,12px);
  padding-top:clamp(4px,.7vw,10px);
  padding-left:clamp(8px,1.4vw,18px);
  padding-right:clamp(8px,1.4vw,18px);
  /* Opens from 4px to 20px between roughly 620px and 880px wide.
     Use 100% of the nav (not 100vw) so Windows classic scrollbars do
     not inflate the calc relative to macOS overlay scrollbars. */
  padding-bottom:clamp(4px,calc((100% - 620px) * .062),20px);
}
.hero-nav-brand{align-self:auto;padding:3px 4px}
.hero-nav-brand .brand-mark{
  width:clamp(21px,2.6vw,34px);
  height:clamp(21px,2.6vw,34px);
}
.hero-nav-brand span{
  display:inline;
  font-size:clamp(11px,1.35vw,17px);
  letter-spacing:-.01em;
}
.hero-nav-links{
  display:flex;
  width:auto;
  justify-content:flex-end;
  gap:0;
}
.hero-nav-links a{
  display:flex;
  align-items:center;
  justify-content:center;
  font-size:clamp(11.5px,1.15vw,15px);
  padding:10px clamp(4px,.8vw,14px);
  min-height:40px;
  line-height:1;
  white-space:nowrap;
}
/* Still no horizontal room for him on a phone — brand plus links use
   357 of the 360 available at 390px. He returns once the bar is wide
   enough that his lane is fully open, so he never appears into a gap
   that is still growing. */
.nav-beaver{display:none}
@media(min-width:981px){
  .nav-beaver{display:block}
}
/* (The old 344px wordmark rule is folded into the 499px one above —
   the words now come off at 500px so Kalvin has somewhere to walk.) */
/* Tracks the fluid bar's bottom edge with headroom to spare; anything
   that clears the navbar derives from this. */
:root{--nav-h:76px}

/* ── Products → team seam: the corner notches ──────────────────────
   #products-sec's host is transparent, so the lit field shows through
   it. #team-sec's host paints an opaque flat var(--charcoal) — set on
   the line directly below it in the source, at identical specificity,
   so the two resolve OPPOSITELY by source order alone.

   That only shows where the cream sheet's rounded top corners cut a
   notch: the notch revealed flat unlit charcoal while the band above
   it showed the lit field, so the corners read as black patches
   against a warm surround. Making the host transparent lets the same
   field show through both, and the corners match the chapter. */
#team-sec.sheet-host--charcoal{background:transparent}

/* ── 19. Product overlays and footer at phone width ────────────────
   These two surfaces were the last never reviewed narrow. All of it is
   phone-only; nothing here changes at 900 or 1280. */
@media(max-width:700px){
  /* The ≤700 block insets .pd-body to 22px, but .pd-cta is a SIBLING
     of .pd-body, not a child, so it never got the memo and kept the
     desktop 48px. At 390 the section runs 22→368 while the CTA content
     runs 48→342. At 320 that costs the heading 52px of its 276px
     measure — exactly enough to break "Want to know more?" over two
     lines and orphan the last word. Inline only; the vertical rhythm
     is right as it is. */
  .pd-cta{padding-inline:22px}

  /* .pd-cta is a flex column with align-items:flex-start, so
     .pd-cta-btns is sized to fit-content — and its max-content is the
     two buttons side by side, 341.8px, frozen however much room there
     is. The width:100% on .pd-btn then resolves against that 341.8
     rather than the column. Below ~386px the box is clamped by the
     available width and looks right by accident; at 430 the buttons
     stop 44px short of the gutter under a heading that reaches it. */
  .pd-cta-btns{align-self:stretch}

  /* `1fr` is minmax(auto,1fr) and that auto floor is content-based, so
     the track refuses to go below its min-content. In Vibe Consult's
     preview the captions and demo note put that floor at 295.8px, so
     on a 320 screen the rail overhangs its own 276px column: 22px
     gutter one side, 2.2px the other. minmax(0,1fr) lets it shrink —
     the carousel has its own overflow-x, so nothing is lost. */
  .pd-section{grid-template-columns:minmax(0,1fr)}

  /* Arrows and progress bar are both display:none at ≤700, so the dots
     are the only carousel control left — at 8x8px, against WCAG 2.5.8's
     24px minimum. The painted pill is the right SIZE; it is the hit
     area that is wrong, so an ::after carries the target and the dot
     keeps its look, its aria-current width change and its per-product
     accent. -16/-8 gives a 24x40 target, and the 16px gap makes the
     pitch 24px too, so neighbours tile edge to edge instead of one
     dot's target swallowing the next. */
  .pd-carousel-dots{gap:16px;margin-top:18px}
  .pd-carousel-dot{position:relative}
  .pd-carousel-dot::after{content:'';position:absolute;inset:-16px -8px}

  /* .footer-copy-mark is a flex row over three things: the mark, a bare
     text node, and the Algonquin link. Flex wraps loose text into an
     anonymous item, so the sentence is really TWO flex items side by
     side — and with nowrap they cannot get out of each other's way. It
     wants 303.6px on one line, so below 343px the text is squeezed to
     150.5px and wraps internally while the link holds its own column.
     At 320 that reads "© 2026 AI Accelerator / Hub ·" BESIDE
     "Algonquin / College" — two columns of a one-line sentence. Wrap
     lets the link drop to its own line instead of being crushed beside
     the text, and only fires when it genuinely cannot fit. */
  .footer-copy-mark{flex-wrap:wrap;row-gap:2px}
}

/* Canonical team profile links preserve the card typography while making
   every student identity directly crawlable. */
.team-profile-name{color:inherit;text-decoration:none;text-underline-offset:3px}
.team-profile-name:hover{text-decoration:underline}
.team-profile-name:focus-visible{outline:2px solid var(--focus);outline-offset:2px;border-radius:2px}

/* ── 21. The rocks mark: animate the box, not the groups ───────────
   Inlining the logo fixed the blur but introduced a per-frame cost.
   The animations sat on SVG <g> elements, and an SVG transform is not
   compositor-accelerated the way a transform on an HTML box is — it
   runs on the main thread and forces layout and paint every frame,
   twice over, since the mark appears in the navbar and the footer.
   Disabling them dropped Layout and Paint to zero, which also cleared
   the drift lights of causing any paint at all.

   The fix is to animate the <svg> element itself, which IS an HTML box.
   The pivot survives the move for free: the SVG's transform-origin
   (51.805, 58.775) is the exact centre of its own viewBox
   (-33.2 -26.225 170 170), so 50% 50% on the element is the same point.

   Two animations on two DIFFERENT properties, so they compose instead
   of fighting over `transform`: opacity for the entrance, rotation for
   the spin. Both are compositable. The old 7s "breathe" scale is gone —
   it shared the transform channel, and a 2.5% pulse was not worth a
   main-thread repaint on every frame of the page's life. */
.brand-mark{
  transform-origin:50% 50%;
  will-change:transform;
}
@media(prefers-reduced-motion:no-preference){
  .brand-mark{
    animation:
      aahr-fade .9s ease-out both,
      aahr-spin 18s linear 1.2s infinite;
  }
}
/* The groups keep their shape and colour and stop animating. */
.aahr-reveal,.aahr-breathe,.aahr-spin{animation:none!important}
@keyframes aahr-fade{from{opacity:0}to{opacity:1}}

/* ── 22. Kalvin on phones ──────────────────────────────────────────
   He was hidden below 981px because the old stepped bar had no room.
   The fluid bar has a lane already — it just needed to open at the
   narrow end too instead of starting at 4px. A flat ~20px floor gives
   him the same strip on a phone that he has on a desktop, and the bar
   only grows from 49px to about 64px to pay for it.

   Clarity: the two stacked drop-shadows were softening a 21px-tall
   sprite into mush — a 5px blur on a body that is only 18px tall is
   most of his silhouette. One tight contact shadow keeps him grounded
   without smearing him. Size is set in the flex-track block below
   (desktop 92×46, phone 70×35) so this intermediate override stays
   aligned with the winning window rather than fighting it. */
.hero-nav{padding-bottom:clamp(20px,calc((100% - 620px) * .062),22px)}
.nav-beaver{
  display:block;
  width:92px;
  height:46px;
  bottom:1px;
  filter:drop-shadow(0 1px 1px rgba(20,18,15,.45));
}
.nav-beaver-sprite{
  width:92px;
  height:46px;
  background-size:736px 46px;
}
@media(max-width:700px){
  .nav-beaver{display:block;--kb-left:12px;--kb-right:12px}
}

/* ── 23. Kalvin takes the leftover gap, not extra bar height ───────
   Buying him a lane with asymmetric padding (4px top, 20px bottom)
   pushed the brand and links up against the top edge and left a bare
   strip under them — that is what read as wrong once he appeared. It
   also cost the logo: squeezing the bar back down had shrunk the mark
   to 21px on a phone.

   He is a flex item between the brand and the links now, so flexbox
   gives him exactly the room that is left over at any width and he
   needs no height of his own. Padding goes back to symmetric, the bar
   returns to ~50px, and the mark keeps a 28px floor.

   Three parts: .nav-beaver is the track (flexible width), .nav-beaver-win
   is the frame window that slides along it, .nav-beaver-sprite is the
   8-frame strip behind the window.

   Window is 92×46 — the sheet cells are 138×69 (102×57 art plus 24px
   more tail room and 12px of raised-tail headroom), displayed at 46/69
   so the body stays 38px tall and one cell still equals one window.
   background-size is 8× the window so the walk cycle frames one cell. */
.hero-nav{
  padding-top:clamp(6px,.8vw,11px);
  padding-bottom:clamp(6px,.8vw,11px);
  overflow:visible;
}
.hero-nav-shell{overflow:visible}
.hero-nav-brand .brand-mark{
  width:clamp(28px,2.8vw,34px);
  height:clamp(28px,2.8vw,34px);
}
.nav-beaver{
  display:block;
  position:relative;
  flex:1 1 auto;
  min-width:0;
  align-self:center;
  height:46px;
  /* Visible so the raised paddle can paint above the 46px lane. Brand
     and links stay z-index 2, so he cannot cover Home. overflow-x/y
     cannot be mixed (visible becomes auto), so the track does not clip.
     preserve-3d so the window's perspective scene is not flattened. */
  overflow:visible;
  transform-style:preserve-3d;
  pointer-events:none;
  user-select:none;
  z-index:0;
  /* Filter here flattens 3D descendants. Contact shadow lives on the
     sprite. .hero-nav .nav-beaver also forces filter:none below. */
  filter:none;
  left:auto;bottom:auto;width:auto;
}
.nav-beaver-win{
  position:absolute;
  top:0;left:0;
  width:92px;
  height:46px;
  --kb-win:92px;
  /* Real 3D scene for the sprite's yaw. perspective() as a transform
     function on the rotating element itself is what made the previous
     pivot read as a 2D squash. Origin at the feet. */
  perspective:220px;
  perspective-origin:50% 80%;
  transform-style:preserve-3d;
  transform-origin:50% 80%;
  backface-visibility:visible;
  /* Visible so the drop-shadow, the 3D pop, and a couple of tail px
     are not sheared by the clip. Filter must NOT live here: it flattens
     preserve-3d children. Brand/links paint above him. */
  overflow:visible;
}
.nav-beaver-sprite{
  display:block;
  width:92px;
  height:46px;
  background:url('kalvin-walk-sprite.png?v=6') left top / 736px 46px no-repeat;
  transform-origin:50% 80%;
  transform-style:preserve-3d;
  backface-visibility:visible;
}
/* Getting him onto a phone is a question of horizontal room, and beside
   the content there is none. Measured track: 0px at 415, 49px at 465.
   Dropping the wordmark bought only ~35px — at 375 that is 23px of
   travel, still twitching rather than walking. Shrinking him or the
   type to claw back more would make him harder to see, which is the
   opposite of what was asked.

   So on phones he stops competing for the row and walks the FULL width
   BEHIND the content instead: absolutely positioned across the bar at
   z-index 0, with the brand and links lifted to z-index 1. The bar's
   own background still paints under him, so he is on the bar rather
   than behind it. He passes behind the wordmark and the links the way
   something walks behind a post — and briefly behind the active pill,
   which is the only opaque thing up there.

   That turns 23px of travel into ~305px, keeps the 28px mark, keeps
   the wordmark, and costs no bar height. Desktop is untouched: it
   keeps the flex track between brand and links. */
@media(max-width:499px){
  .nav-beaver{
    display:block;
    position:absolute;
    left:10px;
    right:10px;
    bottom:2px;
    top:auto;
    width:auto;
    height:35px;
    flex:none;
    z-index:0;
  }
  /* Phone keeps the 46→35 height step; width follows the 138px cell
     so the tail/headroom padding stays in proportion (70/35 = 92/46). */
  .nav-beaver-win{
    width:70px;
    height:35px;
    --kb-win:70px;
  }
  .nav-beaver-sprite{
    width:70px;
    height:35px;
    background-size:560px 35px;
  }
  /* Lifted so he passes behind them rather than over. */
  .hero-nav-brand,
  .hero-nav-links{position:relative;z-index:1}
}

/* ── 24. The tail twitch ───────────────────────────────────────────
   Two causes, both about resampling a thin feature every frame.

   1. The drop-shadow sat on .nav-beaver, which is the TRACK — a static
      full-width box. The thing that moves is .nav-beaver-win inside it,
      so the filter had to be recomputed across the whole track on every
      frame the window slid. A filter re-rasterises what it covers, and
      the thinnest feature in the sprite is the tail, so the tail is
      where that shows. On the window it is computed once over 34px and
      then simply translated.

   2. The sprite was a 1600x115 strip squeezed into 272x19 CSS px: a
      5.9x downscale, with a cell ratio of 1.739 against a window ratio
      of 1.789, so it was stretched 2.9% horizontally as well. Both are
      redone per frame as background-position steps. The asset is re-cut
      to 1104x69 — 8 cells of 138×69 (102px of art plus 36px of empty
      tail room and 12px above the raised paddle) — and displayed at
      92×46 (desktop) / 70×35 (phone). background-size must stay 8×
      the window so only one cell shows.

   Cache-bust to v6: the sheet grew, and a stale v5 912px strip behind
   the new background-size would squeeze all eight frames into the
   window at once. That has bitten this sprite before. */
/* Scoped to the bar rather than bare .nav-beaver: three separate
   .nav-beaver blocks set this filter earlier in the file, and relying
   on source order to beat all of them is exactly how this stylesheet
   accumulated its contradictions. */
.hero-nav .nav-beaver{filter:none}
.hero-nav .nav-beaver-win{filter:none}
.hero-nav .nav-beaver-sprite{filter:drop-shadow(0 1px 1px rgba(20,18,15,.45))}
.nav-beaver-sprite{
  /* PNG first so a browser without image-set() still gets a beaver; the
     lossless webp below is 36KB against 59KB on the first-load path. */
  background-image:url('kalvin-walk-sprite.png?v=6');
  background-image:image-set(
    url('kalvin-walk-sprite.webp?v=3') type('image/webp'),
    url('kalvin-walk-sprite.png?v=6') type('image/png')
  );
  background-size:736px 46px;
}
/* Phone sizes must follow the desktop background-size above, or that
   bare .nav-beaver-sprite rule wins and the strip no longer matches
   the 70px window (all eight frames squeeze into view again). */
@media(max-width:499px){
  .nav-beaver{height:35px;bottom:2px;overflow:visible}
  .nav-beaver-win{width:70px;height:35px;--kb-win:70px}
  .nav-beaver-sprite{
    width:70px;
    height:35px;
    background-size:560px 35px;
  }
}

/* ── 25. Stop the navbar aura repainting on every frame ────────────
   --aah-nav-angle is a registered @property animated 0->360deg over
   16s, and it is consumed as the `from` angle of two conic gradients on
   .hero-nav-shell — one of them under filter:blur(8px).

   A custom property that feeds a gradient cannot be composited. The
   gradient has to be re-rasterised every frame it changes, and the
   blurred copy re-blurred with it, across a full-width position:fixed
   bar, for the life of the page. Because the bar is fixed, that repaint
   also lands in the middle of every scroll, which is exactly when it is
   most felt — this is the sticky scrolling.

   The irony is that the @supports fallback right below the keyframes
   already does the cheap version: it rotates the layers with a
   transform, which the compositor handles. The modern path was the
   expensive one.

   Held still here rather than rotated. The aura is a 1px lit ring plus
   a soft outer glow — .hero-nav covers everything between them — so
   what the rotation actually buys is a slow hue drift on a hairline,
   which is not worth a full-bar repaint every frame. The gradient, the
   glow and the ring all stay exactly as they are.

   (If the drift is wanted back, the fallback's approach is the way: an
   oversized square layer rotated by transform, rasterised once.) */
.hero-nav-shell,
.hero-nav-shell::before,
.hero-nav-shell::after{
  animation:none !important;
}

/* ── 26. The navbar blur was only removed on phones ────────────────
   .hero-nav declares backdrop-filter:blur(12px), and section 22 above
   only cancelled it under max-width:700px — so every desktop was still
   running it. The bar's background resolves to fully opaque charcoal,
   so the blur samples the page behind it, blurs it, and then has that
   result painted over completely. It is invisible at every width, and
   because the bar is position:fixed the browser re-does it on every
   frame the page scrolls beneath it.

   That is the other half of the sticky scrolling, and scoping the
   earlier fix to phones was my mistake. Removed everywhere. */
.hero-nav{
  backdrop-filter:none;
  -webkit-backdrop-filter:none;
}

/* ── 27. Move the light field off the scroller ─────────────────────
   This is the Chrome-only stickiness, and it is my doing. When I built
   the unified light field I attached both layers to #main-scroll as
   position:fixed pseudo-elements. Fixed content INSIDE a scroll
   container means the compositor cannot simply translate that
   scroller's layer -- the fixed children have to stay put while
   everything around them moves -- so Chrome drops the whole scroller to
   MAIN-THREAD SCROLLING. That is precisely the "heavy wheel in Chrome,
   fine everywhere else" symptom: other engines resolve it differently.

   The layers are viewport-fixed anyway, so nothing about them needs to
   live inside the thing that scrolls. Moved onto .app, which is the
   scroller's parent and never scrolls. The scroller is left with no
   fixed descendants and can be composited again.

   The base charcoal moves with them: it was painted on #main-scroll,
   which would now sit opaque on top of the lights and hide them. */
.app{
  position:relative;
  background:var(--charcoal);
  isolation:isolate;
}
#main-scroll{background:transparent}
#main-scroll::before,
#main-scroll::after{content:none;display:none}

.app::before,
.app::after{
  content:'';
  position:fixed;
  inset:-6%;
  z-index:0;
  pointer-events:none;
}
.app::before{
  background:
    radial-gradient(52% 44% at 14% 12%, rgba(224,90,78,.30), transparent 68%),
    radial-gradient(48% 42% at 88% 26%, rgba(61,139,139,.26), transparent 70%),
    radial-gradient(44% 38% at 78% 82%, rgba(212,144,10,.16), transparent 72%),
    radial-gradient(42% 36% at 20% 76%, rgba(2,127,130,.20), transparent 70%),
    radial-gradient(34% 30% at 50% 48%, rgba(224,90,78,.10), transparent 74%);
}
.app::after{
  background:
    radial-gradient(38% 34% at 72% 8%,  rgba(212,144,10,.16), transparent 70%),
    radial-gradient(42% 36% at 8% 52%,  rgba(224,90,78,.16),  transparent 72%),
    radial-gradient(40% 34% at 94% 68%, rgba(2,127,130,.18),  transparent 70%),
    radial-gradient(34% 30% at 40% 94%, rgba(61,139,139,.16), transparent 72%);
}
/* The scroller and everything in it sits above the field. */
#main-scroll{position:relative;z-index:1}

@media(prefers-reduced-motion:no-preference){
  .app::before{animation:aah-drift-a 73s linear infinite}
  .app::after{animation:aah-drift-b 109s linear infinite reverse}
}
/* One layer on phones, same reasoning as before: the gradients are the
   memory cost and the second layer only breaks up the rigid-sheet feel,
   which matters far less at that size. */
@media(max-width:700px){
  .app::after{display:none}
}

/* ══════════════════════════════════════════════════════════════════
   28. RETIRE THE NESTED SCROLLER
   ══════════════════════════════════════════════════════════════════
   html/body were overflow:hidden with #main-scroll as an inner
   scrolling element. That existed to drive scroll-snap chapters — and
   snap was switched off (scroll-snap-type:none, section 781) once the
   sections outgrew the viewport. So the scroller has been buying
   nothing for a while, while still costing Chrome's root-scroller
   behaviour: native trackpad momentum, scroll anchoring, the browser's
   own scrollbar, find-in-page, Home/End. None of that is something a
   rendering fix can give back, which is why the page kept feeling
   unfamiliar in Chrome after each of the earlier repairs.

   This is not a new layout. The site has document-scrolled below 980px
   all along; these are the same declarations, no longer gated to
   narrow widths. */
html,body{
  height:auto;
  overflow:visible;
}
/* Document is the scroller now. Reserve the gutter on every OS so
   Windows classic scrollbars do not steal ~15px that macOS overlays
   leave free — otherwise the hero split shifts between machines. */
html{
  scrollbar-gutter:stable;
  scrollbar-width:thin;
  scrollbar-color:rgba(255,255,255,.18) transparent;
}
html::-webkit-scrollbar{width:10px}
html::-webkit-scrollbar-track{background:transparent}
html::-webkit-scrollbar-thumb{background:rgba(255,255,255,.18);border-radius:999px}
.app{
  display:block;
  height:auto;
  width:100%;
  max-width:100%;
  box-sizing:border-box;
}
.main{
  display:block;
  overflow:visible;
  flex:none;
  min-height:0;
  /* Chaining had nowhere to go once html/body stopped scrolling, and
     with the document scrolling it would suppress the natural boundary
     behaviour people expect. */
  overscroll-behavior-y:auto;
  scrollbar-gutter:auto;
}
/* In-page jumps have to clear the fixed bar; this moves from the old
   scroller to the element that scrolls now. */
html{scroll-padding-top:calc(var(--nav-h) + 8px)}
#main-scroll{scroll-padding-top:0}

/* ── 27. Hero socials: perfect circles, no chipped bottoms ────────
   .social-link used overflow:hidden + backdrop-filter + border-radius
   50%. Chromium clips that combo a pixel short on the bottom, and the
   inline SVG sat on the line box so the pink/blue mark looked sheared.
   The row also sits at the end of .hero-row; a parent clip would take
   the same bite. Equal box, radius 50%, no overflow clip. */
.hero-socials{
  overflow:visible;
  align-items:center;
  padding-bottom:4px;
  line-height:0;
}
.social-link{
  display:flex;
  align-items:center;
  justify-content:center;
  box-sizing:border-box;
  flex:0 0 auto;
  width:40px;
  height:40px;
  aspect-ratio:1/1;
  border-radius:50%;
  line-height:0;
  overflow:visible;
  backdrop-filter:none;
  -webkit-backdrop-filter:none;
}
.social-link svg{display:block;flex-shrink:0}
