{
  "id": "mui_aurora_pricing",
  "name": "Aurora Pricing",
  "width": 1200,
  "height": 780,
  "cornerRadius": 0,
  "fit": "Layout",
  "backgroundColor": "#08090d",
  "opacity": 1,
  "x": -600,
  "y": -390,
  "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": "Aurora Pricing — Exode Motion UI",
    "description": "An interactive 3-tier pricing block with dynamic aurora borealis light curtains, spring-animated Monthly/Annual toggle switch, glowing featured Pro card, and plan select buttons."
  },
  "stateMachine": {
    "inputs": [
      {
        "id": "in_headline",
        "name": "headline",
        "value": {
          "type": "Text",
          "value": "Simple, Transparent Pricing",
          "defaultValue": "Simple, Transparent Pricing"
        }
      },
      {
        "id": "in_sub",
        "name": "sub",
        "value": {
          "type": "Text",
          "value": "Build and deploy living motion without limits. Scale as your product grows.",
          "defaultValue": "Build and deploy living motion without limits. Scale as your product grows."
        }
      },
      {
        "id": "in_starterPrice",
        "name": "starterPrice",
        "value": {
          "type": "Text",
          "value": "$0",
          "defaultValue": "$0"
        }
      },
      {
        "id": "in_proPrice",
        "name": "proPrice",
        "value": {
          "type": "Text",
          "value": "$29",
          "defaultValue": "$29"
        }
      },
      {
        "id": "in_enterprisePrice",
        "name": "enterprisePrice",
        "value": {
          "type": "Text",
          "value": "Custom",
          "defaultValue": "Custom"
        }
      },
      {
        "id": "in_speed",
        "name": "speed",
        "value": {
          "type": "Number",
          "value": 1,
          "defaultValue": 1
        }
      },
      {
        "id": "in_accent",
        "name": "accent",
        "value": {
          "type": "Text",
          "value": "#8b5cf6",
          "defaultValue": "#8b5cf6"
        }
      },
      {
        "id": "in_accent2",
        "name": "accent2",
        "value": {
          "type": "Text",
          "value": "#06b6d4",
          "defaultValue": "#06b6d4"
        }
      },
      {
        "id": "in_bg",
        "name": "bg",
        "value": {
          "type": "Text",
          "value": "#08090d",
          "defaultValue": "#08090d"
        }
      }
    ],
    "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": 780,
      "geometry": {
        "type": "Rectangle",
        "width": 1200,
        "height": 780,
        "cornerRadius": 0
      },
      "cornerRadius": 0,
      "children": [],
      "clipContent": true,
      "style": {
        "fill": {
          "type": "Solid",
          "color": "#08090d",
          "opacity": 1
        },
        "effects": [
          {
            "id": "fx_aurora-pricing",
            "type": "CustomEffect",
            "name": "Aurora Pricing",
            "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/* Aurora Pricing: an interactive 3-tier pricing block inspired by reactvibe.com.\n   Features an ethereal aurora borealis wave curtain across the header,\n   an interactive Monthly / Annual toggle switch with spring physics,\n   and 3 responsive pricing cards with hover elevation, glowing Pro tier highlight,\n   and interactive plan selection buttons. */\nlet S = null;\n\nfunction render(ctx, env) {\n  const W = env.width, H = env.height;\n  if (!S) {\n    S = {\n      isAnnual: false,\n      toggleX: 0,\n      hoverCard: -1,\n      cardElevations: [0, 0, 0],\n      btnHovers: [0, 0, 0],\n      btnPress: [0, 0, 0],\n      ripples: [],\n      wasDown: false,\n      auroraPhase: 0,\n    };\n  }\n  const dt = tick(S);\n\n  const headline = String(inp(env, 'headline', 'Simple, Transparent Pricing'));\n  const sub = String(inp(env, 'sub', 'Build and deploy living motion without limits. Scale as your product grows.'));\n  const proPrice = String(inp(env, 'proPrice', '$29'));\n  const starterPrice = String(inp(env, 'starterPrice', '$0'));\n  const enterprisePrice = String(inp(env, 'enterprisePrice', 'Custom'));\n  const speed = clamp(inp(env, 'speed', 1), 0.1, 4);\n  const accent = inp(env, 'accent', '#8b5cf6');\n  const accent2 = inp(env, 'accent2', '#06b6d4');\n  const bg = inp(env, 'bg', '#08090d');\n\n  S.auroraPhase = (S.auroraPhase + dt * 0.8 * speed) % (Math.PI * 2);\n\n  ctx.save();\n  ctx.translate(-W / 2, -H / 2);\n\n  /* Deep Background */\n  ctx.fillStyle = bg;\n  ctx.fillRect(0, 0, W, H);\n\n  /* ---------- Aurora Borealis Curtains ---------- */\n  const cx = W / 2;\n  const auroraLayers = 3;\n  for (let l = 0; l < auroraLayers; l++) {\n    const lColor = l === 0 ? accent : l === 1 ? accent2 : '#10b981';\n    const lPhase = S.auroraPhase + l * 1.6;\n    const grad = ctx.createLinearGradient(0, 0, W, H * 0.45);\n    grad.addColorStop(0, rgba(lColor, 0.28 / (l + 1)));\n    grad.addColorStop(0.5, rgba(lColor, 0.15 / (l + 1)));\n    grad.addColorStop(1, 'rgba(0,0,0,0)');\n\n    ctx.fillStyle = grad;\n    ctx.beginPath();\n    ctx.moveTo(0, 0);\n    const steps = 30;\n    for (let si = 0; si <= steps; si++) {\n      const sx = (si / steps) * W;\n      const sy = Math.sin(lPhase + (si / steps) * Math.PI * 3) * (H * 0.08) + H * 0.18 + l * 20;\n      ctx.lineTo(sx, sy);\n    }\n    ctx.lineTo(W, 0);\n    ctx.closePath();\n    ctx.fill();\n  }\n\n  /* Soft Vignette */\n  const vig = ctx.createRadialGradient(cx, H * 0.5, W * 0.2, cx, H * 0.5, W * 0.7);\n  vig.addColorStop(0, 'rgba(0,0,0,0)');\n  vig.addColorStop(1, rgba(bg, 0.85));\n  ctx.fillStyle = vig;\n  ctx.fillRect(0, 0, W, H);\n\n  const pt = env.pointer || {};\n  const isMobile = W < 800;\n  const px = pt.isHovered ? pt.localX : -9999;\n  const py = pt.isHovered ? pt.localY : -9999;\n  const isDown = !!(pt.isDown || pt.down);\n\n  /* ---------- Header Section ---------- */\n  let curY = Math.max(26, H * 0.06);\n\n  /* Title */\n  const titleSize = clamp(W * 0.042, isMobile ? 24 : 32, 46);\n  ctx.font = '800 ' + titleSize + 'px ' + SANS;\n  ctx.textAlign = 'center';\n  ctx.textBaseline = 'top';\n\n  const titleGrad = ctx.createLinearGradient(cx - 200, curY, cx + 200, curY);\n  titleGrad.addColorStop(0, '#ffffff');\n  titleGrad.addColorStop(0.5, '#e2e8f0');\n  titleGrad.addColorStop(1, mix('#ffffff', accent2, 0.5));\n  ctx.fillStyle = titleGrad;\n  ctx.fillText(headline, cx, curY);\n\n  curY += titleSize * 1.15 + 10;\n\n  /* Subtitle */\n  const subSize = clamp(W * 0.016, 13, 16);\n  ctx.font = '400 ' + subSize + 'px ' + SANS;\n  ctx.fillStyle = 'rgba(226, 232, 240, 0.75)';\n  const subLines = wrap(ctx, sub, Math.min(W * 0.85, 680));\n  subLines.forEach(function (sl, i) {\n    ctx.fillText(sl, cx, curY + i * (subSize * 1.4));\n  });\n\n  curY += subLines.length * (subSize * 1.4) + 20;\n\n  /* ---------- Monthly / Annual Toggle Switch ---------- */\n  const toggleTrackW = 210;\n  const toggleTrackH = 36;\n  const toggleTrackX = cx - toggleTrackW / 2;\n  const toggleTrackY = curY;\n\n  const inToggle = pt.isHovered && px >= toggleTrackX && px <= toggleTrackX + toggleTrackW && py >= toggleTrackY && py <= toggleTrackY + toggleTrackH;\n  if (!S.wasDown && isDown && inToggle) {\n    S.isAnnual = !S.isAnnual;\n  }\n\n  const targetToggleX = S.isAnnual ? toggleTrackW / 2 : 0;\n  S.toggleX += (targetToggleX - S.toggleX) * Math.min(1, dt * 14);\n\n  /* Toggle Track Base */\n  ctx.fillStyle = 'rgba(255, 255, 255, 0.06)';\n  roundRect(ctx, toggleTrackX, toggleTrackY, toggleTrackW, toggleTrackH, toggleTrackH / 2);\n  ctx.fill();\n\n  ctx.strokeStyle = 'rgba(255, 255, 255, 0.12)';\n  ctx.lineWidth = 1;\n  roundRect(ctx, toggleTrackX, toggleTrackY, toggleTrackW, toggleTrackH, toggleTrackH / 2);\n  ctx.stroke();\n\n  /* Sliding Thumb */\n  const thumbPadding = 3;\n  const thumbW = toggleTrackW / 2 - thumbPadding * 2;\n  const thumbH = toggleTrackH - thumbPadding * 2;\n  const thumbX = toggleTrackX + thumbPadding + S.toggleX;\n  const thumbY = toggleTrackY + thumbPadding;\n\n  ctx.fillStyle = rgba(accent, 0.85);\n  roundRect(ctx, thumbX, thumbY, thumbW, thumbH, thumbH / 2);\n  ctx.fill();\n  glow(ctx, accent, thumbX + thumbW / 2, thumbY + thumbH / 2, 24, 0.5);\n\n  /* Toggle Labels */\n  ctx.font = '600 13px ' + SANS;\n  ctx.textAlign = 'center';\n  ctx.textBaseline = 'middle';\n\n  ctx.fillStyle = !S.isAnnual ? '#ffffff' : 'rgba(255, 255, 255, 0.6)';\n  ctx.fillText('Monthly', toggleTrackX + toggleTrackW * 0.25, toggleTrackY + toggleTrackH / 2);\n\n  ctx.fillStyle = S.isAnnual ? '#ffffff' : 'rgba(255, 255, 255, 0.6)';\n  ctx.fillText('Annual -20%', toggleTrackX + toggleTrackW * 0.75, toggleTrackY + toggleTrackH / 2);\n\n  curY += toggleTrackH + (isMobile ? 24 : 32);\n\n  /* ---------- 3 Pricing Tier Cards ---------- */\n  const cards = [\n    {\n      name: 'Starter',\n      badge: null,\n      price: starterPrice,\n      period: '/month',\n      desc: 'For hobbyists and individual creators getting started with real-time motion.',\n      features: ['Unlimited local renders', '100+ Core motion presets', 'WASM runtime export', 'Community discord access'],\n      cta: 'Start Free',\n      popular: false,\n    },\n    {\n      name: 'Pro',\n      badge: 'MOST POPULAR',\n      price: S.isAnnual ? '$23' : proPrice,\n      period: '/month',\n      desc: 'For professional UI/UX designers and teams shipping high-converting apps.',\n      features: ['Everything in Starter', 'Full ReactVibe & MDE Suite', 'Physics engine & state machines', 'Zero-latency vector runtime', 'Priority support & updates'],\n      cta: 'Upgrade to Pro',\n      popular: true,\n    },\n    {\n      name: 'Enterprise',\n      badge: null,\n      price: enterprisePrice,\n      period: '',\n      desc: 'For design systems and scaleups demanding custom motion compliance.',\n      features: ['Custom runtime compilers', 'Design tokens & SSO sync', 'Dedicated solutions engineer', '99.99% uptime SLA'],\n      cta: 'Contact Sales',\n      popular: false,\n    },\n  ];\n\n  const cardGap = isMobile ? 18 : 22;\n  const totalCardsW = Math.min(W * 0.92, 1080);\n  const cardW = isMobile ? totalCardsW : (totalCardsW - cardGap * 2) / 3;\n  const cardH = isMobile ? 320 : Math.max(340, Math.min(H - curY - 30, 420));\n\n  cards.forEach(function (card, idx) {\n    const cardX = isMobile ? cx - cardW / 2 : cx - totalCardsW / 2 + idx * (cardW + cardGap);\n    const cardY = isMobile ? curY + idx * (cardH + cardGap) : curY;\n\n    const inCard = pt.isHovered && px >= cardX && px <= cardX + cardW && py >= cardY && py <= cardY + cardH;\n    const targetElev = inCard ? 1 : 0;\n    S.cardElevations[idx] += (targetElev - S.cardElevations[idx]) * Math.min(1, dt * 10);\n\n    const elevOffset = S.cardElevations[idx] * 6;\n\n    ctx.save();\n    ctx.translate(cardX + cardW / 2, cardY + cardH / 2 - elevOffset);\n\n    /* Background & Border */\n    if (card.popular) {\n      /* Pro Tier Highlight Glow */\n      glow(ctx, accent, 0, 0, cardW * 0.7, 0.45 * (0.8 + S.cardElevations[idx] * 0.4));\n\n      /* Gradient Background */\n      const proGrad = ctx.createLinearGradient(-cardW / 2, -cardH / 2, cardW / 2, cardH / 2);\n      proGrad.addColorStop(0, 'rgba(28, 22, 45, 0.85)');\n      proGrad.addColorStop(1, 'rgba(15, 17, 28, 0.95)');\n      ctx.fillStyle = proGrad;\n      roundRect(ctx, -cardW / 2, -cardH / 2, cardW, cardH, 18);\n      ctx.fill();\n\n      /* Gradient Border */\n      const borderGrad = ctx.createLinearGradient(-cardW / 2, -cardH / 2, cardW / 2, cardH / 2);\n      borderGrad.addColorStop(0, accent);\n      borderGrad.addColorStop(0.5, accent2);\n      borderGrad.addColorStop(1, accent);\n      ctx.strokeStyle = borderGrad;\n      ctx.lineWidth = 2;\n      roundRect(ctx, -cardW / 2, -cardH / 2, cardW, cardH, 18);\n      ctx.stroke();\n    } else {\n      ctx.fillStyle = 'rgba(15, 17, 24, 0.75)';\n      roundRect(ctx, -cardW / 2, -cardH / 2, cardW, cardH, 18);\n      ctx.fill();\n\n      ctx.strokeStyle = rgba('#ffffff', 0.08 + S.cardElevations[idx] * 0.12);\n      ctx.lineWidth = 1.2;\n      roundRect(ctx, -cardW / 2, -cardH / 2, cardW, cardH, 18);\n      ctx.stroke();\n    }\n\n    /* Popular Badge Pill */\n    if (card.badge) {\n      ctx.font = '700 11px ' + MONO;\n      const bTextW = ctx.measureText(card.badge).width + 20;\n      const bH = 22;\n      const bY = -cardH / 2 - bH / 2;\n      ctx.fillStyle = accent;\n      roundRect(ctx, -bTextW / 2, bY, bTextW, bH, bH / 2);\n      ctx.fill();\n\n      ctx.fillStyle = '#ffffff';\n      ctx.textAlign = 'center';\n      ctx.textBaseline = 'middle';\n      ctx.fillText(card.badge, 0, bY + bH / 2);\n    }\n\n    /* Card Content */\n    let cyInner = -cardH / 2 + 28;\n\n    /* Tier Name */\n    ctx.font = '700 20px ' + SANS;\n    ctx.textAlign = 'left';\n    ctx.textBaseline = 'top';\n    ctx.fillStyle = card.popular ? '#ffffff' : '#e2e8f0';\n    ctx.fillText(card.name, -cardW / 2 + 24, cyInner);\n\n    cyInner += 32;\n\n    /* Price */\n    ctx.font = '800 36px ' + SANS;\n    ctx.fillStyle = '#ffffff';\n    ctx.fillText(card.price, -cardW / 2 + 24, cyInner);\n\n    if (card.period) {\n      const priceW = ctx.measureText(card.price).width;\n      ctx.font = '500 14px ' + SANS;\n      ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';\n      ctx.fillText(card.period, -cardW / 2 + 28 + priceW, cyInner + 16);\n    }\n\n    cyInner += 50;\n\n    /* Description */\n    ctx.font = '400 13px ' + SANS;\n    ctx.fillStyle = 'rgba(226, 232, 240, 0.7)';\n    const descLines = wrap(ctx, card.desc, cardW - 48);\n    descLines.slice(0, 2).forEach(function (dl, di) {\n      ctx.fillText(dl, -cardW / 2 + 24, cyInner + di * 18);\n    });\n\n    cyInner += 44;\n\n    /* Features List */\n    ctx.font = '500 13px ' + SANS;\n    card.features.slice(0, 4).forEach(function (feat, fi) {\n      const fy = cyInner + fi * 24;\n      /* Checkmark icon */\n      ctx.fillStyle = card.popular ? accent2 : 'rgba(255, 255, 255, 0.5)';\n      ctx.fillText('✓', -cardW / 2 + 24, fy);\n\n      ctx.fillStyle = 'rgba(255, 255, 255, 0.85)';\n      ctx.fillText(feat, -cardW / 2 + 42, fy);\n    });\n\n    /* Card CTA Button */\n    const btnW = cardW - 48;\n    const btnH = 42;\n    const btnX = -btnW / 2;\n    const btnY = cardH / 2 - btnH - 24;\n\n    const inBtn = inCard && px >= (cardX + 24) && px <= (cardX + cardW - 24) && py >= (cardY + cardH - btnH - 24) && py <= (cardY + cardH - 24);\n    S.btnHovers[idx] += ((inBtn ? 1 : 0) - S.btnHovers[idx]) * Math.min(1, dt * 10);\n\n    if (inBtn && isDown) S.btnPress[idx] = Math.min(1, S.btnPress[idx] + dt * 14);\n    else S.btnPress[idx] = Math.max(0, S.btnPress[idx] - dt * 10);\n\n    if (!S.wasDown && isDown && inBtn) {\n      S.ripples.push({ x: px, y: py, r: 0, alpha: 0.8, color: card.popular ? '#ffffff' : accent });\n      if (typeof env.emit === 'function') {\n        env.emit(idx === 0 ? 'starterSelect' : idx === 1 ? 'proSelect' : 'enterpriseSelect');\n      }\n    }\n\n    const bScale = 1 + S.btnHovers[idx] * 0.02 - S.btnPress[idx] * 0.03;\n    ctx.save();\n    ctx.translate(0, btnY + btnH / 2);\n    ctx.scale(bScale, bScale);\n\n    if (card.popular) {\n      const cGrad = ctx.createLinearGradient(-btnW / 2, 0, btnW / 2, 0);\n      cGrad.addColorStop(0, accent);\n      cGrad.addColorStop(1, accent2);\n      ctx.fillStyle = cGrad;\n    } else {\n      ctx.fillStyle = rgba('#ffffff', 0.08 + S.btnHovers[idx] * 0.1);\n    }\n    roundRect(ctx, -btnW / 2, -btnH / 2, btnW, btnH, 10);\n    ctx.fill();\n\n    if (!card.popular) {\n      ctx.strokeStyle = rgba('#ffffff', 0.15 + S.btnHovers[idx] * 0.2);\n      ctx.lineWidth = 1;\n      roundRect(ctx, -btnW / 2, -btnH / 2, btnW, btnH, 10);\n      ctx.stroke();\n    }\n\n    ctx.font = '600 14px ' + SANS;\n    ctx.fillStyle = '#ffffff';\n    ctx.textAlign = 'center';\n    ctx.textBaseline = 'middle';\n    ctx.fillText(card.cta, 0, 0);\n    ctx.restore();\n\n    ctx.restore();\n  });\n\n  S.wasDown = isDown;\n\n  /* Ripples */\n  for (let ri = S.ripples.length - 1; ri >= 0; ri--) {\n    const rip = S.ripples[ri];\n    rip.r += dt * 160;\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": []
    }
  ]
}
