ToolsWaves
Free GSAP Landing Page Template · ToolsWaves · SaaS

Free SaaS Agency Hero Template with Sonar Wifi Loader

Free SaaS agency landing page template from ToolsWaves featuring a sonar-pulse loader matching the wifi-style logo, floating pill nav, rotating award badge, 3D photo tilt, animated decorative curves, and phone-ring shake. Free for any commercial or personal project.

SaaSSonarBadge3D TiltDecorative Curves

About this ToolsWaves template

This free SaaS landing page template from ToolsWaves uses a clean, light-themed design for technology agencies, SaaS startups, and B2B software brands that want to feel approachable rather than aggressive. The loader uses a sonar pulse effect that visually matches the brand's wifi-style logo, building unity between loading state and main page. The hero features a floating pill navigation, a rotating award badge that adds credibility, and a photo with subtle 3D tilt on mouse movement.

Use this template for B2B SaaS landing pages, technology consultancies, or any brand that wants to balance professional polish with light interactive moments. ToolsWaves offers this template completely free — copy the three files into your project, replace the demo branding with your actual product or service identity, and customize the blue palette through CSS variables. The animation timings can be adjusted in the JS file to slow down or speed up the choreography depending on your brand pacing.

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 SaaS Agency Hero Template with Sonar Wifi Loader</title>
  <meta name="description" content="Free GSAP SaaS landing page hero from ToolsWaves" />
  <meta name="generator" content="ToolsWaves - https://toolswaves.in/landing-pages" />

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

  <!-- Twitter -->
  <meta name="twitter:card" content="summary_large_image" />
  <meta name="twitter:title" content="Free SaaS Agency Hero Template with Sonar Wifi Loader" />
  <meta name="twitter:description" content="Free GSAP SaaS landing page hero from ToolsWaves" />
  <meta name="twitter:image" content="https://toolswaves.in/og?title=Free%20SaaS%20Agency%20Hero%20Template%20with%20Sonar%20Wifi%20Loader&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 {
    --sky-1: #d7ecfe;
    --sky-2: #c0dffc;
    --blue:  #2563eb;
    --blue-deep: #1d4ed8;
    --blue-soft: #93c5fd;
    --lime:  #d4f543;
    --lime-deep: #b9dc30;
    --navy:  #0f172a;
    --navy-muted: rgba(15, 23, 42, 0.65);
    --white: #ffffff;
    --shadow: 0 20px 50px rgba(15, 23, 42, 0.1);
}

html, body {
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    background: var(--sky-1);
    color: var(--navy);
    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(--navy);
    text-decoration: none;
    font-size: 0.65rem;
    font-weight: 600;
    letter-spacing: 0.12em;
    padding: 0.45rem 0.9rem;
    background: var(--white);
    border-radius: 999px;
    box-shadow: 0 4px 12px rgba(15, 23, 42, 0.08);
    opacity: 0.85;
    transition: opacity 0.3s, transform 0.3s;
}
.back-link:hover { opacity: 1; transform: translateY(-2px); }

/* =========================================================
   LOADER — Sonar signal
   ========================================================= */
.loader {
    position: fixed;
    inset: 0;
    z-index: 999;
    background: var(--sky-1);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2.5rem;
}

.loader__sonar {
    position: relative;
    width: 240px;
    height: 240px;
    display: flex;
    align-items: center;
    justify-content: center;
}
.loader__logo {
    position: relative;
    z-index: 2;
    width: 80px;
    height: 80px;
}
.loader__pulses {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
}
.loader__pulses span {
    position: absolute;
    width: 60px;
    height: 60px;
    border-radius: 50%;
    border: 2px solid var(--blue);
    opacity: 0;
    will-change: transform, opacity;
}

.loader__meta {
    display: flex;
    gap: 1.25rem;
    align-items: center;
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.75rem;
    letter-spacing: 0.25em;
    color: var(--navy-muted);
}
.loader__counter {
    color: var(--blue);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    min-width: 3ch;
}

/* =========================================================
   SAAS SECTION
   ========================================================= */
.saas {
    position: relative;
    width: 100%;
    height: 100vh;
    min-height: 720px;
    overflow: hidden;
    background: linear-gradient(135deg, var(--sky-1) 0%, var(--sky-2) 100%);
}

/* Decorative curves */
.curves {
    position: absolute;
    inset: 0;
    width: 100%; height: 100%;
    z-index: 1;
    pointer-events: none;
}
.curves path {
    stroke-dasharray: 2000;
    stroke-dashoffset: 2000;
}

/* ---------- Top nav (floating pill) ---------- */
.topnav {
    position: relative;
    z-index: 30;
    padding: 1.25rem 2rem;
    display: flex;
    justify-content: center;
}
.topnav__pill {
    display: flex;
    align-items: center;
    gap: 2rem;
    background: var(--white);
    border-radius: 999px;
    padding: 0.65rem 0.75rem 0.65rem 1.5rem;
    box-shadow: 0 10px 40px rgba(15, 23, 42, 0.08);
    max-width: 1100px;
    width: 100%;
}

.topnav__logo {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    text-decoration: none;
    color: var(--navy);
    font-weight: 700;
    font-size: 1.25rem;
    letter-spacing: -0.01em;
    will-change: transform;
}
.topnav__logo-mark { width: 32px; height: 32px; }
.topnav__logo-mark svg { width: 100%; height: 100%; display: block; }

.topnav__links {
    display: flex;
    gap: 1.75rem;
    margin-left: auto;
}
.topnav__links a {
    position: relative;
    color: var(--navy);
    text-decoration: none;
    font-weight: 500;
    font-size: 0.95rem;
    padding: 0.3rem 0.1rem;
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    will-change: transform;
}
.caret {
    font-size: 0.7em;
    color: var(--blue);
    transition: transform 0.3s;
    display: inline-block;
}
.topnav__links a:hover .caret { transform: translateY(2px); }
.topnav__links a::after {
    content: '';
    position: absolute;
    left: 0; bottom: 0;
    width: 100%; height: 2px;
    background: var(--blue);
    transform: scaleX(0);
    transform-origin: right;
    transition: transform 0.35s cubic-bezier(0.65, 0, 0.35, 1);
    border-radius: 2px;
}
.topnav__links a:hover::after { transform: scaleX(1); transform-origin: left; }

.topnav__cta {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    background: var(--lime);
    color: var(--navy);
    text-decoration: none;
    padding: 0.85rem 1.5rem;
    border-radius: 999px;
    font-weight: 700;
    font-size: 0.95rem;
    will-change: transform;
    transition: background 0.3s;
    position: relative;
    overflow: hidden;
}
.topnav__cta svg { width: 15px; height: 15px; transition: transform 0.3s ease; }
.topnav__cta:hover { background: var(--lime-deep); }
.topnav__cta:hover svg { transform: translateX(4px); }

/* ---------- Hero ---------- */
.hero {
    position: relative;
    z-index: 10;
    display: grid;
    grid-template-columns: 1.1fr 1fr;
    gap: 3rem;
    padding: 2rem 4rem 4rem;
    align-items: center;
    min-height: calc(100vh - 100px);
}

/* LEFT */
.hero__left { max-width: 580px; }

.hero__eyebrow {
    display: inline-flex;
    align-items: center;
    gap: 0.75rem;
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.28em;
    color: var(--blue);
    text-transform: uppercase;
    margin-bottom: 1.25rem;
}
.hero__eyebrow-line {
    display: inline-block;
    width: 40px;
    height: 2px;
    background: var(--blue);
    transform-origin: left;
}

.hero__title {
    font-size: clamp(2.2rem, 4.3vw, 3.75rem);
    font-weight: 800;
    line-height: 1.08;
    letter-spacing: -0.02em;
    color: var(--navy);
    margin-bottom: 1.5rem;
}
.hero__line {
    display: block;
    overflow: hidden;
    padding-bottom: 0.04em;
}
.hero__char {
    display: inline-block;
    will-change: transform, color;
    transition: color 0.3s ease;
}
.hero__char.is-space { width: 0.25em; }

.hero__desc {
    font-size: 1rem;
    line-height: 1.55;
    color: var(--navy-muted);
    margin-bottom: 2rem;
    max-width: 460px;
}

.hero__cta-row {
    display: flex;
    align-items: center;
    gap: 1.5rem;
    flex-wrap: wrap;
}

.btn-primary {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--blue);
    color: var(--white);
    text-decoration: none;
    padding: 1rem 2.25rem;
    border-radius: 999px;
    font-weight: 600;
    font-size: 0.95rem;
    overflow: hidden;
    will-change: transform;
    transition: background 0.3s;
    box-shadow: 0 8px 22px rgba(37, 99, 235, 0.3);
}
.btn-primary__label { position: relative; z-index: 2; }
.btn-primary::before {
    content: '';
    position: absolute;
    inset: 0;
    background: var(--navy);
    transform: scaleY(0);
    transform-origin: bottom;
    transition: transform 0.45s cubic-bezier(0.65, 0, 0.35, 1);
    z-index: 1;
}
.btn-primary:hover::before { transform: scaleY(1); transform-origin: top; }

.phone {
    display: inline-flex;
    align-items: center;
    gap: 0.85rem;
    color: var(--navy);
    text-decoration: none;
    will-change: transform;
}
.phone__icon {
    position: relative;
    width: 50px; height: 50px;
    border-radius: 50%;
    background: var(--lime);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--navy);
    flex-shrink: 0;
}
.phone__icon svg { width: 20px; height: 20px; position: relative; z-index: 2; }
.phone__ring {
    position: absolute;
    inset: -6px;
    border: 2px solid var(--lime);
    border-radius: 50%;
    opacity: 0.5;
}
.phone__label {
    display: block;
    font-size: 0.7rem;
    font-weight: 600;
    color: var(--navy-muted);
    letter-spacing: 0.18em;
    margin-bottom: 0.15rem;
}
.phone__number {
    display: block;
    font-size: 1rem;
    font-weight: 700;
    color: var(--navy);
}

/* RIGHT — photo + badges */
.hero__right {
    position: relative;
    min-height: 540px;
}

.hero__photo-shape {
    position: absolute;
    top: -6%;
    right: -4%;
    width: 115%;
    height: 110%;
    background: var(--white);
    border-radius: 55% 40% 50% 45% / 45% 55% 50% 50%;
    opacity: 0.4;
    will-change: transform;
    z-index: 1;
}

.hero__photo {
    position: absolute;
    inset: 0;
    z-index: 2;
    overflow: hidden;
    border-radius: 48% 40% 44% 52% / 42% 50% 50% 48%;
    will-change: transform;
    box-shadow: 0 30px 60px rgba(15, 23, 42, 0.15);
}
.hero__photo img {
    width: 100%; height: 100%;
    object-fit: cover;
    object-position: center 22%;
    user-select: none;
    transition: transform 0.6s ease;
}
.hero__photo:hover img { transform: scale(1.05); }

/* Rotating badge */
.badge {
    position: absolute;
    left: -70px;
    top: 46%;
    width: 160px;
    height: 160px;
    z-index: 5;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--white);
    border-radius: 50%;
    box-shadow: 0 12px 30px rgba(15, 23, 42, 0.15);
    text-decoration: none;
    will-change: transform;
}
.badge svg:first-child {
    width: 100%;
    height: 100%;
    position: absolute;
    inset: 0;
    will-change: transform;
}
.badge__logo {
    position: relative;
    z-index: 2;
    width: 46px;
    height: 46px;
}

/* Stripe circle */
.stripes {
    position: absolute;
    right: -20px;
    bottom: 12%;
    width: 90px;
    height: 90px;
    z-index: 5;
    will-change: transform;
    opacity: 0.85;
}
.stripes svg { width: 100%; height: 100%; }

/* ---------- Initial hidden states ---------- */
[data-magnetic], [data-link], [data-fade], [data-eyebrow], [data-phone], [data-read],
[data-title-line] .hero__char, [data-badge], [data-stripes], [data-shape] { opacity: 0; }

/* ---------- Responsive ---------- */
@media (max-width: 1100px) {
    .hero { grid-template-columns: 1fr; padding: 2rem 2rem 3rem; }
    .hero__right { min-height: 380px; max-width: 500px; margin: 0 auto; }
    .badge { left: -30px; top: 40%; width: 130px; height: 130px; }
    .topnav__links { display: none; }
}
@media (max-width: 760px) {
    .topnav__pill { padding: 0.5rem 0.5rem 0.5rem 1.25rem; }
    .topnav__cta { padding: 0.65rem 1.2rem; font-size: 0.85rem; }
    .topnav__logo-text { font-size: 1.1rem; }
    .hero__cta-row { gap: 1rem; }
    .btn-primary { padding: 0.85rem 1.75rem; font-size: 0.9rem; }
    .badge { width: 100px; height: 100px; left: -20px; }
    .stripes { width: 60px; height: 60px; right: 0; }
}
@media (max-width: 480px) {
    .hero { padding: 1rem 1.25rem 2rem; }
    .hero__right { min-height: 320px; }
    .phone__label { font-size: 0.6rem; }
    .phone__number { font-size: 0.85rem; }
    .phone__icon { width: 42px; height: 42px; }
    .badge { 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 — SaaS Tech</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@500;600&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="banner-saas-tech.css">
</head>
<body>
    

    <!-- ========== LOADER — Sonar signal ========== -->
    <div class="loader" id="loader">
        <div class="loader__sonar" id="sonar">
            <!-- Static logo icon -->
            <svg class="loader__logo" viewBox="0 0 80 80" aria-hidden="true">
                <path d="M40 52 a4 4 0 1 0 0.01 0" fill="#2563eb"/>
                <path d="M40 46 a10 10 0 0 0 -10 -10" stroke="#2563eb" stroke-width="3" stroke-linecap="round" fill="none"/>
                <path d="M40 38 a18 18 0 0 0 -18 -18" stroke="#2563eb" stroke-width="3" stroke-linecap="round" fill="none"/>
                <path d="M40 30 a26 26 0 0 0 -26 -26" stroke="#2563eb" stroke-width="3" stroke-linecap="round" fill="none"/>
            </svg>
            <!-- Emanating rings (JS-driven) -->
            <div class="loader__pulses" id="pulses"></div>
        </div>
        <div class="loader__meta">
            <span class="loader__label" id="loader-label">CONNECTING</span>
            <span class="loader__counter" id="loader-counter">00%</span>
        </div>
    </div>

    <section class="saas" id="saas">

        <!-- Decorative curves -->
        <svg class="curves" viewBox="0 0 1400 800" preserveAspectRatio="none" aria-hidden="true">
            <path id="curve1" d="M-50 700 Q 300 400 200 100" stroke="#93c5fd" stroke-width="1.5" fill="none" stroke-linecap="round"/>
            <path id="curve2" d="M-80 780 Q 180 500 40 200" stroke="#93c5fd" stroke-width="1" fill="none" stroke-linecap="round"/>
            <path id="curve3" d="M680 100 Q 900 300 1100 80" stroke="#93c5fd" stroke-width="1" fill="none" stroke-linecap="round"/>
            <path id="curve4" d="M720 260 Q 860 300 900 220" stroke="#93c5fd" stroke-width="1.5" fill="none" stroke-linecap="round"/>
            <circle cx="80" cy="450" r="6" fill="#2563eb" opacity="0.5"/>
            <circle cx="130" cy="620" r="4" fill="#2563eb" opacity="0.4"/>
            <circle cx="770" cy="150" r="4" fill="#2563eb" opacity="0.5"/>
        </svg>

        <!-- ========== FLOATING NAV PILL ========== -->
        <header class="topnav">
            <div class="topnav__pill">
                <a href="#" class="topnav__logo" data-magnetic>
                    <span class="topnav__logo-mark" id="nav-logo-mark">
                        <svg viewBox="0 0 32 32">
                            <path d="M16 22 a2 2 0 1 0 0.01 0" fill="#2563eb"/>
                            <path d="M16 18 a5 5 0 0 0 -5 -5" stroke="#2563eb" stroke-width="2" stroke-linecap="round" fill="none"/>
                            <path d="M16 13 a9 9 0 0 0 -9 -9" stroke="#2563eb" stroke-width="2" stroke-linecap="round" fill="none"/>
                            <path d="M16 8 a13 13 0 0 0 -13 -13" stroke="#2563eb" stroke-width="2" stroke-linecap="round" fill="none"/>
                        </svg>
                    </span>
                    <span class="topnav__logo-text">Brand</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>Services <span class="caret">&#9662;</span></a>
                    <a href="#" data-magnetic data-link>Projects <span class="caret">&#9662;</span></a>
                    <a href="#" data-magnetic data-link>Blog <span class="caret">&#9662;</span></a>
                    <a href="#" data-magnetic data-link>Team <span class="caret">&#9662;</span></a>
                    <a href="#" data-magnetic data-link>Contact</a>
                </nav>
                <a href="#" class="topnav__cta" data-magnetic>
                    <span>Get A Quote</span>
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
                        <path d="M5 12h14M13 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/>
                    </svg>
                </a>
            </div>
        </header>

        <!-- ========== HERO ========== -->
        <div class="hero">
            <!-- LEFT column -->
            <div class="hero__left">
                <span class="hero__eyebrow" data-eyebrow>
                    <span class="hero__eyebrow-line"></span>
                    SMART SOLUTIONS
                </span>

                <h1 class="hero__title" aria-label="We're The Best Tech Leading Company">
                    <span class="hero__line" data-title-line>We're The Best</span>
                    <span class="hero__line" data-title-line>Tech Leading</span>
                    <span class="hero__line" data-title-line>Company</span>
                </h1>

                <p class="hero__desc" data-fade>
                    We have been operating for over a decade providing top-notch
                    solutions to empower growing businesses and leading teams worldwide.
                </p>

                <div class="hero__cta-row" data-fade>
                    <a href="#" class="btn-primary" data-magnetic data-read>
                        <span class="btn-primary__label">Read More</span>
                    </a>
                    <a href="tel:+20855501122" class="phone" data-magnetic data-phone>
                        <span class="phone__icon">
                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                                <path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6 19.79 19.79 0 01-3.07-8.67A2 2 0 014.11 2h3a2 2 0 012 1.72 12.84 12.84 0 00.7 2.81 2 2 0 01-.45 2.11L8.09 9.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45 12.84 12.84 0 002.81.7A2 2 0 0122 16.92z"
                                      stroke-linecap="round" stroke-linejoin="round"/>
                            </svg>
                            <span class="phone__ring" id="phone-ring"></span>
                        </span>
                        <span class="phone__text">
                            <span class="phone__label">NEED HELP</span>
                            <span class="phone__number">(208) 555-0112</span>
                        </span>
                    </a>
                </div>
            </div>

            <!-- RIGHT column -->
            <div class="hero__right">
                <div class="hero__photo-shape" data-shape></div>
                <div class="hero__photo" id="photo">
                    <img src="https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&w=900&q=80"
                         alt="Professional at laptop"
                         draggable="false" onerror="this.style.display='none'">
                </div>

                <!-- Rotating badge -->
                <a href="#" class="badge" id="badge" data-badge>
                    <svg viewBox="0 0 200 200" aria-hidden="true">
                        <defs>
                            <path id="badgeCircle" d="M 100,100 m -72,0 a 72,72 0 1,1 144,0 a 72,72 0 1,1 -144,0" />
                        </defs>
                        <text font-family="Inter, sans-serif" font-weight="700" font-size="13" letter-spacing="3" fill="#0f172a">
                            <textPath href="#badgeCircle">AGENCY AWARD · WINNING DIGITAL · </textPath>
                        </text>
                    </svg>
                    <svg class="badge__logo" viewBox="0 0 60 60" aria-hidden="true">
                        <path d="M30 42 a3 3 0 1 0 0.01 0" fill="#2563eb"/>
                        <path d="M30 37 a7 7 0 0 0 -7 -7" stroke="#2563eb" stroke-width="2.2" stroke-linecap="round" fill="none"/>
                        <path d="M30 30 a13 13 0 0 0 -13 -13" stroke="#2563eb" stroke-width="2.2" stroke-linecap="round" fill="none"/>
                        <path d="M30 23 a19 19 0 0 0 -19 -19" stroke="#2563eb" stroke-width="2.2" stroke-linecap="round" fill="none"/>
                    </svg>
                </a>

                <!-- Small stripe-circle decoration -->
                <div class="stripes" data-stripes>
                    <svg viewBox="0 0 100 100" aria-hidden="true">
                        <defs>
                            <clipPath id="stripeClip"><circle cx="50" cy="50" r="48"/></clipPath>
                        </defs>
                        <g clip-path="url(#stripeClip)">
                            <rect y="0"  width="100" height="4" fill="#2563eb"/>
                            <rect y="10" width="100" height="4" fill="#2563eb"/>
                            <rect y="20" width="100" height="4" fill="#2563eb"/>
                            <rect y="30" width="100" height="4" fill="#2563eb"/>
                            <rect y="40" width="100" height="4" fill="#2563eb"/>
                            <rect y="50" width="100" height="4" fill="#2563eb"/>
                            <rect y="60" width="100" height="4" fill="#2563eb"/>
                            <rect y="70" width="100" height="4" fill="#2563eb"/>
                            <rect y="80" width="100" height="4" fill="#2563eb"/>
                            <rect y="90" width="100" height="4" fill="#2563eb"/>
                        </g>
                    </svg>
                </div>
            </div>
        </div>
    </section>

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

</body>
</html>
JSbanner-saas-tech.js— GSAP animation timeline
(() => {
    const root = document.getElementById('saas');
    if (!root) return;

    // -------------------------------------------------------------------
    //  GENERATE SONAR PULSES (loader)
    // -------------------------------------------------------------------
    const pulses = document.getElementById('pulses');
    const PULSE_COUNT = 4;
    for (let i = 0; i < PULSE_COUNT; i++) pulses.appendChild(document.createElement('span'));
    const pulseEls = pulses.querySelectorAll('span');

    // -------------------------------------------------------------------
    //  SPLIT TITLE INTO CHARS
    // -------------------------------------------------------------------
    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);
        });
    });

    // -------------------------------------------------------------------
    //  REFERENCES
    // -------------------------------------------------------------------
    const loader = document.getElementById('loader');
    const loaderCounter = document.getElementById('loader-counter');
    const loaderLabel = document.getElementById('loader-label');
    const sonar = document.getElementById('sonar');

    const magnetics = root.querySelectorAll('[data-magnetic]');
    const titleChars = root.querySelectorAll('.hero__char');
    const fades = root.querySelectorAll('[data-fade]');
    const eyebrow = root.querySelector('[data-eyebrow]');
    const eyebrowLine = root.querySelector('.hero__eyebrow-line');
    const photo = document.getElementById('photo');
    const photoShape = root.querySelector('[data-shape]');
    const badge = document.getElementById('badge');
    const badgeCircleSvg = badge.querySelector('svg:first-child');
    const stripes = root.querySelector('[data-stripes]');
    const curves = root.querySelectorAll('.curves path');
    const curveDots = root.querySelectorAll('.curves circle');
    const navLogoMark = document.getElementById('nav-logo-mark');
    const phoneRing = document.getElementById('phone-ring');

    // -------------------------------------------------------------------
    //  INITIAL STATES
    // -------------------------------------------------------------------
    gsap.set(magnetics, { y: -20, opacity: 0 });
    gsap.set(eyebrow, { x: -20, opacity: 0 });
    gsap.set(eyebrowLine, { scaleX: 0 });
    gsap.set(titleChars, { yPercent: 110, opacity: 0 });
    gsap.set(fades, { y: 24, opacity: 0 });
    gsap.set(photoShape, { scale: 0.85, opacity: 0 });
    gsap.set(photo, { scale: 0.92, opacity: 0, y: 30 });
    gsap.set(badge, { scale: 0, opacity: 0, rotation: -90 });
    gsap.set(stripes, { scale: 0.3, opacity: 0, rotation: -30 });
    gsap.set(curveDots, { scale: 0, opacity: 0, transformOrigin: 'center center' });

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

    // Sonar pulses: continuously expanding rings
    const pulseTweens = [];
    pulseEls.forEach((el, i) => {
        const tween = gsap.fromTo(el,
            { scale: 0.6, opacity: 0.6 },
            {
                scale: 3.5,
                opacity: 0,
                duration: 2,
                repeat: -1,
                ease: 'power1.out',
                delay: i * 0.5,
            }
        );
        pulseTweens.push(tween);
    });

    // Sonar container subtle breathe
    gsap.to(sonar, {
        scale: 1.04,
        duration: 1.2,
        yoyo: true,
        repeat: -1,
        ease: 'sine.inOut',
    });

    // Counter + labels
    const p = { v: 0 };
    loaderTl.to(p, {
        v: 100,
        duration: 2.3,
        ease: 'power1.inOut',
        onUpdate: () => {
            loaderCounter.textContent = String(Math.floor(p.v)).padStart(2, '0') + '%';
            if (p.v > 25 && loaderLabel.textContent === 'CONNECTING') loaderLabel.textContent = 'OPTIMIZING';
            if (p.v > 60 && loaderLabel.textContent === 'OPTIMIZING') loaderLabel.textContent = 'ALMOST READY';
            if (p.v > 95 && loaderLabel.textContent === 'ALMOST READY') loaderLabel.textContent = 'SIGNAL ACQUIRED';
        },
    });

    // Final big burst: stop loops, let last pulse fly out
    loaderTl.call(() => {
        pulseTweens.forEach(t => t.pause());
    }, null, '+=0.1');
    loaderTl.to(pulseEls, {
        scale: 8,
        opacity: 0,
        duration: 0.9,
        ease: 'power2.out',
        stagger: 0.05,
    }, '+=0.05');

    // Exit
    loaderTl.to([loaderCounter, loaderLabel], {
        y: -10, opacity: 0, duration: 0.3, stagger: 0.05,
    }, '-=0.5');
    loaderTl.to(sonar, {
        scale: 1.5, opacity: 0, duration: 0.6, ease: 'power3.in',
    }, '-=0.4');
    loaderTl.to(loader, {
        opacity: 0, duration: 0.4, ease: 'power2.inOut',
    }, '-=0.2');
    loaderTl.set(loader, { display: 'none' });

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

        // Decorative curves draw in
        tl.to(curves, {
            strokeDashoffset: 0,
            duration: 1.8,
            stagger: 0.12,
            ease: 'power2.inOut',
        }, 0);
        tl.to(curveDots, {
            scale: 1, opacity: 1,
            duration: 0.5,
            stagger: 0.1,
            ease: 'back.out(2)',
        }, 0.5);

        // Nav pill
        tl.to(magnetics, {
            y: 0, opacity: 1,
            duration: 0.6,
            stagger: 0.05,
        }, 0.2);

        // Eyebrow + line
        tl.to(eyebrow, {
            x: 0, opacity: 1,
            duration: 0.6,
        }, 0.6);
        tl.to(eyebrowLine, {
            scaleX: 1,
            duration: 0.7,
            ease: 'power4.inOut',
        }, 0.7);

        // Title chars
        tl.to(titleChars, {
            yPercent: 0, opacity: 1,
            duration: 1.1,
            stagger: 0.02,
            ease: 'expo.out',
        }, 0.8);

        // Description + CTA row
        tl.to(fades, {
            y: 0, opacity: 1,
            duration: 0.7,
            stagger: 0.15,
        }, 1.2);

        // Photo shape + photo
        tl.to(photoShape, {
            scale: 1, opacity: 1,
            duration: 1.2,
            ease: 'power4.out',
        }, 0.5);
        tl.to(photo, {
            scale: 1, opacity: 1, y: 0,
            duration: 1.3,
            ease: 'power4.out',
        }, 0.7);

        // Badge
        tl.to(badge, {
            scale: 1, opacity: 1, rotation: 0,
            duration: 0.9,
            ease: 'back.out(1.6)',
        }, 1.2);

        // Stripes
        tl.to(stripes, {
            scale: 1, opacity: 0.85, rotation: 0,
            duration: 0.8,
            ease: 'back.out(1.6)',
        }, 1.35);

        tl.call(startContinuous, null, 1.6);
        tl.call(enableInteractions, null, 1.6);
    }

    // -------------------------------------------------------------------
    //  CONTINUOUS LOOPS
    // -------------------------------------------------------------------
    let badgeRot = null;

    function startContinuous() {
        // Badge text rotates continuously
        badgeRot = gsap.to(badgeCircleSvg, {
            rotation: 360,
            duration: 18,
            repeat: -1,
            ease: 'none',
            transformOrigin: 'center center',
        });

        // Photo subtle breathing (baked into transform so parallax can add)
        gsap.to(photo, {
            y: -8,
            duration: 3,
            yoyo: true,
            repeat: -1,
            ease: 'sine.inOut',
        });

        // Phone ring pulse
        gsap.to(phoneRing, {
            scale: 1.15,
            opacity: 0,
            duration: 1.4,
            repeat: -1,
            ease: 'power1.out',
            transformOrigin: 'center center',
        });

        // Stripes slow rotation
        gsap.to(stripes, {
            rotation: '+=360',
            duration: 28,
            repeat: -1,
            ease: 'none',
            transformOrigin: 'center center',
        });

        // Logo sonar arcs subtle animation (stagger opacity)
        const logoArcs = navLogoMark.querySelectorAll('path');
        logoArcs.forEach((arc, i) => {
            gsap.fromTo(arc,
                { opacity: 0.3 },
                {
                    opacity: 1,
                    duration: 0.9,
                    yoyo: true,
                    repeat: -1,
                    ease: 'sine.inOut',
                    delay: i * 0.2,
                }
            );
        });
    }

    // -------------------------------------------------------------------
    //  INTERACTIONS
    // -------------------------------------------------------------------
    function enableInteractions() {

        // ---- Magnetic ----
        magnetics.forEach((el) => {
            const strength = el.classList.contains('topnav__cta') ? 0.35
                : el.classList.contains('btn-primary') ? 0.3
                : el.classList.contains('phone') ? 0.25
                : 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;
                gsap.to(el, {
                    x: (e.clientX - cx) * strength,
                    y: (e.clientY - cy) * strength,
                    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)' });
            });
        });

        // ---- Logo sonar burst on hover ----
        const logo = root.querySelector('.topnav__logo');
        logo.addEventListener('mouseenter', () => {
            const arcs = navLogoMark.querySelectorAll('path');
            gsap.fromTo(arcs,
                { scale: 0.8 },
                {
                    scale: 1.15,
                    duration: 0.25,
                    yoyo: true,
                    repeat: 1,
                    stagger: 0.08,
                    ease: 'sine.inOut',
                    transformOrigin: 'center',
                }
            );
        });

        // ---- Title char proximity + hover ----
        const hero = root.querySelector('.hero');
        let tmx = -9999, tmy = -9999;
        hero.addEventListener('mousemove', (e) => { tmx = e.clientX; tmy = e.clientY; });
        hero.addEventListener('mouseleave', () => {
            tmx = -9999; tmy = -9999;
            titleChars.forEach((c) => gsap.to(c, { y: 0, duration: 0.5, ease: 'power3.out' }));
        });
        gsap.ticker.add(() => {
            if (tmx < 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 = tmx - cx, dy = tmy - cy;
                const dist = Math.sqrt(dx * dx + dy * dy);
                if (dist < 140) gsap.set(c, { y: -(1 - dist / 140) * 14 });
                else gsap.set(c, { y: 0 });
            });
        });
        titleChars.forEach((c) => {
            c.addEventListener('mouseenter', () => gsap.to(c, { color: '#2563eb', duration: 0.2 }));
            c.addEventListener('mouseleave', () => gsap.to(c, { color: '#0f172a', duration: 0.4 }));
        });

        // ---- Badge: speed up rotation on hover ----
        badge.addEventListener('mouseenter', () => {
            if (badgeRot) gsap.to(badgeRot, { timeScale: 5, duration: 0.5 });
            gsap.to(badge, { scale: 1.08, duration: 0.4, ease: 'back.out(2)' });
        });
        badge.addEventListener('mouseleave', () => {
            if (badgeRot) gsap.to(badgeRot, { timeScale: 1, duration: 0.6 });
            gsap.to(badge, { scale: 1, duration: 0.5, ease: 'elastic.out(1, 0.4)' });
        });

        // ---- Phone: tilt-shake on hover (like ringing) ----
        const phoneEl = root.querySelector('[data-phone]');
        const phoneIconSvg = phoneEl.querySelector('.phone__icon svg');
        phoneEl.addEventListener('mouseenter', () => {
            gsap.fromTo(phoneIconSvg,
                { rotation: 0 },
                {
                    rotation: 15,
                    duration: 0.08,
                    yoyo: true,
                    repeat: 5,
                    ease: 'sine.inOut',
                    transformOrigin: 'center center',
                }
            );
        });

        // ---- Photo 3D tilt parallax ----
        const right = root.querySelector('.hero__right');
        let rx = 0, ry = 0, trx = 0, try_ = 0;
        right.addEventListener('mousemove', (e) => {
            const r = right.getBoundingClientRect();
            trx = ((e.clientX - r.left) / r.width - 0.5) * 2;
            try_ = ((e.clientY - r.top) / r.height - 0.5) * 2;
        });
        right.addEventListener('mouseleave', () => { trx = 0; try_ = 0; });

        gsap.ticker.add(() => {
            rx += (trx - rx) * 0.08;
            ry += (try_ - ry) * 0.08;
            gsap.set(photo, {
                rotationY: rx * 6,
                rotationX: -ry * 4,
                x: rx * 10,
                transformPerspective: 1000,
                transformOrigin: 'center',
            });
            gsap.set(photoShape, {
                x: -rx * 6,
                y: -ry * 4,
            });
            gsap.set(badge, {
                x: rx * 14,
                y: ry * 10,
            });
            gsap.set(stripes, {
                x: rx * 18,
                y: ry * 12,
            });
        });

        // ---- Decorative curves respond to cursor (shift) ----
        let gX = 0, gY = 0, gcX = 0, gcY = 0;
        root.addEventListener('mousemove', (e) => {
            const r = root.getBoundingClientRect();
            gX = ((e.clientX - r.left) / r.width - 0.5) * 2;
            gY = ((e.clientY - r.top) / r.height - 0.5) * 2;
        });
        root.addEventListener('mouseleave', () => { gX = 0; gY = 0; });
        const curvesSvg = root.querySelector('.curves');
        gsap.ticker.add(() => {
            gcX += (gX - gcX) * 0.04;
            gcY += (gY - gcY) * 0.04;
            gsap.set(curvesSvg, { x: gcX * 20, y: gcY * 14 });
        });
    }
})();

More GSAP landing pages