ToolsWaves
Free GSAP Landing Page Template ยท ToolsWaves ยท Editorial

Free Editorial Hero Template with Magnetic Navigation and Ruler

Free editorial landing page template from ToolsWaves with massive typographic hero, magnetic navigation, character-proximity hover on the title, interactive ruler ticks, hover-reactive cart counter, and category list with slide reveal. Free for commercial use.

EditorialMagnetic NavInteractiveTypography

About this ToolsWaves template

This free editorial landing page template from ToolsWaves leads with typography. The composition is dominated by oversized headline type where individual characters react to cursor proximity โ€” pulling, scaling, or shifting depending on how close the mouse gets to them. Magnetic navigation responds to mouse position, interactive ruler ticks animate along the page edges to add subtle measurement aesthetics, the cart counter reacts on hover, and the category list slides into view on scroll.

Use this template for editorial brands, magazines, premium e-commerce, publishing platforms, or any site where typography itself communicates value. ToolsWaves provides this template free for any commercial or personal use โ€” copy the HTML, CSS, and JavaScript bundle, replace the placeholder copy with your editorial content, and adjust the typeface via the font-family declarations in the CSS file. The interactive ruler ticks are achieved entirely with CSS and can be removed if you prefer a cleaner edge.

Copy the code

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Free Editorial Hero Template with Magnetic Navigation and Ruler</title>
  <meta name="description" content="Free GSAP editorial landing page hero from ToolsWaves" />
  <meta name="generator" content="ToolsWaves - https://toolswaves.in/landing-pages" />

  <!-- Open Graph -->
  <meta property="og:title" content="Free Editorial Hero Template with Magnetic Navigation and Ruler" />
  <meta property="og:description" content="Free GSAP editorial landing page hero from ToolsWaves" />
  <meta property="og:type" content="website" />
  <meta property="og:image" content="https://toolswaves.in/og?title=Free%20Editorial%20Hero%20Template%20with%20Magnetic%20Navigation%20and%20Ruler&category=Landing%20Page&icon=%F0%9F%93%84" />

  <!-- Twitter -->
  <meta name="twitter:card" content="summary_large_image" />
  <meta name="twitter:title" content="Free Editorial Hero Template with Magnetic Navigation and Ruler" />
  <meta name="twitter:description" content="Free GSAP editorial landing page hero from ToolsWaves" />
  <meta name="twitter:image" content="https://toolswaves.in/og?title=Free%20Editorial%20Hero%20Template%20with%20Magnetic%20Navigation%20and%20Ruler&category=Landing%20Page&icon=%F0%9F%93%84" />

  <!-- Bootstrap (used by template โ€” replace with your own framework if you prefer) -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" />

  <style>
* { margin: 0; padding: 0; box-sizing: border-box; }

:root {
    --coral:     #e35d48;
    --coral-deep:#d84e39;
    --ink:       #0a0a0a;
    --ink-soft:  rgba(10, 10, 10, 0.65);
    --cream:     #f5f0ea;
    --line:      rgba(10, 10, 10, 0.25);
}

html, body {
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    background: var(--coral);
    color: var(--ink);
    overflow: hidden;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

.back-link {
    position: fixed;
    bottom: 0.75rem;
    left: 0.75rem;
    z-index: 1000;
    color: var(--cream);
    text-decoration: none;
    font-size: 0.65rem;
    font-weight: 600;
    letter-spacing: 0.12em;
    padding: 0.45rem 0.9rem;
    background: var(--ink);
    border-radius: 999px;
    opacity: 0.85;
    transition: opacity 0.3s, transform 0.3s;
}
.back-link:hover { opacity: 1; transform: translateY(-2px); }

/* =========================================================
   LOADER โ€” flip counter with line fill
   ========================================================= */
.loader {
    position: fixed;
    inset: 0;
    z-index: 999;
    background: var(--coral);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1.5rem;
}

.loader__num {
    font-family: 'Inter', sans-serif;
    font-weight: 900;
    font-size: clamp(10rem, 28vw, 26rem);
    line-height: 0.85;
    letter-spacing: -0.04em;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
}

.loader__label {
    display: flex;
    align-items: baseline;
    gap: 2px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.8rem;
    letter-spacing: 0.3em;
    color: var(--ink);
}
.loader__dots { display: inline-flex; gap: 0; }
.loader__dots span {
    opacity: 0;
    animation: dotBlink 1.2s ease-in-out infinite;
}
.loader__dots span:nth-child(1) { animation-delay: 0s; }
.loader__dots span:nth-child(2) { animation-delay: 0.2s; }
.loader__dots span:nth-child(3) { animation-delay: 0.4s; }
@keyframes dotBlink {
    0%, 100% { opacity: 0; }
    50% { opacity: 1; }
}

.loader__line {
    width: min(540px, 80vw);
    height: 2px;
    background: rgba(10, 10, 10, 0.15);
    overflow: hidden;
}
.loader__line-fill {
    height: 100%;
    width: 0%;
    background: var(--ink);
}

/* =========================================================
   EDITORIAL SECTION
   ========================================================= */
.editorial {
    position: relative;
    width: 100%;
    height: 100vh;
    min-height: 780px;
    overflow: hidden;
    background: var(--coral);
    padding: 0 2rem;
}

/* ---------- Top nav ---------- */
.topnav {
    position: relative;
    z-index: 10;
    display: flex;
    align-items: center;
    padding: 1.25rem 0;
    gap: 2rem;
}

.topnav__logo {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    text-decoration: none;
    color: var(--ink);
    will-change: transform;
}
.topnav__logo-mark { width: 28px; height: 28px; }
.topnav__logo-mark svg {
    width: 100%; height: 100%;
    transition: transform 0.6s cubic-bezier(0.65, 0, 0.35, 1);
}
.topnav__logo:hover .topnav__logo-mark svg { transform: rotate(180deg); }
.topnav__logo-text {
    font-weight: 700;
    font-size: 1.15rem;
    letter-spacing: -0.01em;
}
.topnav__logo-text em { font-style: normal; font-weight: 500; }

.topnav__links {
    margin: 0 auto;
    display: flex;
    gap: 2.25rem;
}
.topnav__links a {
    position: relative;
    color: var(--ink);
    text-decoration: none;
    font-weight: 500;
    font-size: 0.95rem;
    padding: 0.2rem 0;
    will-change: transform;
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
}
.topnav__links a::after {
    content: '';
    position: absolute;
    left: 0;
    bottom: -2px;
    width: 100%;
    height: 1px;
    background: currentColor;
    transform: scaleX(0);
    transform-origin: right;
    transition: transform 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.topnav__links a:hover::after { transform: scaleX(1); transform-origin: left; }
.topnav__caret { font-size: 0.65em; opacity: 0.7; }

.topnav__actions {
    display: flex;
    align-items: center;
    gap: 1.25rem;
}
.topnav__cart {
    color: var(--ink);
    text-decoration: none;
    font-size: 0.95rem;
    font-weight: 500;
    font-variant-numeric: tabular-nums;
}
.topnav__cart:hover { text-decoration: underline; text-underline-offset: 4px; }

.topnav__cta {
    background: var(--ink);
    color: var(--cream);
    text-decoration: none;
    padding: 0.7rem 1.4rem;
    font-weight: 500;
    font-size: 0.9rem;
    border-radius: 4px;
    display: inline-block;
    will-change: transform;
    transition: background 0.3s ease;
}
.topnav__cta:hover { background: #222; }

/* ---------- Divider ---------- */
.divider {
    height: 1px;
    background: var(--line);
    transform-origin: left;
}

/* ---------- Meta row ---------- */
.meta {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0.9rem 0;
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.75rem;
    font-weight: 500;
    letter-spacing: 0.12em;
    color: var(--ink);
}
.meta__cell {
    display: inline-block;
}
.meta__cell:nth-child(2) { margin: 0 auto; }

/* ---------- Hero (title + categories + desc) ---------- */
.hero {
    position: relative;
    height: calc(100vh - 160px);
    min-height: 620px;
}

.hero__title {
    position: absolute;
    inset: 0;
    font-family: 'Inter', sans-serif;
    font-weight: 900;
    color: var(--ink);
    line-height: 0.82;
    letter-spacing: -0.055em;
    pointer-events: none;
    margin: 0;
}
.hero__line {
    display: block;
    white-space: nowrap;
    pointer-events: auto;
}
.hero__line--top {
    position: absolute;
    top: 6%;
    left: 0;
    font-size: clamp(6rem, 22vw, 24rem);
}
.hero__line--bottom {
    position: absolute;
    bottom: 10%;
    left: 0;
    right: 0;
    font-size: clamp(5rem, 14vw, 16rem);
}
.hero__char {
    display: inline-block;
    will-change: transform, color;
    transition: color 0.3s ease;
}
.hero__char.is-space {
    width: 0.25em;
}

/* Categories (right side, vertically between the two title lines) */
.categories {
    position: absolute;
    right: 0;
    top: 45%;
    transform: translateY(-50%);
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    z-index: 3;
}
.categories li {
    overflow: hidden;
}
.categories a {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 1rem;
    color: var(--ink);
    text-decoration: none;
    font-size: clamp(1.1rem, 1.6vw, 1.5rem);
    font-weight: 500;
    padding: 0.2rem 0;
    transition: transform 0.5s cubic-bezier(0.65, 0, 0.35, 1);
}
.categories a::before {
    content: '';
    position: absolute;
    left: 0; bottom: 0;
    width: 100%; height: 1px;
    background: var(--ink);
    transform: scaleX(0);
    transform-origin: right;
    transition: transform 0.5s cubic-bezier(0.65, 0, 0.35, 1);
}
.categories a:hover {
    transform: translateX(-10px);
}
.categories a:hover::before {
    transform: scaleX(1);
    transform-origin: left;
}
.cat__arrow {
    font-size: 0.9em;
    transition: transform 0.4s cubic-bezier(0.65, 0, 0.35, 1);
    display: inline-block;
}
.categories a:hover .cat__arrow {
    transform: translate(6px, -6px) rotate(5deg);
}

/* Description (bottom left) */
.desc {
    position: absolute;
    left: 0;
    bottom: 12%;
    max-width: 300px;
    z-index: 3;
}
.desc__text {
    font-size: 0.9rem;
    line-height: 1.5;
    color: var(--ink);
    font-weight: 400;
    margin-bottom: 1.25rem;
}
.desc__more {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    color: var(--ink);
    text-decoration: none;
    font-size: 0.95rem;
    font-weight: 500;
    padding: 0.2rem 0;
    will-change: transform;
}
.desc__more::after {
    content: '';
    position: absolute;
    left: 0; bottom: 0;
    width: 100%; height: 1px;
    background: currentColor;
}
.desc__more-arrow {
    display: inline-block;
    transition: transform 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.desc__more:hover .desc__more-arrow {
    transform: translate(6px, -6px) rotate(3deg);
}

/* ---------- Ruler ---------- */
.ruler {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 22px;
    display: flex;
    align-items: flex-end;
    padding: 0 2rem;
    pointer-events: auto;
}
.ruler__tick {
    flex: 1;
    height: 6px;
    background: var(--ink);
    margin-right: 2px;
    transform-origin: bottom;
    transition: height 0.25s cubic-bezier(0.65, 0, 0.35, 1);
    will-change: transform, height;
}
.ruler__tick:nth-child(5n) { height: 12px; }
.ruler__tick:nth-child(10n) { height: 18px; }
.ruler__tick:last-child { margin-right: 0; }

/* ---------- Initial hidden states ---------- */
[data-magnetic], [data-link], [data-cart], [data-meta],
[data-title-line] .hero__char, [data-cat], [data-desc], [data-divider] {
    opacity: 0;
}
[data-divider] { transform: scaleX(0); opacity: 1; }

/* ---------- Responsive ---------- */
@media (max-width: 900px) {
    .topnav__links { display: none; }
    .categories {
        top: auto;
        bottom: 18%;
        transform: none;
        gap: 0.2rem;
    }
    .categories a { font-size: 1rem; }
    .desc { max-width: 260px; bottom: 6%; }
    .hero__line--top { font-size: 22vw; }
    .hero__line--bottom { font-size: 16vw; }
}
@media (max-width: 600px) {
    .meta__cell:nth-child(2) { display: none; }
    .hero__line--top { top: 4%; }
    .categories { display: none; }
}

  </style>
</head>
<body>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Banner โ€” Editorial Bold</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="banner-editorial-bold.css">
</head>
<body>
    

    <!-- ========== LOADER ========== -->
    <div class="loader" id="loader">
        <div class="loader__num" id="loader-num">00</div>
        <div class="loader__label">
            <span id="loader-label-text">LOADING</span><span class="loader__dots"><span>.</span><span>.</span><span>.</span></span>
        </div>
        <div class="loader__line"><div class="loader__line-fill" id="loader-line"></div></div>
    </div>

    <section class="editorial" id="editorial">

        <!-- ========== TOP NAV ========== -->
        <header class="topnav">
            <a href="#" class="topnav__logo" data-magnetic>
                <span class="topnav__logo-mark">
                    <svg viewBox="0 0 32 32" aria-hidden="true">
                        <circle cx="16" cy="16" r="16" fill="#0a0a0a"/>
                        <path d="M10 10l12 12M22 10L10 22" stroke="#e35d48" stroke-width="2.5" stroke-linecap="round"/>
                    </svg>
                </span>
                <span class="topnav__logo-text">Brand<em> FZ</em></span>
            </a>

            <nav class="topnav__links">
                <a href="#" data-magnetic data-link>Home</a>
                <a href="#" data-magnetic data-link>About</a>
                <a href="#" data-magnetic data-link>Portfolio</a>
                <a href="#" data-magnetic data-link>
                    Pages <span class="topnav__caret">&#9662;</span>
                </a>
                <a href="#" data-magnetic data-link>Products</a>
            </nav>

            <div class="topnav__actions">
                <a href="#" class="topnav__cart" data-cart>
                    Cart <span>(<span id="cart-count">0</span>)</span>
                </a>
                <a href="#" class="topnav__cta" data-magnetic>
                    <span>Personal Quote</span>
                </a>
            </div>
        </header>

        <div class="divider" data-divider></div>

        <!-- ========== META ROW ========== -->
        <div class="meta">
            <span class="meta__cell" data-meta>_01</span>
            <span class="meta__cell" data-meta>&copy;2026</span>
            <span class="meta__cell" data-meta>LOS ANGELES, CA</span>
        </div>

        <!-- ========== MAIN HEADLINE ========== -->
        <div class="hero" id="hero-content">
            <h1 class="hero__title" aria-label="Studio Creative Agency">
                <span class="hero__line hero__line--top" data-title-line>Studio</span>
                <span class="hero__line hero__line--bottom" data-title-line>Creative Agency</span>
            </h1>

            <!-- Right-side categories -->
            <ul class="categories" aria-label="Categories">
                <li data-cat><a href="#">Business <span class="cat__arrow">&#8599;</span></a></li>
                <li data-cat><a href="#">Agency <span class="cat__arrow">&#8599;</span></a></li>
                <li data-cat><a href="#">Organization <span class="cat__arrow">&#8599;</span></a></li>
                <li data-cat><a href="#">Startup <span class="cat__arrow">&#8599;</span></a></li>
                <li data-cat><a href="#">Company <span class="cat__arrow">&#8599;</span></a></li>
            </ul>

            <!-- Bottom-left description -->
            <div class="desc" data-desc>
                <p class="desc__text">
                    Crafting digital experiences for brands in design, fintech, saas, and emerging technology.
                </p>
                <a href="#" class="desc__more" data-magnetic>
                    <span class="desc__more-text">Explore more</span>
                    <span class="desc__more-arrow">&#8599;</span>
                </a>
            </div>
        </div>

        <!-- ========== RULER at bottom ========== -->
        <div class="ruler" id="ruler"></div>
    </section>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
    <script src="banner-editorial-bold.js"></script>
</body>
</html>

</body>
</html>
JSbanner-editorial-bold.jsโ€” GSAP animation timeline
(() => {
    const root = document.getElementById('editorial');
    if (!root) return;

    // -------------------------------------------------------------------
    //  SPLIT TITLE LINES INTO CHAR SPANS
    // -------------------------------------------------------------------
    const titleLines = root.querySelectorAll('[data-title-line]');
    titleLines.forEach((line) => {
        const text = line.textContent;
        line.textContent = '';
        [...text].forEach((ch) => {
            const span = document.createElement('span');
            span.className = 'hero__char';
            if (ch === ' ') {
                span.classList.add('is-space');
                span.innerHTML = '&nbsp;';
            } else {
                span.textContent = ch;
            }
            line.appendChild(span);
        });
    });

    // -------------------------------------------------------------------
    //  BUILD RULER TICKS
    // -------------------------------------------------------------------
    const ruler = document.getElementById('ruler');
    const TICK_COUNT = 80;
    for (let i = 0; i < TICK_COUNT; i++) {
        const t = document.createElement('span');
        t.className = 'ruler__tick';
        ruler.appendChild(t);
    }
    const ticks = ruler.querySelectorAll('.ruler__tick');

    // -------------------------------------------------------------------
    //  REFERENCES
    // -------------------------------------------------------------------
    const loader = document.getElementById('loader');
    const loaderNum = document.getElementById('loader-num');
    const loaderLine = document.getElementById('loader-line');
    const loaderLabel = document.getElementById('loader-label-text');

    const magnetics = root.querySelectorAll('[data-magnetic]');
    const links = root.querySelectorAll('[data-link]');
    const cartEl = root.querySelector('[data-cart]');
    const cartCount = document.getElementById('cart-count');
    const metaCells = root.querySelectorAll('[data-meta]');
    const divider = root.querySelector('[data-divider]');
    const titleChars = root.querySelectorAll('.hero__char');
    const catItems = root.querySelectorAll('[data-cat]');
    const desc = root.querySelector('[data-desc]');

    // -------------------------------------------------------------------
    //  INITIAL STATES
    // -------------------------------------------------------------------
    gsap.set(magnetics, { y: -20, opacity: 0 });
    gsap.set(metaCells, { y: 10, opacity: 0 });
    gsap.set(titleChars, { yPercent: 110, opacity: 0 });
    gsap.set(catItems, { x: 40, opacity: 0 });
    gsap.set(desc, { y: 20, opacity: 0 });
    gsap.set(divider, { scaleX: 0 });
    gsap.set(ticks, { scaleY: 0, transformOrigin: 'bottom' });

    // -------------------------------------------------------------------
    //  LOADER TIMELINE
    // -------------------------------------------------------------------
    const loaderTl = gsap.timeline({ onComplete: playScene });

    const n = { v: 0 };
    const labelWords = ['LOADING', 'COMPILING', 'RENDERING', 'READY'];

    loaderTl.to(n, {
        v: 100,
        duration: 2.2,
        ease: 'power1.inOut',
        onUpdate: () => {
            loaderNum.textContent = String(Math.floor(n.v)).padStart(2, '0');
            loaderLine.style.width = n.v + '%';
            // Rotate label word at thresholds
            const idx = Math.min(Math.floor(n.v / 25), labelWords.length - 1);
            if (loaderLabel.textContent !== labelWords[idx]) {
                loaderLabel.textContent = labelWords[idx];
            }
        },
    });

    // Final snap: go from 100 back to 01 (like a counter resetting to first)
    loaderTl.to(n, {
        v: 1,
        duration: 0.3,
        ease: 'power4.inOut',
        onUpdate: () => {
            loaderNum.textContent = String(Math.floor(n.v)).padStart(2, '0');
        },
    });

    // Hold briefly
    loaderTl.to({}, { duration: 0.25 });

    // Exit: number scales up + fades, background slides up revealing scene
    loaderTl.to([loaderNum, loaderLine.parentElement, loaderLabel.parentElement], {
        y: -30,
        opacity: 0,
        duration: 0.5,
        ease: 'power3.in',
        stagger: 0.04,
    });

    loaderTl.to(loader, {
        yPercent: -100,
        duration: 0.9,
        ease: 'power4.inOut',
    });
    loaderTl.set(loader, { display: 'none' });

    // -------------------------------------------------------------------
    //  MAIN SCENE ENTRANCE
    // -------------------------------------------------------------------
    function playScene() {
        const tl = gsap.timeline({ defaults: { ease: 'power3.out' } });

        // Nav + logo + actions
        tl.to(magnetics, {
            y: 0, opacity: 1,
            duration: 0.6,
            stagger: 0.06,
        }, 0);

        // Cart
        tl.to(cartEl, { opacity: 1, duration: 0.4 }, 0.3);

        // Divider draws across
        tl.to(divider, {
            scaleX: 1,
            duration: 0.9,
            ease: 'power4.inOut',
        }, 0.3);

        // Meta row
        tl.to(metaCells, {
            y: 0, opacity: 1,
            duration: 0.5,
            stagger: 0.08,
        }, 0.5);

        // Title chars: huge reveal
        tl.to(titleChars, {
            yPercent: 0, opacity: 1,
            duration: 1.1,
            stagger: { each: 0.02, from: 'start' },
            ease: 'expo.out',
        }, 0.7);

        // Categories stagger in
        tl.to(catItems, {
            x: 0, opacity: 1,
            duration: 0.7,
            stagger: 0.08,
        }, 1.1);

        // Description
        tl.to(desc, {
            y: 0, opacity: 1,
            duration: 0.7,
        }, 1.3);

        // Ruler ticks scale up in a wave
        tl.to(ticks, {
            scaleY: 1,
            duration: 0.5,
            stagger: { each: 0.005, from: 'start' },
            ease: 'power2.out',
        }, 1.5);

        tl.call(enableInteractions, null, 1.8);
    }

    // -------------------------------------------------------------------
    //  INTERACTIVE BEHAVIORS (enabled after entrance)
    // -------------------------------------------------------------------
    function enableInteractions() {
        // ---- Magnetic elements ----
        magnetics.forEach((el) => {
            const strength = el.classList.contains('topnav__cta') ? 0.35
                : el.classList.contains('topnav__logo') ? 0.18
                : 0.22;

            el.addEventListener('mousemove', (e) => {
                const r = el.getBoundingClientRect();
                const cx = r.left + r.width / 2;
                const cy = r.top + r.height / 2;
                const dx = (e.clientX - cx) * strength;
                const dy = (e.clientY - cy) * strength;
                gsap.to(el, { x: dx, y: dy, duration: 0.4, ease: 'power3.out' });
            });
            el.addEventListener('mouseleave', () => {
                gsap.to(el, { x: 0, y: 0, duration: 0.6, ease: 'elastic.out(1, 0.4)' });
            });
        });

        // ---- Title char proximity: chars lift toward cursor ----
        const hero = document.getElementById('hero-content');
        let mx = -9999, my = -9999;
        hero.addEventListener('mousemove', (e) => {
            mx = e.clientX; my = e.clientY;
        });
        hero.addEventListener('mouseleave', () => {
            mx = -9999; my = -9999;
            titleChars.forEach((c) => {
                gsap.to(c, { y: 0, color: '#0a0a0a', duration: 0.5, ease: 'power3.out' });
            });
        });

        const PROX_RADIUS = 180;
        const PROX_STRENGTH = 24;
        gsap.ticker.add(() => {
            if (mx < 0) return;
            titleChars.forEach((c) => {
                const r = c.getBoundingClientRect();
                if (r.width === 0) return;
                const cx = r.left + r.width / 2;
                const cy = r.top  + r.height / 2;
                const dx = mx - cx;
                const dy = my - cy;
                const dist = Math.sqrt(dx * dx + dy * dy);
                if (dist < PROX_RADIUS) {
                    const s = 1 - dist / PROX_RADIUS;
                    gsap.set(c, { y: -s * PROX_STRENGTH });
                } else {
                    gsap.set(c, { y: 0 });
                }
            });
        });

        // Individual char hover: color swap
        titleChars.forEach((c) => {
            c.addEventListener('mouseenter', () => {
                gsap.to(c, { color: '#f5f0ea', duration: 0.2 });
            });
            c.addEventListener('mouseleave', () => {
                gsap.to(c, { color: '#0a0a0a', duration: 0.4 });
            });
        });

        // ---- Ruler ticks ripple toward cursor ----
        const rulerRect = () => ruler.getBoundingClientRect();
        let rulerMouseX = -9999;
        ruler.addEventListener('mousemove', (e) => {
            rulerMouseX = e.clientX;
        });
        ruler.addEventListener('mouseleave', () => {
            rulerMouseX = -9999;
            ticks.forEach((t) => {
                gsap.to(t, { scaleY: 1, duration: 0.5, ease: 'power2.out' });
            });
        });
        gsap.ticker.add(() => {
            if (rulerMouseX < 0) return;
            ticks.forEach((t) => {
                const r = t.getBoundingClientRect();
                const cx = r.left + r.width / 2;
                const dist = Math.abs(rulerMouseX - cx);
                const range = 120;
                if (dist < range) {
                    const s = 1 - dist / range;
                    gsap.set(t, { scaleY: 1 + s * 2.8 });
                } else {
                    gsap.set(t, { scaleY: 1 });
                }
            });
        });

        // ---- Cart: increment count on hover, reset on leave ----
        let cartT = null;
        cartEl.addEventListener('mouseenter', () => {
            clearTimeout(cartT);
            const target = 1 + Math.floor(Math.random() * 4);
            const obj = { v: parseInt(cartCount.textContent, 10) || 0 };
            gsap.to(obj, {
                v: target,
                duration: 0.5,
                snap: 'v',
                ease: 'power2.out',
                onUpdate: () => { cartCount.textContent = Math.round(obj.v); }
            });
        });
        cartEl.addEventListener('mouseleave', () => {
            cartT = setTimeout(() => {
                const obj = { v: parseInt(cartCount.textContent, 10) || 0 };
                gsap.to(obj, {
                    v: 0,
                    duration: 0.4,
                    snap: 'v',
                    ease: 'power2.in',
                    onUpdate: () => { cartCount.textContent = Math.round(obj.v); }
                });
            }, 400);
        });

        // ---- Category row: subtle shake on click ----
        catItems.forEach((item) => {
            const a = item.querySelector('a');
            a.addEventListener('click', (e) => {
                e.preventDefault();
                gsap.fromTo(a,
                    { x: -10 },
                    { x: 0, duration: 0.6, ease: 'elastic.out(1.1, 0.4)' }
                );
            });
        });
    }
})();

More GSAP landing pages