PageSourceSearch

https://fluttergo.ai/_fluttergo-site.js?v=1bd5779732

js fluttergo.ai collected 2026-10-05 14:56:56 UTC 217,056 bytes, 5,119 lines download raw bytes

1/* FlutterGo.AI marketing site — shared interactions (no dependencies).
2   Theme is bootstrapped inline in each page <head> to avoid a flash; this
3   file owns the toggle, persistence, and all scroll/motion behaviour. */
4(function () {
5  'use strict';
6
7  var root = document.documentElement;
8  var prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
9
10  /* ---- Mobile app / Website choice in the hero composer ---------------- */
11  // Mirrors the Home composer's BuildTargetChips. The pick travels with the
12  // prompt as the guest intent's `template` ('mobile-app' | 'website') and,
13  // for signed-in visitors, straight onto the new project's metadata.
14  function guestBuildTarget() {
15    var chip = document.querySelector('[data-guest-build-target][aria-checked="true"]');
16    return chip && chip.getAttribute('data-guest-build-target') === 'website' ? 'website' : 'mobile';
17  }
18  function guestIntentTemplate() {
19    return guestBuildTarget() === 'website' ? 'website' : 'mobile-app';
20  }
21  document.addEventListener('click', function (event) {
22    var chip = event.target && event.target.closest ? event.target.closest('[data-guest-build-target]') : null;
23    if (!chip) return;
24    var group = chip.closest('[data-guest-build-target-group]');
25    if (!group) return;
26    var chips = group.querySelectorAll('[data-guest-build-target]');
27    for (var i = 0; i < chips.length; i += 1) {
28      var on = chips[i] === chip;
29      chips[i].setAttribute('aria-checked', on ? 'true' : 'false');
30      chips[i].classList.toggle('is-selected', on);
31    }
32    var target = chip.getAttribute('data-guest-build-target');
33    var form = chip.closest('[data-guest-prompt-form]');
34    var input = form && form.querySelector('[data-guest-prompt-input]');
35    if (input) {
36      if (!input.hasAttribute('data-mobile-placeholder')) {
37        input.setAttribute('data-mobile-placeholder', input.getAttribute('placeholder') || '');
38      }
39      input.setAttribute(
40        'placeholder',
41        target === 'website'
42          ? 'Describe the website you want: the pages, the content, how it should look. We build it.'
43          : input.getAttribute('data-mobile-placeholder'),
44      );
45    }
46    try {
47      if (window.__odTrack) window.__odTrack('landing_build_target_selected', { build_target: target });
48    } catch (e) {}
49  });
50
51  /* ---- Landing visitor beacon (unique people + country for admin) ----- */
52  function trackClarityFunnel(event, source, detail) {
53    try {
54      if (typeof window.clarity !== 'function') return;
55      window.clarity('event', event);
56      window.clarity('set', 'funnel_step', event);
57      if (source) window.clarity('set', 'funnel_source', String(source).slice(0, 64));
58      if (detail) window.clarity('set', 'funnel_detail', String(detail).slice(0, 100));
59    } catch (e) {}
60  }
61
62  function trackLandingFunnel(event, source, detail) {
63    try {
64      var payload = JSON.stringify({
65        event: event,
66        source: source || null,
67        detail: detail || null,
68      });
69      fetch('/api/landing-funnel', {
70        method: 'POST',
71        credentials: 'include',
72        headers: { 'content-type': 'application/json' },
73        body: payload,
74        keepalive: true,
75      }).catch(function () {});
76      // Mirror to GTM / Meta when present.
77      try {
78        window.dataLayer = window.dataLayer || [];
79        window.dataLayer.push({ event: event, funnel_source: source || null });
80      } catch (e) {}
81      // Mirror to Microsoft Clarity (session recordings / smart events).
82      trackClarityFunnel(event, source, detail);
83    } catch (e) {}
84  }
85
86  (function pingLandingVisit() {
87    try {
88      if (window.location.pathname.indexOf('/admin') === 0) return;
89      // Only marketing/static surfaces — skip the authenticated app shell.
90      var path = window.location.pathname || '/';
91      if (path.indexOf('/home') === 0 || path.indexOf('/projects') === 0) return;
92      if (path.indexOf('/api/') === 0) return;
93      var body = JSON.stringify({
94        path: path + (window.location.search || ''),
95        referrer: document.referrer ? String(document.referrer).slice(0, 300) : '',
96      });
97      fetch('/api/landing-visits', {
98        method: 'POST',
99        credentials: 'include',
100        headers: { 'content-type': 'application/json' },
101        body: body,
102        keepalive: true,
103      }).catch(function () {});
104    } catch (e) {}
105  })();
106
107  /* ---- Theme toggle + persistence ------------------------------------- */
108  function systemTheme() {
109    return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
110  }
111  function currentTheme() {
112    return root.getAttribute('data-theme') || systemTheme();
113  }
114  function applyTheme(theme) {
115    root.setAttribute('data-theme', theme);
116    try { localStorage.setItem('fg-theme', theme); } catch (e) {}
117    // The app (studio + /docs) keeps its theme in `open-design:config`, a
118    // different key. Without mirroring the choice there, toggling dark on the
119    // marketing site left /docs rendering in light — which looked broken.
120    try {
121      var raw = localStorage.getItem('open-design:config');
122      var cfg = raw ? JSON.parse(raw) : {};
123      if (cfg && typeof cfg === 'object') {
124        cfg.theme = theme;
125        localStorage.setItem('open-design:config', JSON.stringify(cfg));
126      }
127    } catch (e) {}
128    window.dispatchEvent(new CustomEvent('fg:theme', { detail: { theme: theme } }));
129  }
130  document.querySelectorAll('[data-theme-toggle]').forEach(function (btn) {
131    btn.addEventListener('click', function () {
132      applyTheme(currentTheme() === 'dark' ? 'light' : 'dark');
133    });
134  });
135
136  // Product page is unpublished from chrome — drop nav/footer links so they
137  // don't resurface from static HTML templates that still list /product/.
138  document.querySelectorAll(
139    'a[href="/product/"], a[href="/product"], a[href="/product/index.html"]',
140  ).forEach(function (el) {
141    el.hidden = true;
142    el.setAttribute('aria-hidden', 'true');
143    el.style.display = 'none';
144  });
145
146  // React to OS theme change only while the user hasn't pinned one.
147  try {
148    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function (e) {
149      if (!localStorage.getItem('fg-theme')) {
150        root.setAttribute('data-theme', e.matches ? 'dark' : 'light');
151        window.dispatchEvent(new CustomEvent('fg:theme', { detail: { theme: e.matches ? 'dark' : 'light' } }));
152      }
153    });
154  } catch (e) {}
155
156  /* ---- Ambient page background (sparks + soft rings) ------------------- */
157  function initPageBackground() {
158    var bg = document.querySelector('.fgs-bg');
159    if (!bg || bg.dataset.fgBgInit) return;
160    bg.dataset.fgBgInit = '1';
161    if (prefersReduced) return;
162
163    var sparkColors = ['var(--spark-blue)', 'var(--spark-sky)', 'var(--spark-violet)'];
164    var i;
165    for (i = 0; i < 28; i += 1) {
166      var spark = document.createElement('span');
167      spark.className = 'fgs-bg__spark';
168      spark.style.setProperty('--x', (4 + Math.random() * 92).toFixed(2) + '%');
169      spark.style.setProperty('--y', (6 + Math.random() * 88).toFixed(2) + '%');
170      spark.style.setProperty('--sz', (4 + Math.random() * 7).toFixed(1) + 'px');
171      spark.style.setProperty('--spark-color', sparkColors[i % sparkColors.length]);
172      spark.style.setProperty('--spark-opacity', (0.28 + Math.random() * 0.38).toFixed(2));
173      spark.style.setProperty('--dur', (14 + Math.random() * 18).toFixed(1) + 's');
174      spark.style.setProperty('--delay', (-Math.random() * 20).toFixed(1) + 's');
175      bg.appendChild(spark);
176    }
177    for (i = 0; i < 6; i += 1) {
178      var ring = document.createElement('span');
179      ring.className = 'fgs-bg__ring';
180      var ringSize = 80 + Math.random() * 160;
181      ring.style.setProperty('--x', (10 + Math.random() * 80).toFixed(2) + '%');
182      ring.style.setProperty('--y', (8 + Math.random() * 84).toFixed(2) + '%');
183      ring.style.setProperty('--ring', ringSize.toFixed(0) + 'px');
184      ring.style.setProperty('--ring-color', sparkColors[i % sparkColors.length]);
185      ring.style.setProperty('--ring-opacity', (0.35 + Math.random() * 0.35).toFixed(2));
186      ring.style.setProperty('--dur', (20 + Math.random() * 16).toFixed(1) + 's');
187      ring.style.setProperty('--delay', (-Math.random() * 24).toFixed(1) + 's');
188      bg.appendChild(ring);
189    }
190  }
191  initPageBackground();
192
193  /* ---- Year stamp ------------------------------------------------------ */
194  document.querySelectorAll('[data-year]').forEach(function (el) {
195    el.textContent = String(new Date().getFullYear());
196  });
197
198  /* ---- Sticky nav state ------------------------------------------------ */
199  var nav = document.querySelector('.fgs-nav');
200  if (nav) {
201    var onScroll = function () { nav.classList.toggle('is-stuck', window.scrollY > 12); };
202    onScroll();
203    window.addEventListener('scroll', onScroll, { passive: true });
204  }
205
206  /* ---- Mobile drawer --------------------------------------------------- */
207  var burger = document.querySelector('[data-burger]');
208  var drawer = document.querySelector('[data-mobile]');
209  if (burger && drawer) {
210    var setOpen = function (open) {
211      drawer.classList.toggle('is-open', open);
212      burger.setAttribute('aria-expanded', String(open));
213      document.body.style.overflow = open ? 'hidden' : '';
214    };
215    burger.addEventListener('click', function () {
216      setOpen(!drawer.classList.contains('is-open'));
217    });
218    drawer.querySelectorAll('a').forEach(function (a) {
219      a.addEventListener('click', function () { setOpen(false); });
220    });
221  }
222
223  /* ---- Scroll reveal --------------------------------------------------- */
224  var revealEls = document.querySelectorAll('.reveal');
225  if (prefersReduced || !('IntersectionObserver' in window)) {
226    revealEls.forEach(function (el) { el.classList.add('is-in'); });
227  } else {
228    var io = new IntersectionObserver(function (entries) {
229      entries.forEach(function (en) {
230        if (en.isIntersecting) { en.target.classList.add('is-in'); io.unobserve(en.target); }
231      });
232    }, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' });
233    revealEls.forEach(function (el) { io.observe(el); });
234  }
235
236  /* ---- Features showcase: spring tilt + lift --------------------------- */
237  (function initFeaturesShowcase() {
238    var section = document.querySelector('[data-fgx]');
239    if (!section || prefersReduced) return;
240    if (!window.matchMedia('(pointer: fine)').matches) return;
241
242    var cards = Array.prototype.slice.call(section.querySelectorAll('[data-fgx-card]'));
243    cards.forEach(function (card) {
244      var rx = 0, ry = 0, tx = 0, ty = 0, raf = 0;
245      var active = false;
246
247      function tick() {
248        rx += (tx - rx) * 0.14;
249        ry += (ty - ry) * 0.14;
250        card.style.transform =
251          'translate3d(0, -8px, 0) scale(1.012) rotateX(' +
252          rx.toFixed(2) + 'deg) rotateY(' + ry.toFixed(2) + 'deg)';
253        if (Math.abs(tx - rx) > 0.02 || Math.abs(ty - ry) > 0.02 || active) {
254          raf = requestAnimationFrame(tick);
255        } else {
256          raf = 0;
257          card.style.transform = '';
258        }
259      }
260
261      card.addEventListener('pointerenter', function () {
262        active = true;
263        card.classList.add('is-lifted');
264        if (!raf) raf = requestAnimationFrame(tick);
265      });
266
267      card.addEventListener('pointermove', function (e) {
268        var rect = card.getBoundingClientRect();
269        var px = (e.clientX - rect.left) / rect.width - 0.5;
270        var py = (e.clientY - rect.top) / rect.height - 0.5;
271        ty = px * 7;
272        tx = py * -5.5;
273        if (!raf) raf = requestAnimationFrame(tick);
274      });
275
276      card.addEventListener('pointerleave', function () {
277        active = false;
278        card.classList.remove('is-lifted');
279        tx = 0;
280        ty = 0;
281        if (!raf) raf = requestAnimationFrame(tick);
282      });
283    });
284  })();
285
286  /* ---- Final CTA: magnetic primary + float parallax -------------------- */
287  (function initFinalCta() {
288    var section = document.querySelector('[data-fgc]');
289    if (!section || prefersReduced) return;
290
291    var magnet = section.querySelector('[data-fgc-magnet]');
292    var fine = window.matchMedia('(pointer: fine)').matches;
293
294    if (magnet && fine) {
295      var mx = 0, my = 0, cx = 0, cy = 0, raf = 0, hovering = false;
296
297      function tick() {
298        cx += (mx - cx) * 0.18;
299        cy += (my - cy) * 0.18;
300        magnet.style.transform =
301          'translate3d(' + cx.toFixed(2) + 'px,' + (cy - 3).toFixed(2) + 'px,0) scale(1.04)';
302        if (hovering || Math.abs(mx - cx) > 0.15 || Math.abs(my - cy) > 0.15) {
303          raf = requestAnimationFrame(tick);
304        } else {
305          raf = 0;
306          magnet.style.transform = '';
307        }
308      }
309
310      magnet.addEventListener('pointerenter', function () {
311        hovering = true;
312        magnet.classList.add('is-magnet');
313        if (!raf) raf = requestAnimationFrame(tick);
314      });
315
316      magnet.addEventListener('pointermove', function (e) {
317        var rect = magnet.getBoundingClientRect();
318        mx = (e.clientX - (rect.left + rect.width / 2)) * 0.22;
319        my = (e.clientY - (rect.top + rect.height / 2)) * 0.22;
320        if (!raf) raf = requestAnimationFrame(tick);
321      });
322
323      magnet.addEventListener('pointerleave', function () {
324        hovering = false;
325        magnet.classList.remove('is-magnet');
326        mx = 0;
327        my = 0;
328        if (!raf) raf = requestAnimationFrame(tick);
329      });
330    }
331
332    if (!fine) return;
333    var floats = Array.prototype.slice.call(section.querySelectorAll('[data-fgc-float]'));
334    if (!floats.length) return;
335    var px = 0, py = 0, sx = 0, sy = 0, fraf = 0;
336
337    function floatTick() {
338      sx += (px - sx) * 0.06;
339      sy += (py - sy) * 0.06;
340      floats.forEach(function (el, i) {
341        var d = 0.35 + i * 0.12;
342        el.style.translate = (sx * 14 * d).toFixed(1) + 'px ' + (sy * 10 * d).toFixed(1) + 'px';
343      });
344      if (Math.abs(px - sx) > 0.001 || Math.abs(py - sy) > 0.001) {
345        fraf = requestAnimationFrame(floatTick);
346      } else {
347        fraf = 0;
348      }
349    }
350
351    section.addEventListener('pointermove', function (e) {
352      var rect = section.getBoundingClientRect();
353      px = ((e.clientX - rect.left) / rect.width - 0.5) * 2;
354      py = ((e.clientY - rect.top) / rect.height - 0.5) * 2;
355      if (!fraf) fraf = requestAnimationFrame(floatTick);
356    });
357  })();
358
359  /* ---- Pricing plan icons: pointer 3D tilt ---------------------------- */
360  (function initPlanIconTilt() {
361    if (prefersReduced || !window.matchMedia('(pointer: fine)').matches) return;
362    var icons = Array.prototype.slice.call(document.querySelectorAll('[data-plan-tilt]'));
363    if (!icons.length) return;
364    icons.forEach(function (icon) {
365      icon.addEventListener('pointermove', function (e) {
366        var r = icon.getBoundingClientRect();
367        if (!r.width || !r.height) return;
368        var px = (e.clientX - r.left) / r.width - 0.5;
369        var py = (e.clientY - r.top) / r.height - 0.5;
370        icon.style.setProperty('--rx', (18 - py * 12).toFixed(2) + 'deg');
371        icon.style.setProperty('--ry', (-22 + px * 18).toFixed(2) + 'deg');
372      });
373      icon.addEventListener('pointerleave', function () {
374        icon.style.setProperty('--rx', '18deg');
375        icon.style.setProperty('--ry', '-22deg');
376      });
377    });
378  })();
379
380  /* ---- Credit packs: select + per-card 3D tilt ------------------------ */
381  (function initCreditPacks() {
382    var section = document.querySelector('[data-fgp]');
383    if (!section) return;
384    var cards = Array.prototype.slice.call(section.querySelectorAll('[data-fgp-card]'));
385    if (!cards.length) return;
386    var finePointer = window.matchMedia('(pointer: fine)').matches;
387
388    function select(card) {
389      cards.forEach(function (c) {
390        var on = c === card;
391        c.classList.toggle('is-selected', on);
392        c.setAttribute('aria-checked', on ? 'true' : 'false');
393        c.tabIndex = on ? 0 : -1;
394      });
395    }
396
397    function resetTilt(card) {
398      card.style.setProperty('--rx', '0deg');
399      card.style.setProperty('--ry', '0deg');
400    }
401
402    cards.forEach(function (card) {
403      if (!card.classList.contains('is-selected')) card.tabIndex = -1;
404      card.addEventListener('click', function () { select(card); });
405      card.addEventListener('keydown', function (e) {
406        var i = cards.indexOf(card);
407        if (e.key === 'Enter' || e.key === ' ') {
408          e.preventDefault();
409          select(card);
410        } else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
411          e.preventDefault();
412          select(cards[(i + 1) % cards.length]);
413          cards[(i + 1) % cards.length].focus();
414        } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
415          e.preventDefault();
416          select(cards[(i - 1 + cards.length) % cards.length]);
417          cards[(i - 1 + cards.length) % cards.length].focus();
418        }
419      });
420
421      if (prefersReduced || !finePointer) return;
422
423      card.addEventListener('pointermove', function (e) {
424        var r = card.getBoundingClientRect();
425        if (!r.width || !r.height) return;
426        var px = (e.clientX - r.left) / r.width - 0.5;
427        var py = (e.clientY - r.top) / r.height - 0.5;
428        card.style.setProperty('--rx', (-py * 11).toFixed(2) + 'deg');
429        card.style.setProperty('--ry', (px * 14).toFixed(2) + 'deg');
430      });
431      card.addEventListener('pointerleave', function () { resetTilt(card); });
432      card.addEventListener('blur', function () { resetTilt(card); });
433    });
434  })();
435
436  /* ---- Pointer parallax / tilt ---------------------------------------- */
437  if (!prefersReduced && window.matchMedia('(pointer: fine)').matches) {
438    var tiltEls = Array.prototype.slice.call(document.querySelectorAll('[data-tilt]'));
439    var floatEls = Array.prototype.slice.call(document.querySelectorAll('[data-parallax]'));
440    var tx = 0, ty = 0, cx = 0, cy = 0, raf = 0;
441    function loop() {
442      cx += (tx - cx) * 0.08; cy += (ty - cy) * 0.08;
443      tiltEls.forEach(function (el) {
444        var rx = (parseFloat(el.dataset.rx) || 8) - cy * 6;
445        var ry = (parseFloat(el.dataset.ry) || -14) + cx * 10;
446        el.style.transform = 'rotateX(' + rx.toFixed(2) + 'deg) rotateY(' + ry.toFixed(2) + 'deg)';
447      });
448      floatEls.forEach(function (el) {
449        var d = parseFloat(el.dataset.parallax) || 1;
450        el.style.transform = 'translate3d(' + (cx * 22 * d).toFixed(1) + 'px,' + (cy * 22 * d).toFixed(1) + 'px,0)';
451      });
452      if (Math.abs(tx - cx) > 0.001 || Math.abs(ty - cy) > 0.001) { raf = requestAnimationFrame(loop); }
453      else { raf = 0; }
454    }
455    window.addEventListener('pointermove', function (e) {
456      tx = (e.clientX / window.innerWidth - 0.5) * 2;
457      ty = (e.clientY / window.innerHeight - 0.5) * 2;
458      if (!raf) raf = requestAnimationFrame(loop);
459    }, { passive: true });
460  }
461
462  /* ---- Pricing billing toggle (monthly / quarterly) -------------------- */
463  var billing = document.querySelector('[data-billing]');
464  if (billing) {
465    var setCycle = function (cycle) {
466      billing.querySelectorAll('button').forEach(function (b) {
467        b.classList.toggle('is-active', b.dataset.cycle === cycle);
468      });
469      document.querySelectorAll('[data-price-monthly]').forEach(function (el) {
470        var monthly = el.getAttribute('data-price-monthly');
471        var quarterly = el.getAttribute('data-price-quarterly');
472        var next = cycle === 'quarterly' && quarterly ? quarterly : monthly;
473        if (typeof window.__fgSetPrice === 'function') {
474          window.__fgSetPrice(el, next, cycle);
475        } else {
476          el.textContent = next;
477        }
478      });
479      document.querySelectorAll('[data-per-monthly]').forEach(function (el) {
480        var monthly = el.getAttribute('data-per-monthly');
481        var quarterly = el.getAttribute('data-per-quarterly');
482        el.textContent = cycle === 'quarterly' && quarterly ? quarterly : monthly;
483      });
484      document.querySelectorAll('[data-billing-href-monthly]').forEach(function (link) {
485        var monthly = link.getAttribute('data-billing-href-monthly');
486        var quarterly = link.getAttribute('data-billing-href-quarterly');
487        link.setAttribute('href', cycle === 'quarterly' && quarterly ? quarterly : monthly);
488      });
489      document.querySelectorAll('[data-save-quarterly]').forEach(function (el) {
490        var text = el.getAttribute('data-save-quarterly') || '';
491        if (cycle === 'quarterly' && text) {
492          el.textContent = text;
493          el.hidden = false;
494        } else {
495          el.textContent = '';
496          el.hidden = true;
497        }
498      });
499    };
500    billing.querySelectorAll('button').forEach(function (b) {
501      b.addEventListener('click', function () { setCycle(b.dataset.cycle); });
502    });
503  }
504
505  /* Pricing Upgrade: stay on this page unless redirecting to Polar or sign-in. */
506  var PENDING_BILLING_KEY = 'fg.pendingBilling';
507  var billingNotice = document.querySelector('[data-billing-notice]');
508
509  function showBillingNotice(message, kind) {
510    if (!billingNotice) return;
511    billingNotice.textContent = message;
512    billingNotice.classList.remove('is-error', 'is-info');
513    billingNotice.classList.add(kind === 'error' ? 'is-error' : 'is-info');
514    billingNotice.hidden = false;
515  }
516
517  function clearBillingNotice() {
518    if (!billingNotice) return;
519    billingNotice.hidden = true;
520    billingNotice.textContent = '';
521    billingNotice.classList.remove('is-error', 'is-info');
522  }
523
524  function activeBillingCycle() {
525    var billingEl = document.querySelector('[data-billing]');
526    if (!billingEl) return 'monthly';
527    var activeBtn = billingEl.querySelector('button.is-active');
528    return activeBtn && activeBtn.dataset.cycle === 'quarterly' ? 'quarterly' : 'monthly';
529  }
530
531  function isCustomerAccount(user) {
532    if (!user || !user.id) return false;
533    // Loopback Local Mode owner is a machine identity — not a signed-up customer.
534    if (user.id === 'fluttergo-local-owner') return false;
535    if (user.email === '[email protected]') return false;
536    if (user.accountType && user.accountType !== 'registered') return false;
537    return true;
538  }
539
540  function isRegisteredBillingUser(user) {
541    return isCustomerAccount(user);
542  }
543
544  /** After login, App.tsx on /home resumes Polar checkout from ?billing=. */
545  function checkoutResumeHomePath(planId, cycle) {
546    var params = new URLSearchParams({ billing: 'checkout', plan: planId || 'pro' });
547    if (cycle === 'quarterly') params.set('interval', 'quarterly');
548    return '/projects?' + params.toString();
549  }
550
551  function signInCheckoutPath(planId, cycle) {
552    var next = checkoutResumeHomePath(planId, cycle);
553    return '/onboarding/sign-in?next=' + encodeURIComponent(next);
554  }
555
556  function savePendingCheckout(planId, cycle) {
557    var intent = {
558      action: 'checkout',
559      plan: planId || 'pro',
560      interval: cycle === 'quarterly' ? 'quarterly' : 'monthly',
561    };
562    try { localStorage.setItem(PENDING_BILLING_KEY, JSON.stringify(intent)); } catch (err) {}
563    try { sessionStorage.setItem(PENDING_BILLING_KEY, JSON.stringify(intent)); } catch (err) {}
564  }
565
566  function askLoginBeforeCheckout(planId, cycle, trigger) {
567    savePendingCheckout(planId, cycle);
568    setUpgradeLoading(trigger, false);
569    showBillingNotice('Please sign in first to continue checkout. We will bring you back here after login.', 'info');
570    window.setTimeout(function () {
571      window.location.href = signInCheckoutPath(planId, cycle);
572    }, 450);
573  }
574
575  function setUpgradeLoading(link, loading) {
576    if (!link) return;
577    if (typeof window.__fgUpgradeLoading === 'function') {
578      window.__fgUpgradeLoading(link, loading);
579      return;
580    }
581    link.classList.toggle('is-loading', loading);
582    if (loading) {
583      if (!link.dataset.fgLabel) link.dataset.fgLabel = link.textContent || '';
584      link.textContent = 'Opening checkout…';
585    } else if (link.dataset.fgLabel) {
586      link.textContent = link.dataset.fgLabel;
587    }
588  }
589
590  function checkoutErrorMessage(payload) {
591    if (payload && payload.error && payload.error.message) return payload.error.message;
592    return 'Checkout is temporarily unavailable. Please try again in a moment.';
593  }
594
595  function startPolarCheckout(planId, cycle, trigger) {
596    clearBillingNotice();
597    setUpgradeLoading(trigger, true);
598    var params = new URLSearchParams({ plan: planId || 'pro' });
599    if (cycle === 'quarterly') params.set('interval', 'quarterly');
600    return fetch('/api/billing/checkout?' + params.toString(), { credentials: 'include' })
601      .then(function (resp) {
602        return resp.json().catch(function () { return {}; }).then(function (json) {
603          return { ok: resp.ok, status: resp.status, json: json };
604        });
605      })
606      .then(function (result) {
607        if (result.ok && result.json && result.json.url) {
608          window.location.href = result.json.url;
609          return;
610        }
611        if (result.status === 401) {
612          askLoginBeforeCheckout(planId, cycle, trigger);
613          return;
614        }
615        setUpgradeLoading(trigger, false);
616        showBillingNotice(checkoutErrorMessage(result.json), 'error');
617      })
618      .catch(function () {
619        setUpgradeLoading(trigger, false);
620        showBillingNotice('Could not reach checkout. Check your connection and try again.', 'error');
621      });
622  }
623
624  function leaveForCheckout(link, planId, cycle) {
625    if (planId) savePendingCheckout(planId, cycle);
626    window.location.assign(link.href || signInCheckoutPath(planId, cycle));
627  }
628
629  // Homepage plan tiles (and any other `data-fg-full-nav` CTA) must do a
630  // real document navigation. Next.js hydrates the marketing export and
631  // intercepts same-origin <a> clicks; the Polar shortcut below then
632  // preventDefault'd and left the visitor on the same page whenever the
633  // session fetch stalled or the notice slot was missing.
634  document.addEventListener('click', function (e) {
635    if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return;
636    var target = e.target;
637    if (!target || typeof target.closest !== 'function') return;
638    var link = target.closest('a[data-fg-full-nav]');
639    if (!link || !link.href) return;
640    e.preventDefault();
641    e.stopImmediatePropagation();
642    leaveForCheckout(
643      link,
644      link.getAttribute('data-billing-plan'),
645      activeBillingCycle(),
646    );
647  }, true);
648
649  var upgradeLinks = document.querySelectorAll('[data-billing-plan]:not([data-fg-full-nav])');
650  if (upgradeLinks.length) {
651    upgradeLinks.forEach(function (link) {
652      link.addEventListener('click', function (e) {
653        if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button === 1) return;
654        e.preventDefault();
655        e.stopImmediatePropagation();
656        var planId = link.getAttribute('data-billing-plan');
657        var cycle = activeBillingCycle();
658        var finished = false;
659        var watchdog = window.setTimeout(function () {
660          if (finished) return;
661          finished = true;
662          leaveForCheckout(link, planId, cycle);
663        }, 4000);
664
665        fetch('/api/auth/session', { credentials: 'include', cache: 'no-store' })
666          .then(function (resp) { return resp.ok ? resp.json() : null; })
667          .then(function (session) {
668            if (finished) return;
669            finished = true;
670            window.clearTimeout(watchdog);
671            if (!isRegisteredBillingUser(session && session.user)) {
672              askLoginBeforeCheckout(planId, cycle, link);
673              return;
674            }
675            return startPolarCheckout(planId, cycle, link);
676          })
677          .catch(function () {
678            if (finished) return;
679            finished = true;
680            window.clearTimeout(watchdog);
681            leaveForCheckout(link, planId, cycle);
682          });
683      });
684    });
685  }
686
687  // Legacy links like /?billing=checkout&plan=pro land on marketing home — route
688  // them through sign-in (or /home resume) instead of a no-op query string.
689  (function routeLegacyBillingCheckoutQuery() {
690    var params;
691    try { params = new URLSearchParams(window.location.search); } catch (err) { return; }
692    var billing = params.get('billing');
693    if (billing !== 'checkout' && billing !== 'upgrade' && billing !== 'plans' && billing !== 'pack') return;
694    // Already on the app shell — leave App.tsx to handle /projects?billing=.
695    if (window.location.pathname.indexOf('/projects') === 0) return;
696
697    var planId = params.get('plan') || 'pro';
698    var packId = params.get('pack') || '';
699    var cycle = params.get('interval') === 'quarterly' ? 'quarterly' : 'monthly';
700    var resumeParams = new URLSearchParams({ billing: billing === 'pack' ? 'pack' : billing });
701    if (packId) resumeParams.set('pack', packId);
702    else if (billing === 'checkout' || billing === 'upgrade' || billing === 'plans') {
703      resumeParams.set('plan', planId);
704    }
705    if (cycle === 'quarterly') resumeParams.set('interval', 'quarterly');
706    var resumeHome = '/projects?' + resumeParams.toString();
707
708    fetch('/api/auth/session', { credentials: 'include', cache: 'no-store' })
709      .then(function (resp) { return resp.ok ? resp.json() : null; })
710      .then(function (session) {
711        if (!isRegisteredBillingUser(session && session.user)) {
712          if (billing === 'checkout' || billing === 'upgrade') {
713            savePendingCheckout(planId, cycle);
714          }
715          window.location.replace(
716            '/onboarding/sign-in?next=' + encodeURIComponent(resumeHome),
717          );
718          return;
719        }
720        window.location.replace(resumeHome);
721      })
722      .catch(function () {
723        window.location.replace(
724          '/onboarding/sign-in?next=' + encodeURIComponent(resumeHome),
725        );
726      });
727  })();
728
729  if (window.location.search.indexOf('checkout_failed=1') !== -1) {
730    showBillingNotice('Checkout is temporarily unavailable. Please try again in a moment.', 'error');
731    try {
732      var cleanUrl = new URL(window.location.href);
733      cleanUrl.searchParams.delete('checkout_failed');
734      window.history.replaceState(null, '', cleanUrl.pathname + cleanUrl.search + cleanUrl.hash);
735    } catch (err) {}
736  }
737
738  /* ---- Auth-aware nav (show signed-in user instead of Sign in) ---------- */
739  function authDisplayName(user) {
740    if (user && user.name && String(user.name).trim()) return String(user.name).trim();
741    var email = user && user.email ? String(user.email) : '';
742    var at = email.indexOf('@');
743    return at > 0 ? email.slice(0, at) : (email || 'Account');
744  }
745
746  function authInitial(user) {
747    var label = authDisplayName(user);
748    return label.charAt(0).toUpperCase() || '?';
749  }
750
751  function buildUserChip(user) {
752    var chip = document.createElement('a');
753    // Studio Account settings — opens Settings directly, then returns to landing.
754    chip.href = '/settings/account';
755    chip.className = 'fgs-user-chip';
756    var name = authDisplayName(user);
757    chip.setAttribute('aria-label', 'Signed in as ' + name + ' — open account settings');
758    chip.title = user.email || name;
759
760    var avatar = document.createElement('span');
761    avatar.className = 'fgs-user-chip__avatar';
762    avatar.setAttribute('aria-hidden', 'true');
763    if (user.avatarUrl) {
764      var img = document.createElement('img');
765      img.src = user.avatarUrl;
766      img.alt = '';
767      avatar.appendChild(img);
768    } else {
769      avatar.textContent = authInitial(user);
770    }
771
772    var label = document.createElement('span');
773    label.className = 'fgs-user-chip__name';
774    label.textContent = name;
775
776    chip.appendChild(avatar);
777    chip.appendChild(label);
778    return chip;
779  }
780
781  /**
782   * Settings gear for the marketing nav.
783   *
784   * The studio's project workspace carries a gear in its header that opens the
785   * full Settings surface. The landing only had the account chip, which lands
786   * on the Account pane, so every other pane (Execution, Billing, App repo,
787   * Team access, ...) was unreachable without opening a project first. This is
788   * the same entry point on the marketing site, defaulting to the same section
789   * the workspace gear does. `/settings/<section>` is a page of its own: it renders the
790   * dialog on an empty host and its close button comes back here.
791   */
792  function buildSettingsGear() {
793    var gear = document.createElement('a');
794    gear.className = 'fgs-settings-gear';
795    gear.href = '/settings/notifications';
796    gear.setAttribute('data-fg-settings-gear', '1');
797    gear.setAttribute('aria-label', 'Open settings');
798    gear.title = 'Settings';
799    gear.innerHTML =
800      '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"'
801        + ' stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">'
802        + '<circle cx="12" cy="12" r="3"></circle>'
803        + '<path d="M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 0 1-2.82 2.83l-.06-.07a1.7 1.7 0 0 0-1.88-.33 1.7 1.7 0 0 0-1.04 1.56V21a2 2 0 0 1-4 0v-.1A1.7 1.7 0 0 0 9 19.4a1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.82l.07-.06a1.7 1.7 0 0 0 .33-1.88 1.7 1.7 0 0 0-1.56-1.04H3a2 2 0 0 1 0-4h.1a1.7 1.7 0 0 0 1.56-1.04 1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.07A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1.04-1.56V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1.04 1.56 1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.07.06a1.7 1.7 0 0 0-.33 1.87V9a1.7 1.7 0 0 0 1.56 1.04H21a2 2 0 0 1 0 4h-.1a1.7 1.7 0 0 0-1.56 1.04Z"></path>'
804      + '</svg>';
805    return gear;
806  }
807
808  /** Mount the gear immediately after the account chip, mirroring the studio topbar. */
809  function ensureSettingsGear() {
810    var navRight = document.querySelector('.fgs-nav__right');
811    if (!navRight || navRight.querySelector('[data-fg-settings-gear]')) return;
812    var gear = buildSettingsGear();
813    var chip = navRight.querySelector('.fgs-user-chip');
814    if (chip) chip.insertAdjacentElement('afterend', gear);
815    else navRight.prepend(gear);
816  }
817
818  function applyAuthLandingCopy(session) {
819    var user = session && session.user;
820    var hint = document.querySelector('[data-guest-prompt-hint]');
821    var lead = document.querySelector('.fgs-clean-lead');
822    if (lead && !lead.dataset.fgGuestLead) {
823      lead.dataset.fgGuestLead = lead.textContent || '';
824    }
825    if (!user) {
826      if (hint) {
827        hint.hidden = false;
828        hint.removeAttribute('aria-hidden');
829        if (hint.dataset.fgGuestHint) hint.textContent = hint.dataset.fgGuestHint;
830      }
831      if (lead && lead.dataset.fgGuestLead) lead.textContent = lead.dataset.fgGuestLead;
832      return;
833    }
834    // Signed-in: drop the guest offer strip and the no-account copy.
835    if (hint) {
836      if (!hint.dataset.fgGuestHint) hint.dataset.fgGuestHint = hint.textContent || '';
837      hint.hidden = true;
838      hint.setAttribute('aria-hidden', 'true');
839      hint.textContent = '';
840    }
841    if (lead) {
842      lead.textContent = 'Turn ideas into Android & iOS apps in minutes';
843    }
844  }
845
846  function applyAuthNav(session) {
847    // Nav "signed in" chrome is for real customer accounts only.
848    var user = isCustomerAccount(session && session.user) ? session.user : null;
849    applyAuthLandingCopy(user ? session : null);
850    var signIn = document.querySelector(
851      '[data-fg-nav-actions] [data-fg-signin],' +
852      '.fgs-nav__right a.fgs-btn--ghost.fgs-btn--sm[href="/onboarding/sign-in"],' +
853      '.fgs-nav__right a.fgs-btn--ghost.fgs-btn--sm[href*="sign-in"],' +
854      '.fgs-nav__right a[href="/onboarding/sign-in"],' +
855      '.fgs-nav__right a[href*="sign-in"]',
856    );
857    // Any primary small nav CTA (Create account / Start Building / Get started).
858    var cta = document.querySelector(
859      '[data-fg-nav-actions] [data-fg-create], .fgs-nav__right > a.fgs-btn--sm:not(.fgs-btn--ghost)',
860    );
861    var mobile = document.querySelector('[data-mobile]');
862
863    function showNavCta(el) {
864      if (!el) return;
865      el.hidden = false;
866      el.removeAttribute('aria-hidden');
867      el.style.display = '';
868      el.style.removeProperty('display');
869    }
870
871    /**
872     * Mobile drawer always needs a clear path back to create.
873     * Header CTAs are display:none under 720px, so the drawer is the only path.
874     */
875    function ensureMobileCreatePaths(signedIn) {
876      if (!mobile) return null;
877
878      var home = mobile.querySelector('[data-fg-mobile-home]');
879      if (!home) {
880        home = document.createElement('a');
881        home.href = '/';
882        home.setAttribute('data-fg-mobile-home', '1');
883        home.textContent = 'Home';
884        mobile.insertBefore(home, mobile.firstChild);
885      }
886
887      var create = mobile.querySelector('[data-fg-mobile-create]');
888      if (!create) {
889        create = mobile.querySelector('a.fgs-btn--block[href*="guest-hero"]');
890      }
891      if (!create) {
892        // Prefer an existing primary block CTA that is not Sign in.
893        var blocks = mobile.querySelectorAll('a.fgs-btn--block');
894        for (var i = 0; i < blocks.length; i++) {
895          var href = blocks[i].getAttribute('href') || '';
896          if (href.indexOf('sign-in') === -1) {
897            create = blocks[i];
898            break;
899          }
900        }
901      }
902      if (!create) {
903        create = document.createElement('a');
904        create.className = 'fgs-btn fgs-btn--block';
905        mobile.appendChild(create);
906      }
907      create.setAttribute('data-fg-mobile-create', '1');
908      create.classList.add('fgs-btn', 'fgs-btn--block');
909      create.textContent = 'Create a project';
910      create.href = '/#guest-hero';
911      showNavCta(create);
912      // Pin Create just under Home so it is the first action in the drawer.
913      if (home.nextSibling !== create) {
914        home.insertAdjacentElement('afterend', create);
915      }
916
917      var projects = mobile.querySelector('[data-fg-mobile-projects]');
918      if (signedIn) {
919        if (!projects) {
920          projects = document.createElement('a');
921          projects.setAttribute('data-fg-mobile-projects', '1');
922          projects.href = '/projects';
923          projects.textContent = 'My projects';
924          create.insertAdjacentElement('afterend', projects);
925        } else {
926          showNavCta(projects);
927        }
928      } else if (projects) {
929        projects.remove();
930      }
931
932      // The nav gear is display:none under 720px, so the drawer is the only
933      // settings path on phones - same reasoning as the Create/Sign in CTAs.
934      var settings = mobile.querySelector('[data-fg-mobile-settings]');
935      if (signedIn) {
936        if (!settings) {
937          settings = document.createElement('a');
938          settings.setAttribute('data-fg-mobile-settings', '1');
939          settings.href = '/settings/notifications';
940          settings.textContent = 'Settings';
941          (projects || create).insertAdjacentElement('afterend', settings);
942        } else {
943          showNavCta(settings);
944        }
945      } else if (settings) {
946        settings.remove();
947      }
948
949      // Hide mobile Sign in block when already authenticated.
950      mobile.querySelectorAll('a.fgs-btn--block[href*="sign-in"]').forEach(function (el) {
951        if (el.hasAttribute('data-fg-mobile-create')) return;
952        if (signedIn) {
953          el.hidden = true;
954          el.setAttribute('aria-hidden', 'true');
955          el.style.display = 'none';
956        } else {
957          showNavCta(el);
958        }
959      });
960
961      return create;
962    }
963
964    if (!user) {
965      document.querySelectorAll('.fgs-user-chip, .fg-user-chip').forEach(function (el) { el.remove(); });
966      document.querySelectorAll('[data-fg-settings-gear]').forEach(function (el) { el.remove(); });
967      try {
968        sessionStorage.removeItem('fg:header-auth-user');
969        localStorage.removeItem('fg:header-auth-user');
970      } catch (e) {}
971      if (cta) {
972        showNavCta(cta);
973        if (cta.dataset.fgGuestLabel) cta.textContent = cta.dataset.fgGuestLabel;
974        if (cta.dataset.fgGuestHref) cta.href = cta.dataset.fgGuestHref;
975      }
976      document.querySelectorAll(
977        '[data-fg-create], [data-fg-create-mobile], .nav-primary-cta, .nav-drawer-cta',
978      ).forEach(function (el) { showNavCta(el); });
979      document.querySelectorAll('[data-fg-signin], [data-fg-signin-mobile]').forEach(function (el) {
980        showNavCta(el);
981      });
982      var signedNav = document.querySelector('[data-fg-nav].is-signed-in, .nav.is-signed-in');
983      if (signedNav) signedNav.classList.remove('is-signed-in');
984      document.documentElement.classList.remove('fg-signed-in');
985      document.documentElement.classList.add('fg-signed-out');
986      ensureMobileCreatePaths(false);
987      if (mobile) {
988        mobile.querySelectorAll('.fgs-mobile-user').forEach(function (el) { el.remove(); });
989      }
990      return;
991    }
992
993    if (signIn && !signIn.dataset.fgAuthReplaced) {
994      // Shared marketing header (`data-fg-nav-actions`) is owned by HEADER_AUTH
995      // in site-header.ts — it paints `.fg-user-chip`. Do not swap in the legacy
996      // `.fgs-user-chip` here or the two scripts fight on every homepage load.
997      if (signIn.closest('[data-fg-nav-actions]')) {
998        signIn.hidden = true;
999        signIn.setAttribute('aria-hidden', 'true');
1000        signIn.style.display = 'none';
1001      } else {
1002        signIn.dataset.fgAuthReplaced = '1';
1003        signIn.replaceWith(buildUserChip(user));
1004      }
1005    } else if (!signIn) {
1006      var navRight = document.querySelector('.fgs-nav__right');
1007      if (navRight && !navRight.querySelector('.fgs-user-chip')) {
1008        var themeToggle = navRight.querySelector('[data-theme-toggle]');
1009        var chip = buildUserChip(user);
1010        if (themeToggle) themeToggle.insertAdjacentElement('afterend', chip);
1011        else navRight.prepend(chip);
1012      }
1013    }
1014
1015    try {
1016      var cachedUser = JSON.stringify({
1017        id: user.id,
1018        email: user.email || '',
1019        name: user.name || '',
1020        avatarUrl: user.avatarUrl || '',
1021        accountType: user.accountType || 'registered',
1022      });
1023      sessionStorage.setItem('fg:header-auth-user', cachedUser);
1024      localStorage.setItem('fg:header-auth-user', cachedUser);
1025    } catch (e) {}
1026    document.documentElement.classList.add('fg-signed-in');
1027    document.documentElement.classList.remove('fg-signed-out');
1028
1029    ensureSettingsGear();
1030
1031    // Signed-in chrome: drop the nav "Create a project" CTA — the chip +
1032    // studio (/home) already cover create/resume. Guests still see it above.
1033    // Cover both legacy (.fgs-nav__right .fgs-btn--sm) and shared header
1034    // (.nav-primary-cta[data-fg-create]) so homepage + grafted pages agree.
1035    var createButtons = document.querySelectorAll(
1036      '[data-fg-create], [data-fg-create-mobile], .nav-primary-cta, .nav-drawer-cta',
1037    );
1038    if (cta) {
1039      cta.hidden = true;
1040      cta.setAttribute('aria-hidden', 'true');
1041      cta.style.display = 'none';
1042    }
1043    createButtons.forEach(function (el) {
1044      el.hidden = true;
1045      el.setAttribute('aria-hidden', 'true');
1046      el.style.display = 'none';
1047    });
1048    var nav = document.querySelector('[data-fg-nav], .fgs-nav, header.nav');
1049    if (nav) nav.classList.add('is-signed-in');
1050
1051    var mobileCta = ensureMobileCreatePaths(true);
1052    if (mobileCta) {
1053      mobileCta.hidden = true;
1054      mobileCta.setAttribute('aria-hidden', 'true');
1055      mobileCta.style.display = 'none';
1056    }
1057
1058    if (mobile && !mobile.querySelector('.fgs-mobile-user')) {
1059      var row = document.createElement('p');
1060      row.className = 'fgs-mobile-user';
1061      row.textContent = 'Signed in as ' + authDisplayName(user);
1062      mobile.appendChild(row);
1063    } else if (mobile && mobileCta) {
1064      // Keep account label at the bottom; actions stay on top.
1065      var existing = mobile.querySelector('.fgs-mobile-user');
1066      if (existing) mobile.appendChild(existing);
1067    }
1068  }
1069
1070  fetch('/api/auth/session', { credentials: 'include', cache: 'no-store' })
1071    .then(function (resp) {
1072      if (!resp.ok) throw new Error('session_http_' + resp.status);
1073      return resp.json();
1074    })
1075    .then(function (session) {
1076      // Do not call applyAuthNav on a missing payload — that used to force
1077      // guest CTAs (Sign in / Create a project) whenever /api/auth/session
1078      // blipped, undoing HEADER_AUTH's signed-in chip on the homepage.
1079      if (!session || typeof session !== 'object') return;
1080      applyAuthNav(session);
1081      // Same customer gate as nav — never show Local/guest projects while
1082      // the chrome still says "Sign in".
1083      loadLandingRecentProjects(
1084        isCustomerAccount(session && session.user) ? session : null,
1085      );
1086    })
1087    .catch(function () { /* keep pending / cached chrome */ });
1088
1089  /* ---- Funnel: nav / signup CTA clicks -------------------------------- */
1090  (function bindFunnelCtas() {
1091    document.addEventListener('click', function (event) {
1092      var target = event.target;
1093      if (!target || typeof target.closest !== 'function') return;
1094      var link = target.closest('a');
1095      if (!link) return;
1096      var href = (link.getAttribute('href') || '').toLowerCase();
1097      var text = (link.textContent || '').trim().toLowerCase();
1098      var isAuthCta =
1099        href.indexOf('sign-in') >= 0 ||
1100        href.indexOf('signup') >= 0 ||
1101        href.indexOf('sign-up') >= 0 ||
1102        href.indexOf('get-started') >= 0 ||
1103        text === 'create account' ||
1104        text === 'sign up' ||
1105        text === 'get started' ||
1106        text === 'start building' ||
1107        text === 'sign in';
1108      if (!isAuthCta) return;
1109      trackLandingFunnel('cta_clicked', 'nav_cta', (link.getAttribute('href') || text).slice(0, 160));
1110      // Signup surface only — login alone is not signup_page_opened.
1111      if (
1112        href.indexOf('get-started') >= 0 ||
1113        href.indexOf('signup') >= 0 ||
1114        href.indexOf('sign-up') >= 0 ||
1115        text === 'create account' ||
1116        text === 'sign up' ||
1117        text === 'start building'
1118      ) {
1119        trackLandingFunnel('signup_page_opened', 'nav_cta', (link.getAttribute('href') || text).slice(0, 160));
1120      }
1121    }, true);
1122  })();
1123
1124  /* ---- Recent projects under landing chat (signed-in) -------------------- */
1125  function formatRelativeTime(ts) {
1126    var ms = typeof ts === 'number' ? ts : Date.parse(ts);
1127    if (!Number.isFinite(ms)) return '';
1128    var diff = Math.max(0, Date.now() - ms);
1129    var mins = Math.floor(diff / 60000);
1130    if (mins < 1) return 'Just now';
1131    if (mins < 60) return mins + 'm ago';
1132    var hours = Math.floor(mins / 60);
1133    if (hours < 24) return hours + 'h ago';
1134    var days = Math.floor(hours / 24);
1135    if (days < 30) return days + 'd ago';
1136    return new Date(ms).toLocaleDateString();
1137  }
1138
1139  function loadLandingRecentProjects(session) {
1140    var section = document.querySelector('[data-guest-recent]');
1141    var row = document.querySelector('[data-guest-recent-row]');
1142    var countEl = document.querySelector('[data-guest-recent-count]');
1143    if (!section || !row) return;
1144    if (!isCustomerAccount(session && session.user)) {
1145      section.hidden = true;
1146      row.textContent = '';
1147      if (countEl) {
1148        countEl.hidden = true;
1149        countEl.textContent = '';
1150      }
1151      return;
1152    }
1153
1154    function isFlutterProject(project) {
1155      if (!project) return false;
1156      if (project.skillId === 'fluttergo-pipeline') return true;
1157      var meta = project.metadata || {};
1158      if (meta.platform === 'flutter') return true;
1159      var targets = meta.platformTargets;
1160      if (Array.isArray(targets) && targets.indexOf('flutter') !== -1) return true;
1161      var pluginId = project.pluginId || '';
1162      var nonFlutterPlugins = {
1163        'example-web-prototype': 1,
1164        'example-live-artifact': 1,
1165        'example-simple-deck': 1,
1166        'od-media-generation': 1,
1167        'example-hyperframes': 1,
1168      };
1169      if (nonFlutterPlugins[pluginId]) return false;
1170      if (meta.kind === 'deck' || meta.kind === 'image' || meta.intent === 'live-artifact') return false;
1171      // FlutterGo default create path has no pluginId — treat as Flutter app.
1172      return !pluginId;
1173    }
1174
1175    function projectRawUrl(projectId, filePath) {
1176      return '/api/projects/' + encodeURIComponent(projectId) + '/raw/' +
1177        String(filePath || '').split('/').map(encodeURIComponent).join('/');
1178    }
1179
1180    /** Flutter template launcher / splash — skip when any real UI art exists. */
1181    function isTemplateFlutterIcon(path) {
1182      var n = String(path || '').toLowerCase().replace(/\\/g, '/');
1183      if (/assets\/(?:images\/)?branding\//.test(n)) return false;
1184      if (/(?:^|\/)(?:logo|wordmark)\./.test(n)) return false;
1185      return (
1186        /web\/icons\/icon-(?:maskable-)?(?:192|512)\.png$/i.test(n) ||
1187        /web\/favicon\.png$/i.test(n) ||
1188        /\/mipmap-[^/]+\/ic_launcher/i.test(n) ||
1189        /appicon\.appiconset\//i.test(n) ||
1190        /icon-app-\d+/i.test(n) ||
1191        /launchimage\.imageset\//i.test(n) ||
1192        /(?:^|\/)launch_image\./i.test(n)
1193      );
1194    }
1195
1196    function isCustomBrandIcon(path) {
1197      var n = String(path || '').toLowerCase().replace(/\\/g, '/');
1198      return (
1199        /assets\/(?:images\/)?branding\/(?:app_icon|logo|launcher)/i.test(n) ||
1200        /(?:^|\/)assets\/logo\.(png|svg|webp|jpe?g)$/i.test(n) ||
1201        /(?:^|\/)(?:logo|wordmark)\.(png|svg|webp|jpe?g)$/i.test(n)
1202      );
1203    }
1204
1205    /** UI art that should fill the phone screen (not a tiny centered tile). */
1206    function isFullBleedArt(path) {
1207      var n = String(path || '').toLowerCase().replace(/\\/g, '/');
1208      if (isTemplateFlutterIcon(n) || isCustomBrandIcon(n)) return false;
1209      // Skip launcher-ish assets that live under assets/images.
1210      if (/adaptive_foreground|adaptive_background|app_icon|launcher_icon|ic_launcher/i.test(n)) {
1211        return false;
1212      }
1213      if (/showcase\/|screenshot|screen[-_]?\d|project_cover|cover\.(png|jpe?g|webp)$/i.test(n)) return true;
1214      if (n.indexOf('assets/images/') === 0 && !/splash|perm-check|icons\//.test(n)) return true;
1215      if (n.indexOf('assets/') === 0 && !/splash|perm-check|icons\/|mipmap/.test(n)) return true;
1216      // Composer paste / upload previews sitting at project root.
1217      if (/^[^/]*(?:paste|screen|cover|preview|mock|shot)[^/]*\.(png|jpe?g|webp|gif)$/i.test(n)) return true;
1218      if (/^[^/]+\.(png|jpe?g|webp|gif)$/i.test(n) && !/icon|favicon|launcher/i.test(n)) return true;
1219      return false;
1220    }
1221
1222    /** Lower score wins. Real UI art first — never prefer Flutter default logo. */
1223    function coverRank(path) {
1224      var n = String(path || '').toLowerCase().replace(/\\/g, '/');
1225      if (isTemplateFlutterIcon(n)) return 80;
1226      if (n === 'showcase/cover.png' || /\/showcase\/cover\.(png|jpe?g|webp)$/.test(n)) return -20;
1227      if (/showcase\/cover\./.test(n) || /project_cover\./.test(n)) return -18;
1228      if (/showcase\/|showcase_|(?:^|\/)(?:app-)?screen-\d+/i.test(n)) return -14;
1229      if (/screenshot/i.test(n)) return -12;
1230      if (/assets\/images\/(?:onboarding|posters|hero|home|feed|empty)\//.test(n)) return -10;
1231      if (/^[^/]*(?:paste|screen|cover|preview|mock|shot)[^/]*\.(png|jpe?g|webp|gif)$/i.test(n)) return -9;
1232      if (n.indexOf('assets/images/') === 0 && !/splash|perm-check|icons\//.test(n)) return -8;
1233      if (/(?:^|\/)(?:logo|wordmark)\.(?:png|jpe?g|webp|svg)$/.test(n)) return -6;
1234      if (/assets\/(?:images\/)?branding\/logo\./.test(n)) return -5;
1235      if (/assets\/(?:images\/)?branding\/app_icon/.test(n)) return -3;
1236      if (n.indexOf('assets/') === 0 && !/splash|perm-check/.test(n)) return -1;
1237      if (/^[^/]+\.(png|jpe?g|webp|gif)$/i.test(n)) return 0;
1238      if (/favicon|launch_image|perm-check|icon-16|icon-32/i.test(n)) return 90;
1239      return 10;
1240    }
1241
1242    function coverKindForPath(path) {
1243      var n = String(path || '').toLowerCase().replace(/\\/g, '/');
1244      if (isFullBleedArt(n)) return 'screen';
1245      if (isCustomBrandIcon(n)) return 'logo';
1246      if (isTemplateFlutterIcon(n) || /app_icon|ic_launcher|icon-512|mipmap/i.test(n)) return 'icon';
1247      return 'image';
1248    }
1249
1250    // Probe only real covers / custom brand marks — never Flutter template logos.
1251    var KNOWN_ICON_SUFFIXES = [
1252      'showcase/cover.png',
1253      'assets/images/onboarding/onboarding_1.png',
1254      'assets/images/onboarding/onboarding_2.png',
1255      'assets/images/posters/poster_1.png',
1256      'assets/images/hero/hero.png',
1257      'assets/branding/app_icon_master_1024.png',
1258      'assets/branding/app_icon.png',
1259      'assets/images/branding/app_icon_master_1024.png',
1260      'assets/images/branding/app_icon.png',
1261      'assets/images/branding/logo.png',
1262      'assets/images/branding/logo.svg',
1263      'assets/logo.png',
1264      'assets/logo.svg',
1265    ];
1266    var KNOWN_ICON_PREFIXES = ['', 'apps/mobile/', 'mobile/', 'app/', 'apps/app/', 'apps/flutter/'];
1267
1268    function pickCoverRef(files) {
1269      if (!Array.isArray(files) || !files.length) return null;
1270      var SKIP = /(?:^|\/)(?:launch_image|perm-check|Icon-16|Icon-32|favicon\.ico|Icon-App-20|Icon-App-29|Icon-App-40|Icon-App-60|Icon-App-76|Icon-App-83)/i;
1271      var list = files.filter(function (f) {
1272        if (!f || f.type === 'dir') return false;
1273        var p = f.path || f.name || '';
1274        return /\.(png|jpe?g|webp|gif|avif|svg)$/i.test(p) && !SKIP.test(p);
1275      });
1276      if (!list.length) return null;
1277
1278      function sortByRank(items) {
1279        return items.slice().sort(function (a, b) {
1280          var pa = a.path || a.name || '';
1281          var pb = b.path || b.name || '';
1282          var d = coverRank(pa) - coverRank(pb);
1283          if (d !== 0) return d;
1284          var sizeDelta = (b.size || 0) - (a.size || 0);
1285          if (sizeDelta !== 0) return sizeDelta;
1286          return (b.mtime || 0) - (a.mtime || 0);
1287        });
1288      }
1289
1290      var uiArt = sortByRank(list.filter(function (f) {
1291        return isFullBleedArt(f.path || f.name || '');
1292      }));
1293      var brand = sortByRank(list.filter(function (f) {
1294        return isCustomBrandIcon(f.path || f.name || '');
1295      }));
1296      // Prefer real screens / UI assets over Flutter's default logo.
1297      var chosen = uiArt[0] || brand[0] || null;
1298      if (!chosen) {
1299        // No real art — skip template Flutter icons; avatar fallback looks better.
1300        return null;
1301      }
1302      var path = chosen.path || chosen.name;
1303      return { path: path, kind: coverKindForPath(path) };
1304    }
1305
1306    function showcaseCoverUrl(project) {
1307      var media = project && project.showcase && project.showcase.media;
1308      if (!media) return null;
1309      var images = media.images || [];
1310      for (var i = 0; i < images.length; i += 1) {
1311        var url = images[i] && images[i].url && String(images[i].url).trim();
1312        if (url) return url;
1313      }
1314      var poster = media.video && media.video.poster && String(media.video.poster).trim();
1315      return poster || null;
1316    }
1317
1318    function escapeXml(value) {
1319      return String(value || '')
1320        .replace(/&/g, '&amp;')
1321        .replace(/</g, '&lt;')
1322        .replace(/>/g, '&gt;')
1323        .replace(/"/g, '&quot;')
1324        .replace(/'/g, '&apos;');
1325    }
1326
1327    function projectAvatarDataUrl(name, seed) {
1328      var trimmed = String(name || '').trim();
1329      var words = trimmed.split(/\s+/).filter(Boolean);
1330      var initials = words.length >= 2
1331        ? ((words[0].charAt(0) || '') + (words[1].charAt(0) || '')).toUpperCase()
1332        : (trimmed ? trimmed.slice(0, 2).toUpperCase() : '?');
1333      var hash = 0;
1334      var key = String(seed || trimmed || 'x');
1335      for (var i = 0; i < key.length; i += 1) {
1336        hash = (hash * 31 + key.charCodeAt(i)) >>> 0;
1337      }
1338      var hue = hash % 360;
1339      var hue2 = (hue + 36) % 360;
1340      var svg =
1341        '<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">' +
1342        '<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">' +
1343        '<stop offset="0%" stop-color="hsl(' + hue + ' 68% 52%)"/>' +
1344        '<stop offset="100%" stop-color="hsl(' + hue2 + ' 72% 42%)"/>' +
1345        '</linearGradient></defs>' +
1346        '<rect width="256" height="256" rx="56" fill="url(#g)"/>' +
1347        '<text x="128" y="152" text-anchor="middle" font-size="' + (initials.length > 1 ? 88 : 104) + '" ' +
1348        'font-weight="700" fill="#ffffff" font-family="ui-sans-serif,system-ui,sans-serif">' +
1349        escapeXml(initials) + '</text></svg>';
1350      return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
1351    }
1352
1353    function applyThumbChrome(thumb, kind) {
1354      if (!thumb) return;
1355      thumb.classList.add('has-cover');
1356      if (kind) thumb.setAttribute('data-cover-kind', kind);
1357      // Keep phone chrome; media CSS decides tile vs fill-screen.
1358      thumb.classList.remove('is-bleed');
1359      thumb.classList.add('is-framed');
1360    }
1361
1362    function setThumbMedia(thumb, src, kind) {
1363      if (!thumb || !thumb.isConnected || !src) {
1364        return Promise.resolve(false);
1365      }
1366      var screen = thumb.querySelector('[data-guest-recent-screen]');
1367      if (!screen) return Promise.resolve(false);
1368      return new Promise(function (resolve) {
1369        var img = document.createElement('img');
1370        img.className = 'fgs-recent__thumb-media';
1371        img.alt = '';
1372        img.decoding = 'async';
1373        img.loading = 'eager';
1374        var settled = false;
1375        function finish(ok) {
1376          if (settled) return;
1377          settled = true;
1378          resolve(ok);
1379        }
1380        img.addEventListener('load', function () {
1381          if (!screen.isConnected) {
1382            finish(false);
1383            return;
1384          }
1385          screen.textContent = '';
1386          screen.appendChild(img);
1387          applyThumbChrome(thumb, kind);
1388          finish(true);
1389        });
1390        img.addEventListener('error', function () {
1391          finish(false);
1392        });
1393        img.src = src;
1394      });
1395    }
1396
1397    function probeAssetExists(url) {
1398      return fetch(url, {
1399        method: 'GET',
1400        credentials: 'include',
1401        headers: { Range: 'bytes=0-0' },
1402        cache: 'force-cache',
1403      }).then(function (resp) {
1404        return resp.ok || resp.status === 206;
1405      }).catch(function () {
1406        return false;
1407      });
1408    }
1409
1410    function loadPathCover(thumb, projectId, coverPath) {
1411      var kind = coverKindForPath(coverPath);
1412      var url = projectRawUrl(projectId, coverPath);
1413      return setThumbMedia(thumb, url, kind);
1414    }
1415
1416    function tryKnownIconPaths(thumb, projectId) {
1417      var paths = [];
1418      KNOWN_ICON_PREFIXES.forEach(function (prefix) {
1419        KNOWN_ICON_SUFFIXES.forEach(function (suffix) {
1420          paths.push(prefix + suffix);
1421        });
1422      });
1423      var i = 0;
1424      function next() {
1425        if (!thumb.isConnected || i >= paths.length) {
1426          return Promise.resolve(false);
1427        }
1428        var path = paths[i++];
1429        var url = projectRawUrl(projectId, path);
1430        return probeAssetExists(url).then(function (ok) {
1431          if (!ok) return next();
1432          return loadPathCover(thumb, projectId, path).then(function (shown) {
1433            return shown ? true : next();
1434          });
1435        });
1436      }
1437      return next();
1438    }
1439
1440    function projectStatusValue(project) {
1441      var s = project && project.status;
1442      if (s && typeof s === 'object' && typeof s.value === 'string') return s.value;
1443      if (typeof s === 'string') return s;
1444      return 'not_started';
1445    }
1446
1447    function statusLabel(status) {
1448      var map = {
1449        not_started: 'Not started',
1450        queued: 'Queued',
1451        running: 'Building',
1452        awaiting_input: 'Needs input',
1453        succeeded: 'Completed',
1454        failed: 'Failed',
1455        canceled: 'Canceled',
1456        published: 'Published',
1457      };
1458      return map[status] || 'Not started';
1459    }
1460
1461    function projectInitials(name) {
1462      var words = String(name || '').trim().split(/\s+/).filter(Boolean);
1463      if (words.length >= 2) {
1464        return ((words[0].charAt(0) || '') + (words[1].charAt(0) || '')).toUpperCase() || '?';
1465      }
1466      var one = words[0] || '?';
1467      return one.slice(0, 2).toUpperCase();
1468    }
1469
1470    function skillLabel(project) {
1471      var skill = project && project.skillId ? String(project.skillId).trim() : '';
1472      return skill || 'fluttergo-pipeline';
1473    }
1474
1475    function paintAvatarFallback(thumb, name, projectId) {
1476      var screen = thumb && thumb.querySelector('[data-guest-recent-screen]');
1477      if (!screen) return;
1478      var img = document.createElement('img');
1479      img.className = 'fgs-recent__thumb-media';
1480      img.alt = '';
1481      img.src = projectAvatarDataUrl(name, projectId);
1482      screen.textContent = '';
1483      screen.appendChild(img);
1484      applyThumbChrome(thumb, 'avatar');
1485    }
1486
1487    function hydrateCover(thumb, project) {
1488      var projectId = project && project.id;
1489      if (!projectId || !thumb) return;
1490      var name = project.name && String(project.name).trim() ? String(project.name).trim() : 'Untitled';
1491      var epoch = (Number(thumb.dataset.coverEpoch) || 0) + 1;
1492      thumb.dataset.coverEpoch = String(epoch);
1493
1494      // Instant unique avatar — real assets replace this when found.
1495      paintAvatarFallback(thumb, name, projectId);
1496
1497      function stillCurrent() {
1498        return thumb.isConnected && thumb.dataset.coverEpoch === String(epoch);
1499      }
1500
1501      function showCover(src, kind) {
1502        if (!stillCurrent() || !src) return Promise.resolve(false);
1503        return setThumbMedia(thumb, src, kind).then(function (ok) {
1504          return ok && stillCurrent();
1505        });
1506      }
1507
1508      var showcaseUrl = showcaseCoverUrl(project);
1509      var chain = showcaseUrl
1510        ? showCover(showcaseUrl, 'screen')
1511        : Promise.resolve(false);
1512
1513      chain
1514        .then(function (ok) {
1515          if (ok || !stillCurrent()) return true;
1516          return fetch('/api/projects/' + encodeURIComponent(projectId) + '/files', {
1517            credentials: 'include',
1518            cache: 'no-store',
1519          })
1520            .then(function (resp) { return resp.ok ? resp.json() : null; })
1521            .then(function (json) {
1522              if (!stillCurrent()) return true;
1523              var cover = pickCoverRef(json && json.files);
1524              if (cover) {
1525                return showCover(projectRawUrl(projectId, cover.path), cover.kind).then(function (shown) {
1526                  if (shown || !stillCurrent()) return true;
1527                  return tryKnownIconPaths(thumb, projectId);
1528                });
1529              }
1530              return tryKnownIconPaths(thumb, projectId);
1531            });
1532        })
1533        .catch(function () {
1534          if (!stillCurrent()) return false;
1535          return tryKnownIconPaths(thumb, projectId);
1536        });
1537    }
1538
1539    fetch('/api/projects', { credentials: 'include', cache: 'no-store' })
1540      .then(function (resp) { return resp.ok ? resp.json() : null; })
1541      .then(function (data) {
1542        var projects = data && Array.isArray(data.projects) ? data.projects.slice() : [];
1543        projects = projects.filter(isFlutterProject);
1544        projects.sort(function (a, b) {
1545          return (b.updatedAt || 0) - (a.updatedAt || 0);
1546        });
1547        var total = projects.length;
1548        projects = projects.slice(0, 12);
1549        row.textContent = '';
1550        if (!projects.length) {
1551          section.hidden = true;
1552          if (countEl) {
1553            countEl.hidden = true;
1554            countEl.textContent = '';
1555          }
1556          return;
1557        }
1558        if (countEl) {
1559          countEl.hidden = false;
1560          countEl.textContent = String(total);
1561        }
1562        projects.forEach(function (project) {
1563          var id = project && typeof project.id === 'string' ? project.id : '';
1564          if (!id) return;
1565          var name = project.name && String(project.name).trim() ? String(project.name).trim() : 'Untitled';
1566          var status = projectStatusValue(project);
1567          var statusText = statusLabel(status);
1568          var relative = formatRelativeTime(project.updatedAt) || 'Recent';
1569          var skill = skillLabel(project);
1570
1571          var card = document.createElement('a');
1572          card.className = 'fgs-recent__card';
1573          card.href = '/projects/' + encodeURIComponent(id);
1574          card.setAttribute('role', 'listitem');
1575          card.title = name;
1576
1577          var thumb = document.createElement('div');
1578          thumb.className = 'fgs-recent__thumb is-framed';
1579          thumb.setAttribute('aria-hidden', 'true');
1580
1581          var stage = document.createElement('div');
1582          stage.className = 'fgs-recent__preview-stage';
1583          var device = document.createElement('div');
1584          device.className = 'fgs-recent__device';
1585          var screen = document.createElement('div');
1586          screen.className = 'fgs-recent__device-screen';
1587          screen.setAttribute('data-guest-recent-screen', '');
1588          var glyph = document.createElement('span');
1589          glyph.className = 'fgs-recent__glyph';
1590          glyph.textContent = projectInitials(name);
1591          screen.appendChild(glyph);
1592          device.appendChild(screen);
1593          stage.appendChild(device);
1594          thumb.appendChild(stage);
1595
1596          // Brand watermark under the phone while covers resolve.
1597          var mark = document.createElement('img');
1598          mark.className = 'fgs-recent__brand-mark';
1599          mark.src = '/brand/fluttergo-appicon.svg';
1600          mark.alt = '';
1601          mark.setAttribute('aria-hidden', 'true');
1602          thumb.appendChild(mark);
1603
1604          var statusPill = document.createElement('span');
1605          statusPill.className = 'fgs-recent__status-pill fgs-recent__status-pill--' + status;
1606          statusPill.textContent = statusText;
1607          thumb.appendChild(statusPill);
1608
1609          var body = document.createElement('div');
1610          body.className = 'fgs-recent__body';
1611
1612          var tag = document.createElement('span');
1613          tag.className = 'fgs-recent__tag';
1614          tag.textContent = 'Prototype';
1615
1616          var title = document.createElement('div');
1617          title.className = 'fgs-recent__name';
1618          title.textContent = name;
1619
1620          var lead = document.createElement('div');
1621          lead.className = 'fgs-recent__lead';
1622          lead.textContent = 'freeform · ' + skill;
1623
1624          var foot = document.createElement('div');
1625          foot.className = 'fgs-recent__foot fgs-recent__foot--' + status;
1626          foot.textContent = statusText + ' · ' + relative;
1627
1628          body.appendChild(tag);
1629          body.appendChild(title);
1630          body.appendChild(lead);
1631          body.appendChild(foot);
1632
1633          card.appendChild(thumb);
1634          card.appendChild(body);
1635          row.appendChild(card);
1636          hydrateCover(thumb, project);
1637        });
1638        section.hidden = row.children.length === 0;
1639      })
1640      .catch(function () {
1641        section.hidden = true;
1642      });
1643  }
1644
1645  /* ---- Landing prompt → Flutter pipeline + attachments -----------------
1646     Product mode is always Android & iOS (fluttergo-pipeline). Attachments,
1647     Figma/website links, and file upload reuse the Home create handoff. */
1648  (function bindGuestPrompt() {
1649    var form = document.querySelector('[data-guest-prompt-form]');
1650    var input = document.querySelector('[data-guest-prompt-input]');
1651    if (!form || !input) return;
1652
1653    var submit = form.querySelector('[data-guest-prompt-submit]');
1654    var statusEl = form.querySelector('[data-guest-prompt-status]');
1655    var statusRow = form.querySelector('[data-guest-prompt-status-row]');
1656    var figmaConnectBtn = form.querySelector('[data-guest-figma-connect]');
1657    var figmaProgress = form.querySelector('[data-guest-figma-progress]');
1658    var figmaPhaseEl = form.querySelector('[data-guest-figma-phase]');
1659    var figmaCountsEl = form.querySelector('[data-guest-figma-counts]');
1660    var figmaInventoryEl = form.querySelector('[data-guest-figma-inventory]');
1661    var figmaViewAllBtn = form.querySelector('[data-guest-figma-view-all]');
1662    var figmaExportsModal = document.querySelector('[data-guest-figma-exports]');
1663    var figmaExportsLead = document.querySelector('[data-guest-figma-exports-lead]');
1664    var figmaExportsStats = document.querySelector('[data-guest-figma-exports-stats]');
1665    var figmaExportsScreens = document.querySelector('[data-guest-figma-exports-screens]');
1666    var figmaExportsAssets = document.querySelector('[data-guest-figma-exports-assets]');
1667    var lastFigmaInventory = {
1668      screenNames: [],
1669      screenCount: 0,
1670      assetCount: 0,
1671      phase: '',
1672      screenPreviews: [],
1673      projectId: '',
1674    };
1675    var figmaConnectPoll = null;
1676    var box = form.querySelector('[data-guest-dropzone]') || form.querySelector('.fgs-prompt__box') || form;
1677    var examplesRow = document.querySelector('[data-guest-examples-row]');
1678    var examplesLabel = document.querySelector('[data-guest-examples-label]');
1679    var refreshBtn = document.querySelector('[data-guest-examples-refresh]');
1680    var chipsEl = form.querySelector('[data-guest-attach-chips]');
1681    var attachRoot = form.querySelector('[data-guest-attach]');
1682    var attachToggle = form.querySelector('[data-guest-attach-toggle]');
1683    var attachMenu = form.querySelector('[data-guest-attach-menu]');
1684    var fileInput = form.querySelector('[data-guest-file-input]');
1685    var imageInput = form.querySelector('[data-guest-image-input]');
1686    var zipInput = form.querySelector('[data-guest-zip-input]');
1687    var githubModal = document.querySelector('[data-guest-github-modal]');
1688    var githubRepo = document.querySelector('[data-guest-github-repo]');
1689    var githubBranch = document.querySelector('[data-guest-github-branch]');
1690    var githubName = document.querySelector('[data-guest-github-name]');
1691    var githubStatus = document.querySelector('[data-guest-github-status]');
1692    var githubConnectBtn = document.querySelector('[data-guest-github-connect]');
1693    var githubSubmitBtn = document.querySelector('[data-guest-github-submit]');
1694    var githubError = document.querySelector('[data-guest-github-error]');
1695    var githubLead = document.querySelector('[data-guest-github-lead]');
1696    var githubChoice = document.querySelector('[data-guest-github-choice]');
1697    var githubFields = document.querySelector('[data-guest-github-fields]');
1698    var githubFoot = document.querySelector('[data-guest-github-foot]');
1699    var githubConnectRow = document.querySelector('[data-guest-github-connect-row]');
1700    var githubBack = document.querySelector('[data-guest-github-back]');
1701    var githubVisibility = null;
1702    var githubInstalled = false;
1703    var linkDialog = form.querySelector('[data-guest-link-dialog]');
1704    var linkInput = form.querySelector('[data-guest-link-input]');
1705    var linkLabel = form.querySelector('[data-guest-link-label]');
1706    var linkCancel = form.querySelector('[data-guest-link-cancel]');
1707    var linkConfirm = form.querySelector('[data-guest-link-confirm]');
1708    var submitting = false;
1709    var stagedFiles = [];
1710    var stagedLinks = [];
1711    var pendingLinkKind = null;
1712
1713    var EXAMPLE_POOL = [
1714      'Food delivery app with live order tracking',
1715      'Habit tracker with streaks and weekly insights',
1716      'Salon booking app with calendar and reminders',
1717      'Expense tracker with charts and budgets',
1718      'Quiz app with leaderboards and shareable results',
1719      'Social feed app with profiles and messaging',
1720      'Booking app for a clinic with calendar and reminders',
1721      'Fitness app with workouts, streaks, and progress charts',
1722    ];
1723
1724    var BUILD_STATUS = [
1725      'Understanding your idea…',
1726      'Preparing your workspace…',
1727      'Uploading references…',
1728      'Opening your project…',
1729      'Almost there…',
1730      'Still opening your project…',
1731    ];
1732
1733    var FIGMA_BUILD_STATUS = [
1734      'Creating your Flutter workspace…',
1735      'Connecting to your Figma account…',
1736      'Reading your Figma file…',
1737      'Listing screens and frames…',
1738      'Exporting screen PNGs…',
1739      'Downloading icons and buttons…',
1740      'Writing figma/OVERVIEW.md…',
1741      'Still pulling from Figma…',
1742    ];
1743
1744    var activeBuildStatus = BUILD_STATUS;
1745
1746    function uuid() {
1747      try {
1748        if (window.crypto && typeof window.crypto.randomUUID === 'function') {
1749          return window.crypto.randomUUID();
1750        }
1751      } catch (e) {}
1752      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
1753        var r = (Math.random() * 16) | 0;
1754        var v = c === 'x' ? r : (r & 0x3) | 0x8;
1755        return v.toString(16);
1756      });
1757    }
1758
1759    function isGenericCreatePrompt(prompt) {
1760      var cleaned = String(prompt || '').trim();
1761      if (!cleaned) return true;
1762      if (/^(please\s+|kindly\s+|dear\s+)?(can you\s+|could you\s+|help me\s+|help\s+|i want to\s+|i need to\s+)?(create|build|make|design|develop|generate)\s+(me\s+)?(an?\s+|the\s+)?(app|application|mobile app|flutter app|android app|ios app)(\s+(similar|like|from|based on|using|with|to)(\s+to)?\s+(the\s+)?(attached\s+)?(image|screenshot|mockup|reference|screen\s*flow|ui|design|provided\s+image|screen))?[\s.!?,]*$/i.test(cleaned)) {
1763        return true;
1764      }
1765      var polite = cleaned.replace(/^(please\s+|kindly\s+|dear\s+)/i, '').trim();
1766      var stripped = polite.replace(/^(please\s+|kindly\s+|dear\s+)?(can you\s+|could you\s+|help me\s+|i want to\s+|i need to\s+)?(create|build|make|design|implement|add|fix|update|improve|optimize|generate|write|turn|develop)\s+((this|that)\s+into\s+)?(an|a|the|this|that|my)?\s*/i, '').trim();
1767      var stop = { a:1, an:1, and:1, app:1, application:1, attached:1, build:1, create:1, design:1, flow:1, for:1, from:1, image:1, kindly:1, like:1, make:1, me:1, mobile:1, mockup:1, my:1, of:1, on:1, please:1, reference:1, screen:1, screenshot:1, similar:1, the:1, this:1, to:1, with:1 };
1768      var words = stripped.replace(/[^\p{L}\p{N}\s-]/gu, ' ').split(/\s+/).filter(Boolean).filter(function (w) { return !stop[w.toLowerCase()]; }).slice(0, 6);
1769      if (words.length) return false;
1770      var tokens = cleaned.toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, ' ').split(/\s+/).filter(Boolean);
1771      return tokens.length <= 8;
1772    }
1773
1774    function projectNameFromPrompt(prompt, opts) {
1775      opts = opts || {};
1776      var hasImages = !!opts.hasImages;
1777      var cleaned = String(prompt || '').trim();
1778      if (!cleaned) return 'Untitled project';
1779      if (isGenericCreatePrompt(cleaned)) {
1780        return hasImages ? 'Untitled project' : 'Untitled project';
1781      }
1782      var polite = cleaned.replace(/^(please\s+|kindly\s+|dear\s+)/i, '').trim();
1783      var stripped = polite.replace(/^(please\s+|kindly\s+|dear\s+)?(can you\s+|could you\s+|help me\s+|i want to\s+|i need to\s+)?(create|build|make|design|implement|add|fix|update|improve|optimize|generate|write|turn|develop)\s+((this|that)\s+into\s+)?(an|a|the|this|that|my)?\s*/i, '').trim();
1784      var stop = { a:1, an:1, and:1, app:1, application:1, attached:1, build:1, create:1, design:1, flow:1, for:1, from:1, image:1, kindly:1, like:1, make:1, me:1, mobile:1, mockup:1, my:1, of:1, on:1, please:1, reference:1, screen:1, screenshot:1, similar:1, the:1, this:1, to:1, with:1 };
1785      var words = stripped.replace(/[^\p{L}\p{N}\s-]/gu, ' ').split(/\s+/).filter(Boolean).filter(function (w) { return !stop[w.toLowerCase()]; }).slice(0, 6);
1786      var title = words.map(function (w) { return w.charAt(0).toUpperCase() + w.slice(1).toLowerCase(); }).join(' ');
1787      if (title) return title.length > 48 ? title.slice(0, 47).trim() + '…' : title;
1788      if (hasImages || isGenericCreatePrompt(cleaned)) return 'Untitled project';
1789      var head = cleaned.split(/\s+/).slice(0, 8).join(' ');
1790      if (!head) return 'Untitled project';
1791      return head.length > 48 ? head.slice(0, 47).trim() + '…' : head;
1792    }
1793
1794    function stripLegacyFlutterCreateWrapper(brief) {
1795      // Older clients appended Skip Quick setup / quality-bar recipes. Cut them
1796      // so stashed guest intents and long-cached JS still pass user words only.
1797      var text = String(brief || '').trim();
1798      if (!text) return text;
1799      var markers = [
1800        /\nThis is a new Flutter app from FlutterGo[^\n]*/i,
1801        /\nSkip Quick setup and every clarifying question-form[^\n]*/i,
1802        /\nRequired quality bar \(already in skills[^\n]*/i,
1803        /\nInfer the app baseline from my idea[^\n]*/i,
1804        /\nFirst actions: TodoWrite[^\n]*/i,
1805      ];
1806      var cut = text.length;
1807      for (var i = 0; i < markers.length; i++) {
1808        var m = text.match(markers[i]);
1809        if (m && typeof m.index === 'number' && m.index < cut) cut = m.index;
1810      }
1811      var figmaBlockIdx = text.indexOf('Figma file URL(s) in this brief are the');
1812      if (figmaBlockIdx >= 0 && figmaBlockIdx < cut) cut = figmaBlockIdx;
1813      var out = text.slice(0, cut).trim().replace(/^Product idea:\s*/i, '').trim();
1814      return out || text;
1815    }
1816
1817    function composeUserBrief(prompt) {
1818      // Pass the user's words through. Append staged links as plain URLs only —
1819      // no FlutterGo recipe / quality-bar essay wrapped around the brief.
1820      var parts = [stripLegacyFlutterCreateWrapper(prompt)];
1821      stagedLinks.forEach(function (link) {
1822        if (!link || !link.url) return;
1823        var label = link.kind === 'figma' ? 'Figma'
1824          : link.kind === 'website' ? 'Website'
1825          : link.kind === 'github' ? 'GitHub'
1826          : 'Link';
1827        parts.push(label + ': ' + link.url);
1828      });
1829      stagedFiles.forEach(function (entry) {
1830        if (!entry || !looksLikeZip(entry.file)) return;
1831        parts.push('ZIP: ' + (entry.file.name || 'upload.zip'));
1832      });
1833      return parts.filter(Boolean).join('\n\n');
1834    }
1835
1836    function kickoffPrompt(userBrief, backendProvider) {
1837      // User input only — plus a Figma SoT block when a Figma link is staged
1838      // or present in the brief (mirrors buildFlutterHomeKickoffPrompt /
1839      // FIGMA_REFERENCE_KICKOFF_BLOCK). After extract, startBuilding rewrites
1840      // pendingPrompt with the sequential queue (foundation + screen 0 only).
1841      var brief = stripLegacyFlutterCreateWrapper(userBrief);
1842      var hasFigma = stagedLinks.some(function (link) {
1843        return link && link.kind === 'figma';
1844      });
1845      if (!hasFigma && /figma\.com\/(?:design|file|proto)\//i.test(brief)) hasFigma = true;
1846      var out = brief;
1847      if (hasFigma) {
1848        var block = [
1849          'Figma file URL(s) in this brief are the visual source of truth for the app.',
1850          'Use the figma-extract on disk (`figma/`) when present. Match each screen 1:1 — do not redesign or collapse frames.',
1851        ].join('\n');
1852        var urls = [];
1853        stagedLinks.forEach(function (link) {
1854          if (link && link.kind === 'figma' && link.url) urls.push(link.url);
1855        });
1856        var parts = [brief, '', block];
1857        urls.forEach(function (url) {
1858          if (brief.indexOf(url) === -1) parts.push('Figma: ' + url);
1859        });
1860        out = parts.filter(Boolean).join('\n');
1861      }
1862      var zipNames = stagedFiles
1863        .filter(function (entry) { return entry && looksLikeZip(entry.file); })
1864        .map(function (entry) { return entry.file.name || 'upload.zip'; });
1865      if (zipNames.length && out.indexOf('Attached ZIP') === -1) {
1866        out = (out ? out + '\n\n' : '') +
1867          'Attached ZIP (' + zipNames.join(', ') + ') is uploaded with this project. Extract it and use the contents as the source or visual reference for the app.';
1868      }
1869      return appendBackendKickoff(out, backendProvider);
1870    }
1871
1872    function appendBackendKickoff(prompt, backendProvider) {
1873      if (!backendProvider) return prompt || '';
1874      var block =
1875        backendProvider === 'supabase'
1876          ? 'Backend: Supabase — invoke `flutter-supabase-backend`. Prefer project secrets `SUPABASE_URL` / `SUPABASE_ANON_KEY` when present; never put the service-role key in Flutter.'
1877          : 'Backend: none — do not assume Supabase; use local/mock data unless the user asks for a backend later.';
1878      var trimmed = String(prompt || '').trim();
1879      if (!trimmed) return block;
1880      if (
1881        trimmed.indexOf('Backend: Supabase') !== -1
1882        || trimmed.indexOf('Backend: none') !== -1
1883      ) {
1884        return trimmed;
1885      }
1886      return trimmed + '\n\n' + block;
1887    }
1888
1889    function sequentialFigmaKickoff(userBrief, build) {
1890      var brief = stripLegacyFlutterCreateWrapper(userBrief);
1891      if (!build || !Array.isArray(build.screens) || build.screens.length === 0) {
1892        return kickoffPrompt(userBrief);
1893      }
1894      var screen = build.screens[typeof build.index === 'number' ? build.index : 0] || build.screens[0];
1895      var total = typeof build.total === 'number' ? build.total : build.screens.length;
1896      var maxPlan = 40;
1897      var planLines = [];
1898      for (var i = 0; i < build.screens.length && i < maxPlan; i++) {
1899        var s = build.screens[i];
1900        var p = s && s.previewAsset
1901          ? (String(s.previewAsset).indexOf('figma/') === 0
1902            ? s.previewAsset
1903            : 'figma/' + String(s.previewAsset).replace(/^\//, ''))
1904          : 'figma/assets/ (preview for node ' + (s && s.id ? s.id : '?') + ')';
1905        planLines.push((i + 1) + '. `' + (s.name || 'Untitled') + '` (`' + (s.id || '') + '`) — preview `' + p + '`');
1906      }
1907      if (build.screens.length > maxPlan) {
1908        planLines.push('… +' + (build.screens.length - maxPlan) + ' more in `figma/screens.json` / `figma/screen-plans.json`');
1909      }
1910      var preview = screen && screen.previewAsset
1911        ? (String(screen.previewAsset).indexOf('figma/') === 0
1912          ? screen.previewAsset
1913          : 'figma/' + String(screen.previewAsset).replace(/^\//, ''))
1914        : null;
1915      var lines = [
1916        brief,
1917        '',
1918        'Build this Flutter app from the Figma extract already on disk (`figma/`). Match each screen 1:1 — keep the existing product direction from my brief.',
1919        '',
1920        '## Screens (' + Math.max(total, 1) + ')',
1921        '',
1922      ].concat(planLines).concat([
1923        '',
1924        'Details per screen: `figma/screen-plans.json`.',
1925        '',
1926        '## Start now',
1927        screen
1928          ? ('Begin with foundation + **screen 1**: `' + (screen.name || 'Untitled') + '` (`' + (screen.id || '') + '`)' + (preview ? (' — open `' + preview + '` first, then implement 1:1. Continue through the remaining screens until the plan is done.') : ' — open its preview PNG first. Continue until every screen is done.'))
1929          : 'Begin with foundation + the first screen from the list (open its preview PNG first). Continue until every screen is done.',
1930      ]);
1931      return lines.filter(Boolean).join('\n').trim();
1932    }
1933
1934
1935    function looksLikeImage(file) {
1936      return file && ((file.type && file.type.indexOf('image/') === 0) || /\.(png|jpe?g|gif|webp|svg|avif|bmp)$/i.test(file.name || ''));
1937    }
1938
1939    function looksLikeZip(file) {
1940      if (!file) return false;
1941      var type = String(file.type || '').toLowerCase();
1942      if (type === 'application/zip' || type === 'application/x-zip-compressed' || type === 'application/x-zip') {
1943        return true;
1944      }
1945      return /\.zip$/i.test(file.name || '');
1946    }
1947
1948    function formatBytes(n) {
1949      if (!Number.isFinite(n) || n < 0) return '';
1950      if (n < 1024) return n + ' B';
1951      if (n < 1024 * 1024) return Math.round(n / 1024) + ' KB';
1952      return (n / (1024 * 1024)).toFixed(1) + ' MB';
1953    }
1954
1955    function normalizeFigmaFileUrl(raw) {
1956      var trimmed = String(raw || '').trim().replace(/[.,;:!?]+$/, '');
1957      if (!trimmed) return '';
1958      if (!/(?:^|\/\/)(?:www\.)?figma\.com\/(?:design|file|proto)\//i.test(trimmed)) {
1959        return '';
1960      }
1961      var withScheme = /^https?:\/\//i.test(trimmed) ? trimmed : 'https://' + trimmed;
1962      try {
1963        var parsed = new URL(withScheme);
1964        if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return '';
1965        var host = parsed.hostname.replace(/^www\./i, '').toLowerCase();
1966        if (host !== 'figma.com' && !/\.figma\.com$/i.test(host)) {
1967          return '';
1968        }
1969        if (!/^\/(?:design|file|proto)\//i.test(parsed.pathname)) return '';
1970        return parsed.toString();
1971      } catch (e) {
1972        return '';
1973      }
1974    }
1975
1976    function scrapeFigmaFileUrlFromText(text) {
1977      var match = String(text || '').match(
1978        /(?:https?:\/\/)?(?:www\.)?figma\.com\/(?:design|file|proto)\/[^\s)\]>'"]+/i,
1979      );
1980      return match ? normalizeFigmaFileUrl(match[0]) : '';
1981    }
1982
1983    function normalizeUrl(raw) {
1984      var value = String(raw || '').trim();
1985      if (!value) return '';
1986      // Bare Figma shares are common; normalize before the generic https:// prefix.
1987      var figma = normalizeFigmaFileUrl(value);
1988      if (figma) return figma;
1989      if (!/^https?:\/\//i.test(value)) value = 'https://' + value;
1990      try {
1991        var parsed = new URL(value);
1992        if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return '';
1993        return parsed.toString();
1994      } catch (e) {
1995        return '';
1996      }
1997    }
1998
1999    function detectLinkKind(url) {
2000      try {
2001        var host = new URL(url).hostname.replace(/^www\./, '').toLowerCase();
2002        if (host === 'figma.com' || host.endsWith('.figma.com')) return 'figma';
2003        if (host === 'github.com' || host === 'gist.github.com') return 'github';
2004        return 'website';
2005      } catch (e) {
2006        return 'website';
2007      }
2008    }
2009
2010    function linkTitle(kind, url) {
2011      try {
2012        var u = new URL(url);
2013        if (kind === 'figma') return 'Figma · ' + (u.pathname.split('/').filter(Boolean)[1] || u.hostname);
2014        if (kind === 'github') {
2015          var parts = u.pathname.split('/').filter(Boolean);
2016          return parts.length >= 2 ? parts[0] + '/' + parts[1] : u.hostname;
2017        }
2018        return u.hostname.replace(/^www\./, '');
2019      } catch (e) {
2020        return url;
2021      }
2022    }
2023
2024    function closeAttachMenu() {
2025      if (!attachMenu || !attachToggle) return;
2026      attachMenu.hidden = true;
2027      attachToggle.setAttribute('aria-expanded', 'false');
2028    }
2029
2030    function openAttachMenu() {
2031      if (!attachMenu || !attachToggle) return;
2032      attachMenu.hidden = false;
2033      attachToggle.setAttribute('aria-expanded', 'true');
2034    }
2035
2036    function closeLinkDialog() {
2037      if (!linkDialog) return;
2038      linkDialog.hidden = true;
2039      pendingLinkKind = null;
2040      if (linkInput) linkInput.value = '';
2041    }
2042
2043    function openLinkDialog(kind) {
2044      pendingLinkKind = kind || 'website';
2045      closeAttachMenu();
2046      if (!linkDialog) return;
2047      if (linkLabel) {
2048        linkLabel.textContent = pendingLinkKind === 'figma'
2049          ? 'Paste a Figma file link'
2050          : 'Paste a website URL';
2051      }
2052      if (linkInput) {
2053        linkInput.placeholder = pendingLinkKind === 'figma'
2054          ? 'https://www.figma.com/file/…'
2055          : 'https://example.com';
2056        linkInput.value = '';
2057      }
2058      linkDialog.hidden = false;
2059      if (linkInput) linkInput.focus();
2060    }
2061
2062    function renderAttachChips() {
2063      if (!chipsEl) return;
2064      chipsEl.textContent = '';
2065      var hasFiles = stagedFiles.length > 0;
2066      var hasLinks = stagedLinks.length > 0;
2067      if (!hasFiles && !hasLinks) {
2068        chipsEl.hidden = true;
2069        return;
2070      }
2071      chipsEl.hidden = false;
2072
2073      stagedLinks.forEach(function (link, index) {
2074        var chip = document.createElement('div');
2075        chip.className = 'fgs-attach-chip fgs-attach-chip--link';
2076        var glyph = document.createElement('span');
2077        glyph.className = 'fgs-attach-chip__glyph';
2078        glyph.textContent = link.kind === 'figma' ? 'F' : link.kind === 'github' ? 'G' : 'W';
2079        chip.appendChild(glyph);
2080        var meta = document.createElement('div');
2081        meta.className = 'fgs-attach-chip__meta';
2082        var name = document.createElement('div');
2083        name.className = 'fgs-attach-chip__name';
2084        name.textContent = link.title || link.url;
2085        var type = document.createElement('div');
2086        type.className = 'fgs-attach-chip__type';
2087        type.textContent = link.kind === 'figma' ? 'Figma link' : link.kind === 'github' ? 'GitHub' : 'Website URL';
2088        meta.appendChild(name);
2089        meta.appendChild(type);
2090        chip.appendChild(meta);
2091        var remove = document.createElement('button');
2092        remove.type = 'button';
2093        remove.className = 'fgs-attach-chip__remove';
2094        remove.setAttribute('aria-label', 'Remove link');
2095        remove.textContent = '×';
2096        remove.addEventListener('click', function () {
2097          stagedLinks.splice(index, 1);
2098          renderAttachChips();
2099        });
2100        chip.appendChild(remove);
2101        chipsEl.appendChild(chip);
2102      });
2103
2104      stagedFiles.forEach(function (entry, index) {
2105        var chip = document.createElement('div');
2106        chip.className = 'fgs-attach-chip';
2107        if (entry.previewUrl) {
2108          var img = document.createElement('img');
2109          img.className = 'fgs-attach-chip__thumb';
2110          img.src = entry.previewUrl;
2111          img.alt = '';
2112          chip.appendChild(img);
2113        } else {
2114          var glyph = document.createElement('span');
2115          glyph.className = 'fgs-attach-chip__glyph';
2116          glyph.textContent = (entry.file.name || '?').charAt(0).toUpperCase();
2117          chip.appendChild(glyph);
2118        }
2119        var meta = document.createElement('div');
2120        meta.className = 'fgs-attach-chip__meta';
2121        var name = document.createElement('div');
2122        name.className = 'fgs-attach-chip__name';
2123        name.textContent = entry.file.name || 'file';
2124        var type = document.createElement('div');
2125        type.className = 'fgs-attach-chip__type';
2126        type.textContent = (looksLikeImage(entry.file) ? 'Image' : looksLikeZip(entry.file) ? 'ZIP' : 'File') +
2127          (entry.file.size ? ' · ' + formatBytes(entry.file.size) : '');
2128        meta.appendChild(name);
2129        meta.appendChild(type);
2130        chip.appendChild(meta);
2131        var remove = document.createElement('button');
2132        remove.type = 'button';
2133        remove.className = 'fgs-attach-chip__remove';
2134        remove.setAttribute('aria-label', 'Remove ' + (entry.file.name || 'file'));
2135        remove.textContent = '×';
2136        remove.addEventListener('click', function () {
2137          if (entry.previewUrl) {
2138            try { URL.revokeObjectURL(entry.previewUrl); } catch (e) {}
2139          }
2140          stagedFiles.splice(index, 1);
2141          renderAttachChips();
2142        });
2143        chip.appendChild(remove);
2144        chipsEl.appendChild(chip);
2145      });
2146    }
2147
2148    function stageFiles(fileList) {
2149      var files = Array.prototype.slice.call(fileList || []);
2150      files.forEach(function (file) {
2151        if (!file) return;
2152        var exists = stagedFiles.some(function (entry) {
2153          return entry.file.name === file.name && entry.file.size === file.size && entry.file.lastModified === file.lastModified;
2154        });
2155        if (exists) return;
2156        var previewUrl = looksLikeImage(file) ? URL.createObjectURL(file) : null;
2157        stagedFiles.push({ file: file, previewUrl: previewUrl });
2158      });
2159      renderAttachChips();
2160      if (files.some(looksLikeZip)) {
2161        showReadyStatus('Your zip is uploaded. Describe the app or click Start building.');
2162      }
2163    }
2164
2165    function stageLink(kind, rawUrl) {
2166      var url = normalizeUrl(rawUrl);
2167      if (!url) return false;
2168      var resolvedKind = kind || detectLinkKind(url);
2169      if (kind === 'figma' && detectLinkKind(url) !== 'figma') {
2170        // Still allow — user may paste a Figma share host we don't classify.
2171        resolvedKind = 'figma';
2172      }
2173      var exists = stagedLinks.some(function (entry) { return entry.url === url; });
2174      if (exists) return true;
2175      stagedLinks.push({
2176        kind: resolvedKind,
2177        url: url,
2178        title: linkTitle(resolvedKind, url),
2179      });
2180      renderAttachChips();
2181      return true;
2182    }
2183
2184    function uploadProjectFiles(projectId, files) {
2185      if (!files.length) return Promise.resolve([]);
2186      var uploaded = [];
2187      var chain = Promise.resolve();
2188      for (var i = 0; i < files.length; i += 12) {
2189        (function (batch) {
2190          chain = chain.then(function () {
2191            var formData = new FormData();
2192            batch.forEach(function (file) { formData.append('files', file); });
2193            return fetch('/api/projects/' + encodeURIComponent(projectId) + '/upload', {
2194              method: 'POST',
2195              credentials: 'include',
2196              body: formData,
2197            }).then(function (resp) {
2198              if (!resp.ok) throw new Error('Could not upload attachments.');
2199              return resp.json();
2200            }).then(function (json) {
2201              var list = json && Array.isArray(json.files) ? json.files : [];
2202              list.forEach(function (f) {
2203                uploaded.push({
2204                  path: f.path,
2205                  name: f.originalName || f.name,
2206                  kind: /\.(png|jpe?g|gif|webp|svg|avif|bmp)$/i.test(f.name || '') ? 'image' : 'file',
2207                  size: f.size,
2208                });
2209              });
2210            });
2211          });
2212        })(files.slice(i, i + 12));
2213      }
2214      return chain.then(function () { return uploaded; });
2215    }
2216
2217    function buildCreateBody(prompt, name, projectId, opts) {
2218      opts = opts || {};
2219      var backendProvider = opts.backendProvider || null;
2220      var figmaLinks = stagedLinks.filter(function (link) {
2221        return link && link.kind === 'figma' && link.url;
2222      });
2223      var scrapedFromBrief = '';
2224      if (figmaLinks.length === 0) {
2225        scrapedFromBrief = scrapeFigmaFileUrlFromText(prompt);
2226      }
2227      var body = {
2228        id: projectId,
2229        name: name,
2230        skillId: 'fluttergo-pipeline',
2231        designSystemId: null,
2232        pendingPrompt: kickoffPrompt(prompt, backendProvider),
2233        conversationMode: 'design',
2234        skipDiscoveryBrief: true,
2235        metadata: {
2236          kind: 'prototype',
2237          platform: 'flutter',
2238          platformTargets: ['flutter'],
2239          skipDiscoveryBrief: true,
2240          promptTemplate: { prompt: prompt },
2241          // The Website chip: the project's product is its website module.
2242          buildTarget: guestBuildTarget() === 'website' ? 'website' : undefined,
2243          referenceLinks: stagedLinks.map(function (link) {
2244            return { kind: link.kind, url: link.url, title: link.title };
2245          }),
2246        },
2247      };
2248      if (backendProvider) {
2249        body.metadata.backendProvider = backendProvider;
2250        body.metadata.backendSetup = {
2251          status: backendProvider === 'none' ? 'skipped' : 'selected',
2252          selectedAt: Date.now(),
2253        };
2254      }
2255      // Same binding Home uses for "From Figma": od-figma-migration so create
2256      // applies figma-extract + connector OAuth instead of treating the URL as
2257      // decorative prompt text. Target stack is Flutter — FlutterGo's product.
2258      var figmaUrl = figmaLinks.length > 0
2259        ? (normalizeFigmaFileUrl(figmaLinks[0].url) || figmaLinks[0].url)
2260        : scrapedFromBrief;
2261      if (figmaUrl) {
2262        body.pluginId = 'od-figma-migration';
2263        body.pluginInputs = {
2264          figmaUrl: figmaUrl,
2265          targetStack: 'Flutter (Dart) — Android & iOS',
2266        };
2267      }
2268      return body;
2269    }
2270
2271    function resizeInput() {
2272      input.style.height = 'auto';
2273      // Floor was 96px, which left the prompt card visibly half-empty beside a
2274      // one-line placeholder. 68px still clears ~3 lines before it grows.
2275      //
2276      // Only what the visitor has typed grows the box. A textarea's
2277      // scrollHeight counts the placeholder too, and the animated placeholder
2278      // is a whole sentence: on a phone it wraps to six lines, so an empty
2279      // composer opened at the 220px cap and ate half the fold before anyone
2280      // had typed anything.
2281      var next = input.value ? Math.min(Math.max(input.scrollHeight, 68), 220) : 68;
2282      input.style.height = next + 'px';
2283    }
2284
2285    function setPrompt(text) {
2286      input.value = text;
2287      resizeInput();
2288      input.focus();
2289    }
2290
2291    function renderExamples() {
2292      if (!examplesRow) return;
2293      var shuffled = EXAMPLE_POOL.slice().sort(function () { return Math.random() - 0.5; });
2294      var pick = shuffled.slice(0, 3);
2295      examplesRow.textContent = '';
2296      pick.forEach(function (text) {
2297        var btn = document.createElement('button');
2298        btn.type = 'button';
2299        btn.className = 'fgs-example-chip';
2300        btn.setAttribute('data-guest-prompt-example', '');
2301        btn.setAttribute('data-example', text);
2302        btn.textContent = text;
2303        btn.addEventListener('click', function () {
2304          setPrompt(text);
2305        });
2306        examplesRow.appendChild(btn);
2307      });
2308    }
2309
2310    Array.prototype.forEach.call(document.querySelectorAll('[data-guest-tile]'), function (tile) {
2311      tile.addEventListener('click', function () {
2312        Array.prototype.forEach.call(document.querySelectorAll('[data-guest-tile]'), function (el) {
2313          el.classList.remove('is-active');
2314        });
2315        tile.classList.add('is-active');
2316        var text = (tile.getAttribute('data-example') || '').trim();
2317        if (text) setPrompt(text);
2318      });
2319    });
2320
2321    if (refreshBtn) {
2322      refreshBtn.addEventListener('click', function () {
2323        refreshBtn.classList.add('is-spinning');
2324        renderExamples();
2325        window.setTimeout(function () {
2326          refreshBtn.classList.remove('is-spinning');
2327        }, 220);
2328      });
2329    }
2330
2331    if (attachToggle && attachMenu) {
2332      attachToggle.addEventListener('click', function (event) {
2333        event.preventDefault();
2334        event.stopPropagation();
2335        if (attachMenu.hidden) openAttachMenu();
2336        else closeAttachMenu();
2337      });
2338      document.addEventListener('click', function (event) {
2339        if (!attachRoot) return;
2340        if (!attachRoot.contains(event.target)) {
2341          closeAttachMenu();
2342          if (linkDialog && !linkDialog.contains(event.target)) closeLinkDialog();
2343        }
2344      });
2345      document.addEventListener('keydown', function (event) {
2346        if (event.key === 'Escape') {
2347          closeAttachMenu();
2348          closeLinkDialog();
2349        }
2350      });
2351    }
2352
2353    Array.prototype.forEach.call(form.querySelectorAll('[data-guest-attach-action]'), function (item) {
2354      item.addEventListener('click', function (event) {
2355        var action = item.getAttribute('data-guest-attach-action');
2356        if (action === 'files') {
2357          event.preventDefault();
2358          closeAttachMenu();
2359          if (fileInput) fileInput.click();
2360        } else if (action === 'images') {
2361          event.preventDefault();
2362          closeAttachMenu();
2363          if (imageInput) imageInput.click();
2364        } else if (action === 'figma' || action === 'website') {
2365          event.preventDefault();
2366          openLinkDialog(action);
2367        } else if (action === 'github') {
2368          event.preventDefault();
2369          closeAttachMenu();
2370          openGithubModal();
2371        } else if (action === 'zip') {
2372          event.preventDefault();
2373          closeAttachMenu();
2374          if (zipInput) zipInput.click();
2375        } else {
2376          closeAttachMenu();
2377        }
2378      });
2379    });
2380
2381    if (linkCancel) {
2382      linkCancel.addEventListener('click', function (event) {
2383        event.preventDefault();
2384        closeLinkDialog();
2385      });
2386    }
2387    if (linkConfirm) {
2388      linkConfirm.addEventListener('click', function (event) {
2389        event.preventDefault();
2390        var ok = stageLink(pendingLinkKind || 'website', linkInput ? linkInput.value : '');
2391        if (!ok && linkInput) {
2392          linkInput.focus();
2393          return;
2394        }
2395        closeLinkDialog();
2396      });
2397    }
2398    if (linkInput) {
2399      linkInput.addEventListener('keydown', function (event) {
2400        if (event.key !== 'Enter') return;
2401        event.preventDefault();
2402        if (linkConfirm) linkConfirm.click();
2403      });
2404    }
2405
2406    if (fileInput) {
2407      fileInput.addEventListener('change', function () {
2408        stageFiles(fileInput.files);
2409        fileInput.value = '';
2410      });
2411    }
2412    if (imageInput) {
2413      imageInput.addEventListener('change', function () {
2414        stageFiles(imageInput.files);
2415        imageInput.value = '';
2416      });
2417    }
2418
2419    if (box) {
2420      ['dragenter', 'dragover'].forEach(function (type) {
2421        box.addEventListener(type, function (event) {
2422          event.preventDefault();
2423          event.stopPropagation();
2424          box.classList.add('is-dragover');
2425        });
2426      });
2427      ['dragleave', 'drop'].forEach(function (type) {
2428        box.addEventListener(type, function (event) {
2429          event.preventDefault();
2430          event.stopPropagation();
2431          box.classList.remove('is-dragover');
2432        });
2433      });
2434      box.addEventListener('drop', function (event) {
2435        var files = event.dataTransfer && event.dataTransfer.files;
2436        if (files && files.length) stageFiles(files);
2437      });
2438    }
2439
2440    input.addEventListener('paste', function (event) {
2441      var clipboard = event.clipboardData;
2442      if (!clipboard) return;
2443      var files = [];
2444      // Prefer clipboard.files (Firefox / some macOS paths), then items.
2445      if (clipboard.files && clipboard.files.length) {
2446        Array.prototype.forEach.call(clipboard.files, function (file) {
2447          if (file) files.push(file);
2448        });
2449      } else if (clipboard.items) {
2450        Array.prototype.forEach.call(clipboard.items, function (item) {
2451          if (item.kind === 'file') {
2452            var asFile = item.getAsFile();
2453            if (asFile) files.push(asFile);
2454          }
2455        });
2456      }
2457      if (files.length) {
2458        event.preventDefault();
2459        stageFiles(files);
2460        return;
2461      }
2462      var text = clipboard.getData('text') || clipboard.getData('text/plain') || '';
2463      if (text) {
2464        var trimmedPaste = String(text).trim();
2465        var figmaPaste = scrapeFigmaFileUrlFromText(trimmedPaste);
2466        if (figmaPaste && /^[\s]*((?:https?:\/\/)?(?:www\.)?figma\.com\/\S+)[\s]*$/i.test(trimmedPaste)) {
2467          event.preventDefault();
2468          stageLink('figma', figmaPaste);
2469          return;
2470        }
2471        var match = trimmedPaste.match(/^https?:\/\/\S+$/i);
2472        if (match) {
2473          event.preventDefault();
2474          stageLink(detectLinkKind(match[0]), match[0]);
2475        }
2476      }
2477    });
2478
2479    input.addEventListener('input', resizeInput);
2480    input.addEventListener('keydown', function (event) {
2481      if (event.key !== 'Enter' || event.shiftKey) return;
2482      event.preventDefault();
2483      if (typeof form.requestSubmit === 'function') form.requestSubmit();
2484      else form.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
2485    });
2486
2487    var statusTimer = null;
2488    function startBuildingUI(opts) {
2489      opts = opts || {};
2490      activeBuildStatus = opts.figma ? FIGMA_BUILD_STATUS : BUILD_STATUS;
2491      if (!submit) return;
2492      submit.disabled = true;
2493      submit.classList.add('is-building');
2494      var spinner = submit.querySelector('.fgs-prompt__spinner');
2495      if (spinner) spinner.hidden = false;
2496      if (figmaConnectBtn) figmaConnectBtn.hidden = true;
2497      if (statusRow) statusRow.hidden = false;
2498      if (statusEl) {
2499        statusEl.hidden = false;
2500        statusEl.classList.remove('is-error');
2501        statusEl.style.color = '';
2502        statusEl.textContent = activeBuildStatus[0];
2503        statusEl.removeAttribute('data-figma-phase');
2504      }
2505      if (opts.figma) {
2506        showFigmaProgressPanel('Creating your Flutter workspace…');
2507      } else {
2508        hideFigmaProgressPanel();
2509      }
2510      var step = 0;
2511      clearInterval(statusTimer);
2512      statusTimer = setInterval(function () {
2513        step += 1;
2514        if (!statusEl) {
2515          clearInterval(statusTimer);
2516          return;
2517        }
2518        // Prefer live Figma phase from the server when present.
2519        if (statusEl.getAttribute('data-figma-phase') === '1') return;
2520        var idx = Math.min(step, activeBuildStatus.length - 1);
2521        statusEl.textContent = activeBuildStatus[idx];
2522      }, 1200);
2523    }
2524
2525    function showFigmaProgressPanel(phase) {
2526      if (!figmaProgress) return;
2527      figmaProgress.hidden = false;
2528      if (figmaPhaseEl) figmaPhaseEl.textContent = phase || 'Connecting to your Figma account…';
2529      if (figmaCountsEl) figmaCountsEl.textContent = '';
2530      if (figmaInventoryEl) figmaInventoryEl.innerHTML = '';
2531    }
2532
2533    function hideFigmaProgressPanel() {
2534      if (!figmaProgress) return;
2535      figmaProgress.hidden = true;
2536      if (figmaPhaseEl) figmaPhaseEl.textContent = '';
2537      if (figmaCountsEl) figmaCountsEl.textContent = '';
2538      if (figmaInventoryEl) figmaInventoryEl.innerHTML = '';
2539      if (figmaViewAllBtn) figmaViewAllBtn.hidden = true;
2540      closeFigmaExportsModal();
2541    }
2542
2543    function projectRawPath(projectId, relPath) {
2544      var safe = String(relPath || '')
2545        .split('/')
2546        .filter(Boolean)
2547        .map(function (seg) { return encodeURIComponent(seg); })
2548        .join('/');
2549      return '/api/projects/' + encodeURIComponent(projectId) + '/raw/' + safe;
2550    }
2551
2552    function escapeHtml(value) {
2553      return String(value == null ? '' : value)
2554        .replace(/&/g, '&amp;')
2555        .replace(/</g, '&lt;')
2556        .replace(/>/g, '&gt;')
2557        .replace(/"/g, '&quot;');
2558    }
2559
2560    function renderFigmaInventory(screenNames, screenCount, assetCount) {
2561      var names = Array.isArray(screenNames)
2562        ? screenNames.filter(function (n) { return typeof n === 'string' && n.trim(); })
2563        : [];
2564      var totalScreens = typeof screenCount === 'number' && screenCount > 0
2565        ? screenCount
2566        : names.length;
2567      var totalAssets = typeof assetCount === 'number' && assetCount > 0 ? assetCount : 0;
2568      lastFigmaInventory = {
2569        screenNames: names,
2570        screenCount: totalScreens,
2571        assetCount: totalAssets,
2572        phase: (figmaPhaseEl && figmaPhaseEl.textContent) || lastFigmaInventory.phase || '',
2573        screenPreviews: Array.isArray(lastFigmaInventory.screenPreviews)
2574          ? lastFigmaInventory.screenPreviews
2575          : [],
2576        projectId: lastFigmaInventory.projectId || '',
2577      };
2578
2579      if (figmaCountsEl) {
2580        var parts = [];
2581        if (totalScreens > 0) {
2582          parts.push(totalScreens + ' screen' + (totalScreens === 1 ? '' : 's'));
2583        }
2584        if (totalAssets > 0) {
2585          parts.push(totalAssets + ' asset' + (totalAssets === 1 ? '' : 's'));
2586        }
2587        figmaCountsEl.textContent = parts.join(' · ');
2588      }
2589      if (figmaViewAllBtn) {
2590        figmaViewAllBtn.hidden = !(totalScreens > 0 || names.length > 0 || totalAssets > 0);
2591        figmaViewAllBtn.textContent = totalScreens > 12 || names.length > 12
2592          ? 'View all ' + Math.max(totalScreens, names.length) + ' exports'
2593          : 'View all exports';
2594      }
2595      if (figmaInventoryEl) {
2596        var show = names.slice(0, 12);
2597        var html = '';
2598        for (var i = 0; i < show.length; i++) {
2599          html += '<li style="animation-delay:' + (i * 35) + 'ms">' +
2600            escapeHtml(show[i]) + '</li>';
2601        }
2602        if (Math.max(totalScreens, names.length) > show.length) {
2603          var more = Math.max(totalScreens, names.length) - show.length;
2604          html += '<li class="is-more" data-guest-figma-view-all-chip role="button" tabindex="0">+' +
2605            more + ' more</li>';
2606        }
2607        figmaInventoryEl.innerHTML = html;
2608      }
2609      // Keep the open inventory dialog in sync as names/previews arrive.
2610      if (figmaExportsModal && !figmaExportsModal.hidden) {
2611        openFigmaExportsModal();
2612      }
2613    }
2614
2615    function guessFigmaPreviewPath(screen) {
2616      if (!screen) return '';
2617      var preview = typeof screen.previewAsset === 'string' ? screen.previewAsset.trim() : '';
2618      if (preview) {
2619        if (preview.indexOf('figma/') === 0) return preview;
2620        if (preview.indexOf('assets/') === 0) return 'figma/' + preview;
2621        return 'figma/assets/' + preview.replace(/^\//, '');
2622      }
2623      var id = typeof screen.id === 'string' ? screen.id.trim() : '';
2624      if (!id) return '';
2625      // figma-extract writes screen PNGs as `<nodeId with : → ->.png`.
2626      return 'figma/assets/' + id.replace(/:/g, '-') + '.png';
2627    }
2628
2629    function openFigmaExportsModal() {
2630      if (!figmaExportsModal) return;
2631      var names = lastFigmaInventory.screenNames || [];
2632      var previews = Array.isArray(lastFigmaInventory.screenPreviews)
2633        ? lastFigmaInventory.screenPreviews
2634        : [];
2635      var totalScreens = lastFigmaInventory.screenCount || names.length;
2636      var totalAssets = lastFigmaInventory.assetCount || 0;
2637      var projectId = lastFigmaInventory.projectId || '';
2638      var bodyEl = figmaExportsModal.querySelector('.guest-figma-exports__body');
2639      var priorScroll = bodyEl ? bodyEl.scrollTop : 0;
2640      if (figmaExportsLead) {
2641        figmaExportsLead.textContent = lastFigmaInventory.phase
2642          ? lastFigmaInventory.phase
2643          : 'Everything FlutterGo pulled from your file so far.';
2644      }
2645      if (figmaExportsStats) {
2646        figmaExportsStats.innerHTML =
2647          '<div class="guest-figma-exports__stat"><strong>' + totalScreens +
2648          '</strong><span>Screens</span></div>' +
2649          '<div class="guest-figma-exports__stat"><strong>' + totalAssets +
2650          '</strong><span>Assets (icons / buttons / previews)</span></div>';
2651      }
2652      if (figmaExportsScreens) {
2653        if (!names.length && !previews.length) {
2654          figmaExportsScreens.innerHTML =
2655            '<li><div class="guest-figma-exports__meta"><span class="name">No screen names yet — wait until Figma pull finishes, or reconnect Figma and try again.</span></div></li>';
2656        } else {
2657          var listHtml = '';
2658          var rows = previews.length ? previews : names.map(function (name) {
2659            return { name: name, previewPath: '' };
2660          });
2661          for (var i = 0; i < rows.length; i++) {
2662            var row = rows[i];
2663            var idx = String(i + 1).padStart(2, '0');
2664            var thumb = '';
2665            if (projectId && row.previewPath) {
2666              thumb =
2667                '<img class="guest-figma-exports__thumb" src="' +
2668                projectRawPath(projectId, row.previewPath) +
2669                '" alt="' + escapeHtml(row.name) + ' preview" loading="lazy" ' +
2670                'onerror="this.classList.add(\'is-missing\');this.nextElementSibling&&this.nextElementSibling.classList.add(\'is-on\')" />';
2671            }
2672            listHtml +=
2673              '<li class="guest-figma-exports__screen">' +
2674              '<div class="guest-figma-exports__frame">' +
2675              '<span class="guest-figma-exports__badge">' + idx + '</span>' +
2676              thumb +
2677              '<div class="guest-figma-exports__placeholder' + (thumb ? '' : ' is-on') + '">' +
2678              (thumb ? 'Loading preview…' : 'Preview pending') +
2679              '</div>' +
2680              '</div>' +
2681              '<div class="guest-figma-exports__meta">' +
2682              '<span class="idx">' + idx + '</span>' +
2683              '<span class="name">' + escapeHtml(row.name) + '</span>' +
2684              '</div></li>';
2685          }
2686          if (totalScreens > rows.length) {
2687            listHtml +=
2688              '<li class="guest-figma-exports__more" role="note">' +
2689              '+' + (totalScreens - rows.length) +
2690              ' more screens — full list in figma/OVERVIEW.md after open' +
2691              '</li>';
2692          }
2693          figmaExportsScreens.innerHTML = listHtml;
2694        }
2695      }
2696      if (figmaExportsAssets) {
2697        if (totalAssets > 0) {
2698          figmaExportsAssets.textContent =
2699            totalAssets + ' PNG files downloaded into figma/assets/ — screen previews plus icons and buttons (capped for large files).';
2700        } else if (names.length > 0) {
2701          figmaExportsAssets.textContent =
2702            'Screens were listed, but asset download is still empty — reconnect Figma if icons/previews are missing after the project opens.';
2703        } else {
2704          figmaExportsAssets.textContent =
2705            'Asset download is still in progress, or none have landed yet.';
2706        }
2707      }
2708      figmaExportsModal.hidden = false;
2709      try {
2710        document.documentElement.classList.add('guest-figma-exports-open');
2711      } catch (e) {}
2712      if (bodyEl) {
2713        bodyEl.scrollTop = priorScroll;
2714      }
2715    }
2716
2717    function closeFigmaExportsModal() {
2718      if (!figmaExportsModal) return;
2719      figmaExportsModal.hidden = true;
2720      try {
2721        document.documentElement.classList.remove('guest-figma-exports-open');
2722      } catch (e) {}
2723    }
2724
2725    if (figmaViewAllBtn) {
2726      figmaViewAllBtn.addEventListener('click', function (event) {
2727        event.preventDefault();
2728        openFigmaExportsModal();
2729      });
2730    }
2731    if (figmaInventoryEl) {
2732      figmaInventoryEl.addEventListener('click', function (event) {
2733        var chip = event.target && event.target.closest
2734          ? event.target.closest('[data-guest-figma-view-all-chip]')
2735          : null;
2736        if (!chip) return;
2737        event.preventDefault();
2738        openFigmaExportsModal();
2739      });
2740      figmaInventoryEl.addEventListener('keydown', function (event) {
2741        if (event.key !== 'Enter' && event.key !== ' ') return;
2742        var chip = event.target && event.target.closest
2743          ? event.target.closest('[data-guest-figma-view-all-chip]')
2744          : null;
2745        if (!chip) return;
2746        event.preventDefault();
2747        openFigmaExportsModal();
2748      });
2749    }
2750    if (figmaExportsModal) {
2751      figmaExportsModal.addEventListener('click', function (event) {
2752        var dismiss = event.target && event.target.closest
2753          ? event.target.closest('[data-guest-figma-exports-dismiss]')
2754          : null;
2755        if (dismiss) {
2756          event.preventDefault();
2757          closeFigmaExportsModal();
2758        }
2759      });
2760      document.addEventListener('keydown', function (event) {
2761        if (event.key === 'Escape' && figmaExportsModal && !figmaExportsModal.hidden) {
2762          closeFigmaExportsModal();
2763        }
2764      });
2765    }
2766
2767    function setFigmaPhase(text, meta) {
2768      meta = meta || {};
2769      if (text) lastFigmaInventory.phase = text;
2770      if (figmaProgress) {
2771        figmaProgress.hidden = false;
2772        if (figmaPhaseEl && text) figmaPhaseEl.textContent = text;
2773        renderFigmaInventory(meta.screenNames, meta.screenCount, meta.assetCount);
2774      }
2775      if (!statusEl || !text) return;
2776      statusEl.setAttribute('data-figma-phase', '1');
2777      statusEl.classList.remove('is-error');
2778      statusEl.style.color = '';
2779      statusEl.textContent = text;
2780    }
2781
2782    function isFigmaConnectError(message) {
2783      return /connect your figma|figma account|CONNECTOR_NOT_CONNECTED|CONNECTOR_AUTH_CONFIG_REQUIRED|FIGMA_EXTRACT_FAILED|without (?:file )?access|composio|figma connection|authorization|not enabled on this server|auth_configs/i.test(
2784        String(message || ''),
2785      );
2786    }
2787
2788    function isFigmaUnavailableError(message) {
2789      var text = String(message || '');
2790      // Only treat as "Composio off" when that is the actual fault — not every
2791      // error that happens to mention the word Composio (e.g. scoped-key
2792      // auth_configs permission failures).
2793      if (/auth_configs/i.test(text) && /permission|write access|read access/i.test(text)) {
2794        return false;
2795      }
2796      return /CONNECTOR_AUTH_CONFIG_REQUIRED|composio is not configured|not enabled on this server|admin → connectors|admin-configured composio/i.test(
2797        text,
2798      );
2799    }
2800
2801    function isComposioNotConfiguredError(message) {
2802      var text = String(message || '');
2803      return /composio is not configured|admin-configured composio|require a signed-in session with admin-configured composio/i.test(
2804        text,
2805      );
2806    }
2807
2808    function isAuthConfigsPermissionError(message) {
2809      var text = String(message || '');
2810      return /auth_configs/i.test(text) && /permission|write access|read access|cannot create auth configs/i.test(text);
2811    }
2812
2813    function markFigmaConnectAction(action) {
2814      var btn = form.querySelector('[data-guest-figma-connect]');
2815      if (!btn) return;
2816      if (action === 'integrations') {
2817        btn.setAttribute('data-figma-action', 'integrations');
2818      } else {
2819        btn.removeAttribute('data-figma-action');
2820      }
2821    }
2822
2823    function resetBuildingUI(message, options) {
2824      submitting = false;
2825      clearInterval(statusTimer);
2826      if (statusEl) statusEl.removeAttribute('data-figma-phase');
2827      hideFigmaProgressPanel();
2828      if (submit) {
2829        submit.disabled = false;
2830        submit.classList.remove('is-building');
2831        var spinner = submit.querySelector('.fgs-prompt__spinner');
2832        if (spinner) spinner.hidden = true;
2833      }
2834      var showFigmaConnect = Boolean(
2835        options && options.figmaConnect != null
2836          ? options.figmaConnect
2837          : isFigmaConnectError(message),
2838      );
2839      var connectLabel =
2840        options && typeof options.connectLabel === 'string' && options.connectLabel.trim()
2841          ? options.connectLabel.trim()
2842          : 'Connect Figma';
2843      // Keep the real API/setup message — overwriting it made Connect look broken.
2844      var displayMessage = message || '';
2845      if (statusRow) statusRow.hidden = !displayMessage;
2846      if (statusEl) {
2847        if (displayMessage) {
2848          statusEl.hidden = false;
2849          statusEl.textContent = displayMessage;
2850          statusEl.classList.add('is-error');
2851          statusEl.style.color = '#b91c1c';
2852        } else {
2853          statusEl.hidden = true;
2854          statusEl.textContent = '';
2855          statusEl.classList.remove('is-error');
2856          statusEl.style.color = '';
2857        }
2858      }
2859      var btn = form.querySelector('[data-guest-figma-connect]');
2860      if (btn) {
2861        figmaConnectBtn = btn;
2862        btn.hidden = !showFigmaConnect;
2863        btn.disabled = false;
2864        btn.textContent = connectLabel;
2865      }
2866    }
2867
2868    /**
2869     * Visible acknowledgement that a Start building click landed, held for as
2870     * long as the session probe runs. Returns its own undo, so the caller
2871     * cannot leave the button stuck disabled on any branch.
2872     */
2873    var PROBE_STATUS_TEXT = 'Checking your account…';
2874
2875    function startProbeFeedback() {
2876      if (submit) {
2877        submit.disabled = true;
2878        submit.classList.add('is-building');
2879        var spinner = submit.querySelector('.fgs-prompt__spinner');
2880        if (spinner) spinner.hidden = false;
2881      }
2882      if (statusRow) statusRow.hidden = false;
2883      if (statusEl) {
2884        statusEl.hidden = false;
2885        statusEl.classList.remove('is-error');
2886        statusEl.style.color = '';
2887        statusEl.textContent = PROBE_STATUS_TEXT;
2888      }
2889      var done = false;
2890      return function endProbeFeedback() {
2891        if (done) return;
2892        done = true;
2893        if (submit) {
2894          submit.disabled = false;
2895          submit.classList.remove('is-building');
2896          var sp = submit.querySelector('.fgs-prompt__spinner');
2897          if (sp) sp.hidden = true;
2898        }
2899        // Only clear the line we wrote — the next step may already have
2900        // replaced it with real build status or a real error.
2901        if (statusEl && statusEl.textContent === PROBE_STATUS_TEXT) {
2902          statusEl.hidden = true;
2903          statusEl.textContent = '';
2904          if (statusRow) statusRow.hidden = true;
2905        }
2906      };
2907    }
2908
2909    function showReadyStatus(message) {
2910      submitting = false;
2911      clearInterval(statusTimer);
2912      if (statusEl) statusEl.removeAttribute('data-figma-phase');
2913      hideFigmaProgressPanel();
2914      if (submit) {
2915        submit.disabled = false;
2916        submit.classList.remove('is-building');
2917        var spinner = submit.querySelector('.fgs-prompt__spinner');
2918        if (spinner) spinner.hidden = true;
2919      }
2920      if (statusRow) statusRow.hidden = !message;
2921      if (statusEl) {
2922        if (message) {
2923          statusEl.hidden = false;
2924          statusEl.textContent = message;
2925          statusEl.classList.remove('is-error');
2926          statusEl.style.color = '';
2927        } else {
2928          statusEl.hidden = true;
2929          statusEl.textContent = '';
2930          statusEl.classList.remove('is-error');
2931          statusEl.style.color = '';
2932        }
2933      }
2934      var connectBtn = form.querySelector('[data-guest-figma-connect]');
2935      if (connectBtn) connectBtn.hidden = true;
2936    }
2937
2938    function readApiErrorMessage(body, fallback) {
2939      var err = body && body.error;
2940      if (!err) return fallback;
2941      if (typeof err === 'string' && err.trim()) return err;
2942      if (typeof err.message === 'string' && err.message.trim()) return err.message;
2943      return fallback;
2944    }
2945
2946    function connectFigmaFromLanding(clickedBtn) {
2947      var btn = clickedBtn || form.querySelector('[data-guest-figma-connect]') || figmaConnectBtn;
2948      if (!btn) return;
2949      figmaConnectBtn = btn;
2950      if (btn.getAttribute('data-figma-action') === 'integrations') {
2951        window.location.assign('/integrations');
2952        return;
2953      }
2954      btn.disabled = true;
2955      btn.textContent = 'Connecting…';
2956      if (statusRow) statusRow.hidden = false;
2957      if (statusEl) {
2958        statusEl.hidden = false;
2959        statusEl.classList.remove('is-error');
2960        statusEl.style.color = '';
2961        statusEl.textContent = 'Checking Figma connection…';
2962      }
2963
2964      function fail(message, connectLabel) {
2965        resetBuildingUI(message, {
2966          figmaConnect: true,
2967          connectLabel: connectLabel || 'Connect Figma',
2968        });
2969      }
2970
2971      ensureSession()
2972        .then(function () {
2973          return fetch('/api/connectors/figma', {
2974            credentials: 'include',
2975            cache: 'no-store',
2976          }).then(function (r) {
2977            if (r.status === 401) throw new Error('AUTH_REQUIRED');
2978            if (!r.ok) return null;
2979            return r.json();
2980          });
2981        })
2982        .then(function (detail) {
2983          var connector = detail && detail.connector ? detail.connector : detail;
2984          var auth = connector && connector.auth ? connector.auth : null;
2985          if (connector && connector.status === 'connected') {
2986            if (statusEl) {
2987              statusEl.classList.remove('is-error');
2988              statusEl.style.color = '';
2989              statusEl.textContent = 'Figma connected — click Start building.';
2990            }
2991            btn.hidden = true;
2992            btn.disabled = false;
2993            btn.textContent = 'Connect Figma';
2994            return null;
2995          }
2996          // auth.configured=false only means the Figma toolkit auth-config has
2997          // not been prepared yet — prepare() below creates it when the
2998          // Composio API key is present. Only bail to Integrations when
2999          // Composio itself is off.
3000          if (auth && auth.configured === false) {
3001            return fetch('/api/connectors/composio/config', {
3002              credentials: 'include',
3003              cache: 'no-store',
3004            }).then(function (r) {
3005              return r.ok ? r.json() : { configured: false };
3006            }).then(function (cfg) {
3007              if (!cfg || cfg.configured !== true) {
3008                btn.setAttribute('data-figma-action', 'integrations');
3009                fail(
3010                  'Figma is not enabled on this server yet (Composio is not configured). Open Integrations for setup, then connect your Figma account before Start building.',
3011                  'Open Integrations',
3012                );
3013                return null;
3014              }
3015              if (statusEl) statusEl.textContent = 'Opening Figma authorization…';
3016              return fetch('/api/connectors/auth-configs/prepare', {
3017                method: 'POST',
3018                credentials: 'include',
3019                headers: { 'Content-Type': 'application/json' },
3020                body: JSON.stringify({ connectorIds: ['figma'] }),
3021              }).then(function (r) {
3022                if (r.status === 401) throw new Error('AUTH_REQUIRED');
3023                if (!r.ok) {
3024                  return r.json().then(function (body) {
3025                    throw new Error(readApiErrorMessage(body, 'Could not prepare Figma connection.'));
3026                  }, function () {
3027                    throw new Error('Could not prepare Figma connection.');
3028                  });
3029                }
3030                return r.json();
3031              });
3032            });
3033          }
3034          if (statusEl) statusEl.textContent = 'Opening Figma authorization…';
3035          return fetch('/api/connectors/auth-configs/prepare', {
3036            method: 'POST',
3037            credentials: 'include',
3038            headers: { 'Content-Type': 'application/json' },
3039            body: JSON.stringify({ connectorIds: ['figma'] }),
3040          }).then(function (r) {
3041            if (r.status === 401) throw new Error('AUTH_REQUIRED');
3042            if (!r.ok) {
3043              return r.json().then(function (body) {
3044                throw new Error(readApiErrorMessage(body, 'Could not prepare Figma connection.'));
3045              }, function () {
3046                throw new Error('Could not prepare Figma connection.');
3047              });
3048            }
3049            return r.json();
3050          });
3051        })
3052        .then(function (prep) {
3053          if (!prep) return null;
3054          var result = prep.results ? prep.results.figma : null;
3055          if (result && result.status === 'custom_required') {
3056            throw new Error(result.message || 'Figma needs more setup in Integrations.');
3057          }
3058          if (result && result.status === 'error') {
3059            throw new Error(result.message || 'Could not prepare Figma connection.');
3060          }
3061          return fetch('/api/connectors/figma/connect', {
3062            method: 'POST',
3063            credentials: 'include',
3064          }).then(function (r) {
3065            if (r.status === 401) throw new Error('AUTH_REQUIRED');
3066            if (!r.ok) {
3067              return r.json().then(function (body) {
3068                throw new Error(readApiErrorMessage(body, 'Could not start Figma connection.'));
3069              }, function () {
3070                throw new Error('Could not start Figma connection.');
3071              });
3072            }
3073            return r.json();
3074          });
3075        })
3076        .then(function (json) {
3077          if (!json) return;
3078          if (json.auth && json.auth.kind === 'connected') {
3079            if (statusEl) {
3080              statusEl.classList.remove('is-error');
3081              statusEl.style.color = '';
3082              statusEl.textContent = 'Figma connected — click Start building.';
3083            }
3084            btn.hidden = true;
3085            btn.disabled = false;
3086            btn.textContent = 'Connect Figma';
3087            return;
3088          }
3089          var url = json.auth && json.auth.redirectUrl;
3090          if (!url) throw new Error('No Figma authorization URL returned.');
3091          var popup = window.open(url, 'fluttergo-figma-connect', 'width=980,height=820');
3092          if (!popup) {
3093            // Popup blocked — continue in this tab.
3094            window.location.assign(url);
3095            return;
3096          }
3097          if (statusEl) {
3098            statusEl.textContent = 'Waiting for Figma authorization…';
3099          }
3100          btn.disabled = false;
3101          btn.textContent = 'Connect Figma';
3102          var polls = 0;
3103          if (figmaConnectPoll) clearInterval(figmaConnectPoll);
3104          figmaConnectPoll = setInterval(function () {
3105            polls += 1;
3106            fetch('/api/connectors/status', {
3107              credentials: 'include',
3108              cache: 'no-store',
3109            })
3110              .then(function (r) { return r.ok ? r.json() : null; })
3111              .then(function (data) {
3112                var figma = data && data.statuses ? data.statuses.figma : null;
3113                var status = figma && typeof figma.status === 'string' ? figma.status : '';
3114                if (status === 'connected') {
3115                  clearInterval(figmaConnectPoll);
3116                  figmaConnectPoll = null;
3117                  if (statusEl) {
3118                    statusEl.classList.remove('is-error');
3119                    statusEl.style.color = '';
3120                    statusEl.textContent = 'Figma connected — click Start building.';
3121                  }
3122                  btn.hidden = true;
3123                  btn.disabled = false;
3124                  btn.textContent = 'Connect Figma';
3125                } else if (polls >= 45) {
3126                  clearInterval(figmaConnectPoll);
3127                  figmaConnectPoll = null;
3128                  fail('Figma authorization timed out. Click Connect Figma to try again.');
3129                }
3130              })
3131              .catch(function () {});
3132          }, 2000);
3133        })
3134        .catch(function (err) {
3135          if (err && err.message === 'AUTH_REQUIRED') {
3136            resetBuildingUI();
3137            goAuthGate('Connect Figma to build from your design file');
3138            return;
3139          }
3140          var msg =
3141            err && err.message
3142              ? err.message
3143              : 'Could not connect Figma. Try again from Integrations.';
3144          if (isComposioNotConfiguredError(msg)) {
3145            btn.setAttribute('data-figma-action', 'integrations');
3146            fail(
3147              'Figma is not enabled on this server yet (Composio is not configured). Ask an admin to save a Project API key under Admin → Connectors, then connect your Figma account.',
3148              'Open Integrations',
3149            );
3150            return;
3151          }
3152          if (isAuthConfigsPermissionError(msg)) {
3153            btn.setAttribute('data-figma-action', 'integrations');
3154            fail(
3155              msg,
3156              'Open Integrations',
3157            );
3158            return;
3159          }
3160          fail(msg);
3161        });
3162    }
3163
3164    // Delegated click — survives duplicate markup / late DOM and always hits
3165    // the button the user actually pressed.
3166    document.addEventListener('click', function (event) {
3167      var target = event.target;
3168      if (!target || !target.closest) return;
3169      var btn = target.closest('[data-guest-figma-connect]');
3170      if (!btn || !form.contains(btn)) return;
3171      event.preventDefault();
3172      event.stopPropagation();
3173      connectFigmaFromLanding(btn);
3174    });
3175
3176    function goWorkplace(projectId, conversationId) {
3177      var path = '/projects/' + encodeURIComponent(projectId);
3178      if (conversationId) {
3179        path += '/conversations/' + encodeURIComponent(conversationId);
3180      }
3181      window.location.assign(path);
3182    }
3183
3184    var FLUTTER_ZIP_MAX_BYTES = 220 * 1024 * 1024;
3185    var githubConnectPoll = null;
3186
3187    function setGithubError(message) {
3188      if (!githubError) return;
3189      if (message) {
3190        githubError.hidden = false;
3191        githubError.textContent = message;
3192      } else {
3193        githubError.hidden = true;
3194        githubError.textContent = '';
3195      }
3196    }
3197
3198    function setGithubStatus(text, tone) {
3199      if (!githubStatus) return;
3200      githubStatus.textContent = text;
3201      githubStatus.classList.remove('is-ok', 'is-wait');
3202      if (tone) githubStatus.classList.add(tone);
3203    }
3204
3205    function closeGithubModal() {
3206      if (!githubModal) return;
3207      githubModal.hidden = true;
3208      document.body.style.overflow = '';
3209      githubVisibility = null;
3210      if (githubConnectPoll) {
3211        clearInterval(githubConnectPoll);
3212        githubConnectPoll = null;
3213      }
3214      setGithubError('');
3215      syncGithubModalUi();
3216    }
3217
3218    function syncGithubModalUi() {
3219      if (githubChoice) githubChoice.hidden = Boolean(githubVisibility);
3220      if (githubFields) githubFields.hidden = !githubVisibility;
3221      if (githubFoot) githubFoot.hidden = !githubVisibility;
3222      if (githubConnectRow) githubConnectRow.hidden = githubVisibility !== 'private';
3223      if (githubBack) githubBack.hidden = !githubVisibility;
3224      if (githubLead) {
3225        githubLead.textContent = githubVisibility === 'public'
3226          ? 'Paste a public GitHub link. No GitHub login needed.'
3227          : githubVisibility === 'private'
3228            ? 'Connect GitHub to clone a private repository.'
3229            : 'Public repos need a link only. Private repos need GitHub connected.';
3230      }
3231    }
3232
3233    function setGithubVisibility(kind) {
3234      githubVisibility = kind === 'public' || kind === 'private' ? kind : null;
3235      setGithubError('');
3236      syncGithubModalUi();
3237      if (githubVisibility === 'private') {
3238        setGithubStatus('Checking GitHub connection…', 'is-wait');
3239        ensureSession()
3240          .then(function () { return refreshGithubStatus(); })
3241          .catch(function () {
3242            githubVisibility = null;
3243            syncGithubModalUi();
3244            closeGithubModal();
3245            goAuthGate('Import a private GitHub repository');
3246          });
3247        return;
3248      }
3249      if (githubVisibility === 'public' && githubRepo) {
3250        window.setTimeout(function () { githubRepo.focus(); }, 40);
3251      }
3252    }
3253
3254    function refreshGithubStatus() {
3255      return fetch('/api/github/app/status', { credentials: 'include', cache: 'no-store' })
3256        .then(function (r) { return r.ok ? r.json() : null; })
3257        .then(function (st) {
3258          if (st && st.installed) {
3259            githubInstalled = true;
3260            setGithubStatus(
3261              st.accountLogin ? ('Connected as ' + st.accountLogin) : 'GitHub connected',
3262              'is-ok',
3263            );
3264            if (githubConnectBtn) githubConnectBtn.textContent = 'Refresh';
3265            return st;
3266          }
3267          githubInstalled = false;
3268          if (st && st.githubAccountRestricted) {
3269            setGithubStatus('GitHub account restricted', '');
3270            setGithubError(st.message || 'This GitHub account cannot install the app.');
3271            return st;
3272          }
3273          setGithubStatus('Connect GitHub to import a private repository', '');
3274          if (githubConnectBtn) githubConnectBtn.textContent = 'Connect GitHub';
3275          return st;
3276        })
3277        .catch(function () {
3278          githubInstalled = false;
3279          setGithubStatus('Could not check GitHub status', '');
3280          return null;
3281        });
3282    }
3283
3284    function openGithubModal() {
3285      if (!githubModal) return;
3286      githubVisibility = null;
3287      githubInstalled = false;
3288      githubModal.hidden = false;
3289      document.body.style.overflow = 'hidden';
3290      setGithubError('');
3291      syncGithubModalUi();
3292    }
3293
3294    function connectGithubFromModal() {
3295      setGithubError('');
3296      setGithubStatus('Waiting for GitHub…', 'is-wait');
3297      if (githubConnectBtn) githubConnectBtn.disabled = true;
3298      ensureSession()
3299        .then(function () {
3300          return fetch('/api/github/app/authorize-url', { credentials: 'include', cache: 'no-store' })
3301            .then(function (r) { return r.ok ? r.json() : null; })
3302            .then(function (json) {
3303              var url = json && json.url;
3304              if (!url) {
3305                return fetch('/api/github/app/install-url', { credentials: 'include', cache: 'no-store' })
3306                  .then(function (r) { return r.ok ? r.json() : null; })
3307                  .then(function (install) {
3308                    if (!install || !install.url) throw new Error(json && json.error ? json.error : 'GitHub connect is unavailable.');
3309                    return install.url;
3310                  });
3311              }
3312              return url;
3313            });
3314        })
3315        .then(function (url) {
3316          window.open(url, 'fluttergo-github-connect', 'width=980,height=820');
3317          var polls = 0;
3318          if (githubConnectPoll) clearInterval(githubConnectPoll);
3319          githubConnectPoll = setInterval(function () {
3320            polls += 1;
3321            refreshGithubStatus().then(function (st) {
3322              if (st && st.installed) {
3323                clearInterval(githubConnectPoll);
3324                githubConnectPoll = null;
3325                if (githubConnectBtn) githubConnectBtn.disabled = false;
3326              } else if (polls >= 72) {
3327                clearInterval(githubConnectPoll);
3328                githubConnectPoll = null;
3329                if (githubConnectBtn) githubConnectBtn.disabled = false;
3330                setGithubError('GitHub connect timed out. Finish the popup, then click Connect again.');
3331                setGithubStatus('Not connected', '');
3332              }
3333            });
3334          }, 2500);
3335        })
3336        .catch(function (err) {
3337          if (githubConnectBtn) githubConnectBtn.disabled = false;
3338          setGithubStatus('Not connected', '');
3339          setGithubError(err && err.message ? err.message : 'Could not open GitHub connect.');
3340        });
3341    }
3342
3343    function importGithubRepo() {
3344      var repo = githubRepo ? String(githubRepo.value || '').trim() : '';
3345      if (!githubVisibility) {
3346        setGithubError('Choose public or private first.');
3347        return;
3348      }
3349      if (githubVisibility === 'private' && !githubInstalled) {
3350        setGithubError('Connect GitHub to import a private repository.');
3351        return;
3352      }
3353      if (!repo) {
3354        setGithubError('Enter a GitHub repository (owner/repo or URL).');
3355        if (githubRepo) githubRepo.focus();
3356        return;
3357      }
3358      var branch = githubBranch ? String(githubBranch.value || '').trim() : '';
3359      var name = githubName ? String(githubName.value || '').trim() : '';
3360      setGithubError('');
3361      if (githubSubmitBtn) {
3362        githubSubmitBtn.disabled = true;
3363        githubSubmitBtn.textContent = 'Importing…';
3364      }
3365      ensureSession()
3366        .then(function () {
3367          var body = { repo: repo, requireFlutter: true };
3368          if (branch) body.branch = branch;
3369          if (name) body.name = name;
3370          return fetch('/api/import/github', {
3371            method: 'POST',
3372            credentials: 'include',
3373            headers: { 'Content-Type': 'application/json' },
3374            body: JSON.stringify(body),
3375          });
3376        })
3377        .then(function (r) {
3378          return r.json().then(function (json) {
3379            return { ok: r.ok, status: r.status, json: json };
3380          }, function () {
3381            return { ok: r.ok, status: r.status, json: null };
3382          });
3383        })
3384        .then(function (result) {
3385          if (!result.ok) {
3386            var code = result.json && result.json.error && result.json.error.code;
3387            var msg =
3388              result.json && result.json.error && typeof result.json.error.message === 'string'
3389                ? result.json.error.message
3390                : 'The GitHub repository could not be imported.';
3391            if (code === 'GITHUB_CONNECT_REQUIRED') {
3392              setGithubError('Connect GitHub first, then import again.');
3393              setGithubStatus('GitHub connection required', '');
3394            } else {
3395              setGithubError(msg);
3396            }
3397            throw new Error(msg);
3398          }
3399          var projectId =
3400            result.json && result.json.project && typeof result.json.project.id === 'string'
3401              ? result.json.project.id
3402              : '';
3403          if (!projectId) throw new Error('Import succeeded but no project id was returned.');
3404          var conversationId =
3405            result.json && typeof result.json.conversationId === 'string'
3406              ? result.json.conversationId
3407              : '';
3408          var entryFile =
3409            result.json && typeof result.json.entryFile === 'string'
3410              ? result.json.entryFile
3411              : '';
3412          closeGithubModal();
3413          var path = '/projects/' + encodeURIComponent(projectId);
3414          if (conversationId) path += '/conversations/' + encodeURIComponent(conversationId);
3415          if (entryFile) path += (path.indexOf('?') === -1 ? '?' : '&') + 'file=' + encodeURIComponent(entryFile);
3416          window.location.assign(path);
3417        })
3418        .catch(function (err) {
3419          if (githubSubmitBtn) {
3420            githubSubmitBtn.disabled = false;
3421            githubSubmitBtn.textContent = 'Import repository';
3422          }
3423          if (err && err.message === 'AUTH_REQUIRED') {
3424            closeGithubModal();
3425            goAuthGate('Import a Flutter app from GitHub');
3426            return;
3427          }
3428          if (!githubError || githubError.hidden) {
3429            setGithubError(err && err.message ? err.message : 'Import failed.');
3430          }
3431        });
3432    }
3433
3434    function isZipOnlyStaging() {
3435      if (stagedLinks.length) return false;
3436      if (stagedFiles.length !== 1) return false;
3437      return looksLikeZip(stagedFiles[0] && stagedFiles[0].file);
3438    }
3439
3440    function stageUploadedZip(file) {
3441      if (!file) return;
3442      if (file.size > FLUTTER_ZIP_MAX_BYTES) {
3443        resetBuildingUI(
3444          'This ZIP is too large (' +
3445            (file.size / (1024 * 1024)).toFixed(1) +
3446            ' MB). Limit is 220 MB.',
3447        );
3448        return;
3449      }
3450      if (!looksLikeZip(file)) {
3451        resetBuildingUI('Please upload a .zip file.');
3452        return;
3453      }
3454      stageFiles([file]);
3455      showReadyStatus('Your zip is uploaded. Describe the app or click Start building.');
3456      try { input.focus(); } catch (e) {}
3457    }
3458
3459    function postFlutterZip(file, prompt) {
3460      return new Promise(function (resolve, reject) {
3461        var xhr = new XMLHttpRequest();
3462        var formData = new FormData();
3463        formData.append('file', file);
3464        if (prompt) formData.append('prompt', prompt);
3465        xhr.upload.addEventListener('progress', function (event) {
3466          if (!event.lengthComputable || !statusEl) return;
3467          var pct = Math.min(100, Math.round((event.loaded / event.total) * 100));
3468          statusEl.hidden = false;
3469          statusEl.classList.remove('is-error');
3470          statusEl.style.color = '';
3471          statusEl.textContent = pct >= 100
3472            ? 'Extracting your zip…'
3473            : 'Uploading your zip… ' + pct + '%';
3474        });
3475        xhr.addEventListener('load', function () {
3476          var payload = null;
3477          try { payload = xhr.responseText ? JSON.parse(xhr.responseText) : null; } catch (e) {}
3478          if (xhr.status === 401) {
3479            reject(new Error('AUTH_REQUIRED'));
3480            return;
3481          }
3482          if (xhr.status < 200 || xhr.status >= 300) {
3483            var msg =
3484              payload && payload.error
3485                ? (typeof payload.error === 'string' ? payload.error : payload.error.message)
3486                : 'Flutter ZIP import failed (' + xhr.status + ').';
3487            reject(new Error(msg || 'Flutter ZIP import failed.'));
3488            return;
3489          }
3490          resolve(payload);
3491        });
3492        xhr.addEventListener('error', function () {
3493          reject(new Error('Network error while uploading the ZIP.'));
3494        });
3495        xhr.open('POST', '/api/import/flutter-zip');
3496        xhr.withCredentials = true;
3497        xhr.send(formData);
3498      });
3499    }
3500
3501    function importOrCreateFromStagedZip(brief, displayPrompt) {
3502      var file = stagedFiles[0] && stagedFiles[0].file;
3503      if (!file) {
3504        promptBackendChoice(brief, displayPrompt).then(function (choice) {
3505          if (!choice || !choice.provider) return;
3506          beginCreateProjectFromBrief(brief, displayPrompt, choice);
3507        });
3508        return;
3509      }
3510      submitting = true;
3511      startBuildingUI();
3512      if (statusRow) statusRow.hidden = false;
3513      if (statusEl) {
3514        statusEl.hidden = false;
3515        statusEl.classList.remove('is-error');
3516        statusEl.style.color = '';
3517        statusEl.textContent = 'Extracting your zip…';
3518      }
3519      postFlutterZip(file, brief)
3520        .then(function (data) {
3521          var projectId =
3522            data && data.project && typeof data.project.id === 'string'
3523              ? data.project.id
3524              : '';
3525          if (!projectId) throw new Error('Import succeeded but no project id was returned.');
3526          var conversationId =
3527            data && typeof data.conversationId === 'string' ? data.conversationId : '';
3528          try {
3529            sessionStorage.removeItem('fg:guest-intent-autosend');
3530            if (brief) {
3531              sessionStorage.setItem('od:auto-send-prompt:' + projectId, brief);
3532            }
3533          } catch (e) {}
3534          goWorkplace(projectId, conversationId);
3535        })
3536        .catch(function (err) {
3537          submitting = false;
3538          if (err && err.message === 'AUTH_REQUIRED') {
3539            resetBuildingUI();
3540            goAuthGate(brief);
3541            return;
3542          }
3543          var msg = err && err.message ? String(err.message) : '';
3544          if (/not a Flutter app/i.test(msg)) {
3545            if (submit) {
3546              submit.disabled = false;
3547              submit.classList.remove('is-building');
3548            }
3549            clearInterval(statusTimer);
3550            promptBackendChoice(brief, displayPrompt).then(function (choice) {
3551              if (!choice || !choice.provider) {
3552                showReadyStatus('Your zip is uploaded. Describe the app or click Start building.');
3553                return;
3554              }
3555              beginCreateProjectFromBrief(brief, displayPrompt, choice);
3556            });
3557            return;
3558          }
3559          resetBuildingUI(
3560            msg || 'The zip could not be imported.',
3561          );
3562        });
3563    }
3564
3565    /** True only for a real signed-up customer account (not guest / Local owner). */
3566    function isRegisteredUser(user) {
3567      return isCustomerAccount(user);
3568    }
3569
3570    /** Registered session only — guest minting removed. */
3571    function ensureSession() {
3572      return fetchAuthSession().then(function (session) {
3573        if (isRegisteredUser(session && session.user)) return session;
3574        throw new Error('AUTH_REQUIRED');
3575      });
3576    }
3577
3578    /*
3579     * Bounded on purpose.
3580     *
3581     * Every Start building click waits on this probe before it does anything
3582     * at all, and `fetch` against a stalled connection never settles — it does
3583     * not reject, it just never calls you back. An unbounded probe therefore
3584     * turned the entire homepage inert: the click was accepted, nothing was
3585     * painted, nothing navigated, no error appeared, and every further click
3586     * joined the same silence. Reproduced against production by holding
3587     * /api/auth/session open; it is pixel-identical to the reported bug.
3588     *
3589     * A probe that has not answered inside the deadline resolves as "no
3590     * session", which routes the visitor to the auth gate — somewhere — rather
3591     * than to nowhere.
3592     */
3593    var SESSION_PROBE_TIMEOUT_MS = 8000;
3594
3595    function fetchAuthSession() {
3596      var controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
3597      var init = { credentials: 'include', cache: 'no-store' };
3598      if (controller) init.signal = controller.signal;
3599      var timer = null;
3600      var deadline = new Promise(function (resolve) {
3601        timer = setTimeout(function () {
3602          if (controller) {
3603            try { controller.abort(); } catch (e) {}
3604          }
3605          resolve(null);
3606        }, SESSION_PROBE_TIMEOUT_MS);
3607      });
3608      var probe = fetch('/api/auth/session', init)
3609        .then(function (r) { return r.ok ? r.json() : null; })
3610        .catch(function () { return null; });
3611      return Promise.race([probe, deadline]).then(function (session) {
3612        clearTimeout(timer);
3613        return session;
3614      });
3615    }
3616
3617    function stashBuildIntent(brief) {
3618      try {
3619        sessionStorage.setItem('fg:guest-intent-prompt', brief);
3620        sessionStorage.setItem('fg:guest-intent-target', guestBuildTarget());
3621        sessionStorage.setItem('fg:guest-intent-arrived', '1');
3622        sessionStorage.setItem('fg:guest-intent-mode', 'flutter');
3623        sessionStorage.removeItem('fg:guest-intent-autosend');
3624        sessionStorage.setItem(
3625          'fg:guest-intent-has-images',
3626          stagedFiles.some(function (entry) {
3627            return looksLikeImage(entry && entry.file);
3628          }) ? '1' : '0',
3629        );
3630        sessionStorage.setItem(
3631          'fg:guest-intent-links',
3632          JSON.stringify(stagedLinks || []),
3633        );
3634      } catch (e) {}
3635      void persistStagedAttachmentsForResume();
3636    }
3637
3638    function openStagedAttachmentDb() {
3639      return new Promise(function (resolve, reject) {
3640        try {
3641          var req = indexedDB.open('fg-guest-staged-attachments', 1);
3642          req.onupgradeneeded = function () {
3643            var db = req.result;
3644            if (!db.objectStoreNames.contains('pending')) {
3645              db.createObjectStore('pending');
3646            }
3647          };
3648          req.onsuccess = function () { resolve(req.result); };
3649          req.onerror = function () { reject(req.error || new Error('idb open failed')); };
3650        } catch (e) {
3651          reject(e);
3652        }
3653      });
3654    }
3655
3656    function persistStagedAttachmentsForResume() {
3657      if (!stagedFiles.length && !stagedLinks.length) {
3658        return clearPersistedStagedAttachments();
3659      }
3660      return openStagedAttachmentDb()
3661        .then(function (db) {
3662          return Promise.all(
3663            stagedFiles.map(function (entry) {
3664              var file = entry && entry.file;
3665              if (!file) return null;
3666              return file.arrayBuffer().then(function (buf) {
3667                return {
3668                  name: file.name,
3669                  type: file.type || 'application/octet-stream',
3670                  lastModified: file.lastModified || Date.now(),
3671                  buffer: buf,
3672                };
3673              });
3674            }),
3675          ).then(function (serialized) {
3676            return new Promise(function (resolve, reject) {
3677              var tx = db.transaction('pending', 'readwrite');
3678              tx.objectStore('pending').put(
3679                {
3680                  files: serialized.filter(Boolean),
3681                  links: stagedLinks.slice(),
3682                  savedAt: Date.now(),
3683                },
3684                'resume',
3685              );
3686              tx.oncomplete = function () { resolve(); };
3687              tx.onerror = function () { reject(tx.error); };
3688            });
3689          });
3690        })
3691        .catch(function () {});
3692    }
3693
3694    function clearPersistedStagedAttachments() {
3695      return openStagedAttachmentDb()
3696        .then(function (db) {
3697          return new Promise(function (resolve) {
3698            var tx = db.transaction('pending', 'readwrite');
3699            tx.objectStore('pending').delete('resume');
3700            tx.oncomplete = function () { resolve(); };
3701            tx.onerror = function () { resolve(); };
3702          });
3703        })
3704        .catch(function () {});
3705    }
3706
3707    function restorePersistedStagedAttachments() {
3708      return openStagedAttachmentDb()
3709        .then(function (db) {
3710          return new Promise(function (resolve) {
3711            var tx = db.transaction('pending', 'readonly');
3712            var req = tx.objectStore('pending').get('resume');
3713            req.onsuccess = function () { resolve(req.result || null); };
3714            req.onerror = function () { resolve(null); };
3715          });
3716        })
3717        .then(function (payload) {
3718          if (!payload) return false;
3719          if (Array.isArray(payload.links) && payload.links.length && stagedLinks.length === 0) {
3720            stagedLinks = payload.links.slice();
3721          }
3722          if (Array.isArray(payload.files) && payload.files.length && stagedFiles.length === 0) {
3723            payload.files.forEach(function (item) {
3724              if (!item || !item.buffer) return;
3725              try {
3726                var file = new File([item.buffer], item.name || 'attachment', {
3727                  type: item.type || 'application/octet-stream',
3728                  lastModified: item.lastModified || Date.now(),
3729                });
3730                var previewUrl = looksLikeImage(file) ? URL.createObjectURL(file) : '';
3731                stagedFiles.push({ file: file, previewUrl: previewUrl });
3732              } catch (e) {}
3733            });
3734          }
3735          try {
3736            if (typeof renderAttachChips === 'function') renderAttachChips();
3737          } catch (e) {}
3738          return stagedFiles.length > 0 || stagedLinks.length > 0;
3739        })
3740        .catch(function () { return false; });
3741    }
3742
3743    function takeStashedBuildPrompt() {
3744      try {
3745        var prompt = sessionStorage.getItem('fg:guest-intent-prompt');
3746        sessionStorage.removeItem('fg:guest-intent-autosend');
3747        return prompt && String(prompt).trim() ? String(prompt).trim() : '';
3748      } catch (e) {
3749        return '';
3750      }
3751    }
3752
3753    function takeStashedLinks() {
3754      try {
3755        var raw = sessionStorage.getItem('fg:guest-intent-links');
3756        sessionStorage.removeItem('fg:guest-intent-links');
3757        if (!raw) return [];
3758        var parsed = JSON.parse(raw);
3759        return Array.isArray(parsed) ? parsed : [];
3760      } catch (e) {
3761        return [];
3762      }
3763    }
3764
3765    function authGateResumeUrl(brief) {
3766      // File uploads still resume in the landing importer, which owns their
3767      // IndexedDB payload and upload flow. This handoff carries text prompts.
3768      if (stagedFiles.length) return Promise.resolve('/?resumeBuild=1');
3769      if (!brief) return Promise.resolve('/home');
3770      // The opaque id travels in `next` through password/provider auth and
3771      // the verification email, including when it opens in a different tab.
3772      // sessionStorage is the fallback if saving the intent is unavailable.
3773      var controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
3774      var timer;
3775      var deadline = new Promise(function (resolve) {
3776        timer = setTimeout(function () {
3777          if (controller) controller.abort();
3778          resolve('/home');
3779        }, 4000);
3780      });
3781      var request = fetch('/api/guest-intents', {
3782        method: 'POST',
3783        credentials: 'same-origin',
3784        headers: { 'Content-Type': 'application/json' },
3785        signal: controller ? controller.signal : undefined,
3786        body: JSON.stringify({ prompt: brief, source: 'landing', template: guestIntentTemplate() }),
3787      }).then(function (response) {
3788        return response.ok ? response.json() : null;
3789      }).then(function (result) {
3790        var id = result && result.intentId;
3791        return typeof id === 'string' && /^[\w-]{1,128}$/.test(id)
3792          ? '/home?intent=' + encodeURIComponent(id)
3793          : '/home';
3794      }).catch(function () { return '/home'; });
3795      return Promise.race([request, deadline]).finally(function () { clearTimeout(timer); });
3796    }
3797
3798    /*
3799     * The home ↔ signup loop breaker is a timestamp, not a sticky flag.
3800     *
3801     * It used to be the literal '1', cleared only by a later home load that
3802     * happened to take the "not resuming" branch of resumeBuildAfterAuth(). A
3803     * visitor who opened signup, came back without finishing and stayed on the
3804     * page left it set — and from then on every Start building click fell into
3805     * the loop breaker below and did nothing but write one line of red text.
3806     * Permanent state is the wrong shape for "we just bounced": a short window
3807     * is long enough to catch the bounce and cannot outlive it.
3808     */
3809    var AUTH_GATE_TTL_MS = 60000;
3810
3811    function authGateIsActive() {
3812      try {
3813        var raw = sessionStorage.getItem('fg:auth-gate-active');
3814        if (!raw) return false;
3815        var at = Number(raw);
3816        // '1' is the legacy sticky value. Treating it as expired is what heals
3817        // a browser that is already stuck, on its very next click.
3818        if (!isFinite(at) || at <= 1 || Date.now() - at > AUTH_GATE_TTL_MS) {
3819          clearAuthGate();
3820          return false;
3821        }
3822        return true;
3823      } catch (e) {
3824        return false;
3825      }
3826    }
3827
3828    /**
3829     * Logged-out Start building → signup page (new visitors convert there;
3830     * sign-in is one click away). Restore the prompt for review in Home.
3831     * Guarded so home ↔ signup cannot blink in a redirect loop.
3832     */
3833    function goAuthGate(brief) {
3834      // Never redirect away from the auth pages themselves.
3835      try {
3836        if (location.pathname.indexOf('/onboarding/') === 0) return;
3837      } catch (e) {}
3838      try {
3839        if (authGateIsActive()) {
3840          // Stay put after one bounce — but tell the visitor why Start
3841          // building did not continue.
3842          resetBuildingUI('Sign in to continue building, then we will open your project.');
3843          return;
3844        }
3845        sessionStorage.setItem('fg:auth-gate-active', String(Date.now()));
3846      } catch (e) {}
3847      if (brief) stashBuildIntent(brief);
3848      submitting = true;
3849      if (submit) submit.disabled = true;
3850      trackLandingFunnel('signup_page_opened', 'hero_prompt', 'get-started');
3851      authGateResumeUrl(brief).then(function (next) {
3852        window.location.assign('/onboarding/get-started?next=' + encodeURIComponent(next));
3853      });
3854    }
3855
3856    function clearAuthGate() {
3857      try {
3858        sessionStorage.removeItem('fg:auth-gate-active');
3859      } catch (e) {}
3860    }
3861
3862    if (zipInput) {
3863      zipInput.addEventListener('change', function () {
3864        var file = zipInput.files && zipInput.files[0];
3865        zipInput.value = '';
3866        if (file) stageUploadedZip(file);
3867      });
3868    }
3869
3870    if (githubModal) {
3871      Array.prototype.forEach.call(document.querySelectorAll('[data-guest-github-dismiss]'), function (btn) {
3872        btn.addEventListener('click', function (event) {
3873          event.preventDefault();
3874          closeGithubModal();
3875        });
3876      });
3877      if (githubConnectBtn) {
3878        githubConnectBtn.addEventListener('click', function (event) {
3879          event.preventDefault();
3880          connectGithubFromModal();
3881        });
3882      }
3883      if (githubSubmitBtn) {
3884        githubSubmitBtn.addEventListener('click', function (event) {
3885          event.preventDefault();
3886          importGithubRepo();
3887        });
3888      }
3889      Array.prototype.forEach.call(document.querySelectorAll('[data-guest-github-visibility]'), function (btn) {
3890        btn.addEventListener('click', function (event) {
3891          event.preventDefault();
3892          setGithubVisibility(btn.getAttribute('data-guest-github-visibility'));
3893        });
3894      });
3895      if (githubBack) {
3896        githubBack.addEventListener('click', function (event) {
3897          event.preventDefault();
3898          githubVisibility = null;
3899          setGithubError('');
3900          syncGithubModalUi();
3901        });
3902      }
3903      if (githubRepo) {
3904        githubRepo.addEventListener('keydown', function (event) {
3905          if (event.key === 'Enter') {
3906            event.preventDefault();
3907            importGithubRepo();
3908          }
3909        });
3910      }
3911      document.addEventListener('keydown', function (event) {
3912        if (event.key === 'Escape' && githubModal && !githubModal.hidden) {
3913          closeGithubModal();
3914        }
3915      });
3916    }
3917
3918    function createProjectFromBrief(brief, displayPrompt) {
3919      if (submitting) return;
3920      if (isZipOnlyStaging()) {
3921        importOrCreateFromStagedZip(brief, displayPrompt);
3922        return;
3923      }
3924      promptBackendChoice(brief, displayPrompt).then(function (choice) {
3925        if (!choice || !choice.provider) return;
3926        beginCreateProjectFromBrief(brief, displayPrompt, choice);
3927      });
3928    }
3929
3930    /**
3931     * Full-page backend setup before landing create.
3932     * Auto-creates a Supabase project from the app brief when linked.
3933     */
3934    function promptBackendChoice(brief, displayPrompt) {
3935      return new Promise(function (resolve) {
3936        var existing = document.getElementById('fg-backend-picker');
3937        if (existing) existing.remove();
3938        var existingStyle = document.getElementById('fg-backend-picker-style');
3939        if (existingStyle) existingStyle.remove();
3940
3941        if (!document.getElementById('fg-backend-picker-style')) {
3942          var style = document.createElement('style');
3943          style.id = 'fg-backend-picker-style';
3944          style.textContent = [
3945            '#fg-backend-picker{position:fixed;inset:0;z-index:260;display:flex;align-items:flex-start;justify-content:center;',
3946            'padding:clamp(40px,8vh,72px) 20px 32px;overflow:auto;color:#e9eef9;',
3947            'background:radial-gradient(60% 40% at 50% 0%,rgba(80,198,253,.08),transparent 55%),rgba(4,6,12,.72);',
3948            'backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}',
3949            '#fg-backend-picker *{box-sizing:border-box}',
3950            '#fg-backend-picker [hidden]{display:none!important}',
3951            '.fg-bp-card{position:relative;width:100%;max-width:520px;margin:0 auto;border-radius:20px;overflow:hidden;',
3952            'background:radial-gradient(90% 55% at 12% -10%,rgba(91,200,255,.07),transparent 50%),',
3953            'radial-gradient(70% 45% at 100% 0%,rgba(80,198,253,.06),transparent 45%),#0a101b;',
3954            'border:1px solid rgba(148,170,220,.14);box-shadow:0 1px 0 rgba(233,238,249,.04) inset,0 28px 64px rgba(0,0,0,.55);',
3955            'display:flex;flex-direction:column;min-height:0}',
3956            '.fg-bp-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:22px 24px 0}',
3957            '.fg-bp-kicker{margin:0 0 8px;font:500 10.5px/1.2 "JetBrains Mono",ui-monospace,Menlo,monospace;',
3958            'letter-spacing:.22em;text-transform:uppercase;
3958color:#77839b}',
3959            '.fg-bp-head h1{margin:0;max-width:16ch;font:italic 400 clamp(26px,3.4vw,32px)/1.15 Fraunces,Georgia,"Times New Roman",serif;',
3960            'letter-spacing:-.02em;color:#e9eef9}',
3961            '.fg-bp-close{appearance:none;width:34px;height:34px;margin-top:2px;border-radius:999px;',
3962            'border:1px solid rgba(148,170,220,.16);background:transparent;color:#94a0b6;cursor:pointer;font:inherit;font-size:18px;line-height:1}',
3963            '.fg-bp-close:hover{background:rgba(148,170,220,.06);border-color:rgba(148,170,220,.28);color:#e9eef9}',
3964            '.fg-bp-body{padding:14px 24px 8px;display:flex;flex-direction:column;gap:18px}',
3965            '.fg-bp-lead{margin:0;max-width:42ch;font:400 14.5px/1.55 Inter,system-ui,sans-serif;color:#94a0b6}',
3966            '.fg-bp-options{display:flex;flex-direction:column;gap:0;border-top:1px solid rgba(148,170,220,.1);border-bottom:1px solid rgba(148,170,220,.1)}',
3967            '.fg-bp-option{position:relative;border:0;background:transparent;overflow:hidden;',
3968            'transition:background 180ms cubic-bezier(.23,1,.32,1)}',
3969            '.fg-bp-option+.fg-bp-option{border-top:1px solid rgba(148,170,220,.1)}',
3970            '.fg-bp-option[data-active="true"]{background:linear-gradient(90deg,rgba(80,198,253,.08),rgba(80,198,253,.02) 42%,transparent)}',
3971            '.fg-bp-option[data-active="true"]::before{content:"";position:absolute;left:0;top:10px;bottom:10px;width:2px;border-radius:2px;background:#50C6FD}',
3972            '.fg-bp-option-head{display:flex;width:100%;align-items:flex-start;gap:14px;padding:16px 4px 16px 14px;text-align:left;',
3973            'background:transparent;border:0;color:inherit;cursor:pointer;font:inherit}',
3974            '.fg-bp-radio{flex:0 0 auto;width:18px;height:18px;margin-top:3px;border-radius:999px;border:1.5px solid rgba(148,170,220,.35);',
3975            'display:grid;place-items:center}',
3976            '.fg-bp-radio i{width:8px;height:8px;border-radius:999px;background:transparent;opacity:0;transform:scale(.6);',
3977            'transition:opacity 140ms cubic-bezier(.23,1,.32,1),transform 180ms cubic-bezier(.23,1,.32,1),background 140ms}',
3978            '.fg-bp-option[data-active="true"] .fg-bp-radio{border-color:#50C6FD;box-shadow:0 0 0 3px rgba(80,198,253,.12)}',
3979            '.fg-bp-option[data-active="true"] .fg-bp-radio i{background:#50C6FD;opacity:1;transform:scale(1)}',
3980            '.fg-bp-logo{flex:0 0 auto;width:34px;height:34px;border-radius:11px;display:grid;place-items:center;',
3981            'background:rgba(148,170,220,.06);border:1px solid rgba(148,170,220,.1);overflow:hidden}',
3982            '.fg-bp-logo img{width:18px;height:18px;display:block}',
3983            '.fg-bp-logo--skip{font:italic 400 16px/1 Fraunces,Georgia,serif;color:#94a0b6}',
3984            '.fg-bp-copy{min-width:0;flex:1 1 auto;padding-right:8px}',
3985            '.fg-bp-copy strong{display:block;font:600 15px/1.3 Inter,system-ui,sans-serif;letter-spacing:-.01em;color:#e9eef9}',
3986            '.fg-bp-copy em{display:block;margin-top:3px;font:400 13px/1.45 Inter,system-ui,sans-serif;font-style:normal;color:#77839b}',
3987            '.fg-bp-badge{margin-left:auto;flex:0 0 auto;margin-top:2px;font:500 10.5px/1 "JetBrains Mono",ui-monospace,Menlo,monospace;',
3988            'letter-spacing:.04em;color:#77839b;white-space:nowrap;padding:0;border:0;background:transparent;border-radius:0}',
3989            '.fg-bp-badge[data-on="true"]{color:#34d399;background:transparent;border:0}',
3990            '.fg-bp-detail{display:none;padding:0 4px 18px 46px}',
3991            '.fg-bp-option[data-active="true"] .fg-bp-detail{display:block}',
3992            '.fg-bp-detail p{margin:0 0 10px;font:400 13px/1.5 Inter,system-ui,sans-serif;color:#94a0b6}',
3993            '.fg-bp-row{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:8px}',
3994            '.fg-bp-select,.fg-bp-input{flex:1 1 180px;min-width:0;height:38px;border-radius:10px;border:1px solid rgba(148,170,220,.18);',
3995            'background:#070b14;color:#e9eef9;padding:0 12px;font:inherit;font-size:13px}',
3996            '.fg-bp-field{display:flex;flex-direction:column;gap:4px;flex:1 1 100%;margin-bottom:8px}',
3997            '.fg-bp-field label{font:600 12px/1.3 Inter,system-ui,sans-serif;color:#c2cde1}',
3998            '.fg-bp-field span{font:400 11.5px/1.4 Inter,system-ui,sans-serif;color:#77839b}',
3999            '.fg-bp-btn{appearance:none;border-radius:10px;padding:8px 14px;font:600 13px/1.2 Inter,system-ui,sans-serif;',
4000            'cursor:pointer;border:1px solid transparent;height:38px}',
4001            '.fg-bp-btn:disabled{opacity:.45;cursor:not-allowed}',
4002            '.fg-bp-btn--ghost{background:transparent;color:#c2cde1;border-color:rgba(148,170,220,.18)}',
4003            '.fg-bp-btn--ghost:hover{background:rgba(148,170,220,.06)}',
4004            '.fg-bp-btn--soft{background:rgba(148,170,220,.08);color:#e9eef9;border-color:rgba(148,170,220,.14)}',
4005            '.fg-bp-btn--soft:hover{background:rgba(148,170,220,.12)}',
4006            '.fg-bp-btn--primary{background:#50C6FD;color:#0A1424;border-color:#50C6FD}',
4007            '.fg-bp-btn--primary:hover{filter:brightness(1.05)}',
4008            '.fg-bp-status{margin:0;font:400 12.5px/1.45 Inter,system-ui,sans-serif;color:#94a0b6;min-height:1.2em}',
4009            '.fg-bp-status[data-ok="true"]{color:#94a0b6}',
4010            '.fg-bp-status[data-err="true"]{color:#fca5a5}',
4011            '.fg-bp-foot{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:16px 24px 20px;',
4012            'border-top:1px solid rgba(148,170,220,.1);background:linear-gradient(180deg,transparent,rgba(4,6,12,.35))}',
4013            '.fg-bp-foot .fg-bp-status{flex:1 1 auto;min-width:0;color:#77839b}',
4014            '.fg-bp-foot-actions{display:flex;gap:10px;flex:0 0 auto}',
4015            '@media (max-width:560px){.fg-bp-foot{flex-direction:column;align-items:stretch}.fg-bp-foot-actions{justify-content:flex-end}',
4016            '.fg-bp-detail{padding-left:14px}}',
4017          ].join('');
4018
4019          document.head.appendChild(style);
4020        }
4021
4022        var page = document.createElement('div');
4023        page.id = 'fg-backend-picker';
4024        page.setAttribute('data-testid', 'landing-backend-picker');
4025        page.setAttribute('role', 'dialog');
4026        page.setAttribute('aria-modal', 'true');
4027        page.setAttribute('aria-labelledby', 'fg-bp-title');
4028        page.innerHTML =
4029          '<div class="fg-bp-card">' +
4030            '<div class="fg-bp-head">' +
4031              '<div>' +
4032                '<p class="fg-bp-kicker">Before the workspace</p>' +
4033                '<h1 id="fg-bp-title">Where should data live?</h1>' +
4034              '</div>' +
4035              '<button type="button" class="fg-bp-close" data-bp-cancel aria-label="Cancel">×</button>' +
4036            '</div>' +
4037            '<div class="fg-bp-body">' +
4038              '<p class="fg-bp-lead">Supabase if you want auth and a database on day one — or skip and wire it later.</p>' +
4039              '<div class="fg-bp-options" role="radiogroup" aria-label="Where should data live?">' +
4040                '<div class="fg-bp-option" data-provider="supabase" data-active="false">' +
4041                  '<button type="button" class="fg-bp-option-head" data-select="supabase" role="radio" aria-checked="false">' +
4042                    '<span class="fg-bp-radio" aria-hidden="true"><i></i></span>' +
4043                    '<span class="fg-bp-logo" aria-hidden="true"><img src="/brand/tech/supabase.svg" alt=""></span>' +
4044                    '<span class="fg-bp-copy"><strong>Supabase</strong><em>Auth, Postgres, Storage, and Realtime for this app</em></span>' +
4045                    '<span class="fg-bp-badge" data-badge="supabase">Checking…</span>' +
4046                  '</button>' +
4047                  '<div class="fg-bp-detail" data-body="supabase">' +
4048                    '<p data-sb-copy>Connect your Supabase account — we will create a project for this app once you are linked.</p>' +
4049                    '<div class="fg-bp-row" data-sb-connect-row>' +
4050                      '<button type="button" class="fg-bp-btn fg-bp-btn--soft" data-sb-connect>Connect Supabase</button>' +
4051                    '</div>' +
4052                    '<div data-sb-manual hidden>' +
4053                      '<p>OAuth is not set on this server. Paste project credentials from Supabase → Project Settings → API.</p>' +
4054                      '<div class="fg-bp-field"><label for="fg-bp-sb-url">Project URL</label>' +
4055                      '<span>https://xxxxx.supabase.co</span>' +
4056                      '<input id="fg-bp-sb-url" class="fg-bp-input" data-sb-url type="url" placeholder="https://xxxxx.supabase.co" autocomplete="off"></div>' +
4057                      '<div class="fg-bp-field"><label for="fg-bp-sb-anon">Publishable key</label>' +
4058                      '<span>anon / publishable key (not the service-role key)</span>' +
4059                      '<input id="fg-bp-sb-anon" class="fg-bp-input" data-sb-anon type="password" placeholder="eyJ…" autocomplete="off"></div>' +
4060                      '<div class="fg-bp-row"><button type="button" class="fg-bp-btn fg-bp-btn--soft" data-sb-manual-use>Use these credentials</button></div>' +
4061                    '</div>' +
4062                    '<div class="fg-bp-row" data-sb-projects hidden>' +
4063                      '<select class="fg-bp-select" data-sb-select aria-label="Supabase project"></select>' +
4064                      '<button type="button" class="fg-bp-btn fg-bp-btn--soft" data-sb-use>Use this project</button>' +
4065                    '</div>' +
4066                    '<p class="fg-bp-status" data-sb-status></p>' +
4067                  '</div>' +
4068                '</div>' +
4069                '<div class="fg-bp-option" data-provider="none" data-active="false">' +
4070                  '<button type="button" class="fg-bp-option-head" data-select="none" role="radio" aria-checked="false">' +
4071                    '<span class="fg-bp-radio" aria-hidden="true"><i></i></span>' +
4072                    '<span class="fg-bp-logo fg-bp-logo--skip" aria-hidden="true">—</span>' +
4073                    '<span class="fg-bp-copy"><strong>Skip for now</strong><em>Design the screens first. Link a backend when you need it.</em></span>' +
4074                    '<span class="fg-bp-badge">Later</span>' +
4075                  '</button>' +
4076                '</div>' +
4077              '</div>' +
4078            '</div>' +
4079            '<div class="fg-bp-foot">' +
4080              '<p class="fg-bp-status" data-bp-foot-status>Pick Supabase, or skip for now.</p>' +
4081              '<div class="fg-bp-foot-actions">' +
4082                '<button type="button" class="fg-bp-btn fg-bp-btn--ghost" data-bp-cancel>Cancel</button>' +
4083                '<button type="button" class="fg-bp-btn fg-bp-btn--primary" data-bp-continue disabled>Open workspace</button>' +
4084              '</div>' +
4085            '</div>' +
4086          '</div>';
4087
4088        document.body.appendChild(page);
4089        document.body.style.overflow = 'hidden';
4090
4091        var selected = null;
4092        var supabaseConnected = false;
4093        var supabaseOauthConfigured = false;
4094        var supabaseCredentials = null;
4095        var supabaseProvisioning = false;
4096        var supabaseProvisionFailed = false;
4097        var appProjectName = projectNameFromPrompt(displayPrompt || brief || '', {
4098          hasImages: stagedFiles.length > 0,
4099        });
4100        var continueBtn = page.querySelector('[data-bp-continue]');
4101        var footStatus = page.querySelector('[data-bp-foot-status]');
4102        var sbBadge = page.querySelector('[data-badge="supabase"]');
4103        var sbStatus = page.querySelector('[data-sb-status]');
4104        var sbProjects = page.querySelector('[data-sb-projects]');
4105        var sbSelect = page.querySelector('[data-sb-select]');
4106        var sbConnectRow = page.querySelector('[data-sb-connect-row]');
4107        var sbManual = page.querySelector('[data-sb-manual]');
4108        var sbUrl = page.querySelector('[data-sb-url]');
4109        var sbAnon = page.querySelector('[data-sb-anon]');
4110        var closed = false;
4111        var pollTimer = null;
4112
4113        function close(result) {
4114          if (closed) return;
4115          closed = true;
4116          if (pollTimer) clearInterval(pollTimer);
4117          page.remove();
4118          document.body.style.overflow = '';
4119          resolve(result);
4120        }
4121
4122        function setBadge(el, on, label) {
4123          el.setAttribute('data-on', on ? 'true' : 'false');
4124          el.textContent = label;
4125        }
4126
4127        function syncContinue() {
4128          var ready = false;
4129          var msg = 'Pick Supabase, or skip for now.';
4130          if (selected === 'none') {
4131            ready = true;
4132            msg = 'No backend yet — you will open the workspace and build the UI first.';
4133          } else if (selected === 'supabase') {
4134            if (supabaseCredentials) {
4135              ready = true;
4136              msg = 'Credentials ready — they will be saved when the app is created.';
4137            } else if (supabaseProvisioning) {
4138              msg = 'Creating a Supabase project for this app…';
4139            } else if (supabaseConnected && supabaseOauthConfigured) {
4140              msg = 'Creating a Supabase project for this app…';
4141            } else if (!supabaseOauthConfigured) {
4142              msg = 'Paste your project URL and publishable key, then continue.';
4143            } else {
4144              msg = 'Connect Supabase here, then continue.';
4145            }
4146          }
4147
4148          continueBtn.disabled = !ready;
4149          footStatus.textContent = msg;
4150          footStatus.removeAttribute('data-err');
4151          footStatus.setAttribute('data-ok', ready ? 'true' : 'false');
4152        }
4153
4154        function setSelected(provider) {
4155          selected = provider;
4156          page.querySelectorAll('.fg-bp-option').forEach(function (panel) {
4157            var on = panel.getAttribute('data-provider') === provider;
4158            panel.setAttribute('data-active', on ? 'true' : 'false');
4159            var head = panel.querySelector('[data-select]');
4160            if (head) head.setAttribute('aria-checked', on ? 'true' : 'false');
4161          });
4162          if (provider === 'supabase' && supabaseConnected && supabaseOauthConfigured && !supabaseCredentials) {
4163            autoProvisionSupabaseProject();
4164          }
4165          syncContinue();
4166        }
4167
4168        function loadSupabaseStatus() {
4169          return fetch('/api/supabase/oauth/status', { credentials: 'include', cache: 'no-store' })
4170            .then(function (r) { return r.ok ? r.json() : null; })
4171            .then(function (status) {
4172              if (!status) {
4173                setBadge(sbBadge, false, 'Unavailable');
4174                sbStatus.textContent = 'Could not read Supabase status.';
4175                sbStatus.setAttribute('data-err', 'true');
4176                return;
4177              }
4178              supabaseOauthConfigured = Boolean(status.configured);
4179              if (!status.configured) {
4180                setBadge(sbBadge, false, 'Paste keys');
4181                sbStatus.textContent = 'Paste project URL + publishable key below (OAuth is not set on this server).';
4182                sbStatus.removeAttribute('data-err');
4183                sbConnectRow.hidden = true;
4184                sbProjects.hidden = true;
4185                sbManual.hidden = false;
4186                syncContinue();
4187                return;
4188              }
4189              sbManual.hidden = true;
4190              supabaseConnected = Boolean(status.connected);
4191              setBadge(sbBadge, supabaseCredentials ? true : supabaseConnected, supabaseCredentials ? 'Ready' : (supabaseConnected ? (supabaseProvisioning ? 'Creating…' : 'Linked') : 'Not linked'));
4192              sbConnectRow.hidden = supabaseConnected;
4193              if (supabaseConnected) {
4194                sbStatus.textContent = 'Account connected.';
4195                sbStatus.setAttribute('data-ok', 'true');
4196                if (selected === 'supabase' && !supabaseCredentials) {
4197                  return autoProvisionSupabaseProject();
4198                }
4199                return;
4200              }
4201              sbProjects.hidden = true;
4202              sbStatus.textContent = 'Not linked yet — connect to continue with Supabase.';
4203              syncContinue();
4204            })
4205            .catch(function () {
4206              setBadge(sbBadge, false, 'Error');
4207              sbStatus.textContent = 'Could not read Supabase status.';
4208              sbStatus.setAttribute('data-err', 'true');
4209            });
4210        }
4211
4212        function autoProvisionSupabaseProject() {
4213          if (supabaseProvisioning || supabaseCredentials || !appProjectName) return;
4214          supabaseProvisioning = true;
4215          supabaseProvisionFailed = false;
4216          sbProjects.hidden = true;
4217          sbStatus.textContent = 'Creating a Supabase project for this app…';
4218          sbStatus.removeAttribute('data-err');
4219          setBadge(sbBadge, true, 'Creating…');
4220          syncContinue();
4221          return fetch('/api/supabase/projects', {
4222            method: 'POST',
4223            credentials: 'include',
4224            cache: 'no-store',
4225            headers: { 'content-type': 'application/json' },
4226            body: JSON.stringify({ name: appProjectName }),
4227          })
4228            .then(function (r) {
4229              return r.json().then(function (body) {
4230                if (!r.ok) {
4231                  throw new Error((body && body.error && body.error.message) || 'Could not create Supabase project');
4232                }
4233                return body;
4234              });
4235            })
4236            .then(function (created) {
4237              if (created && created.credentials && created.credentials.url && created.credentials.anonKey) {
4238                supabaseCredentials = {
4239                  url: created.credentials.url,
4240                  anonKey: created.credentials.anonKey,
4241                };
4242                sbStatus.textContent = 'Supabase project ready.';
4243                sbStatus.setAttribute('data-ok', 'true');
4244                setBadge(sbBadge, true, 'Ready');
4245                syncContinue();
4246                return;
4247              }
4248              throw new Error('Project is still starting — pick an existing project below.');
4249            })
4250            .catch(function (err) {
4251              supabaseProvisionFailed = true;
4252              supabaseCredentials = null;
4253              sbStatus.textContent = err && err.message ? err.message : 'Could not create Supabase project';
4254              sbStatus.setAttribute('data-err', 'true');
4255              setBadge(sbBadge, true, 'Linked');
4256              return loadSupabaseProjects();
4257            })
4258            .finally(function () {
4259              supabaseProvisioning = false;
4260              syncContinue();
4261            });
4262        }
4263
4264        function loadSupabaseProjects() {
4265          sbProjects.hidden = false;
4266          sbSelect.innerHTML = '<option value=\"\">Loading projects…</option>';
4267          return fetch('/api/supabase/projects', { credentials: 'include', cache: 'no-store' })
4268            .then(function (r) {
4269              if (!r.ok) throw new Error('Could not list Supabase projects');
4270              return r.json();
4271            })
4272            .then(function (body) {
4273              var projects = body && Array.isArray(body.projects) ? body.projects : [];
4274              if (!projects.length) {
4275                sbSelect.innerHTML = '<option value=\"\">No projects found</option>';
4276                sbStatus.textContent = 'Connected, but no projects yet — create one in Supabase to continue.';
4277                syncContinue();
4278                return;
4279              }
4280              sbSelect.innerHTML = projects.map(function (p) {
4281                var label = (p.name || p.ref || 'Project') + (p.ref ? ' · ' + p.ref : '');
4282                return '<option value=\"' + String(p.ref).replace(/\"/g, '') + '\">' + label.replace(/</g, '&lt;') + '</option>';
4283              }).join('');
4284              sbStatus.textContent = supabaseProvisionFailed
4285                ? 'Automatic setup failed — pick an existing project to continue.'
4286                : 'Pick a project to prefill secrets.';
4287              syncContinue();
4288            })
4289            .catch(function (err) {
4290              sbSelect.innerHTML = '<option value=\"\">Could not load</option>';
4291              sbStatus.textContent = err && err.message ? err.message : 'Could not list projects';
4292              sbStatus.setAttribute('data-err', 'true');
4293              syncContinue();
4294            });
4295        }
4296
4297
4298        function startSupabaseOAuth() {
4299          sbStatus.textContent = 'Opening Supabase…';
4300          sbStatus.removeAttribute('data-err');
4301          fetch('/api/supabase/oauth/start', {
4302            method: 'POST',
4303            credentials: 'include',
4304            headers: { 'content-type': 'application/json' },
4305            body: '{}',
4306          })
4307            .then(function (r) {
4308              return r.text().then(function (text) {
4309                var body = text ? JSON.parse(text) : null;
4310                if (!r.ok) {
4311                  throw new Error((body && body.error && body.error.message) || ('HTTP ' + r.status));
4312                }
4313                return body;
4314              });
4315            })
4316            .then(function (started) {
4317              if (!started || !started.authorizeUrl) throw new Error('No authorize URL returned');
4318              window.open(started.authorizeUrl, '_blank', 'noopener,noreferrer');
4319              sbStatus.textContent = 'Finish signing in with Supabase, then this page will refresh.';
4320            })
4321            .catch(function (err) {
4322              sbStatus.textContent = err && err.message ? err.message : 'Could not start Supabase connect';
4323              sbStatus.setAttribute('data-err', 'true');
4324            });
4325        }
4326
4327        function useSupabaseProject() {
4328          var ref = sbSelect.value;
4329          if (!ref) return;
4330          sbStatus.textContent = 'Fetching credentials…';
4331          fetch('/api/supabase/projects/' + encodeURIComponent(ref) + '/credentials', {
4332            credentials: 'include',
4333            cache: 'no-store',
4334          })
4335            .then(function (r) {
4336              return r.json().then(function (body) {
4337                if (!r.ok) throw new Error((body && body.error && body.error.message) || 'Could not load credentials');
4338                return body;
4339              });
4340            })
4341            .then(function (creds) {
4342              if (!creds || !creds.anonKey || !creds.url) {
4343                throw new Error(creds && creds.unavailable === 'project_paused'
4344                  ? 'That project is paused in Supabase. Resume it, then try again.'
4345                  : 'Anon key unavailable for that project.');
4346              }
4347              supabaseCredentials = { url: creds.url, anonKey: creds.anonKey };
4348              sbStatus.textContent = 'Project credentials ready.';
4349              sbStatus.setAttribute('data-ok', 'true');
4350              syncContinue();
4351            })
4352            .catch(function (err) {
4353              supabaseCredentials = null;
4354              sbStatus.textContent = err && err.message ? err.message : 'Could not load credentials';
4355              sbStatus.setAttribute('data-err', 'true');
4356              syncContinue();
4357            });
4358        }
4359
4360
4361        function onSupabaseMessage(event) {
4362          var data = event && event.data;
4363          if (!data || data.type !== 'supabase-oauth') return;
4364          if (data.ok) {
4365            sbStatus.textContent = 'Connected — creating Supabase project…';
4366            loadSupabaseStatus().then(function () {
4367              if (selected === 'supabase') autoProvisionSupabaseProject();
4368            });
4369          } else {
4370            sbStatus.textContent = data.message || 'Supabase connect failed';
4371            sbStatus.setAttribute('data-err', 'true');
4372          }
4373        }
4374
4375        var bc = null;
4376        try {
4377          bc = new BroadcastChannel('open-design-supabase-oauth');
4378          bc.onmessage = function (ev) { onSupabaseMessage({ data: ev.data }); };
4379        } catch (e) {}
4380        window.addEventListener('message', onSupabaseMessage);
4381        pollTimer = setInterval(function () {
4382          if (selected === 'supabase' && !supabaseConnected) loadSupabaseStatus();
4383        }, 4000);
4384
4385        page.querySelectorAll('[data-select]').forEach(function (btn) {
4386          btn.addEventListener('click', function () {
4387            setSelected(btn.getAttribute('data-select'));
4388          });
4389        });
4390        page.querySelector('[data-sb-connect]').addEventListener('click', startSupabaseOAuth);
4391        page.querySelector('[data-sb-use]').addEventListener('click', useSupabaseProject);
4392        page.querySelector('[data-sb-manual-use]').addEventListener('click', function () {
4393          var url = (sbUrl.value || '').trim();
4394          var anonKey = (sbAnon.value || '').trim();
4395          if (!url || !anonKey) {
4396            sbStatus.textContent = 'Enter both the project URL and publishable key.';
4397            sbStatus.setAttribute('data-err', 'true');
4398            return;
4399          }
4400          if (!/^https:\/\//i.test(url)) {
4401            sbStatus.textContent = 'Project URL must start with https://';
4402            sbStatus.setAttribute('data-err', 'true');
4403            return;
4404          }
4405          supabaseCredentials = { url: url, anonKey: anonKey };
4406          sbStatus.textContent = 'Credentials ready — continue to save them on create.';
4407          sbStatus.setAttribute('data-ok', 'true');
4408          sbStatus.removeAttribute('data-err');
4409          setBadge(sbBadge, true, 'Ready');
4410          syncContinue();
4411        });
4412        function cancelPicker() {
4413          window.removeEventListener('message', onSupabaseMessage);
4414          if (bc) try { bc.close(); } catch (e) {}
4415          close(null);
4416        }
4417        page.querySelectorAll('[data-bp-cancel]').forEach(function (btn) {
4418          btn.addEventListener('click', cancelPicker);
4419        });
4420        page.addEventListener('click', function (ev) {
4421          if (ev.target === page) cancelPicker();
4422        });
4423        continueBtn.addEventListener('click', function () {
4424          if (continueBtn.disabled || !selected) return;
4425          window.removeEventListener('message', onSupabaseMessage);
4426          if (bc) try { bc.close(); } catch (e) {}
4427          var result = { provider: selected };
4428          if (selected === 'supabase' && supabaseCredentials) {
4429            result.supabaseCredentials = supabaseCredentials;
4430          }
4431          close(result);
4432        });
4433
4434        loadSupabaseStatus();
4435      });
4436    }
4437
4438    function beginCreateProjectFromBrief(brief, displayPrompt, choice) {
4439      if (submitting) return;
4440      submitting = true;
4441      var backendProvider = choice && choice.provider ? choice.provider : null;
4442      var supabaseCredentials = choice && choice.supabaseCredentials ? choice.supabaseCredentials : null;
4443      var hasFigmaLinkEarly = stagedLinks.some(function (link) {
4444        return link && link.kind === 'figma' && link.url;
4445      }) || /figma\.com\/(?:design|file|proto)\//i.test(brief);
4446      startBuildingUI({ figma: hasFigmaLinkEarly });
4447      trackLandingFunnel('project_creation_started', 'landing_prompt', null);
4448
4449      stashBuildIntent(brief);
4450
4451      fetch('/api/guest-intents', {
4452        method: 'POST',
4453        headers: { 'Content-Type': 'application/json' },
4454        credentials: 'include',
4455        body: JSON.stringify({
4456          prompt: brief,
4457          source: 'landing',
4458          template: guestIntentTemplate(),
4459        }),
4460      }).catch(function () {});
4461
4462      var filesToUpload = stagedFiles.map(function (entry) { return entry.file; });
4463      var name = projectNameFromPrompt(displayPrompt || brief, { hasImages: filesToUpload.length > 0 });
4464      var projectId = uuid();
4465      var createBody = buildCreateBody(brief, name, projectId, {
4466        backendProvider: backendProvider || null,
4467      });
4468      var hasFigmaLink = hasFigmaLinkEarly || Boolean(createBody.pluginId === 'od-figma-migration');
4469      if (hasFigmaLink && createBody.pluginId !== 'od-figma-migration') {
4470        var forcedUrl =
4471          (createBody.pluginInputs && createBody.pluginInputs.figmaUrl) ||
4472          scrapeFigmaFileUrlFromText(brief);
4473        if (forcedUrl) {
4474          createBody.pluginId = 'od-figma-migration';
4475          createBody.pluginInputs = {
4476            figmaUrl: forcedUrl,
4477            targetStack: 'Flutter (Dart) — Android & iOS',
4478          };
4479        }
4480      }
4481
4482      // Create itself is fast (Flutter seed). Figma PNG pull runs in the
4483      // background and is polled separately — do not block create for minutes.
4484      var createAbort = typeof AbortController !== 'undefined' ? new AbortController() : null;
4485      var createTimer = null;
4486      if (createAbort) {
4487        createTimer = setTimeout(function () {
4488          try { createAbort.abort(); } catch (e) {}
4489        }, 45000);
4490      }
4491
4492      function mapCreateError(body, fallback) {
4493        var err = body && body.error;
4494        if (!err) return fallback;
4495        if (typeof err === 'string' && err.trim()) return err;
4496        var code = typeof err.code === 'string' ? err.code : '';
4497        var msg = typeof err.message === 'string' ? err.message.trim() : '';
4498        if (code === 'CONNECTOR_AUTH_CONFIG_REQUIRED') {
4499          return msg || 'Figma is not enabled on this server yet (Composio is not configured). Ask an admin to enable Figma under Admin → Connectors, then connect your Figma account before Start building.';
4500        }
4501        if (code === 'CONNECTOR_NOT_CONNECTED') {
4502          return msg || 'Connect your Figma account first (Integrations → Figma), then try again. We will not start the chat without file access.';
4503        }
4504        if (code === 'FIGMA_EXTRACT_FAILED') {
4505          return msg || 'Could not pull screens and assets from Figma. Reconnect Figma, confirm the file is shared with your account, then try again.';
4506        }
4507        if (code === 'VALIDATION_FAILED' && /figma/i.test(msg)) {
4508          return msg;
4509        }
4510        if (code === 'NOT_FOUND' && /figma/i.test(msg)) {
4511          return msg;
4512        }
4513        if (code === 'plugin-not-found' || code === 'missing-input') {
4514          return msg || 'Figma setup is incomplete. Connect Figma and paste a file URL, then try again.';
4515        }
4516        return msg || fallback;
4517      }
4518
4519      function waitForFigmaExtract(createdId) {
4520        var deadline = Date.now() + 12 * 60 * 1000;
4521        var missingMetaSince = Date.now();
4522        lastFigmaInventory.projectId = createdId || lastFigmaInventory.projectId || '';
4523        function poll() {
4524          return fetch('/api/projects/' + encodeURIComponent(createdId), {
4525            credentials: 'include',
4526            cache: 'no-store',
4527          }).then(function (r) {
4528            if (r.status === 404) {
4529              throw new Error(
4530                'Could not pull screens and assets from Figma (extract failed). Reconnect Figma, confirm the file is shared with your account, then try again.',
4531              );
4532            }
4533            if (!r.ok) {
4534              throw new Error('Could not check Figma extract status. Please try again.');
4535            }
4536            return r.json();
4537          }).then(function (body) {
4538            var project = body && body.project ? body.project : body;
4539            var fe = project && project.metadata ? project.metadata.figmaExtract : null;
4540            if (!fe) {
4541              // Extract should have been started with od-figma-migration. If
4542              // metadata never appears, do NOT treat that as success (that used
4543              // to open an empty design/ workspace while the progress UI lied).
4544              if (Date.now() - missingMetaSince > 20000) {
4545                throw new Error(
4546  
4546                'Figma extract did not start. Paste a full Figma file URL (figma.com/design/…), connect Figma, and try again.',
4547                );
4548              }
4549              if (Date.now() > deadline) {
4550                throw new Error(
4551                  'Pulling screens and assets from Figma timed out. This is usually a temporary Figma rate limit — not because the file is too big (a dozen screens is fine). Wait 2–5 minutes, then try once more.',
4552                );
4553              }
4554              return new Promise(function (resolve) {
4555                setTimeout(resolve, 1200);
4556              }).then(poll);
4557            }
4558            if (fe.phase) {
4559              setFigmaPhase(String(fe.phase), {
4560                screenNames: fe.screenNames,
4561                screenCount: fe.screenCount,
4562                assetCount: fe.assetCount,
4563              });
4564            }
4565            // Refresh real frame names + PNG paths while extract is still running
4566            // so "View all exports" can show phone previews as soon as files land.
4567            var enrichPromise = enrichFigmaInventoryFromProject(createdId);
4568            if (fe.status === 'ok' || fe.ok === true) {
4569              setFigmaPhase(
4570                fe.phase || ('Pulled ' + (fe.screenCount || 0) + ' screens'),
4571                {
4572                  screenNames: fe.screenNames,
4573                  screenCount: fe.screenCount,
4574                  assetCount: fe.assetCount,
4575                },
4576              );
4577              return enrichPromise.then(function () {
4578                return null;
4579              });
4580            }
4581            if (fe.status === 'failed' || (fe.ok === false && fe.status !== 'running')) {
4582              throw new Error(
4583                (fe.error && String(fe.error).trim())
4584                  || 'Could not pull screens and assets from Figma. Reconnect Figma and try again.',
4585              );
4586            }
4587            if (Date.now() > deadline) {
4588              throw new Error(
4589                'Pulling screens and assets from Figma timed out. This is usually a temporary Figma rate limit — not because the file is too big (a dozen screens is fine). Wait 2–5 minutes, then try once more.',
4590              );
4591            }
4592            return enrichPromise.catch(function () {
4593              return null;
4594            }).then(function () {
4595              return new Promise(function (resolve) {
4596                setTimeout(resolve, 1500);
4597              });
4598            }).then(poll);
4599          });
4600        }
4601        setFigmaPhase('Connecting to your Figma account…');
4602        return poll();
4603      }
4604
4605      function enrichFigmaInventoryFromProject(createdId) {
4606        // Prefer live screens.json so "View all exports" lists real frame names
4607        // (and preview paths) even when metadata.screenNames was capped/empty.
4608        return fetch(
4609          projectRawPath(createdId, 'figma/screens.json'),
4610          { credentials: 'include', cache: 'no-store' },
4611        ).then(function (r) {
4612          if (!r.ok) return null;
4613          return r.json();
4614        }).then(function (json) {
4615          var screens = json && Array.isArray(json.screens) ? json.screens : [];
4616          if (!screens.length) return null;
4617          var names = [];
4618          var previews = [];
4619          for (var i = 0; i < screens.length; i++) {
4620            var s = screens[i];
4621            if (!s || typeof s.name !== 'string' || !s.name.trim()) continue;
4622            names.push(s.name.trim());
4623            previews.push({
4624              name: s.name.trim(),
4625              previewPath: guessFigmaPreviewPath(s),
4626            });
4627          }
4628          if (!names.length) return null;
4629          lastFigmaInventory.screenNames = names;
4630          lastFigmaInventory.screenCount = Math.max(lastFigmaInventory.screenCount || 0, names.length);
4631          lastFigmaInventory.screenPreviews = previews;
4632          lastFigmaInventory.projectId = createdId;
4633          renderFigmaInventory(
4634            lastFigmaInventory.screenNames,
4635            lastFigmaInventory.screenCount,
4636            lastFigmaInventory.assetCount,
4637          );
4638          return null;
4639        }).catch(function () {
4640          return null;
4641        });
4642      }
4643
4644      function postCreate() {
4645        return fetch('/api/projects', {
4646          method: 'POST',
4647          credentials: 'include',
4648          headers: { 'Content-Type': 'application/json' },
4649          body: JSON.stringify(createBody),
4650          signal: createAbort ? createAbort.signal : undefined,
4651        })
4652          .then(function (r) {
4653            if (createTimer) clearTimeout(createTimer);
4654            if (r.status === 401) {
4655              throw new Error('AUTH_REQUIRED');
4656            }
4657            if (!r.ok) {
4658              return r.json().then(function (body) {
4659                throw new Error(mapCreateError(body, 'Could not start your project. Try again.'));
4660              }, function () {
4661                throw new Error('Could not start your project. Try again.');
4662              });
4663            }
4664            return r.json();
4665          })
4666          .then(function (data) {
4667            if (!hasFigmaLink) return data;
4668            var createdId =
4669              data && data.project && typeof data.project.id === 'string'
4670                ? data.project.id
4671                : projectId;
4672            return waitForFigmaExtract(createdId).then(function () {
4673              return fetch('/api/projects/' + encodeURIComponent(createdId), {
4674                credentials: 'include',
4675                cache: 'no-store',
4676              }).then(function (r) {
4677                if (!r.ok) return data;
4678                return r.json().then(function (body) {
4679                  var project = body && body.project ? body.project : body;
4680                  var build = project && project.metadata ? project.metadata.figmaBuild : null;
4681                  var brief =
4682                    (createBody && createBody.pendingPrompt) ||
4683                    (data && data.project && data.project.pendingPrompt) ||
4684                    '';
4685                  if (!build || !brief) {
4686                    if (project) data.project = project;
4687                    return data;
4688                  }
4689                  var sequential = sequentialFigmaKickoff(brief, build);
4690                  return fetch('/api/projects/' + encodeURIComponent(createdId), {
4691                    method: 'PATCH',
4692                    credentials: 'include',
4693                    headers: { 'Content-Type': 'application/json' },
4694                    body: JSON.stringify({ pendingPrompt: sequential }),
4695                  }).then(function (patchResp) {
4696                    if (patchResp.ok) {
4697                      return patchResp.json().then(function (patched) {
4698                        var next = patched && patched.project ? patched.project : project;
4699                        data.project = Object.assign({}, next, {
4700                          pendingPrompt: sequential,
4701                          metadata: Object.assign({}, next.metadata || {}, { figmaBuild: build }),
4702                        });
4703                        return data;
4704                      }, function () {
4705                        data.project = Object.assign({}, project, { pendingPrompt: sequential });
4706                        return data;
4707                      });
4708                    }
4709                    data.project = Object.assign({}, project, { pendingPrompt: sequential });
4710                    return data;
4711                  }, function () {
4712                    data.project = Object.assign({}, project, { pendingPrompt: sequential });
4713                    return data;
4714                  });
4715                }, function () {
4716                  return data;
4717                });
4718              }, function () {
4719                return data;
4720              });
4721            });
4722          });
4723      }
4724
4725      // Preflight Figma access so we fail on the landing page with a clear
4726      // Connect CTA instead of starting a freestyle chat that scrapes wrong frames.
4727      var preflight = Promise.resolve();
4728      if (hasFigmaLink) {
4729        // Always keep the Figma plugin bind — never strip it to "just start".
4730        // Without a connected account, figma-extract cannot read the file.
4731        if (!createBody.pluginId) {
4732          var ensureUrl =
4733            (createBody.pluginInputs && createBody.pluginInputs.figmaUrl) ||
4734            scrapeFigmaFileUrlFromText(brief);
4735          if (ensureUrl) {
4736            createBody.pluginId = 'od-figma-migration';
4737            createBody.pluginInputs = {
4738              figmaUrl: ensureUrl,
4739              targetStack: 'Flutter (Dart) — Android & iOS',
4740            };
4741          }
4742        }
4743        preflight = fetch('/api/connectors/figma', {
4744          credentials: 'include',
4745          cache: 'no-store',
4746        })
4747          .then(function (r) {
4748            if (r.status === 401) throw new Error('AUTH_REQUIRED');
4749            if (!r.ok) return null;
4750            return r.json();
4751          })
4752          .then(function (detail) {
4753            var connector = detail && detail.connector ? detail.connector : detail;
4754            if (!connector) {
4755              // Probe failed — still hard-block; daemon gate is the fallback.
4756              throw new Error(
4757                'Connect your Figma account first, then click Start building. We need file access before extracting screens — we will not freestyle from a public scrape.',
4758              );
4759            }
4760            if (connector.status === 'connected') return null;
4761            throw new Error(
4762              'Connect your Figma account first, then click Start building. We need file access before extracting screens — we will not freestyle from a public scrape.',
4763            );
4764          });
4765      }
4766
4767      preflight
4768        .then(function () {
4769          return postCreate();
4770        })
4771        .then(function (data) {
4772          var createdId =
4773            data && data.project && typeof data.project.id === 'string'
4774              ? data.project.id
4775              : projectId;
4776          var conversationId =
4777            data && typeof data.conversationId === 'string' ? data.conversationId : '';
4778          return uploadProjectFiles(createdId, filesToUpload).then(function (uploaded) {
4779            var secretChain = Promise.resolve();
4780            if (supabaseCredentials && supabaseCredentials.url && supabaseCredentials.anonKey) {
4781              secretChain = fetch('/api/projects/' + encodeURIComponent(createdId) + '/secrets', {
4782                method: 'PUT',
4783                credentials: 'include',
4784                headers: { 'Content-Type': 'application/json' },
4785                body: JSON.stringify({ kind: 'env', target: 'SUPABASE_URL', value: supabaseCredentials.url }),
4786              }).then(function () {
4787                return fetch('/api/projects/' + encodeURIComponent(createdId) + '/secrets', {
4788                  method: 'PUT',
4789                  credentials: 'include',
4790                  headers: { 'Content-Type': 'application/json' },
4791                  body: JSON.stringify({ kind: 'env', target: 'SUPABASE_ANON_KEY', value: supabaseCredentials.anonKey }),
4792                });
4793              }).catch(function (err) {
4794                console.warn('Failed to save Supabase credentials on create', err);
4795              });
4796            }
4797            return secretChain.then(function () {
4798            try {
4799              clearAuthGate();
4800              sessionStorage.removeItem('fg:guest-intent-autosend');
4801              sessionStorage.setItem('od:auto-send-first:' + createdId, '1');
4802              if (createBody.pendingPrompt) {
4803                sessionStorage.setItem('od:auto-send-prompt:' + createdId, createBody.pendingPrompt);
4804              } else {
4805                sessionStorage.removeItem('od:auto-send-prompt:' + createdId);
4806              }
4807              if (uploaded.length) {
4808                sessionStorage.setItem(
4809                  'od:auto-send-attachments:' + createdId,
4810                  JSON.stringify(uploaded),
4811                );
4812              } else {
4813                sessionStorage.removeItem('od:auto-send-attachments:' + createdId);
4814              }
4815            } catch (e) {}
4816            void clearPersistedStagedAttachments();
4817            goWorkplace(createdId, conversationId);
4818            });
4819          });
4820        })
4821        .catch(function (err) {
4822          if (createTimer) clearTimeout(createTimer);
4823          if (err && err.message === 'AUTH_REQUIRED') {
4824            resetBuildingUI();
4825            goAuthGate(brief);
4826            return;
4827          }
4828          var aborted =
4829            (err && err.name === 'AbortError')
4830            || (createAbort && createAbort.signal && createAbort.signal.aborted);
4831          var message = aborted
4832            ? 'Starting the project took too long. Please try again.'
4833            : (err && err.message ? err.message : 'Could not start your project. Try again.');
4834          trackLandingFunnel(
4835            'project_creation_error',
4836            'landing_prompt',
4837            String(message).slice(0, 160),
4838          );
4839          if (isFigmaUnavailableError(message)) {
4840            markFigmaConnectAction('integrations');
4841            resetBuildingUI(message, {
4842              figmaConnect: true,
4843              connectLabel: 'Open Integrations',
4844            });
4845          } else if (isFigmaConnectError(message)) {
4846            markFigmaConnectAction('connect');
4847            resetBuildingUI(message, {
4848              figmaConnect: true,
4849              connectLabel: 'Connect Figma',
4850            });
4851          } else {
4852            markFigmaConnectAction('connect');
4853            resetBuildingUI(message);
4854          }
4855        });
4856    }
4857
4858    form.addEventListener('submit', function (event) {
4859      event.preventDefault();
4860      if (submitting) return;
4861      var prompt = (input.value || '').trim();
4862      if (!prompt && stagedFiles.length === 0 && stagedLinks.length === 0) {
4863        input.focus();
4864        return;
4865      }
4866      if (!prompt) {
4867        prompt = stagedLinks.length
4868          ? 'Build an Android & iOS app from the attached references.'
4869          : 'Build from the attached references.';
4870      }
4871      var brief = composeUserBrief(prompt);
4872      // Always count the click — even when auth gate redirects next.
4873      trackLandingFunnel('cta_clicked', 'hero_prompt', 'submit');
4874
4875      // Acknowledge the click BEFORE the session probe, not after it. Nothing
4876      // used to move until the probe answered, so a slow one was
4877      // indistinguishable from a dead button — and a stalled one WAS a dead
4878      // button.
4879      var endProbeFeedback = startProbeFeedback();
4880
4881      // Production rule:
4882      // - no registered account → Create account (prompt stashed for resume)
4883      // - registered session → create project and enter the workplace
4884      fetchAuthSession()
4885        .then(function (session) {
4886          endProbeFeedback();
4887          if (!isRegisteredUser(session && session.user)) {
4888            goAuthGate(brief);
4889            return;
4890          }
4891          createProjectFromBrief(brief, prompt);
4892        })
4893        .catch(function () {
4894          endProbeFeedback();
4895          goAuthGate(brief);
4896        });
4897    });
4898
4899    // After login/signup (?resumeBuild=1 or autosend flag), continue create.
4900    (function resumeBuildAfterAuth() {
4901      var wantResume = false;
4902      try {
4903        wantResume = new URLSearchParams(location.search).get('resumeBuild') === '1';
4904        if (sessionStorage.getItem('fg:guest-intent-autosend') === '1') wantResume = true;
4905      } catch (e) {}
4906      if (!wantResume) {
4907        // Normal home visit — allow a future Send to open signup again.
4908        clearAuthGate();
4909        return;
4910      }
4911
4912      // Strip resumeBuild immediately so a failed gate cannot blink home ↔ signup.
4913      try {
4914        var clean = new URL(location.href);
4915        if (clean.searchParams.has('resumeBuild')) {
4916          clean.searchParams.delete('resumeBuild');
4917          window.history.replaceState({}, '', clean.pathname + clean.search + clean.hash);
4918        }
4919      } catch (e) {}
4920
4921      fetchAuthSession()
4922        .then(function (session) {
4923          if (!isRegisteredUser(session && session.user)) {
4924            // Still signed out — open signup once. Do not loop.
4925            if (authGateIsActive()) {
4926              try {
4927                sessionStorage.removeItem('fg:guest-intent-autosend');
4928              } catch (e) {}
4929              // Declining THIS automatic redirect is the whole loop breaker.
4930              // Releasing the gate afterwards is what keeps the visitor's next
4931              // deliberate Start building click working — a click is not a loop.
4932              clearAuthGate();
4933              return;
4934            }
4935            goAuthGate(takeStashedBuildPrompt() || '');
4936            return;
4937          }
4938          clearAuthGate();
4939          var saved = takeStashedBuildPrompt();
4940          if (!saved) return;
4941          var stashedLinks = takeStashedLinks();
4942          restorePersistedStagedAttachments().then(function () {
4943            if (stashedLinks.length && stagedLinks.length === 0) {
4944              stagedLinks = stashedLinks.slice();
4945            }
4946            try {
4947              if (typeof renderAttachChips === 'function') renderAttachChips();
4948            } catch (e) {}
4949            input.value = saved;
4950            input.dispatchEvent(new Event('input', { bubbles: true }));
4951            createProjectFromBrief(saved, saved);
4952            void clearPersistedStagedAttachments();
4953          });
4954        })
4955        .catch(function () {});
4956    })();
4957
4958    if (examplesLabel) examplesLabel.textContent = 'Try an example app idea';
4959    input.placeholder = 'Describe the app you want…';
4960    renderExamples();
4961    resizeInput();
4962
4963    // Nav "Create a project" → /#guest-hero: scroll hero into view and focus prompt.
4964    (function focusHeroFromHash() {
4965      function go() {
4966        if (location.hash !== '#guest-hero') return;
4967        var hero = document.getElementById('guest-hero');
4968        if (hero) {
4969          try {
4970            hero.scrollIntoView({ behavior: 'smooth', block: 'start' });
4971          } catch (e) {
4972            hero.scrollIntoView(true);
4973          }
4974        }
4975        try {
4976          input.focus({ preventScroll: true });
4977        } catch (e) {
4978          input.focus();
4979        }
4980      }
4981      go();
4982      window.addEventListener('hashchange', go);
4983    })();
4984  })();
4985
4986  /* ---- Contact sales modal --------------------------------------------- */
4987  var SALES_EMAIL = '[email protected]';
4988
4989  function buildSalesMailto(fields) {
4990    var bodyLines = [
4991      (fields.message || '').trim(),
4992      '',
4993      '---',
4994      fields.name ? 'Name: ' + fields.name : '',
4995      fields.email ? 'Email: ' + fields.email : '',
4996      fields.company ? 'Company: ' + fields.company : ''
4997    ].filter(Boolean);
4998    var params = new URLSearchParams();
4999    params.set('subject', 'FlutterGo Enterprise inquiry');
5000    params.set('body', bodyLines.join('\n'));
5001    return 'mailto:' + SALES_EMAIL + '?' + params.toString();
5002  }
5003
5004  function ensureContactSalesModal() {
5005    var existing = document.getElementById('fg-contact-sales');
5006    if (existing) return existing;
5007
5008    var backdrop = document.createElement('div');
5009    backdrop.id = 'fg-contact-sales';
5010    backdrop.className = 'fgs-contact-backdrop';
5011    backdrop.hidden = true;
5012    backdrop.innerHTML =
5013      '<div class="fgs-contact-modal" role="dialog" aria-modal="true" aria-labelledby="fg-contact-sales-title">' +
5014        '<h2 id="fg-contact-sales-title" class="fgs-contact-modal__title">Contact sales</h2>' +
5015        '<p class="fgs-contact-modal__lead">Tell us about your team and what you need. We will open your email app with a draft you can send to our sales team.</p>' +
5016        '<form class="fgs-contact-form">' +
5017          '<label class="fgs-contact-field"><span>Name</span><input type="text" name="name" autocomplete="name" /></label>' +
5018          '<label class="fgs-contact-field"><span>Work email</span><input type="email" name="email" autocomplete="email" required /></label>' +
5019          '<label class="fgs-contact-field"><span>Company (optional)</span><input type="text" name="company" autocomplete="organization" /></label>' +
5020          '<label class="fgs-contact-field"><span>How can we help?</span><textarea name="message" rows="5" required placeholder="Team size, usage needs, security requirements, timeline…"></textarea></label>' +
5021          '<p class="fgs-contact-error" role="alert" hidden></p>' +
5022          '<div class="fgs-contact-actions">' +
5023            '<button type="button" class="fgs-btn fgs-btn--ghost fgs-btn--sm" data-contact-cancel>Cancel</button>' +
5024            '<button type="submit" class="fgs-btn fgs-btn--sm">Open email draft</button>' +
5025          '</div>' +
5026        '</form>' +
5027      '</div>';
5028
5029    document.body.appendChild(backdrop);
5030
5031    var form = backdrop.querySelector('.fgs-contact-form');
5032    var errorEl = backdrop.querySelector('.fgs-contact-error');
5033
5034    function closeModal() {
5035      backdrop.hidden = true;
5036      document.body.style.overflow = '';
5037    }
5038
5039    backdrop.addEventListener('click', function (e) {
5040      if (e.target === backdrop) closeModal();
5041    });
5042    backdrop.querySelector('[data-contact-cancel]').addEventListener('click', closeModal);
5043
5044    document.addEventListener('keydown', function (e) {
5045      if (e.key === 'Escape' && !backdrop.hidden) closeModal();
5046    });
5047
5048    form.addEventListener('submit', function (e) {
5049      e.preventDefault();
5050      var data = new FormData(form);
5051      var message = String(data.get('message') || '').trim();
5052      var email = String(data.get('email') || '').trim();
5053      if (!message) {
5054        errorEl.textContent = 'Tell us what you need so we can help.';
5055        errorEl.hidden = false;
5056        return;
5057      }
5058      if (!email) {
5059        errorEl.textContent = 'Add your email so we can reply.';
5060        errorEl.hidden = false;
5061        return;
5062      }
5063      errorEl.hidden = true;
5064      window.location.href = buildSalesMailto({
5065        name: String(data.get('name') || '').trim(),
5066        email: email,
5067        company: String(data.get('company') || '').trim(),
5068        message: message
5069      });
5070      closeModal();
5071      form.reset();
5072    });
5073
5074    return backdrop;
5075  }
5076
5077  function openContactSalesModal(prefill) {
5078    var backdrop = ensureContactSalesModal();
5079    var form = backdrop.querySelector('.fgs-contact-form');
5080    if (prefill && prefill.email) form.email.value = prefill.email;
5081    if (prefill && prefill.name) form.name.value = prefill.name;
5082    backdrop.hidden = false;
5083    document.body.style.overflow = 'hidden';
5084    var focusTarget = form.message || form.email;
5085    if (focusTarget) focusTarget.focus();
5086  }
5087
5088  document.querySelectorAll('[data-contact-sales]').forEach(function (btn) {
5089    btn.addEventListener('click', function () {
5090      fetch('/api/auth/session', { credentials: 'include', cache: 'no-store' })
5091        .then(function (resp) { return resp.ok ? resp.json() : null; })
5092        .then(function (session) {
5093          var user = session && session.user;
5094          openContactSalesModal(user ? { email: user.email, name: user.name } : null);
5095        })
5096        .catch(function () { openContactSalesModal(null); });
5097    });
5098  });
5099
5100  /* ---- Legal TOC scrollspy + smooth offset ---------------------------- */
5101  var tocLinks = Array.prototype.slice.call(document.querySelectorAll('.fgs-toc a'));
5102  if (tocLinks.length && 'IntersectionObserver' in window) {
5103    var byId = {};
5104    tocLinks.forEach(function (a) {
5105      var id = a.getAttribute('href').slice(1);
5106      if (id) byId[id] = a;
5107    });
5108    var spy = new IntersectionObserver(function (entries) {
5109      entries.forEach(function (en) {
5110        if (en.isIntersecting) {
5111          tocLinks.forEach(function (a) { a.classList.remove('is-active'); });
5112          var a = byId[en.target.id];
5113          if (a) a.classList.add('is-active');
5114        }
5115      });
5116    }, { rootMargin: '-20% 0px -70% 0px' });
5117    document.querySelectorAll('.fgs-prose h2[id]').forEach(function (h) { spy.observe(h); });
5118  }
5119})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.