/* base.css — shared by every page on jamresources.com.
   Extracted from index.html on 2026-07-27. Home-only styles (hero, ticker,
   services, lab, process) stayed inline; everything here is used by two or
   more pages. Load order matters: this file first, then any page-local
   <style> block, so page-local rules win ties. */

  /* ============ COLOUR / TYPE / MOTION TOKENS ============
     Moved to tokens.css on 2026-08-20 — see that file's header for why.
     Every page loads tokens.css BEFORE this file. If colours vanish site-wide,
     that link is missing, not these rules. */


  /* ============ FONT FALLBACKS ============ */
  /* Metric-matched local fallbacks so the swap doesn't reflow the page.
     Numbers are measured, not guessed (2026-07-28): size-adjust is the
     rendered width ratio vs Arial at the weights the site actually uses
     (Syne 800 uppercase display strings: 1.650; Manrope ~500 body copy:
     1.04); ascent/descent come from each font's hhea table divided by
     size-adjust (Syne 925/-275 @1000upm, Manrope 2132/-600 @2000upm). */
  @font-face{
    font-family:'Syne Fallback';src:local('Arial');
    size-adjust:165%;ascent-override:56.1%;descent-override:16.7%;line-gap-override:0%;
  }
  @font-face{
    font-family:'Manrope Fallback';src:local('Arial');
    size-adjust:104%;ascent-override:102.5%;descent-override:28.9%;line-gap-override:0%;
  }

  /* ============ SHARED FRAME ============ */
  /* Native cross-document View Transitions — a Services tile morphs into its
     detail page's hero, matched by view-transition-name (set per-service in
     each page's own <style> block: the .svc tile here, .svc-hero there).
     Unsupported browsers ignore this at-rule entirely and just navigate —
     there is no fallback to write, that's the whole point of the spec. */
  @view-transition{
    navigation:auto;
  }

  *{margin:0;padding:0;box-sizing:border-box}
  /* scroll-padding-top clears the FIXED nav for every in-page anchor at once.
     It must match the SCROLLED nav height (14px padding, not the resting 22px),
     because by the time an anchor jump lands the nav has already shrunk.
     Per-anchor scroll-margin does not scale — there are ~20 anchors site-wide
     plus every ⌘K hash destination, and only [id^="lab-"] was ever covered. */
  html{scroll-behavior:smooth;overflow-x:hidden;scroll-padding-top:88px}
  /* dark-only site: tell the UA so native selects, scrollbars and form
     controls stop rendering light against it */
  :root{color-scheme:dark}
  body{
    background:var(--void);
    color:var(--white);
    font-family:var(--body);
    font-size:16px;
    line-height:1.6;
    -webkit-font-smoothing:antialiased;
  }

  /* ============ WRAPPING ============
     The one typographic thing this site was missing. Tracking was already
     optically scaled before this — the display values are em-based
     (-.01em reads as -0.7px at 70px and -1.12px at 112px), so they tighten
     with size on their own and were deliberately left alone.

     balance on headings: the browser evens the line lengths instead of
     filling greedily and dropping a stray word onto the last line. Headings
     only — it is capped at a handful of lines by design and costs layout time
     proportional to the text, so it has no business on body copy. It does NOT
     override an explicit <br>, which is why the hand-broken headlines
     ("Six ways<br>to get loud.") are unaffected; the ones this actually fixes
     are the long unbroken ones like /products/ at 4 lines and /about/ at 6.

     pretty on body copy: leaves the last line at least two words, so a
     paragraph never ends on a lone orphan.

     Both degrade to normal wrapping where unsupported — no fallback needed,
     and nothing shifts on browsers that lack them. */
  h1,h2,h3,.sec-title{ text-wrap:balance }
  p,li,.sub,.lede,.blurb,.kicker{ text-wrap:pretty }
  /* NOTE — body must NOT carry overflow-x:hidden.
     html's overflow-x:hidden above already propagates to the viewport, so
     the copy here was redundant; what it was not was harmless. Any overflow
     value other than visible makes body a scroll container, and because body
     is exactly as tall as its own content it is a scroll container that can
     never scroll. Every view() timeline resolves to the nearest scrollport,
     found that dead one, and went permanently inactive — which silently
     disabled both the reveal engine and the process glow site-wide.
     If horizontal overflow ever returns, fix the element that overflows;
     do not put overflow-x back on body. */
  ::selection{background:var(--raspberry);color:var(--void)}
  a{color:inherit;text-decoration:none}
  :focus-visible{outline:2px solid var(--cyan);outline-offset:3px;border-radius:var(--r-xs)}
  /* No <img> ships today — every mark is inline SVG, gradient or canvas. This
     is the guard for the first photo that lands (a case study on /work/):
     width/height keep the box reserved so it can't shift layout on load. */
  img,video{max-width:100%;height:auto}

  /* ============ SKIP LINK ============
     WCAG 2.2 §2.4.1 Bypass Blocks, Level A. Every page repeats the same nav
     block above a full-height hero, so without this a keyboard user tabs the
     whole nav on every page.
     Hidden by TRANSFORM, never display:none or visibility:hidden — those also
     remove it from the tab order, which is the standard way to ship a skip
     link that silently does nothing. */
  .skip-link{
    position:fixed;top:12px;left:12px;z-index:var(--z-skip);
    font-family:var(--mono);font-size:.72rem;letter-spacing:.1em;text-transform:uppercase;
    background:var(--white);color:var(--void);
    padding:.85em 1.2em;border-radius:var(--r-pill);
    transform:translateY(calc(-100% - 24px));
    transition:transform .18s var(--ease-out);
  }
  .skip-link:focus{transform:translateY(0)}
  @media (prefers-reduced-motion:reduce){.skip-link{transition:none}}
  /* the jump target must not draw a ring just because we moved focus to it */
  main:focus{outline:none}

  /* ============ BACKGROUND ============
     Replaces the cursor-reactive WebGL ink sim (fluid.js) on the homepage
     and the drifting .blob-fallback everywhere else. The sim went because
     it was the only thing on the page that moved in response to the
     pointer, so the eye tracked it instead of the copy; the blobs went so
     that all eight pages share one treatment instead of two.

     One static layer, every page, no per-page body class. To change the
     treatment, change this one rule.

     +2026-08-20 — "static, no JS" no longer holds. That is a deliberate
     override by Jake, not drift. The layer now drifts very slowly (jam.css §6)
     and jam.js parks it whenever the reader stops scrolling.

     The rationale above still governs the SHAPE of the exception. What got the
     ink sim deleted was motion that tracked the POINTER and pulled the eye off
     the copy — so the replacement is neither pointer-driven nor perceptible as
     motion: 82s per cycle, under 2% travel, and fully stopped the moment
     scrolling stops. Pointer-reactivity now lives only on discrete targets the
     reader is already looking at (base.js MAGNETS), never on this layer. */
  .bg{
    position:fixed;inset:0;z-index:0;pointer-events:none;
    background:
      radial-gradient(120% 90% at 12% -10%, color-mix(in oklab, var(--brand-2) 22%, transparent) 0%, transparent 55%),
      radial-gradient(100% 80% at 92% 108%, color-mix(in oklab, var(--heat) 15%, transparent) 0%, transparent 55%);
  }

  /* film grain */
  .grain{
    position:fixed;inset:-50%;width:200%;height:200%;z-index:var(--z-grain);pointer-events:none;opacity:.05;
    background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
    animation:grainShift 1.2s steps(4) infinite;
  }
  @keyframes grainShift{
    0%{transform:translate(0,0)}25%{transform:translate(-2%,1%)}
    50%{transform:translate(1%,-2%)}75%{transform:translate(-1%,2%)}100%{transform:translate(2%,1%)}
  }

  /* ============ NAV ============ */
  nav{
    position:fixed;top:0;left:0;right:0;z-index:var(--z-nav);
    display:flex;align-items:center;justify-content:space-between;
    padding:22px var(--gutter);
    transition:background .4s,backdrop-filter .4s,padding .4s;
  }
  nav.scrolled{background:var(--scrim-2);backdrop-filter:blur(14px);padding:14px var(--gutter)}
  .wordmark{font-family:var(--display);font-weight:800;font-size:1.35rem;letter-spacing:.02em;display:flex;align-items:baseline;gap:.45em;line-height:1}
  .wordmark .jam{background:linear-gradient(100deg,var(--cyan),var(--cobalt) 60%,var(--cobalt-deep));-webkit-background-clip:text;background-clip:text;color:transparent}
  .wordmark .res{font-family:var(--mono);font-weight:400;font-size:.62rem;letter-spacing:.34em;color:var(--dim);text-transform:uppercase}
  .wordmark .dia{font-style:normal;color:var(--raspberry);font-size:.6em;line-height:1;transform:translateY(-.05em)}
  .nav-links{display:flex;gap:var(--sp-2);align-items:center;font-family:var(--mono);font-size:.78rem;letter-spacing:.12em;text-transform:uppercase}
  .nav-links a.nl{color:var(--dim);transition:color .25s;position:relative}
  @media (hover:hover){.nav-links a.nl:hover{color:var(--white)}}
  .nav-links a.nl::after{content:'';position:absolute;left:0;bottom:-5px;height:1px;width:0;background:var(--raspberry);transition:width .3s}
  @media (hover:hover){.nav-links a.nl:hover::after{width:100%}}
  .btn{
    display:inline-flex;align-items:center;gap:.6em;
    font-family:var(--mono);font-size:.78rem;letter-spacing:.12em;text-transform:uppercase;
    color:var(--void);background:var(--white);border:1px solid var(--white);
    padding:.85em 1.6em;border-radius:var(--r-pill);cursor:pointer;
    transition:background .3s,color .3s,transform .3s,box-shadow .3s;
  }
  @media (hover:hover){.btn:hover{background:var(--raspberry);border-color:var(--raspberry);color:var(--white);transform:translateY(-2px);box-shadow:0 12px 40px -8px rgba(255,46,99,.55)}}
  .btn.ghost{background:transparent;color:var(--white);border:1px solid var(--line-3)}
  @media (hover:hover){.btn.ghost:hover{border-color:var(--white);background:var(--line-1);color:var(--white);box-shadow:none}}
  .nav-burger{display:none}
  @media(max-width:760px){
    /* JS-dependent collapse only under html.js — no-JS mobile keeps real links */
    .js .nav-links a.nl{display:none}
    html:not(.js) .nav-links{flex-wrap:wrap;justify-content:flex-end;font-size:.62rem;gap:10px 14px}
    .btn{white-space:nowrap;font-size:.72rem;padding:.75em 1.3em}
    .js .nav-burger{
      display:flex;flex-direction:column;justify-content:center;gap:5px;
      width:44px;height:44px;padding:10px;margin-left:10px;flex:0 0 auto;
      background:none;border:0;cursor:pointer;position:relative;z-index:2;
    }
    .nav-burger span{display:block;height:2px;width:100%;background:var(--white);border-radius:var(--r-xs);transition:transform .3s,opacity .3s}
    #nav.menu-open{background:var(--scrim-4);backdrop-filter:blur(14px)}
    #nav.menu-open .nav-burger span:nth-child(1){transform:translateY(7px) rotate(45deg)}
    #nav.menu-open .nav-burger span:nth-child(2){opacity:0}
    #nav.menu-open .nav-burger span:nth-child(3){transform:translateY(-7px) rotate(-45deg)}
    #nav.menu-open .nav-links{
      position:absolute;top:100%;left:0;right:0;
      flex-direction:column;align-items:stretch;gap:0;
      padding:8px var(--gutter) 24px;
      background:var(--scrim-4);backdrop-filter:blur(14px);
      border-bottom:1px solid var(--line-1);
    }
    #nav.menu-open .nav-links a.nl{display:block;font-size:1rem;padding:14px 0;border-bottom:1px solid var(--line-1)}
    #nav.menu-open .nav-links .btn{margin-top:18px;justify-content:center}
  }
  /* The header row is logo + CTA + ⌘K + burger. Adding the ⌘K trigger pushed it past the
     viewport at every width up to 467px — the burger clipped 32px off-screen at 431px and
     10px at 360px, silently masked by the overflow-x:hidden above. Drop the header CTA
     below 560px: that clears the overflow band with margin, and the logo/CTA gap only
     becomes comfortable (24px) from ~560px up. The CTA is the safe one to cut — it still
     appears in the open drawer below, and in every page hero. */
  @media(max-width:560px){
    #nav:not(.menu-open) .nav-links .btn{display:none}
  }
  /* below ~430px the wordmark suffix doesn't fit either */
  @media(max-width:430px){
    .wordmark .res{display:none}
    .nav-burger{margin-left:4px}
  }

  /* ============ LAYOUT ============ */
  main{position:relative;z-index:var(--z-content)}
  section{position:relative}
  .shell{max-width:1280px;margin:0 auto;padding:0 var(--gutter)}
  /* .hero is display:flex;flex-direction:column, which makes its horizontal
     margins the CROSS-axis margins — an "auto" cross-axis margin disables
     stretch per spec, so .shell fell back to sizing off its own content
     (the unbroken word "impossible" in the h1) instead of the container.
     Forcing the width here restores the fill-then-cap-at-1280px behavior
     every other .shell gets for free in normal block flow. */
  .hero .shell{width:100%}
  .eyebrow{
    font-family:var(--mono);font-size:.75rem;letter-spacing:.3em;text-transform:uppercase;color:var(--dim);
    display:flex;align-items:center;gap:14px;margin-bottom:26px;
  }
  /* eyebrow marker: terminal block cursor (was a 34px stroke — 2026-08-20 slop audit; ties to the >-prompt mark) */
  .eyebrow::before{content:'';width:9px;height:1.05em;background:var(--raspberry)}
  @media (prefers-reduced-motion: no-preference){.eyebrow::before{animation:eb-blink 1.3s steps(2,jump-none) infinite}}
  @keyframes eb-blink{50%{opacity:.3}}

  /* ============ SECTION CHROME ============ */
  .band{background:var(--panel);padding:var(--sp-6) 0}
  .band.translucent{background:rgba(8,8,13,.66);backdrop-filter:blur(4px)}
  .sec-head{display:flex;align-items:flex-end;justify-content:space-between;gap:30px;margin-bottom:var(--sp-4);flex-wrap:wrap}
  .sec-head>div{min-width:0}
  .sec-head h2{
    font-family:var(--display);font-weight:800;text-transform:uppercase;
    /* the 8vw cap keeps the longest single word (DISCIPLINES. — 380px at
       --step-4's 2.2rem floor) inside a 375px viewport's 335px content box;
       min() hands back to the token as soon as the viewport affords it */
    font-size:min(var(--step-4),8vw);line-height:1;letter-spacing:-.01em;
  }
  .sec-head .kicker{max-width:340px;color:var(--dim);font-size:.98rem;font-weight:500}

  /* ============ CTA ============ */
  .cta-sec{padding:var(--sp-7) 0;text-align:center}
  .cta-sec h2{
    font-family:var(--display);font-weight:800;text-transform:uppercase;
    font-size:var(--step-6);line-height:.96;letter-spacing:-.015em;
    text-shadow:0 2px 30px var(--scrim-1);
  }
  .cta-sec h2 .ink{
    background:linear-gradient(95deg,var(--gold),var(--ember) 40%,var(--crimson) 80%,var(--crimson-deep));
    -webkit-background-clip:text;background-clip:text;color:transparent;
  }
  .cta-sec .sub{color:var(--dim-bright);max-width:520px;margin:26px auto 0;font-weight:500;text-shadow:0 1px 18px var(--scrim-3)}
  .cta-sec .btn{margin-top:42px;font-size:.9rem;padding:1.1em 2.4em}
  .cta-sec .mail{
    display:block;margin-top:26px;font-family:var(--mono);font-size:.8rem;letter-spacing:.14em;color:var(--dim);
    transition:color .3s;
  }
  @media (hover:hover){.cta-sec .mail:hover{color:var(--cyan)}}

  /* ============ FOOTER ============ */
  footer{
    position:relative;z-index:var(--z-content);border-top:1px solid var(--line-2);
    background:var(--scrim-3);backdrop-filter:blur(8px);
    padding:34px 0;
  }
  .foot-row{display:flex;justify-content:space-between;align-items:center;gap:20px;flex-wrap:wrap}
  .foot-row .fm{font-family:var(--mono);font-size:.68rem;letter-spacing:.16em;text-transform:uppercase;color:var(--dim)}
  @media (hover:hover){.foot-row .fm a:hover{color:var(--white)}}

  /* ============ PROCESS STEPS (canonical — used by home and service pages) ============ */
  .steps{display:grid;grid-template-columns:1fr;gap:var(--sp-1)}
  @media(min-width:640px){.steps{grid-template-columns:repeat(2,1fr)}}
  @media(min-width:1080px){.steps{grid-template-columns:repeat(4,1fr)}}
  .step{
    position:relative;border:1px solid var(--line-2);border-radius:var(--r-card);padding:30px 26px 34px;
    background:rgba(11,11,18,.7);overflow:hidden;transition:border-color .3s;
    /* query container: a .step is ~590px wide in a single-column stack but
       ~290px in the 4-up grid — its internal rhythm follows ITS width, not
       the viewport's. Base styles are the no-@container fallback. */
    container-type:inline-size;
  }
  /* informational cards: hover tints the border, no lift — lifts are for links */
  @media (hover:hover){.step:hover{border-color:var(--sc,var(--cyan))}}
  .step::before{
    content:'';position:absolute;top:0;left:0;right:0;height:3px;
    background:linear-gradient(90deg,var(--sc,var(--cyan)),transparent);opacity:.85;
  }
  .step .s-num{
    font-family:var(--mono);font-size:.72rem;letter-spacing:.2em;color:var(--sc,var(--cyan));margin-bottom:46px;display:block;
  }
  .step h3{font-family:var(--display);font-weight:800;font-size:1.5rem;text-transform:uppercase;margin-bottom:12px}
  .step p{color:var(--dim);font-size:.94rem;font-weight:500}
  .step .ghost-num{
    position:absolute;right:-6px;bottom:-34px;font-family:var(--display);font-weight:800;font-size:7rem;line-height:1;
    color:transparent;-webkit-text-stroke:1px var(--line-1);pointer-events:none;
  }
  /* container-driven rhythm — must sit AFTER the base .step descendant
     rules above: same specificity, so source order decides. */
  @container (min-width:480px){
    .step .s-num{margin-bottom:18px}
    .step h3{font-size:1.75rem}
    .step p{max-width:60ch;font-size:1rem}
  }

  /* ============ PROCESS PROGRESS GLOW ============ */
  /* Each step glows up in its own --sc color as it crosses into view, so
     "no mystery" is demonstrated on scroll rather than only written in the
     kicker above it. The entrance fade itself belongs to .reveal — and
     because every .step on the page is also a .reveal, the combined rule
     that runs both timelines together lives in the REVEAL block below.
     This rule is the one that covers a .step that is NOT also a .reveal. */
  @supports (animation-timeline: view()){
    @media (prefers-reduced-motion: no-preference){
      .step{
        /* Longhands, not the shorthand — see the note in KINETIC REVEAL
           ENGINE below. The shorthand was resetting animation-duration to
           0s, which is why this glow never actually ran. */
        animation-name:step-glow-in;
        animation-duration:auto;
        animation-timing-function:linear;
        animation-fill-mode:both;
        animation-timeline:view();
        animation-range:entry 0% cover 35%;
      }
    }
  }
  @keyframes step-glow-in{
    from{box-shadow:none;border-color:var(--line-2)}
    to{box-shadow:0 0 34px -8px var(--sc,var(--cyan));border-color:var(--sc,var(--cyan))}
  }

  /* ============ POINTER-AWARE VERBS ============ */
  .verb-coarse{display:none}
  @media (hover:none){.verb-fine{display:none}.verb-coarse{display:inline}}

  /* ============ NAV CURRENT PAGE ============ */
  .nav-links a.nl[aria-current="page"]{color:var(--white)}
  .nav-links a.nl[aria-current="page"]::after{width:100%}

  /* ============ SERVICE PARENT LINK ============ */
  /* The five service pages have no nav entry of their own (nav points at
     /#services), so this is the real way back up — aria-current on a nav
     link would assert something false here. */
  .svc-back{
    display:inline-flex;align-items:center;gap:.6em;min-height:44px;
    font-family:var(--mono);font-size:.72rem;letter-spacing:.16em;text-transform:uppercase;
    color:var(--dim);margin-bottom:var(--sp-1);transition:color .3s;
  }
  .svc-back::before{content:'←';color:var(--sc,var(--cyan))}
  @media(hover:hover){.svc-back:hover{color:var(--white)}}

  .svc-icon{display:block;width:48px;height:48px;margin-bottom:var(--sp-1);}

  /* ============ MORE SERVICES STRIP ============ */
  .more-svc{display:flex;gap:12px;flex-wrap:wrap;margin-top:var(--sp-4)}
  .more-svc a{
    font-family:var(--mono);font-size:.72rem;letter-spacing:.16em;text-transform:uppercase;color:var(--dim);
    border:1px solid var(--line-3);border-radius:var(--r-pill);padding:.7em 1.4em;min-height:44px;
    display:inline-flex;align-items:center;gap:.6em;transition:color .3s,border-color .3s,transform .3s;
  }
  @media(hover:hover){.more-svc a:hover{color:var(--white);border-color:var(--sc,var(--cyan));transform:translateY(-2px)}}
  .more-svc a i{font-style:normal;color:var(--sc,var(--cyan))}

  /* ============ SCROLL PROGRESS ============ */
  .scroll-progress{
    position:fixed;top:0;left:0;height:2px;width:100%;z-index:var(--z-progress);
    transform-origin:0 50%;transform:scaleX(0);
    background:linear-gradient(90deg,var(--cyan),var(--cobalt),var(--violet),var(--raspberry));
    pointer-events:none;
  }

  /* ============ KINETIC REVEAL ENGINE ============ */
  /* Two engines, one look. Where the browser has element-driven scroll
     timelines, an element's own progress through the viewport IS the
     animation clock — no listener, no observer, no class toggle, and the
     reveal scrubs backwards if you scroll back up. Everywhere else,
     base.js falls back to the IntersectionObserver it has always used
     and toggles .show exactly as before.

     base.js picks the engine and marks the root: html.st means scroll
     timelines are live, so exactly ONE of the two blocks below applies.
     The hidden state still only exists under html.js — if the script
     never runs at all, every page stays fully visible. */

  /* fallback engine — IntersectionObserver toggles .show */
  .js:not(.st) .reveal{opacity:0;transform:translateY(34px);transition:opacity .8s var(--ease-out),transform .8s var(--ease-out)}
  .js:not(.st) .reveal.show{opacity:1;transform:none}
  .js:not(.st) .reveal.d1{transition-delay:.08s}
  .js:not(.st) .reveal.d2{transition-delay:.16s}
  .js:not(.st) .reveal.d3{transition-delay:.24s}
  .js:not(.st) .reveal.d4{transition-delay:.32s}

  /* native engine — no JS in the loop at all.
     The d1–d4 stagger can't be a transition-delay here (there is no
     transition, and a scroll timeline has no wall clock to delay
     against), so it becomes range: each step finishes a little deeper
     into the scroll, which reads as the same cascade. */
  /* Gated on no-preference, not just @supports. A scroll-driven animation
     takes its progress from the timeline rather than from a clock, so the
     global `animation-duration:.01ms` reduced-motion guard at the bottom of
     this file does NOT neutralize it. The motion has to be withheld by
     never declaring the animation in the first place — which also means the
     reduced-motion path needs no !important overrides and no hand-restating
     of the finished visual state: with no animation declared, .reveal and
     .step simply render at their natural, fully-arrived styles. */
  @supports (animation-timeline: view()){
   @media (prefers-reduced-motion: no-preference){
    .st .reveal{
      /* Longhands on purpose — never the `animation` shorthand here. The
         shorthand resets every longhand it omits, which drops
         animation-duration onto its initial 0s; a scroll-driven animation
         with a 0s active interval has nothing to scrub through and sticks
         on its from-keyframe forever. `auto` is what stretches the
         animation across the range below. */
      animation-name:reveal-in;
      animation-duration:auto;
      animation-timing-function:linear;
      animation-fill-mode:both;
      animation-timeline:view();
      animation-range:entry 0% cover 24%;
    }
    .st .reveal.d1{animation-range:entry 0% cover 30%}
    .st .reveal.d2{animation-range:entry 0% cover 36%}
    .st .reveal.d3{animation-range:entry 0% cover 42%}
    .st .reveal.d4{animation-range:entry 0% cover 48%}

    /* ---- ABOVE THE FOLD RUNS ON A CLOCK, NOT ON SCROLL ----------------
       A view() range like `entry 0% cover 42%` means "finish once the
       element is 42% of the way up the scrollport". That is exactly right
       for something you scroll TO, and exactly wrong for something that is
       ALREADY ON SCREEN when the page paints: its progress is fixed by
       where it happens to sit, and it simply stays there until the visitor
       scrolls.

       Measured on the live homepage at 1440x900, scroll 0, before any
       input: .eyebrow 1.0, h1.d1 1.0, p.sub.d2 0.92, and
       div.cta-row.d3 *0.48* — the primary call to action, the button this
       whole page exists to get pressed, rendered at under half opacity for
       every first-time visitor. Lighthouse scored it a real WCAG contrast
       failure at 2.87:1 (needs 4.5:1), because contrast is computed on what
       is actually painted, not on the token values.

       The hero is above the fold on every page by definition, so it gets
       the document timeline and staggers on delay instead — the same
       .08s cascade the no-scroll-timeline fallback above already uses.
       Everything below the fold keeps the scroll ranges, which is where
       they earn their keep.

       animation-duration must be restated: the block above sets `auto`,
       which is meaningful only against a scroll timeline and collapses to
       0s on a clock — the animation would never visibly run. */
    .st .hero .reveal,
    .st .craft-hero .reveal{
      animation-timeline:auto;
      animation-range:normal;
      animation-duration:.7s;
      animation-timing-function:var(--ease-out);
    }
    .st .hero .reveal.d1,.st .craft-hero .reveal.d1{animation-delay:.08s}
    .st .hero .reveal.d2,.st .craft-hero .reveal.d2{animation-delay:.16s}
    .st .hero .reveal.d3,.st .craft-hero .reveal.d3{animation-delay:.24s}
    .st .hero .reveal.d4,.st .craft-hero .reveal.d4{animation-delay:.32s}

    /* Process steps are BOTH .step and .reveal, and animation-name is a
       single property rather than an additive list — the .st .reveal rule
       above (0,0,2,0) outranks the .step glow rule (0,0,1,0), so without
       this block it would silently delete the glow instead of coexisting
       with it. Two scroll-driven animations on one element have to be
       composed by hand: one comma-separated list per longhand, same order
       throughout. They don't fight — reveal-in owns opacity/transform,
       step-glow-in owns box-shadow/border-color. */
    .st .step.reveal{
      animation-name:reveal-in,step-glow-in;
      animation-duration:auto,auto;
      animation-timing-function:linear,linear;
      animation-fill-mode:both,both;
      animation-timeline:view(),view();
      animation-range:entry 0% cover 24%,entry 0% cover 35%;
    }
    .st .step.reveal.d1{animation-range:entry 0% cover 30%,entry 0% cover 35%}
    .st .step.reveal.d2{animation-range:entry 0% cover 36%,entry 0% cover 35%}
    .st .step.reveal.d3{animation-range:entry 0% cover 42%,entry 0% cover 35%}
    .st .step.reveal.d4{animation-range:entry 0% cover 48%,entry 0% cover 35%}
   }
  }
  @keyframes reveal-in{
    from{opacity:0;transform:translateY(34px)}
    to{opacity:1;transform:none}
  }

  /* ============ TOUCH FEEDBACK (shared) ============ */
  /* nav/button press states — see index.html's inline
     TOUCH FEEDBACK block for the home-only svc/card rules. */
  .btn:active{transform:translateY(0) scale(.97)}

  /* ============ COMMAND DECK ============ */
  /* trigger lives in every nav, always visible (mobile and desktop) —
     unlike .nl links it never hides into the burger menu, since it IS
     the fast path. Markup + behavior in cmdk.js. */
  .cmdk-trigger{
    display:flex;align-items:center;gap:8px;background:transparent;cursor:pointer;
    border:1px solid var(--line-2);border-radius:var(--r-pill);color:var(--dim);
    min-height:44px; /* WCAG touch target — was 30px, the last sub-44 control */
    font-family:var(--mono);font-size:.68rem;letter-spacing:.08em;padding:.5em .8em;
    transition:border-color .25s,color .25s;position:relative;z-index:2;
  }
  @media (hover:hover){.cmdk-trigger:hover{color:var(--white);border-color:var(--cyan)}}
  .cmdk-trigger kbd{
    font-family:var(--mono);border:1px solid var(--line-3);border-radius:var(--r-xs);
    padding:.1em .4em;font-size:.9em;color:var(--dim);
  }
  @media(max-width:640px){.cmdk-trigger .cmdk-trigger-label{display:none}}

  .cmdk-root{position:fixed;inset:0;z-index:var(--z-modal);visibility:hidden;pointer-events:none}
  .cmdk-root.open{visibility:visible;pointer-events:auto}
  .cmdk-veil{
    position:absolute;inset:0;background:var(--scrim-2);backdrop-filter:blur(6px);
    opacity:0;transition:opacity .25s ease;
  }
  .cmdk-root.open .cmdk-veil{opacity:1}
  .cmdk-panel{
    position:relative;max-width:560px;margin:min(14vh,120px) auto 0;
    background:var(--panel);border:1px solid var(--line-2);border-radius:var(--r-md);
    box-shadow:var(--shadow-lift);overflow:hidden;
    opacity:0;transform:translateY(-10px) scale(.98);transition:opacity .25s ease,transform .25s var(--ease-out);
  }
  .cmdk-root.open .cmdk-panel{opacity:1;transform:none}
  .cmdk-row1{display:flex;align-items:center;gap:12px;padding:18px 20px;border-bottom:1px solid var(--line-2)}
  .cmdk-icon{color:var(--cyan);font-size:1.1rem}
  .cmdk-input{
    flex:1;background:none;border:0;color:var(--white);font-family:var(--body);
    font-size:1rem;font-weight:500;
  }
  .cmdk-input::placeholder{color:var(--dim)}
  /* The input is auto-focused on open, so its ring was suppressed as noise.
     That also removed the only cue when you Tab back to it from the list.
     :focus-visible keeps the mouse-open case clean and the keyboard case visible. */
  .cmdk-input:focus-visible{outline:2px solid var(--cyan);outline-offset:2px;border-radius:var(--r-xs)}
  .cmdk-esc{
    font-family:var(--mono);font-size:.68rem;color:var(--dim);border:1px solid var(--line-3);
    border-radius:var(--r-xs);padding:.2em .5em;flex:none;
  }
  .cmdk-list{max-height:min(50vh,420px);overflow-y:auto;padding:8px}
  .cmdk-group{
    font-family:var(--mono);font-size:.64rem;letter-spacing:.16em;text-transform:uppercase;
    color:var(--dim);padding:14px 12px 6px;
  }
  .cmdk-opt{
    display:flex;justify-content:space-between;align-items:baseline;gap:14px;
    padding:11px 12px;border-radius:var(--r-sm);color:var(--white);text-decoration:none;
  }
  .cmdk-opt.on{background:rgba(45,226,255,.1)}
  .cmdk-opt.on .cmdk-opt-label{color:var(--cyan)}
  .cmdk-opt-label{font-family:var(--display);font-weight:800;font-size:.95rem;text-transform:uppercase}
  .cmdk-opt-hint{font-size:.8rem;color:var(--dim);font-weight:500;text-align:right;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .cmdk-empty{display:none;padding:34px 20px;text-align:center;color:var(--dim);font-size:.88rem;font-weight:500}
  .cmdk-empty.show{display:block}
  @media(max-width:640px){
    .cmdk-panel{margin-top:8vh;max-width:calc(100% - 24px)}
    .cmdk-opt-hint{display:none}
  }

  @media (prefers-reduced-motion:reduce){
    *,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}
    html{scroll-behavior:auto}
    .reveal,.js .reveal{opacity:1;transform:none}
    /* Nothing to switch off here for the scroll-driven engine. Both it and
       the process-step glow are declared inside a
       `@media (prefers-reduced-motion: no-preference)` gate, so under this
       query they were never declared at all and both land at their natural
       finished styles on their own. The blanket animation-duration override
       above could not have stopped them anyway — a scroll-driven animation
       is clocked by position, not time. */
  }

  /* ============ MOTION TOGGLE (html.motion-off) ============ */
  /* The footer toggle's class-based mirror of the block above — a media
     query can't be OR'd with a selector, so the guard exists twice. KEEP
     THE TWO IN SYNC. One real difference: under OS-level reduce the
     scroll-driven animations were never declared, but under the toggle
     they were — and being position-clocked they shrug off the duration
     override, so they must be switched off by name. animation-fill-mode
     is 'both', so removing the name lands elements on their natural,
     fully-arrived styles. */
  html.motion-off *,html.motion-off *::before,html.motion-off *::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}
  html.motion-off{scroll-behavior:auto}
  html.motion-off .reveal{opacity:1;transform:none}
  html.motion-off :is(.reveal,.step){animation-name:none!important}

  .motion-toggle{
    font-family:var(--mono);font-size:.68rem;letter-spacing:.14em;text-transform:uppercase;
    background:transparent;color:var(--dim);border:1px solid var(--line-2);border-radius:var(--r-pill);
    padding:.5em 1.1em;min-height:44px;cursor:pointer;display:inline-flex;align-items:center;
    transition:color .25s,border-color .25s;
  }
  @media(hover:hover){.motion-toggle:hover{color:var(--white);border-color:var(--line-3)}}
  .motion-toggle[aria-pressed="true"]{color:var(--white);border-color:var(--cyan)}

  /* ============ WORDMARK MICROINTERACTION ============ */
  /* The ◆ flips on hover — one small, deliberate move on the mark itself.
     Rides the reduced-motion/motion-off guards (transition-duration). */
  .wordmark .dia{display:inline-block;transition:transform .5s var(--ease-out)}
  @media(hover:hover){.wordmark:hover .dia{transform:rotate(180deg) scale(1.2)}}
