# Morphing Pill Menu

> A pill-shaped control that morphs into a menu surface — compact actions with a premium transform.

**Definition:** Morphing Pill Menu is a navigation 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: Navigation
- Stack: HTML, CSS, JavaScript, and GSAP
- Added: 2026-08-01
- Canonical page: https://html.bosconvertpro.com/collection/morphing-pill-menu
- Full-page preview: https://html.bosconvertpro.com/collection/morphing-pill-menu/full
- Live demo: https://html.bosconvertpro.com/demos/morphing-pill-menu/index.html
- Markdown mirror: https://html.bosconvertpro.com/collection/morphing-pill-menu/index.md
- License: https://html.bosconvertpro.com/terms

## How to use
1. Open https://html.bosconvertpro.com/collection/morphing-pill-menu/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/morphing-pill-menu.
3. Paste the files into your project and adjust timing, easing, and content as needed.

## FAQ
### What is Morphing Pill Menu?

Morphing Pill Menu is a navigation UI animation from collections.html. A pill-shaped control that morphs into a menu surface — compact actions with a premium transform.

### What is Morphing Pill Menu built with?

Morphing Pill Menu 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 Morphing Pill Menu in my project?

Open the animation page (https://html.bosconvertpro.com/collection/morphing-pill-menu), 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/morphing-pill-menu/index.html.

### Is Morphing Pill Menu free to use?

Yes. The Morphing Pill Menu 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>Morphing Pill Menu — Liquid Glass</title>
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link rel="preconnect" href="https://images.unsplash.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>
    <!--
      Morphing Pill Menu
      ------------------
      Compact liquid-glass pill morphs into a full navigation panel.
      Surface grows in place (width / height / radius); content fades mid-morph.

      Configure timing via data-mpm-duration / data-mpm-ease / data-mpm-stagger.
    -->
    <div
      class="mpm"
      data-mpm
      data-mpm-duration="1.0"
      data-mpm-ease="elastic.out(1,0.7)"
      data-mpm-stagger="0.05"
    >
      <!-- Backdrop: rich imagery so liquid glass refraction reads clearly -->
      <div class="mpm__bg" aria-hidden="true">
        <img
          class="mpm__bg-image"
          src="https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?auto=format&fit=crop&w=2400&q=90"
          alt=""
          draggable="false"
        />
        <!-- Subtle gradient wash so glass stays legible on bright patches -->
        <div class="mpm__bg-wash"></div>
      </div>

      <!-- Page dim while open — never intercepts clicks -->
      <div class="mpm__scrim" data-mpm-scrim aria-hidden="true"></div>

      <!--
        THE PILL IS THE CONTAINER
        Liquid glass layers: filter → refraction → specular → highlight → tint → edge
      -->
      <div class="mpm__surface" data-mpm-surface>
        <div class="mpm__glass" aria-hidden="true">
          <!-- Chromium: SVG displacement for edge lensing; others fall back to CSS blur -->
          <div class="mpm__glass-filter"></div>
          <div class="mpm__glass-refraction"></div>
          <div class="mpm__glass-specular"></div>
          <div class="mpm__glass-highlight"></div>
          <div class="mpm__glass-tint"></div>
          <div class="mpm__glass-edge"></div>
          <div class="mpm__glass-inner"></div>
        </div>

        <!--
          Toggle stays visible through the morph (top-center).
          "Menu" / "Close" share one roller; icon faces (dots / ×) roll with it.
        -->
        <button
          type="button"
          class="mpm__toggle"
          data-mpm-toggle
          aria-expanded="false"
          aria-controls="mpm-panel"
          aria-label="Open navigation menu"
        >
          <span class="mpm__toggle-text">
            <span class="mpm__toggle-text-roller" data-mpm-toggle-roller>
              <span class="mpm__toggle-line">Menu</span>
              <span class="mpm__toggle-line">Close</span>
            </span>
          </span>
          <span class="mpm__toggle-icon" aria-hidden="true">
            <span class="mpm__toggle-icon-roller" data-mpm-icon-roller>
              <!-- Closed: 2×2 app-grid icon -->
              <span class="mpm__toggle-icon-face">
                <svg
                  class="mpm__icon-grid"
                  viewBox="0 0 16 16"
                  width="15"
                  height="15"
                  fill="currentColor"
                  aria-hidden="true"
                >
                  <rect data-mpm-icon-cell x="1.5" y="1.5" width="5" height="5" rx="1.25" />
                  <rect data-mpm-icon-cell x="9.5" y="1.5" width="5" height="5" rx="1.25" />
                  <rect data-mpm-icon-cell x="1.5" y="9.5" width="5" height="5" rx="1.25" />
                  <rect data-mpm-icon-cell x="9.5" y="9.5" width="5" height="5" rx="1.25" />
                </svg>
              </span>
              <!-- Open: refined close mark -->
              <span class="mpm__toggle-icon-face">
                <svg
                  viewBox="0 0 16 16"
                  width="14"
                  height="14"
                  fill="none"
                  stroke="currentColor"
                  stroke-width="1.6"
                  stroke-linecap="round"
                  aria-hidden="true"
                >
                  <path d="M3.5 3.5 L12.5 12.5 M12.5 3.5 L3.5 12.5" />
                </svg>
              </span>
            </span>
          </span>
        </button>

        <div
          class="mpm__panel"
          id="mpm-panel"
          data-mpm-panel
          role="dialog"
          aria-modal="true"
          aria-label="Navigation"
          aria-hidden="true"
          inert
        >
          <div class="mpm__body">
            <!-- Featured project thumbnail -->
            <a
              class="mpm__media"
              data-mpm-media
              href="#atelier"
              aria-label="View Atelier project"
            >
              <img
                class="mpm__media-image"
                src="https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?auto=format&fit=crop&w=800&q=85"
                alt="Minimal interior — Atelier project"
                draggable="false"
              />
              <span class="mpm__media-shade" data-mpm-media-shade aria-hidden="true"></span>
              <span class="mpm__media-cue" data-mpm-media-cue aria-hidden="true">View</span>
            </a>

            <nav class="mpm__nav" aria-label="Primary">
              <ul class="mpm__list">
                <li class="mpm__item">
                  <a class="mpm__link" data-mpm-link href="#work">
                    <span class="mpm__link-text" data-mpm-link-text>Work</span>
                  </a>
                </li>
                <li class="mpm__item">
                  <a class="mpm__link" data-mpm-link href="#studio">
                    <span class="mpm__link-text" data-mpm-link-text>Studio</span>
                  </a>
                </li>
                <li class="mpm__item">
                  <a class="mpm__link" data-mpm-link href="#atelier">
                    <span class="mpm__link-text" data-mpm-link-text>Atelier</span>
                    <span class="mpm__link-badge" data-mpm-link-badge>New</span>
                  </a>
                </li>
                <li class="mpm__item">
                  <a class="mpm__link" data-mpm-link href="#contact">
                    <span class="mpm__link-text" data-mpm-link-text>Contact</span>
                  </a>
                </li>
              </ul>
            </nav>
          </div>
        </div>
      </div>
    </div>

    <!--
      Liquid glass SVG filter
      feTurbulence + feDisplacementMap bends the backdrop near curved edges
      (Chrome / Edge). Other browsers use the CSS blur stack only.
    -->
    <svg class="mpm-svg" width="0" height="0" aria-hidden="true" focusable="false">
      <defs>
        <filter
          id="liquid-glass-lens"
          x="-8%"
          y="-8%"
          width="116%"
          height="116%"
          color-interpolation-filters="sRGB"
        >
          <!-- Soft frosted base -->
          <feGaussianBlur in="SourceGraphic" stdDeviation="1.2" result="blurred" />
          <!-- Organic edge distortion map -->
          <feTurbulence
            type="fractalNoise"
            baseFrequency="0.012 0.03"
            numOctaves="2"
            seed="7"
            result="noise"
          />
          <feDisplacementMap
            in="blurred"
            in2="noise"
            scale="28"
            xChannelSelector="R"
            yChannelSelector="G"
            result="displaced"
          />
          <!-- Slight brightness lift on refracted glass -->
          <feComponentTransfer in="displaced" result="lit">
            <feFuncA type="identity" />
            <feFuncR type="linear" slope="1.06" intercept="0.02" />
            <feFuncG type="linear" slope="1.05" intercept="0.015" />
            <feFuncB type="linear" slope="1.04" intercept="0.01" />
          </feComponentTransfer>
        </filter>
      </defs>
    </svg>

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

## Source — styles.css
````css
/* =============================================================================
   Morphing Pill Menu — Professional Liquid Glass
   -----------------------------------------------------------------------------
   Liquid Glass vs classic glassmorphism:
   • Glassmorphism → one fill + blur
   • Liquid Glass  → refraction stack + specular rim + edge lens + depth shadow
     that thickens when the pill morphs open
   ============================================================================= */

/* ---------- Reset ---------- */

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

html,
body {
  height: 100%;
}

body {
  font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", sans-serif;
  background: #08090c;
  color: #0a0a0a;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

button,
a {
  font: inherit;
  color: inherit;
  text-decoration: none;
  border: 0;
  background: none;
  cursor: pointer;
}

ul {
  list-style: none;
}

img {
  display: block;
  max-width: 100%;
}

/* ---------- Stage ---------- */

.mpm {
  --mpm-fg: rgba(255, 252, 248, 0.94);
  --mpm-fg-soft: rgba(255, 255, 255, 0.78);
  --mpm-accent: #7eb8ff;
  --mpm-margin: 32px;
  --mpm-font: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;

  /* Liquid glass material tokens */
  --glass-blur: 22px;
  --glass-blur-open: 36px;
  --glass-sat: 1.55;
  --glass-bright: 1.08;
  --glass-tint: rgba(255, 255, 255, 0.1);
  --glass-tint-open: rgba(255, 255, 255, 0.14);
  --glass-cool: rgba(180, 210, 255, 0.06);

  position: relative;
  width: 100%;
  height: 100dvh;
  min-height: 100vh;
  overflow: hidden;
  background-color: #08090c;
  font-family: var(--mpm-font);
  -webkit-font-smoothing: antialiased;
}

/* ---------- Backdrop ---------- */

.mpm__bg {
  position: absolute;
  inset: 0;
  z-index: 1;
}

.mpm__bg-image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  transform: scale(1.03);
  filter: saturate(1.08) contrast(1.05);
}

/* Keeps the glass readable without killing the photo */
.mpm__bg-wash {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 70% 55% at 50% 30%, transparent 0%, rgba(8, 9, 12, 0.35) 100%),
    linear-gradient(180deg, rgba(8, 9, 12, 0.25) 0%, transparent 35%, transparent 65%, rgba(8, 9, 12, 0.45) 100%);
  pointer-events: none;
}

.mpm__scrim {
  position: absolute;
  inset: 0;
  z-index: 3;
  background:
    radial-gradient(ellipse 80% 60% at 50% 20%, rgba(6, 8, 14, 0.2), rgba(6, 8, 14, 0.5));
  opacity: 0;
  pointer-events: none;
}

/* ---------- Morphing surface ---------- */

.mpm__surface {
  position: absolute;
  top: 28px;
  left: 50%;
  z-index: 100;
  width: 110px;
  height: 46px;
  border-radius: 23px;
  transform: translateX(-50%);
  overflow: hidden;
  isolation: isolate;
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.28) inset,
    0 -1px 0 rgba(0, 0, 0, 0.12) inset,
    0 10px 32px rgba(0, 0, 0, 0.32),
    0 2px 8px rgba(0, 0, 0, 0.18);
  will-change: width, height, border-radius, box-shadow;
  transition: box-shadow 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}

/* Thicker material when open — deeper cast + richer inner rim */
.mpm[data-mpm-open] .mpm__surface {
  box-shadow:
    0 1.5px 0 rgba(255, 255, 255, 0.35) inset,
    0 0 0 0.5px rgba(255, 255, 255, 0.12) inset,
    0 -2px 12px rgba(0, 0, 0, 0.15) inset,
    0 32px 72px rgba(0, 0, 0, 0.48),
    0 14px 32px rgba(0, 0, 0, 0.3),
    0 0 40px rgba(120, 170, 255, 0.06);
}

/* ---------- Liquid glass material stack ----------
   1. filter      — SVG displacement lens (Chrome/Edge)
   2. refraction  — multi-pass blur / sat / contrast
   3. specular    — soft light catch (top + corners)
   4. highlight   — thin top rim streak
   5. tint        — cool milky body
   6. edge        — refractive perimeter stroke
   7. inner       — subtle bottom occlusion
*/

.mpm__glass {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}

/* Optional SVG lens — ignored gracefully where unsupported */
.mpm__glass-filter {
  position: absolute;
  inset: -2px;
  border-radius: inherit;
  backdrop-filter: url(#liquid-glass-lens);
  -webkit-backdrop-filter: url(#liquid-glass-lens);
  opacity: 0.85;
}

.mpm__glass-refraction {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background:
    linear-gradient(
      165deg,
      rgba(255, 255, 255, 0.08) 0%,
      rgba(255, 255, 255, 0.02) 45%,
      rgba(200, 220, 255, 0.04) 100%
    );
  backdrop-filter:
    blur(var(--glass-blur))
    saturate(var(--glass-sat))
    brightness(var(--glass-bright))
    contrast(1.05);
  -webkit-backdrop-filter:
    blur(var(--glass-blur))
    saturate(var(--glass-sat))
    brightness(var(--glass-bright))
    contrast(1.05);
  transition:
    backdrop-filter 0.5s cubic-bezier(0.22, 1, 0.36, 1),
    -webkit-backdrop-filter 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}

.mpm[data-mpm-open] .mpm__glass-refraction {
  backdrop-filter:
    blur(var(--glass-blur-open))
    saturate(var(--glass-sat))
    brightness(1.1)
    contrast(1.06);
  -webkit-backdrop-filter:
    blur(var(--glass-blur-open))
    saturate(var(--glass-sat))
    brightness(1.1)
    contrast(1.06);
}

/* Specular volume — simulates light through a curved liquid lens */
.mpm__glass-specular {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background:
    linear-gradient(
      155deg,
      rgba(255, 255, 255, 0.55) 0%,
      rgba(255, 255, 255, 0.14) 22%,
      rgba(255, 255, 255, 0.03) 42%,
      transparent 58%
    ),
    radial-gradient(
      110% 70% at 12% -5%,
      rgba(255, 255, 255, 0.42) 0%,
      transparent 42%
    ),
    radial-gradient(
      80% 60% at 95% 105%,
      rgba(0, 0, 0, 0.22) 0%,
      transparent 50%
    ),
    radial-gradient(
      60% 40% at 70% 0%,
      rgba(200, 225, 255, 0.12) 0%,
      transparent 55%
    );
  mix-blend-mode: soft-light;
  opacity: 1;
}

/* Thin top highlight streak — glass catching a key light */
.mpm__glass-highlight {
  position: absolute;
  left: 8%;
  right: 8%;
  top: 0;
  height: 1px;
  border-radius: 999px;
  background: linear-gradient(
    90deg,
    transparent 0%,
    rgba(255, 255, 255, 0.15) 15%,
    rgba(255, 255, 255, 0.75) 50%,
    rgba(255, 255, 255, 0.15) 85%,
    transparent 100%
  );
  opacity: 0.9;
  filter: blur(0.3px);
}

.mpm[data-mpm-open] .mpm__glass-highlight {
  left: 6%;
  right: 6%;
  opacity: 1;
}

/* Cool milky body */
.mpm__glass-tint {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background:
    linear-gradient(
      180deg,
      var(--glass-tint) 0%,
      rgba(255, 255, 255, 0.04) 55%,
      var(--glass-cool) 100%
    );
  transition: background 0.5s ease;
}

.mpm[data-mpm-open] .mpm__glass-tint {
  background:
    linear-gradient(
      180deg,
      var(--glass-tint-open) 0%,
      rgba(255, 255, 255, 0.06) 50%,
      rgba(170, 200, 255, 0.08) 100%
    );
}

/* Refractive edge stroke via mask composite */
.mpm__glass-edge {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 1px solid transparent;
  background:
    linear-gradient(
        140deg,
        rgba(255, 255, 255, 0.72) 0%,
        rgba(255, 255, 255, 0.22) 28%,
        rgba(255, 255, 255, 0.06) 50%,
        rgba(180, 210, 255, 0.18) 72%,
        rgba(255, 255, 255, 0.4) 100%
      )
      border-box;
  -webkit-mask:
    linear-gradient(#fff 0 0) padding-box,
    linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(#fff 0 0) padding-box,
    linear-gradient(#fff 0 0);
  mask-composite: exclude;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.55),
    inset 0 -1px 0 rgba(0, 0, 0, 0.14);
  opacity: 0.95;
}

/* Soft bottom occlusion — glass thickness / contact shadow inside the pill */
.mpm__glass-inner {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow:
    inset 0 -18px 28px rgba(0, 0, 0, 0.08),
    inset 0 12px 20px rgba(255, 255, 255, 0.04);
  opacity: 0.7;
  transition: opacity 0.45s ease;
}

.mpm[data-mpm-open] .mpm__glass-inner {
  opacity: 1;
  box-shadow:
    inset 0 -28px 40px rgba(0, 0, 0, 0.1),
    inset 0 16px 28px rgba(255, 255, 255, 0.05);
}

/* ---------- Toggle ---------- */

.mpm__toggle {
  position: absolute;
  top: 0;
  left: 50%;
  z-index: 2;
  transform: translateX(-50%);
  height: 46px;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 8px 0 18px;
  color: var(--mpm-fg-soft);
  -webkit-tap-highlight-color: transparent;
}

.mpm__toggle:focus-visible {
  outline: 2px solid rgba(255, 255, 255, 0.55);
  outline-offset: 3px;
  border-radius: 999px;
}

.mpm__toggle-text {
  overflow: hidden;
  height: 21px;
}

.mpm__toggle-text-roller {
  display: flex;
  flex-direction: column;
  will-change: transform;
}

.mpm__toggle-line {
  display: block;
  height: 21px;
  line-height: 21px;
  font-size: 17px;
  font-weight: 500;
  letter-spacing: -0.01em;
  text-align: center;
  white-space: nowrap;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
}

.mpm__toggle-icon {
  width: 30px;
  height: 30px;
  border-radius: 999px;
  background:
    linear-gradient(160deg, rgba(255, 255, 255, 0.32) 0%, rgba(255, 255, 255, 0.12) 100%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.45),
    inset 0 -1px 0 rgba(0, 0, 0, 0.08),
    0 0 0 1px rgba(255, 255, 255, 0.1),
    0 2px 6px rgba(0, 0, 0, 0.15);
  color: var(--mpm-fg-soft);
  overflow: hidden;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

.mpm__toggle-icon-roller {
  display: flex;
  flex-direction: column;
  will-change: transform;
}

.mpm__toggle-icon-face {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  flex-shrink: 0;
}

/* ---------- Expanded panel ---------- */

.mpm__panel {
  position: absolute;
  top: 0;
  left: 50%;
  z-index: 1;
  width: min(600px, calc(100vw - 24px));
  height: 280px;
  transform: translateX(-50%);
  padding: 56px var(--mpm-margin) var(--mpm-margin);
  box-sizing: border-box;
  color: var(--mpm-fg);
  visibility: hidden;
}

.mpm__body {
  display: grid;
  grid-template-columns: 190px 1fr;
  align-content: center;
  align-items: stretch;
  gap: 32px;
  width: 100%;
  height: 100%;
}

/* Featured media */
.mpm__media {
  position: relative;
  display: block;
  border-radius: 16px;
  overflow: hidden;
  cursor: pointer;
  min-height: 0;
  box-shadow:
    0 0 0 1px rgba(255, 255, 255, 0.12),
    0 8px 24px rgba(0, 0, 0, 0.25);
}

.mpm__media-image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  will-change: transform;
}

.mpm__media-shade {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(
    to top,
    rgba(0, 0, 0, 0.55) 0%,
    rgba(0, 0, 0, 0) 50%
  );
  opacity: 0;
  pointer-events: none;
}

.mpm__media-cue {
  position: absolute;
  left: 14px;
  bottom: 11px;
  z-index: 2;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: -0.01em;
  color: #ffffff;
  pointer-events: none;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}

.mpm__media:focus-visible {
  outline: 2px solid var(--mpm-accent);
  outline-offset: 3px;
}

/* Nav links */
.mpm__nav {
  display: flex;
  align-items: center;
}

.mpm__list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0;
  padding: 1rem 0;
}

.mpm__item {
  display: block;
}

.mpm__link {
  position: relative;
  display: inline-flex;
  align-items: flex-start;
  gap: 10px;
  color: var(--mpm-fg);
  cursor: pointer;
  padding-block: 2px;
  -webkit-tap-highlight-color: transparent;
}

.mpm__link-text {
  position: relative;
  display: inline-block;
  white-space: nowrap;
  font-size: clamp(28px, 3vw, 34px);
  font-weight: 400;
  letter-spacing: -0.02em;
  line-height: 1.08;
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.2);
}

.mpm__link-badge {
  align-self: flex-start;
  margin-top: 0.35em;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--mpm-accent);
  text-shadow: 0 0 12px rgba(126, 184, 255, 0.45);
}

.mpm__link:focus-visible {
  outline: none;
}

.mpm__link:focus-visible .mpm__link-text {
  text-decoration: underline;
  text-underline-offset: 4px;
}

/* ---------- Character split ---------- */

.mpm__char {
  position: relative;
  display: inline-block;
  overflow-x: visible;
  overflow-y: hidden;
  padding-bottom: 0.12em;
  margin-bottom: -0.12em;
  vertical-align: top;
}

.mpm__char-inner {
  position: relative;
  display: inline-block;
  will-change: transform;
}

.mpm__char-face {
  display: block;
}

.mpm__char-face--next {
  position: absolute;
  left: 0;
  top: 0;
  transform: translateY(110%);
}

.mpm__char-space {
  display: inline-block;
  white-space: pre;
}

/* ---------- SVG defs (off-screen) ---------- */

.mpm-svg {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
  pointer-events: none;
}

/* ---------- Responsive ---------- */

@media (max-width: 600px) {
  .mpm {
    --mpm-margin: 22px;
  }

  .mpm__panel {
    height: 430px;
  }

  .mpm__body {
    grid-template-columns: 1fr;
    gap: 20px;
  }

  .mpm__media {
    height: 150px;
  }

  .mpm__link-text {
    font-size: clamp(26px, 8vw, 32px);
  }
}

/* Prefer reduced motion: drop non-essential glass transitions */
@media (prefers-reduced-motion: reduce) {
  .mpm__surface,
  .mpm__glass-refraction,
  .mpm__glass-tint,
  .mpm__glass-inner {
    transition: none;
  }
}

/*
  Fallback when SVG backdrop-filter is unsupported:
  the refraction layer alone still produces a rich frosted liquid look.
*/
@supports not ((backdrop-filter: url(#liquid-glass-lens)) or (-webkit-backdrop-filter: url(#liquid-glass-lens))) {
  .mpm__glass-filter {
    display: none;
  }

  .mpm__glass-refraction {
    backdrop-filter:
      blur(26px)
      saturate(1.6)
      brightness(1.1)
      contrast(1.06);
    -webkit-backdrop-filter:
      blur(26px)
      saturate(1.6)
      brightness(1.1)
      contrast(1.06);
  }

  .mpm[data-mpm-open] .mpm__glass-refraction {
    backdrop-filter:
      blur(40px)
      saturate(1.65)
      brightness(1.12)
      contrast(1.07);
    -webkit-backdrop-filter:
      blur(40px)
      saturate(1.65)
      brightness(1.12)
      contrast(1.07);
  }
}
````

## Source — script.js
````javascript
/**
 * Morphing Pill Menu — Liquid Glass
 * ---------------------------------
 * Learner map:
 * 1. fitClosed()     — size the surface to the toggle’s intrinsic width
 * 2. openMenu()      — grow width → height (staggered), fade panel, rise chars
 * 3. closeMenu()     — reverse with elastic ease; restore closed pill
 * 4. rollToggle()    — Menu/Close text + dots/× icon share one yPercent roller
 * 5. splitChars()    — dual-face letters for open reveal + hover flip
 * 6. pulseIcon()     — idle / hover pulse on the 2×2 grid icon
 * 7. media hover     — “View” cue rolls up + image zooms slightly
 *
 * Timing is driven by data-mpm-duration / data-mpm-ease / data-mpm-stagger.
 */

(function () {
  "use strict";

  /* ---------- boot when GSAP + CustomEase are ready ---------- */

  function whenReady(callback, attempt) {
    attempt = attempt || 0;
    if (typeof gsap === "undefined" || typeof CustomEase === "undefined") {
      if (attempt < 100) {
        setTimeout(function () {
          whenReady(callback, attempt + 1);
        }, 50);
      } else {
        console.error("[Morphing Pill Menu] GSAP / CustomEase failed to load");
      }
      return;
    }
    callback();
  }

  whenReady(function () {
    gsap.registerPlugin(CustomEase);

    /* Soft “pop” settle used on open content */
    if (!CustomEase.get("mpmEase")) {
      CustomEase.create("mpmEase", "M0,0 C0.3,0.9 0.1,1 1,1");
    }
    /* Slightly snappier in/out for rollers + hover flips */
    if (!CustomEase.get("mpmEaseInOut")) {
      CustomEase.create("mpmEaseInOut", "M0,0 C0.7,0 0.16,1 1,1");
    }

    const instances = [];

    document.querySelectorAll("[data-mpm]").forEach(function (root) {
      const api = initMorphingPillMenu(root);
      if (api) instances.push(api);
    });

    /* Optional global hooks (handy for demos / iframes) */
    window.MorphingPillMenu = {
      refresh: function () {
        instances.forEach(function (api) {
          api.kill();
        });
        instances.length = 0;
        document.querySelectorAll("[data-mpm]").forEach(function (root) {
          const api = initMorphingPillMenu(root);
          if (api) instances.push(api);
        });
      },
      open: function () {
        instances.forEach(function (api) {
          api.open();
        });
      },
      close: function () {
        instances.forEach(function (api) {
          api.close();
        });
      },
    };
  });

  /* ---------- one menu instance ---------- */

  function initMorphingPillMenu(root) {
    const surface = root.querySelector("[data-mpm-surface]");
    const toggle = root.querySelector("[data-mpm-toggle]");
    const panel = root.querySelector("[data-mpm-panel]");
    const scrim = root.querySelector("[data-mpm-scrim]");
    const media = root.querySelector("[data-mpm-media]");
    const mediaImage = media ? media.querySelector("img") : null;
    const mediaShade = media
      ? media.querySelector("[data-mpm-media-shade]")
      : null;
    const mediaCue = media ? media.querySelector("[data-mpm-media-cue]") : null;
    const links = Array.from(root.querySelectorAll("[data-mpm-link]"));

    if (!surface || !toggle || !panel || !links.length) return null;

    const duration = parseFloat(root.dataset.mpmDuration || "1.0");
    const ease = root.dataset.mpmEase || "elastic.out(1,0.7)";
    const stagger = parseFloat(root.dataset.mpmStagger || "0.05");
    /* Elastic open → slightly different elastic on close so it doesn’t overshoot as hard */
    const closeEase =
      ease.indexOf("elastic") === 0 ? "elastic.out(1,0.9)" : ease;
    const panelFadeOut = 0.3;

    const toggleTextRoller = toggle.querySelector("[data-mpm-toggle-roller]");
    const toggleIconRoller = toggle.querySelector("[data-mpm-icon-roller]");
    /* Cells inside the closed 2×2 grid icon — used for idle / hover pulse */
    const iconCells =
      toggleIconRoller && toggleIconRoller.children[0]
        ? Array.from(
            toggleIconRoller.children[0].querySelectorAll("[data-mpm-icon-cell]")
          )
        : [];
    const badges = Array.from(root.querySelectorAll("[data-mpm-link-badge]"));

    let isOpen = false;
    let contentReady = false; /* panel fully visible — gates media hover */
    let timeline = null;
    let closedSize = { w: surface.offsetWidth, h: surface.offsetHeight };
    /* Per-link: true once open char rise finishes (allows hover flip) */
    const linkReady = links.map(function () {
      return false;
    });

    /* Split link labels + media cue into dual-face characters */
    const linkCharInners = links.map(function (link) {
      const textEl = link.querySelector("[data-mpm-link-text]");
      return textEl ? splitChars(textEl) : [];
    });
    const mediaCueChars = mediaCue ? splitChars(mediaCue) : [];

    /* ---------- character splitter ---------- */

    /**
     * Turns plain text into dual-face letter spans so we can roll
     * characters up (open reveal) or flip them (hover).
     * Returns an array of .mpm__char-inner elements to tween.
     */
    function splitChars(element) {
      const text = element.textContent;
      element.textContent = "";
      const inners = [];

      text.split("").forEach(function (char) {
        if (char === " ") {
          const space = document.createElement("span");
          space.className = "mpm__char-space";
          space.textContent = " ";
          element.appendChild(space);
          return;
        }

        const charWrap = document.createElement("span");
        charWrap.className = "mpm__char";

        const inner = document.createElement("span");
        inner.className = "mpm__char-inner";

        const face = document.createElement("span");
        face.className = "mpm__char-face";
        face.textContent = char;

        /* Duplicate face parked below — hover rolls it into view */
        const faceNext = document.createElement("span");
        faceNext.className = "mpm__char-face mpm__char-face--next";
        faceNext.textContent = char;
        faceNext.setAttribute("aria-hidden", "true");

        inner.appendChild(face);
        inner.appendChild(faceNext);
        charWrap.appendChild(inner);
        element.appendChild(charWrap);
        inners.push(inner);
      });

      return inners;
    }

    /* ---------- sizing helpers ---------- */

    function panelSize() {
      return { w: panel.offsetWidth, h: panel.offsetHeight };
    }

    /**
     * Fit the surface snugly around the toggle (closed pill).
     * Width follows the toggle’s content so “Menu” never clips.
     */
    function fitClosed() {
      if (isOpen) return;
      gsap.set(surface, { clearProps: "width,height,borderRadius" });
      const width = Math.ceil(toggle.offsetWidth);
      const height = surface.offsetHeight;
      gsap.set(surface, {
        width: width,
        borderRadius: height / 2,
      });
      closedSize = { w: width, h: height };
    }

    function setAria(open) {
      toggle.setAttribute("aria-expanded", String(open));
      toggle.setAttribute(
        "aria-label",
        open ? "Close navigation menu" : "Open navigation menu"
      );
      panel.setAttribute("aria-hidden", String(!open));
      if (open) {
        panel.removeAttribute("inert");
        root.setAttribute("data-mpm-open", "");
      } else {
        panel.setAttribute("inert", "");
        root.removeAttribute("data-mpm-open");
      }
    }

    /* ---------- toggle roller (Menu ↔ Close, dots ↔ ×) ---------- */

    function rollToggle(open) {
      gsap.to([toggleTextRoller, toggleIconRoller], {
        yPercent: open ? -50 : 0,
        y: 0,
        duration: 0.4,
        ease: "mpmEaseInOut",
        force3D: true,
        overwrite: "auto",
      });
    }

    /* ---------- link chars ---------- */

    function resetLinkChars() {
      linkCharInners.forEach(function (inners, index) {
        linkReady[index] = false;
        gsap.killTweensOf(inners);
        /* Parked below the clip — openMenu rises them to 0 */
        gsap.set(inners, { yPercent: 110, y: 0 });
      });
    }

    /**
     * Hover flip: roll dual-face letters up one line, then snap back
     * so the next hover can fire again.
     */
    function flipLink(index) {
      const inners = linkCharInners[index];
      if (!isOpen || !linkReady[index] || !inners.length) return;

      gsap.killTweensOf(inners);
      gsap.set(inners, { yPercent: 0, y: 0 });
      gsap.to(inners, {
        yPercent: -110,
        y: 0,
        duration: 0.55,
        ease: "mpmEaseInOut",
        stagger: 0.02,
        force3D: true,
        onComplete: function () {
          gsap.set(inners, { yPercent: 0, y: 0 });
        },
      });
    }

    /* ---------- grid icon pulse (closed only) ---------- */

    function pulseIcon() {
      if (isOpen || !iconCells.length) return;
      gsap.fromTo(
        iconCells,
        { opacity: 1 },
        {
          opacity: 0.22,
          duration: 0.22,
          yoyo: true,
          repeat: 1,
          ease: "power2.inOut",
          stagger: { grid: [2, 2], from: "center", each: 0.06 },
          overwrite: "auto",
        }
      );
    }

    /* ---------- media cue reset ---------- */

    function resetMediaCue() {
      if (!mediaCueChars.length) return;
      gsap.killTweensOf(mediaCueChars);
      if (mediaCue) gsap.killTweensOf(mediaCue);
      gsap.set(mediaCueChars, { yPercent: 110, y: 0 });
      if (mediaCue) gsap.set(mediaCue, { autoAlpha: 1 });
      if (mediaShade) gsap.set(mediaShade, { opacity: 0 });
    }

    /* ---------- open / close ---------- */

    function openMenu() {
      if (isOpen) return;
      isOpen = true;
      setAria(true);

      const size = panelSize();
      if (timeline) timeline.kill();

      resetLinkChars();
      resetMediaCue();
      contentReady = false;

      gsap.set(panel, { autoAlpha: 0 });
      gsap.set(badges, { autoAlpha: 0 });
      if (mediaImage) gsap.set(mediaImage, { scale: 1.12, force3D: true });

      timeline = gsap.timeline({ defaults: { force3D: true } });

      /* 1) Roll label + icon to “Close” */
      rollToggle(true);

      /* 2) Morph surface — width first, height slightly later (organic stretch) */
      timeline.to(
        surface,
        {
          width: size.w,
          borderRadius: 24,
          duration: duration,
          ease: ease,
        },
        0.08
      );
      timeline.to(
        surface,
        {
          height: size.h,
          duration: duration,
          ease: ease,
        },
        0.08 + 0.07
      );

      /* 3) Dim the page behind the glass */
      if (scrim) {
        timeline.to(
          scrim,
          { opacity: 1, duration: 0.6, ease: "power2.out" },
          0.08
        );
      }

      /* 4) Fade panel once the morph has some room */
      timeline.to(
        panel,
        {
          autoAlpha: 1,
          duration: 0.45,
          ease: "power2.out",
          onComplete: function () {
            contentReady = true;
          },
        },
        0.08 + 0.24 * duration
      );

      /* 5) Media image settles from slight overscale */
      if (mediaImage) {
        timeline.to(
          mediaImage,
          {
            scale: 1,
            duration: duration,
            ease: "mpmEase",
          },
          0.08 + 0.24 * duration
        );
      }

      /* 6) Link characters rise into place, staggered per row */
      const linksStart = 0.08 + 0.3 * duration;
      linkCharInners.forEach(function (inners, index) {
        timeline.to(
          inners,
          {
            yPercent: 0,
            y: 0,
            duration: 0.7,
            ease: "mpmEase",
            stagger: 0.0125,
            onComplete: function () {
              linkReady[index] = true;
              /* If pointer already rests on the link, flip immediately */
              if (links[index].matches(":hover")) flipLink(index);
            },
          },
          linksStart + index * stagger
        );
      });

      /* 7) “New” badge fades after the last link */
      if (badges.length) {
        timeline.to(
          badges,
          { autoAlpha: 1, duration: 0.4, ease: "power2.out" },
          linksStart + linkCharInners.length * stagger
        );
      }

      /* 8) Move focus into the panel for keyboard users */
      timeline.call(
        function () {
          const first = panel.querySelector("a[href]");
          if (first) first.focus({ preventScroll: true });
        },
        null,
        0.08 + 0.6 * duration
      );
    }

    function closeMenu() {
      if (!isOpen) return;
      isOpen = false;
      setAria(false);
      contentReady = false;

      if (timeline) timeline.kill();
      timeline = gsap.timeline({ defaults: { force3D: true } });

      rollToggle(false);

      /* Panel out quickly, then surface collapses (height before width) */
      timeline.to(
        panel,
        { autoAlpha: 0, duration: panelFadeOut, ease: "power2.in" },
        0
      );
      timeline.to(
        surface,
        {
          height: closedSize.h,
          duration: 0.85 * duration,
          ease: closeEase,
        },
        0.12
      );
      timeline.to(
        surface,
        {
          width: closedSize.w,
          borderRadius: closedSize.h / 2,
          duration: 0.85 * duration,
          ease: closeEase,
        },
        0.19
      );

      if (scrim) {
        timeline.to(
          scrim,
          { opacity: 0, duration: 0.45, ease: "power2.in" },
          0
        );
      }

      timeline.call(resetLinkChars, null, panelFadeOut);
      timeline.call(resetMediaCue, null, panelFadeOut);
      toggle.focus({ preventScroll: true });
    }

    function toggleMenu() {
      if (isOpen) closeMenu();
      else openMenu();
    }

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

    function onKeydown(event) {
      if (event.key === "Escape" && isOpen) closeMenu();
    }

    function onPointerDown(event) {
      if (!isOpen) return;
      if (!surface.contains(event.target)) closeMenu();
    }

    toggle.addEventListener("click", toggleMenu);
    toggle.addEventListener("mouseenter", pulseIcon);

    /* Media: hover reveal + click closes menu */
    if (media) {
      media.addEventListener("click", function () {
        if (isOpen) closeMenu();
      });

      media.addEventListener("mouseenter", function () {
        if (!isOpen || !contentReady || !mediaCueChars.length) return;

        gsap.killTweensOf(mediaCueChars);
        if (mediaCue) {
          gsap.killTweensOf(mediaCue);
          gsap.set(mediaCue, { autoAlpha: 1 });
        }
        gsap.set(mediaCueChars, { yPercent: 110, y: 0 });
        gsap.to(mediaCueChars, {
          yPercent: 0,
          y: 0,
          duration: 0.45,
          ease: "mpmEase",
          stagger: 0.02,
          force3D: true,
        });

        if (mediaShade) {
          gsap.to(mediaShade, {
            opacity: 1,
            duration: 0.4,
            ease: "power2.out",
            overwrite: "auto",
          });
        }
        if (mediaImage) {
          gsap.to(mediaImage, {
            scale: 1.06,
            duration: 0.7,
            ease: "mpmEaseInOut",
            force3D: true,
            overwrite: "auto",
          });
        }
      });

      media.addEventListener("mouseleave", function () {
        if (!mediaCueChars.length) return;

        gsap.killTweensOf(mediaCueChars);
        if (mediaCue) {
          gsap.killTweensOf(mediaCue);
          gsap.to(mediaCue, {
            autoAlpha: 0,
            duration: 0.35,
            ease: "power2.in",
            onComplete: function () {
              gsap.set(mediaCueChars, { yPercent: 110, y: 0 });
              gsap.set(mediaCue, { autoAlpha: 1 });
            },
          });
        }
        if (mediaShade) {
          gsap.to(mediaShade, {
            opacity: 0,
            duration: 0.35,
            ease: "power2.in",
            overwrite: "auto",
          });
        }
        if (mediaImage && isOpen && contentReady) {
          gsap.to(mediaImage, {
            scale: 1,
            duration: 0.6,
            ease: "mpmEaseInOut",
            force3D: true,
            overwrite: "auto",
          });
        }
      });
    }

    links.forEach(function (link, index) {
      link.addEventListener("mouseenter", function () {
        flipLink(index);
      });
      link.addEventListener("focus", function () {
        flipLink(index);
      });
      /* Choosing a nav item dismisses the menu */
      link.addEventListener("click", function () {
        if (isOpen) closeMenu();
      });
    });

    document.addEventListener("keydown", onKeydown);
    document.addEventListener("pointerdown", onPointerDown);

    /* ---------- init presentation ---------- */

    setAria(false);
    resetLinkChars();
    resetMediaCue();
    gsap.set(panel, { autoAlpha: 0 });
    fitClosed();

    /* Re-measure after fonts load so the closed pill width is accurate */
    if (document.fonts && document.fonts.ready) {
      document.fonts.ready.then(function () {
        fitClosed();
      });
    }

    /* Idle pulse every 6s while closed (skipped if reduced motion) */
    let idleTimer = null;
    const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)");

    function scheduleIdlePulse() {
      if (reduceMotion.matches) return;
      idleTimer = setTimeout(function () {
        pulseIcon();
        scheduleIdlePulse();
      }, 6000);
    }
    scheduleIdlePulse();

    /* Speed up everything dramatically under reduced motion */
    gsap.matchMedia().add("(prefers-reduced-motion: reduce)", function () {
      gsap.globalTimeline.timeScale(20);
      return function () {
        gsap.globalTimeline.timeScale(1);
      };
    });

    let resizeTimer = null;
    function onResize() {
      clearTimeout(resizeTimer);
      resizeTimer = setTimeout(function () {
        if (isOpen) {
          const size = panelSize();
          gsap.set(surface, {
            width: size.w,
            height: size.h,
            borderRadius: 24,
          });
        } else {
          fitClosed();
        }
      }, 150);
    }

    function onVisibility() {
      if (document.hidden) gsap.globalTimeline.pause();
      else gsap.globalTimeline.resume();
    }

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

    return {
      open: openMenu,
      close: closeMenu,
      toggle: toggleMenu,
      kill: function () {
        window.removeEventListener("resize", onResize);
        document.removeEventListener("visibilitychange", onVisibility);
        document.removeEventListener("keydown", onKeydown);
        document.removeEventListener("pointerdown", onPointerDown);
        clearTimeout(idleTimer);
        clearTimeout(resizeTimer);
        if (timeline) timeline.kill();
      },
    };
  }
})();
````
