/* ── why: before → after bento ───────────────────── */
.wgrid{position:relative;display:grid;grid-template-columns:repeat(12,1fr);gap:clamp(12px,1.4vw,20px);max-width:var(--maxw);margin:0 auto}
.w-ring,.w-orb,.w-dot{position:absolute;z-index:0;border-radius:50%;pointer-events:none}
.w-ring{width:clamp(190px,24vw,330px);aspect-ratio:1;border:1px solid var(--rule);right:-5%;top:-11%;animation:wfloat 17s var(--ease) infinite alternate}
.w-ring.two{width:clamp(120px,15vw,210px);left:-4%;right:auto;top:auto;bottom:-7%;border-style:dashed;border-color:var(--rule-soft);animation-duration:23s;animation-direction:alternate-reverse}
.w-orb{width:clamp(90px,11vw,150px);aspect-ratio:1;left:22%;bottom:-6%;background:radial-gradient(circle at 50% 50%,rgba(255,77,0,.2),rgba(255,77,0,0) 70%);filter:blur(26px);animation:wfloat 21s var(--ease) infinite alternate-reverse}
.w-dot{width:9px;height:9px;right:14%;bottom:6%;background:var(--accent);opacity:.65;animation:wfloat 13s var(--ease) infinite alternate}
@keyframes wfloat{0%{transform:translate3d(0,0,0)}100%{transform:translate3d(-14px,-22px,0)}}
.wcard{position:relative;z-index:1;grid-column:span 6;display:flex;flex-direction:column;gap:clamp(16px,1.8vw,24px);padding:clamp(20px,2.2vw,32px);border:1px solid var(--rule);border-radius:var(--r-box);background:rgba(252,252,251,.74);backdrop-filter:blur(14px) saturate(1.2);-webkit-backdrop-filter:blur(14px) saturate(1.2);box-shadow:0 1px 0 rgba(255,255,255,.6) inset,0 20px 46px -34px rgba(10,10,10,.3)}
.wcard.wide{grid-column:span 7}
.wcard.narrow{grid-column:span 5}
.w-head{display:flex;align-items:baseline;gap:clamp(12px,1.4vw,20px)}
.w-head h3{font-size:clamp(19px,1.75vw,27px);max-width:26ch}
.ba{display:grid;grid-template-columns:1fr auto 1fr;align-items:stretch;gap:clamp(8px,.9vw,14px);margin-top:auto}
.ba-col{display:flex;flex-direction:column;gap:11px;padding:clamp(14px,1.5vw,20px);border-radius:var(--r-in)}
.ba-col.before{border:1px dashed var(--ink-20);background:var(--wash)}
.ba-col.after{border:1px solid rgba(255,77,0,.24);background:rgba(255,77,0,.055)}
.ba-tag{display:flex;flex-wrap:wrap;align-items:center;gap:7px;font:500 11px/1.3 var(--mono);letter-spacing:.11em;text-transform:uppercase;color:var(--muted)}
.ba-tag b{padding:4px 9px 3px;border-radius:var(--r-pill);background:rgba(10,10,10,.62);color:var(--paper);font-weight:500;letter-spacing:.13em}
.after .ba-tag b{background:var(--accent);box-shadow:0 6px 14px -8px rgba(255,77,0,.9)}
.ba-v{font-size:clamp(28px,2.7vw,40px);font-weight:600;letter-spacing:-.045em;line-height:1;color:var(--muted)}
.after .ba-v{color:var(--accent)}
.ba-v i{font-style:normal;font-size:.42em;font-weight:500;letter-spacing:-.02em;vertical-align:.5em;margin-left:.1em}
.ba-col p{font-size:13.5px;line-height:1.52;color:var(--ink-70)}
.ba-go{align-self:center;display:grid;place-items:center;width:clamp(30px,2.6vw,36px);height:clamp(30px,2.6vw,36px);border-radius:50%;background:var(--ink);color:var(--paper);box-shadow:0 10px 22px -12px rgba(10,10,10,.6)}
.ba-go .ic{width:15px;height:15px}

/* ── brand marquee ────────────────────────────────── */
.brands{position:relative;z-index:1;padding:clamp(6px,1.4vw,18px) 0 clamp(58px,7vw,100px)}
.brands-inner{max-width:var(--maxw);margin:0 auto;padding:0 var(--pad);display:flex;flex-direction:column;align-items:center;gap:22px}
.marquee{position:relative;width:100%;overflow:hidden;-webkit-mask-image:linear-gradient(90deg,transparent,#000 10%,#000 90%,transparent);mask-image:linear-gradient(90deg,transparent,#000 10%,#000 90%,transparent)}
.marquee-track{display:flex;width:max-content;animation:slide 42s linear infinite}
.marquee-set{display:flex;align-items:center;gap:clamp(26px,4vw,58px);padding-right:clamp(26px,4vw,58px)}
.marquee:hover .marquee-track{animation-play-state:paused}
@keyframes slide{to{transform:translateX(-50%)}}
.brand{font-size:clamp(17px,1.7vw,24px);font-weight:500;letter-spacing:-.03em;color:var(--ink);white-space:nowrap}
/* Six real client marks replaced six invented names. Normalised on HEIGHT,
   which is how a logo row is read - one optical line - rather than on width,
   which would make a long wordmark like "Glas Schmidsfelden" tower over
   "LV2XL". The marks were -trim'd at build time so the height is measured on
   their INK, not on whatever transparent padding each exporter left behind;
   without that one mark reads 20% smaller than its neighbours for no visible
   reason. `width:auto` keeps each mark's own proportions. */
.brand-logo{height:clamp(18px,1.9vw,26px);width:auto;flex:none;display:block;object-fit:contain}
.sep{width:5px;height:5px;flex:none;border-radius:50%;background:var(--accent);opacity:.85}
/* The honeypot. Off-screen rather than display:none, because some bots skip
   hidden fields; never focusable, never announced, and always empty when a
   human submits. */
.capture .hp{position:absolute!important;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;opacity:0;pointer-events:none}
/* The error line inherits the footer's own white so it introduces NO new
   colour pairing - the contrast gate holds six accepted pairings by exact
   {fg,bg,ratio} triple and a seventh fails the build. It is announced by
   role="alert" and marked on the input with aria-invalid, so it does not rely
   on colour to be perceived. */
.capture-err{flex-basis:100%;margin-top:8px;font-size:13px;line-height:1.4;color:#fff}

/* ── section shell ────────────────────────────────── */
.sect{position:relative;z-index:1;max-width:var(--maxw);margin:0 auto;padding:0 var(--pad) clamp(64px,8vw,120px)}
.sect-head{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;flex-wrap:wrap;margin-bottom:clamp(28px,3.4vw,44px)}
.sect-title{display:flex;flex-direction:column;gap:12px}
.sect-title h2{font-size:clamp(28px,3.3vw,46px)}
.sect-lead{max-width:46ch;font-size:16px;color:var(--muted)}

/* ── selected work ────────────────────────────────── */
.wk-stage{position:relative;padding-top:26px}
.wk-tab{position:absolute;left:0;top:0;z-index:4;display:grid;align-items:center;height:clamp(52px,4.4vw,64px);padding:0 clamp(20px,2.2vw,30px);border:1px solid var(--rule);border-radius:var(--r-pill);background:var(--paper);overflow:hidden;transition:width .62s var(--ease);box-shadow:0 14px 30px -20px rgba(10,10,10,.4)}
.wk-tab-in{grid-area:1/1;display:inline-flex;align-items:baseline;gap:clamp(10px,1.2vw,16px);width:max-content;white-space:nowrap;transition:opacity .34s var(--ease)}
.js .wk-tab-in{opacity:0}
.wk-tab-in.on{opacity:1}
/* Every label shares grid-area 1/1, so with no JS to pick one they would all
   print on top of each other. All but the first, off. */
html:not(.js) .wk-tab-in~.wk-tab-in{opacity:0}
.wk-tab h3{font-size:clamp(18px,1.9vw,28px)}
/* The slide width and the track gap are tokens rather than literals because
   the edge fade below is DERIVED from them: the strip of the next card that
   peeks past the active one is exactly `100% - basis - gap`. Restating that
   width as a hand-tuned pixel value is how a fade drifts off the thing it is
   meant to soften - too narrow and the hard line comes back, too wide and it
   starts dissolving the ACTIVE card, which at 1440 is 102px away and at 375 is
   14px away. One source of truth, three widths, no tuning. */
.wk-stage{--wk-gap:clamp(12px,1.2vw,18px);--wk-basis:90%;
  /* The strip the fade owns. Normally that is exactly the peek - the sliver of
     the next card showing past the active one - but at 375 the peek is 14px and
     a 14px ramp cannot reach zero before the clip, which leaves a tenth of the
     step still standing. So the floor is 82% of the distance to the ACTIVE
     card's right edge, which is a fraction of a measured gap rather than a
     magic number, and therefore cannot touch that card at any width.
     Resolves to 102px at 1440, 45px at 768, 22px at 375. */
  --peek:max(calc(100% - var(--wk-basis) - var(--wk-gap)),calc((100% - var(--wk-basis)) * .82));
  /* Scaled, because a blur radius is only safe relative to the band it lives
     in: `backdrop-filter` edge-clamps, so a 10px blur inside a 15px band drags
     opaque pixels back out to the cut and rebuilds the hard line it was added
     to remove. Measured at 375: 10px blur left 76/255 at the cut where the mask
     alone left 10. */
  --wk-blur:clamp(3px,.7vw,10px)}
/* THE CAROUSEL IS A NATIVE SCROLLER. It was a JS drag engine writing
   translate3d until 2026-08-24, and it was replaced wholesale rather than
   tuned, because the complaint ("swiping hangs, and it is too hard to swipe")
   was not a threshold problem. Four defects were measured on the live site and
   every one of them is structural to hand-rolling a touch gesture:

     - The gesture could STICK. `locked` was set before a try/caught
       setPointerCapture, the window-level release was gated on `!locked`, and
       nothing listened for lostpointercapture or blur. Lose the capture and the
       carousel stayed in drag state forever - after which merely HOVERING
       dragged the track.
     - The lock needed 14px of horizontal travel while `touch-action:pan-y` let
       the browser claim the gesture for page scroll at ~8-10px. The browser
       always got to decide first, so a thumb swipe that started with any
       vertical drift scrolled the page and the carousel never moved.
     - A press starting on a link did nothing at all: startDrag bailed on
       `a,button`, and on a phone the "Learn more" pill sits in the bottom-left
       corner, which is 45% of the card's width and exactly where a thumb lands.
     - Grabbing mid-animation snapped the track ~102px, because the 0.78s
       transition kept running while x0 was recorded against the settled offset.

   A native scroll container has none of these. The browser owns the gesture,
   arbitrates the two axes itself, cannot leave a stuck flag behind, and lets a
   press start anywhere including on a link. What is left in JS is state, not
   physics. */
/* The horizontal inset stays HERE, not on the scroller. A scroll container's
   END padding is not counted in scrollWidth, so 2px of padding-right on
   `.wk-fade` made the last slide's snap position 2px beyond the maximum
   scrollLeft and it could never quite land. Measured: wanted 3270, could reach
   3268. `.wk-view` no longer clips, so an inset here is purely geometric and
   reproduces the original padding box exactly. */
.wk-view{position:relative;padding:2px 2px 0;outline:none;cursor:grab}
/* The affordance. Nothing on the card said it could be dragged, so the drag
   that already existed went undiscovered and the arrows read as the only way
   through. `grab` is the whole discovery mechanism on a desktop pointer - a
   mouse cannot pan a scroll container natively, so site.js keeps a small
   mouse-only shim behind this cursor. Touch never enters it. */
.wk-view:active{cursor:grabbing}
.wk-view:focus-visible{border-radius:var(--r-box);box-shadow:0 0 0 2px var(--accent)}
/* `.wk-fade` is the SCROLLER, and it is also the element carrying the edge
   mask. Both on purpose. A mask paints against the element's own border box and
   does not travel with the scrolled content, so the fade stays pinned to the
   right edge at every scroll position - verified in a render before this was
   built on. It is not on `.wk-view` for the reason recorded in index.html: a
   mask there would erase the :focus-visible ring, which is a box-shadow painted
   outside the border box and therefore outside the mask's painting area.

   Only the BOTTOM padding lives here, and it is load-bearing: `.wk-fade` clips
   vertically, and the card's `0 32px 64px -46px` shadow needs the same 4px of
   room below it that `.wk-view`'s padding box used to give it. The horizontal
   inset stays on `.wk-view` for the scrollWidth reason recorded above.
   `overscroll-behavior-x:contain` replaces what the old wheel handler used
   preventDefault for: an unconsumed horizontal overscroll is what Chrome and
   Safari turn into a back/forward navigation, and swiping a carousel must never
   leave the page. Scrollbars are hidden because the peek fade IS the
   affordance; a scrollbar under the card reads as a broken crop. */
.wk-fade{overflow-x:auto;overflow-y:hidden;scroll-snap-type:x mandatory;overscroll-behavior-x:contain;
  padding-bottom:4px;
  scrollbar-width:none;-ms-overflow-style:none}
.wk-fade::-webkit-scrollbar{display:none}
/* Released only while the mouse shim is writing scrollLeft by hand: mandatory
   snapping fights a per-frame scrollLeft write and the card stutters under the
   cursor. Touch never reaches this class. */
.wk-fade.drag{scroll-snap-type:none;user-select:none;-webkit-user-select:none}
.wk-track{display:flex;gap:var(--wk-gap)}
/* Without this the LAST slide can never rest where the first three do. Its snap
   position is one full peek beyond the scroller's maximum scrollLeft, so the
   browser clamps and leaves the previous card hanging in from the left - a
   resting state the transform version never had. Measured at 1440: the last
   slide wants 3270 and the maximum was 3151, exactly the 119px peek. The spacer
   is 102px because the flex gap contributes the remaining 17. */
.wk-track::after{content:"";flex:0 0 calc(100% - var(--wk-basis) - var(--wk-gap))}
.wk-slide{flex:0 0 var(--wk-basis);min-width:0;scroll-snap-align:start}

/* ── the peek fades instead of being guillotined ──────
   `.wk-view` clips with overflow:hidden, so the next card ended at a dead
   straight vertical line 530px tall. Measured against a maximum-contrast
   control that cut was a 21/255 luminance step; the mask alone takes it to
   4/255 and the blur band to 1/255.

   Two layers, and they do different jobs. The mask ramps the CONTENT to
   transparent, so what shows through is the real page - the grillage grid
   included. A paper-coloured wash would have covered that grid with a pale
   band. The blur band on top is what makes it read as depth rather than as
   something being faded out.

   RIGHT EDGE ONLY, deliberately. At every resting position the LEFT edge of
   the viewport is the left edge of the ACTIVE card - the previous slide sits a
   full gap beyond it - so a symmetric mask has nothing to soften there and
   instead dissolves the active card's border and the first characters of its
   body copy. That was rendered and rejected, not reasoned about.

   Degradation: no mask support leaves today's hard edge; no backdrop-filter
   leaves an empty box, because the band has no background of its own. Neither
   can remove anything that is on the page now. */
.wk-fade{-webkit-mask-image:linear-gradient(to right,#000 calc(100% - var(--peek)),rgba(0,0,0,.86) calc(100% - var(--peek)*.66),rgba(0,0,0,.42) calc(100% - var(--peek)*.3),transparent 100%);
  mask-image:linear-gradient(to right,#000 calc(100% - var(--peek)),rgba(0,0,0,.86) calc(100% - var(--peek)*.66),rgba(0,0,0,.42) calc(100% - var(--peek)*.3),transparent 100%)}
/* The band peaks in the MIDDLE of the strip and tapers back to nothing before
   the clip. That taper is the whole reason this band is safe: a mask that runs
   to full opacity at the edge lets the blur's edge-clamped samples paint right
   up to the boundary, which measured 76/255 at 375 and 8/255 at 768 - a hard
   line rebuilt by the thing meant to soften it. Tapered, the same blur measures
   0.1/255 at 768 and 1440. */
/* z-index 3, not higher: `.wk-fade` carries a mask and so is its own
   stacking context, which already puts this band above every slide. The
   only thing left to order against is `.wk-tab` at 4, and the title chip
   overlaps the top of the viewport and must stay crisp on top of it. */
.wk-view::after{content:"";position:absolute;top:0;right:0;bottom:0;z-index:3;pointer-events:none;
  width:var(--peek);
  backdrop-filter:blur(var(--wk-blur));-webkit-backdrop-filter:blur(var(--wk-blur));
  -webkit-mask-image:linear-gradient(to right,transparent 0,rgba(0,0,0,.55) 34%,#000 58%,rgba(0,0,0,.5) 78%,transparent 100%);
  mask-image:linear-gradient(to right,transparent 0,rgba(0,0,0,.55) 34%,#000 58%,rgba(0,0,0,.5) 78%,transparent 100%)}
.wk-card{position:relative;height:clamp(400px,42vw,530px);border:1px solid var(--rule);border-radius:var(--r-box);overflow:hidden;background:var(--wash);box-shadow:0 32px 64px -46px rgba(10,10,10,.44)}
.wk-card img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.wk-veil{position:absolute;left:0;right:0;bottom:0;height:66%;z-index:1;pointer-events:none;backdrop-filter:blur(22px) saturate(1.12);-webkit-backdrop-filter:blur(22px) saturate(1.12);-webkit-mask-image:linear-gradient(to top,#000 56%,transparent);mask-image:linear-gradient(to top,#000 56%,transparent);background:linear-gradient(to top,rgba(252,252,251,.94),rgba(252,252,251,.62) 52%,rgba(252,252,251,0));transition:opacity .5s var(--ease)}
/* Scoped like .reveal: only site.js adds .on, so unscoped these three rules
   turn Selected Work into four unlabelled photographs with no links when the
   script does not run. */
.js .wk-veil{opacity:0}
.wk-slide.on .wk-veil{opacity:1}
.wk-body{position:absolute;left:0;right:clamp(150px,15vw,220px);bottom:0;z-index:2;display:flex;flex-direction:column;gap:13px;padding:clamp(20px,2.4vw,34px);pointer-events:none;transition:opacity .5s var(--ease)}
.js .wk-body{opacity:0}
.wk-slide.on .wk-body{opacity:1}
.wk-role{font:500 10.5px/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:var(--muted)}
.wk-body p{max-width:54ch;font-size:clamp(14.5px,1.1vw,17px);line-height:1.5;color:var(--ink-70)}
.wk-body .tags{padding:2px 0 0}
.wk-body .tags span{background:rgba(252,252,251,.72);border-color:rgba(10,10,10,.15);color:var(--ink-70)}
.wk-more{position:absolute;right:clamp(20px,2.4vw,34px);bottom:clamp(20px,2.4vw,34px);z-index:3;display:inline-flex;align-items:center;gap:9px;padding:13px 22px;border-radius:var(--r-pill);background:var(--ink);color:#fff;font-size:14.5px;transition:opacity .5s var(--ease),transform .3s var(--ease),background .3s var(--ease)}
.js .wk-more{opacity:0;pointer-events:none}
.wk-slide.on .wk-more{opacity:1;pointer-events:auto}
.wk-more:hover{color:#fff;background:#1d1d1d;transform:translateY(-2px)}
.wk-more .ic{width:16px;height:16px;transition:transform .3s var(--ease)}
.wk-more:hover .ic{transform:translate(2px,-2px)}
.wk-foot{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-top:clamp(22px,2.4vw,32px)}
/* 15px, not the handoff's 9px, and at EVERY width. 10px dots on a 19px pitch
   put a pair of WCAG 2.5.8 24px target circles in overlap, so the spacing
   exception does not apply and the dots fail AA - including at 768, which is
   an iPad in portrait and unambiguously a touch surface. 25px pitch clears it.
   Costs 18px of width on one row of four dots, inside a .wk-foot that has
   1,101px of slack at 1440; the arrows are pinned right and do not move.
   Signed off by Max 2026-08-24, registered as delta #14. */
.wk-dots{display:flex;align-items:center;gap:15px}
/* Was `color-mix(in oklab, var(--c) 28%, transparent)`, which Safari below
   16.2 cannot parse - and an unparsed background on a bordless, paddingless
   button is not a degraded dot, it is NO dot: the carousel's position
   indicator simply vanished on older iPhones. Mixing a colour with
   `transparent` yields that colour at 28% alpha, and an element with no
   children renders identically whether the alpha is in the colour or on the
   element, so `opacity` is the same picture with no parsing cliff. */
.wk-dot{position:relative;width:10px;height:10px;padding:0;border:0;border-radius:var(--r-pill);background:var(--c);opacity:.28;cursor:pointer;transition:width .55s var(--ease),opacity .55s var(--ease)}
.wk-dot:hover{opacity:.58}
.wk-dot.on{width:38px;opacity:1}
.wk-arrows{display:flex;gap:10px}
.wk-nav{position:relative;display:grid;place-items:center;width:clamp(42px,3.4vw,52px);height:clamp(42px,3.4vw,52px);border:1px solid var(--rule);border-radius:50%;background:var(--paper);color:var(--ink);cursor:pointer;transition:transform .3s var(--ease),border-color .3s var(--ease),opacity .3s var(--ease)}
/* 42px at 375. The arrows sit in a 10px-gap row, so a 44px hit box fits
   between them without overlapping. */
.wk-nav::after{content:"";position:absolute;left:50%;top:50%;width:44px;height:44px;transform:translate(-50%,-50%)}
/* The dots are 10px on a 19px pitch, so 44px wide is not reachable without
   moving them apart; the box takes the full pitch instead, leaving the row
   contiguous with no dead space. The 6px of extra gap below is what lifts the
   pitch clear of the 24px WCAG 2.5.8 floor. The same navigation is available
   at a full 44x44 through the arrows, and by swiping. */
.wk-dot::after{content:"";position:absolute;left:50%;top:50%;width:calc(100% + 15px);height:44px;transform:translate(-50%,-50%)}
.wk-nav:hover{border-color:var(--ink-35);transform:scale(1.07)}
.wk-nav:disabled{opacity:.26;cursor:default;transform:none}
.wk-nav .ic{width:18px;height:18px}
.tags{display:flex;flex-wrap:wrap;gap:6px;padding:2px 6px 0}
.tags span{padding:5px 11px;border:1px solid var(--rule);border-radius:var(--r-pill);font:500 10.5px/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}
/* ── four ways in ─────────────────────────────────── */
.services{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:16px}
.svc-card{display:flex;flex-direction:column;gap:11px;padding:clamp(22px,2.2vw,30px);border:1px solid var(--rule);border-radius:var(--r-card);background:rgba(255,255,255,.74);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);transition:border-color .35s var(--ease),transform .35s var(--ease)}
.svc-card:hover{border-color:rgba(10,10,10,.24);transform:translateY(-3px)}
.svc-ico{width:44px;height:44px;display:grid;place-items:center;margin-bottom:8px;border-radius:14px;background:var(--wash);color:var(--ink);transition:background .35s var(--ease),color .35s var(--ease)}
/* Lucide static glyphs, masked with currentColor — same technique as the design system's Icon */
.ic{display:inline-block;flex:0 0 auto;width:22px;height:22px;background:transparent;-webkit-mask:var(--src) center/contain no-repeat;mask:var(--src) center/contain no-repeat}
.ic.ready{background:currentColor}
/* Without JS the preloader never adds .ready, so every icon on the site stayed
   a transparent 22px box - the black circles in the before/after cards render
   as solid dots with no arrow in them. The mask itself needs no JS at all (the
   URL is an inline --src in the markup), so painting currentColor immediately
   is enough. Safe because check-assets.mjs asserts every icon file resolves
   and the deploy script curls each one: the failure this guards against -
   a missing mask painting a solid square - fails the build before it can ship. */
html:not(.js) .ic{background:currentColor}
.svc-ico .ic{width:22px;height:22px}
.svc-card:hover .svc-ico{background:rgba(255,77,0,.09);color:var(--accent)}
.svc-card h3{font-size:clamp(18px,1.5vw,22px)}
.svc-card p{font-size:14px;line-height:1.55;color:var(--ink-70)}

/* ── process: centre spine, alternating sides ─────── */
.process{position:relative}
.spine{position:absolute;left:50%;top:0;bottom:0;width:1px;transform:translateX(-.5px);background:var(--rule)}
.spine i{position:absolute;left:0;top:0;width:100%;height:100%;background:var(--accent);transform:scaleY(var(--p,0));transform-origin:top center;transition:transform .2s linear}
.psteps{display:flex;flex-direction:column;gap:clamp(38px,6vw,86px);margin:0;padding:0;list-style:none}
.pstep{position:relative;display:grid;grid-template-columns:1fr 1fr;align-items:center;gap:clamp(28px,5vw,78px)}
.pdot{position:absolute;left:50%;top:50%;z-index:2;width:13px;height:13px;margin:-6.5px 0 0 -6.5px;border-radius:50%;background:var(--paper);box-shadow:0 0 0 1.5px var(--rule) inset;transition:background .45s var(--ease),box-shadow .45s var(--ease)}
.pstep.lit .pdot{background:var(--accent);box-shadow:0 0 0 5px rgba(255,77,0,.14)}
.pbody{display:flex;flex-direction:column;gap:13px}
.pbody h3{font-size:clamp(24px,2.6vw,38px)}
.pbody p{max-width:44ch;font-size:15px;line-height:1.55;color:var(--ink-70)}
.step-n{font:500 11px/1 var(--mono);letter-spacing:.16em;text-transform:uppercase;color:var(--muted)}
.pstep.lit .step-n{color:var(--accent)}
.pstep:nth-child(even) .pbody{grid-column:2;order:2}
.pstep:nth-child(even) .part{grid-column:1;order:1}
.part{display:grid;place-items:center;min-height:clamp(190px,16vw,232px)}
.part .ico{width:190px;height:132px;margin:0;transform:scale(1.6)}
.pstep.lit .pipe-line{animation:flow 1.6s linear infinite}
.iso-inline.big{width:1.6em;height:.95em;margin:0;font-size:clamp(110px,13vw,190px)}
.part-doc{display:flex;align-items:center;gap:14px;transform:scale(2.9)}
.part-doc .doc{flex:none;width:44px;height:58px}
.part-doc .doc:last-child{transform:rotate(5deg) translateY(6px)}
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:4px}
.p-out{position:relative;display:grid;place-items:center;padding-top:clamp(44px,6vw,80px)}
.ba-go.down{transform:rotate(90deg)}
.chips span{padding:5px 11px;border:1px solid var(--rule);border-radius:var(--r-pill);font:500 10.5px/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}

/* ── closing CTA ──────────────────────────────────── */
.close-cta{position:relative;z-index:1;max-width:var(--maxw);margin:0 auto;padding:0 var(--pad) clamp(60px,7vw,104px)}
.cta-box{display:grid;justify-items:center;gap:16px;padding:clamp(48px,7vw,104px) clamp(24px,4vw,56px);border:1px solid var(--rule);border-radius:clamp(28px,3vw,44px);background:rgba(255,255,255,.7);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);text-align:center}
.cta-box h2{font-size:clamp(38px,5.8vw,86px);letter-spacing:-.042em}
.cta-box p{max-width:40ch;font-size:16.5px;color:var(--muted)}
.cta-box .btn{margin-top:14px}

/* ── proof ────────────────────────────────────────── */
.proof{display:grid;grid-template-columns:1.05fr 1fr;gap:16px;align-items:stretch}
.quote{display:flex;flex-direction:column;gap:18px;justify-content:center;padding:clamp(28px,3.4vw,48px);border:1px solid var(--rule);border-radius:var(--r-box);background:rgba(255,255,255,.74);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}
.quote-mark{font-size:60px;line-height:.35;font-weight:700;color:var(--accent)}
.quote p{font-size:clamp(21px,2.3vw,32px);font-weight:500;letter-spacing:-.03em;line-height:1.2;max-width:26ch}
.quote footer{display:flex;flex-direction:column;gap:5px;margin-top:8px}
.q-name{font-size:15px;font-weight:500}
.q-role{font:500 11px/1 var(--mono);letter-spacing:.16em;text-transform:uppercase;color:var(--muted)}
.stats-ink{display:flex;flex-direction:column;justify-content:center;gap:clamp(24px,3vw,42px);padding:clamp(28px,3.4vw,48px);border-radius:var(--r-box);background:var(--ink);color:#fff}
.stats-row{display:flex;flex-wrap:wrap;gap:clamp(24px,4vw,56px)}
.s-block{display:flex;flex-direction:column;gap:9px}
.s-v{font-size:clamp(34px,3.6vw,52px);font-weight:600;letter-spacing:-.045em;line-height:1}
.s-l{font:500 11px/1 var(--mono);letter-spacing:.16em;text-transform:uppercase;color:rgba(255,255,255,.55)}
.stats-ink p{max-width:38ch;font-size:14px;line-height:1.55;color:rgba(255,255,255,.6)}

/* ── footer: contained ink block over the blurred grillage ── */
.footer{position:relative;z-index:1;padding:0 var(--pad) clamp(20px,3vw,34px)}
.foot-block{position:relative;overflow:hidden;isolation:isolate;max-width:var(--maxw);margin:0 auto;padding:clamp(30px,4vw,60px);border-radius:clamp(28px,3vw,44px);background:rgba(10,10,10,.9);backdrop-filter:blur(30px) saturate(.85);-webkit-backdrop-filter:blur(30px) saturate(.85);color:#fff;box-shadow:0 44px 92px -54px rgba(10,10,10,.6)}
.foot-block>*:not(.glow){position:relative;z-index:1}
.foot-glow{width:46%;left:-6%;top:-26%;background:radial-gradient(circle at 50% 50%,rgba(255,77,0,.3),rgba(255,77,0,0) 70%);filter:blur(56px);animation:drift 30s var(--ease) infinite alternate}
.footer a{color:rgba(255,255,255,.68)}
.footer a:hover{color:var(--accent)}
.eyebrow.inv{color:rgba(255,255,255,.55)}
.foot-cta{display:flex;align-items:flex-end;justify-content:space-between;gap:clamp(24px,4vw,60px);flex-wrap:wrap}
.foot-cta-copy{display:flex;flex-direction:column;gap:14px}
.foot-cta h2{max-width:20ch;font-size:clamp(28px,3.6vw,50px);color:#fff}
.capture{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
/* 16px, not 15px. iOS Safari zooms the page whenever a focused input is under
   16px and does not zoom back out, so tapping this field left a phone visitor
   on a magnified, sideways-scrolling page. The only change in P3 with any
   visible effect: +1.5px on this one row, mobile only. Signed off by Max
   2026-08-24, registered as delta #2.
   `outline:none` stays, but it is no longer the whole story - the global
   :focus-visible rule in site.css draws a real ring, and the border colour
   below still marks focus for a pointer user. Removing an outline without
   replacing it is how a keyboard user loses their place. */
.capture input{width:clamp(180px,22vw,250px);padding:13px 18px;border:1px solid rgba(255,255,255,.18);border-radius:var(--r-pill);background:rgba(255,255,255,.06);color:#fff;font:inherit;font-size:16px;outline:none;transition:border-color .3s var(--ease),background .3s var(--ease)}
.capture input:focus-visible{outline:2px solid #fff;outline-offset:2px}
.capture input::placeholder{color:rgba(255,255,255,.42)}
.capture input:focus{border-color:var(--accent);background:rgba(255,255,255,.1)}
.btn-inv{background:#fff;color:var(--ink)}
.btn-inv:hover{background:#efefec;color:var(--ink)}
.capture-done{font:500 11px/1.5 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--accent)}
.foot-rule{height:1px;background:rgba(255,255,255,.14);margin:clamp(34px,4vw,56px) 0}
.foot-rule.thin{background:rgba(255,255,255,.09);margin:clamp(26px,3vw,40px) 0 20px}
.foot-grid{display:flex;flex-wrap:wrap;justify-content:space-between;gap:clamp(30px,6vw,88px)}
.foot-brand{max-width:30ch;display:flex;flex-direction:column;gap:14px}
.wordmark.inv,.wordmark.inv:hover{color:#fff}
.wordmark.inv .wm-thin{color:rgba(255,255,255,.55)}
.foot-brand p{font-size:14px;color:rgba(255,255,255,.6)}
.socials{display:flex;gap:8px;margin-top:6px}
.socials a{width:38px;height:38px;display:grid;place-items:center;border:1px solid rgba(255,255,255,.18);border-radius:50%;color:rgba(255,255,255,.8);transition:border-color .3s var(--ease),color .3s var(--ease),background .3s var(--ease)}
.socials a:hover{border-color:var(--accent);color:var(--accent);background:rgba(255,77,0,.1)}
.socials .ic{width:17px;height:17px}
.foot-cols{display:flex;flex-wrap:wrap;gap:clamp(28px,5vw,76px)}
.foot-col{display:flex;flex-direction:column;gap:11px;font-size:14px}
.foot-h{font:500 11px/1 var(--mono);letter-spacing:.16em;text-transform:uppercase;color:rgba(255,255,255,.45);margin-bottom:6px}
.foot-bottom{display:flex;align-items:center;justify-content:space-between;gap:18px;flex-wrap:wrap}
.foot-note{font:400 12px/1 var(--mono);letter-spacing:.06em;color:rgba(255,255,255,.45)}
.foot-legal{display:flex;align-items:center;gap:clamp(12px,1.6vw,22px);flex-wrap:wrap;font-size:13px}
.cookie-btn{padding:0;border:0;background:none;font:inherit;font-size:13px;color:rgba(255,255,255,.68);cursor:pointer;transition:color .3s var(--ease)}
.cookie-btn:hover{color:var(--accent)}

/* ── tap targets, footer and carousel ─────────────── */
/* Same technique as site.css: a transparent ::after grows the hit area and
   the visible box never moves.
   Two of these cannot reach 44px, and the honest number is written down
   rather than rounded up. The footer link columns run on an 11px gap (32px
   pitch) and the legal row wraps on a 12px row-gap (25px pitch); taking the
   full pitch keeps every row contiguous with no dead strip between links, and
   both clear the 24px WCAG 2.5.8 AA floor. Reaching 44 would mean opening up
   the footer's mobile spacing, which is a visible change to a design declared
   final - so it is registered as a measurement, not silently widened. */
.socials a,.cookie-btn,.foot-col a,.foot-legal a{position:relative}
.socials a::after{content:"";position:absolute;left:50%;top:50%;width:44px;height:44px;transform:translate(-50%,-50%)}
.foot-col a::after{content:"";position:absolute;left:0;right:0;top:50%;height:32px;transform:translateY(-50%)}
.cookie-btn::after,.foot-legal a::after{content:"";position:absolute;left:0;right:0;top:50%;height:25px;transform:translateY(-50%)}

/* ── display settings ─────────────────────────────── */

/* ── responsive ───────────────────────────────────── */
@media (max-width:1080px){
.wcard,.wcard.wide,.wcard.narrow{grid-column:span 12}
.spine{left:6px}
.pstep{grid-template-columns:1fr;gap:20px;padding-left:34px}
.pstep:nth-child(even) .pbody,.pstep:nth-child(even) .part{grid-column:1;order:0}
.pstep:nth-child(even) .part{order:1}
.pdot{left:6px;top:8px;margin:0 0 0 -6.5px}
.part{min-height:0}
.p-out{display:block;height:clamp(46px,6vw,80px);padding-top:0;margin-left:0}
.p-out .ba-go{position:absolute;left:6px;bottom:0;justify-self:auto;transform:translateX(-50%) rotate(90deg)}
.part .ico{transform:scale(1.25)}
.iso-inline.big{font-size:clamp(120px,16vw,150px)}
.part-doc{transform:scale(2.3)}
.proof{grid-template-columns:1fr}
}
/* ── the card becomes a VERTICAL SHOWCASE below 960 ───────────────────────
   Max, on the phone view: "very bad. Square format, not enough place for the
   text." Both halves of that were measurable at 390px.

   SQUARE: `.wk-card` sized its height and width independently - a fixed
   `clamp(430px,92vw,540px)` against a 92% width - so its aspect ratio swung
   from 0.709 at 375 through roughly 1:1 between 500 and 960 and out to 2.025
   at 1440. Nobody chose a square; it is where two unrelated clamps crossed.

   NOT ENOUGH PLACE FOR THE TEXT: every word was absolutely positioned ON TOP
   of the photograph, above a blur veil covering the bottom 66%. Measured at
   390: the eyebrow, a 125-character paragraph and four tag chips came to 275px
   of a 430px card - the copy covered 64% of the image, and the image had to
   act as its background. Two things fighting for one rectangle.

   The fix is to stop overlapping them. The image gets a fixed aspect ratio at
   the top, the copy sits below it on the card's own ground at full contrast,
   and the card grows to whatever the copy needs. Nothing is cropped to make
   room for anything else.

   The image ratio matches the rendition actually served, which is why there
   are two: `<picture>` switches at 767 to a 3:4 mobile derivative, and between
   768 and 960 the 16:9 desktop file is still the one being downloaded. The
   manifest's media query is deliberately NOT moved to 960 - wire-media.mjs can
   only fill empty <image-slot> placeholders, so a manifest change would not
   propagate to already-wired markup and would silently do nothing. */
@media (max-width:960px){
.wk-stage{--wk-basis:92%}
/* Height comes from the content now. `align-items:stretch` on the flex track
   plus a full-width card makes every slide as tall as the tallest copy, so
   three-chip and four-chip cards still line up. */
.wk-slide{display:flex}
.wk-card{height:auto;width:100%;display:flex;flex-direction:column}
/* The image block. `position:relative` + a static img undoes the absolute
   inset:0 fill that the overlay layout needed. */
/* The image block takes the PLATE's own shape, defaulting to the 3:4 of the
   mobile photographic rendition. The three graphic plates are letterboxed onto
   that same 3:4 canvas by build-media - 336px of Shopify artwork inside 907px
   of canvas - so showing the canvas whole put the plate's own padding on screen
   as a void under the artwork. Their declared aspects (2:1, 16:10, 16:9) are
   all WIDER than 3:4, so cropping to them removes padding and can never reach
   the artwork. `--pic-ar` is emitted by wire-media.mjs beside `--fit`/`--plate`
   so the markup and the generator agree. */
/* `flex:1 1 auto`, so the IMAGE absorbs the slack, not a gap in the copy.
   Slides are equal height (the tallest card wins), and the four cards carry
   very different amounts of image: a 3:4 photograph against a 2:1 graphic. Left
   to `flex:0 0 auto` the difference came out as ~380px of empty card between
   the tag chips and the "Learn more" pill, which reads as a broken card. No
   distribution of too little content across too much space is a good one - so
   the space goes to the picture, where more of it is simply a bigger plate. */
.wk-card picture{position:relative;flex:1 1 auto;width:100%;min-height:0;aspect-ratio:var(--pic-ar,3/4);overflow:hidden}
/* `--plate` finally does something. It is written onto three cards by
   wire-media.mjs (`--fit:contain;--plate:#FAFAFA`) and NOTHING has ever read
   it here - sections.css hardcodes `object-fit:cover` - so the letterboxed
   plates showed --wash slivers around their own ground. Desktop keeps the
   hardcoded cover: changing it there would alter the live 1440 render, which
   is out of scope for this round. */
/* `--fit` and `--plate` finally do something, and this is the layout that
   needed them. They are written onto three cards by wire-media.mjs
   (`--fit:contain;--plate:#FAFAFA`) and NOTHING has ever read them here -
   sections.css hardcodes `object-fit:cover`. Once the image block grows to
   absorb the card's slack, cover would crop a composed graphic whose whole
   subject is its arrangement, which is the exact thing `contain` was declared
   to prevent. Contained on its own ground, a taller block just frames the plate
   more generously. Desktop keeps the hardcoded cover: changing it there would
   alter the live 1440 render, which is out of scope for this round. */
.wk-card img{position:static;width:100%;height:100%;object-fit:var(--fit,cover);background:var(--plate,var(--wash))}
/* The veil exists ONLY to hold text over a photograph. There is no text over
   the photograph any more. */
.wk-veil{display:none}
/* `flex:0 0 auto`: the copy is exactly as tall as the copy. If it grows too,
   it shares the slack with the picture and reopens the same gap between the tag
   chips and the pill, just a smaller one - measured 112-133px before this. */
.wk-body{position:static;right:auto;flex:0 0 auto;padding:18px 18px 0;pointer-events:auto}
.wk-more{position:static;align-self:flex-start;margin:14px 18px 18px;margin-top:auto}
/* Always visible below 960, and this is not cosmetic. During a swipe the
   incoming card is half on screen, and `.js .wk-body{opacity:0}` would render
   it as a photograph with no words - which reads as a broken card rather than
   as the next one arriving. Stated as an explicit exclusion rather than left
   to source order: adding a scope prefix is a specificity change to every rule
   it touches, and this file has already shipped one blank page that way. */
.wk-slide .wk-body,.wk-slide .wk-more{opacity:1}
.wk-slide .wk-more{pointer-events:auto}
}
@media (min-width:768px) and (max-width:960px){
/* 768-960 still downloads the 16:9 desktop rendition, so the block that holds
   it says 16:9. A 3:4 box here would crop a third of the plate away. */
.wk-card picture{aspect-ratio:16/9}
}
@media (max-width:767px){
.ba{grid-template-columns:1fr}
.ba-go{justify-self:center;transform:rotate(90deg)}
.sect-head{align-items:flex-start}
.foot-cta{align-items:flex-start}
.capture{width:100%}
.capture input{flex:1 1 160px;width:auto;min-width:0}
.foot-bottom{flex-direction:column;align-items:flex-start;gap:14px}
}
@media (prefers-reduced-motion:reduce){
.marquee-track,.foot-glow,.w-ring,.w-orb,.w-dot{animation:none!important}
/* The carousel answers a wheel as well as the arrows, so a reader who has asked
   for less motion can trigger a 1,090px horizontal slide by scrolling. The
   destination is unchanged - only the journey to it is dropped - so no resting
   pixel moves. Since 2026-08-24 the track no longer transitions at all (it is a
   native scroller, not a transform), and this is enforced in site.js instead:
   go() passes behavior:'auto' rather than 'smooth' under reduce. Kept as a
   belt-and-braces no-op in case a transition is ever put back on the track. */
.wk-track{transition:none}
/* The scroller itself must not smooth-scroll either. A UA or a future rule
   setting scroll-behavior:smooth would reintroduce the journey behind JS's
   back, because scrollTo({behavior:'auto'}) does NOT override a CSS
   scroll-behavior on the element. */
.wk-fade{scroll-behavior:auto!important}
}
