
Make it stand out
Introduce your brand
<div class="zakini-intro" id="zakini-intro"> <div class="panel left"> <img class="panel-img" alt="Left panel" src="https://static1.squarespace.com/static/689991212a7345610c783fa7/t/68c93729b852960b1ce44e20/1758017321361/Group+9.png"> </div> <div class="panel right"> <img class="panel-img" alt="Right panel" src="https://static1.squarespace.com/static/689991212a7345610c783fa7/t/68c93742dd55742bd2309962/1758017346783/Group+10.png"> </div> <img class="logo" alt="ZAKINI" src="https://static1.squarespace.com/static/689991212a7345610c783fa7/t/68c9376e319c0108a9a42eb6/1758017390626/Group+11.png" /> </div> <style> /* Stage */ #zakini-intro{ position: relative; height: 100vh; overflow: hidden; background: transparent; } /* Panels: visible at load (screenshot start) */ #zakini-intro .panel{ position: absolute; top: 50%; transform: translateY(-50%); /* no X shift initially */ width: clamp(140px, 21.8vw, 419px); height: clamp(64px, 11.5vw, 222px); z-index: 2; will-change: transform; } #zakini-intro .panel.left { left: 0; } #zakini-intro .panel.right { right: 0; } #zakini-intro .panel-img{ width: 100%; height: 100%; object-fit: contain; pointer-events: none; user-select: none; } /* Logo: centered, scales .5 -> 1; JS caps final width to fit the diamond */ #zakini-intro .logo{ position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%) scale(.5); width: 200px; /* temporary; JS will set a safe max-width */ height: auto; opacity: 0; z-index: 3; pointer-events: none; user-select: none; will-change: transform, opacity, width; } @media (prefers-reduced-motion: reduce){ .panel, .logo{ transition: none !important; animation: none !important; } } </style> <!-- GSAP + ScrollTrigger --> <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script> <script> document.addEventListener("DOMContentLoaded", () => { gsap.registerPlugin(ScrollTrigger); // ---- Tunables ---- // ---- Tunables (target the exact stop position) ---- // Desired diamond width at the center (where the tips overlap). // It's a clamped value: min .. preferred (as vw of the section) .. max const DIAMOND_MIN_PX = 40; // try 38–46 to make it smaller/bigger const DIAMOND_PREF_VW = 0.8; // 2.6% of section width (raise -> larger diamond) const DIAMOND_MAX_PX = 120; // upper cap const $section = document.getElementById("zakini-intro"); const $left = document.querySelector("#zakini-intro .panel.left"); const $right = document.querySelector("#zakini-intro .panel.right"); const $logo = document.querySelector("#zakini-intro .logo"); // compute distances each refresh (clamp sizes change with viewport) function metrics() { const sw = $section.clientWidth; // section width (panels are positioned in this space) const center = sw / 2; const pw = $left.offsetWidth; // panel width const ph = $left.offsetHeight; // panel height (not strictly needed now) // distance so inner edge hits the exact center (no overlap) const toCenter = center - pw; // target diamond width (clamped) const preferred = sw * DIAMOND_PREF_VW; // % of section width const overlap = Math.max(DIAMOND_MIN_PX, Math.min(preferred, DIAMOND_MAX_PX)); // move each panel half of the diamond width past center const leftX = toCenter + overlap / 2; const rightX = -(toCenter + overlap / 2); // keep logo inside the diamond (with a little padding) const LOGO_PADDING_PX = 0; const logoMax = Math.max(0, overlap - LOGO_PADDING_PX * 2); return { leftX, rightX, overlap, logoMax }; } // set initial safe logo width (also on resize) function sizeLogo() { const { logoMax } = metrics(); $logo.style.maxWidth = logoMax + "px"; $logo.style.width = Math.min(160, logoMax) + "px"; // base size, still capped } sizeLogo(); const tl = gsap.timeline({ scrollTrigger: { trigger: "#zakini-intro", start: "top top", end: "+=150%", scrub: true, pin: true, anticipatePin: 1, invalidateOnRefresh: true, onRefresh: sizeLogo }, defaults: { ease: "power3.out" } }); // Panels slide inward and stop overlapped, forming the diamond tl.to("#zakini-intro .panel.left", { x: () => metrics().leftX, duration: 1 }, 0) .to("#zakini-intro .panel.right", { x: () => metrics().rightX, duration: 1 }, 0) // Logo appears at the end, scaling .5 -> 1 and staying inside diamond .to("#zakini-intro .logo", { opacity: 1, scale: 1, duration: 0.35 }, 0.90); // later = closer to end; adjust 0.90..0.98 to taste // Re-measure when everything loads window.addEventListener("load", () => { sizeLogo(); ScrollTrigger.refresh(); }); // Re-measure on resize as clamps change window.addEventListener("resize", sizeLogo); }); </script>
<div class="timeline-wrapper" id="half-arc-timeline"> <div class="pin"> <!-- static heading --> <h2 class="static-heading"> We Work With the People <br>Who Shape Environments. </h2> <div class="viewport"> <div class="ring" aria-hidden="true"> <div class="ring-item"><span class="ring-num">1</span></div> <div class="ring-item"><span class="ring-num">2</span></div> <div class="ring-item"><span class="ring-num">3</span></div> <div class="ring-item"><span class="ring-num">4</span></div> <div class="ring-item"><span class="ring-num">5</span></div> </div> <div class="center-text"> <h2 class="title">Instant</h2> <p class="desc">What once took months, now happens in moments.</p> <a href="#" class="btn">Learn More</a> </div> </div> </div> </div> <style> /* scoped vars */ #half-arc-timeline{ --ring: min(140vmin, 1920px); /* diameter */ --dot: 64px; --outside: 48px; /* how far big numbers sit outside the arc */ --tick: 8px; /* <— size of the small moving dots */ --stroke: 2px; --accent: #fff; --fg: #e7ecf6; --muted: #aeb7c2; --bg: rgba(2, 1, 34, 1); background: var(--bg); position: relative; } .after-content{ background:#0a0a0a; padding: 480vh 6vw; color:#e7ecf6; } .after-content .container{ max-width: 900px; margin: 0 auto; } /* heading */ #half-arc-timeline .static-heading{ position: absolute; top: 6%; left: 50%; transform: translateX(-50%); margin: 0; width: min(88vw, 1000px); text-align: center; color: var(--fg); font-size: clamp(20px, 3vw, 36px); font-weight: 600; line-height: 1.2; z-index: 3; } /* layout */ #half-arc-timeline .pin{ position: relative; height: 100vh; } #half-arc-timeline .viewport{ position: absolute; left: 0; right: 0; bottom: 0; height: calc(var(--ring) / 2); overflow: visible; } /* ring */ #half-arc-timeline .ring{ --rot: -90deg; /* 1 at top on load */ position: absolute; left: 50%; top: 90px; /* moved 48px higher than original */ width: var(--ring); height: var(--ring); transform: translateX(-50%); border-radius: 50%; box-shadow: inset 0 0 0 var(--stroke) rgba(255,255,255,.18); } /* spokes */ #half-arc-timeline .ring-item{ position: absolute; left: 50%; top: 50%; width: 1px; height: 1px; transform-origin: 0 0; transform: rotate(calc(var(--deg, 0deg) + var(--rot, 0deg))); transition: transform .35s cubic-bezier(.2,.7,.2,1); } /* numbers: OUTSIDE the arc */ #half-arc-timeline .ring-num{ display: grid; place-items: center; width: var(--dot); height: var(--dot); border-radius: 50%; border: 1px solid rgba(255,255,255,.5); color: var(--fg); background: rgba(0,0,0,.4); backdrop-filter: blur(3px); transform: translate( calc(var(--ring)/2 + var(--outside) - var(--stroke) - var(--dot)/2), calc(-1 * var(--dot)/2) ) rotate(calc(-1 * (var(--deg, 0deg) + var(--rot, 0deg)))); font-size: 14px; font-weight: 900; letter-spacing: 1.25px; } #half-arc-timeline .ring-item.is-active .ring-num{ border-color: var(--accent); background: var(--accent); color: #111; } /* NEW: tiny dot sitting ON the arc (one per item) */ #half-arc-timeline .ring-dot{ position: absolute; left: 0; top: 0; width: var(--tick); height: var(--tick); border-radius: 50%; background: #fff; /* place on stroke (no counter-rotate needed) */ transform: translate( calc(var(--ring)/2 - var(--stroke) - var(--tick)/2), calc(-1 * var(--tick)/2) ); box-shadow: 0 0 0 1px rgba(255,255,255,.35) inset, 0 0 4px rgba(255,255,255,.25); } /* center text */ #half-arc-timeline .center-text{ position: absolute; left: 50%; top: 35%; transform: translateX(-50%); text-align: center; width: min(88vw, 720px); padding: 0 12px; z-index: 2; color: var(--fg); } #half-arc-timeline .center-text .title{ margin: 0 0 .6rem; font-size: clamp(28px, 4.4vw, 52px); line-height: 1.1; transition: opacity .25s ease; } #half-arc-timeline .center-text .desc{ margin: 0; color: var(--muted); font-size: clamp(14px, 1.3vw, 16px); transition: opacity .25s ease; } #half-arc-timeline .center-text .btn{ display:inline-block; margin-top:1rem; padding:.6rem 1.2rem; background:#fff; color:#111; font-weight:900; text-transform:uppercase; border-radius:16px; text-decoration:none; transition:background .3s ease, opacity .25s ease; letter-spacing:1.25px; font-size:14px; line-height:1; padding:32px 64px; margin-top:clamp(32px,7.29vw,140px); border: 1px solid #fff; } #half-arc-timeline .center-text .btn:hover{ background: transparent; color: #fff; } </style> <script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js"></script> <script> /* start at top on reload/return */ if ('scrollRestoration' in history) history.scrollRestoration = 'manual'; function jumpToTop(){ window.scrollTo(0,0); setTimeout(()=>window.scrollTo(0,0),0); } addEventListener('load', jumpToTop); addEventListener('pageshow', e => { if (e.persisted) jumpToTop(); }); (() => { gsap.registerPlugin(ScrollTrigger); const scene = document.getElementById('half-arc-timeline'); const pinEl = scene.querySelector('.pin'); const ring = scene.querySelector('.ring'); const items = Array.from(scene.querySelectorAll('.ring-item')); const titleEl = scene.querySelector('.center-text .title'); const descEl = scene.querySelector('.center-text .desc'); const btnEl = scene.querySelector('.center-text .btn'); const titles = ['Instant','Predictive','Accessible','Intelligent','Designed for you']; const descs = [ 'What once took months, now happens in moments.', 'So you never have to wait for symptoms.', 'No longer limited by location or availability.', 'Where data tells your story — and changes your outcome.', 'Because you’re not a checkbox or a protocol.' ]; const btns = [ { text: 'Learn More', href: '#instant' }, { text: 'See How', href: '#predictive' }, { text: 'Get Started', href: '#accessible' }, { text: 'Discover', href: '#intelligent' }, { text: 'Try It Now', href: '#designed' } ]; const N = items.length; const STEP = 360 / N; items.forEach((item, i) => item.style.setProperty('--deg', (STEP * i) + 'deg')); /* NEW: add one tiny dot per item (rotates with the spoke) */ items.forEach((item) => { const d = document.createElement('span'); d.className = 'ring-dot'; item.appendChild(d); }); let active = -1; function setActive(i){ if (i === active) return; items[active]?.classList.remove('is-active'); items[i]?.classList.add('is-active'); titleEl.style.opacity = 0; descEl.style.opacity = 0; btnEl.style.opacity = 0; setTimeout(() => { titleEl.textContent = titles[i] || ''; descEl.textContent = descs[i] || ''; btnEl.textContent = btns[i]?.text || ''; btnEl.href = btns[i]?.href || '#'; titleEl.style.opacity = 1; descEl.style.opacity = 1; btnEl.style.opacity = 1; }, 140); active = i; } ring.style.setProperty('--rot', '-90deg'); setActive(0); ScrollTrigger.create({ trigger: scene, start: "top top", /*end: () => "+=" + (window.innerHeight * 1.25),*/ end: () => "+=" + (window.innerHeight * 3), /*scrub: true,*/ scrub: 1.2, pin: pinEl, pinSpacing: true, onUpdate: self => { const p = self.progress; const rot = -90 - p * ((N - 1) * STEP); ring.style.setProperty('--rot', rot + 'deg'); const idx = Math.min(N - 1, Math.round(p * (N - 1))); setActive(idx); } }); requestAnimationFrame(() => ScrollTrigger.refresh()); setTimeout(() => ScrollTrigger.refresh(), 150); })(); </script> <script> /* DROP-IN PATCH: normalize initial load if we start inside the scene */ (() => { const scene = document.getElementById('half-arc-timeline'); if (!scene || !window.ScrollTrigger) return; const inScene = () => { const r = scene.getBoundingClientRect(); const top = r.top + (pageYOffset || 0); const bottom = top + r.height; const y = pageYOffset || 0; return y > top - 2 && y < bottom + 2; }; async function normalizeAtBoot() { try { await (document.fonts && document.fonts.ready); } catch(e){} requestAnimationFrame(() => { if (!inScene()) return; // Hide during normalization to avoid any blink/jump const prevVis = scene.style.visibility; scene.style.visibility = 'hidden'; const r = scene.getBoundingClientRect(); const top = r.top + (pageYOffset || 0); // Nudge scroll to just above trigger start so pinning measures correctly scrollTo(0, Math.max(0, top - 1)); // After scroll, force a fresh measure requestAnimationFrame(() => { try { ScrollTrigger.refresh(true); } catch(e){} scene.style.visibility = prevVis; }); }); } if (document.readyState === 'complete') normalizeAtBoot(); else addEventListener('load', normalizeAtBoot); // Handle Safari back/forward cache restores addEventListener('pageshow', e => { if (e.persisted) normalizeAtBoot(); }); })(); </script>