Skip to content

Instantly share code, notes, and snippets.

@kazzohikaru
Created March 27, 2026 20:56
Show Gist options
  • Select an option

  • Save kazzohikaru/11dd76d64c7c37609dfba3c1f1d1d743 to your computer and use it in GitHub Desktop.

Select an option

Save kazzohikaru/11dd76d64c7c37609dfba3c1f1d1d743 to your computer and use it in GitHub Desktop.
ScrollSynced Carousel V3 (Gsap + Swiper)
<!--
Horizontal, responsive carousel (Swiper-based) enhanced with GSAP ScrollTrigger for
scroll-synchronized animations. It supports both scrubbed (scroll-controlled) and free-swiping modes.
-->
<div class="viewport-wrapper">
<div class="content-scroll">
<main class="main-content">
<div class="dummy-block">
<div>
<h2>ScrollSynced Carousel (Gsap + Swiper)</h2>
<h3>Scroll Down<br></h3>
</div>
</div>
<div class="carousel" id="carousel_1">
<div class="wrapper">
<div class="text-before">
<h2>Fragments of a Digital Collapse.</h2>
</div>
<div class="swiper-container">
<div class="swiper-column-gap"></div>
<div class="swiper-wrapper">
<div class="swiper-slide no-interaction">
<div class="card">
<div class="media-container">
<picture>
<source srcset="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-01-300x300.webp 300w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-01-435x435.webp 435w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-01-820x820.webp 820w" sizes="(max-width:51.29875em) 100vw, 500px">
<img class="fit-cover middle-center" src="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-01.png" width="1024" height="1024" alt="Exploding digital billboard structure" decoding="sync">
</picture>
</div>
<div class="card-text">
<h3 class='title'>Billboard Implosion</h3>
<p>A towering billboard disintegrates into sharp shards and tangled frames, caught in the moment of collapse amid a surreal desert landscape.</p>
<a href="https://codepen.io/luis-lessrain/pen/ogNKBmx" class="cta-button"><span class="text">Glitchfy More</span></a>
</div>
</div>
</div>
<div class="swiper-slide no-interaction">
<div class="card">
<div class="media-container">
<picture>
<source srcset="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-02-300x300.webp 300w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-02-435x435.webp 435w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-02-820x820.webp 820w" sizes="(max-width:51.29875em) 100vw, 500px">
<img class="fit-cover middle-center" src="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-02.png" width="1024" height="1024" alt="Twisted billboard becoming a sculpture" decoding="sync">
</picture>
</div>
<div class="card-text">
<h3 class='title'>Signal Vortex</h3>
<p>A digital storm of wires and signage spirals upward, twisting a collapsed billboard into a sculpture of chaotic motion and force.</p>
<a href="https://codepen.io/luis-lessrain/pen/ogNKBmx" class="cta-button"><span class="text">Glitchfy More</span></a>
</div>
</div>
</div>
<div class="swiper-slide no-interaction">
<div class="card">
<div class="media-container">
<picture>
<source srcset="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-03-300x300.webp 300w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-03-435x435.webp 435w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-03-820x820.webp 820w" sizes="(max-width:51.29875em) 100vw, 500px">
<img class="fit-cover middle-center" src="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-03.png" width="1024" height="1024" alt="Giant shark built from mechanical debris" decoding="sync">
</picture>
</div>
<div class="card-text">
<h3 class='title'>Machine Shark</h3>
<p>A monstrous shark forged from steel and circuitry explodes from the ground, jaws wide in an apocalyptic display of mechanical fury.</p>
<a href="https://codepen.io/luis-lessrain/pen/ogNKBmx" class="cta-button"><span class="text">Glitchfy More</span></a>
</div>
</div>
</div>
<div class="swiper-slide no-interaction">
<div class="card">
<div class="media-container">
<picture>
<source srcset="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-04-300x300.webp 300w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-04-435x435.webp 435w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-04-820x820.webp 820w" sizes="(max-width:51.29875em) 100vw, 500px">
<img class="fit-cover middle-center" src="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-04.png" width="1024" height="1024" alt="Decomposed cartoon head in desert" decoding="async">
</picture>
</div>
<div class="card-text">
<h3 class='title'>Desert Mascot Ruin</h3>
<p>A decaying robotic icon lies half-buried in the sand, its twisted metal mouth agape like a relic from a lost theme park.</p>
<a href="https://codepen.io/luis-lessrain/pen/ogNKBmx" class="cta-button"><span class="text">Glitchfy More</span></a>
</div>
</div>
</div>
<div class="swiper-slide no-interaction">
<div class="card">
<div class="media-container">
<picture>
<source srcset="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-05-300x300.webp 300w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-05-435x435.webp 435w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-05-820x820.webp 820w" sizes="(max-width:51.29875em) 100vw, 500px">
<img class="fit-cover middle-center" src="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-05.png" width="1024" height="1024" alt="Exploding structure of digital billboards" decoding="async">
</picture>
</div>
<div class="card-text">
<h3 class='title'>Outburst of Screens</h3>
<p>An eruption of digital panels, wires, and fragments tears through the desert silence in a spectacular explosion of tech debris.</p>
<a href="https://codepen.io/luis-lessrain/pen/ogNKBmx" class="cta-button"><span class="text">Glitchfy More</span></a>
</div>
</div>
</div>
<div class="swiper-slide no-interaction">
<div class="card">
<div class="media-container">
<picture>
<source srcset="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-06-300x300.webp 300w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-06-435x435.webp 435w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-06-820x820.webp 820w" sizes="(max-width:51.29875em) 100vw, 500px">
<img class="fit-cover middle-center" src="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-06.png" width="1024" height="1024" alt="Swirling paper chaos in industrial yard" decoding="async">
</picture>
</div>
<div class="card-text">
<h3 class='title'>Twister of Sheets</h3>
<p>A whirlwind of tangled wires and flying sheets twists through a barren yard, captured mid-motion like a storm made of sketches.</p>
<a href="https://codepen.io/luis-lessrain/pen/ogNKBmx" class="cta-button"><span class="text">Glitchfy More</span></a>
</div>
</div>
</div>
</div>
</div>
<div class="swiper-pagination-container">
<div class="swiper-pagination-wrapper swiper-no-swiping">
<div class="swiper-pagination"></div>
</div>
</div>
</div>
</div>
<div class="carousel" id="carousel_2">
<div class="wrapper">
<div class="text-before">
<h2>Broadcasts from a Broken World.</h2>
</div>
<div class="swiper-container">
<div class="swiper-column-gap"></div>
<div class="swiper-wrapper">
<div class="swiper-slide no-interaction">
<div class="card">
<div class="media-container">
<picture>
<source srcset="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-07-300x300.webp 300w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-07-435x435.webp 435w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-07-820x820.webp 820w" sizes="(max-width:51.29875em) 100vw, 500px">
<img class="fit-cover middle-center" src="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-07.png" width="1024" height="1024" loading="lazy" decoding="async" alt="Glitched mouse character with distorted facial features">
</picture>
</div>
<div class="card-text">
<h3 class='title'>Glitch Mascot Face</h3>
<p>An iconic cartoon face is fragmented by digital decay, smiling eerily through bands of glitch and static distortion.</p>
<a href="https://codepen.io/luis-lessrain/pen/ogNKBmx" class="cta-button"><span class="text">Glitchfy More</span></a>
</div>
</div>
</div>
<div class="swiper-slide no-interaction">
<div class="card">
<div class="media-container">
<picture>
<source srcset="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-08-300x300.webp 300w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-08-435x435.webp 435w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-08-820x820.webp 820w" sizes="(max-width:51.29875em) 100vw, 500px">
<img class="fit-cover middle-center" src="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-08.png" width="1024" height="1024" loading="lazy" decoding="async" alt="Exploding structure of a giant cartoon mouse head">
</picture>
</div>
<div class="card-text">
<h3 class='title'>Mascot in Ruin</h3>
<p>A once-cheerful icon now collapses violently, scattering wooden debris and fragments across a barren digital wasteland.</p>
<a href="https://codepen.io/luis-lessrain/pen/ogNKBmx" class="cta-button"><span class="text">Glitchfy More</span></a>
</div>
</div>
</div>
<div class="swiper-slide no-interaction">
<div class="card">
<div class="media-container">
<picture>
<source srcset="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-09-300x300.webp 300w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-09-435x435.webp 435w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-09-820x820.webp 820w" sizes="(max-width:51.29875em) 100vw, 500px">
<img class="fit-cover middle-center" src="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-09.png" width="1024" height="1024" loading="lazy" decoding="async" alt="Organic architecture glitching in desert">
</picture>
</div>
<div class="card-text">
<h3 class='title'>Pixel Spire</h3>
<p>A twisted tower of organic forms and circular voids disintegrates under digital strain, like architecture rewired by data corruption.</p>
<a href="https://codepen.io/luis-lessrain/pen/ogNKBmx" class="cta-button"><span class="text">Glitchfy More</span></a>
</div>
</div>
</div>
<div class="swiper-slide no-interaction">
<div class="card">
<div class="media-container">
<picture>
<source srcset="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-10-300x300.webp 300w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-10-435x435.webp 435w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-10-820x820.webp 820w" sizes="(max-width:51.29875em) 100vw, 500px">
<img class="fit-cover middle-center" src="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-10.png" width="1024" height="1024" loading="lazy" decoding="async" alt="Collapsed alien-looking structure with glitch effects">
</picture>
</div>
<div class="card-text">
<h3 class='title'>Cradle of Collapse</h3>
<p>An alien form splinters apart in the heat, caught between architecture and abstraction, held together by threads of unreality.</p>
<a href="https://codepen.io/luis-lessrain/pen/ogNKBmx" class="cta-button"><span class="text">Glitchfy More</span></a>
</div>
</div>
</div>
<div class="swiper-slide no-interaction">
<div class="card">
<div class="media-container">
<picture>
<source srcset="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-11-300x300.webp 300w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-11-435x435.webp 435w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-11-820x820.webp 820w" sizes="(max-width:51.29875em) 100vw, 500px">
<img class="fit-cover middle-center" src="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-11.png" width="1024" height="1024" loading="lazy" decoding="async" alt="Cartoon dog statue overtaken by mechanical wires">
</picture>
</div>
<div class="card-text">
<h3 class='title'>Playland Remains</h3>
<p>A cartoon dog gives a thumbs up from a mound of mechanical wreckage, half-cheerful, half-abandoned by time and nature.</p>
<a href="https://codepen.io/luis-lessrain/pen/ogNKBmx" class="cta-button"><span class="text">Glitchfy More</span></a>
</div>
</div>
</div>
<div class="swiper-slide no-interaction">
<div class="card">
<div class="media-container">
<picture>
<source srcset="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-12-300x300.webp 300w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-12-435x435.webp 435w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-12-820x820.webp 820w" sizes="(max-width:51.29875em) 100vw, 500px">
<img class="fit-cover middle-center" src="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-12.png" width="1024" height="1024" loading="lazy" decoding="async" alt="Distorted mickey-shaped head floating above desert">
</picture>
</div>
<div class="card-text">
<h3 class='title'>Echo in the Dust</h3>
<p>Suspended above a deserted wasteland, a fragmented mouse-shaped relic hovers like the ghost of a forgotten broadcast dream.</p>
<a href="https://codepen.io/luis-lessrain/pen/ogNKBmx" class="cta-button"><span class="text">Glitchfy More</span></a>
</div>
</div>
</div>
</div>
<div class="swiper-navigation-container">
<div class="swiper-navigation-wrapper swiper-no-swiping">
<div class="swiper-prev hide">
<svg aria-hidden="true" focusable="false" viewBox="0 0 40 40">
<path fill="currentColor" d="m27.042 4.792 1.333 1.25-13.917 13.917 13.917 13.833-1.333 1.333-15.167-15.166L27.042 4.792z"></path>
</svg>
</div>
<div class="swiper-next hide">
<svg aria-hidden="true" focusable="false" viewBox="0 0 40 40">
<path fill="currentColor" d="m13.208 35.125-1.333-1.25 13.917-13.917L11.875 6.125l1.333-1.333 15.167 15.166Z"></path>
</svg>
</div>
</div>
</div>
</div>
<div class="swiper-pagination-container">
<div class="swiper-pagination-wrapper swiper-no-swiping">
<div class="swiper-pagination"></div>
</div>
</div>
</div>
</div>
<div class="carousel" id="carousel_3">
<div class="wrapper">
<div class="text-before">
<h2>Artifacts of Collapse.</h2>
</div>
<div class="swiper-container">
<div class="swiper-column-gap"></div>
<div class="swiper-wrapper">
<div class="swiper-slide no-interaction">
<div class="card">
<div class="media-container">
<picture>
<source srcset="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-13-300x300.webp 300w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-13-435x435.webp 435w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-13-820x820.webp 820w" sizes="(max-width:51.29875em) 100vw, 500px">
<img class="fit-cover middle-center" src="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-13.png" width="1024" height="1024" loading="lazy" decoding="async" alt="Pagoda-like structure breaking apart with glitch fragments">
</picture>
</div>
<div class="card-text">
<h3 class='title'>Fractured Pagoda</h3>
<p>A spiraling structure collapses into shards under digital wind, its once-majestic form disassembled by glitching vectors.</p>
<a href="https://codepen.io/luis-lessrain/pen/ogNKBmx" class="cta-button"><span class="text">Glitchfy More</span></a>
</div>
</div>
</div>
<div class="swiper-slide no-interaction">
<div class="card">
<div class="media-container">
<picture>
<source srcset="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-14-300x300.webp 300w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-14-435x435.webp 435w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-14-820x820.webp 820w" sizes="(max-width:51.29875em) 100vw, 500px">
<img class="fit-cover middle-center" src="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-14.png" width="1024" height="1024" loading="lazy" decoding="async" alt="Detached mouse mascot head with cables in post-apocalyptic setting">
</picture>
</div>
<div class="card-text">
<h3 class='title'>Lost Icon Head</h3>
<p>A familiar character's head lies split on the asphalt, its wireframe nerves still twitching at sunset.</p>
<a href="https://codepen.io/luis-lessrain/pen/ogNKBmx" class="cta-button"><span class="text">Glitchfy More</span></a>
</div>
</div>
</div>
<div class="swiper-slide no-interaction">
<div class="card">
<div class="media-container">
<picture>
<source srcset="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-15-300x300.webp 300w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-15-435x435.webp 435w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-15-820x820.webp 820w" sizes="(max-width:51.29875em) 100vw, 500px">
<img class="fit-cover middle-center" src="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-15.png" width="1024" height="1024" loading="lazy" decoding="async" alt="Billboard structure spiraling into a digital tornado">
</picture>
</div>
<div class="card-text">
<h3 class='title'>Billboard Tornado</h3>
<p>What once was signage has twisted into a data-storm, pulling itself skyward from the barren desert floor.</p>
<a href="https://codepen.io/luis-lessrain/pen/ogNKBmx" class="cta-button"><span class="text">Glitchfy More</span></a>
</div>
</div>
</div>
<div class="swiper-slide no-interaction">
<div class="card">
<div class="media-container">
<picture>
<source srcset="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-16-300x300.webp 300w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-16-435x435.webp 435w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-16-820x820.webp 820w" sizes="(max-width:51.29875em) 100vw, 500px">
<img class="fit-cover middle-center" src="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-16.png" width="1024" height="1024" loading="lazy" decoding="async" alt="Glitch-formed ocean wave crashing under a digital sun">
</picture>
</div>
<div class="card-text">
<h3 class='title'>Wave Error</h3>
<p>A great crest of glitch surges forward like a corrupted tsunami, its foam pixelated into static.</p>
<a href="https://codepen.io/luis-lessrain/pen/ogNKBmx" class="cta-button"><span class="text">Glitchfy More</span></a>
</div>
</div>
</div>
<div class="swiper-slide no-interaction">
<div class="card">
<div class="media-container">
<picture>
<source srcset="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-17-300x300.webp 300w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-17-435x435.webp 435w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-17-820x820.webp 820w" sizes="(max-width:51.29875em) 100vw, 500px">
<img class="fit-cover middle-center" src="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-17.png" width="1024" height="1024" loading="lazy" decoding="async" alt="Cartoon character face melting through glitch distortions">
</picture>
</div>
<div class="card-text">
<h3 class='title'>Smile Collapse</h3>
<p>A distorted character smiles through fractured pixels, its cheer smeared across broken data streams.</p>
<a href="https://codepen.io/luis-lessrain/pen/ogNKBmx" class="cta-button"><span class="text">Glitchfy More</span></a>
</div>
</div>
</div>
<div class="swiper-slide no-interaction">
<div class="card">
<div class="media-container">
<picture>
<source srcset="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-18-300x300.webp 300w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-18-435x435.webp 435w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-18-820x820.webp 820w" sizes="(max-width:51.29875em) 100vw, 500px">
<img class="fit-cover middle-center" src="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-18.png" width="1024" height="1024" loading="lazy" decoding="async" alt="Constructed mascot face from patched metal with glitching">
</picture>
</div>
<div class="card-text">
<h3 class='title'>Mascot Redux</h3>
<p>Mechanical ears and a stitched-together smile define a rebuilt amusement relic, warped by repetition and wear.</p>
<a href="https://codepen.io/luis-lessrain/pen/ogNKBmx" class="cta-button"><span class="text">Glitchfy More</span></a>
</div>
</div>
</div>
<div class="swiper-slide no-interaction">
<div class="card">
<div class="media-container">
<picture>
<source srcset="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-19-300x300.webp 300w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-19-435x435.webp 435w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-19-820x820.webp 820w" sizes="(max-width:51.29875em) 100vw, 500px">
<img class="fit-cover middle-center" src="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-19.png" width="1024" height="1024" loading="lazy" decoding="async" alt="Cybernetic shark erupting through ground debris">
</picture>
</div>
<div class="card-text">
<h3 class='title'>Steel Shark</h3>
<p>A serrated predator bursts through layers of earth and code, jaws open in a mechanical frenzy.</p>
<a href="https://codepen.io/luis-lessrain/pen/ogNKBmx" class="cta-button"><span class="text">Glitchfy More</span></a>
</div>
</div>
</div>
<div class="swiper-slide no-interaction">
<div class="card">
<div class="media-container">
<picture>
<source srcset="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-20-300x300.webp 300w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-20-435x435.webp 435w, https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-20-820x820.webp 820w" sizes="(max-width:51.29875em) 100vw, 500px">
<img class="fit-cover middle-center" src="https://www.lessrain.com/dev/images-2025/carousel/lr-demo-img-20.png" width="1024" height="1024" loading="lazy" decoding="async" alt="Curved wave of threads and clouds in surreal motion">
</picture>
</div>
<div class="card-text">
<h3 class='title'>Threaded Tide</h3>
<p>A wave formed of winding lines and cloud wisps curls in slow motion, frozen between reality and render.</p>
<a href="https://codepen.io/luis-lessrain/pen/ogNKBmx" class="cta-button"><span class="text">Glitchfy More</span></a>
</div>
</div>
</div>
</div>
</div>
<div class="swiper-pagination-container">
<div class="swiper-pagination-wrapper swiper-no-swiping">
<div class="swiper-pagination"></div>
</div>
</div>
</div>
</div>
<div class="dummy-block">
<div>
<h3><br>Scroll Up</h3>
<h2>ScrollSynced Carousel (Gsap + Swiper)</h2>
</div>
</div>
</main>
</div>
</div>
<div id="app_blocker"></div>
<div class="resources-layer">
<div class="resources">
<a href="https://www.lessrain.com">Less Rain GmbH</a>
<a href="https://codepen.io/collection/bNyZkZ">JavaScript Codepen Collection</a>
<a href="https://codepen.io/collection/vOBVrG">Codepen Challenges Collection</a>
</div>
</div>
// Utils https://assets.codepen.io/573855/utils-v3.js
gsap.registerPlugin(ScrollTrigger, ScrollSmoother);
ScrollTrigger.config({
limitCallbacks: true,
ignoreMobileResize: true,
autoRefreshEvents: "DOMContentLoaded,load"
});
const scroller = (() => {
if (
typeof gsap === "undefined" ||
typeof ScrollSmoother === "undefined" ||
utils.device.isTouch()
) {
document.body.classList.add("normalize-scroll");
return null;
}
return {
initialize: (
contentSelector = ".content-scroll",
wrapperSelector = ".viewport-wrapper"
) =>
ScrollSmoother.create({
content: contentSelector,
wrapper: wrapperSelector,
smooth: 2,
effects: false,
normalizeScroll: true,
preventDefault: true
})
};
})();
const createCarousel = () => {
let DOM = {};
let swiper = null;
let swiperInitialized = false;
let gsapAnimation = null;
let isScrubActive = false;
let isSwiperNavigation = false;
let centeredSlides = true;
let currentActiveSlideIndex = 0;
let options = {};
let slidesInteraction = false;
let isTouching = false;
let slideOpacity = true;
const defaultOptions = {
selector: null,
centeredSlides: true,
slideOpacity: true,
isScrubActive: false,
isScrubOnTouchActive: false,
scrubDir: 1
};
const _initializeSwiper = (selectorEl) => {
if (!selectorEl) return;
const swiperOptions = {
init: false,
runCallbacksOnInit: true,
direction: "horizontal",
slidesPerView: "auto",
centeredSlides,
centeredSlidesBounds: false,
slidesOffsetBefore: _getSlidesOffset(),
slidesOffsetAfter: _getSlidesOffsetAfter(),
spaceBetween: 0,
initialSlide: currentActiveSlideIndex,
loop: false,
speed: 700,
roundLengths: false,
preloadImages: false,
touchMoveStopPropagation: false,
threshold: utils.device.isTouch() ? 10 : 6,
passiveListeners: true,
preventClicks: true,
watchSlidesProgress: slideOpacity,
watchSlidesVisibility: false,
grabCursor: !utils.device.isTouch(),
customTransition: true,
slideToClickedSlide: false,
virtualTranslate: false,
watchOverflow: false,
resistanceRatio: 0.85,
on: {
init: _onSwiperInit,
setTransition: _onSetTransition,
progress: _onSwiperProgress,
touchStart: _onTouchStart
}
};
// Add scrub-specific config
if (isScrubActive) {
swiperOptions.updateOnWindowResize = false;
swiperOptions.grabCursor = false;
utils.dom.addClass(DOM.swiper, "swiper-no-swiping");
} else {
// Attach pagination only if it exists
if (DOM.swiperPagination) {
swiperOptions.pagination = {
el: DOM.swiperPagination,
type: "bullets",
clickable: true
};
}
// Setup navigation buttons if available and non touch
if (!utils.device.isTouch()) _setupNavigation();
// Attach bounds-checking callbacks
swiperOptions.on.touchMove = _onTouchMove;
swiperOptions.on.touchEnd = _onTouchEnd;
swiperOptions.on.transitionStart = _checkBounds;
swiperOptions.on.transitionEnd = _checkBounds;
}
swiper = new Swiper(selectorEl, swiperOptions);
utils.system.nextTick(() => {
swiper.init();
_updateSwiperStateByProgress(0);
_update();
});
};
/**
* Gets the spacing between Swiper slides based on the `.swiper-column-gap` element.
* @returns {number}
*/
const _getSlideSpacing = () => {
return DOM.cachedSlideSpacing ?? 0;
};
/**
* Calculates horizontal offset before the first Swiper slide,
* based on layout breakpoints and centered slide settings.
* @returns {number}
*/
const _getSlidesOffset = () => {
const spacingOffset = _getSlideSpacing(); // already cached
const bodyWidth = document.body.clientWidth;
const maxWrapperSize = _getMaxWrapperSize();
const adjustedMax = maxWrapperSize + 0.5;
const viewportWidth = window.innerWidth;
if (viewportWidth < adjustedMax) {
return centeredSlides && viewportWidth > DOM.mdBreakpoint
? 0
: spacingOffset;
}
if (centeredSlides) return 0;
//const additionalSpacing = spacingOffset// * 2;
const wrapperWidth = maxWrapperSize - spacingOffset;
const padding = (bodyWidth - wrapperWidth) * 0.5;
return Math.max(padding, spacingOffset);
};
/**
* Calculates horizontal offset after the last Swiper slide.
* Adjusts for cases where there are too few slides to fill the width.
* @returns {number}
*/
const _getSlidesOffsetAfter = () => {
const beforeOffset = _getSlidesOffset();
if (centeredSlides || !swiperInitialized || !swiper) {
return beforeOffset;
}
const slides = swiper.slides || [];
const spacing = _getSlideSpacing();
const slideCount = slides.length;
let totalSlideWidth = 0;
for (let i = 0; i < slideCount; i++) {
totalSlideWidth += slides[i]?.offsetWidth || 0;
}
const containerWidth = swiper.width;
const remainingSpace = containerWidth - beforeOffset - totalSlideWidth;
if (remainingSpace > 0) {
const compensation =
Math.round(remainingSpace + spacing * (slideCount - 1)) + 1;
return -compensation;
}
return beforeOffset;
};
/**
* Checks swiper bounds (start/end) and updates navigation arrow visibility.
*/
const _checkBounds = () => {
if (!swiper || !swiperInitialized || !isSwiperNavigation) return;
const isBeginning = swiper.isBeginning;
const isEnd = swiper.isEnd;
_updateSwiperNavigation(isBeginning, isEnd);
};
/**
* Configures Swiper pagination if available and scrub mode is off.
*/
const _setupPagination = () => {
if (!DOM.swiperPagination) return;
swiper.params.pagination = {
el: DOM.swiperPagination,
type: "bullets",
clickable: true
};
};
const _setupNavigation = () => {
const container = DOM.swiperNavigationContainer;
if (!container) return;
DOM.swiperNext = container.querySelector(".swiper-next");
DOM.swiperPrev = container.querySelector(".swiper-prev");
isSwiperNavigation = true;
if (DOM.swiperNext) {
DOM.swiperNext.addEventListener("click", () => {
swiper.slideTo(swiper.activeIndex + 1);
});
}
if (DOM.swiperPrev) {
DOM.swiperPrev.addEventListener("click", () => {
swiper.slideTo(swiper.activeIndex - 1);
});
}
};
const _onSwiperInit = () => {
swiperInitialized = true;
_toggleSlidesInteraction(true);
};
const _toggleSlidesInteraction = (enabled = true) => {
if (!swiperInitialized || !swiper || slidesInteraction == enabled) return;
const slides = swiper.slides;
const len = slides.length;
let slide;
for (let i = 0; i < len; i++) {
slide = slides[i];
if (!slide) continue;
!enabled
? utils.dom.addClass(slide, "no-interaction")
: utils.dom.removeClass(slide, "no-interaction");
}
slidesInteraction = enabled;
};
/**
* Callback to apply transition duration to all slides manually.
* @param {number} speed - Transition duration in milliseconds.
*/
const _onSetTransition = (speed) => {
if (!swiperInitialized || !swiper) return;
const slides = swiper.slides;
const len = slides.length;
let slide;
for (let i = 0; i < len; i++) {
slide = slides[i];
if (slide && slide.style) {
slide.style.transition = `${speed}ms`;
}
}
};
/**
* Callback to apply visual effects based on Swiper progress.
* Primarily controls per-slide opacity
* @param {number} progress - Overall progress of Swiper (0–1).
*/
// Constants for Swiper slide opacity effect
const OPACITY_THRESHOLD = 0.6; // Threshold below which we disable interaction
const OPACITY_DIFF_THRESHOLD = 0.01; // Skip if opacity hasn't changed significantly
const OPACITY_MIN_PROGRESS = 0.25; // Minimum slide progress to begin fading
const OPACITY_MAX_PROGRESS = 0.85; //1; // Max slide progress
const OPACITY_MIN_VALUE = 0.25; // Faded-out opacity
const OPACITY_MAX_VALUE = 1; // Fully visible opacity
const _onSwiperProgressNotInUse = (progress) => {
if (!swiperInitialized || !swiper || !slideOpacity) return;
const slides = swiper.slides;
const len = slides.length;
let i = 0,
slide,
slideProgress,
absProgress,
opacity,
currentOpacity,
hasClass;
while (i < len) {
slide = slides[i++];
if (!slide) continue;
slideProgress = utils.math.clamp(slide.progress ?? -1, -1, 1);
absProgress = utils.math.clamp(
Math.abs(slideProgress),
OPACITY_MIN_PROGRESS,
OPACITY_MAX_PROGRESS
);
opacity = utils.math.interpolateRange(
absProgress,
OPACITY_MIN_PROGRESS,
OPACITY_MAX_PROGRESS,
OPACITY_MAX_VALUE,
OPACITY_MIN_VALUE
);
opacity = ((opacity * 1000) | 0) / 1000; // Fast toFixed(3)
//Use custom property instead of style.opacity
slide.style.setProperty("--swiper-slide-opacity", (1 - opacity).toFixed(3));
if (!isTouching) {
hasClass = slide.classList.contains("no-interaction");
opacity < OPACITY_THRESHOLD
? !hasClass && utils.dom.addClass(slide, "no-interaction")
: hasClass && utils.dom.removeClass(slide, "no-interaction");
}
}
};
const _onSwiperProgress = (progress) => {
if (!swiperInitialized || !swiper || !slideOpacity) return;
const slides = swiper.slides;
const len = slides.length;
let i = 0,
slide,
slideProgress,
absProgress,
opacity,
currentOpacity,
hasClass;
while (i < len) {
slide = slides[i++];
if (!slide) continue;
slideProgress = utils.math.clamp(slide.progress ?? -1, -1, 1);
absProgress = utils.math.clamp(
Math.abs(slideProgress),
OPACITY_MIN_PROGRESS,
OPACITY_MAX_PROGRESS
);
opacity = utils.math.interpolateRange(
absProgress,
OPACITY_MIN_PROGRESS,
OPACITY_MAX_PROGRESS,
OPACITY_MAX_VALUE,
OPACITY_MIN_VALUE
);
// opacity = Math.pow(opacity, 1.1);
opacity = ((opacity * 1000) | 0) / 1000; // Fast toFixed(3)
currentOpacity = parseFloat(slide.style.opacity || 1);
if (Math.abs(currentOpacity - opacity) > OPACITY_DIFF_THRESHOLD) {
slide.style.opacity = opacity;
}
if (!isTouching) {
hasClass = slide.classList.contains("no-interaction");
opacity < OPACITY_THRESHOLD
? !hasClass && utils.dom.addClass(slide, "no-interaction")
: hasClass && utils.dom.removeClass(slide, "no-interaction");
}
}
};
/**
* Callback triggered when user starts interacting with Swiper (touch/drag).
* Clears all transition styles to allow natural dragging.
*/
const _onTouchStart = () => {
if (!swiperInitialized || !swiper || isScrubActive) return;
const slides = swiper.slides;
const len = slides.length;
let slide;
for (let i = 0; i < len; i++) {
slide = slides[i];
if (slide && slide.style) {
slide.style.transition = "";
}
}
};
const _onTouchMove = () => {
if (!swiperInitialized || !swiper || isScrubActive) return;
isTouching = true;
if (!utils.device.isTouch()) {
_toggleSlidesInteraction(false);
}
};
const _onTouchEnd = () => {
if (!swiperInitialized || !swiper || isScrubActive) return;
isTouching = false;
_checkBounds();
if (!utils.device.isTouch()) {
_toggleSlidesInteraction(true);
}
};
/**
* Recalculates Swiper layout, navigation, and associated content positioning.
*/
const _update = () => {
_updateSwiper();
_updateTextBeforeWrapper();
_updateSwiperNavigationContainer();
};
const _getSlideSpacingFromDOM = () => {
const spacingEl = DOM.swiperSpacing;
return spacingEl ? Math.ceil(spacingEl.offsetWidth) : 0;
};
/**
* Updates Swiper layout dynamically: offsets, spacing, centering.
* Also updates pagination and visual effects.
*/
const _updateSwiper = () => {
if (!swiperInitialized || !swiper) return;
// Call to ensure transition is fully cleared before layout updates
swiper.transitionEnd?.();
// Re-evaluate `centeredSlides` based on screen size
const isSmallScreen = window.innerWidth < DOM.mdBreakpoint;
centeredSlides = isSmallScreen ? false : options.centeredSlides;
DOM.cachedSlideSpacing = _getSlideSpacingFromDOM();
swiper.params.slidesOffsetBefore = _getSlidesOffset();
swiper.params.slidesOffsetAfter = _getSlidesOffsetAfter();
swiper.params.spaceBetween = _getSlideSpacing();
swiper.params.centeredSlides = centeredSlides;
swiper.update();
swiper.pagination?.update?.();
_onSwiperProgress(swiper.progress);
};
/**
* Toggles visibility of Swiper navigation buttons based on scroll bounds.
* @param {boolean} isBeginning - True if at the first slide.
* @param {boolean} isEnd - True if at the last slide.
*/
const _updateSwiperNavigation = (isBeginning, isEnd) => {
if (!isSwiperNavigation) return;
if (DOM.swiperNext) {
const nextClassList = DOM.swiperNext.classList;
const shouldBeHidden = isEnd;
if (nextClassList.contains("hide") !== shouldBeHidden) {
nextClassList.toggle("hide", shouldBeHidden);
}
}
if (DOM.swiperPrev) {
const prevClassList = DOM.swiperPrev.classList;
const shouldBeHidden = isBeginning;
if (prevClassList.contains("hide") !== shouldBeHidden) {
prevClassList.toggle("hide", shouldBeHidden);
}
}
};
/**
* Updates custom CSS vars for aligning text before the Swiper.
* Based on current wrapper width, offset, and slide layout.
*/
const _updateTextBeforeWrapper = () => {
const { textBefore, mediaContainerRef } = DOM;
if (!swiper || !textBefore || !mediaContainerRef) return;
const bodyWidth = document.body.clientWidth;
const slideWidth = mediaContainerRef.offsetWidth;
const wDiff = Math.max(0, (bodyWidth - _getMaxWrapperSize()) * 0.5);
const slideOffset = centeredSlides
? (bodyWidth - slideWidth) * 0.5 + _getSlidesOffset()
: _getSlidesOffset();
const beforeWidth = bodyWidth - slideOffset - wDiff;
const marginLeft = slideOffset;
textBefore.style.cssText = `--swiper-text-before-width: ${beforeWidth}px; --swiper-text-before-margin-left: ${marginLeft}px;`;
};
/**
* Updates CSS variable for Swiper navigation container height
* to match the current media (slide) container height.
*/
let lastNavigationHeight = -1;
const _updateSwiperNavigationContainer = () => {
const { swiperNavigationContainer, mediaContainerRef } = DOM;
if (!swiper || !swiperNavigationContainer || !mediaContainerRef) return;
const height = mediaContainerRef.offsetHeight;
if (height === lastNavigationHeight) return; // Skip if no change
swiperNavigationContainer.style.setProperty(
"--swiper-navigation-height",
`${height}px`
);
lastNavigationHeight = height;
};
/**
* Sets up a GSAP ScrollTrigger that scrubs Swiper based on scroll progress.
*/
const _proxy = {
set _updateSwiperStateByProgress(value) {
_updateSwiperStateByProgress(value);
}
};
const _initializeGsapAnimation = () => {
if (!isScrubActive || gsapAnimation) return;
const slowDownFactor = 0.5;
const getLVH = utils.css.getLVH;
let cachedWrapperWidth = DOM.swiperWrapper?.offsetWidth || 0;
let cachedSlideHeight =
(swiper?.slides.length || 0) * getLVH() * slowDownFactor;
gsapAnimation = gsap.to(_proxy, {
_updateSwiperStateByProgress: 1,
duration: 1,
ease: "none",
scrollTrigger: {
id: `pin-${options.selector?.replace("#", "")}`,
trigger: DOM.trigger,
pin: DOM.pin,
pinSpacing: true,
scrub: true,
invalidateOnRefresh: true,
start: () => `${DOM.trigger.offsetHeight * 0.5}px ${getLVH() * 0.5}px`,
end: () => `+=${Math.max(cachedWrapperWidth, cachedSlideHeight)}px`,
//onUpdate: (self) => _updateSwiperStateByProgress(self.progress),
onRefreshInit: () => {
if (swiper && swiperInitialized) {
swiper.updateSize();
_update();
}
cachedWrapperWidth = DOM.swiperWrapper?.offsetWidth || 0;
cachedSlideHeight =
(swiper?.slides.length || 0) * getLVH() * slowDownFactor;
},
onRefresh: () => {
if (swiper && swiperInitialized) {
_update();
}
}
}
});
};
/**
* Applies scroll progress (0–1) to Swiper's internal translate state.
* Used for ScrollTrigger-based scrubbing.
* @param {number} progress - Normalized scroll progress (0 to 1)
*/
//let lastScrubProgress = -1;
const _updateSwiperStateByProgress = (progress) => {
if (!swiper || !swiperInitialized) return;
const clamped = utils.math.clamp(isNaN(progress) ? 0 : progress, 0, 1);
// Avoid unnecessary state updates for small changes
//if ((clamped * 1000 | 0) === (lastScrubProgress * 1000 | 0)) return;
//lastScrubProgress = clamped;
const directionAdjusted = options.scrubDir === -1 ? 1 - clamped : clamped;
const min = swiper.minTranslate();
const max = swiper.maxTranslate();
const translate = (max - min) * directionAdjusted + min;
swiper.translateTo(translate, 0); // 0 = no duration
swiper.updateActiveIndex();
swiper.updateSlidesClasses();
};
const _getMaxWrapperSize = () => {
const val = DOM.maxWrapperSize;
return Number.isFinite(val) && val > 0 ? val : document.body.clientWidth;
};
/**
* Resets internal state and cached references
*/
const _reset = () => {
DOM = Object.create(null); // avoids prototype inheritance issues
swiper = null;
swiperInitialized = false;
gsapAnimation = null;
isScrubActive = false;
isSwiperNavigation = false;
centeredSlides = true;
slideOpacity = true;
currentActiveSlideIndex = 0;
options = { ...defaultOptions };
};
/**
* Applies `loading="lazy"` and `decoding="async"` to images
* if Swiper is outside the initial viewport.
*/
const _maybeLazyLoadImages = () => {
const swiperEl = DOM.swiper;
if (!swiperEl) return;
const { top, bottom } = swiperEl.getBoundingClientRect();
const viewportHeight = window.innerHeight;
if (top < viewportHeight && bottom > 0) return; // Swiper is in view
const images = swiperEl.querySelectorAll("img");
let img;
for (let i = 0; i < images.length; i++) {
img = images[i];
if (!img.hasAttribute("loading")) img.setAttribute("loading", "lazy");
if (!img.hasAttribute("decoding")) img.setAttribute("decoding", "async");
}
};
/**
* Main initializer: sets up swiper instance and (optionally) ScrollTrigger.
* @param {Object} opts
*/
const initialize = (opts = {}) => {
_reset();
options = { ...defaultOptions, ...opts };
const el = utils.dom.resolveElement(options.selector);
if (!el) {
console.warn("[gsapSwiper] Invalid or missing selector.");
return;
}
const swiperEl = el.querySelector(".swiper-container");
const wrapperEl = swiperEl?.querySelector(".swiper-wrapper") || null;
const spacingEl = swiperEl?.querySelector(".swiper-column-gap") || null;
DOM.el = el;
DOM.mediaContainerRef = el.querySelector(".media-container");
DOM.textBefore = el.querySelector(".text-before");
DOM.swiper = swiperEl;
DOM.swiperSpacing = spacingEl;
DOM.swiperWrapper = wrapperEl;
DOM.cachedSlideSpacing = null;
centeredSlides = options.centeredSlides;
slideOpacity = options.slideOpacity;
DOM.maxWrapperSize = utils.css.getCssVarValue(el, "--max-wrapper-size", true);
DOM.mdBreakpoint =
utils.css.getCssVarValue(el, "--md-breakpoint", true) + 0.5;
isScrubActive = !utils.device.isTouch() && options.isScrubActive;
if (utils.device.isTouch() && options.isScrubOnTouchActive) {
isScrubActive = true;
}
if (!DOM.swiper) {
console.warn(
`[gsapSwiper] Could not find .swiper-container in ${options.selector}`
);
return;
}
if (isScrubActive) {
el.dataset.scrub = "true";
DOM.pin = swiperEl;
DOM.trigger = wrapperEl;
_initializeGsapAnimation();
} else {
DOM.swiperPagination = el.querySelector(".swiper-pagination");
DOM.swiperNavigationContainer = el.querySelector(
".swiper-navigation-container"
);
}
//_maybeLazyLoadImages();
_initializeSwiper(swiperEl);
};
return {
initialize,
update: () => {
if (swiperInitialized && swiper) _update();
},
isScrubbing: () => isScrubActive
};
};
document.addEventListener("DOMContentLoaded", () => {
if (scroller) scroller.initialize();
const carousels = [];
const carousel1 = createCarousel();
carousel1.initialize({
selector: "#carousel_1",
isScrubActive: true,
isScrubOnTouchActive: true
});
carousels.push(carousel1);
const carousel2 = createCarousel();
carousel2.initialize({
selector: "#carousel_2",
isScrubActive: false,
slideOpacity: false
});
carousels.push(carousel2);
const carousel3 = createCarousel();
carousel3.initialize({
selector: "#carousel_3",
isScrubActive: true,
isScrubOnTouchActive: true,
slideOpacity: false,
scrubDir: -1
});
carousels.push(carousel3);
const globalRefresh = () => {
carousels.forEach((instance) => {
if (!instance.isScrubbing()) {
instance.update();
}
});
ScrollTrigger.refresh();
};
const hideBlocker = () => {
const blocker = document.getElementById("app_blocker");
if (!blocker) return;
blocker.classList.add("hide");
// Remove after transition ends (or fallback timeout)
const cleanup = () => {
blocker.removeEventListener("transitionend", cleanup);
blocker.remove();
};
blocker.addEventListener("transitionend", cleanup);
setTimeout(() => {
if (document.body.contains(blocker)) blocker.remove();
}, 350);
};
if (utils.device.isTouch()) {
window.addEventListener("orientationchange", () => {
utils.system.nextTick(globalRefresh, null, 500);
});
} else {
window.addEventListener("resize", () => {
utils.system.nextTick(globalRefresh);
});
}
const isCodePen = document.referrer.includes("codepen.io");
const hostDomains = isCodePen ? ["codepen.io"] : [];
hostDomains.push(window.location.hostname);
const links = document.getElementsByTagName("a");
utils.url.validateLinks(links, hostDomains);
utils.system.nextTick(
() => {
globalRefresh();
hideBlocker();
},
null,
300
);
});
<script src="https://assets.codepen.io/573855/utils-v3.min.js?v=3"></script>
<script src="https://unpkg.co/gsap@3/dist/gsap.min.js"></script>
<script src="https://assets.codepen.io/16327/ScrollTrigger.min.js?v=3.12"></script>
<script src="https://assets.codepen.io/16327/ScrollSmoother.min.js?v=3.12"></script>
<script src="https://assets.codepen.io/573855/swiper.min.js"></script>
:root {
--dark-color-h: 334.29;
--dark-color-s: 32.03%;
--dark-color-l: 30%;
--light-color-h: 19.2;
--light-color-s: 30.86%;
--light-color-l: 84.12%;
--dark-color: hsl(
var(--dark-color-h),
var(--dark-color-s),
var(--dark-color-l)
);
--dark-color-darker: hsl(
var(--dark-color-h),
var(--dark-color-s),
calc(var(--dark-color-l) - 10%)
);
--dark-color-lighter: hsl(
var(--dark-color-h),
var(--dark-color-s),
calc(var(--dark-color-l) + 10%)
);
--dark-color-translucent: hsla(
var(--dark-color-h),
var(--dark-color-s),
var(--dark-color-l),
0.75
);
--dark-color-darker-translucent: hsla(
var(--dark-color-h),
var(--dark-color-s),
calc(var(--dark-color-l) - 10%),
0.75
);
--light-color: hsl(
var(--light-color-h),
var(--light-color-s),
var(--light-color-l)
);
--light-color-darker: hsl(
var(--light-color-h),
var(--light-color-s),
calc(var(--light-color-l) - 10%)
);
--light-color-lighter: hsl(
var(--light-color-h),
var(--light-color-s),
calc(var(--light-color-l) + 10%)
);
--light-color-translucent: hsla(
var(--light-color-h),
var(--light-color-s),
var(--light-color-l),
0.75
);
--light-color-darker-translucent: hsla(
var(--light-color-h),
var(--light-color-s),
calc(var(--light-color-l) - 10%),
0.75
);
--bg-color: var(--dark-color);
--text-color: var(--light-color);
--resources-bg-color: var(--dark-color-darker-translucent);
--resources-active-color: color-mix(
in srgb,
var(--light-color) 75%,
transparent
);
--resources-color: var(--text-color);
--spacing: 1.5rem;
--md-breakpoint: 51.25rem;
--button-color: var(--light-color);
--button-bg-color: var(--dark-color-lighter);
--button-border-color: var(--dark-color-lighter);
--button-active-color: var(--dark-color-darker);
--button-active-bg-color: var(--light-color);
--button-active-border-color: var(--light-color);
}
*,
::after,
::before {
border-style: solid;
border-width: 0;
box-sizing: border-box;
}
* {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
body {
color: var(--text-color);
font-family: Inter, Roboto, "Helvetica Neue", "Arial Nova", "Nimbus Sans",
Arial, sans-serif;
line-height: 1.5;
margin: 0;
min-height: 100vh;
padding: 0;
}
#app_blocker,
body {
background: var(--bg-color);
width: 100%;
}
#app_blocker {
display: block;
inset: 0;
min-height: 100lvh;
opacity: 1;
pointer-events: auto;
position: fixed;
transition: opacity 0.3s ease;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
z-index: 999;
}
#app_blocker.hide {
opacity: 0;
pointer-events: none;
}
:is(h1, h2, h3, h4) {
font-family: Charter, "Bitstream Charter", "Sitka Text", Cambria, serif;
font-weight: 500;
letter-spacing: 0.025em;
line-height: 1.2;
margin: 0;
text-wrap: balance;
text-wrap: pretty;
}
h2 {
font-size: 3.1575rem;
}
h2,
h3 {
color: var(--light-color);
}
h3 {
font-size: 1.33313rem;
}
p {
color: var(--light-color-darker-translucent);
font-size: 1rem;
letter-spacing: 0.025em;
}
.viewport-wrapper {
height: auto;
overflow: visible;
position: relative;
}
.content-scroll {
display: block;
width: 100%;
}
body:not(.normalize-scroll) .viewport-wrapper {
bottom: 0;
height: 100%;
left: 0;
overflow: hidden;
position: fixed;
right: 0;
top: 0;
width: 100%;
}
body:not(.normalize-scroll) .content-scroll {
overflow: visible;
}
body:not(.normalize-scroll)
.content-scroll
:is(.swiper-container, .text-before, .swiper-pagination-wrapper) {
perspective: 2000px;
}
body:not(.normalize-scroll) .content-scroll .swiper-slide > * {
outline: thin solid transparent;
transform-style: preserve-3d;
}
body:not(.normalize-scroll)
.content-scroll
:is(.text-before > *, .swiper-navigation-wrapper
> *, .swiper-pagination-wrapper > *) {
transform-style: preserve-3d;
}
body:not(.normalize-scroll)
.content-scroll
:is(.swiper-navigation-wrapper > *, .swiper-pagination-wrapper > *) {
outline: thin solid transparent;
}
main {
margin: 0;
}
.wrapper,
main {
display: block;
position: relative;
}
.wrapper {
margin-inline: auto;
width: 100%;
}
.media-container {
aspect-ratio: var(--media-aspect-ratio);
display: grid;
position: relative;
width: 100%;
}
.media-container > * {
aspect-ratio: inherit;
grid-area: 1/-1;
position: relative;
}
picture {
display: block;
height: 100%;
position: relative;
}
:is(img, svg) {
border: 0;
display: block;
height: auto;
max-width: 100%;
outline: none;
pointer-events: none;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
vertical-align: middle;
}
img {
color: transparent;
background-color: rgb(223, 207, 159);
font: 0/0 a;
margin: 0;
padding: 0;
position: relative;
text-shadow: none;
}
.fit-cover {
background-repeat: no-repeat;
background-size: cover;
height: 100%;
-o-object-fit: cover;
object-fit: cover;
width: 100%;
}
.fit-cover.middle-center {
background-position: 50% 50%;
-o-object-position: 50% 50%;
object-position: 50% 50%;
}
.carousel {
--swiper-column-gap: var(--spacing);
--swiper-column-gap-md-down: calc(var(--spacing) * 0.5);
--swiper-slides-perview: 2.25;
--swiper-slides-perview-md-down: 1.05;
--swiper-col-width: calc(1 / var(--swiper-slides-perview));
--swiper-col-width-md-down: calc(1 / var(--swiper-slides-perview-md-down));
--swiper-navigation-color: var(--light-color-darker);
--swiper-navigation-bg-color: var(--dark-color-darker-translucent);
--swiper-pagination-color: var(--dark-color-lighter);
--swiper-pagination-active-color: var(--light-color-darker);
--media-aspect-ratio: 8 / 5;
--media-aspect-ratio-md-down: 4 / 3;
display: block;
position: relative;
}
.carousel + .carousel {
margin-top: calc(var(--spacing) * 2);
}
[data-scrub] {
--pin-spacer-bg-color: var(--bg-color);
}
[data-scrub] [class*="pin-spacer"]:before {
background: var(--pin-spacer-bg-color, transparent);
content: "";
display: block;
height: 100%;
left: 0;
pointer-events: none;
position: absolute;
top: 0;
width: 100%;
}
.text-before {
display: block;
margin-left: var(--swiper-text-before-margin-left, 0);
max-width: var(--swiper-text-before-width, 100%);
padding: 0 var(--spacing) calc(var(--spacing) * 1.5) 0;
position: relative;
width: 100%;
}
.swiper-column-gap,
.text-before {
pointer-events: none;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.swiper-column-gap {
border: 0;
height: 0.0625rem;
margin: -0.0625rem calc(-1 * var(--swiper-column-gap));
overflow-x: clip;
overflow-y: clip;
padding: 0;
position: absolute;
width: var(--swiper-column-gap);
}
.swiper-slide {
height: none;
height: auto;
min-width: 0;
position: relative;
width: calc(
(
var(--max-wrapper-size, 100%) - var(--swiper-column-gap) *
(var(--swiper-slides-perview) + 1)
) * var(--swiper-col-width)
);
}
.card {
background: var(--dark-color-darker);
}
.card,
.card-text {
display: block;
height: 100%;
position: relative;
}
.card-text {
background: inherit;
padding: var(--spacing);
}
.card-text p {
-webkit-box-orient: vertical;
display: -webkit-box;
-webkit-hyphens: none;
hyphens: none;
-webkit-line-clamp: var(--max-lines, 2);
margin-bottom: 0;
margin-top: calc(var(--spacing) * 0.25);
overflow: hidden;
overscroll-behavior: none;
visibility: visible;
white-space: normal;
word-wrap: break-word;
}
.cta-button {
align-items: center;
background-color: var(--button-bg-color);
border: thin solid var(--button-border-color);
color: var(--button-color);
cursor: pointer;
font-size: 0.8125rem;
font-weight: 500;
justify-content: center;
letetr-spacing: 0.025em;
margin-top: calc(var(--spacing) * 0.5);
min-height: 2rem;
min-width: 12ch;
pointer-events: auto;
position: relative;
text-decoration: none;
transition: all 0.2s ease-in-out;
width: -moz-max-content;
width: max-content;
}
.cta-button,
.cta-button .text {
display: inline-flex;
font-family: inherit;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.cta-button .text {
cursor: inherit;
font-size: inherit;
letter-spacing: inherit;
line-height: inherit;
padding: 0.25rem 0.5rem;
pointer-events: none;
text-align: center;
text-transform: inherit;
width: auto;
}
.cta-button:visited {
background-color: var(--button-bg-color);
border: thin solid var(--button-border-color);
color: var(--button-color);
}
.cta-button:active,
.cta-button:focus-visible {
background-color: var(--button-active-bg-color);
border: thin solid var(--button-active-border-color);
color: var(--button-active-color);
}
.cta-button:focus-visible {
outline: none;
}
.no-interaction {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.no-interaction,
.no-interaction .cta-button {
pointer-events: none;
}
[data-scrub] :is(.swiper-navigation-container, .swiper-pagination-container),
body.is-touch .swiper-navigation-container {
display: none;
}
.swiper-navigation-container {
display: block;
height: var(--swiper-navigation-height, 100%);
left: 0;
pointer-events: none;
position: absolute;
top: 0;
width: 100%;
z-index: 1;
}
.swiper-navigation-wrapper {
display: grid;
height: 100%;
pointer-events: none;
position: relative;
width: 100%;
}
.swiper-prev {
margin-right: auto;
}
.swiper-prev.hide {
transform: translate3d(-100%, 0, 0);
}
.swiper-next {
margin-left: auto;
}
.swiper-next.hide {
transform: translate3d(100%, 0, 0);
}
.swiper-next,
.swiper-prev {
background: var(--swiper-navigation-bg-color);
color: var(--swiper-navigation-color);
cursor: pointer;
display: block;
grid-area: 1/-1;
margin-block: auto;
padding: calc(var(--spacing) * 0.25);
pointer-events: auto;
position: relative;
transition: transform 0.3s cubic-bezier(0, 0, 0, 1);
}
.swiper-next svg,
.swiper-prev svg {
height: 1.5rem;
margin: auto;
pointer-events: none;
position: relative;
width: 1.5rem;
}
.swiper-pagination-container {
display: block;
margin-top: var(--spacing);
pointer-events: none;
position: relative;
}
.swiper-pagination-wrapper {
align-items: center;
display: flex;
height: 100%;
justify-content: center;
pointer-events: none;
position: relative;
}
.swiper-pagination {
pointer-events: auto;
position: relative;
}
.swiper-pagination .swiper-pagination-bullet {
background-color: var(--swiper-pagination-color);
height: 0.75rem;
opacity: 1;
position: relative;
width: 0.75rem;
}
.swiper-pagination .swiper-pagination-bullet:after {
content: "";
height: 1.375rem;
left: 50%;
pointer-events: auto;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: 1.375rem;
}
.swiper-pagination .swiper-pagination-bullet + .swiper-pagination-bullet {
margin-left: 1rem;
}
.swiper-pagination .swiper-pagination-bullet-active {
background: var(--swiper-pagination-active-color);
pointer-events: none;
}
:is(#carousel_1, #carousel_2, #carousel_3) {
--max-wrapper-size: 75rem;
}
#carousel_2 {
--media-aspect-ratio: 1/1;
background: var(--dark-color-darker);
padding-block: calc(var(--spacing) * 1.5);
}
#carousel_2 .card-text {
background: #000;
}
#carousel_3 {
--bg-color: var(--dark-color-lighter);
--media-aspect-ratio: 3/3.25;
background: var(--dark-color-lighter);
margin-top: 0;
padding: calc(var(--spacing) * 1.5) 0 calc(var(--spacing) * 3);
}
.dummy-block {
display: block;
position: relative;
width: 100%;
}
.dummy-block > * {
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
margin-inline: auto;
padding-block: 3rem;
pointer-events: none;
text-align: center;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
width: calc(100% - var(--spacing) * 2);
}
.dummy-block :is(h2, h3) {
color: var(--light-color);
font-family: Inter, Roboto, "Helvetica Neue", "Arial Nova", "Nimbus Sans",
Arial, sans-serif;
font-size: 1rem;
margin-bottom: 0;
}
.resources-layer {
bottom: 0;
display: block;
position: fixed;
right: 0;
z-index: 1000;
}
.resources {
background: var(--resources-bg-color);
display: grid;
font-family: Inter, Roboto, "Helvetica Neue", "Arial Nova", "Nimbus Sans",
Arial, sans-serif;
font-size: 0.6875rem;
font-weight: 300;
grid-auto-flow: column;
line-height: 1.3;
padding: 0.5rem;
pointer-events: auto;
}
.resources a {
align-content: center;
display: grid;
justify-content: center;
padding: 0 0.5rem;
place-content: center;
transition: color 0.2s ease-in-out;
}
.resources a,
.resources a:visited {
color: var(--resources-color);
}
.resources a:active,
.resources a:focus-visible {
color: var(--resources-active-color);
}
.resources a:focus-visible {
outline: none;
}
.resources a:not(:first-child) {
border-inline-start: thin solid currentColor;
}
@media (max-width: 51.29875em) {
h2 {
font-size: 2.36875rem;
}
p {
font-size: 0.9375rem;
line-height: 1.3;
}
.media-container {
aspect-ratio: var(--media-aspect-ratio-md-down);
}
.text-before {
margin-left: 0;
max-width: 100%;
padding: 0 calc(var(--spacing) * 0.5) calc(var(--spacing) * 1.5);
}
.swiper-column-gap {
margin: -0.0625rem calc(-1 * var(--swiper-column-gap-md-down));
width: var(--swiper-column-gap-md-down);
}
.swiper-slide {
width: calc(
(
100% - var(--swiper-column-gap-md-down) *
(var(--swiper-slides-perview-md-down) + 1)
) * var(--swiper-col-width-md-down)
);
}
.card-text {
padding: var(--spacing) calc(var(--spacing) * 0.5);
}
.dummy-block > * {
width: calc(100% - var(--spacing));
}
}
@media (hover: hover) and (pointer: fine) {
.cta-button:active:not(:hover),
.cta-button:hover {
background-color: var(--button-active-bg-color);
border: thin solid var(--button-active-border-color);
color: var(--button-active-color);
}
.resources a:active:not(:hover),
.resources a:hover {
color: var(--resources-active-color);
}
}
<link href="https://assets.codepen.io/573855/swiper.min.css" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment