{
  "id": "mui_flux_hero",
  "name": "Flux Hero",
  "width": 1200,
  "height": 760,
  "cornerRadius": 0,
  "fit": "Layout",
  "backgroundColor": "#000000",
  "opacity": 1,
  "x": -600,
  "y": -380,
  "responsive": {
    "enabled": true,
    "mode": "constraint",
    "width": {
      "type": "fixed"
    },
    "height": {
      "type": "fixed"
    },
    "maintainAspectRatio": false,
    "scaleObjects": false,
    "breakpoints": [
      {
        "id": "bp_mobile",
        "name": "Mobile",
        "minWidth": 0,
        "maxWidth": 599
      },
      {
        "id": "bp_tablet",
        "name": "Tablet",
        "minWidth": 600,
        "maxWidth": 1023
      },
      {
        "id": "bp_desktop",
        "name": "Desktop",
        "minWidth": 1024
      }
    ]
  },
  "layoutProps": {
    "mode": "linear",
    "direction": "vertical",
    "gap": 0,
    "padding": {
      "top": 0,
      "right": 0,
      "bottom": 0,
      "left": 0
    },
    "alignItems": "stretch",
    "layoutSizingHorizontal": "fixed",
    "layoutSizingVertical": "fixed"
  },
  "seo": {
    "title": "Flux Hero — Exode Motion UI",
    "description": "A responsive full hero block with living rotating fluid gradient mesh, glowing badge, dual action buttons, and a 3D perspective dashboard preview card that reacts to cursor movement."
  },
  "stateMachine": {
    "inputs": [
      {
        "id": "in_badge",
        "name": "badge",
        "value": {
          "type": "Text",
          "value": "✦ EXODE 2.0 IS LIVE",
          "defaultValue": "✦ EXODE 2.0 IS LIVE"
        }
      },
      {
        "id": "in_headline",
        "name": "headline",
        "value": {
          "type": "Text",
          "value": "Craft Living Interfaces at the Speed of Thought",
          "defaultValue": "Craft Living Interfaces at the Speed of Thought"
        }
      },
      {
        "id": "in_sub",
        "name": "sub",
        "value": {
          "type": "Text",
          "value": "The next-generation motion design environment. Zero video bloat, sub-millisecond execution, complete state-machine control.",
          "defaultValue": "The next-generation motion design environment. Zero video bloat, sub-millisecond execution, complete state-machine control."
        }
      },
      {
        "id": "in_primaryCta",
        "name": "primaryCta",
        "value": {
          "type": "Text",
          "value": "Start Creating Free",
          "defaultValue": "Start Creating Free"
        }
      },
      {
        "id": "in_secondaryCta",
        "name": "secondaryCta",
        "value": {
          "type": "Text",
          "value": "Explore Showcase",
          "defaultValue": "Explore Showcase"
        }
      },
      {
        "id": "in_palette",
        "name": "palette",
        "value": {
          "type": "Number",
          "value": 0,
          "defaultValue": 0
        }
      },
      {
        "id": "in_speed",
        "name": "speed",
        "value": {
          "type": "Number",
          "value": 1,
          "defaultValue": 1
        }
      },
      {
        "id": "in_glow",
        "name": "glow",
        "value": {
          "type": "Number",
          "value": 1,
          "defaultValue": 1
        }
      },
      {
        "id": "in_accent",
        "name": "accent",
        "value": {
          "type": "Text",
          "value": "#2868ff",
          "defaultValue": "#2868ff"
        }
      }
    ],
    "layers": [
      {
        "name": "Base",
        "entryStateId": "s_idle",
        "states": [
          {
            "id": "s_idle",
            "name": "Idle",
            "loop": true,
            "transitions": []
          }
        ]
      }
    ],
    "logicNodes": []
  },
  "animations": [],
  "physics": {
    "enabled": false,
    "gravity": {
      "x": 0,
      "y": 9.8
    }
  },
  "animationFlow": [],
  "navigation": [],
  "stateVariables": [],
  "objects": [
    {
      "id": "stage",
      "name": "Stage",
      "type": "Group",
      "transform": {
        "x": 0,
        "y": 0,
        "rotation": 0,
        "scale_x": 1,
        "scale_y": 1,
        "opacity": 1,
        "skew_x": 0,
        "skew_y": 0
      },
      "width": 1200,
      "height": 760,
      "geometry": {
        "type": "Rectangle",
        "width": 1200,
        "height": 760,
        "cornerRadius": 0
      },
      "cornerRadius": 0,
      "children": [],
      "clipContent": true,
      "style": {
        "fill": {
          "type": "Solid",
          "color": "#000000",
          "opacity": 1
        },
        "effects": [
          {
            "id": "fx_flux-hero",
            "type": "CustomEffect",
            "name": "Flux Hero",
            "isEnabled": true,
            "params": {},
            "code": "/* Shared by every motion-ui effect (prepended to each one).\n   Rules for this file and the effects: the .bexode compiler minifies effect\n   code, so use explicit semicolons, block comments only, and never write a\n   minus followed by a negative value (it collapses to a decrement). */\n\n/* Read a live state-machine input, falling back to a default. */\nfunction inp(env, name, fallback) {\n  const v = env.inputs ? env.inputs[name] : undefined;\n  if (v === undefined || v === null || v === '') return fallback;\n  if (typeof fallback === 'number') {\n    const n = Number(v);\n    return isFinite(n) ? n : fallback;\n  }\n  if (typeof fallback === 'boolean') return v === true || v === 1 || v === 'true';\n  return v;\n}\n\n/* '#rgb' / '#rrggbb' to [r, g, b]; anything else falls back to white. */\nfunction rgbOf(hex) {\n  let h = String(hex || '').trim().replace('#', '');\n  if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];\n  if (!/^[0-9a-fA-F]{6}$/.test(h)) return [255, 255, 255];\n  const n = parseInt(h, 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\n\nfunction rgba(hex, a) {\n  const c = rgbOf(hex);\n  return 'rgba(' + c[0] + ',' + c[1] + ',' + c[2] + ',' + Math.max(0, Math.min(1, a)) + ')';\n}\n\nfunction mix(a, b, t) {\n  const x = rgbOf(a), y = rgbOf(b);\n  const r = Math.round(x[0] + (y[0] - x[0]) * t);\n  const g = Math.round(x[1] + (y[1] - x[1]) * t);\n  const bl = Math.round(x[2] + (y[2] - x[2]) * t);\n  return '#' + ((1 << 24) + (r << 16) + (g << 8) + bl).toString(16).slice(1);\n}\n\n/* A soft round glow, rendered once per colour and blitted with drawImage\n   (much cheaper than shadowBlur on every particle). */\nconst __glows = {};\nfunction glowSprite(hex) {\n  if (__glows[hex]) return __glows[hex];\n  const s = 64;\n  const c = document.createElement('canvas');\n  c.width = s; c.height = s;\n  const g = c.getContext('2d');\n  const gr = g.createRadialGradient(s / 2, s / 2, 0, s / 2, s / 2, s / 2);\n  gr.addColorStop(0, rgba(hex, 1));\n  gr.addColorStop(0.18, rgba(hex, 0.55));\n  gr.addColorStop(0.45, rgba(hex, 0.14));\n  gr.addColorStop(1, rgba(hex, 0));\n  g.fillStyle = gr;\n  g.fillRect(0, 0, s, s);\n  __glows[hex] = c;\n  return c;\n}\n\nfunction glow(ctx, hex, x, y, r, a) {\n  if (r <= 0 || a <= 0) return;\n  ctx.globalAlpha = Math.min(1, a);\n  ctx.drawImage(glowSprite(hex), x - r, y - r, r * 2, r * 2);\n  ctx.globalAlpha = 1;\n}\n\n/* Frame clock: seconds since start and a clamped delta. */\nfunction tick(S) {\n  const now = performance.now();\n  if (!S.t0) { S.t0 = now; S.last = now; }\n  S.dt = Math.min(0.05, Math.max(0.001, (now - S.last) / 1000));\n  S.last = now;\n  S.age = (now - S.t0) / 1000;\n  return S.dt;\n}\n\n/* Deterministic pseudo-random so layouts are stable across resizes. */\nfunction rng(seed) {\n  let s = seed >>> 0 || 1;\n  return function () {\n    s = (s * 1664525 + 1013904223) >>> 0;\n    return s / 4294967296;\n  };\n}\n\nfunction ease(x) { return x < 0.5 ? 4 * x * x * x : 1 + (Math.pow(2 * x - 2, 3)) / 2; }\nfunction clamp(v, a, b) { return v < a ? a : v > b ? b : v; }\n\nconst SANS = 'Inter, Helvetica, Arial, sans-serif';\nconst MONO = 'ui-monospace, Menlo, monospace';\n\nfunction roundRect(ctx, x, y, w, h, r) {\n  r = Math.min(r, w / 2, h / 2);\n  ctx.beginPath();\n  ctx.moveTo(x + r, y);\n  ctx.arcTo(x + w, y, x + w, y + h, r);\n  ctx.arcTo(x + w, y + h, x, y + h, r);\n  ctx.arcTo(x, y + h, x, y, r);\n  ctx.arcTo(x, y, x + w, y, r);\n  ctx.closePath();\n}\n\n/* Shared backdrop: flat colour, soft vignette and fine grain. */\nfunction backdrop(ctx, W, H, bg, tint, amt) {\n  ctx.fillStyle = bg;\n  ctx.fillRect(0, 0, W, H);\n  if (tint && amt > 0) {\n    const gr = ctx.createRadialGradient(W / 2, H * 0.45, 0, W / 2, H * 0.45, Math.max(W, H) * 0.7);\n    gr.addColorStop(0, rgba(tint, 0.10 * amt));\n    gr.addColorStop(1, rgba(tint, 0));\n    ctx.fillStyle = gr;\n    ctx.fillRect(0, 0, W, H);\n  }\n  const vg = ctx.createRadialGradient(W / 2, H / 2, Math.min(W, H) * 0.3, W / 2, H / 2, Math.max(W, H) * 0.8);\n  vg.addColorStop(0, 'rgba(0,0,0,0)');\n  vg.addColorStop(1, 'rgba(0,0,0,0.45)');\n  ctx.fillStyle = vg;\n  ctx.fillRect(0, 0, W, H);\n}\n\n/* Greedy word wrap with the current ctx.font. */\nfunction wrap(ctx, text, maxW) {\n  const out = [];\n  String(text).split('\\n').forEach(function (para) {\n    const words = para.split(' ');\n    let cur = '';\n    for (let i = 0; i < words.length; i++) {\n      const next = cur ? cur + ' ' + words[i] : words[i];\n      if (cur && ctx.measureText(next).width > maxW) { out.push(cur); cur = words[i]; } else cur = next;\n    }\n    out.push(cur);\n  });\n  return out;\n}\n\n/* Largest font size (up to max) at which text wraps into maxLines within maxW. */\nfunction fitFont(ctx, text, weight, family, maxW, maxLines, max, min) {\n  let fs = max;\n  while (fs > min) {\n    ctx.font = weight + ' ' + fs + 'px ' + family;\n    if (wrap(ctx, text, maxW).length <= maxLines) break;\n    fs = fs * 0.92;\n  }\n  ctx.font = weight + ' ' + fs + 'px ' + family;\n  return fs;\n}\n\n/* Smoothed pointer that idles along a slow figure-eight when the cursor is away. */\nfunction follow(S, env, W, H, k) {\n  const pt = env.pointer || {};\n  const t = S.age || 0;\n  const tx = pt.isHovered ? pt.localX : W / 2 + Math.sin(t * 0.37) * W * 0.3;\n  const ty = pt.isHovered ? pt.localY : H / 2 + Math.sin(t * 0.74) * H * 0.22;\n  if (S.px === undefined) { S.px = tx; S.py = ty; }\n  S.px += (tx - S.px) * Math.min(1, S.dt * (k || 6));\n  S.py += (ty - S.py) * Math.min(1, S.dt * (k || 6));\n  S.inside = !!pt.isHovered;\n  return pt;\n}\n\n/* Optional centred headline over a background effect. */\nfunction title(ctx, W, H, text, sub, ink, accent, bg) {\n  if (!text) return;\n  if (bg) {\n    /* Soft scrim so the headline reads over busy motion. */\n    const sc = ctx.createRadialGradient(W / 2, H / 2, 0, W / 2, H / 2, Math.min(W * 0.5, 560));\n    sc.addColorStop(0, rgba(bg, 0.72));\n    sc.addColorStop(1, rgba(bg, 0));\n    ctx.fillStyle = sc;\n    ctx.fillRect(0, 0, W, H);\n  }\n  ctx.textAlign = 'center';\n  ctx.textBaseline = 'alphabetic';\n  const maxW = Math.min(W * 0.86, 900);\n  const fs = fitFont(ctx, text, 600, SANS, maxW, 2, clamp(W * 0.065, 26, 72), 16);\n  const lines = wrap(ctx, text, maxW);\n  const lh = fs * 1.08;\n  const y0 = H / 2 - (lines.length * lh) / 2 + fs * 0.8 - (sub ? fs * 0.35 : 0);\n  ctx.fillStyle = ink;\n  lines.forEach(function (l, i) { ctx.fillText(l, W / 2, y0 + i * lh); });\n  if (sub) {\n    ctx.font = '500 ' + clamp(fs * 0.24, 11, 15) + 'px ' + MONO;\n    ctx.fillStyle = rgba(accent, 0.85);\n    ctx.fillText(String(sub).toUpperCase(), W / 2, y0 + (lines.length - 1) * lh + fs * 0.9);\n  }\n}\n\n/* Flux Hero: a full interactive hero block inspired by reactvibe.com.\n   Features a rotating fluid gradient mesh backdrop, floating ambient orbs,\n   pill badge, large headline, subline, dual responsive buttons with hover/press\n   physics, and a perspective-tilted floating dashboard preview card that\n   reacts dynamically to cursor coordinates. */\nlet S = null;\n\nconst PALETTES = [\n  ['#2868ff', '#4f80ff', '#779fff', '#1a3fa8'], /* Blue */\n  ['#5e17eb', '#8b5cff', '#c1a6ff', '#360999'], /* Purple */\n  ['#00796b', '#26a69a', '#80cbc4', '#004d40'], /* Teal */\n  ['#ff6b00', '#ffa726', '#ffd180', '#b34700'], /* Orange */\n  ['#007bff', '#00c6ff', '#80d8ff', '#0056b3'], /* Sky */\n];\n\nfunction render(ctx, env) {\n  const W = env.width, H = env.height;\n  if (!S) {\n    S = {\n      angle: 0,\n      badgeHover: 0,\n      btn1Hover: 0,\n      btn1Press: 0,\n      btn2Hover: 0,\n      btn2Press: 0,\n      cardTiltX: 0,\n      cardTiltY: 0,\n      cardFloat: 0,\n      wasDown: false,\n      ripples: [],\n    };\n  }\n  const dt = tick(S);\n\n  const badgeText = String(inp(env, 'badge', '✦ EXODE 2.0 IS LIVE'));\n  const heading = String(inp(env, 'headline', 'Craft Living Interfaces at the Speed of Thought'));\n  const sub = String(inp(env, 'sub', 'The next-generation motion design environment. Zero video bloat, sub-millisecond execution, complete state-machine control.'));\n  const btn1Label = String(inp(env, 'primaryCta', 'Start Creating Free'));\n  const btn2Label = String(inp(env, 'secondaryCta', 'Explore Showcase'));\n  const palIdx = Math.floor(Math.abs(inp(env, 'palette', 0))) % PALETTES.length;\n  const speed = clamp(inp(env, 'speed', 1), 0.1, 4);\n  const glowIntensity = clamp(inp(env, 'glow', 1), 0, 2);\n  const customAccent = inp(env, 'accent', '');\n\n  S.angle = (S.angle + dt * 22 * speed) % 360;\n  S.cardFloat = (S.cardFloat + dt * 1.4 * speed) % (Math.PI * 2);\n\n  const curPal = PALETTES[palIdx];\n  const c1 = customAccent || curPal[0];\n  const c2 = curPal[1];\n  const c3 = curPal[2];\n  const cDark = curPal[3];\n\n  ctx.save();\n  ctx.translate(-W / 2, -H / 2);\n\n  /* Deep Backdrop */\n  ctx.fillStyle = '#06070a';\n  ctx.fillRect(0, 0, W, H);\n\n  /* Rotating Atmospheric Linear Gradient */\n  const rad = (S.angle * Math.PI) / 180;\n  const cx = W / 2, cy = H * 0.42;\n  const diag = Math.sqrt(W * W + H * H);\n\n  const x0 = cx - Math.cos(rad) * (diag * 0.45);\n  const y0 = cy - Math.sin(rad) * (diag * 0.45);\n  const x1 = cx + Math.cos(rad) * (diag * 0.45);\n  const y1 = cy + Math.sin(rad) * (diag * 0.45);\n\n  const bgGrad = ctx.createLinearGradient(x0, y0, x1, y1);\n  bgGrad.addColorStop(0, rgba(cDark, 0.85));\n  bgGrad.addColorStop(0.35, rgba(c1, 0.55));\n  bgGrad.addColorStop(0.7, rgba(c2, 0.45));\n  bgGrad.addColorStop(1, rgba(c3, 0.70));\n\n  ctx.fillStyle = bgGrad;\n  ctx.fillRect(0, 0, W, H);\n\n  /* Fluid Ambient Floating Orbs */\n  if (glowIntensity > 0) {\n    const orb1X = cx + Math.sin(S.age * 0.65 * speed) * (W * 0.3);\n    const orb1Y = cy + Math.cos(S.age * 0.55 * speed) * (H * 0.2);\n    glow(ctx, c1, orb1X, orb1Y, Math.min(W, H) * 0.5 * glowIntensity, 0.55);\n\n    const orb2X = cx - Math.cos(S.age * 0.5 * speed) * (W * 0.35);\n    const orb2Y = cy - Math.sin(S.age * 0.75 * speed) * (H * 0.22);\n    glow(ctx, c3, orb2X, orb2Y, Math.min(W, H) * 0.45 * glowIntensity, 0.45);\n  }\n\n  /* Vignette scrim for readability */\n  const vig = ctx.createRadialGradient(cx, cy, Math.min(W, H) * 0.25, cx, cy, diag * 0.65);\n  vig.addColorStop(0, 'rgba(0,0,0,0)');\n  vig.addColorStop(0.7, 'rgba(6,7,10,0.6)');\n  vig.addColorStop(1, 'rgba(6,7,10,0.92)');\n  ctx.fillStyle = vig;\n  ctx.fillRect(0, 0, W, H);\n\n  /* Subtle background grid lines */\n  ctx.save();\n  ctx.strokeStyle = 'rgba(255, 255, 255, 0.035)';\n  ctx.lineWidth = 1;\n  const gridStep = Math.max(36, Math.min(60, W / 24));\n  for (let gx = 0; gx < W; gx += gridStep) {\n    ctx.beginPath();\n    ctx.moveTo(gx, 0);\n    ctx.lineTo(gx, H);\n    ctx.stroke();\n  }\n  for (let gy = 0; gy < H; gy += gridStep) {\n    ctx.beginPath();\n    ctx.moveTo(0, gy);\n    ctx.lineTo(W, gy);\n    ctx.stroke();\n  }\n  ctx.restore();\n\n  /* Pointer tracking & Card tilt smoothing */\n  const pt = env.pointer || {};\n  const isMobile = W < 640;\n  const isTablet = W >= 640 && W < 1024;\n  const px = pt.isHovered ? pt.localX : cx;\n  const py = pt.isHovered ? pt.localY : cy;\n\n  const targetTiltX = pt.isHovered ? ((px - cx) / cx) * 12 : 0;\n  const targetTiltY = pt.isHovered ? ((py - cy) / cy) * 10 : 0;\n  S.cardTiltX += (targetTiltX - S.cardTiltX) * Math.min(1, dt * 6);\n  S.cardTiltY += (targetTiltY - S.cardTiltY) * Math.min(1, dt * 6);\n\n  /* ---------- Content Layout ---------- */\n  let curY = Math.max(30, H * 0.07);\n\n  /* 1. Pill Badge */\n  if (badgeText) {\n    ctx.font = '600 ' + (isMobile ? 11 : 12) + 'px ' + MONO;\n    const badgeW = ctx.measureText(badgeText).width + 32;\n    const badgeH = isMobile ? 26 : 30;\n    const badgeX = cx - badgeW / 2;\n    const badgeY = curY;\n\n    const inBadge = pt.isHovered && px >= badgeX && px <= badgeX + badgeW && py >= badgeY && py <= badgeY + badgeH;\n    S.badgeHover += ((inBadge ? 1 : 0) - S.badgeHover) * Math.min(1, dt * 10);\n\n    /* Badge background with subtle border */\n    ctx.fillStyle = rgba('#ffffff', 0.06 + S.badgeHover * 0.08);\n    roundRect(ctx, badgeX, badgeY, badgeW, badgeH, badgeH / 2);\n    ctx.fill();\n\n    ctx.strokeStyle = rgba(c1, 0.4 + S.badgeHover * 0.4);\n    ctx.lineWidth = 1;\n    roundRect(ctx, badgeX, badgeY, badgeW, badgeH, badgeH / 2);\n    ctx.stroke();\n\n    /* Glowing indicator dot */\n    const dotX = badgeX + 13;\n    const dotY = badgeY + badgeH / 2;\n    ctx.fillStyle = c2;\n    ctx.beginPath();\n    ctx.arc(dotX, dotY, 3, 0, Math.PI * 2);\n    ctx.fill();\n    glow(ctx, c2, dotX, dotY, 10, 0.8);\n\n    ctx.textAlign = 'left';\n    ctx.textBaseline = 'middle';\n    ctx.fillStyle = '#ffffff';\n    ctx.fillText(badgeText, badgeX + 22, badgeY + badgeH / 2);\n\n    curY += badgeH + (isMobile ? 16 : 22);\n  }\n\n  /* 2. Main Headline */\n  const titleSize = clamp(W * 0.046, isMobile ? 24 : 34, 56);\n  ctx.font = '800 ' + titleSize + 'px ' + SANS;\n  ctx.textAlign = 'center';\n  ctx.textBaseline = 'top';\n\n  const maxTextW = Math.min(W * 0.9, 960);\n  const titleLines = wrap(ctx, heading, maxTextW);\n  const lineH = titleSize * 1.14;\n\n  titleLines.forEach(function (line, idx) {\n    const ly = curY + idx * lineH;\n    /* Gradient text fill */\n    const textGrad = ctx.createLinearGradient(cx - maxTextW * 0.4, ly, cx + maxTextW * 0.4, ly);\n    textGrad.addColorStop(0, '#ffffff');\n    textGrad.addColorStop(0.55, '#f0f3fa');\n    textGrad.addColorStop(1, mix('#ffffff', c2, 0.45));\n    ctx.fillStyle = textGrad;\n    ctx.fillText(line, cx, ly);\n  });\n\n  curY += titleLines.length * lineH + (isMobile ? 12 : 18);\n\n  /* 3. Subtitle */\n  const subSize = clamp(W * 0.018, isMobile ? 13 : 15, 18);\n  ctx.font = '400 ' + subSize + 'px ' + SANS;\n  const subLines = wrap(ctx, sub, Math.min(W * 0.85, 780));\n  const subLineH = subSize * 1.45;\n\n  ctx.fillStyle = 'rgba(235, 240, 255, 0.78)';\n  subLines.forEach(function (line, idx) {\n    ctx.fillText(line, cx, curY + idx * subLineH);\n  });\n\n  curY += subLines.length * subLineH + (isMobile ? 22 : 30);\n\n  /* 4. Action Buttons (Primary & Secondary) */\n  const btnH = isMobile ? 44 : 50;\n  const btn1W = Math.max(isMobile ? 150 : 180, ctx.measureText(btn1Label).width + 50);\n  const btn2W = Math.max(isMobile ? 140 : 170, ctx.measureText(btn2Label).width + 46);\n  const btnGap = isMobile ? 12 : 16;\n  const totalBtnsW = isMobile ? W * 0.88 : btn1W + btnGap + btn2W;\n\n  let btn1X, btn1Y, btn2X, btn2Y;\n  if (isMobile) {\n    btn1X = cx - totalBtnsW / 2;\n    btn1Y = curY;\n    btn2X = cx - totalBtnsW / 2;\n    btn2Y = curY + btnH + btnGap;\n    curY += btnH * 2 + btnGap + 24;\n  } else {\n    btn1X = cx - (btn1W + btnGap + btn2W) / 2;\n    btn1Y = curY;\n    btn2X = btn1X + btn1W + btnGap;\n    btn2Y = curY;\n    curY += btnH + 34;\n  }\n\n  const inBtn1 = pt.isHovered && px >= btn1X && px <= (btn1X + (isMobile ? totalBtnsW : btn1W)) && py >= btn1Y && py <= btn1Y + btnH;\n  const inBtn2 = pt.isHovered && px >= btn2X && px <= (btn2X + (isMobile ? totalBtnsW : btn2W)) && py >= btn2Y && py <= btn2Y + btnH;\n\n  S.btn1Hover += ((inBtn1 ? 1 : 0) - S.btn1Hover) * Math.min(1, dt * 10);\n  S.btn2Hover += ((inBtn2 ? 1 : 0) - S.btn2Hover) * Math.min(1, dt * 10);\n\n  const isDown = !!(pt.isDown || pt.down);\n  if (inBtn1 && isDown) S.btn1Press = Math.min(1, S.btn1Press + dt * 14);\n  else S.btn1Press = Math.max(0, S.btn1Press - dt * 10);\n\n  if (inBtn2 && isDown) S.btn2Press = Math.min(1, S.btn2Press + dt * 14);\n  else S.btn2Press = Math.max(0, S.btn2Press - dt * 10);\n\n  if (!S.wasDown && isDown) {\n    if (inBtn1) {\n      S.ripples.push({ x: px, y: py, r: 0, alpha: 0.8, color: '#ffffff' });\n      if (typeof env.emit === 'function') env.emit('primaryClick');\n    } else if (inBtn2) {\n      S.ripples.push({ x: px, y: py, r: 0, alpha: 0.8, color: c2 });\n      if (typeof env.emit === 'function') env.emit('secondaryClick');\n    }\n  }\n  S.wasDown = isDown;\n\n  /* Render Primary Button */\n  const b1Scale = 1 + S.btn1Hover * 0.03 - S.btn1Press * 0.04;\n  const b1ActualW = isMobile ? totalBtnsW : btn1W;\n  ctx.save();\n  ctx.translate(btn1X + b1ActualW / 2, btn1Y + btnH / 2);\n  ctx.scale(b1Scale, b1Scale);\n\n  /* Primary glow behind button */\n  glow(ctx, c1, 0, 0, b1ActualW * 0.65, 0.45 * (0.8 + S.btn1Hover * 0.6));\n\n  /* Fluid gradient fill for button */\n  const btnGrad = ctx.createLinearGradient(-b1ActualW / 2, -btnH / 2, b1ActualW / 2, btnH / 2);\n  btnGrad.addColorStop(0, mix(c1, '#ffffff', 0.15));\n  btnGrad.addColorStop(1, mix(c2, cDark, 0.25));\n  ctx.fillStyle = btnGrad;\n  roundRect(ctx, -b1ActualW / 2, -btnH / 2, b1ActualW, btnH, 12);\n  ctx.fill();\n\n  /* Primary button border highlight */\n  ctx.strokeStyle = rgba('#ffffff', 0.35 + S.btn1Hover * 0.35);\n  ctx.lineWidth = 1.2;\n  roundRect(ctx, -b1ActualW / 2, -btnH / 2, b1ActualW, btnH, 12);\n  ctx.stroke();\n\n  /* Label */\n  ctx.font = '600 ' + (isMobile ? 14 : 15) + 'px ' + SANS;\n  ctx.fillStyle = '#ffffff';\n  ctx.textAlign = 'center';\n  ctx.textBaseline = 'middle';\n  ctx.fillText(btn1Label, 0, 0);\n  ctx.restore();\n\n  /* Render Secondary Button */\n  const b2Scale = 1 + S.btn2Hover * 0.03 - S.btn2Press * 0.04;\n  const b2ActualW = isMobile ? totalBtnsW : btn2W;\n  ctx.save();\n  ctx.translate(btn2X + b2ActualW / 2, btn2Y + btnH / 2);\n  ctx.scale(b2Scale, b2Scale);\n\n  /* Secondary Glass Fill */\n  ctx.fillStyle = rgba('#ffffff', 0.05 + S.btn2Hover * 0.08);\n  roundRect(ctx, -b2ActualW / 2, -btnH / 2, b2ActualW, btnH, 12);\n  ctx.fill();\n\n  /* Glass Border */\n  ctx.strokeStyle = rgba('#ffffff', 0.18 + S.btn2Hover * 0.32);\n  ctx.lineWidth = 1.2;\n  roundRect(ctx, -b2ActualW / 2, -btnH / 2, b2ActualW, btnH, 12);\n  ctx.stroke();\n\n  /* Secondary Label */\n  ctx.font = '600 ' + (isMobile ? 14 : 15) + 'px ' + SANS;\n  ctx.fillStyle = '#f0f4ff';\n  ctx.textAlign = 'center';\n  ctx.textBaseline = 'middle';\n  ctx.fillText(btn2Label, 0, 0);\n  ctx.restore();\n\n  /* 5. 3D Floating Mockup Card */\n  const cardW = Math.min(W * 0.88, 920);\n  const cardH = Math.max(160, Math.min(H - curY - 30, 340));\n\n  if (cardH > 140) {\n    const cardX = cx - cardW / 2;\n    const cardY = curY + Math.sin(S.cardFloat) * 6;\n\n    ctx.save();\n    ctx.translate(cx, cardY + cardH / 2);\n    /* Perspective tilt emulation */\n    ctx.transform(1, S.cardTiltY * 0.003, S.cardTiltX * 0.003, 1, 0, 0);\n\n    /* Card Glow */\n    glow(ctx, c1, 0, -cardH * 0.3, cardW * 0.5, 0.35);\n\n    /* Card Base */\n    ctx.fillStyle = 'rgba(13, 15, 23, 0.75)';\n    roundRect(ctx, -cardW / 2, -cardH / 2, cardW, cardH, 18);\n    ctx.fill();\n\n    /* Card Gradient Border */\n    const cardBorderGrad = ctx.createLinearGradient(-cardW / 2, -cardH / 2, cardW / 2, cardH / 2);\n    cardBorderGrad.addColorStop(0, rgba(c1, 0.6));\n    cardBorderGrad.addColorStop(0.5, rgba('#ffffff', 0.15));\n    cardBorderGrad.addColorStop(1, rgba(c3, 0.4));\n    ctx.strokeStyle = cardBorderGrad;\n    ctx.lineWidth = 1.5;\n    roundRect(ctx, -cardW / 2, -cardH / 2, cardW, cardH, 18);\n    ctx.stroke();\n\n    /* Header Bar inside card */\n    const barH = 38;\n    ctx.fillStyle = 'rgba(255, 255, 255, 0.04)';\n    roundRect(ctx, -cardW / 2, -cardH / 2, cardW, barH, 18);\n    ctx.fill();\n\n    /* Window dots */\n    const dotColors = ['#ff5f56', '#ffbd2e', '#27c93f'];\n    for (let i = 0; i < 3; i++) {\n      ctx.fillStyle = dotColors[i];\n      ctx.beginPath();\n      ctx.arc(-cardW / 2 + 20 + i * 16, -cardH / 2 + barH / 2, 4.5, 0, Math.PI * 2);\n      ctx.fill();\n    }\n\n    /* Window Title */\n    ctx.font = '500 12px ' + MONO;\n    ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';\n    ctx.textAlign = 'center';\n    ctx.textBaseline = 'middle';\n    ctx.fillText('exode-runtime-v2.0.wasm — 60 FPS', 0, -cardH / 2 + barH / 2);\n\n    /* Live Telemetry / Graphic inside card */\n    const innerY = -cardH / 2 + barH + 16;\n    const innerH = cardH - barH - 32;\n\n    /* Animated Sine Wave Spectrum */\n    ctx.strokeStyle = rgba(c2, 0.7);\n    ctx.lineWidth = 2.5;\n    ctx.beginPath();\n    const waveCount = 40;\n    for (let wi = 0; wi <= waveCount; wi++) {\n      const wx = -cardW / 2 + 24 + (wi / waveCount) * (cardW - 48);\n      const phase = S.age * 3 * speed + wi * 0.25;\n      const wy = innerY + innerH / 2 + Math.sin(phase) * (innerH * 0.28);\n      if (wi === 0) ctx.moveTo(wx, wy);\n      else ctx.lineTo(wx, wy);\n    }\n    ctx.stroke();\n\n    /* Glow on wave crests */\n    const crestWi = Math.floor((Math.sin(S.age * 2) * 0.5 + 0.5) * waveCount);\n    const crestX = -cardW / 2 + 24 + (crestWi / waveCount) * (cardW - 48);\n    const crestY = innerY + innerH / 2 + Math.sin(S.age * 3 * speed + crestWi * 0.25) * (innerH * 0.28);\n    glow(ctx, '#ffffff', crestX, crestY, 24, 0.9);\n\n    /* Status text at bottom of card */\n    ctx.font = '600 11px ' + MONO;\n    ctx.fillStyle = rgba(c1, 0.9);\n    ctx.textAlign = 'left';\n    ctx.fillText('STATE: ACTIVE // ZERO LATENCY', -cardW / 2 + 24, cardH / 2 - 14);\n\n    ctx.textAlign = 'right';\n    ctx.fillStyle = 'rgba(255,255,255,0.4)';\n    ctx.fillText('PHYSICS ENGINE: ONLINE', cardW / 2 - 24, cardH / 2 - 14);\n\n    ctx.restore();\n  }\n\n  /* Animate Ripples */\n  for (let ri = S.ripples.length - 1; ri >= 0; ri--) {\n    const rip = S.ripples[ri];\n    rip.r += dt * 180;\n    rip.alpha -= dt * 1.5;\n    if (rip.alpha <= 0) {\n      S.ripples.splice(ri, 1);\n      continue;\n    }\n    ctx.strokeStyle = rgba(rip.color, rip.alpha);\n    ctx.lineWidth = 2;\n    ctx.beginPath();\n    ctx.arc(rip.x, rip.y, rip.r, 0, Math.PI * 2);\n    ctx.stroke();\n  }\n\n  ctx.restore();\n}\n"
          }
        ]
      },
      "layoutProps": {
        "layoutSizingHorizontal": "fill",
        "layoutSizingVertical": "fill"
      },
      "zIndex": 1,
      "interactions": []
    }
  ]
}
