To provide the best experiences, we use technologies like cookies to store and/or access device information. Consenting to these technologies will allow us to process data such as browsing behaviour or unique IDs on this site. Not consenting or withdrawing consent, may adversely affect certain features and functions.
The technical storage or access is strictly necessary for the legitimate purpose of enabling the use of a specific service explicitly requested by the subscriber or user, or for the sole purpose of carrying out the transmission of a communication over an electronic communications network.
The technical storage or access is necessary for the legitimate purpose of storing preferences that are not requested by the subscriber or user.
The technical storage or access that is used exclusively for statistical purposes.
The technical storage or access that is used exclusively for anonymous statistical purposes. Without a subpoena, voluntary compliance on the part of your Internet Service Provider, or additional records from a third party, information stored or retrieved for this purpose alone cannot usually be used to identify you.
The technical storage or access is required to create user profiles to send advertising, or to track the user on a website or across several websites for similar marketing purposes.
Manage consent
document.addEventListener("DOMContentLoaded", function () {
if (typeof gsap === 'undefined' || typeof ScrollTrigger === 'undefined') {
console.error('GSAP or ScrollTrigger is not loaded');
return;
}
gsap.registerPlugin(ScrollTrigger);
const cards = gsap.utils.toArray(".stack-card");
if (cards.length === 0) return;
const lastCard = cards[cards.length - 1];
const stackWrapper = document.querySelector(".stack-wrapper");
const nextSection = stackWrapper?.nextElementSibling;
// Animation settings
const scrollDistancePerCard = 400; // Scroll distance for each card animation
const cardScale = 0.94; // Scale for cards that move up
const cardMoveY = -120; // How much cards move up
// Get the tallest card height to ensure full visibility
let maxCardHeight = 0;
cards.forEach(card => {
const cardH = Math.max(card.scrollHeight, card.offsetHeight, card.clientHeight);
if (cardH > maxCardHeight) maxCardHeight = cardH;
});
// Calculate total wrapper height needed for all sequential animations
const totalScrollDistance = cards.length * scrollDistancePerCard;
// Set wrapper height to accommodate all sequential scroll animations
if (stackWrapper) {
const wrapperHeight = totalScrollDistance + maxCardHeight + 300;
stackWrapper.style.height = wrapperHeight + "px";
stackWrapper.style.minHeight = wrapperHeight + "px";
}
// Track cumulative scroll offset for sequential animation
let cumulativeOffset = 0;
cards.forEach((card, i) => {
const isLastCard = i === cards.length - 1;
// Calculate start and end points for sequential animation
// Each card starts after the previous one finishes
const cardStartOffset = cumulativeOffset;
const cardEndOffset = cumulativeOffset + scrollDistancePerCard;
// Update cumulative offset for next card
cumulativeOffset += scrollDistancePerCard;
gsap.to(card, {
y: cardMoveY,
scale: isLastCard ? 1 : cardScale,
scrollTrigger: {
trigger: stackWrapper,
start: `top+=${cardStartOffset} top+=120`,
end: `top+=${cardEndOffset} top+=120`,
scrub: true,
onLeave: () => {
if (isLastCard && stackWrapper) {
// Release the last card first
card.classList.add("released");
stackWrapper.classList.add("collapsed");
// Remove spacing
card.style.marginBottom = "0";
card.style.paddingBottom = "0";
stackWrapper.style.marginBottom = "0";
stackWrapper.style.paddingBottom = "0";
// Ensure card overflow is visible
card.style.overflow = "visible";
card.style.maxHeight = "none";
// Use double requestAnimationFrame to ensure DOM is fully updated
requestAnimationFrame(() => {
requestAnimationFrame(() => {
// Get the card's full height - use scrollHeight for complete content
const cardFullHeight = Math.max(
card.scrollHeight,
card.offsetHeight,
card.clientHeight
);
// Get wrapper's current top position
const wrapperRect = stackWrapper.getBoundingClientRect();
const wrapperTop = wrapperRect.top + window.scrollY;
// Get card's current position relative to wrapper
const cardRect = card.getBoundingClientRect();
const cardTop = cardRect.top + window.scrollY;
const cardOffsetFromWrapper = Math.max(0, cardTop - wrapperTop);
// Calculate wrapper height: card offset + full card height + generous buffer
const wrapperHeight = cardOffsetFromWrapper + cardFullHeight + 100;
stackWrapper.style.height = wrapperHeight + "px";
stackWrapper.style.minHeight = wrapperHeight + "px";
stackWrapper.style.maxHeight = "none";
// Force a reflow to ensure height is applied
stackWrapper.offsetHeight;
});
});
// Ensure next section starts right after with no gap
if (nextSection) {
nextSection.style.marginTop = "0";
nextSection.style.paddingTop = "0";
nextSection.style.position = "relative";
nextSection.style.zIndex = "10";
}
}
},
onEnterBack: () => {
if (isLastCard && stackWrapper) {
// Restore sticky behavior
card.classList.remove("released");
stackWrapper.classList.remove("collapsed");
// Restore wrapper height
let maxCardHeight = 0;
cards.forEach(c => {
const cardH = Math.max(c.scrollHeight, c.offsetHeight, c.clientHeight);
if (cardH > maxCardHeight) maxCardHeight = cardH;
});
const totalScroll = cards.length * scrollDistancePerCard;
const wrapperHeight = totalScroll + maxCardHeight + 300;
stackWrapper.style.height = wrapperHeight + "px";
stackWrapper.style.minHeight = wrapperHeight + "px";
}
}
}
});
});
});