# Capsule Hero

> An editorial full-viewport hero with a stadium-shaped video capsule, character intro, and full-bleed expand — premium brand landings.

**Definition:** Capsule Hero is a components UI animation from collections.html. It is built with HTML, CSS, JavaScript, and GSAP, ships with a live preview, and is free to copy into personal and commercial projects.

## Facts
- Category: Components
- Stack: HTML, CSS, JavaScript, and GSAP
- Added: 2026-08-03
- Canonical page: https://html.bosconvertpro.com/collection/capsule-hero
- Full-page preview: https://html.bosconvertpro.com/collection/capsule-hero/full
- Live demo: https://html.bosconvertpro.com/demos/capsule-hero/index.html
- Markdown mirror: https://html.bosconvertpro.com/collection/capsule-hero/index.md
- License: https://html.bosconvertpro.com/terms

## How to use
1. Open https://html.bosconvertpro.com/collection/capsule-hero/full to preview the animation in full screen.
2. Copy index.html, styles.css, and script.js from the code tabs on https://html.bosconvertpro.com/collection/capsule-hero.
3. Paste the files into your project and adjust timing, easing, and content as needed.

## FAQ
### What is Capsule Hero?

Capsule Hero is a components UI animation from collections.html. An editorial full-viewport hero with a stadium-shaped video capsule, character intro, and full-bleed expand — premium brand landings.

### What is Capsule Hero built with?

Capsule Hero is built with HTML, CSS, JavaScript, and GSAP. The source for every file is shown on the animation page and can be copied straight into your project.

### How do I use Capsule Hero in my project?

Open the animation page (https://html.bosconvertpro.com/collection/capsule-hero), preview it live, then copy index.html, styles.css, and script.js from the code tabs. Paste the files into your project and adjust timing, easing, and content as needed. A standalone demo runs at https://html.bosconvertpro.com/demos/capsule-hero/index.html.

### Is Capsule Hero free to use?

Yes. The Capsule Hero demo code may be used in personal and commercial projects. You may not resell or redistribute the collection itself as a competing template pack. Full terms: https://html.bosconvertpro.com/terms.

## Source — index.html
````html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Capsule Hero — Aether Trail</title>
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link rel="preconnect" href="https://cdn.jsdelivr.net" />
    <link rel="preconnect" href="https://videos.pexels.com" />
    <link rel="preconnect" href="https://images.pexels.com" />
    <link
      href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap"
      rel="stylesheet"
    />
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <!--
      Capsule Hero
      Editorial full-viewport hero: large edge glyphs frame a stadium-shaped
      video capsule. Intro reveals characters, then opens the capsule.
      Click capsule / Play pill to expand video full-bleed; click again to close.
      Video: free Pexels stock (Kelly) — man running in the woods.
    -->
    <section
      class="capsule-hero"
      data-capsule-hero
      data-capsule-width="58"
      data-capsule-height="36"
      data-duration="0"
      data-hold="0"
      data-ease="capsuleInOut"
      data-autoplay="false"
      aria-label="Aether Trail Collection hero"
    >
      <div class="capsule-hero__glyph-slot capsule-hero__glyph-slot--left" aria-hidden="true">
        <div class="capsule-hero__glyph" data-capsule-hero-glyph-left>AE</div>
      </div>

      <div class="capsule-hero__glyph-slot capsule-hero__glyph-slot--right" aria-hidden="true">
        <div class="capsule-hero__glyph" data-capsule-hero-glyph-right>25</div>
      </div>

      <div class="capsule-hero__capsule" data-capsule-hero-capsule>
        <div class="capsule-hero__media" data-capsule-hero-media>
          <video
            class="capsule-hero__video"
            data-capsule-hero-video
            muted
            loop
            playsinline
            preload="metadata"
            src="https://videos.pexels.com/video-files/5677397/5677397-hd_1920_1080_24fps.mp4"
            poster="https://images.pexels.com/videos/5677397/pexels-photo-5677397.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1920"
            aria-hidden="true"
          ></video>
          <img
            class="capsule-hero__poster"
            data-capsule-hero-poster
            src="https://images.pexels.com/videos/5677397/pexels-photo-5677397.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1920"
            alt=""
            aria-hidden="true"
            draggable="false"
          />
        </div>
      </div>

      <button
        type="button"
        class="capsule-hero__toggle"
        data-capsule-hero-toggle
        aria-expanded="false"
        aria-label="Play the video"
      >
        <span class="capsule-hero__toggle-label" data-capsule-hero-toggle-label>
          <span class="capsule-hero__toggle-text" data-capsule-hero-toggle-text>Play</span>
        </span>
      </button>

      <div class="capsule-hero__meta">
        <div class="capsule-hero__meta-group" data-capsule-hero-meta-item>
          <div class="capsule-hero__meta-label">Collection</div>
          <div class="capsule-hero__meta-value">Alpine Run</div>
        </div>
        <div class="capsule-hero__meta-group" data-capsule-hero-meta-item>
          <div class="capsule-hero__meta-label">Season</div>
          <div class="capsule-hero__meta-value">Autumn 25</div>
        </div>
        <div class="capsule-hero__meta-group" data-capsule-hero-meta-item>
          <div class="capsule-hero__meta-label">Drop</div>
          <div class="capsule-hero__meta-value">Sept 12</div>
        </div>
      </div>

      <p class="capsule-hero__copy" data-capsule-hero-meta-item>
        Built for the ridge before sunrise. Light shell, deep grip, quiet
        confidence — filmed on the forest line at first light.
      </p>
    </section>

    <script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrollTrigger.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/CustomEase.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/SplitText.min.js"></script>
    <script src="script.js"></script>
  </body>
</html>
````

## Source — styles.css
````css
/* ─────────────────────────────────────────────
   Capsule Hero — editorial full-viewport hero
   Stadium clip-path capsule + edge glyphs + cursor pill
───────────────────────────────────────────── */

*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

:root {
  --capsule-hero-bg: #e8e6e1;
  --capsule-hero-fg: #12110f;
  --capsule-hero-fg-soft: rgba(18, 17, 15, 0.78);
  --capsule-hero-fg-muted: rgba(18, 17, 15, 0.48);
  --capsule-hero-media-bg: #0c0b0a;
  --capsule-hero-margin: 44px;
  --capsule-hero-font: Inter, system-ui, -apple-system, BlinkMacSystemFont,
    "Segoe UI", sans-serif;

  /* Animated via GSAP → clip-path inset */
  --capsule-hero-clip-t: 28%;
  --capsule-hero-clip-r: 21%;
  --capsule-hero-clip-b: 28%;
  --capsule-hero-clip-l: 21%;
  --capsule-hero-clip-rad: 140px;
}

html,
body {
  width: 100%;
  min-height: 100%;
}

body {
  font-family: var(--capsule-hero-font);
  background: var(--capsule-hero-bg);
  color: var(--capsule-hero-fg);
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

img,
video {
  display: block;
  max-width: none;
  -webkit-user-drag: none;
  user-select: none;
}

/* ── Root ──────────────────────────────────── */

.capsule-hero {
  position: relative;
  width: 100%;
  min-height: 100svh;
  height: 100dvh;
  overflow: hidden;
  background: var(--capsule-hero-bg);
  color: var(--capsule-hero-fg);
  font-family: var(--capsule-hero-font);
  -webkit-font-smoothing: antialiased;
}

/* ── Edge glyphs ───────────────────────────── */

.capsule-hero__glyph-slot {
  position: absolute;
  top: 50%;
  z-index: 1;
  transform: translateY(-50%);
  pointer-events: none;
}

.capsule-hero__glyph-slot--left {
  left: 0;
}

.capsule-hero__glyph-slot--left .capsule-hero__glyph {
  margin-left: -0.04em;
}

.capsule-hero__glyph-slot--right {
  right: 0;
}

.capsule-hero__glyph {
  display: block;
  font-size: clamp(180px, 44vh, 24.5vw);
  font-weight: 300;
  letter-spacing: -0.06em;
  line-height: 1;
  color: var(--capsule-hero-fg);
  white-space: nowrap;
  will-change: transform;
  font-feature-settings: "ss01" on, "cv11" on;
}

/* SplitText / fallback char wrappers */
.capsule-hero__glyph-char,
.capsule-hero__glyph .char {
  display: inline-block;
  will-change: transform;
}

.capsule-hero__glyph-mask,
.capsule-hero__glyph .char-mask {
  display: inline-block;
  overflow: hidden;
  vertical-align: top;
}

/* ── Capsule (clip-path window) ────────────── */

.capsule-hero__capsule {
  position: absolute;
  inset: 0;
  z-index: 2;
  clip-path: inset(
    var(--capsule-hero-clip-t) var(--capsule-hero-clip-r)
      var(--capsule-hero-clip-b) var(--capsule-hero-clip-l) round
      var(--capsule-hero-clip-rad)
  );
  cursor: pointer;
  will-change: clip-path;
}

@media (hover: hover) and (pointer: fine) {
  .capsule-hero__capsule {
    cursor: none;
  }
}

.capsule-hero__media {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background: var(--capsule-hero-media-bg);
  transform-origin: 50% 50%;
  will-change: transform;
}

/* Subtle cinematic grade over the stock clip */
.capsule-hero__media::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    linear-gradient(
      180deg,
      rgba(12, 11, 10, 0.18) 0%,
      transparent 28%,
      transparent 62%,
      rgba(12, 11, 10, 0.28) 100%
    ),
    linear-gradient(
      90deg,
      rgba(12, 11, 10, 0.12) 0%,
      transparent 22%,
      transparent 78%,
      rgba(12, 11, 10, 0.12) 100%
    );
  mix-blend-mode: multiply;
  opacity: 0.85;
}

.capsule-hero__video,
.capsule-hero__poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 40%;
  filter: contrast(1.04) saturate(0.92) brightness(0.96);
}

/* ── Play / Close — premium white pill ─────── */

.capsule-hero__toggle {
  position: absolute;
  top: calc(
    var(--capsule-hero-clip-t) +
      (100% - var(--capsule-hero-clip-t) - var(--capsule-hero-clip-b)) * 0.5
  );
  left: calc(
    var(--capsule-hero-clip-l) +
      (100% - var(--capsule-hero-clip-l) - var(--capsule-hero-clip-r)) * 0.82
  );
  z-index: 3;
  width: 72px;
  height: 72px;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, 0.95);
  border-radius: 999px;
  display: flex;
  align-items: center;
  justify-content: center;
  background:
    linear-gradient(180deg, #ffffff 0%, #f4f3f0 100%);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 1) inset,
    0 -1px 0 rgba(0, 0, 0, 0.04) inset,
    0 10px 28px rgba(18, 17, 15, 0.18),
    0 3px 8px rgba(18, 17, 15, 0.1);
  cursor: pointer;
  pointer-events: none;
  color: var(--capsule-hero-fg);
  font-family: inherit;
  -webkit-tap-highlight-color: transparent;
  will-change: transform;
  transition: box-shadow 0.35s cubic-bezier(0.16, 1, 0.3, 1);
}

.capsule-hero__toggle:focus-visible {
  outline: 2px solid rgba(18, 17, 15, 0.85);
  outline-offset: 3px;
}

@media (hover: hover) and (pointer: fine) {
  .capsule-hero__toggle {
    top: 0;
    left: 0;
  }
}

.capsule-hero__toggle-label {
  position: relative;
  display: block;
  width: 100%;
  height: 1.15em;
  line-height: 1.15;
  overflow: hidden;
  /* Soft vertical mask so text rolls in/out cleanly */
  -webkit-mask-image: linear-gradient(
    180deg,
    transparent 0%,
    #000 18%,
    #000 82%,
    transparent 100%
  );
  mask-image: linear-gradient(
    180deg,
    transparent 0%,
    #000 18%,
    #000 82%,
    transparent 100%
  );
}

.capsule-hero__toggle-text {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  text-align: center;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: #12110f;
  white-space: nowrap;
  will-change: transform, opacity, filter;
  backface-visibility: hidden;
}

/* ── Bottom meta + copy ────────────────────── */

.capsule-hero__copy {
  position: absolute;
  right: var(--capsule-hero-margin);
  bottom: var(--capsule-hero-margin);
  z-index: 3;
  max-width: 320px;
  font-size: 14px;
  font-weight: 400;
  line-height: 1.6;
  letter-spacing: -0.012em;
  color: var(--capsule-hero-fg-soft);
  pointer-events: none;
  text-wrap: pretty;
}

.capsule-hero__meta {
  position: absolute;
  left: var(--capsule-hero-margin);
  bottom: var(--capsule-hero-margin);
  z-index: 3;
  display: flex;
  gap: 36px;
  pointer-events: none;
}

.capsule-hero__meta-group {
  position: relative;
  min-width: 0;
}

.capsule-hero__meta-label {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--capsule-hero-fg-muted);
  margin-bottom: 6px;
}

.capsule-hero__meta-value {
  font-size: 13px;
  font-weight: 500;
  letter-spacing: -0.015em;
  color: var(--capsule-hero-fg);
}

/* ── Responsive ────────────────────────────── */

@media (max-width: 991px) {
  :root {
    --capsule-hero-margin: 32px;
  }

  .capsule-hero__toggle {
    width: 68px;
    height: 68px;
  }

  .capsule-hero__meta {
    gap: 28px;
  }

  .capsule-hero__meta-group:last-child {
    display: none;
  }
}

@media (max-width: 767px) {
  :root {
    --capsule-hero-margin: 20px;
  }

  .capsule-hero__glyph-slot {
    transform: none;
  }

  .capsule-hero__glyph-slot--left {
    top: 8%;
  }

  .capsule-hero__glyph-slot--right {
    top: auto;
    bottom: 8%;
  }

  .capsule-hero__glyph {
    font-size: clamp(104px, 22vh, 42vw);
  }

  .capsule-hero__copy {
    display: none;
  }

  .capsule-hero__toggle {
    width: 64px;
    height: 64px;
  }

  .capsule-hero__toggle-text {
    font-size: 10px;
    letter-spacing: 0.08em;
  }

  .capsule-hero__meta {
    gap: 22px;
  }

  .capsule-hero__meta-label {
    font-size: 10px;
    letter-spacing: 0.08em;
  }

  .capsule-hero__meta-value {
    font-size: 12px;
  }
}

/* ── Reduced motion ────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  .capsule-hero__capsule,
  .capsule-hero__media,
  .capsule-hero__glyph,
  .capsule-hero__toggle-text {
    will-change: auto;
  }
}
````

## Source — script.js
````javascript
/**
 * Capsule Hero
 * Full-viewport editorial hero: edge glyphs + stadium clip-path capsule.
 * Intro character reveal → capsule open; click expands video full-bleed.
 * Requires GSAP + ScrollTrigger + CustomEase (+ optional SplitText).
 */
(function () {
  "use strict";

  /* --------------------------------------------------------------------------
   * Constants
   * -------------------------------------------------------------------------- */

  var EASE_DEFAULT = "capsuleInOut";
  var EASE_SOFT = "power2.out";
  var EASE_LABEL = "capsuleLabel";
  var EASE_LABEL_OUT = "capsuleLabelOut";
  var EASE_CURSOR = "expo.out";

  var GLYPH_GAP = 24;
  var INTRO_OPEN_DURATION = 1.15;
  var INTRO_META_OFFSET = 0.82 * INTRO_OPEN_DURATION;
  var CHAR_DURATION = 0.85;
  var CHAR_STAGGER = 0.07;
  var DEFAULT_EXPAND_DURATION = 1.15;
  var LABEL_DURATION = 0.72;
  var LABEL_STAGGER = 0.04;
  var MEDIA_SCALE_START = 1.12;
  var RESIZE_DEBOUNCE_MS = 150;

  /* --------------------------------------------------------------------------
   * Boot
   * -------------------------------------------------------------------------- */

  function waitForGsap(callback, attempt) {
    attempt = attempt || 0;
    if (
      typeof gsap !== "undefined" &&
      typeof ScrollTrigger !== "undefined" &&
      typeof CustomEase !== "undefined"
    ) {
      callback();
      return;
    }
    if (attempt < 150) {
      setTimeout(function () {
        waitForGsap(callback, attempt + 1);
      }, 50);
      return;
    }
    console.error("[Capsule Hero] GSAP plugins failed to load from CDN.");
  }

  function registerPlugins() {
    var plugins = [ScrollTrigger, CustomEase];
    if (typeof SplitText !== "undefined") plugins.push(SplitText);
    gsap.registerPlugin.apply(gsap, plugins);

    CustomEase.create("capsuleSoft", "M0,0 C0.3,0.9 0.1,1 1,1");
    CustomEase.create(EASE_DEFAULT, "M0,0 C0.7,0 0.16,1 1,1");
    /* Soft premium label in/out — no hard expo snap */
    CustomEase.create(EASE_LABEL, "M0,0 C0.16,1 0.3,1 1,1");
    CustomEase.create(EASE_LABEL_OUT, "M0,0 C0.4,0 0.2,1 1,1");
  }

  function init() {
    registerPlugins();

    var roots = document.querySelectorAll("[data-capsule-hero]");
    if (!roots.length) return;

    var instances = [];

    roots.forEach(function (root) {
      var instance = createCapsuleHero(root);
      if (instance) instances.push(instance);
    });

    window.CapsuleHeroAPI = {
      instances: instances,
      refresh: function () {
        instances.forEach(function (inst) {
          inst.refresh();
        });
      },
      expand: function () {
        instances.forEach(function (inst) {
          inst.expand();
        });
      },
      collapse: function () {
        instances.forEach(function (inst) {
          inst.collapse();
        });
      },
      toggle: function () {
        instances.forEach(function (inst) {
          inst.toggle();
        });
      },
    };
  }

  /* --------------------------------------------------------------------------
   * Helpers
   * -------------------------------------------------------------------------- */

  function readNumber(el, name, fallback) {
    var raw = el.getAttribute("data-" + name);
    if (raw === null || raw === "") return fallback;
    var num = parseFloat(raw);
    return isNaN(num) ? fallback : num;
  }

  function readString(el, name, fallback) {
    var raw = el.getAttribute("data-" + name);
    return raw === null || raw === "" ? fallback : raw;
  }

  function readBool(el, name, fallback) {
    var raw = el.getAttribute("data-" + name);
    if (raw === null) return fallback;
    return raw === "true";
  }

  function hasFinePointer() {
    return window.matchMedia("(hover: hover) and (pointer: fine)").matches;
  }

  function isInViewport(el) {
    var rect = el.getBoundingClientRect();
    return rect.top < window.innerHeight && rect.bottom > 0;
  }

  /**
   * Split element into masked characters.
   * Prefers GSAP SplitText; falls back to a lightweight DOM split.
   */
  function splitChars(el) {
    if (typeof SplitText !== "undefined" && SplitText.create) {
      try {
        var split = SplitText.create(el, {
          type: "chars",
          mask: "chars",
          charsClass: "capsule-hero__glyph-char",
        });
        var masks = split.masks || [];
        if (masks.length) {
          gsap.set(masks, { overflowX: "visible", overflowY: "clip" });
        }
        return {
          chars: split.chars || [],
          masks: masks,
          revert: function () {
            split.revert();
          },
        };
      } catch (err) {
        console.warn("[Capsule Hero] SplitText failed, using fallback.", err);
      }
    }

    return splitCharsFallback(el);
  }

  function splitCharsFallback(el) {
    var text = el.textContent || "";
    el.textContent = "";
    var chars = [];
    var masks = [];

    for (var i = 0; i < text.length; i++) {
      var mask = document.createElement("span");
      mask.className = "capsule-hero__glyph-mask";
      mask.setAttribute("aria-hidden", "true");

      var char = document.createElement("span");
      char.className = "capsule-hero__glyph-char";
      char.textContent = text[i];

      mask.appendChild(char);
      el.appendChild(mask);
      masks.push(mask);
      chars.push(char);
    }

    return {
      chars: chars,
      masks: masks,
      revert: function () {
        el.textContent = text;
      },
    };
  }

  /* --------------------------------------------------------------------------
   * Instance
   * -------------------------------------------------------------------------- */

  /**
   * @param {HTMLElement} root
   */
  function createCapsuleHero(root) {
    var leftGlyph = root.querySelector("[data-capsule-hero-glyph-left]");
    var rightGlyph = root.querySelector("[data-capsule-hero-glyph-right]");
    var capsule = root.querySelector("[data-capsule-hero-capsule]");
    var media = root.querySelector("[data-capsule-hero-media]");
    var video = root.querySelector("[data-capsule-hero-video]");
    var poster = root.querySelector("[data-capsule-hero-poster]");
    var toggle = root.querySelector("[data-capsule-hero-toggle]");
    var toggleLabel = root.querySelector("[data-capsule-hero-toggle-label]");
    var metaItems = Array.prototype.slice.call(
      root.querySelectorAll("[data-capsule-hero-meta-item]")
    );

    if (!leftGlyph || !rightGlyph || !capsule || !media || !toggle || !toggleLabel) {
      return null;
    }

    var config = {
      capsuleWidth: readNumber(root, "capsule-width", 58),
      capsuleHeight: readNumber(root, "capsule-height", 36),
      duration: readNumber(root, "duration", 0),
      hold: readNumber(root, "hold", 0),
      ease: readString(root, "ease", EASE_DEFAULT),
      autoplay: readBool(root, "autoplay", false),
    };

    var expandDuration =
      config.duration > 0 ? config.duration : DEFAULT_EXPAND_DURATION;
    var finePointer = hasFinePointer();
    var reducedMotion = window.matchMedia(
      "(prefers-reduced-motion: reduce)"
    ).matches;

    // Live clip-path state (tweened by GSAP)
    var clip = { t: 32, r: 50, b: 32, l: 50, rad: 140 };
    var targetClip = null;

    var introTl = null;
    var enterTrigger = null;
    var leftSplit = null;
    var rightSplit = null;
    var currentToggleText = toggleLabel.querySelector(
      "[data-capsule-hero-toggle-text]"
    );

    var booted = false;
    var glyphsVisible = false;
    var expanded = false;
    var hasVideo = false;
    var resizeTimer = null;

    var wrapWidth = 0;
    var leftGlyphWidth = 0;
    var rightGlyphWidth = 0;
    var isMobile = false;
    var leftPush = 0;
    var rightPush = 0;

    /* ---- Layout math ---- */

    function capsuleSizeForViewport() {
      var vw = window.innerWidth || document.documentElement.clientWidth;
      if (vw <= 767) return { width: 88, height: 42 };
      if (vw <= 991) return { width: 72, height: 34 };
      return {
        width: config.capsuleWidth,
        height: config.capsuleHeight,
      };
    }

    function computeTargetClip() {
      var size = capsuleSizeForViewport();
      var vw = window.innerWidth || document.documentElement.clientWidth;
      var vh = window.innerHeight || document.documentElement.clientHeight;

      wrapWidth = root.clientWidth || vw;
      leftGlyphWidth = leftGlyph.offsetWidth;
      rightGlyphWidth = rightGlyph.offsetWidth;
      isMobile = vw <= 767;

      var maxPush = wrapWidth * 0.18;
      leftPush = isMobile
        ? maxPush
        : Math.max(0, Math.min(maxPush, wrapWidth * 0.5 - leftGlyphWidth - GLYPH_GAP));
      rightPush = isMobile
        ? maxPush
        : Math.max(
            0,
            Math.min(maxPush, wrapWidth * 0.5 - rightGlyphWidth - GLYPH_GAP)
          );

      var leftInset = (100 - size.width) / 2;
      var rightInset = leftInset;

      if (!isMobile) {
        // Sit the capsule between the edge glyphs with a fixed gap
        leftInset = ((leftGlyphWidth + GLYPH_GAP) / wrapWidth) * 100;
        rightInset = ((rightGlyphWidth + GLYPH_GAP) / wrapWidth) * 100;
        var available = 100 - leftInset - rightInset;
        if (available > size.width) {
          var extra = (available - size.width) / 2;
          leftInset += extra;
          rightInset += extra;
        }
      }

      targetClip = {
        l: leftInset,
        r: rightInset,
        t: (100 - size.height) / 2,
        b: (100 - size.height) / 2,
        // Stadium radius ≈ half the capsule height in pixels
        rad: ((size.height / 100) * vh) / 2,
      };
    }

    function applyClip() {
      gsap.set(root, {
        "--capsule-hero-clip-t": clip.t + "%",
        "--capsule-hero-clip-r": clip.r + "%",
        "--capsule-hero-clip-b": clip.b + "%",
        "--capsule-hero-clip-l": clip.l + "%",
        "--capsule-hero-clip-rad": clip.rad + "px",
      });

      if (!isMobile) {
        // Desktop: glyphs track the live left/right inset so they slide
        // off-screen as the capsule expands to full bleed.
        var leftX = Math.min(
          leftPush,
          wrapWidth * (clip.l / 100) - GLYPH_GAP - leftGlyphWidth
        );
        var rightX = Math.min(
          rightPush,
          wrapWidth * (clip.r / 100) - GLYPH_GAP - rightGlyphWidth
        );
        gsap.set(leftGlyph, { x: leftX, force3D: true });
        gsap.set(rightGlyph, { x: -rightX, force3D: true });
      }
    }

    /**
     * @param {boolean} collapsedCenter  true = zero-width at center (intro start)
     */
    function setCollapsedState(collapsedCenter) {
      clip.t = targetClip.t;
      clip.b = targetClip.b;
      clip.rad = targetClip.rad;
      clip.l = collapsedCenter ? 50 : targetClip.l;
      clip.r = collapsedCenter ? 50 : targetClip.r;
      applyClip();

      if (isMobile) {
        gsap.set(leftGlyph, {
          x: collapsedCenter ? leftPush : 0,
          force3D: true,
        });
        gsap.set(rightGlyph, {
          x: collapsedCenter ? -rightPush : 0,
          force3D: true,
        });
      }
    }

    /* ---- Toggle label morph (smooth Play ↔ Close) ---- */

    var labelTl = null;

    function swapToggleText(label) {
      if (labelTl) labelTl.kill();

      var next = document.createElement("span");
      next.className = "capsule-hero__toggle-text";
      next.setAttribute("data-capsule-hero-toggle-text", "");
      next.textContent = label;
      toggleLabel.appendChild(next);

      var prev = currentToggleText;
      currentToggleText = next;

      gsap.set(next, {
        yPercent: 120,
        y: 0,
        autoAlpha: 0,
        filter: "blur(4px)",
        force3D: true,
      });

      labelTl = gsap.timeline({
        defaults: { force3D: true },
        onComplete: function () {
          labelTl = null;
        },
      });

      if (prev) {
        labelTl.to(
          prev,
          {
            yPercent: -120,
            autoAlpha: 0,
            filter: "blur(4px)",
            duration: LABEL_DURATION * 0.85,
            ease: EASE_LABEL_OUT,
            onComplete: function () {
              if (prev.parentNode) prev.parentNode.removeChild(prev);
            },
          },
          0
        );
      }

      labelTl.to(
        next,
        {
          yPercent: 0,
          autoAlpha: 1,
          filter: "blur(0px)",
          duration: LABEL_DURATION,
          ease: EASE_LABEL,
        },
        prev ? LABEL_STAGGER : 0
      );

      /* Soft press pulse on the white pill */
      if (!reducedMotion) {
        labelTl.fromTo(
          toggle,
          { scale: 1 },
          {
            scale: 0.94,
            duration: 0.18,
            ease: "power2.out",
            yoyo: true,
            repeat: 1,
            overwrite: "auto",
          },
          0
        );
      }
    }

    /* ---- Intro sequence ---- */

    function buildIntroTimeline() {
      if (introTl) introTl.kill();
      introTl = gsap.timeline({ paused: true });

      var chars = [];
      if (leftSplit && leftSplit.chars) {
        chars = chars.concat(leftSplit.chars);
      }
      if (rightSplit && rightSplit.chars) {
        chars = chars.concat(rightSplit.chars);
      }

      if (chars.length) {
        introTl.fromTo(
          chars,
          { yPercent: 110, y: 0 },
          {
            yPercent: 0,
            y: 0,
            duration: CHAR_DURATION,
            ease: config.ease,
            stagger: CHAR_STAGGER,
            force3D: true,
            immediateRender: true,
          },
          0
        );
      }

      var charEnd = chars.length
        ? CHAR_DURATION + CHAR_STAGGER * (chars.length - 1)
        : 0;
      var openAt = chars.length ? charEnd + config.hold : 0;

      // Open capsule from center to stadium shape
      introTl.to(
        clip,
        {
          l: targetClip.l,
          r: targetClip.r,
          duration: INTRO_OPEN_DURATION,
          ease: config.ease,
          onUpdate: applyClip,
        },
        openAt
      );

      if (isMobile) {
        introTl.to(
          [leftGlyph, rightGlyph],
          {
            x: 0,
            duration: INTRO_OPEN_DURATION,
            ease: config.ease,
            force3D: true,
          },
          openAt
        );
      }

      introTl.to(
        media,
        {
          scale: 1,
          duration: INTRO_OPEN_DURATION,
          ease: config.ease,
          force3D: true,
        },
        openAt
      );

      if (metaItems.length) {
        introTl.to(
          metaItems,
          {
            autoAlpha: 1,
            y: 0,
            duration: 0.55,
            ease: config.ease,
            stagger: 0.08,
            force3D: true,
          },
          openAt + INTRO_META_OFFSET
        );
      }

      // Touch devices: show the fixed pill with the meta
      if (!finePointer) {
        introTl.to(
          toggle,
          {
            autoAlpha: 1,
            y: 0,
            duration: 0.55,
            ease: config.ease,
            force3D: true,
          },
          openAt + INTRO_META_OFFSET
        );
      }
    }

    function runIntro() {
      function start() {
        leftSplit = splitChars(leftGlyph);
        rightSplit = splitChars(rightGlyph);

        gsap.set(leftSplit.chars.concat(rightSplit.chars), {
          yPercent: 110,
          y: 0,
          force3D: true,
        });

        computeTargetClip();
        setCollapsedState(true);
        glyphsVisible = true;
        gsap.set([leftGlyph, rightGlyph], { autoAlpha: 1 });

        buildIntroTimeline();
        introTl.play();
      }

      if (booted) return;
      booted = true;

      if (document.fonts && document.fonts.status !== "loaded") {
        document.fonts.ready.then(start);
      } else {
        start();
      }
    }

    /* ---- Expand / collapse ---- */

    function expand() {
      if (expanded) return;
      expanded = true;

      root.setAttribute("data-expanded", "true");
      toggle.setAttribute("aria-expanded", "true");
      toggle.setAttribute("aria-label", "Close the video");
      swapToggleText("Close");

      if (hasVideo) {
        video.play().catch(function () {});
      }

      gsap.to(clip, {
        t: 0,
        r: 0,
        b: 0,
        l: 0,
        rad: 0,
        duration: expandDuration,
        ease: config.ease,
        overwrite: "auto",
        onUpdate: applyClip,
      });

      if (isMobile) {
        gsap.killTweensOf([leftGlyph, rightGlyph]);
        gsap.to(leftGlyph, {
          x: -(leftGlyphWidth + GLYPH_GAP),
          duration: expandDuration * 0.8,
          ease: config.ease,
          force3D: true,
        });
        gsap.to(rightGlyph, {
          x: rightGlyphWidth + GLYPH_GAP,
          duration: expandDuration * 0.8,
          ease: config.ease,
          force3D: true,
        });
      }

      if (metaItems.length) {
        gsap.killTweensOf(metaItems);
        gsap.to(metaItems, {
          autoAlpha: 0,
          y: 10,
          duration: 0.3,
          ease: EASE_SOFT,
          force3D: true,
        });
      }
    }

    function collapse() {
      if (!expanded) return;
      expanded = false;

      root.setAttribute("data-expanded", "false");
      toggle.setAttribute("aria-expanded", "false");
      toggle.setAttribute("aria-label", "Play the video");
      swapToggleText("Play");

      if (hasVideo && !config.autoplay) {
        video.pause();
      }

      computeTargetClip();

      gsap.to(clip, {
        t: targetClip.t,
        r: targetClip.r,
        b: targetClip.b,
        l: targetClip.l,
        rad: targetClip.rad,
        duration: expandDuration,
        ease: config.ease,
        overwrite: "auto",
        onUpdate: applyClip,
      });

      if (isMobile) {
        gsap.killTweensOf([leftGlyph, rightGlyph]);
        gsap.to([leftGlyph, rightGlyph], {
          x: 0,
          duration: expandDuration * 0.8,
          delay: expandDuration * 0.2,
          ease: config.ease,
          force3D: true,
        });
      }

      if (metaItems.length) {
        gsap.killTweensOf(metaItems);
        gsap.to(metaItems, {
          autoAlpha: 1,
          y: 0,
          duration: 0.3,
          ease: EASE_SOFT,
          force3D: true,
          delay: expandDuration * 0.35,
        });
      }
    }

    function toggleExpand() {
      if (expanded) collapse();
      else expand();
    }

    /* ---- Custom cursor pill (desktop) ---- */

    var quickX = gsap.quickTo(toggle, "x", {
      duration: 0.5,
      ease: EASE_CURSOR,
    });
    var quickY = gsap.quickTo(toggle, "y", {
      duration: 0.5,
      ease: EASE_CURSOR,
    });

    function pointerLocal(event) {
      var rect = root.getBoundingClientRect();
      return {
        x: event.clientX - rect.left,
        y: event.clientY - rect.top,
      };
    }

    function showToggle() {
      gsap.to(toggle, {
        autoAlpha: 1,
        scale: 1,
        duration: 0.45,
        ease: "back.out(1.5)",
        overwrite: "auto",
      });
    }

    function hideToggle() {
      gsap.to(toggle, {
        autoAlpha: 0,
        scale: 0,
        duration: 0.3,
        ease: "power3.out",
        overwrite: "auto",
      });
    }

    function onPointerEnter(event) {
      if (!finePointer || !booted) return;
      var pos = pointerLocal(event);
      gsap.set(toggle, { x: pos.x, y: pos.y });
      quickX(pos.x);
      quickY(pos.y);
      showToggle();
    }

    function onPointerMove(event) {
      if (!finePointer || !booted) return;
      var pos = pointerLocal(event);
      quickX(pos.x);
      quickY(pos.y);
    }

    function onPointerLeave() {
      if (!finePointer) return;
      hideToggle();
    }

    function onToggleFocus() {
      if (!finePointer) return;
      var x =
        wrapWidth *
        ((clip.l + 0.82 * (100 - clip.l - clip.r)) / 100);
      var y =
        (root.clientHeight || window.innerHeight) *
        ((clip.t + 0.5 * (100 - clip.t - clip.b)) / 100);
      gsap.set(toggle, { x: x, y: y });
      quickX(x);
      quickY(y);
      showToggle();
    }

    function onToggleBlur() {
      if (!finePointer) return;
      hideToggle();
    }

    /* ---- Global handlers ---- */

    function onKeydown(event) {
      if (event.key === "Escape" && expanded) {
        collapse();
      }
    }

    function onResize() {
      clearTimeout(resizeTimer);
      resizeTimer = setTimeout(function () {
        if (expanded) return;
        if (introTl && introTl.isActive()) return;
        computeTargetClip();
        setCollapsedState(!glyphsVisible);
        if (enterTrigger) ScrollTrigger.refresh();
      }, RESIZE_DEBOUNCE_MS);
    }

    function onVisibility() {
      if (document.hidden) {
        gsap.globalTimeline.pause();
        if (hasVideo) video.pause();
      } else {
        gsap.globalTimeline.resume();
        if (hasVideo && (config.autoplay || expanded)) {
          video.play().catch(function () {});
        }
      }
    }

    /* ---- Wire events ---- */

    toggle.addEventListener("click", toggleExpand);
    capsule.addEventListener("click", toggleExpand);

    capsule.addEventListener("pointerenter", onPointerEnter);
    capsule.addEventListener("pointermove", onPointerMove);
    capsule.addEventListener("pointerleave", onPointerLeave);
    toggle.addEventListener("focus", onToggleFocus);
    toggle.addEventListener("blur", onToggleBlur);

    document.addEventListener("keydown", onKeydown);
    window.addEventListener("resize", onResize);
    document.addEventListener("visibilitychange", onVisibility);

    /* ---- Initial state ---- */

    hasVideo = !!(
      video &&
      video.getAttribute("src") &&
      video.getAttribute("src").trim()
    );

    if (video) {
      if (hasVideo) {
        if (poster) poster.style.display = "none";
        if (config.autoplay) {
          video.play().catch(function () {});
        }
      } else {
        video.style.display = "none";
      }
    }

    computeTargetClip();
    setCollapsedState(true);

    gsap.set(media, {
      scale: MEDIA_SCALE_START,
      transformOrigin: "50% 50%",
      force3D: true,
    });
    gsap.set([leftGlyph, rightGlyph], { autoAlpha: 0 });

    if (metaItems.length) {
      gsap.set(metaItems, { autoAlpha: 0, y: 16, force3D: true });
    }

    if (finePointer) {
      gsap.set(toggle, {
        xPercent: -50,
        yPercent: -50,
        autoAlpha: 0,
        scale: 0,
        force3D: true,
      });
    } else {
      gsap.set(toggle, {
        xPercent: -50,
        yPercent: -50,
        autoAlpha: 0,
        y: 16,
        force3D: true,
      });
    }

    toggle.setAttribute("aria-expanded", "false");
    toggle.setAttribute("aria-label", "Play the video");
    root.setAttribute("data-expanded", "false");

    if (reducedMotion) {
      gsap.globalTimeline.timeScale(20);
    }

    // Start intro when section is in view (or immediately if already visible)
    requestAnimationFrame(function () {
      if (isInViewport(root)) {
        runIntro();
      } else {
        enterTrigger = ScrollTrigger.create({
          trigger: root,
          start: "top 80%",
          once: true,
          onEnter: runIntro,
        });
      }
      ScrollTrigger.refresh();
    });

    /* ---- Public API ---- */

    return {
      refresh: function () {
        if (expanded) return;
        computeTargetClip();
        setCollapsedState(!glyphsVisible);
        if (enterTrigger) ScrollTrigger.refresh();
      },
      expand: expand,
      collapse: collapse,
      toggle: toggleExpand,
      kill: function () {
        window.removeEventListener("resize", onResize);
        document.removeEventListener("visibilitychange", onVisibility);
        document.removeEventListener("keydown", onKeydown);
        toggle.removeEventListener("click", toggleExpand);
        capsule.removeEventListener("click", toggleExpand);
        capsule.removeEventListener("pointerenter", onPointerEnter);
        capsule.removeEventListener("pointermove", onPointerMove);
        capsule.removeEventListener("pointerleave", onPointerLeave);
        toggle.removeEventListener("focus", onToggleFocus);
        toggle.removeEventListener("blur", onToggleBlur);

        if (introTl) introTl.kill();
        if (labelTl) labelTl.kill();
        if (enterTrigger) enterTrigger.kill();
        if (leftSplit && leftSplit.revert) leftSplit.revert();
        if (rightSplit && rightSplit.revert) rightSplit.revert();
        leftSplit = null;
        rightSplit = null;
      },
    };
  }

  /* --------------------------------------------------------------------------
   * Start
   * -------------------------------------------------------------------------- */

  waitForGsap(function () {
    if (document.readyState === "loading") {
      document.addEventListener("DOMContentLoaded", init);
    } else {
      init();
    }
  });
})();
````
