# Modal Cards

> A product modal with layered hero crossfades, rising content panels, and carousel navigation — polished multi-brand showcases.

**Definition:** Modal Cards 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-07
- Canonical page: https://html.bosconvertpro.com/collection/modal-cards
- Full-page preview: https://html.bosconvertpro.com/collection/modal-cards/full
- Live demo: https://html.bosconvertpro.com/demos/modal-cards/index.html
- Markdown mirror: https://html.bosconvertpro.com/collection/modal-cards/index.md
- License: https://html.bosconvertpro.com/terms

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

## FAQ
### What is Modal Cards?

Modal Cards is a components UI animation from collections.html. A product modal with layered hero crossfades, rising content panels, and carousel navigation — polished multi-brand showcases.

### What is Modal Cards built with?

Modal Cards 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 Modal Cards in my project?

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

### Is Modal Cards free to use?

Yes. The Modal Cards 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>Modal Cards</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&display=swap"
      rel="stylesheet"
    />
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <main class="page">
      <button
        type="button"
        class="open-btn"
        id="openBtn"
        aria-haspopup="dialog"
        aria-controls="modal"
      >
        Open
      </button>
    </main>

    <div class="overlay" id="overlay" hidden>
      <div class="overlay__backdrop" data-close-modal></div>

      <div
        class="modal"
        id="modal"
        role="dialog"
        aria-modal="true"
        aria-labelledby="modalTitle"
        tabindex="-1"
      >
        <div class="modal__shell">
          <!-- Fixed hero stage: layered BGs + brands (smooth opacity crossfade) -->
          <div class="hero-stage" id="heroStage">
            <div class="hero-stage__bgs" aria-hidden="true">
              <div
                class="hero-bg is-active"
                data-hero-bg="0"
                data-theme="grok"
              ></div>
              <div class="hero-bg" data-hero-bg="1" data-theme="claude"></div>
              <div class="hero-bg" data-hero-bg="2" data-theme="codex"></div>
              <div class="hero-bg" data-hero-bg="3" data-theme="cursor"></div>
            </div>

            <div class="hero-stage__brands">
              <div class="hero-brand is-active" data-hero-brand="0">
                <img
                  class="hero-brand__logo"
                  src="logos/xai.svg"
                  alt=""
                  width="36"
                  height="36"
                  draggable="false"
                />
                <p class="hero-brand__company">xAI</p>
                <h2 class="hero-brand__title" id="modalTitle">Grok</h2>
                <p class="hero-brand__tagline">
                  Sharp, current, and a little irreverent.
                </p>
              </div>

              <div class="hero-brand" data-hero-brand="1">
                <img
                  class="hero-brand__logo"
                  src="logos/claude.svg"
                  alt=""
                  width="36"
                  height="36"
                  draggable="false"
                />
                <p class="hero-brand__company">Anthropic</p>
                <h2 class="hero-brand__title">Claude</h2>
                <p class="hero-brand__tagline">
                  Thoughtful reasoning with a careful touch.
                </p>
              </div>

              <div class="hero-brand" data-hero-brand="2">
                <img
                  class="hero-brand__logo"
                  src="logos/openai.svg"
                  alt=""
                  width="36"
                  height="36"
                  draggable="false"
                />
                <p class="hero-brand__company">OpenAI</p>
                <h2 class="hero-brand__title">Codex</h2>
                <p class="hero-brand__tagline">
                  An engineering teammate that ships.
                </p>
              </div>

              <div class="hero-brand" data-hero-brand="3">
                <img
                  class="hero-brand__logo"
                  src="logos/cursor.svg"
                  alt=""
                  width="36"
                  height="36"
                  draggable="false"
                />
                <p class="hero-brand__company">Anysphere</p>
                <h2 class="hero-brand__title">Cursor</h2>
                <p class="hero-brand__tagline">
                  The AI code editor, built to build.
                </p>
              </div>
            </div>
          </div>

          <!-- Scrollable content stage -->
          <div class="content-stage" id="contentStage">
            <div class="content-panel is-active" data-content="0" data-accent="#111111">
              <div class="content-panel__scroll">
                <div class="content-panel__copy">
                  <p>
                    Grok is xAI's flagship model, wired directly into the pulse
                    of X. It answers with real-time knowledge of what's happening
                    in the world right now, and it isn't shy about having a
                    personality while doing it.
                  </p>
                  <p>
                    Its reasoning modes trade speed for depth on demand, making
                    it a strong pick for research that touches breaking news,
                    market movements, or anything where yesterday's snapshot
                    isn't good enough.
                  </p>
                  <p>
                    Use it when you want a model that stays current, argues back
                    when needed, and still ships clear answers under pressure.
                  </p>
                </div>
              </div>
            </div>

            <div class="content-panel" data-content="1" data-accent="#C26A45">
              <div class="content-panel__scroll">
                <div class="content-panel__copy">
                  <p>
                    Claude is Anthropic's family of models, known for nuanced
                    writing, honest reasoning, and a knack for holding enormous
                    amounts of context without losing the thread.
                  </p>
                  <p>
                    It shines in long-form work: refactoring a sprawling
                    codebase with Claude Code, drafting prose that actually
                    sounds human, or thinking through ambiguous problems where
                    judgment matters as much as speed.
                  </p>
                  <p>
                    Teams lean on Claude when the work is messy, multi-step, and
                    needs a calm partner that doesn't drop the plot.
                  </p>
                </div>
              </div>
            </div>

            <div class="content-panel" data-content="2" data-accent="#3B7DD8">
              <div class="content-panel__scroll">
                <div class="content-panel__copy">
                  <p>
                    Codex is OpenAI's agentic coding companion, powered by the
                    frontier GPT line. Hand it an issue and it plans, writes,
                    tests, and opens the pull request, in your terminal, your
                    editor, or a sandboxed cloud machine.
                  </p>
                  <p>
                    It's built for delegation: parallel tasks, long autonomous
                    runs, and code review that catches what tired human eyes
                    skim past. Less autocomplete, more teammate.
                  </p>
                  <p>
                    Point it at a backlog item and let it drive the loop from
                    plan to patch while you stay in the reviewer's seat.
                  </p>
                </div>
              </div>
            </div>

            <div class="content-panel" data-content="3" data-accent="#6B5CE7">
              <div class="content-panel__scroll">
                <div class="content-panel__copy">
                  <p>
                    Cursor isn't just a model, it's where models come to work.
                    Tab predicts your next edit before you think it; Composer
                    and its agents take a prompt and reshape entire features
                    across files.
                  </p>
                  <p>
                    With frontier models from every lab a keystroke away, plus
                    deep codebase awareness and background agents, it turns
                    "choose an LLM" into "choose them all."
                  </p>
                  <p>
                    And the work doesn't stop when you close the laptop. Cloud
                    agents keep shipping while you sleep, so progress doesn't
                    wait on your next open tab.
                  </p>
                </div>
              </div>
            </div>
          </div>

          <footer class="modal__footer">
            <div class="dots" id="dots" role="tablist" aria-label="Slides">
              <button
                type="button"
                class="dot is-active"
                role="tab"
                aria-selected="true"
                aria-label="Grok"
                data-dot="0"
              ></button>
              <button
                type="button"
                class="dot"
                role="tab"
                aria-selected="false"
                aria-label="Claude"
                data-dot="1"
              ></button>
              <button
                type="button"
                class="dot"
                role="tab"
                aria-selected="false"
                aria-label="Codex"
                data-dot="2"
              ></button>
              <button
                type="button"
                class="dot"
                role="tab"
                aria-selected="false"
                aria-label="Cursor"
                data-dot="3"
              ></button>
            </div>

            <div class="nav">
              <button
                type="button"
                class="nav__btn"
                id="prevBtn"
                aria-label="Previous slide"
              >
                <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
                  <path
                    d="M10 3.5L5.5 8 10 12.5"
                    stroke="currentColor"
                    stroke-width="1.6"
                    stroke-linecap="round"
                    stroke-linejoin="round"
                  />
                </svg>
              </button>
              <button
                type="button"
                class="nav__btn"
                id="nextBtn"
                aria-label="Next slide"
              >
                <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
                  <path
                    d="M6 3.5L10.5 8 6 12.5"
                    stroke="currentColor"
                    stroke-width="1.6"
                    stroke-linecap="round"
                    stroke-linejoin="round"
                  />
                </svg>
              </button>
            </div>
          </footer>
        </div>
      </div>
    </div>

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

## Source — styles.css
````css
/* =========================================================
   Modal Cards — layered hero crossfade + rise content
   ========================================================= */

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

:root {
  --bg: #f4f4f5;
  --surface: #ffffff;
  --text: #2f3338;
  --shadow-modal:
    0 2px 4px rgba(15, 23, 42, 0.03),
    0 12px 24px rgba(15, 23, 42, 0.06),
    0 40px 80px rgba(15, 23, 42, 0.1);
  --radius-modal: 28px;
  --radius-hero: 18px;
  --modal-w: 428px;
  --modal-h: 540px;
  --hero-h: 200px;
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --font: "Inter", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
}

html {
  height: 100%;
  -webkit-text-size-adjust: 100%;
}

body {
  min-height: 100%;
  min-height: 100dvh;
  font-family: var(--font);
  font-weight: 400;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  line-height: 1.5;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  cursor: pointer;
}

button:focus-visible {
  outline: 2px solid #3b82f6;
  outline-offset: 3px;
}

/* ---------------------------------------------------------
   Page + open button
   --------------------------------------------------------- */

.page {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem 1.25rem;
}

.open-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 148px;
  height: 52px;
  padding: 0 1.75rem;
  border-radius: 999px;
  background: #111827;
  color: #ffffff;
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  box-shadow:
    0 1px 2px rgba(15, 23, 42, 0.08),
    0 8px 24px rgba(15, 23, 42, 0.12);
  transition:
    transform 0.3s var(--ease-out),
    box-shadow 0.3s var(--ease-out),
    background 0.2s ease;
}

.open-btn:hover {
  background: #1f2937;
  transform: translateY(-2px);
  box-shadow:
    0 2px 4px rgba(15, 23, 42, 0.08),
    0 14px 32px rgba(15, 23, 42, 0.16);
}

.open-btn:active {
  transform: translateY(0) scale(0.98);
}

/* ---------------------------------------------------------
   Overlay + Modal shell
   --------------------------------------------------------- */

.overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
  padding: 1.25rem;
  pointer-events: none;
}

.overlay[hidden] {
  display: none;
}

.overlay.is-open {
  pointer-events: auto;
}

.overlay__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(244, 244, 245, 0.72);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  opacity: 0;
  cursor: pointer;
}

.modal {
  position: relative;
  z-index: 1;
  width: min(100%, var(--modal-w));
  opacity: 0;
  transform: translateY(24px) scale(0.94);
  will-change: transform, opacity;
  outline: none;
}

.modal__shell {
  position: relative;
  display: flex;
  flex-direction: column;
  height: min(var(--modal-h), calc(100dvh - 2.5rem));
  max-height: calc(100dvh - 2.5rem);
  background: var(--surface);
  border-radius: var(--radius-modal);
  box-shadow: var(--shadow-modal);
  border: 1px solid rgba(255, 255, 255, 0.9);
  overflow: hidden;
  padding: 1rem 1rem 0;
}

/* ---------------------------------------------------------
   Hero stage — fixed frame, layered BGs + brands
   --------------------------------------------------------- */

.hero-stage {
  position: relative;
  flex-shrink: 0;
  height: var(--hero-h);
  border-radius: var(--radius-hero);
  overflow: hidden;
  isolation: isolate;
  /* Dark fallback while layers fade */
  background: #121214;
}

.hero-stage__bgs {
  position: absolute;
  inset: 0;
  z-index: 0;
}

.hero-bg {
  position: absolute;
  inset: 0;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: scale(1.04);
  will-change: opacity, transform;
  backface-visibility: hidden;
  transform-origin: center center;
}

.hero-bg.is-active {
  opacity: 1;
  visibility: visible;
  transform: scale(1);
  z-index: 1;
}

.hero-bg::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* Theme backgrounds */
.hero-bg[data-theme="grok"] {
  background:
    radial-gradient(ellipse 90% 100% at 28% 45%, #5a5a64 0%, transparent 52%),
    radial-gradient(ellipse 70% 80% at 72% 55%, #18181c 0%, transparent 48%),
    radial-gradient(ellipse 50% 60% at 50% 100%, #0a0a0c 0%, transparent 60%),
    linear-gradient(160deg, #2e2e34 0%, #121214 55%, #0a0a0c 100%);
}

.hero-bg[data-theme="grok"]::after {
  background:
    radial-gradient(circle at 35% 40%, rgba(255, 255, 255, 0.12) 0%, transparent 40%),
    radial-gradient(circle at 70% 60%, rgba(0, 0, 0, 0.35) 0%, transparent 45%);
}

.hero-bg[data-theme="claude"] {
  background:
    radial-gradient(ellipse 95% 110% at 22% 38%, #f0b090 0%, transparent 50%),
    radial-gradient(ellipse 75% 90% at 78% 65%, #7a3a28 0%, transparent 48%),
    radial-gradient(ellipse 50% 50% at 55% 50%, #c47855 0%, transparent 55%),
    linear-gradient(155deg, #d99070 0%, #b56040 50%, #8a3e2a 100%);
}

.hero-bg[data-theme="claude"]::after {
  background:
    radial-gradient(circle at 30% 35%, rgba(255, 220, 190, 0.35) 0%, transparent 40%),
    radial-gradient(circle at 75% 70%, rgba(60, 20, 10, 0.4) 0%, transparent 45%);
}

.hero-bg[data-theme="codex"] {
  background:
    radial-gradient(ellipse 90% 100% at 20% 40%, #7ab0e8 0%, transparent 50%),
    radial-gradient(ellipse 70% 85% at 82% 50%, #e0a060 0%, transparent 48%),
    radial-gradient(ellipse 55% 60% at 55% 55%, #5a90d0 0%, transparent 50%),
    linear-gradient(150deg, #5a9ad8 0%, #4a7ec0 45%, #3a6aa8 100%);
}

.hero-bg[data-theme="codex"]::after {
  background:
    radial-gradient(circle at 25% 40%, rgba(180, 220, 255, 0.35) 0%, transparent 40%),
    radial-gradient(circle at 80% 55%, rgba(240, 170, 90, 0.45) 0%, transparent 42%);
}

.hero-bg[data-theme="cursor"] {
  background:
    radial-gradient(ellipse 90% 100% at 25% 35%, #a898f0 0%, transparent 50%),
    radial-gradient(ellipse 70% 85% at 80% 70%, #1e1450 0%, transparent 48%),
    radial-gradient(ellipse 50% 55% at 50% 55%, #5a48c8 0%, transparent 50%),
    linear-gradient(155deg, #7b6ae8 0%, #4a3ab0 50%, #2a1e70 100%);
}

.hero-bg[data-theme="cursor"]::after {
  background:
    radial-gradient(circle at 28% 35%, rgba(200, 190, 255, 0.4) 0%, transparent 40%),
    radial-gradient(circle at 75% 70%, rgba(20, 10, 60, 0.5) 0%, transparent 45%);
}

/* Brand overlays (logo + title) */
.hero-stage__brands {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: grid;
  place-items: center;
  pointer-events: none;
}

.hero-brand {
  grid-area: 1 / 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 1.25rem 1.5rem;
  max-width: 90%;
  color: #fff;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  will-change: opacity, transform;
  backface-visibility: hidden;
}

.hero-brand.is-active {
  opacity: 1;
  visibility: visible;
}

.hero-brand__logo {
  display: block;
  width: 36px;
  height: 36px;
  margin-bottom: 0.85rem;
  object-fit: contain;
  filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.18));
  user-select: none;
  pointer-events: none;
}

.hero-brand__company {
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.72);
  margin-bottom: 0.4rem;
}

.hero-brand__title {
  font-size: 2rem;
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1.1;
  color: #ffffff;
  margin-bottom: 0.4rem;
}

.hero-brand__tagline {
  font-size: 0.875rem;
  font-weight: 400;
  line-height: 1.4;
  color: rgba(255, 255, 255, 0.78);
  max-width: 18rem;
}

/* ---------------------------------------------------------
   Content stage — scrollable stacked panels
   --------------------------------------------------------- */

.content-stage {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  margin-top: 0.15rem;
}

.content-panel {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  z-index: 0;
  will-change: opacity;
}

.content-panel.is-active {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  z-index: 2;
}

.content-panel.is-leaving {
  visibility: visible;
  pointer-events: none;
  z-index: 1;
}

.content-panel.is-entering {
  visibility: visible;
  pointer-events: none;
  z-index: 3;
}

.content-panel__scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: 1.25rem 0.4rem 5.25rem 0.55rem;
  /* Firefox: hide by default, reveal on hover */
  scrollbar-width: thin;
  scrollbar-color: transparent transparent;
  transition: scrollbar-color 0.35s ease;
}

.content-panel__scroll:hover,
.content-panel__scroll:focus-within,
.content-panel__scroll.is-scrolling {
  scrollbar-color: rgba(15, 23, 42, 0.22) rgba(15, 23, 42, 0.04);
}

/* WebKit premium scrollbar — reserved width, thumb fades in */
.content-panel__scroll::-webkit-scrollbar {
  width: 10px;
}

.content-panel__scroll::-webkit-scrollbar-track {
  background: transparent;
  margin: 6px 0 4.5rem;
  border-radius: 999px;
}

.content-panel__scroll::-webkit-scrollbar-thumb {
  background-color: transparent;
  background-clip: padding-box;
  border: 3px solid transparent;
  border-radius: 999px;
  box-shadow: none;
  transition:
    background-color 0.35s ease,
    box-shadow 0.35s ease;
}

/* Soft rail on hover */
.content-panel__scroll:hover::-webkit-scrollbar-track,
.content-panel__scroll:focus-within::-webkit-scrollbar-track,
.content-panel__scroll.is-scrolling::-webkit-scrollbar-track {
  background: rgba(15, 23, 42, 0.035);
}

/* Pill thumb — soft graphite with light depth */
.content-panel__scroll:hover::-webkit-scrollbar-thumb,
.content-panel__scroll:focus-within::-webkit-scrollbar-thumb,
.content-panel__scroll.is-scrolling::-webkit-scrollbar-thumb {
  background-color: rgba(15, 23, 42, 0.18);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.35);
}

.content-panel__scroll:hover::-webkit-scrollbar-thumb:hover,
.content-panel__scroll:focus-within::-webkit-scrollbar-thumb:hover,
.content-panel__scroll.is-scrolling::-webkit-scrollbar-thumb:hover {
  background-color: rgba(15, 23, 42, 0.3);
}

.content-panel__scroll:hover::-webkit-scrollbar-thumb:active,
.content-panel__scroll:focus-within::-webkit-scrollbar-thumb:active,
.content-panel__scroll.is-scrolling::-webkit-scrollbar-thumb:active {
  background-color: rgba(15, 23, 42, 0.42);
}

.content-panel__copy {
  display: flex;
  flex-direction: column;
  gap: 0.95rem;
}

.content-panel__copy p {
  font-size: 0.875rem;
  line-height: 1.62;
  color: #5a6169;
  letter-spacing: -0.006em;
  will-change: opacity, transform;
}

/* ---------------------------------------------------------
   Footer
   --------------------------------------------------------- */

.modal__footer {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 2rem 1.35rem 1.2rem;
  pointer-events: none;
  background: linear-gradient(
    to top,
    rgba(255, 255, 255, 1) 48%,
    rgba(255, 255, 255, 0.92) 68%,
    rgba(255, 255, 255, 0) 100%
  );
}

.dots,
.nav {
  pointer-events: auto;
  display: flex;
  align-items: center;
}

.dots {
  gap: 0.45rem;
  padding: 0.25rem 0;
}

.dot {
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: #d1d5db;
  transition:
    background 0.25s ease,
    width 0.45s var(--ease-out);
  flex-shrink: 0;
}

.dot:hover:not(.is-active) {
  background: #9ca3af;
}

.dot.is-active {
  width: 18px;
  background: var(--dot-accent, #111111);
}

.nav {
  gap: 0.4rem;
}

.nav__btn {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 1px solid rgba(15, 23, 42, 0.08);
  background: #ffffff;
  color: #4b5563;
  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
  transition:
    background 0.2s ease,
    color 0.2s ease,
    border-color 0.2s ease,
    transform 0.2s var(--ease-out),
    box-shadow 0.2s ease;
}

.nav__btn:hover {
  background: #f9fafb;
  color: #111827;
  border-color: rgba(15, 23, 42, 0.12);
  box-shadow: 0 2px 6px rgba(15, 23, 42, 0.06);
}

.nav__btn:active {
  transform: scale(0.94);
}

.nav__btn svg {
  display: block;
}

body.is-modal-open {
  overflow: hidden;
}

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

@media (max-width: 480px) {
  :root {
    --modal-w: 100%;
    --radius-modal: 24px;
    --hero-h: 168px;
    --modal-h: 520px;
  }

  .hero-brand__title {
    font-size: 1.75rem;
  }

  .content-panel__copy p {
    font-size: 0.8125rem;
  }

  .modal__footer {
    padding-left: 1.1rem;
    padding-right: 1.1rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  .open-btn,
  .dot,
  .nav__btn {
    transition: none;
  }
}
````

## Source — script.js
````javascript
/**
 * Modal Cards
 * - Hero BG + logo: long smooth opacity dissolve (layered, no blur)
 * - Content: bottom → top rise with opacity
 * - Body panels are scrollable
 */
(function () {
  "use strict";

  if (typeof gsap === "undefined") {
    console.error("[Modal Cards] GSAP is required.");
    return;
  }

  // Smoother GPU transforms
  gsap.config({ force3D: true });
  gsap.defaults({ ease: "power2.out" });

  const prefersReduced = window.matchMedia(
    "(prefers-reduced-motion: reduce)"
  ).matches;

  const overlay = document.getElementById("overlay");
  const modal = document.getElementById("modal");
  const openBtn = document.getElementById("openBtn");
  const prevBtn = document.getElementById("prevBtn");
  const nextBtn = document.getElementById("nextBtn");
  const dotsRoot = document.getElementById("dots");
  const backdrop = overlay.querySelector(".overlay__backdrop");
  const footer = modal.querySelector(".modal__footer");

  const heroBgs = Array.from(document.querySelectorAll("[data-hero-bg]"));
  const heroBrands = Array.from(document.querySelectorAll("[data-hero-brand]"));
  const panels = Array.from(document.querySelectorAll("[data-content]"));
  const dots = Array.from(dotsRoot.querySelectorAll(".dot"));
  const closeTargets = Array.from(
    document.querySelectorAll("[data-close-modal]")
  );

  const TOTAL = panels.length;
  const accents = panels.map(function (p) {
    return p.dataset.accent || "#111111";
  });

  let current = 0;
  let isOpen = false;
  let isAnimating = false;
  let lastFocus = null;
  let activeTl = null;
  let touchStartX = 0;
  let touchStartY = 0;

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

  function clampIndex(i) {
    return ((i % TOTAL) + TOTAL) % TOTAL;
  }

  function killTl() {
    if (activeTl) {
      activeTl.kill();
      activeTl = null;
    }
  }

  function updateDots(index) {
    dots.forEach(function (dot, i) {
      const on = i === index;
      dot.classList.toggle("is-active", on);
      dot.setAttribute("aria-selected", on ? "true" : "false");
    });
    dotsRoot.style.setProperty("--dot-accent", accents[index]);
  }

  function setHeroInstant(index) {
    heroBgs.forEach(function (bg, i) {
      const on = i === index;
      bg.classList.toggle("is-active", on);
      gsap.set(bg, {
        autoAlpha: on ? 1 : 0,
        scale: on ? 1 : 1.04,
        zIndex: on ? 2 : 0,
      });
    });

    heroBrands.forEach(function (brand, i) {
      const on = i === index;
      brand.classList.toggle("is-active", on);
      gsap.set(brand, {
        autoAlpha: on ? 1 : 0,
        y: 0,
        scale: 1,
      });
    });
  }

  function setPanelInstant(index) {
    panels.forEach(function (panel, i) {
      const on = i === index;
      panel.classList.toggle("is-active", on);
      panel.classList.remove("is-leaving", "is-entering");
      gsap.set(panel, { autoAlpha: on ? 1 : 0 });

      const paras = panel.querySelectorAll(".content-panel__copy p");
      gsap.set(paras, { autoAlpha: 1, y: 0 });

      const scroll = panel.querySelector(".content-panel__scroll");
      if (scroll) scroll.scrollTop = 0;
    });
  }

  function showIndex(index) {
    setHeroInstant(index);
    setPanelInstant(index);
    updateDots(index);
  }

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

  function openModal(index) {
    if (isOpen || isAnimating) return;

    killTl();
    current = clampIndex(index);
    lastFocus = document.activeElement;
    isAnimating = true;
    isOpen = true;

    overlay.hidden = false;
    overlay.classList.add("is-open");
    document.body.classList.add("is-modal-open");

    showIndex(current);

    const brand = heroBrands[current];
    const bg = heroBgs[current];
    const paras = panels[current].querySelectorAll(".content-panel__copy p");

    gsap.set(backdrop, { autoAlpha: 0 });
    gsap.set(modal, { autoAlpha: 0, y: 28, scale: 0.94 });
    gsap.set(bg, { autoAlpha: 1, scale: 1.06 });
    gsap.set(brand, { autoAlpha: 0, y: 14, scale: 0.98 });
    gsap.set(paras, { autoAlpha: 0, y: 32 });
    gsap.set(footer, { autoAlpha: 0, y: 10 });

    if (prefersReduced) {
      gsap.set(backdrop, { autoAlpha: 1 });
      gsap.set(modal, { autoAlpha: 1, y: 0, scale: 1 });
      gsap.set(bg, { autoAlpha: 1, scale: 1 });
      gsap.set(brand, { autoAlpha: 1, y: 0, scale: 1 });
      gsap.set(paras, { autoAlpha: 1, y: 0 });
      gsap.set(footer, { autoAlpha: 1, y: 0 });
      isAnimating = false;
      modal.focus({ preventScroll: true });
      return;
    }

    activeTl = gsap.timeline({
      onComplete: function () {
        isAnimating = false;
        activeTl = null;
        modal.focus({ preventScroll: true });
      },
    });

    activeTl
      .to(backdrop, { autoAlpha: 1, duration: 0.45, ease: "power2.out" })
      .to(
        modal,
        {
          autoAlpha: 1,
          y: 0,
          scale: 1,
          duration: 0.6,
          ease: "power3.out",
        },
        0.05
      )
      .to(
        bg,
        {
          scale: 1,
          duration: 1.05,
          ease: "power2.out",
        },
        0.08
      )
      .to(
        brand,
        {
          autoAlpha: 1,
          y: 0,
          scale: 1,
          duration: 0.65,
          ease: "power3.out",
        },
        0.2
      )
      .to(
        paras,
        {
          autoAlpha: 1,
          y: 0,
          duration: 0.6,
          stagger: 0.08,
          ease: "power3.out",
        },
        0.32
      )
      .to(
        footer,
        {
          autoAlpha: 1,
          y: 0,
          duration: 0.4,
          ease: "power2.out",
        },
        0.36
      );
  }

  function closeModal() {
    if (!isOpen || isAnimating) return;
    isAnimating = true;
    killTl();

    const finish = function () {
      showIndex(current);
      overlay.classList.remove("is-open");
      overlay.hidden = true;
      document.body.classList.remove("is-modal-open");
      isOpen = false;
      isAnimating = false;
      activeTl = null;
      if (lastFocus && typeof lastFocus.focus === "function") {
        lastFocus.focus({ preventScroll: true });
      }
    };

    if (prefersReduced) {
      gsap.set([backdrop, modal], { clearProps: "all" });
      finish();
      return;
    }

    activeTl = gsap.timeline({ onComplete: finish });
    activeTl
      .to(modal, {
        autoAlpha: 0,
        y: 14,
        scale: 0.96,
        duration: 0.32,
        ease: "power2.inOut",
      })
      .to(
        backdrop,
        { autoAlpha: 0, duration: 0.3, ease: "power2.inOut" },
        0
      );
  }

  /* ---------- slide change ---------- */

  function goTo(nextIndex) {
    if (!isOpen || isAnimating) return;

    nextIndex = clampIndex(nextIndex);
    if (nextIndex === current) return;

    killTl();
    isAnimating = true;

    const from = current;
    const to = nextIndex;

    const outBg = heroBgs[from];
    const inBg = heroBgs[to];
    const outBrand = heroBrands[from];
    const inBrand = heroBrands[to];
    const outPanel = panels[from];
    const inPanel = panels[to];
    const outParas = outPanel.querySelectorAll(".content-panel__copy p");
    const inParas = inPanel.querySelectorAll(".content-panel__copy p");
    const inScroll = inPanel.querySelector(".content-panel__scroll");

    updateDots(to);

    if (prefersReduced) {
      showIndex(to);
      current = to;
      isAnimating = false;
      return;
    }

    // Classes for stacking
    outPanel.classList.remove("is-active");
    outPanel.classList.add("is-leaving");
    inPanel.classList.add("is-entering");
    inPanel.classList.remove("is-active");

    outBg.classList.remove("is-active");
    inBg.classList.add("is-active");
    outBrand.classList.remove("is-active");
    inBrand.classList.add("is-active");

    if (inScroll) inScroll.scrollTop = 0;

    // --- prepare layers ---
    // Outgoing BG stays fully visible underneath
    gsap.set(outBg, { autoAlpha: 1, scale: 1, zIndex: 1 });
    // Incoming BG on top, starts invisible, slightly scaled up
    gsap.set(inBg, { autoAlpha: 0, scale: 1.06, zIndex: 3 });

    gsap.set(outBrand, { autoAlpha: 1, y: 0, scale: 1 });
    gsap.set(inBrand, { autoAlpha: 0, y: 10, scale: 0.985 });

    gsap.set(outPanel, { autoAlpha: 1 });
    gsap.set(inPanel, { autoAlpha: 1 });
    gsap.set(outParas, { autoAlpha: 1, y: 0 });
    gsap.set(inParas, { autoAlpha: 0, y: 36 });

    activeTl = gsap.timeline({
      onComplete: function () {
        // Cleanup
        outPanel.classList.remove("is-leaving");
        inPanel.classList.remove("is-entering");
        inPanel.classList.add("is-active");

        heroBgs.forEach(function (bg, i) {
          const on = i === to;
          bg.classList.toggle("is-active", on);
          gsap.set(bg, {
            autoAlpha: on ? 1 : 0,
            scale: on ? 1 : 1.04,
            zIndex: on ? 2 : 0,
          });
        });

        heroBrands.forEach(function (brand, i) {
          const on = i === to;
          brand.classList.toggle("is-active", on);
          gsap.set(brand, { autoAlpha: on ? 1 : 0, y: 0, scale: 1 });
        });

        panels.forEach(function (panel, i) {
          const on = i === to;
          panel.classList.toggle("is-active", on);
          panel.classList.remove("is-leaving", "is-entering");
          gsap.set(panel, { autoAlpha: on ? 1 : 0 });
          if (!on) {
            const ps = panel.querySelectorAll(".content-panel__copy p");
            gsap.set(ps, { autoAlpha: 1, y: 0 });
          }
        });

        current = to;
        isAnimating = false;
        activeTl = null;
      },
    });

    /*
      Hero dissolve:
      - Incoming BG gently fades + eases scale 1.06 → 1 (long, silky)
      - Outgoing BG stays put until mid-fade, then soft-fades (overlap = no flash)
      - Brand soft crossfade with tiny rise (no blur)
    */
    activeTl
      // Incoming background — primary smooth dissolve
      .to(
        inBg,
        {
          autoAlpha: 1,
          scale: 1,
          duration: 0.95,
          ease: "power2.inOut",
        },
        0
      )
      // Outgoing background — delayed soft exit so colors melt
      .to(
        outBg,
        {
          autoAlpha: 0,
          scale: 1.02,
          duration: 0.7,
          ease: "power2.inOut",
        },
        0.28
      )

      // Brand out
      .to(
        outBrand,
        {
          autoAlpha: 0,
          y: -6,
          scale: 0.99,
          duration: 0.38,
          ease: "power2.in",
        },
        0.05
      )
      // Brand in (slightly after bg starts)
      .to(
        inBrand,
        {
          autoAlpha: 1,
          y: 0,
          scale: 1,
          duration: 0.62,
          ease: "power3.out",
        },
        0.28
      )

      // Content out (quick)
      .to(
        outParas,
        {
          autoAlpha: 0,
          y: -10,
          duration: 0.32,
          stagger: 0.025,
          ease: "power2.in",
        },
        0
      )
      // Content in — bottom → top
      .to(
        inParas,
        {
          autoAlpha: 1,
          y: 0,
          duration: 0.62,
          stagger: 0.08,
          ease: "power3.out",
        },
        0.34
      );
  }

  function next() {
    goTo(current + 1);
  }

  function prev() {
    goTo(current - 1);
  }

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

  if (openBtn) {
    openBtn.addEventListener("click", function () {
      openModal(0);
    });
  }

  closeTargets.forEach(function (el) {
    el.addEventListener("click", closeModal);
  });

  prevBtn.addEventListener("click", prev);
  nextBtn.addEventListener("click", next);

  dots.forEach(function (dot) {
    dot.addEventListener("click", function () {
      const idx = parseInt(dot.getAttribute("data-dot"), 10);
      if (Number.isNaN(idx)) return;
      goTo(idx);
    });
  });

  document.addEventListener("keydown", function (e) {
    if (!isOpen) return;
    if (e.key === "Escape") {
      e.preventDefault();
      closeModal();
    } else if (e.key === "ArrowRight") {
      e.preventDefault();
      next();
    } else if (e.key === "ArrowLeft") {
      e.preventDefault();
      prev();
    }
  });

  modal.addEventListener(
    "touchstart",
    function (e) {
      if (!e.changedTouches || !e.changedTouches[0]) return;
      touchStartX = e.changedTouches[0].clientX;
      touchStartY = e.changedTouches[0].clientY;
    },
    { passive: true }
  );

  modal.addEventListener(
    "touchend",
    function (e) {
      if (!isOpen || isAnimating) return;
      if (!e.changedTouches || !e.changedTouches[0]) return;

      const dx = e.changedTouches[0].clientX - touchStartX;
      const dy = e.changedTouches[0].clientY - touchStartY;

      if (e.target && e.target.closest && e.target.closest(".content-panel__scroll")) {
        const scroller = e.target.closest(".content-panel__scroll");
        if (scroller.scrollHeight > scroller.clientHeight + 4) {
          if (Math.abs(dx) < 72 || Math.abs(dx) < Math.abs(dy) * 1.6) return;
        }
      }

      if (Math.abs(dx) < 48 || Math.abs(dx) < Math.abs(dy) * 1.2) return;
      if (dx < 0) next();
      else prev();
    },
    { passive: true }
  );

  /* Premium scrollbar: stay visible while scrolling */
  panels.forEach(function (panel) {
    const scroller = panel.querySelector(".content-panel__scroll");
    if (!scroller) return;

    let hideTimer = null;

    scroller.addEventListener(
      "scroll",
      function () {
        scroller.classList.add("is-scrolling");
        if (hideTimer) window.clearTimeout(hideTimer);
        hideTimer = window.setTimeout(function () {
          scroller.classList.remove("is-scrolling");
        }, 900);
      },
      { passive: true }
    );
  });

  // Init
  showIndex(0);
})();
````
