ToolsWaves
Free GSAP Landing Page Template · ToolsWaves · Legal

Free Law Firm Landing Page Template with Scales of Justice Loader

Free law firm landing page template from ToolsWaves with a scales-of-justice loader where the beam oscillates and settles to balance, a circular attorney photo cluster with parallax, floating stat badges, wavy line backdrop, magnetic navigation, and phone-number scramble on hover.

LawScalesElegantMagnetic NavStats

About this ToolsWaves template

This free law firm landing page template from ToolsWaves opens with a scales-of-justice loader animation where the beam oscillates and settles into perfect balance — a fitting visual for the values most legal brands want to project: weight, fairness, and resolution. The hero scene combines a circular cluster of attorney photographs arranged with parallax depth, floating stat badges that build credibility, a wavy line backdrop that adds subtle motion, and magnetic navigation that responds to cursor proximity. The firm's phone number scrambles into focus on hover, adding a premium interactive touch.

Use this template for boutique law firms, legal tech startups, dispute resolution services, or professional advocacy organizations. ToolsWaves offers this template completely free with no licensing fees or attribution requirements. Replace the demo branding and attorney photos with your firm's actual team, update the stat badges with real numbers (cases won, years in practice, client satisfaction), and adjust the brand colors through the CSS variables exposed at the top of the stylesheet.

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 Law Firm Landing Page Template with Scales of Justice Loader</title>
  <meta name="description" content="Free GSAP law firm landing page hero from ToolsWaves" />
  <meta name="generator" content="ToolsWaves - https://toolswaves.in/landing-pages" />

  <!-- Open Graph -->
  <meta property="og:title" content="Free Law Firm Landing Page Template with Scales of Justice Loader" />
  <meta property="og:description" content="Free GSAP law firm landing page hero from ToolsWaves" />
  <meta property="og:type" content="website" />
  <meta property="og:image" content="https://toolswaves.in/og?title=Free%20Law%20Firm%20Landing%20Page%20Template%20with%20Scales%20of%20Justice%20Loader&category=Landing%20Page&icon=%F0%9F%93%84" />

  <!-- Twitter -->
  <meta name="twitter:card" content="summary_large_image" />
  <meta name="twitter:title" content="Free Law Firm Landing Page Template with Scales of Justice Loader" />
  <meta name="twitter:description" content="Free GSAP law firm landing page hero from ToolsWaves" />
  <meta name="twitter:image" content="https://toolswaves.in/og?title=Free%20Law%20Firm%20Landing%20Page%20Template%20with%20Scales%20of%20Justice%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 {
    --bg:       #faf6ef;
    --bg-soft:  #f0e9de;
    --ink:      #2e1a47;
    --ink-deep: #1f0f32;
    --muted:    rgba(46, 26, 71, 0.6);
    --line:     rgba(46, 26, 71, 0.15);
    --orange:   #e66f3e;
    --orange-deep: #d35b2c;
    --purple:   #3d2655;
    --lavender: #d5c7e5;
    --white:    #ffffff;
}

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

/* =========================================================
   LOADER — Scales of justice
   ========================================================= */
.loader {
    position: fixed;
    inset: 0;
    z-index: 999;
    background: var(--bg);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2rem;
}

.loader__scales {
    width: min(240px, 55vw);
    height: auto;
    filter: drop-shadow(0 15px 25px rgba(46, 26, 71, 0.1));
}
.loader__svg { width: 100%; height: auto; display: block; }

#scales-beam {
    transform-origin: 120px 60px;
    will-change: transform;
}

.loader__meta {
    display: flex;
    gap: 1.25rem;
    align-items: center;
    font-family: 'Inter', sans-serif;
    font-size: 0.75rem;
    letter-spacing: 0.3em;
    color: var(--muted);
    font-weight: 600;
    text-transform: uppercase;
}
.loader__counter {
    color: var(--orange);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    min-width: 3ch;
    letter-spacing: 0.1em;
}

/* =========================================================
   COUNSEL SECTION
   ========================================================= */
.counsel {
    position: relative;
    width: 100%;
    min-height: 100vh;
    overflow: hidden;
    background: var(--bg);
}

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

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

.topnav__logo {
    display: inline-flex;
    align-items: center;
    gap: 0.75rem;
    text-decoration: none;
    color: var(--ink);
    will-change: transform;
}
.topnav__logo-mark {
    width: 60px; height: 60px;
    flex-shrink: 0;
}
.topnav__logo-mark svg {
    width: 100%; height: 100%;
    transition: transform 0.6s cubic-bezier(0.65, 0, 0.35, 1);
}
.topnav__logo-text {
    display: flex;
    flex-direction: column;
    line-height: 1.1;
}
.topnav__logo-text small {
    font-size: 0.6rem;
    font-weight: 600;
    letter-spacing: 0.28em;
    color: var(--muted);
    text-transform: uppercase;
    margin-bottom: 0.1rem;
}
.topnav__logo-text big {
    font-family: 'Playfair Display', serif;
    font-size: 1.35rem;
    font-weight: 700;
    color: var(--ink);
    letter-spacing: 0.03em;
}

.topnav__nav {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: 2rem;
}
.topnav__nav a {
    position: relative;
    color: var(--ink);
    text-decoration: none;
    font-weight: 500;
    font-size: 0.95rem;
    padding: 0.3rem 0;
    will-change: transform;
}
.topnav__home {
    width: 38px; height: 38px;
    border-radius: 50%;
    background: var(--bg-soft);
    display: inline-flex !important;
    align-items: center;
    justify-content: center;
    color: var(--orange);
    padding: 0 !important;
    transition: background 0.3s, transform 0.3s;
}
.topnav__home:hover { background: var(--orange); color: #fff; }
.topnav__home svg { width: 18px; height: 18px; }
.topnav__home::after { display: none !important; }
.topnav__nav a:not(.topnav__home)::after {
    content: '';
    position: absolute;
    left: 0; bottom: -2px;
    width: 100%; height: 1.5px;
    background: var(--orange);
    transform: scaleX(0);
    transform-origin: right;
    transition: transform 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.topnav__nav a:not(.topnav__home):hover::after { transform: scaleX(1); transform-origin: left; }

.topnav__phone {
    display: inline-flex;
    align-items: center;
    gap: 0.65rem;
    color: var(--ink);
    text-decoration: none;
    font-size: 1rem;
    font-weight: 600;
    will-change: transform;
    font-variant-numeric: tabular-nums;
}
.topnav__phone-icon {
    width: 34px; height: 34px;
    background: var(--orange);
    color: #fff;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.topnav__phone-icon svg { width: 16px; height: 16px; }

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

/* ----- Photo cluster ----- */
.photos {
    position: relative;
    height: 620px;
    max-height: 70vh;
    max-width: 720px;
}
.photo {
    position: absolute;
    border-radius: 50%;
    overflow: hidden;
    cursor: pointer;
    box-shadow: 0 20px 50px rgba(46, 26, 71, 0.2);
    will-change: transform;
}
.photo img {
    width: 100%; height: 100%;
    object-fit: cover;
    object-position: center 30%;
    user-select: none;
    transition: transform 0.6s ease;
}
.photo:hover img { transform: scale(1.08); }

.photo--team { top: 4%;  left: 0;    width: 46%; aspect-ratio: 1; }
.photo--walk { top: 20%; left: 48%;  width: 34%; aspect-ratio: 1; }
.photo--desk { top: 62%; left: 24%;  width: 38%; aspect-ratio: 1; }

.photo-badge {
    position: absolute;
    background: var(--white);
    border-radius: 14px;
    padding: 0.75rem 1rem;
    box-shadow: 0 10px 24px rgba(46, 26, 71, 0.12);
    display: flex;
    align-items: center;
    gap: 0.6rem;
    z-index: 5;
    will-change: transform;
}
.photo-badge__num {
    font-family: 'Playfair Display', serif;
    font-size: 1.75rem;
    font-weight: 800;
    color: var(--orange);
    line-height: 1;
    font-variant-numeric: tabular-nums;
}
.photo-badge__num em { font-style: normal; font-size: 0.7em; color: var(--purple); }
.photo-badge__label {
    font-size: 0.65rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    color: var(--ink);
    text-transform: uppercase;
    line-height: 1.2;
}
.photo-badge--1 { top: 28%; left: 40%; }
.photo-badge--2 { top: 86%; left: 5%; }

/* ----- Title ----- */
.hero__title {
    font-family: 'Playfair Display', serif;
    font-size: clamp(3rem, 7vw, 6.5rem);
    line-height: 1.02;
    letter-spacing: -0.01em;
    font-weight: 700;
    color: var(--ink);
}
.hero__line {
    display: block;
    overflow: hidden;
    padding-bottom: 0.04em;
}
.hero__line em {
    font-style: italic;
    color: var(--orange);
    font-weight: 700;
    display: inline-block;
    will-change: transform;
}
.hero__line b {
    color: var(--purple);
    font-weight: 800;
    display: inline-block;
    will-change: transform;
}
.hero__char {
    display: inline-block;
    will-change: transform;
    transition: color 0.3s ease, transform 0.3s;
}
.hero__char.is-space { width: 0.2em; }

/* Desc + CTA row */
.hero__desc {
    margin-top: 1.5rem;
    font-size: 0.95rem;
    line-height: 1.55;
    color: var(--muted);
    font-weight: 400;
}

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

.cta {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    background: var(--orange);
    color: #fff;
    text-decoration: none;
    padding: 0.95rem 1.75rem;
    border-radius: 999px;
    font-weight: 600;
    font-size: 0.95rem;
    box-shadow: 0 12px 24px rgba(230, 111, 62, 0.35);
    overflow: hidden;
    will-change: transform;
}
.cta__label, .cta__arrow { position: relative; z-index: 2; }
.cta__arrow svg { width: 14px; height: 14px; transition: transform 0.3s; }
.cta::before {
    content: '';
    position: absolute;
    inset: 0;
    background: var(--purple);
    transform: scaleX(0);
    transform-origin: right;
    transition: transform 0.5s cubic-bezier(0.65, 0, 0.35, 1);
    z-index: 1;
}
.cta:hover::before { transform: scaleX(1); transform-origin: left; }
.cta:hover .cta__arrow svg { transform: translateX(4px); }

.hero__secondary {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    color: var(--ink);
    text-decoration: none;
    font-weight: 600;
    font-size: 0.95rem;
    will-change: transform;
    position: relative;
}
.hero__secondary svg { width: 28px; height: 28px; color: var(--purple); transition: transform 0.4s; }
.hero__secondary:hover svg { transform: rotate(360deg); }
.hero__secondary span { position: relative; }
.hero__secondary span::after {
    content: '';
    position: absolute;
    left: 0; bottom: -3px;
    width: 100%; height: 1.5px;
    background: var(--ink);
    transform: scaleX(0);
    transform-origin: right;
    transition: transform 0.4s;
}
.hero__secondary:hover span::after { transform: scaleX(1); transform-origin: left; }

/* ---------- Initial hidden states ---------- */
[data-magnetic], [data-link], [data-fade], [data-title-line] em,
[data-title-line] b, [data-photo], [data-badge] { opacity: 0; }

/* ---------- Responsive ---------- */
@media (max-width: 1100px) {
    .hero { grid-template-columns: 1fr; padding: 2rem 2rem 3rem; }
    .photos { height: 420px; margin: 0 auto; }
    .topnav { padding: 1rem 1.5rem; gap: 1rem; }
    .topnav__nav { display: none; }
}
@media (max-width: 760px) {
    .topnav__logo-mark { width: 44px; height: 44px; }
    .topnav__logo-text big { font-size: 1.1rem; }
    .topnav__logo-text small { font-size: 0.55rem; }
    .topnav__phone { gap: 0.4rem; font-size: 0.85rem; }
    .topnav__phone-icon { width: 28px; height: 28px; }
    .hero { padding: 1.5rem 1.25rem; }
    .photos { height: 340px; }
    .photo-badge { padding: 0.5rem 0.75rem; }
    .photo-badge__num { font-size: 1.3rem; }
    .photo-badge__label { font-size: 0.55rem; }
    .hero__title { font-size: clamp(2.4rem, 11vw, 4rem); }
}
@media (max-width: 480px) {
    .topnav__logo-text small { display: none; }
    .topnav__phone span:last-child { display: none; }
    .photos { height: 300px; }
    .photo-badge--1 { display: none; }
    .hero__cta-row { gap: 1rem; }
    .cta { padding: 0.75rem 1.25rem; font-size: 0.85rem; }
}

  </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 — Counsel</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&family=Playfair+Display:ital,wght@0,500;0,700;0,800;1,700&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="banner-counsel.css">
</head>
<body>
    

    <!-- ========== LOADER — Scales of justice ========== -->
    <div class="loader" id="loader">
        <div class="loader__scales">
            <svg class="loader__svg" viewBox="0 0 240 280" aria-hidden="true">
                <!-- Base -->
                <rect x="100" y="245" width="40" height="8" rx="2" fill="#2e1a47"/>
                <rect x="90" y="252" width="60" height="4" rx="2" fill="#2e1a47"/>
                <!-- Pole -->
                <rect x="118" y="55" width="4" height="195" fill="#2e1a47"/>
                <!-- Top ornament -->
                <circle cx="120" cy="48" r="8" fill="#e66f3e"/>
                <path d="M120 34 L123 43 L132 43 L125 49 L128 58 L120 52 L112 58 L115 49 L108 43 L117 43 Z" fill="#e66f3e" opacity="0.4"/>
                <!-- Beam (animated) -->
                <g id="scales-beam">
                    <rect x="30" y="58" width="180" height="3" rx="1.5" fill="#2e1a47"/>
                    <!-- Left chain -->
                    <line x1="50" y1="60" x2="50" y2="130" stroke="#2e1a47" stroke-width="1.5" stroke-dasharray="2 2"/>
                    <line x1="65" y1="60" x2="55" y2="130" stroke="#2e1a47" stroke-width="1.5" stroke-dasharray="2 2"/>
                    <!-- Left pan -->
                    <ellipse cx="52" cy="132" rx="32" ry="5" fill="#e66f3e"/>
                    <path d="M22 132 Q 52 162 82 132" fill="#d86332"/>
                    <!-- Right chain -->
                    <line x1="190" y1="60" x2="190" y2="130" stroke="#2e1a47" stroke-width="1.5" stroke-dasharray="2 2"/>
                    <line x1="175" y1="60" x2="185" y2="130" stroke="#2e1a47" stroke-width="1.5" stroke-dasharray="2 2"/>
                    <!-- Right pan -->
                    <ellipse cx="188" cy="132" rx="32" ry="5" fill="#e66f3e"/>
                    <path d="M158 132 Q 188 162 218 132" fill="#d86332"/>
                </g>
            </svg>
        </div>
        <div class="loader__meta">
            <span class="loader__label" id="loader-label">BUILDING TRUST</span>
            <span class="loader__counter" id="loader-counter">0%</span>
        </div>
    </div>

    <section class="counsel" id="counsel">

        <!-- Background wavy lines -->
        <svg class="waves" viewBox="0 0 1400 900" preserveAspectRatio="none" aria-hidden="true">
            <path id="wave1" d="M900 550 Q 1000 450 1100 520 T 1300 500 T 1500 480" stroke="#d5c7e5" stroke-width="1.2" fill="none"/>
            <path id="wave2" d="M870 600 Q 990 500 1120 570 T 1320 550 T 1500 530" stroke="#d5c7e5" stroke-width="1" fill="none"/>
            <path id="wave3" d="M840 650 Q 970 560 1140 620 T 1340 600 T 1500 590" stroke="#d5c7e5" stroke-width="1.3" fill="none"/>
            <path id="wave4" d="M810 700 Q 950 620 1160 680 T 1360 660 T 1500 650" stroke="#d5c7e5" stroke-width="0.9" fill="none"/>
            <path id="wave5" d="M780 750 Q 930 680 1180 740 T 1380 720 T 1500 720" stroke="#d5c7e5" stroke-width="1.1" fill="none"/>
            <path id="wave6" d="M750 800 Q 910 740 1200 800 T 1400 790 T 1500 790" stroke="#d5c7e5" stroke-width="1" fill="none"/>
        </svg>

        <!-- ========== TOP NAV ========== -->
        <header class="topnav">
            <a href="#" class="topnav__logo" data-magnetic>
                <span class="topnav__logo-mark" id="logo-mark">
                    <svg viewBox="0 0 70 70" aria-hidden="true">
                        <circle cx="35" cy="35" r="33" fill="none" stroke="#2e1a47" stroke-width="2"/>
                        <circle cx="48" cy="22" r="7" fill="#e66f3e"/>
                        <path d="M8 48 L18 34 L26 42 L36 26 L48 40 L58 30 L62 48 Z" fill="#2e1a47"/>
                    </svg>
                </span>
                <span class="topnav__logo-text">
                    <small>THE LAW OFFICE OF</small>
                    <big>Counsel &amp; Partners</big>
                </span>
            </a>

            <nav class="topnav__nav">
                <a href="#" class="topnav__home" data-magnetic data-link aria-label="Home">
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
                        <path d="M3 12l9-9 9 9M5 10v10h4v-6h6v6h4V10" stroke-linecap="round" stroke-linejoin="round"/>
                    </svg>
                </a>
                <a href="#" data-magnetic data-link>About</a>
                <a href="#" data-magnetic data-link>Services</a>
                <a href="#" data-magnetic data-link>Resources</a>
                <a href="#" data-magnetic data-link>Testimonials</a>
                <a href="#" data-magnetic data-link>Contact</a>
            </nav>

            <a href="tel:+17204456880" class="topnav__phone" data-magnetic data-phone>
                <span class="topnav__phone-icon">
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                        <path d="M22 16.9v3a2 2 0 01-2.2 2 19.8 19.8 0 01-8.6-3.1 19.5 19.5 0 01-6-6A19.8 19.8 0 012.1 4.2 2 2 0 014.1 2h3a2 2 0 012 1.7c.1 1 .3 1.9.6 2.8a2 2 0 01-.5 2.1L8.1 10a16 16 0 006 6l1.3-1.3a2 2 0 012.1-.4 12.8 12.8 0 002.8.7 2 2 0 011.7 2z"
                              stroke-linecap="round" stroke-linejoin="round"/>
                    </svg>
                </span>
                <span id="phone-number">(720) 445-6880</span>
            </a>
        </header>

        <!-- ========== HERO ========== -->
        <div class="hero" id="hero">

            <!-- Photo cluster (left) -->
            <div class="photos">
                <div class="photo photo--team" data-photo>
                    <img src="https://images.unsplash.com/photo-1522071820081-009f0129c71c?auto=format&fit=crop&w=800&q=80"
                         alt="" draggable="false" onerror="this.parentElement.style.display='none'">
                </div>
                <div class="photo photo--walk" data-photo>
                    <img src="https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&w=500&q=80"
                         alt="" draggable="false" onerror="this.parentElement.style.display='none'">
                </div>
                <div class="photo photo--desk" data-photo>
                    <img src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=500&q=80"
                         alt="" draggable="false" onerror="this.parentElement.style.display='none'">
                </div>
                <!-- Floating accent badges -->
                <div class="photo-badge photo-badge--1" data-badge>
                    <span class="photo-badge__num">15<em>+</em></span>
                    <span class="photo-badge__label">Years<br>Practice</span>
                </div>
                <div class="photo-badge photo-badge--2" data-badge>
                    <span class="photo-badge__num">240<em>+</em></span>
                    <span class="photo-badge__label">Cases<br>Won</span>
                </div>
            </div>

            <!-- Title (right) -->
            <h1 class="hero__title" aria-label="Your Partner in Business">
                <span class="hero__line" data-title-line><em>Your</em></span>
                <span class="hero__line" data-title-line><em>Partner in</em></span>
                <span class="hero__line" data-title-line><b>Business</b></span>
            </h1>

            <p class="hero__desc" data-fade>
                Counsel &amp; Partners delivers clear, thoughtful legal guidance for<br>
                growing businesses and ambitious founders.
            </p>

            <div class="hero__cta-row" data-fade>
                <a href="#" class="cta" data-magnetic data-cta>
                    <span class="cta__label">Book a Consultation</span>
                    <span class="cta__arrow">
                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
                            <path d="M5 12h14M13 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/>
                        </svg>
                    </span>
                </a>
                <a href="#" class="hero__secondary" data-magnetic>
                    <span>Watch our story</span>
                    <svg viewBox="0 0 24 24" fill="currentColor">
                        <circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.2"/>
                        <path d="M10 8l6 4-6 4z"/>
                    </svg>
                </a>
            </div>
        </div>
    </section>

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

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

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

    const magnetics = root.querySelectorAll('[data-magnetic]');
    const titleEms = root.querySelectorAll('[data-title-line] em, [data-title-line] b');
    const fades = root.querySelectorAll('[data-fade]');
    const photos = root.querySelectorAll('[data-photo]');
    const badges = root.querySelectorAll('[data-badge]');
    const waves = root.querySelectorAll('.waves path');
    const logoMark = document.getElementById('logo-mark');

    // -------------------------------------------------------------------
    //  INITIAL STATES
    // -------------------------------------------------------------------
    gsap.set(magnetics, { y: -15, opacity: 0 });
    gsap.set(fades, { y: 20, opacity: 0 });
    gsap.set(titleEms, { yPercent: 110, opacity: 0 });
    gsap.set(photos, { scale: 0.6, opacity: 0 });
    gsap.set(badges, { scale: 0, opacity: 0, rotation: -15 });

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

    // Scales oscillate back and forth, gradually settling
    const swings = [
        { rot: 15,  duration: 0.9 },
        { rot: -12, duration: 0.8 },
        { rot: 9,   duration: 0.7 },
        { rot: -6,  duration: 0.6 },
        { rot: 3,   duration: 0.5 },
        { rot: 0,   duration: 0.4 },
    ];
    swings.forEach((s) => {
        loaderTl.to(scalesBeam, {
            rotation: s.rot,
            duration: s.duration,
            ease: 'sine.inOut',
        });
    });

    // Counter + labels (parallel with swings)
    const p = { v: 0 };
    loaderTl.to(p, {
        v: 100,
        duration: 3.5,
        ease: 'power1.inOut',
        onUpdate: () => {
            loaderCounter.textContent = Math.floor(p.v) + '%';
            if (p.v > 30 && loaderLabel.textContent === 'BUILDING TRUST') loaderLabel.textContent = 'REVIEWING BRIEFS';
            if (p.v > 60 && loaderLabel.textContent === 'REVIEWING BRIEFS') loaderLabel.textContent = 'FINDING BALANCE';
            if (p.v > 95 && loaderLabel.textContent === 'FINDING BALANCE') loaderLabel.textContent = 'READY TO COUNSEL';
        },
    }, 0);

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

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

        // Background waves draw in
        tl.to(waves, {
            strokeDashoffset: 0,
            duration: 2.5,
            stagger: 0.12,
            ease: 'power2.inOut',
        }, 0);

        // Top nav
        tl.to(magnetics, {
            y: 0, opacity: 1,
            duration: 0.6,
            stagger: 0.06,
        }, 0.2);

        // Photos scale in (stagger)
        tl.to(photos, {
            scale: 1, opacity: 1,
            duration: 1,
            stagger: 0.15,
            ease: 'back.out(1.6)',
        }, 0.5);

        // Badges pop after photos
        tl.to(badges, {
            scale: 1, opacity: 1, rotation: 0,
            duration: 0.8,
            stagger: 0.12,
            ease: 'back.out(2)',
        }, 1);

        // Title emphases rise
        tl.to(titleEms, {
            yPercent: 0, opacity: 1,
            duration: 1.1,
            stagger: 0.12,
            ease: 'expo.out',
        }, 0.9);

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

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

    // -------------------------------------------------------------------
    //  CONTINUOUS
    // -------------------------------------------------------------------
    function startContinuous() {
        // Badges gentle float
        badges.forEach((badge, i) => {
            gsap.to(badge, {
                y: '-=8',
                rotation: i % 2 === 0 ? 2 : -2,
                duration: 2 + i * 0.4,
                yoyo: true,
                repeat: -1,
                ease: 'sine.inOut',
            });
        });

        // Phone icon subtle pulse
        const phoneIcon = root.querySelector('.topnav__phone-icon');
        gsap.to(phoneIcon, {
            scale: 1.08,
            duration: 1.2,
            yoyo: true,
            repeat: -1,
            ease: 'sine.inOut',
        });
    }

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

        // ---- Magnetic ----
        magnetics.forEach((el) => {
            const strength = el.classList.contains('cta') ? 0.35
                : el.classList.contains('topnav__home') ? 0.4
                : 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: icon rotates on hover ----
        const logoEl = root.querySelector('.topnav__logo');
        logoEl.addEventListener('mouseenter', () => {
            gsap.to(logoMark.querySelector('svg'), { rotation: 360, duration: 0.8, ease: 'power3.inOut' });
        });
        logoEl.addEventListener('mouseleave', () => {
            gsap.set(logoMark.querySelector('svg'), { rotation: 0 });
        });

        // ---- Title emphasis hover: lift + color shift ----
        titleEms.forEach((em) => {
            em.addEventListener('mouseenter', () => {
                gsap.to(em, {
                    y: -8,
                    duration: 0.35,
                    ease: 'back.out(2)',
                });
            });
            em.addEventListener('mouseleave', () => {
                gsap.to(em, {
                    y: 0,
                    duration: 0.5,
                    ease: 'elastic.out(1, 0.4)',
                });
            });
        });

        // ---- Photos: parallax + hover tilt ----
        photos.forEach((photo) => {
            photo.addEventListener('mouseenter', () => {
                gsap.to(photo, {
                    scale: 1.06,
                    duration: 0.4,
                    ease: 'back.out(2)',
                    zIndex: 10,
                });
            });
            photo.addEventListener('mouseleave', () => {
                gsap.to(photo, {
                    scale: 1,
                    duration: 0.5,
                    ease: 'elastic.out(1, 0.4)',
                    zIndex: 2,
                });
            });
        });

        // ---- Badges: click wiggles ----
        badges.forEach((badge) => {
            badge.addEventListener('mouseenter', () => {
                gsap.fromTo(badge,
                    { rotation: -5 },
                    { rotation: 5, duration: 0.15, yoyo: true, repeat: 3, ease: 'sine.inOut',
                      onComplete: () => gsap.to(badge, { rotation: 0, duration: 0.3 })
                    }
                );
            });
        });

        // ---- Phone: number scrambles on hover ----
        const phoneEl = root.querySelector('[data-phone]');
        const phoneNumEl = document.getElementById('phone-number');
        const phoneFinal = phoneNumEl.textContent;
        phoneEl.addEventListener('mouseenter', () => {
            let t = 0;
            const scramble = () => {
                t++;
                if (t >= 7) {
                    phoneNumEl.textContent = phoneFinal;
                    return;
                }
                phoneNumEl.textContent = phoneFinal.split('').map((ch) => {
                    if (/\d/.test(ch)) return String(Math.floor(Math.random() * 10));
                    return ch;
                }).join('');
                setTimeout(scramble, 40);
            };
            scramble();
            // Also wobble phone icon
            const icon = phoneEl.querySelector('.topnav__phone-icon svg');
            gsap.fromTo(icon,
                { rotation: 0 },
                { rotation: 15, duration: 0.08, yoyo: true, repeat: 4, ease: 'sine.inOut',
                  transformOrigin: 'center' }
            );
        });

        // ---- Photo parallax on mouse ----
        const photosScene = document.querySelector('.photos');
        let mx = 0, my = 0, cmx = 0, cmy = 0;
        photosScene.addEventListener('mousemove', (e) => {
            const r = photosScene.getBoundingClientRect();
            mx = ((e.clientX - r.left) / r.width - 0.5) * 2;
            my = ((e.clientY - r.top) / r.height - 0.5) * 2;
        });
        photosScene.addEventListener('mouseleave', () => { mx = 0; my = 0; });

        gsap.ticker.add(() => {
            cmx += (mx - cmx) * 0.05;
            cmy += (my - cmy) * 0.05;
            photos.forEach((photo, i) => {
                if (photo.matches(':hover')) return;
                const depth = 0.4 + i * 0.15;
                gsap.set(photo, {
                    x: cmx * 20 * depth,
                    y: cmy * 14 * depth,
                });
            });
            badges.forEach((badge, i) => {
                gsap.set(badge, {
                    x: cmx * (15 + i * 5),
                    y: cmy * (10 + i * 3),
                });
            });
        });

        // ---- Waves drift subtly with overall cursor ----
        const wavesSvg = root.querySelector('.waves');
        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; });
        gsap.ticker.add(() => {
            gcx += (gx - gcx) * 0.04;
            gcy += (gy - gcy) * 0.04;
            gsap.set(wavesSvg, { x: gcx * 25, y: gcy * 18 });
        });
    }
})();

More GSAP landing pages