← GPT-AI-VIDEO
WEBSITE · 43.4 KB

vendor/OpenMontage/.agents/skills/hyperframes-animation/examples/problem-mockup-overwhelm.html

Workspace snapshot · 09/04 14:52

Preview

Source
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=1920, height=1080" />
    <title>Scene — HyperFrames Problem Mockup Overwhelm</title>

    <!--
      Blueprint: problem-mockup-overwhelm (HyperFrames preview)
      HyperFrames-native version of the scene-02-mockup-morph-overwhelm concept.

      Choreography (4 phases, 6 seconds total):
        0.00 – 0.55s   Three HyperFrames workflow mockups spring-in (left → right → center)
                       Check panel left · render queue right · preview timeline center
        0.33 – 0.95s   Nine scattered HyperFrames tool icons stagger in around the cluster
        3.20 – 3.80s   MORPH:
                         center mockup compositor-scale down to the avatar footprint
                         borderRadius repaint (42px → 50% = reads as circle)
                         content fades out at 0-40% of morph
                         non-center mockups + icons exit concurrently
                         avatar pop (scale 0 → 1) starts at morph trigger
                         avatar layer opacity fades in at 50% of morph
                         at 85-100% of morph, mockup-center fades to 0 → avatar visible underneath
        3.53 – 4.40s   Eight task bubbles stagger-enter in a radial pattern around the avatar
        4.40 – 6.00s   Idle: bubble micro-float + avatar orbit dots + avatar breath
          paint-only tween. The "rect → circle" effect is completed by the final 0.51s-onward
          opacity hand-off to the avatar element rendered underneath at z-index 20.
        - Both mockup layer and avatar layer stay in DOM (no `{showX && <Y />}` conditional render);
          opacity gates visibility — required because HyperFrames seek can move time backwards.
        - All `Math.sin(frame * ...)` continuous motion replaced by:
            mockup float / bubble float / orbit dots → shared onUpdate scene-ticker reading tl.time()
            avatar breath → finite sine.inOut yoyo (multiplicative onto pop scale)
        - The animated dark-frosted-glass blob background was replaced with a static
          gradient + frosted overlay for renderer determinism and file size.
        - Avatar video source replaced with a CSS gradient circle (no asset required).
          To use a real video, swap the inner .avatar-disc for a <video muted playsinline>.
        - Bubbles are generated via JS `forEach` at script load — positions pre-baked in
          inline `left`/`top`; GSAP tweens only `scale`/`opacity`/`x`/`y`.

      Spring → ease mapping (from skill SKILL.md):
        spring(stiffness:70,  damping:14)  → back.out(1.4)    (mockup entry)
        spring(stiffness:180, damping:14)  → back.out(1.6)    (icon entry)
        spring(stiffness:80,  damping:18)  → power3.out       (morph driver)
        spring(stiffness:120, damping:14)  → back.out(1.4)    (avatar pop)
        spring(stiffness:180, damping:12)  → back.out(1.4)    (bubble pop)
    -->

    <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>

    <style>
      :root {
        --bg-base: #0a0a0f;
        --text-primary: #ffffff;
        --surface-dark: #000000;
        --surface-mid: #16213e;
        --surface-light: rgba(255, 255, 255, 0.98);
        --line-soft: rgba(139, 92, 246, 0.5);
        --bubble-bg: rgba(255, 255, 255, 0.95);
        --bubble-border: rgba(139, 92, 246, 0.5);
        --bubble-text: #1f2937;
        --brand-cyan: #06b6d4;
        --brand-teal: #14b8a6;
        --brand-green: #00f2ea;
        --brand-blue: #0ea5e9;
        --brand-purple: #8b5cf6;
        --brand-pink: #ec4899;
        --brand-orange: #fb923c;
        --brand-ink: #0f0f0f;
        --accent-lime: #facc15;
        --accent-mint: #ffffff;
        --warn-amber: #ffc107;
        --tt-cyan: #00f2ea;
      }
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      html,
      body {
        margin: 0;
        width: 1920px;
        height: 1080px;
        overflow: hidden;
        background: var(--bg-base);
        font-family:
          "Inter",
          system-ui,
          -apple-system,
          sans-serif;
        color: var(--text-primary);
      }

      /* ============================================================
         BACKGROUND (dark frosted glass, matching TSX reference)
         ============================================================ */
      .bg {
        position: absolute;
        inset: 0;
        overflow: hidden;
        background: radial-gradient(
          ellipse 120% 100% at 50% 20%,
          rgba(30, 30, 45, 1) 0%,
          rgba(18, 18, 28, 1) 40%,
          rgba(10, 10, 15, 1) 100%
        );
      }
      .bg::before {
        content: "";
        position: absolute;
        inset: 0;
        background: rgba(15, 15, 25, 0.4);
        backdrop-filter: blur(80px) saturate(180%);
        -webkit-backdrop-filter: blur(80px) saturate(180%);
      }
      .bg::after {
        content: "";
        position: absolute;
        inset: 0;
        background:
          linear-gradient(
            180deg,
            rgba(255, 255, 255, 0.08) 0%,
            rgba(255, 255, 255, 0.02) 5%,
            transparent 15%,
            transparent 100%
          ),
          linear-gradient(0deg, rgba(139, 92, 246, 0.05) 0%, transparent 20%, transparent 100%);
      }
      .bg-frost {
        position: absolute;
        inset: 0;
        opacity: 0.5;
        filter: blur(120px);
        background:
          radial-gradient(
            circle 550px at 30% 35%,
            rgba(139, 92, 246, 0.7) 0%,
            rgba(139, 92, 246, 0.3) 40%,
            transparent 70%
          ),
          radial-gradient(
            circle 500px at 70% 65%,
            rgba(6, 182, 212, 0.6) 0%,
            rgba(6, 182, 212, 0.25) 40%,
            transparent 70%
          ),
          radial-gradient(
            circle 400px at 50% 80%,
            rgba(251, 146, 60, 0.5) 0%,
            rgba(251, 146, 60, 0.2) 40%,
            transparent 70%
          ),
          radial-gradient(
            circle 350px at 20% 60%,
            rgba(236, 72, 153, 0.4) 0%,
            rgba(236, 72, 153, 0.15) 40%,
            transparent 70%
          );
      }
      .vignette {
        position: absolute;
        inset: 0;
        background-image:
          radial-gradient(
            ellipse 80% 80% at 50% 50%,
            transparent 0%,
            transparent 50%,
            rgba(0, 0, 0, 0.3) 100%
          ),
          url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E"),
          repeating-linear-gradient(
            0deg,
            transparent,
            transparent 2px,
            rgba(255, 255, 255, 0.00075) 2px,
            rgba(255, 255, 255, 0.00075) 4px
          );
        pointer-events: none;
        z-index: 400;
      }

      /* ============================================================
         MOCKUP CLUSTER (Phase 1-2)
         ============================================================ */
      .mockup-cluster {
        position: absolute;
        inset: 0;
        perspective: 1400px;
        transform-style: preserve-3d;
      }

      /* Fitness-style phone shells */
      .mockup {
        position: absolute;
        left: 50%;
        top: 50%;
        width: 320px;
        height: 650px;
        border-radius: 42px;
        background: transparent;
        transform-origin: center center;
        transform-style: preserve-3d;
        will-change: transform, opacity;
      }
      .mockup-left {
        z-index: 10;
      }
      .mockup-right {
        z-index: 12;
      }
      .mockup-center {
        z-index: 25;
        background: var(--surface-dark);
        box-shadow:
          0 24px 70px rgba(0, 0, 0, 0.4),
          0 0 40px rgba(0, 242, 234, 0.15);
        overflow: hidden;
      }

      .phone-shell {
        position: absolute;
        inset: 0;
        border-radius: 42px;
        background: #000000;
        padding: 8px;
        box-shadow:
          0 18px 60px rgba(0, 0, 0, 0.3),
          0 4px 16px rgba(0, 0, 0, 0.18),
          inset 0 0 0 3px #333333;
        overflow: hidden;
        will-change: opacity;
      }

      .phone-notch {
        position: absolute;
        top: 8px;
        left: 50%;
        width: 100px;
        height: 26px;
        border-radius: 0 0 18px 18px;
        background: #000000;
        transform: translateX(-50%);
        z-index: 5;
      }

      .phone-screen {
        position: relative;
        width: 100%;
        height: 100%;
        border-radius: 35px;
        overflow: hidden;
        display: flex;
        flex-direction: column;
      }

      .mockup-left .phone-screen,
      .mockup-right .phone-screen {
        background: rgba(255, 255, 255, 0.98);
        padding-top: 30px;
      }

      .mockup-center .phone-screen {
        background: #000000;
      }

      .mockup-content {
        flex: 1;
        display: flex;
        flex-direction: column;
        min-height: 0;
        will-change: opacity;
      }

      /* --- HyperFrames Check (left) --- */
      .yt-header {
        padding: 14px 18px;
        background: #f9f9f9;
        border-bottom: 1px solid #e5e5e5;
        display: flex;
        align-items: center;
        gap: 10px;
        font: 600 16px/1 Inter;
        color: #0f0f0f;
      }
      .yt-header svg .hf-logo-left {
        fill: url(#hf-mini-grad-left);
      }
      .yt-body {
        padding: 18px;
        color: #0f0f0f;
      }
      .yt-body h3 {
        font: 700 17px/1.2 Inter;
        margin-bottom: 12px;
      }
      .yt-thumb {
        height: 130px;
        background: linear-gradient(135deg, #1a1a2e, #16213e);
        border-radius: 10px;
        margin-bottom: 14px;
        padding: 18px;
        display: grid;
        gap: 8px;
        align-content: center;
      }
      .code-line {
        height: 10px;
        border-radius: 5px;
        background: rgba(255, 255, 255, 0.24);
      }
      .code-line:nth-child(1) {
        width: 74%;
        background: #ff0000;
      }
      .code-line:nth-child(2) {
        width: 58%;
      }
      .code-line:nth-child(3) {
        width: 86%;
      }
      .code-line:nth-child(4) {
        width: 46%;
        background: rgba(255, 255, 255, 0.3);
      }
      .code-line:nth-child(5) {
        width: 68%;
      }
      .code-line:nth-child(6) {
        width: 52%;
        background: rgba(255, 255, 255, 0.28);
      }
      .yt-progress-fill {
        width: 72%;
        height: 100%;
        background: #ff0000;
        border-radius: 3px;
      }
      .yt-progress-row {
        display: flex;
        justify-content: space-between;
        font: 500 12px/1 Inter;
        color: #606060;
        margin-bottom: 6px;
      }
      .yt-progress-track {
        height: 5px;
        background: #e5e5e5;
        border-radius: 3px;
        overflow: hidden;
      }
      .yt-task-label {
        font: 500 12px/1 Inter;
        color: #606060;
        margin-top: 16px;
        margin-bottom: 8px;
      }
      .yt-task {
        display: flex;
        align-items: center;
        gap: 8px;
        margin-bottom: 6px;
        font: 400 12px/1.2 Inter;
        color: #606060;
      }
      .yt-task::before {
        content: "";
        width: 13px;
        height: 13px;
        border: 2px solid #909090;
        border-radius: 50%;
        background: transparent;
        flex-shrink: 0;
      }
      .yt-footer {
        margin-top: auto;
        padding: 12px 18px;
        background: #fff4e5;
        border-top: 1px solid #ffe0b2;
        font: 500 12px/1 Inter;
        color: #e65100;
        display: flex;
        align-items: center;
        gap: 8px;
      }

      /* --- HyperFrames Preview (center) — content fades during morph --- */
      .tt-statusbar {
        padding: 10px 14px;
        display: flex;
        justify-content: space-between;
        font: 500 12px/1 Inter;
        color: var(--accent-mint);
      }
      .tt-preview {
        flex: 1;
        position: relative;
        background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
      }
      .tt-overlay {
        position: absolute;
        top: 14px;
        left: 14px;
        right: 14px;
        background: rgba(0, 0, 0, 0.6);
        border-radius: 10px;
        padding: 10px 12px;
        border: none;
        backdrop-filter: blur(10px);
        -webkit-backdrop-filter: blur(10px);
      }
      .tt-overlay-title {
        font: 500 12px/1 Inter;
        color: white;
        margin-bottom: 4px;
      }
      .tt-overlay-sub {
        font: 400 10px/1.2 Inter;
        color: rgba(216, 254, 255, 0.72);
      }
      .tt-play {
        position: absolute;
        top: 50%;
        left: 50%;
        width: 60px;
        height: 60px;
        margin: -30px 0 0 -30px;
        border-radius: 50%;
        background: rgba(255, 255, 255, 0.3);
        box-shadow: none;
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 22px;
        color: white;
      }
      .tt-side {
        position: absolute;
        right: 12px;
        bottom: 70px;
        display: flex;
        flex-direction: column;
        gap: 14px;
        font: 500 10px/1 Inter;
        color: white;
        text-align: center;
      }
      .tt-side div {
        display: flex;
        flex-direction: column;
        gap: 3px;
      }
      .tt-side span {
        font-size: 18px;
        font-weight: 800;
        color: #ffffff;
      }
      .tt-timeline {
        padding: 12px;
        background: #111111;
      }
      .tt-strip {
        display: flex;
        gap: 4px;
        margin-bottom: 8px;
      }
      .tt-clip {
        flex: 1;
        height: 32px;
        border-radius: 4px;
        background: #333333;
        opacity: 0.6;
      }
      .tt-clip.active {
        background: var(--tt-cyan);
        opacity: 1;
      }
      .tt-note {
        font: 500 10px/1 Inter;
        color: #888888;
        text-align: center;
      }
      .tt-tabs {
        padding: 10px 18px;
        background: #000000;
        display: flex;
        justify-content: space-around;
        font: 500 11px/1 Inter;
        color: white;
      }

      /* --- HyperFrames Render (right) --- */
      .ig-header {
        padding: 12px 16px;
        border-bottom: 1px solid #efefef;
        display: flex;
        align-items: center;
        gap: 10px;
        font: 600 15px/1 Inter;
        color: #262626;
      }
      .ig-body {
        padding: 14px;
        flex: 1;
        color: #262626;
      }
      .ig-body h3 {
        font: 500 13px/1 Inter;
        margin-bottom: 12px;
      }
      .ig-warn {
        background: #fff3cd;
        border: 1px solid #ffc107;
        border-radius: 10px;
        padding: 10px 12px;
        margin-bottom: 14px;
      }
      .ig-warn-title {
        font: 500 12px/1.1 Inter;
        color: #856404;
      }
      .ig-warn-sub {
        font: 400 11px/1.2 Inter;
        color: #856404;
        margin-top: 4px;
      }
      .ig-task-label {
        font: 500 13px/1 Inter;
        color: #262626;
        margin-bottom: 8px;
      }
      .ig-task {
        display: flex;
        align-items: center;
        gap: 8px;
        margin-bottom: 6px;
        font: 400 12px/1.2 Inter;
        color: #666666;
      }
      .ig-task::before {
        content: "";
        width: 12px;
        height: 12px;
        border: 2px solid #c7c7c7;
        border-radius: 50%;
        background: transparent;
        flex-shrink: 0;
      }
      .ig-time {
        margin-top: 14px;
        background: #ffebee;
        border-radius: 8px;
        padding: 10px 12px;
        font: 500 12px/1 Inter;
        color: #c62828;
      }
      .ig-footer {
        margin-top: auto;
        padding: 12px 14px;
        border-top: 1px solid #efefef;
        background: #fafafa;
      }
      .ig-share {
        background: linear-gradient(90deg, #833ab4, #fd1d1d, #fcaf45);
        border-radius: 10px;
        padding: 11px 0;
        text-align: center;
        font: 600 13px/1 Inter;
        color: #ffffff;
      }

      /* ============================================================
         PLATFORM ICONS — scattered, positions pre-baked in CSS
         ============================================================ */
      .platform-icon {
        position: absolute;
        border-radius: 22%;
        background: rgba(255, 255, 255, 0.98);
        display: flex;
        align-items: center;
        justify-content: center;
        box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
        border: none;
        z-index: 30;
        will-change: transform, opacity;
      }
      .platform-icon svg {
        width: 62%;
        height: 62%;
      }
      .platform-icon .icon-type {
        font: 800 25px/1 Inter;
        letter-spacing: 0;
        color: #063238;
      }
      .platform-icon .icon-small {
        font-size: 20px;
      }

      /* Nine icon positions — taken from the the source SCATTERED_ICONS array */
      .pi-youtube {
        left: calc(22% + 100px);
        top: calc(20% - 179px);
        width: 102px;
        height: 102px;
      }
      .pi-google {
        left: calc(60% + 16px);
        top: calc(15% - 113px);
        width: 96px;
        height: 96px;
      }
      .pi-instagram {
        left: calc(88% - 73px);
        top: calc(25% - 103px);
        width: 102px;
        height: 102px;
      }
      .pi-tiktok {
        left: 10%;
        top: calc(48% - 101px);
        width: 90px;
        height: 90px;
      }
      .pi-tripadvisor {
        left: calc(94% - 89px);
        top: calc(55% - 45px);
        width: 84px;
        height: 84px;
      }
      .pi-facebook {
        left: 15%;
        top: 75%;
        width: 90px;
        height: 90px;
      }
      .pi-twitter {
        left: 42%;
        top: 85%;
        width: 84px;
        height: 84px;
      }
      .pi-linkedin {
        left: 70%;
        top: 85%;
        width: 78px;
        height: 78px;
      }
      .pi-yelp {
        left: calc(92% - 165px);
        top: calc(72% + 4px);
        width: 90px;
        height: 90px;
      }

      /* ============================================================
         AVATAR + BUBBLES (Phase 3-4)
         Initially opacity 0; opacity tween reveals at morph hand-off.
         ============================================================ */
      .avatar-with-bubbles {
        position: absolute;
        inset: 0;
        opacity: 0;
        will-change: opacity;
      }
      .avatar-stage {
        position: absolute;
        left: 50%;
        top: 50%;
        z-index: 20;
        will-change: transform;
      }
      .avatar-ring {
        width: 220px;
        height: 220px;
        border-radius: 50%;
        padding: 5px;
        background: linear-gradient(135deg, #14b8a6, #06b6d4, #0ea5e9);
        box-shadow:
          0 0 60px rgba(20, 184, 166, 0.5),
          0 0 120px rgba(6, 182, 212, 0.3);
      }
      .avatar-disc {
        width: 100%;
        height: 100%;
        border-radius: 50%;
        border: 3px solid rgba(255, 255, 255, 0.25);
        background:
          radial-gradient(circle at 40% 35%, rgba(255, 255, 255, 0.2), transparent 50%),
          conic-gradient(from 40deg, #14b8a6 0%, #06b6d4 35%, #0ea5e9 65%, #8b5cf6 100%);
        overflow: hidden;
      }
      .orbit-dot {
        position: absolute;
        left: 50%;
        top: 50%;
        width: 10px;
        height: 10px;
        margin: -5px 0 0 -5px;
        border-radius: 50%;
        will-change: transform, opacity;
      }
      .orbit-dot:nth-child(1) {
        background: var(--accent-lime);
      }
      .orbit-dot:nth-child(2) {
        background: var(--brand-purple);
      }
      .orbit-dot:nth-child(3) {
        background: var(--accent-lime);
      }
      .orbit-dot:nth-child(4) {
        background: var(--brand-purple);
      }

      .task-bubble {
        position: absolute;
        z-index: 30;
        background: var(--bubble-bg);
        border: 3px solid var(--bubble-border);
        border-radius: 24px;
        padding: 22px 34px;
        box-shadow: 0 12px 35px rgba(139, 92, 246, 0.3);
        font: 500 26px/1.4 Inter;
        color: var(--bubble-text);
        white-space: nowrap;
        will-change: transform, opacity;
      }
    </style>
  </head>
  <body>
    <div
      id="root"
      data-composition-id="main"
      data-start="0"
      data-duration="6"
      data-width="1920"
      data-height="1080"
    >
      <div class="bg"></div>
      <div class="bg-frost"></div>

      <div
        id="problem-mockup-overwhelm-scene"
        class="scene clip"
        data-start="0"
        data-duration="6"
        data-track-index="1"
      >
        <!-- ===== Phase 1-2: Mockup cluster + scattered icons ===== -->
        <div class="mockup-cluster">
          <!-- HyperFrames Check mockup (left) -->
          <div class="mockup mockup-left">
            <div class="phone-shell">
              <div class="phone-notch"></div>
              <div class="phone-screen">
                <div class="mockup-content yt-content">
                  <div class="yt-header">
                    <svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true">
                      <defs>
                        <linearGradient id="hf-mini-grad-left" x1="2" y1="4" x2="22" y2="20">
                          <stop offset="0%" stop-color="#ff0000" />
                          <stop offset="100%" stop-color="#cc0000" />
                        </linearGradient>
                      </defs>
                      <path
                        class="hf-logo-left"
                        d="M4 9.5 11.5 5c2.7-1.6 5.7.8 5.1 3.8L15 17.5c-.5 2.8-3.8 4-6 2.2L3.2 15c-2-1.6-1.5-4.2.8-5.5Z"
                      />
                      <path
                        class="hf-logo-left"
                        d="M13.2 6.5 20.8 11c2.4 1.4 2.4 4.6 0 6l-7.6 4.5c-2.6 1.5-5.8-.7-5.2-3.6l1.8-9c.5-2.8 3-3.8 3.4-2.4Z"
                        opacity="0.82"
                      />
                    </svg>
                    HyperFrames Check
                  </div>
                  <div class="yt-body">
                    <h3>Validate composition</h3>
                    <div class="yt-thumb">
                      <div class="code-line"></div>
                      <div class="code-line"></div>
                      <div class="code-line"></div>
                      <div class="code-line"></div>
                      <div class="code-line"></div>
                      <div class="code-line"></div>
                    </div>
                    <div class="yt-progress-row"><span>Lint + inspect</span><span>72%</span></div>
                    <div class="yt-progress-track"><div class="yt-progress-fill"></div></div>
                    <div class="yt-task-label">Render-safe rules:</div>
                    <div class="yt-task">data-start / duration set</div>
                    <div class="yt-task">Timeline registered</div>
                    <div class="yt-task">Assets resolve locally</div>
                    <div class="yt-task">Text fits every frame</div>
                  </div>
                  <div class="yt-footer">npm run check before render</div>
                </div>
              </div>
            </div>
          </div>

          <!-- HyperFrames Preview mockup (center, morph target) -->
          <div class="mockup mockup-center">
            <div class="phone-shell">
              <div class="phone-notch"></div>
              <div class="phone-screen">
                <div class="mockup-content tt-content">
                  <div class="tt-statusbar"><span>9:41</span><span>HF</span></div>
                  <div class="tt-preview">
                    <div class="tt-overlay">
                      <div class="tt-overlay-title">Scene 05 · seekable</div>
                      <div class="tt-overlay-sub">GSAP timeline + data clips</div>
                    </div>
                    <div class="tt-play">HF</div>
                    <div class="tt-side">
                      <div><span>12</span>Clips</div>
                      <div><span>4</span>Tracks</div>
                      <div><span>V</span>Vars</div>
                      <div><span>MP4</span>Render</div>
                    </div>
                  </div>
                  <div class="tt-timeline">
                    <div class="tt-strip">
                      <div class="tt-clip"></div>
                      <div class="tt-clip"></div>
                      <div class="tt-clip active"></div>
                      <div class="tt-clip"></div>
                      <div class="tt-clip"></div>
                      <div class="tt-clip"></div>
                      <div class="tt-clip"></div>
                      <div class="tt-clip"></div>
                    </div>
                    <div class="tt-note">0 errors · frame-safe render</div>
                  </div>
                  <div class="tt-tabs">
                    <span>Clips</span><span>Tracks</span><span>Vars</span><span>Render</span>
                  </div>
                </div>
              </div>
            </div>
          </div>

          <!-- HyperFrames Render mockup (right) -->
          <div class="mockup mockup-right">
            <div class="phone-shell">
              <div class="phone-notch"></div>
              <div class="phone-screen">
                <div class="mockup-content ig-content">
                  <div class="ig-header">
                    <svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true">
                      <rect
                        x="3"
                        y="4"
                        width="18"
                        height="14"
                        rx="3"
                        fill="none"
                        stroke="#833ab4"
                        stroke-width="2"
                      />
                      <path d="M9 9.2 15 12 9 14.8Z" fill="#fd1d1d" />
                      <path d="M6 21h12" stroke="#fcaf45" stroke-width="2" stroke-linecap="round" />
                    </svg>
                    Render Queue
                  </div>
                  <div class="ig-body">
                    <h3>Export HyperFrames scene</h3>
                    <div class="ig-warn">
                      <div class="ig-warn-title">Ready for MP4 render</div>
                      <div class="ig-warn-sub">lint · validate · inspect passed</div>
                    </div>
                    <div class="ig-task-label">Pipeline:</div>
                    <div class="ig-task">Preview locally</div>
                    <div class="ig-task">Validate runtime</div>
                    <div class="ig-task">Inspect layout</div>
                    <div class="ig-task">Render MP4</div>
                    <div class="ig-task">Publish share link</div>
                    <div class="ig-time">Draft render: ~45 sec</div>
                  </div>
                  <div class="ig-footer">
                    <div class="ig-share">Publish link</div>
                  </div>
                </div>
              </div>
            </div>
          </div>

          <!-- Nine scattered HyperFrames workflow icons -->
          <div class="platform-icon pi-youtube">
            <svg viewBox="0 0 24 24" aria-hidden="true">
              <defs>
                <linearGradient id="hf-orbit-grad" x1="2" y1="4" x2="22" y2="20">
                  <stop offset="0%" stop-color="#14b8a6" />
                  <stop offset="100%" stop-color="#06b6d4" />
                </linearGradient>
              </defs>
              <path
                d="M4 9.5 11.5 5c2.7-1.6 5.7.8 5.1 3.8L15 17.5c-.5 2.8-3.8 4-6 2.2L3.2 15c-2-1.6-1.5-4.2.8-5.5Z"
                fill="url(#hf-orbit-grad)"
              />
              <path
                d="M13.2 6.5 20.8 11c2.4 1.4 2.4 4.6 0 6l-7.6 4.5c-2.6 1.5-5.8-.7-5.2-3.6l1.8-9c.5-2.8 3-3.8 3.4-2.4Z"
                fill="url(#hf-orbit-grad)"
                opacity="0.82"
              />
            </svg>
          </div>
          <div class="platform-icon pi-google">
            <span class="icon-type">&lt;/&gt;</span>
          </div>
          <div class="platform-icon pi-instagram">
            <span class="icon-type icon-small">GS</span>
          </div>
          <div class="platform-icon pi-tiktok">
            <span class="icon-type icon-small">CSS</span>
          </div>
          <div class="platform-icon pi-tripadvisor">
            <svg viewBox="0 0 24 24" aria-hidden="true">
              <path
                d="M12 3 20 7.5v9L12 21 4 16.5v-9Z"
                fill="none"
                stroke="#063238"
                stroke-width="1.6"
              />
              <path
                d="M12 3v9l8-4.5M12 12 4 7.5M12 12v9"
                stroke="#06b6d4"
                stroke-width="1.6"
                stroke-linecap="round"
              />
            </svg>
          </div>
          <div class="platform-icon pi-facebook">
            <svg viewBox="0 0 24 24" aria-hidden="true">
              <path
                d="M4 14v-4M8 17V7M12 19V5M16 16V8M20 13v-2"
                stroke="#063238"
                stroke-width="2.4"
                stroke-linecap="round"
              />
              <path
                d="M8 17V7M16 16V8"
                stroke="#06b6d4"
                stroke-width="2.4"
                stroke-linecap="round"
              />
            </svg>
          </div>
          <div class="platform-icon pi-twitter">
            <span class="icon-type icon-small">CAP</span>
          </div>
          <div class="platform-icon pi-linkedin">
            <span class="icon-type icon-small">MP4</span>
          </div>
          <div class="platform-icon pi-yelp">
            <span class="icon-type icon-small">PUB</span>
          </div>
        </div>
        <!-- /.mockup-cluster -->

        <!-- ===== Phase 3-4: Avatar + task bubbles ===== -->
        <div class="avatar-with-bubbles" id="avatar-bubbles">
          <div class="avatar-stage" id="avatar-stage">
            <div class="avatar-ring">
              <div class="avatar-disc"></div>
            </div>
            <div class="orbit-dot" id="orbit-0"></div>
            <div class="orbit-dot" id="orbit-1"></div>
            <div class="orbit-dot" id="orbit-2"></div>
            <div class="orbit-dot" id="orbit-3"></div>
          </div>
          <!-- task bubbles appended by JS -->
        </div>
      </div>
      <!-- /.scene -->

      <div class="vignette"></div>
    </div>

    <script>
      /* ================================================================
         CONSTANTS
         ================================================================ */
      const W = 1920,
        H = 1080;
      const MOCKUP_CENTER_W = 320; // .mockup-center intrinsic width (matches CSS)
      const MOCKUP_CENTER_H = 650;
      const AVATAR_DIAMETER = 220;
      const MORPH_END_SCALE_X = AVATAR_DIAMETER / MOCKUP_CENTER_W; // ≈ 0.6875
      const MORPH_END_SCALE_Y = AVATAR_DIAMETER / MOCKUP_CENTER_H; // ≈ 0.3385

      const TIMING = {
        // Phase 1 — center stack → side-phone fan-out
        mockupCenterAt: 0.08,
        mockupLeftAt: 0.2,
        mockupRightAt: 0.28,
        mockupEntryDur: 0.7,
        mockupFanDur: 1.08,

        // Phase 2 — icon entries
        iconsAt: 0.33,
        iconEntryDur: 0.45,
        iconStagger: 0.07,

        // Phase 3 — morph
        morphAt: 3.2,
        morphDur: 0.6,

        // Avatar (pop concurrent with morph; layer opacity in at 50% of morph)
        avatarPopAt: 3.2,
        avatarPopDur: 0.55,
        avatarLayerInAt: 3.5,
        avatarLayerInDur: 0.3,

        // Phase 4 — bubbles
        bubblesAt: 3.53,
        bubbleDur: 0.45,
        bubbleStagger: 0.07,
      };

      const BUBBLE_TASKS = [
        { label: "Missing data-duration", angle: 270, dx: 14, dy: -8 }, // top
        { label: "Timeline not registered", angle: 315, dx: 122, dy: 63 }, // top-right
        { label: "Same-track overlaps", angle: 0, dx: 86, dy: 63 }, // right
        { label: "Text overflow at 5s", angle: 45, dx: 79, dy: -9 }, // bottom-right
        { label: "Asset path mismatch", angle: 90 }, // bottom
        { label: "Non-deterministic motion", angle: 135, dx: -124, dy: -32 }, // bottom-left
        { label: "Sub-comp wiring drift", angle: 180, dx: -79, dy: 1 }, // left
        { label: "Render blocked by console", angle: 225, dx: -128, dy: 45 }, // top-left
      ];
      const BUBBLE_RADIUS = 420; // px from screen center
      const BUBBLE_CENTER_X = W / 2;
      const BUBBLE_CENTER_Y = H / 2;

      /* ================================================================
         INITIAL STATE (GSAP owns the transform from here on)
         ================================================================ */
      // All phones start stacked at center, then the side phones fan out like the Fitness showcase.
      gsap.set(".mockup-left", {
        xPercent: -50,
        yPercent: -50,
        x: 0,
        y: 0,
        rotation: 0,
        rotationY: 0,
        scale: 0.86,
        opacity: 0,
      });
      gsap.set(".mockup-right", {
        xPercent: -50,
        yPercent: -50,
        x: 0,
        y: 0,
        rotation: 0,
        rotationY: 0,
        scale: 0.86,
        opacity: 0,
      });
      gsap.set(".mockup-center", {
        xPercent: -50,
        yPercent: -50,
        x: 0,
        y: 0,
        rotation: 0,
        rotationY: 0,
        scale: 0.84,
        opacity: 0,
      });

      gsap.set(".platform-icon", { scale: 0, opacity: 0 });

      // Avatar — sits centered. xPercent/yPercent centering, pop starts at scale 0.
      gsap.set(".avatar-stage", { xPercent: -50, yPercent: -50, scale: 0 });

      /* Build task bubbles deterministically at script load.
         Positions baked into inline style; GSAP only tweens scale/opacity. */
      const stage = document.getElementById("avatar-bubbles");
      BUBBLE_TASKS.forEach((task, i) => {
        const rad = (task.angle * Math.PI) / 180;
        const x = BUBBLE_CENTER_X + Math.cos(rad) * BUBBLE_RADIUS + (task.dx || 0);
        const y = BUBBLE_CENTER_Y + Math.sin(rad) * BUBBLE_RADIUS + (task.dy || 0);
        const el = document.createElement("div");
        el.className = "task-bubble";
        el.id = "bubble-" + i;
        el.style.left = x + "px";
        el.style.top = y + "px";
        el.textContent = task.label;
        stage.appendChild(el);
        // GSAP-owned centering + initial hidden state.
        gsap.set(el, { xPercent: -50, yPercent: -50, scale: 0, opacity: 0 });
      });

      /* ================================================================
         TIMELINE
         ================================================================ */
      window.__timelines = window.__timelines || {};
      const tl = gsap.timeline({ paused: true });
      window.__timelines["main"] = tl;

      /* ---------------------------------------------------------------
         PHASE 1: Mockup entries (center stack → fan-out)
         spring(70, 14) → back.out(1.4)
         --------------------------------------------------------------- */
      tl.to(
        ".mockup-center",
        { opacity: 1, scale: 1.0, duration: TIMING.mockupEntryDur, ease: "back.out(1.4)" },
        TIMING.mockupCenterAt,
      );
      tl.to(
        ".mockup-left",
        { opacity: 1, duration: TIMING.mockupEntryDur, ease: "back.out(1.4)" },
        TIMING.mockupLeftAt,
      );
      tl.to(
        ".mockup-left",
        {
          x: -370,
          rotation: -10,
          rotationY: 6,
          scale: 0.92,
          duration: TIMING.mockupFanDur,
          ease: "expo.out",
        },
        TIMING.mockupLeftAt,
      );
      tl.to(
        ".mockup-right",
        { opacity: 1, duration: TIMING.mockupEntryDur, ease: "back.out(1.4)" },
        TIMING.mockupRightAt,
      );
      tl.to(
        ".mockup-right",
        {
          x: 370,
          rotation: 10,
          rotationY: -6,
          scale: 0.92,
          duration: TIMING.mockupFanDur,
          ease: "expo.out",
        },
        TIMING.mockupRightAt,
      );

      /* ---------------------------------------------------------------
         PHASE 2: Platform icons (staggered)
         spring(180, 14) → back.out(1.6)
         --------------------------------------------------------------- */
      tl.to(
        ".platform-icon",
        {
          scale: 1,
          opacity: 1,
          duration: TIMING.iconEntryDur,
          ease: "back.out(1.6)",
          stagger: { each: TIMING.iconStagger, from: "start" },
        },
        TIMING.iconsAt,
      );

      /* ---------------------------------------------------------------
         PHASE 3: MORPH (the core glue)
         spring(80, 18) → power3.out
         --------------------------------------------------------------- */
      // 1. Center mockup compositor-scales toward the avatar footprint.
      tl.to(
        ".mockup-center",
        {
          scaleX: MORPH_END_SCALE_X,
          scaleY: MORPH_END_SCALE_Y,
          duration: TIMING.morphDur,
          ease: "power3.out",
        },
        TIMING.morphAt,
      );

      // 2. borderRadius repaint — reads as a circle by morph end.
      tl.to(
        ".mockup-center",
        {
          borderRadius: "50%",
          duration: TIMING.morphDur,
          ease: "power3.out",
        },
        TIMING.morphAt,
      );

      // 3. Surface shifts: black card → cyan-gradient glow.
      tl.to(
        ".mockup-center",
        {
          background: "linear-gradient(135deg, #14B8A6, #06B6D4, #0EA5E9)",
          boxShadow: "0 0 50px rgba(20, 184, 166, 0.50), 0 0 100px rgba(6, 182, 212, 0.25)",
          duration: TIMING.morphDur,
          ease: "power3.out",
        },
        TIMING.morphAt,
      );

      // 4. Content fades during first 40% of morph (hides aspect-ratio mismatch).
      tl.to(
        ".mockup-center .mockup-content",
        {
          opacity: 0,
          duration: TIMING.morphDur * 0.4,
          ease: "power2.out",
        },
        TIMING.morphAt,
      );

      // 4b. Fade the physical phone shell early so the morph reads as a clean surface hand-off.
      tl.to(
        ".mockup-center .phone-shell",
        {
          opacity: 0,
          duration: TIMING.morphDur * 0.45,
          ease: "power2.out",
        },
        TIMING.morphAt,
      );

      // 5. Hand-off: at 85-100% of morph, container fades to 0 → avatar reveals underneath.
      tl.to(
        ".mockup-center",
        {
          opacity: 0,
          duration: TIMING.morphDur * 0.15,
          ease: "none",
        },
        TIMING.morphAt + TIMING.morphDur * 0.85,
      );

      /* Non-center mockups exit concurrently with morph */
      tl.to(
        [".mockup-left", ".mockup-right"],
        {
          opacity: 0,
          scale: 0.85,
          duration: TIMING.morphDur * 0.55,
          ease: "power2.out",
        },
        TIMING.morphAt,
      );

      /* Platform icons exit concurrently with morph (edge-out stagger) */
      tl.to(
        ".platform-icon",
        {
          opacity: 0,
          scale: 0.85,
          duration: TIMING.morphDur * 0.5,
          ease: "power2.out",
          stagger: { each: 0.025, from: "edges" },
        },
        TIMING.morphAt,
      );

      /* Avatar pop — spring(120, 14) → back.out(1.4), concurrent with morph trigger */
      tl.fromTo(
        ".avatar-stage",
        { scale: 0 },
        { scale: 1, duration: TIMING.avatarPopDur, ease: "back.out(1.4)" },
        TIMING.avatarPopAt,
      );

      /* Avatar layer opacity in at 50% of morph */
      tl.to(
        "#avatar-bubbles",
        {
          opacity: 1,
          duration: TIMING.avatarLayerInDur,
          ease: "power2.out",
        },
        TIMING.avatarLayerInAt,
      );

      /* ---------------------------------------------------------------
         PHASE 4: Task bubbles (staggered radial entry)
         spring(180, 12) → back.out(1.4)
         --------------------------------------------------------------- */
      tl.to(
        ".task-bubble",
        {
          scale: 1,
          opacity: 0.95,
          duration: TIMING.bubbleDur,
          ease: "back.out(1.4)",
          stagger: { each: TIMING.bubbleStagger, from: "start" },
        },
        TIMING.bubblesAt,
      );

      /* ---------------------------------------------------------------
         CONTINUOUS MOTION (shared scene-ticker onUpdate)
         Reads tl.time() each frame; cheaper than N independent onUpdates.
         Drives:
           - mockup floating (Phase 1-2 only — gated to t < morphAt)
           - platform-icon floating (same)
           - orbit dots around avatar
           - avatar breath (multiplicative on pop scale)
           - bubble micro-float (after bubblesAt)
         --------------------------------------------------------------- */
      const orbitDots = [
        document.getElementById("orbit-0"),
        document.getElementById("orbit-1"),
        document.getElementById("orbit-2"),
        document.getElementById("orbit-3"),
      ];
      const ORBIT_RADIUS = 130;
      const ORBIT_SPEED = 2; // degrees per timeline-time unit (i.e. per second × 30)

      const avatarStage = document.getElementById("avatar-stage");
      const bubbleEls = Array.from(document.querySelectorAll(".task-bubble"));
      const mockupLeft = document.querySelector(".mockup-left");
      const mockupRight = document.querySelector(".mockup-right");
      const platformIcons = Array.from(document.querySelectorAll(".platform-icon"));

      tl.to(
        { tick: 0 },
        {
          tick: 1,
          duration: 6.0,
          ease: "none",
          onUpdate: function () {
            const t = tl.time();

            /* Mockup + icon floating (only while they're visible — pre-morph) */
            if (t < TIMING.morphAt) {
              const mlY = Math.sin(t * 0.36) * 3;
              const mrY = Math.sin(t * 0.45 + 1) * 3;
              gsap.set(mockupLeft, { y: mlY });
              gsap.set(mockupRight, { y: mrY });

              platformIcons.forEach((el, i) => {
                const fx = Math.sin((t * 30 + i * 30) * 0.025) * 5;
                const fy = Math.cos((t * 30 + i * 20) * 0.03) * 4;
                gsap.set(el, { x: fx, y: fy });
              });
            }

            /* Avatar breath (multiplicative on pop scale, only after pop completes) */
            const popEnd = TIMING.avatarPopAt + TIMING.avatarPopDur;
            if (t >= popEnd) {
              const idleT = t - popEnd;
              const breath = 1 + Math.sin(idleT * 0.72) * 0.03;
              // popScale at this point is 1.0 (back.out settled). Multiplicative → still 1.0 × breath.
              gsap.set(avatarStage, { scale: 1 * breath });
            }

            /* Orbit dots — always cycling once avatar layer is visible */
            if (t >= TIMING.avatarLayerInAt) {
              orbitDots.forEach((dot, i) => {
                const angle = (t * 30 * ORBIT_SPEED + i * 90) * (Math.PI / 180);
                const x = Math.cos(angle) * ORBIT_RADIUS;
                const y = Math.sin(angle) * ORBIT_RADIUS;
                const op = 0.7 + Math.sin(t * 30 * 0.1 + i) * 0.3;
                gsap.set(dot, { x: x, y: y, opacity: op });
              });
            }

            /* Bubble micro-float (after they enter) */
            if (t >= TIMING.bubblesAt + TIMING.bubbleDur) {
              bubbleEls.forEach((el, i) => {
                const fx = Math.sin((t * 30 + i * 24) * 0.015) * 5;
                const fy = Math.cos((t * 30 + i * 30) * 0.018) * 4;
                gsap.set(el, { x: fx, y: fy });
              });
            }
          },
        },
        0,
      );
    </script>
  </body>
</html>