Copy 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">▾</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>©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">↗</span></a></li>
<li data-cat><a href="#">Agency <span class="cat__arrow">↗</span></a></li>
<li data-cat><a href="#">Organization <span class="cat__arrow">↗</span></a></li>
<li data-cat><a href="#">Startup <span class="cat__arrow">↗</span></a></li>
<li data-cat><a href="#">Company <span class="cat__arrow">↗</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">↗</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>JS banner-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 = ' ';
} 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)' }
);
});
});
}
})();