Reuben Bowlby

Contact

Let's build something trustworthy.

Tell me about your team, your AI system, and what you're trying to ship. I'll get back to you within 24 hours.

Name is required.
A valid email is required.
Message is required.
/gi, '') .replace(/sonw+s*=s*"[^"]*"/gi, '') .replace(/sonw+s*=s*'[^']*'/gi, ''); status.innerHTML = sanitized; } else { status.textContent = msg; } } form.addEventListener('submit', async function (e) { e.preventDefault(); clearErrors(); var data = { name: form.name.value, email: form.email.value, company: form.company.value, project_type: form.project_type.value, message: form.message.value, 'cf-turnstile-response': (window.turnstile && window.turnstile.getResponse) ? window.turnstile.getResponse() : '' }; // Client-side validation var hasErr = false; if (!data.name.trim()) { setError('name', true); hasErr = true; } if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email.trim())) { setError('email', true); hasErr = true; } if (!data.message.trim()) { setError('message', true); hasErr = true; } if (hasErr) { showStatus('Please fix the highlighted fields and try again.', 'error'); return; } btn.classList.add('loading'); btn.disabled = true; try { var resp = await fetch('/contact/submit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); var json = await resp.json(); if (resp.ok && json.success) { form.reset(); if (window.turnstile && window.turnstile.reset) window.turnstile.reset(); showStatus('Message sent. ' + escapeHtml(json.message), 'success', true); } else if (resp.status === 429) { showStatus(json.error || 'Too many submissions. Please try again later.', 'error'); if (window.turnstile && window.turnstile.reset) window.turnstile.reset(); } else if (resp.status === 403) { showStatus(json.error || 'Anti-spam verification failed. Please refresh and try again.', 'error'); if (window.turnstile && window.turnstile.reset) window.turnstile.reset(); } else if (json.fields) { Object.keys(json.fields).forEach(function (k) { setError(k, true); }); showStatus('Please fix the highlighted fields and try again.', 'error'); } else { showStatus(json.error || 'Something went wrong. Please try again.', 'error'); if (window.turnstile && window.turnstile.reset) window.turnstile.reset(); } } catch (err) { showStatus('Network error. Please check your connection and try again.', 'error'); if (window.turnstile && window.turnstile.reset) window.turnstile.reset(); } finally { btn.classList.remove('loading'); btn.disabled = false; } }); })();