1<!DOCTYPE html><html lang="en"> <head><meta name="sentry-route-name" content="%2Fproduct%2Fmakers"/><meta charset="utf-8"><!-- Persona attribute BEFORE first paint (no-flicker rule, same pattern 2 as dark-mode gates): the header CTA's maker label is CSS keyed off 3 html[data-persona], so the right label renders on the very first 4 frame instead of swapping at DOMContentLoaded. Keys owned by 5 dev-entry.js (ritual_dev_entry / ritual_maker_entry). -->
5<script> 6 try { 7 var p = 8 localStorage.getItem("ritual_maker_entry") === "1" 9 ? "maker" 10 : localStorage.getItem("ritual_dev_entry") === "1" 11 ? "developer" 12 : ""; 13 if (p) document.documentElement.setAttribute("data-persona", p); 14 } catch (e) { 15 /* storage blocked: default CTA renders */ 16 } 17 </script>
17<!-- Same no-flicker rule, for the verified developer coming back from the 18 email link. The CLI is a client:only island, so the marketing page is 19 what paints while React loads, and they watched the page they had just 20 left reassemble itself before the console they came back for arrived. 21 Stamping html here covers that gap from the first frame; CliHeroPlugin 22 mounts straight into fullscreen underneath it and clears the flag. 23 24 The style is INLINE rather than in cli.css: that sheet ships with the 25 island's chunk, which by definition has not loaded yet at the moment 26 this needs to be painted. 27 28 Scoped to the developer pages, since ?return=1 is a common enough 29 param to belong to something else elsewhere. -->
29<script> 30 try { 31 if ( 32 location.pathname.indexOf("/product/developer") === 0 && 33 new URLSearchParams(location.search).get("return") === "1" 34 ) { 35 document.documentElement.setAttribute("data-cli-return", ""); 36 /* Never strand them on a dark screen: if the island fails to mount 37 (JS blocked, chunk 404), the page comes back on its own. Longer 38 than a slow mount, shorter than a visitor's patience. */ 39 setTimeout(function () { 40 document.documentElement.removeAttribute("data-cli-return"); 41 }, 8000); 42 } 43 } catch (e) { 44 /* no URL API / blocked: the old flash is the fallback, not a failure */ 45 } 46 </script>
46<style> 47 /* #1B1A2A is --cli-bg, hardcoded for the same reason the style is inline: 48 the sheet that defines the token has not arrived yet. */ 49 html[data-cli-return] { 50 background: #1b1a2a; 51 } 52 html[data-cli-return]::before { 53 content: ""; 54 position: fixed; 55 inset: 0; 56 background: #1b1a2a; 57 /* Under the takeover (100) so the console draws over it, above 58 everything else so the page underneath never shows. */ 59 z-index: 99; 60 /* A full-screen overlay that ate clicks would be its own bug. */ 61 pointer-events: none; 62 } 63 html[data-cli-return] body { 64 overflow: hidden; 65 } 66 </style><link rel="icon" type="image/png" href="/favicon.png"><link rel="apple-touch-icon" href="/favicon.png"><!-- interactive-widget=resizes-content: the on-screen keyboard SHRINKS the 67 layout viewport, so the prelogin CLI's fullscreen input + action button 68 sit above the keyboard instead of behind it on mobile. Harmless on 69 pages without the CLI (only changes keyboard-resize behavior). --><meta name="viewport" content="width=device-width, initial-scale=1, interactive-widget=resizes-content"><meta name="generator" content="Astro v6.4.8"><meta name="description" content="Turn the one line you would have typed into instructions that get you the app you meant. Works with Lovable, Claude, v0, Replit and more."><link rel="canonical" href="https://ritual.work/product/makers/"><!-- Open Graph --><meta property="og:type" content="website"><meta property="og:title" content="The spec your agent deserves · Ritual"><meta property="og:description" content="Turn the one line you would have typed into instructions that get you the app you meant. Works with Lovable, Claude, v0, Replit and more."><meta property="og:url" content="https://ritual.work/product/makers/"><meta property="og:image" content="https://ritual.work/hero-graph.png"><meta property="og:site_name" content="Ritual"><!-- Twitter --><meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="The spec your agent deserves · Ritual"><meta name="twitter:description" content="Turn the one line you would have typed into instructions that get you the app you meant. Works with Lovable, Claude, v0, Replit and more."><meta name="twitter:image" content="https://ritual.work/hero-graph.png"><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"><title>The spec your agent deserves · Ritual</title>
69<script>(function(){const posthogKey = "phc_uDVTitoexBzZHYAc46FpaX2uRMhA8EWmZFNPfEDDykAM"; 70const posthogHost = "https://us.i.posthog.com"; 71const releaseSha = "0db980f"; 72 73 !function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once register_for_session unregister unregister_for_session opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]); 74 75 // Don't send analytics from local dev hosts â keeps localhost test 76 // events (and their localhost $current_url / fake UTMs) out of the 77 // production PostHog project. The stub still exists, so capture/register 78 // calls below are harmless no-ops and the CTA-routing logic still runs. 79 var phHost = location.hostname; 80 var phIsDevHost = 81 phHost === "localhost" || 82 phHost === "127.0.0.1" || 83 phHost === "0.0.0.0" || 84 phHost === "::1" || 85 phHost === "[::1]" || 86 phHost.slice(-6) === ".local"; 87 if (!phIsDevHost) { 88 // Scope replay to the developer funnel â watch where users stall on 89 // /product/developer + the quickstart, without recording the whole site. 90 var phPath = location.pathname; 91 var phFunnelPage = 92 phPath.indexOf('/product/developer') === 0 || 93 phPath.indexOf('/product/makers') === 0 || 94 phPath.indexOf('/docs/quickstart') === 0; 95 96 posthog.init(posthogKey, { 97 api_host: posthogHost, 98 person_profiles: 'identified_only', 99 // Session replay stays OFF by default; `loaded` below turns it on for 100 // the funnel pages only. Inputs are masked, and anything tagged 101 // .ph-no-capture (e.g. the minted token) is masked in the replay too. 102 // (Also needs Session Replay enabled in the PostHog project settings.) 103 disable_session_recording: true, 104 session_recording: { maskAllInputs: true, maskTextSelector: '.ph-no-capture' }, 105 // MUST start replay from `loaded`, NOT synchronously after init(). 106 // 107 // This call used to sit right below init() behind a 108 // `posthog.startSessionRecording && â¦` guard, and it never once ran. 109 // At that moment window.posthog is still the SNIPPET STUB, which only 110 // proxies a fixed method list â capture, identify, register, 111 // onFeatureFlags and friends. `startSessionRecording` is NOT on that 112 // list, so the guard evaluated falsy and skipped silently, forever. 113 // Verified in-browser 2026-08-06 against production: at snippet time 114 // `!!posthog.startSessionRecording` is false; after the SDK loads it is 115 // a function, and `sessionRecording.status` reads "disabled" because 116 // nothing ever started it. $recording_status was "disabled" on all 117 // 1,782 developer-page events in the preceding two days. 118 // 119 // `loaded` fires with the REAL SDK, so the method exists. Same class of 120 // bug as the arm-resolution race this PR fixes: code assuming PostHog 121 // is ready when only its stub is. 122 loaded: function (ph) { 123 if (!phFunnelPage) return; 124 try { ph.startSessionRecording(); } catch (_) {} 125 }, 126 }); 127 // Deploy provenance on every client event (see frontmatter comment). 128 // `register` IS on the stub's proxy list, so this one is safe here. 129 if (releaseSha) { 130 posthog.register({ release_sha: releaseSha }); 131 } 132 } 133 134 // CTA click delegation: any element (or ancestor up to 6 levels)
135 // with data-ph-cta becomes a tracked CTA. We also pick up the 136 // resolved destination so "Get started" â /ritual-mcp/docs#install 137 // tells us where they went without needing a separate prop. 138 (function () { 139 function readCta(el) { 140 for (var i = 0; i < 6 && el && el !== document; i++) { 141 if (el.dataset && el.dataset.phCta) { 142 return { 143 cta: el.dataset.phCta, 144 location: el.dataset.phCtaLocation || null, 145 href: el.getAttribute && (el.getAttribute("href") || el.getAttribute("formaction")) || null, 146 text: (el.innerText || el.textContent || "").trim().slice(0, 80) || null, 147 }; 148 } 149 el = el.parentElement; 150 } 151 return null; 152 } 153 document.addEventListener("click", function (ev) { 154 var info = readCta(ev.target); 155 if (!info) return; 156 try { 157 window.posthog && window.posthog.capture && window.posthog.capture("cta_clicked", info); 158 } catch (_) {} 159 }, { capture: true }); 160 // Form submit tracking: tag the <form> with data-ph-cta to 161 // capture the actual submission (more meaningful than a click 162 // for the waitlist form). 163 document.addEventListener("submit", function (ev) { 164 var info = readCta(ev.target); 165 if (!info) return; 166 try { 167 window.posthog && window.posthog.capture && window.posthog.capture("cta_submitted", info); 168 } catch (_) {} 169 }, { capture: true }); 170 })(); 171 // Developer-entrant detection + CTA routing lives in the bundled 172 // module src/scripts/dev-entry.js (mounted via DevEntryRouting.astro), 173 // which can import the shared Keycloak/connect-flow constants. Kept 174 // out of this inline snippet so we don't duplicate auth config here. 175 })();</script>
175<script type="module" src="/_astro/DevEntryRouting.astro_astro_type_script_index_0_lang.BMf2VZsx.js"></script>
175<link rel="stylesheet" href="/_astro/PostHogScript.CAMvvd4h.css"><style>.cta-label-maker,html[data-persona=maker] .cta-label-default{display:none}html[data-persona=maker] .cta-label-maker{display:inline} 176.cb{display:flex;align-items:center;gap:16px;padding:14px 44px;background:var(--color-accent-50, #fff5e6);border-top:1px solid var(--color-accent-100, #ffe6c2);border-bottom:1px solid var(--color-accent-100, #ffe6c2);color:var(--color-ink-700, #2a2c33);font-size:15px;line-height:1.45}.cb__stars{display:flex;gap:3px;flex:none}.cb__stars b{font-size:18px;line-height:1;color:var(--color-accent-500, #ff8a14)}.cb__stars b.cb__star--off{color:#d9d6cf}.cb__msg{flex:1;min-width:0}.cb__msg strong{color:var(--color-ink-900, #0e0f12);font-weight:600}.cb__go{flex:none;display:inline-flex;align-items:center;gap:8px;background:var(--color-accent-500, #ff8a14);color:#fff;border:0;border-radius:10px;padding:10px 16px;font:inherit;font-weight:600;font-size:14px;cursor:pointer}.cb__go:hover{background:var(--color-accent-600, #ee7100)}.cb__x{flex:none;width:28px;height:28px;border-radius:8px;border:1px solid var(--color-accent-100, #ffe6c2);background:transparent;color:var(--color-ink-500, #4b4e57);font:inherit;font-size:15px;line-height:1;cursor:pointer}.cb__x:focus-visible,.cb__go:focus-visible{outline:2px solid var(--color-accent-700, #c95c00);outline-offset:2px}@media(max-width:720px){.cb{padding-left:20px;padding-right:20px;flex-wrap:wrap}.cb__msg{flex-basis:100%}}@media(prefers-reduced-motion:no-preference){.cb{animation:cbDrop .35s ease-out}@keyframes cbDrop{0%{transform:translateY(-6px);opacity:0}to{transform:none;opacity:1}}} 177</style><link rel="stylesheet" href="/_astro/nocode.F2lD66tx.css"><link rel="stylesheet" href="/_astro/makers.CaNfzecx.css">
177<script type="module" src="/_astro/page.Bm8nvw0p.js"></script>
177</head> <body class="min-h-screen flex flex-col"> <header class="sticky top-0 z-50 bg-canvas/85 backdrop-blur border-b border-ink-100"> <div class="mx-auto max-w-7xl px-6 h-20 flex items-center justify-between"> <a href="/" class="flex items-center gap-2" aria-label="Ritual home"> <img src="/ritual-logo.svg" alt="Ritual" class="h-9 w-auto"> </a> <!-- Desktop nav --> <nav class="hidden md:flex items-center gap-2 text-base text-ink-700"> <a href="/" class="px-4 py-2.5 rounded-md hover:text-ink-900 transition-colors"> Home </a><a href="/problem" class="px-4 py-2.5 rounded-md hover:text-ink-900 transition-colors"> Problem </a><div class="relative group"> <button type="button" class="inline-flex items-center gap-1 px-4 py-2.5 rounded-md hover:text-ink-900 transition-colors" aria-haspopup="true" aria-expanded="false"> Product <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="h-4 w-4 transition-transform group-hover:rotate-180"> <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.06l3.71-3.83a.75.75 0 111.08 1.04l-4.25 4.39a.75.75 0 01-1.08 0L5.21 8.27a.75.75 0 01.02-1.06z" clip-rule="evenodd"></path> </svg> </button> <div class="invisible opacity-0 translate-y-1 group-hover:visible group-hover:opacity-100 group-hover:translate-y-0 group-focus-within:visible group-focus-within:opacity-100 group-focus-within:translate-y-0 transition-all absolute left-0 top-full pt-2 w-72"> <div class="rounded-xl bg-white shadow-lg ring-1 ring-ink-100 p-2"> <a href="/product/builder" class="block px-3 py-2.5 rounded-lg hover:bg-ink-50 transition-colors"> <div class="text-sm font-semibold text-ink-900">Product</div> <div class="text-xs text-ink-500 mt-0.5">Clear and compelling PRDs</div> </a><a href="/product/developer" class="block px-3 py-2.5 rounded-lg hover:bg-ink-50 transition-colors"> <div class="text-sm font-semibold text-ink-900">Developer</div> <div class="text-xs text-ink-500 mt-0.5">Agentic coding without the drift</div> </a><a href="/product/makers" class="block px-3 py-2.5 rounded-lg hover:bg-ink-50 transition-colors"> <div class="text-sm font-semibold text-ink-900">Prototyping</div> <div class="text-xs text-ink-500 mt-0.5">Turn ideas into better builds</div> </a> </div> </div> </div><div class="relative group"> <button type="button" class="inline-flex items-center gap-1 px-4 py-2.5 rounded-md hover:text-ink-900 transition-colors" aria-haspopup="true" aria-expanded="false"> Use Cases <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="h-4 w-4 transition-transform group-hover:rotate-180"> <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.06l3.71-3.83a.75.75 0 111.08 1.04l-4.25 4.39a.75.75 0 01-1.08 0L5.21 8.27a.75.75 0 01.02-1.06z" clip-rule="evenodd"></path> </svg> </button> <div class="invisible opacity-0 translate-y-1 group-hover:visible group-hover:opacity-100 group-hover:translate-y-0 group-focus-within:visible group-focus-within:opacity-100 group-focus-within:translate-y-0 transition-all absolute left-0 top-full pt-2 w-72"> <div class="rounded-xl bg-white shadow-lg ring-1 ring-ink-100 p-2"> <a href="/use-cases/agentic-coding" class="block px-3 py-2.5 rounded-lg hover:bg-ink-50 transition-colors"> <div class="text-sm font-semibold text-ink-900">Agentic Coding</div> <div class="text-xs text-ink-500 mt-0.5">Context & constraints for coding agents</div> </a><a href="/use-cases/requirements" class="block px-3 py-2.5 rounded-lg hover:bg-ink-50 transition-colors"> <div class="text-sm font-semibold text-ink-900">Product Requirements</div> <div class="text-xs text-ink-500 mt-0.5">Build-ready specs without the rework</div> </a><a href="/use-cases/prototyping" class="block px-3 py-2.5 rounded-lg hover:bg-ink-50 transition-colors"> <div class="text-sm font-semibold text-ink-900">Prototyping</div> <div class="text-xs text-ink-500 mt-0.5">Turn ideas into better builds</div> </a><a href="/use-cases/strategy" class="block px-3 py-2.5 rounded-lg hover:bg-ink-50 transition-colors"> <div class="text-sm font-semibold text-ink-900">Strategy & Transformation</div> <div class="text-xs text-ink-500 mt-0.5">Turn initiatives into actionable explorations</div> </a> </div> </div> </div><a href="/about" class="px-4 py-2.5 rounded-md hover:text-ink-900 transition-colors"> About </a><a href="/technology" class="px-4 py-2.5 rounded-md hover:text-ink-900 transition-colors"> Technology </a> </nav> <div class="flex items-center gap-2"> <a href="/product/makers#nc-idea" data-ph-cta="get_started" data-ph-cta-location="header" class="hidden sm:inline-flex items-center rounded-full bg-ink-900 text-white text-base px-5 py-2.5 hover:bg-ink-700 transition-colors"> <span class="cta-label-default">Get started for free</span><span class="cta-label-maker">
177Build Now</span> </a> <!-- Mobile menu toggle --> <button type="button" class="md:hidden inline-flex items-center justify-center h-11 w-11 rounded-md text-ink-700 hover:bg-ink-50 transition-colors" aria-label="Toggle menu" aria-expanded="false" aria-controls="mobile-menu" data-mobile-toggle> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5 [data-open=true]_&]:hidden" aria-hidden="true"> <line x1="3" y1="6" x2="21" y2="6"></line> <line x1="3" y1="12" x2="21" y2="12"></line> <line x1="3" y1="18" x2="21" y2="18"></line> </svg> </button> </div> </div> <!-- Mobile menu panel --> <div id="mobile-menu" class="md:hidden hidden border-t border-ink-100 bg-canvas" data-mobile-panel> <nav class="mx-auto max-w-7xl px-6 py-4 flex flex-col gap-1.5 text-sm"> <a href="/" class="flex items-center min-h-[44px] px-3 py-2.5 rounded-lg text-ink-900 hover:bg-ink-50 transition-colors"> Home </a><a href="/problem" class="flex items-center min-h-[44px] px-3 py-2.5 rounded-lg text-ink-900 hover:bg-ink-50 transition-colors"> Problem </a><div class="py-1"> <div class="px-3 py-2 text-xs font-semibold uppercase tracking-wider text-ink-400"> Product </div> <a href="/product/builder" class="flex flex-col justify-center min-h-[44px] px-3 py-2.5 rounded-lg hover:bg-ink-50 transition-colors"> <div class="text-sm font-semibold text-ink-900">Product</div> <div class="text-xs text-ink-500 mt-0.5">Clear and compelling PRDs</div> </a><a href="/product/developer" class="flex flex-col justify-center min-h-[44px] px-3 py-2.5 rounded-lg hover:bg-ink-50 transition-colors"> <div class="text-sm font-semibold text-ink-900">Developer</div> <div class="text-xs text-ink-500 mt-0.5">Agentic coding without the drift</div> </a><a href="/product/makers" class="flex flex-col justify-center min-h-[44px] px-3 py-2.5 rounded-lg hover:bg-ink-50 transition-colors"> <div class="text-sm font-semibold text-ink-900">Prototyping</div> <div class="text-xs text-ink-500 mt-0.5">Turn ideas into better builds</div> </a> </div><div class="py-1"> <div class="px-3 py-2 text-xs font-semibold uppercase tracking-wider text-ink-400"> Use Cases </div> <a href="/use-cases/agentic-coding" class="flex flex-col justify-center min-h-[44px] px-3 py-2.5 rounded-lg hover:bg-ink-50 transition-colors"> <div class="text-sm font-semibold text-ink-900">Agentic Coding</div> <div class="text-xs text-ink-500 mt-0.5">Context & constraints for coding agents</div> </a><a href="/use-cases/requirements" class="flex flex-col justify-center min-h-[44px] px-3 py-2.5 rounded-lg hover:bg-ink-50 transition-colors"> <div class="text-sm font-semibold text-ink-900">Product Requirements</div> <div class="text-xs text-ink-500 mt-0.5">Build-ready specs without the rework</div> </a><a href="/use-cases/prototyping" class="flex flex-col justify-center min-h-[44px] px-3 py-2.5 rounded-lg hover:bg-ink-50 transition-colors"> <div class="text-sm font-semibold text-ink-900">Prototyping</div> <div class="text-xs text-ink-500 mt-0.5">Turn ideas into better builds</div> </a><a href="/use-cases/strategy" class="flex flex-col justify-center min-h-[44px] px-3 py-2.5 rounded-lg hover:bg-ink-50 transition-colors"> <div class="text-sm font-semibold text-ink-900">Strategy & Transformation</div> <div class="text-xs text-ink-500 mt-0.5">Turn initiatives into actionable explorations</div> </a> </div><a href="/about" class="flex items-center min-h-[44px] px-3 py-2.5 rounded-lg text-ink-900 hover:bg-ink-50 transition-colors"> About </a><a href="/technology" class="flex items-center min-h-[44px] px-3 py-2.5 rounded-lg text-ink-900 hover:bg-ink-50 transition-colors"> Technology </a> <a href="/product/makers#nc-idea" data-ph-cta="get_started" data-ph-cta-location="header_mobile" class="mt-2 inline-flex items-center justify-center min-h-[44px] rounded-full bg-ink-900 text-white text-sm px-4 py-2.5 hover:bg-ink-700 transition-colors"> <span class="cta-label-default">Get started for free</span><span class="cta-label-maker">Build Now</span> </a> </nav> </div> </header>
177<script> 178 (function () { 179 var p = document.documentElement.getAttribute("data-persona"); 180 if (!p) return; 181 if (p === "developer" && location.pathname.indexOf("/product/developer") === 0) return; 182 document 183 .querySelectorAll('a[data-ph-cta-location="header"], a[data-ph-cta-location="header_mobile"]') 184 .forEach(function (a) { 185 if (p === "maker") { 186 a.setAttribute("href", "/product/makers#nc-idea"); 187 a.setAttribute("data-ph-cta", "nav_build_now"); 188 } else { 189 a.setAttribute("href", "/product/developer"); 190 a.setAttribute("data-ph-cta", "nav_dev_hero"); 191 } 192 a.removeAttribute("target"); 193 a.removeAttribute("rel"); 194 }); 195 })(); 196</script>
196
196<script> 197 (function () { 198 function init() { 199 var btn = document.querySelector("[data-mobile-toggle]"); 200 var panel = document.querySelector("[data-mobile-panel]"); 201 if (!btn || !panel) return; 202 btn.addEventListener("click", function () { 203 var open = panel.classList.toggle("hidden") === false; 204 btn.setAttribute("aria-expanded", open ? "true" : "false"); 205 }); 206 } 207 if (document.readyState === "loading") { 208 document.addEventListener("DOMContentLoaded", init); 209 } else { 210 init(); 211 } 212 })(); 213</script>
213 <main class="flex-1">
213<script>(function(){const page = "makers"; 214 215 (function () { 216 try { 217 var params = new URLSearchParams(location.search); 218 var refine = params.get("from") === "refine"; 219 var rated = parseInt(params.get("rated") || "", 10); 220 var stars = rated >= 1 && rated <= 5 ? rated : 0; 221 if (!refine && !stars) return; 222 var idea = (params.get("idea") || "").replace(/[\x00-\x1f\x7f]+/g, " ").replace(/\s+/g, " ").trim().slice(0, 200); 223 224 // Their words into the composer, through the pending-idea record the 225 // fold's builder consumes once at mount (PromptBuilder: takePendingIdea). 226 if (page === "makers" && idea) { 227 try { localStorage.setItem("rtl_nc_pending_v1", JSON.stringify({ idea: idea, at: Date.now(), from: "checkback" })); } catch (_) {} 228 } 229 230 // The address is cleaned now: a refresh is a normal visit, and the 231 // campaign tag stays for analytics. 232 ["from", "rated", "idea"].forEach(function (k) { params.delete(k); }); 233 var q = params.toString(); 234 try { history.replaceState(null, "", location.pathname + (q ? "?" + q : "") + location.hash); } catch (_) {} 235 236 var lead, body, cta; 237 if (page === "next") { 238 lead = stars ? "Thanks, you rated your spec " + stars + " out of 5." : "Welcome back."; 239 body = "Glad it landed. The next question is what it should become."; 240 cta = "Plan what's next"; 241 } else if (stars) { 242 lead = "Thanks, you rated your spec " + stars + " out of 5."; 243 body = "Ritual now settles the decisions that matter before the build. Try it on the same idea."; 244 cta = "Try it on this idea"; 245 } else { 246 lead = "Add a few words and send it again."; 247 body = "Ritual now settles the decisions that matter before the build."; 248 cta = "Try it on this idea"; 249 } 250 251 var strip = document.createElement("div"); 252 strip.className = "cb"; 253 strip.setAttribute("role", "status"); 254 strip.setAttribute("data-checkback", page); 255 if (stars) { 256 var row = document.createElement("div"); 257 row.className = "cb__stars"; 258 row.setAttribute("aria-label", stars + " of 5"); 259 for (var i = 1; i <= 5; i++) { 260 var b = document.createElement("b"); 261 b.textContent = "â "; 262 if (i > stars) b.className = "cb__star--off"; 263 row.appendChild(b); 264 } 265 strip.appendChild(row); 266 } 267 var msg = document.createElement("div"); 268 msg.className = "cb__msg"; 269 var strong = document.createElement("strong"); 270 strong.textContent = lead; 271 msg.appendChild(strong); 272 msg.appendChild(document.createTextNode(" " + body)); 273 strip.appendChild(msg); 274 var go = document.createElement("button"); 275 go.type = "button"; 276 go.className = "cb__go"; 277 go.textContent = cta + " "; 278 var arrow = document.createElement("span"); 279 arrow.setAttribute("aria-hidden", "true"); 280 arrow.textContent = "â"; 281 go.appendChild(arrow); 282 strip.appendChild(go); 283 var x = document.createElement("button"); 284 x.type = "button"; 285 x.className = "cb__x"; 286 x.setAttribute("aria-label", "Dismiss"); 287 x.textContent = "â"; 288 strip.appendChild(x); 289 290 var main = document.querySelector("main"); 291 (main || document.body).insertBefore(strip, (main || document.body).firstChild); 292 293 var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; 294 go.addEventListener("click", function () { 295 try { window.posthog && window.posthog.capture && window.posthog.capture("makers_checkback_cta", { page: page }); } catch (_) {} 296 var target, input; 297 if (page === "next") { 298 target = document.querySelector(".nx__share") || document.getElementById("nx-next-idea"); 299 input = target && target.querySelector("input"); 300 } else { 301 target = document.getElementById("nc-idea"); 302 input = target && target.querySelector(".pb__input"); 303 } 304 if (!target) return; 305 // Focus first, inside the click gesture (or iOS refuses the keyboard), 306 // and stamped as ours so it is not counted as a person clicking in. 307 if (input) { 308 window.__ritualProgrammaticFocusAt = Date.now(); 309 try { input.focus({ preventScroll: true }); } catch (_) {} 310 } 311 target.scrollIntoView({ behavior: reduce ? "auto" : "smooth", block: "center" }); 312 }); 313 x.addEventListener("click", function () { strip.remove(); }); 314
315 try { 316 window.posthog && window.posthog.capture && window.posthog.capture("makers_checkback_landed", { 317 page: page, 318 from: refine ? "refine" : null, 319 rated: stars || null, 320 has_idea: !!idea, 321 }); 322 } catch (_) {} 323 } catch (_) { 324 /* the page is unchanged without the strip */ 325 } 326 })(); 327})();</script>
327 <section class="nch" data-astro-cid-2hrt3eon> <div class="nch__inner" data-astro-cid-2hrt3eon> <p class="nch__eyebrow" data-astro-cid-2hrt3eon>You bring the idea</p> <h1 class="nch__title" data-astro-cid-2hrt3eon> <span id="nch-agent" data-astro-cid-2hrt3eon>The agent</span> built exactly what you asked for.<br data-astro-cid-2hrt3eon>Let's work through the problem. 328</h1> <p class="nch__sub" data-astro-cid-2hrt3eon> 329Your one line leaves a hundred decisions to the agent. We settle the few 330 that matter, so your tool builds the app you meant. 331</p> <div id="nc-idea" class="nch__ask" data-astro-cid-2hrt3eon> <style>astro-island,astro-slot,astro-static-slot{display:contents}</style>
331<script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).load=e;window.dispatchEvent(new Event("astro:load"));})();</script>
331<script>(()=>{var g=Object.defineProperty;var w=(c,s,d)=>s in c?g(c,s,{enumerable:!0,configurable:!0,writable:!0,value:d}):c[s]=d;var l=(c,s,d)=>w(c,typeof s!="symbol"?s+"":s,d);var E=new Set(["__proto__","constructor","prototype"]);{let c={0:t=>y(t),1:t=>d(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(d(t)),5:t=>new Set(d(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t),11:t=>Number.POSITIVE_INFINITY*t},s=t=>{let[p,e]=t;return p in c?c[p](e):void 0},d=t=>t.map(s),y=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([p,e])=>[p,s(e)]));class f extends HTMLElement{constructor(){super(...arguments);l(this,"Component");l(this,"hydrator");l(this,"hydrate",async()=>{var b;if(!this.hydrator||!this.isConnected)return;let e=(b=this.parentElement)==null?void 0:b.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let n=this.querySelectorAll("astro-slot"),r={},i=this.querySelectorAll("template[data-astro-template]");for(let o of i){let a=o.closest(this.tagName);a!=null&&a.isSameNode(this)&&(r[o.getAttribute("data-astro-template")||"default"]=o.innerHTML,o.remove())}for(let o of n){let a=o.closest(this.tagName);a!=null&&a.isSameNode(this)&&(r[o.getAttribute("name")||"default"]=o.innerHTML)}let u;try{u=this.hasAttribute("props")?y(JSON.parse(this.getAttribute("props"))):{}}catch(o){let a=this.getAttribute("component-url")||"<unknown>",v=this.getAttribute("component-export");throw v&&(a+=` (export ${v})`),console.error(`[hydrate] Error parsing props for component ${a}`,this.getAttribute("props"),o),o}let h;await this.hydrator(this)(this.Component,u,r,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});l(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),n.disconnect(),this.childrenConnectedCallback()},n=new MutationObserver(()=>{var r;((r=this.lastChild)==null?void 0:r.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});n.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}getRetryImportUrl(e){let n=new URL(e,document.baseURI),r=`astro-retry=${Date.now()}`,i=n.hash.replace(/^#/,"");return n.hash=i?`${i}&${r}`:r,n.toString()}async importWithRetry(e){try{return await import(e)}catch(n){return await new Promise(r=>setTimeout(r,1e3)),import(this.getRetryImportUrl(e))}}handleHydrationError(e){let n=this.getAttribute("component-url"),r=new CustomEvent("astro:hydration-error",{cancelable:!0,bubbles:!0,composed:!0,detail:{error:e,componentUrl:n}});this.dispatchEvent(r)&&console.error(`[astro-island] Error hydrating ${n}`,e)}async start(){let e=JSON.parse(this.getAttribute("opts")),n=this.getAttribute("client");if(Astro[n]===void 0){window.addEventListener(`astro:${n}`,()=>this.start(),{once:!0});return}try{await Astro[n](async()=>{let r=this.getAttribute("renderer-url");try{let[i,{default:u}]=await Promise.all([this.importWithRetry(this.getAttribute("component-url")),r?this.importWithRetry(r):Promise.resolve({default:()=>()=>{}})]),h=this.getAttribute("component-export")||"default";if(h.includes(".")){this.Component=i;for(let m of h.split(".")){if(E.has(m)||!this.Component||typeof this.Component!="object"&&typeof this.Component!="function"||!Object.hasOwn(this.Component,m))throw new Error(`Invalid component export path: ${h}`);this.Component=this.Component[m]}}else{if(E.has(h))throw new Error(`Invalid component export path: ${h}`);this.Component=i[h]}return this.hydrator=u,this.hydrate}catch(i){return this.handleHydrationError(i),()=>{}}},e,this)}catch(r){this.handleHydrationError(r)}}attributeChangedCallback(){this.hydrate()}}l(f,"observedAttributes",["props"]),customElements.get("astro-island")||customElements.define("astro-island",f)}})();</script>
331<astro-island uid="Z1SrQ9b" prefix="r4" component-url="/_astro/PromptBuilder.28fePJCs.js" component-export="default" renderer-url="/_astro/client.Boy0L0Dy.js" props="{"seed":[0,"build a habit tracker for my team"],"source":[0,"fold"],"data-astro-cid-2hrt3eon":[0,true]}" ssr client="load" opts="{"name":"PromptBuilder","value":true}" await-children><div class="pb"><div></div><form class="pb__ask"><textarea class="pb__input" rows="1" data-own="false" maxLength="5000" placeholder="what do you want to build?"></textarea><button class="pb__go" type="submit" disabled="" title="type your own idea to continue">â</button></form></div><!--astro:end--></astro-island> </div> <p class="nch__tools" data-astro-cid-2hrt3eon> <span class="nch__tools-label" data-astro-cid-2hrt3eon>Works with</span> <span class="nch__tool" data-astro-cid-2hrt3eon>v0</span> · <span class="nch__tool" data-astro-cid-2hrt3eon>Claude</span> · <span class="nch__tool" data-astro-cid-2hrt3eon>Lovable</span> · <span class="nch__tool" data-astro-cid-2hrt3eon>Replit</span> · and more 332</p>
332<script> 333 (function () { 334 if (matchMedia("(prefers-reduced-motion: reduce)").matches) return; 335 var tools = document.querySelectorAll(".nch__tool"); 336 if (!tools.length) return; 337 /* The idea box, excluding the refine step's field, which carries 338 .pb__input too and appears in this section after a run. */ 339 var field = document.querySelector("#nc-idea form.pb__ask:not(.pb__ask--tweak) .pb__input"); 340 /* THEIRS, not merely non-empty: the seeded example types itself into 341 this field on load, and stopping for our own demo text would mean 342 the cycle never ran for anybody. data-own is the island's own 343 answer to that question, the same signal the type-anywhere handler 344 below reads. */ 345 function theirText() { 346 return !!field && field.value.trim() !== "" && field.getAttribute("data-own") === "true"; 347 } 348 var i = -1; 349 var timer = setInterval(function () { 350 if (theirText()) return stop(); 351 if (i >= 0) tools[i].classList.remove("nch__tool--on"); 352 i = (i + 1) % tools.length; 353 tools[i].classList.add("nch__tool--on"); 354 }, 1600); 355 function stop() { 356 clearInterval(timer); 357 // Leave nothing lit: a frozen highlight on one tool reads as that 358 // tool being selected, which is not a claim this line makes. 359 for (var n = 0; n < tools.length; n++) tools[n].classList.remove("nch__tool--on"); 360 } 361 /* The tick alone would take up to 1.6s to notice, which is long 362 enough to blink once more while they type. This catches it at the 363 keystroke instead; the tick stays as the backstop for changes React 364 makes without a DOM input event, such as picking a suggestion. 365 Deferred a frame because data-own is written by React's re-render, 366 which has not happened yet when the event fires. */ 367 if (field) { 368 field.addEventListener("input", function () { 369 requestAnimationFrame(function () { 370 if (theirText()) stop(); 371 }); 372 }); 373 } 374 })(); 375 </script>
375
375<script> 376 (function () { 377 if (new URLSearchParams(location.search).get("h") !== "rotate") return; 378 if (matchMedia("(prefers-reduced-motion: reduce)").matches) return; 379 var el = document.getElementById("nch-agent"); 380 if (!el) return; 381 var names = ["Lovable", "Claude", "v0", "Replit"]; 382 var i = 0; 383 el.classList.add("nch__agent--rotating"); 384 var rot = setInterval(function () { 385 // The post-run swap below rewrites this headline and takes the span 386 // with it. Stop rather than tick forever against a detached node. 387 if (!el.isConnected) return clearInterval(rot); 388 el.classList.add("nch__agent--out"); 389 setTimeout(function () { 390 i = (i + 1) % names.length; 391 el.textContent = names[i]; 392 el.classList.remove("nch__agent--out"); 393 }, 250); 394 }, 2600); 395 el.textContent = names[0]; 396 })(); 397 </script>
397 <!-- 398 The hero opens by accusing the agent of building exactly what you asked 399 for. Once the module below has actually produced the spec, that line is 400 an unanswered accusation sitting over its own answer, so it becomes the 401 next beat: what we did that the agent would not have. 402 403 The subhead goes with it rather than being rewritten. It exists to 404 explain the accusation ("your one line leaves a hundred decisions"), and 405 once the spec is on screen the explanation is redundant with the thing 406 itself. Two sentences saying one idea is the overload the CEO named on 407 2026-08-13, so the fix is deletion, not a second draft. 408 -->
408<script> 409 (function () { 410 var title = document.querySelector(".nch__title"); 411 if (!title) return; 412 var sub = document.querySelector(".nch__sub"); 413 var original = title.innerHTML; 414 var done = false; 415 window.addEventListener("ritual:makers-spec-ready", function () { 416 if (done) return; // a second run must not re-fade a settled hero 417 done = true; 418 var apply = function () { 419 title.textContent = "We settled what your agent would have guessed."; 420 title.classList.remove("nch__fade-out"); 421 // Hidden, not removed: an idle reset below brings it back. The 422 // removed version left "We settledâ¦" in past tense over an empty 423 // input after start-a-new-idea, claiming a run no longer there. 424 if (sub) sub.style.display = "none"; 425 }; 426 if (matchMedia("(prefers-reduced-motion: reduce)").matches) return apply(); 427 title.classList.add("nch__fade-out"); 428 if (sub) sub.classList.add("nch__fade-out"); 429 setTimeout(apply, 220); 430 }); 431 window.addEventListener("ritual:makers-hero-reset", function () { 432 done = false; 433 title.innerHTML = original; 434 title.classList.remove("nch__fade-out"); 435 if (sub) { 436 sub.style.display = ""; 437 sub.classList.remove("nch__fade-out"); 438 } 439 }); 440 })(); 441 </script>
441 </div> </section> <div id="nc-demos" data-astro-cid-2hrt3eon> <astro-island uid="Zzy0uN" prefix="r5" component-url="/_astro/BuildRace.CWyqq53F.js" component-export="default" renderer-url="/_astro/client.Boy0L0Dy.js" props="{"showPrompt":[0,false],"after":[0,"ritual:makers-seed-typed"],"data-astro-cid-2hrt3eon":[0,true]}" ssr client="load" opts="{"name":"BuildRace","value":true}" await-children><div class="br" aria-label="The same idea, sent two ways"><div class="br__stage"><div class="br__side"><p class="br__lab">What the agent gets without Ritual</p><div class="br__scrap"><span class="br__scrap-quote"><span class="br__q" aria-hidden="true">â</span>build a habit tracker for my team<span class="br__q" aria-hidden="true">â</span></span><span class="br__scrap-dim">6 words, 100 open questions</span></div></div><div class="br__side br__side--ritual"><p class="br__lab br__lab--r">What the agent gets with Ritual</p><figure class="br__plan"><figcaption class="br__plan-head"><b>YOUR SPEC · habit tracker</b><span class="br__chip">Ritual Prototype Spec</span></figcaption><div class="br__sec"><div class="br__sec-clip"><div class="br__sec-body"><span class="br__n" aria-hidden="true">01</span><div class="br__sec-text"><b>What weâre building</b><span>A shared page where the team sees the team, quiet by design</span></div></div></div></div><div class="br__sec"><div class="br__sec-clip"><div class="br__sec-body"><span class="br__n" aria-hidden="true">02</span><div class="br__sec-text"><b>Scope & non-goals</b><span>No notifications, badges, or manager view</span></div></div></div></div><div class="br__sec"><div class="br__sec-clip"><div class="br__sec-body"><span class="br__n" aria-hidden="true">03</span><div class="br__sec-text"><b>Screens & flow</b><span>The check-in first, then the grid, then the team view</span><span class="br__track"><i class="br__fill"></i></span></div></div></div></div><div class="br__sec"><div class="br__sec-clip"><div class="br__sec-body"><span class="br__n" aria-hidden="true">08</span><div class="br__sec-text"><b>Done when</b><span>A teammate checks off a habit and the team sees it</span></div></div></div></div></figure></div><div class="br__foot"><div><p class="br__verdict">A guess, delegated.</p><p class="br__tally br__tally--bad">9 corrections later, still not your app</p></div><div><p class="br__verdict br__verdict--good">A decision, delivered.</p><p class="br__tally br__tally--good">First pass. Nothing to correct.</p></div></div></div></div><!--astro:end--></astro-island> <astro-island uid="Z17DG0b" prefix="r6" component-url="/_astro/PromptCompare.3fIkvSfH.js" component-export="default" renderer-url="/_astro/client.Boy0L0Dy.js" props="{"data-astro-cid-2hrt3eon":[0,true]}" ssr client="load" opts="{"name":"PromptCompare","value":true}" await-children><section aria-labelledby="side-by-side-heading" class="mx-auto w-full max-w-[1040px] px-6 py-20"><header class="flex flex-col items-center gap-2 text-center"><h2 id="side-by-side-heading" class="text-[36px] font-semibold leading-tight tracking-tight text-ink-900">Side by side</h2><p class="text-[17px] text-ink-400">Try these ideas, written both ways.</p></header><div role="tablist" aria-label="Example ideas" class="mt-8 flex flex-wrap justify-center gap-2"><button role="tab" type="button" id="tab-pulse" aria-selected="true" aria-controls="panel-pulse" class="rounded-full border border-ink-300 bg-canvas-soft px-[18px] py-[9px] text-[14px] font-semibold text-ink-900">Employee pulse tool</button><button role="tab" type="button" id="tab-feedback" aria-selected="false" aria-controls="panel-feedback" class="rounded-full border border-ink-200 px-[18px] py-[9px] text-[14px] text-ink-400 transition-colors hover:border-ink-300 hover:text-ink-900">Customer feedback</button><button role="tab" type="button" id="tab-pipeline" aria-selected="false" aria-controls="panel-pipeline" class="rounded-full border border-ink-200 px-[18px] py-[9px] text-[14px] text-ink-400 transition-colors hover:border-ink-300 hover:text-ink-900">Sales pipeline</button></div><div role="tabpanel" id="panel-pulse" aria-labelledby="tab-pulse" class="mt-6 grid items-start gap-6 md:grid-cols-[46fr_54fr]"><div class="pt-3 pr-3 md:pt-6">
441<div aria-labelledby="start-pulse-label" class="-rotate-2 rounded-[20px] border border-accent-100 bg-accent-50 px-7 pt-6 pb-8 shadow-[0_1px_2px_rgba(14,15,18,0.04),0_18px_36px_-20px_rgba(238,113,0,0.35)]"><div class="flex items-center justify-between"><span id="start-pulse-label" class="text-[11.5px] font-bold uppercase tracking-[0.12em] text-accent-600">Where everyone starts</span><span class="text-[11px] font-semibold uppercase tracking-[0.08em] text-accent-600/70">1 line</span></div><p class="mt-5 text-[26px] font-medium leading-[1.3] text-ink-900 text-balance"><span class="text-accent-600" aria-hidden="true">â</span>build a weekly pulse survey for our employees<span class="text-accent-600" aria-hidden="true">â</span></p></div></div><article aria-labelledby="spec-pulse-label" class="flex max-h-[700px] flex-col overflow-hidden rounded-[20px] border border-accent-100 bg-canvas shadow-[0_2px_4px_rgba(14,15,18,0.03),0_28px_56px_-28px_rgba(238,113,0,0.45)]"><header class="flex flex-col gap-4 border-b border-ink-100 px-7 pt-6 pb-5"><div class="flex items-center justify-between"><span id="spec-pulse-label" class="text-[11.5px] font-bold uppercase tracking-[0.12em] text-accent-600">What we would send</span><span class="rounded-full border border-ink-100 px-2.5 py-0.5 text-[11px] font-semibold uppercase tracking-[0.08em] text-ink-400">Prototype spec</span></div><div class="flex flex-col gap-1.5"><h3 class="text-[22px] font-semibold leading-tight tracking-tight text-ink-900 text-balance">Employee pulse tool</h3><p class="text-[12.5px] tabular-nums text-ink-300">8<!-- --> sections · <!-- -->1,580<!-- --> words · v1</p></div></header><ol class="spec-scroll min-h-0 flex-1 overflow-y-auto px-7 py-2"><li class="grid grid-cols-[36px_1fr] gap-x-4 border-b border-ink-100 py-3.5 last:border-b-0"><span aria-hidden="true" class="pt-[3px] text-[13px] font-semibold tabular-nums text-accent-600">01</span><div class="flex flex-col gap-3"><button type="button" aria-expanded="true" aria-controls="spec-pulse-section-0" class="flex w-full items-baseline justify-between gap-4 text-left"><span class="text-[15.5px] font-bold leading-snug text-ink-900">What We're Building</span><span aria-hidden="true" class="shrink-0 text-[12px] text-ink-300 transition-transform" style="transform:rotate(45deg)">+</span></button><div id="spec-pulse-section-0" class="flex flex-col gap-3"><div class="flex flex-col gap-1.5"><h4 class="text-[11px] font-bold uppercase tracking-[0.12em] text-ink-300">Persona & operating altitude</h4><p class="text-[13.5px] leading-[1.55] text-ink-700 text-pretty">You are building a usable v1 for employees answering a weekly workplace pulse and admins looking for broader themes.</p></div><p class="text-[11px] font-bold uppercase tracking-[0.12em] text-ink-300/80">What we're making · Target user & job</p></div></div></li><li class="grid grid-cols-[36px_1fr] gap-x-4 border-b border-ink-100 py-3.5 last:border-b-0"><span aria-hidden="true" class="pt-[3px] text-[13px] font-semibold tabular-nums text-accent-600">02</span><div class="flex flex-col gap-3"><button type="button" aria-expanded="true" aria-controls="spec-pulse-section-1" class="flex w-full items-baseline justify-between gap-4 text-left"><span class="text-[15.5px] font-bold leading-snug text-ink-900">Scope & Non-goals</span><span aria-hidden="true" class="shrink-0 text-[12px] text-ink-300 transition-transform" style="transform:rotate(45deg)">+</span></button><div id="spec-pulse-section-1" class="flex flex-col gap-3"><div class="flex flex-col gap-1.5"><h4 class="text-[11px] font-bold uppercase tracking-[0.12em] text-ink-300">In this cut</h4><ul class="flex flex-col gap-1 pl-4"><li class="text-[13.5px] leading-[1.55] text-ink-700 text-pretty">Let employees choose anonymous or identifiable replies before answering, with a plain explanation of who can see each choice.</li><li class="text-[13.5px] leading-[1.55] text-ink-700 text-pretty">Ask three repeatable questions about workplace health, emerging concerns, and needed attention.</li></ul></div><p class="text-[11px] font-bold uppercase tracking-[0.12em] text-ink-300/80">Explicitly not now · Timebox</p></div></div></li><li class="grid grid-cols-[36px_1fr] gap-x-4 border-b border-ink-100 py-3.5 last:border-b-0"><span aria-hidden="true" class="pt-[3px] text-[13px] font-semibold tabular-nums text-ink-300">03</span><div class="flex flex-col gap-3"><button type="button" aria-expanded="false" aria-controls="spec-pulse-section-2" class="flex w-full items-baseline justify-between gap-4 text-left"><span class="text-[15.5px] font-bold leading-snug text-ink-900">Screens & Flow</span><span aria-hidden="true" class="shrink-0 text-[12px] text-ink-300 transition-transform" style="transform:none">+</span></button><p id="spec-pulse-section-2" class="text-[12.5px] leading-relaxed text-ink-400">Screen list · Primary path</p></div></li><li class="grid grid-cols-[36px_1fr] gap-x-4 border-b border-ink-100 py-3.5 last:border-b-0"><span aria-hidden="true" class="pt-[3px] text-[13px] font-semibold tabular-nums text-ink-300">04</span><div class="flex flex-col gap-3"><button type="button" aria-expanded="false" aria-controls="spec-pulse-section-3" class="flex w-full items-baseline justify-between gap-4 text-left"><span class="text-[15.5px] font-bold leading-snug text-ink-900">Interactions & States</span><span aria-hidden="true" class="shrink-0 text-[12px] text-ink-300 transition-transform" style="transform:none">+</span></button><p id="spec-pulse-section-3" class="text-[12.5px] leading-relaxed text-ink-400">What's clickable · States per screen · Transitions & feedback · Edge cases</p></div></li><li class="grid grid-cols-[36px_1fr] gap-x-4 border-b border-ink-100 py-3.5 last:border-b-0"><span aria-hidden="true" class="pt-[3px] text-[13px] font-semibold tabular-nums text-ink-300">05</span><div class="flex flex-col gap-3"><button type="button" aria-expanded="false" aria-controls="spec-pulse-section-4" class="flex w-full items-baseline justify-between gap-4 text-left"><span class="text-[15.5px] font-bold leading-snug text-ink-900">Data & Sample Content</span><span aria-hidden="true" class="shrink-0 text-[12px] text-ink-300 transition-transform" style="transform:none">+</span></button><p id="spec-pulse-section-4" class="text-[12.5px] leading-relaxed text-ink-400">Entities & fields · Sample data · Edge content</p></div></li><li class="grid grid-cols-[36px_1fr] gap-x-4 border-b border-ink-100 py-3.5 last:border-b-0"><span aria-hidden="true" class="pt-[3px] text-[13px] font-semibold tabular-nums text-ink-300">06</span><div class="flex flex-col gap-3"><button type="button" aria-expanded="false" aria-controls="spec-pulse-section-5" class="flex w-full items-baseline justify-between gap-4 text-left"><span class="text-[15.5px] font-bold leading-snug text-ink-900">What's Real vs Placeholder</span><span aria-hidden="true" class="shrink-0 text-[12px] text-ink-300 transition-transform" style="transform:none">+</span></button><p id="spec-pulse-section-5" class="text-[12.5px] leading-relaxed text-ink-400">What's actually wired · What's placeholder · Integrations & persistence</p></div></li><li class="grid grid-cols-[36px_1fr] gap-x-4 border-b border-ink-100 py-3.5 last:border-b-0"><span aria-hidden="true" class="pt-[3px] text-[13px] font-semibold tabular-nums text-ink-300">07</span><div class="flex flex-col gap-3"><button type="button" aria-expanded="false" aria-controls="spec-pulse-section-6" class="flex w-full items-baseline justify-between gap-4 text-left"><span class="text-[15.5px] font-bold leading-snug text-ink-900">Look & Feel</span><span aria-hidden="true" class="shrink-0 text-[12px] text-ink-300 transition-transform" style="transform:none">+</span></button><p id="spec-pulse-section-6" class="text-[12.5px] leading-relaxed text-ink-400">Fidelity level · Visual direction & references · Voice of the copy</p></div></li><li class="grid grid-cols-[36px_1fr] gap-x-4 border-b border-ink-100 py-3.5 last:border-b-0"><span aria-hidden="true" class="pt-[3px] text-[13px] font-semibold tabular-nums text-ink-300">08</span><div class="flex flex-col gap-3"><button type="button" aria-expanded="false" aria-controls="spec-pulse-section-7" class="flex w-full items-baseline justify-between gap-4 text-left"><span class="text-[15.5px] font-bold leading-snug text-ink-900">Done When</span><span aria-hidden="true" class="shrink-0 text-[12px] text-ink-300 transition-transform" style="transform:none">+</span></button><p id="spec-pulse-section-7" class="text-[12.5px] leading-relaxed text-ink-400">Acceptance checks</p></div></li></ol></article></div></section><!--astro:end--></astro-island> </div>
441<script> 442 (function () { 443 var demos = document.getElementById("nc-demos"); 444 var hero = document.querySelector(".nch"); 445 // Resolved on use, not here: this script runs inline ABOVE the closing 446 // section, so at parse time the element does not exist yet and a 447 // captured lookup is null forever. That is how the fold's hide silently 448 // never applied to it while the demos (above the script) folded fine. 449 function closing() { 450 return document.getElementById("nc-idea-closing"); 451 } 452 function engage(e) { 453 if (hero) hero.classList.add("nch--engaged"); 454 var src = e && e.detail && e.detail.source; 455 if (src !== "fold") return; 456 if (demos) demos.style.display = "none"; 457 // The closing "Get started with your own idea" folds too: with a live 458 // run one screen up, a second start-here input directly under it says 459 // "wait" and "start over" at once. 460 var c = closing(); 461 if (c) c.style.display = "none"; 462 } 463 window.addEventListener("ritual:makers-statement", engage); 464 window.addEventListener("ritual:makers-spec-ready", engage); 465 window.addEventListener("ritual:makers-idle", function (e) { 466 // THE HERO'S UNDO SITS ON THE SAME SIDE OF THE SOURCE GATE AS ITS DO. 467 // engage() compacts the hero for EITHER widget's run (above the gate), 468 // so the un-compact and the headline reset must be unscoped the same 469 // way: gated to the fold, a run started in the closing widget 470 // compacted the hero and nothing could ever un-compact it, leaving 471 // the past-tense "We settledâ¦" headline over an empty input. 472 if (hero) hero.classList.remove("nch--engaged"); 473 window.dispatchEvent(new CustomEvent("ritual:makers-hero-reset")); 474 // Scoped like the fold: the closing widget's own idle must not 475 // restore a page state that belongs to the fold widget's run. 476 var src = e && e.detail && e.detail.source; 477 if (src !== "fold") return; 478 if (demos) demos.style.display = ""; 479 var c = closing(); 480 if (c) c.style.display = ""; 481 }); 482 })(); 483 </script>
483 <section id="nc-idea-closing" class="ncc ncc--closing nc-orbit-card" data-astro-cid-2hrt3eon> <div class="ncc__inner" data-astro-cid-2hrt3eon> <h2 data-astro-cid-2hrt3eon>Get started with your own idea</h2> <p data-astro-cid-2hrt3eon>Describe your idea, and <strong class="ncc__oneclick" data-astro-cid-2hrt3eon>one click</strong> takes Ritual's prototype spec into the agent of your choice.</p> </div> <astro-island uid="1V0Sdh" prefix="r7" component-url="/_astro/PromptBuilder.28fePJCs.js" component-export="default" renderer-url="/_astro/client.Boy0L0Dy.js" props="{"source":[0,"closing"],"data-astro-cid-2hrt3eon":[0,true]}" ssr client="load" opts="{"name":"PromptBuilder","value":true}" await-children><div class="pb"><div></div><form class="pb__ask"><textarea class="pb__input" rows="1" data-own="true" maxLength="5000" placeholder="what do you want to build?"></textarea><button class="pb__go" type="submit" disabled="">â</button></form></div><!--astro:end--></astro-island> </section>
483<script> 484 (function () { 485 function ideaInput() { 486 // The idea field became a textarea (free-form input, 2026-08-26). 487 var el = document.querySelector("#nc-idea .pb__input"); 488 return el && !el.disabled ? el : null; 489 } 490 function focusIdeaInput() { 491 var input = ideaInput(); 492 if (!input) return; 493 // Read by PromptBuilder's onFocus: this focus is the page's doing, not 494 // a person's, and must not count as interest. A STAMP, not a flag: the 495 // focus here can land before hydration, so nothing may ever consume it, 496 // and a boolean left set would swallow their next real click. 497 window.__ritualProgrammaticFocusAt = Date.now(); 498 input.focus({ preventScroll: true }); 499 } 500 window.addEventListener("hashchange", function () { 501 if (location.hash === "#nc-idea") focusIdeaInput(); 502 }); 503 document.addEventListener("click", function (e) { 504 // A modified or middle click opens a new tab and leaves THIS one where 505 // it was, so stealing focus into an off-screen input here moved the 506 // caret somewhere the visitor cannot see, silently, in the tab they 507 // just left behind (review finding). The type-anywhere 508 // handler below already checks modifiers; this one now matches it. 509 if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return; 510 var a = e.target && e.target.closest ? e.target.closest('a[href$="#nc-idea"]') : null; 511 if (!a) return; 512 // Synchronously, inside the gesture. Scrolling stays native. 513 focusIdeaInput(); 514 }); 515 if (location.hash === "#nc-idea") focusIdeaInput(); 516 })(); 517 </script>
517
517<script> 518 (function () { 519 var sections = [ 520 document.getElementById("nc-idea"), 521 document.getElementById("nc-idea-closing"), 522 ].filter(Boolean); 523 if (!sections.length) return; 524 // Whichever entry point is actually on screen gets the keystroke, so the 525 // reader who starts typing halfway down lands in the one in front of them 526 // rather than being yanked back to the fold. 527 function visibleInput() { 528 for (var i = 0; i < sections.length; i++) { 529 var box = sections[i].getBoundingClientRect(); 530 if (box.bottom < 80 || box.top > window.innerHeight - 80) continue; 531 // The IDEA field only. The refine step's "what should change?" input 532 // carries .pb__input too, and catching it meant a stray keystroke 533 // landed in the box the visitor was mid-thought in (review finding). The idea form is the one that is not --tweak. 534 var forms = sections[i].querySelectorAll("form.pb__ask"); 535 for (var j = 0; j < forms.length; j++) { 536 if (forms[j].className.indexOf("pb__ask--tweak") !== -1) continue; 537 var el = forms[j].querySelector(".pb__input"); 538 if (el && !el.disabled) return el; 539 } 540 } 541 return null; 542 } 543 /* Anything that does its own thing with a printable key. Buttons are the 544 one that bit: Space activates a focused button, and with only fields 545 exempted, a keyboard user tabbing to the submit arrow and pressing 546 Space had the activation swallowed, focus yanked into the idea box, and 547 a space typed into it (review finding). */ 548 function ownsKeystrokes(t) { 549 if (!t || !t.tagName) return false; 550 if (t.isContentEditable) return true; 551 var tag = t.tagName; 552 if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || tag === "BUTTON" || tag === "A") return true; 553 // Anything explicitly made interactive, including custom widgets. 554 if (t.getAttribute && t.getAttribute("role")) return true; 555 return typeof t.tabIndex === "number" && t.tabIndex >= 0; 556 } 557 window.addEventListener("keydown", function (e) { 558 if (e.metaKey || e.ctrlKey || e.altKey) return; 559 if (e.key.length !== 1) return; 560 if (ownsKeystrokes(e.target)) return; 561 var input = visibleInput(); 562 if (!input) return; 563 if (document.activeElement === input) return; 564 e.preventDefault(); 565 window.__ritualProgrammaticFocusAt = Date.now(); 566 // ...and unlike every other programmatic focus, this one supplies the 567 // character itself, so the island must not select the seed on our 568 // behalf. See the onFocus block in PromptBuilder. 569 window.__ritualSelfWriteAt = Date.now(); 570 input.focus({ preventScroll: true }); 571 // React owns this value, so set it through the native setter and fire 572 // an input event, or the component's state never sees the character. 573 // 574 // Append to THEIR text, replace OURS. Appending unconditionally grafted 575 // the keystroke onto the seeded example ("build a habit tracker for my 576 // teamx"), which then read as the visitor's own idea and submitted 577 // (review finding). data-own is the island telling us which it 578 // is; absent means an input that never had a seed, so append. 579 var own = input.getAttribute("data-own") !== "false"; 580 // FROM THE ELEMENT'S OWN PROTOTYPE. The idea field became a textarea in 581 // this change, and HTMLInputElement's value setter throws "Illegal 582 // invocation" when called on one. The throw landed after 583 // preventDefault, so the keystroke was cancelled and then never 584 // written: type-anywhere died silently, and with it the first letter of 585 // every sentence this handler exists to save. 586 var proto = 587 input.tagName === "TEXTAREA" 588 ? window.HTMLTextAreaElement.prototype 589 : window.HTMLInputElement.prototype; 590 var setter = Object.getOwnPropertyDescriptor(proto, "value").set; 591 setter.call(input, (own ? (input.value || "") : "") + e.key); 592 // Ours no longer. Set here rather than waiting for React to re-render 593 // the attribute from state: that costs a render, and a second keystroke 594 // arriving inside that window read "false" again and replaced what the 595 // first one had just typed ("xyz" came out "yz"). React writes the same 596 // value moments later, so this only closes the gap. 597 input.setAttribute("data-own", "true"); 598 input.dispatchEvent(new Event("input", { bubbles: true })); 599 }); 600 })(); 601 </script>
601 </main> <footer class="bg-canvas border-t border-ink-100"> <div class="mx-auto max-w-7xl px-6 py-10 md:py-14"> <div class="grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-10"> <div class="col-span-2 md:col-span-1"> <a href="/" class="flex items-center gap-2" aria-label="Ritual home"> <img src="/ritual-logo.svg" alt="Ritual" class="h-7 w-auto"> </a> <p class="mt-4 text-sm text-ink-500 max-w-xs"> 602Move from idea to action with AIâpowered collaboration. 603</p> <div class="mt-5 flex items-center gap-3"> <a href="https://www.linkedin.com/company/ritualai" target="_blank" rel="noopener noreferrer" aria-label="Ritual on LinkedIn" class="inline-flex h-9 w-9 items-center justify-center rounded-full ring-1 ring-ink-100 text-ink-500 hover:text-ink-900 hover:ring-ink-300 transition-colors"> <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="h-4 w-4"> <path d="M20.452 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.356V9h3.414v1.561h.046c.477-.9 1.637-1.852 3.37-1.852 3.602 0 4.266 2.37 4.266 5.455v6.288zM5.337 7.433a2.062 2.062 0 01-2.063-2.064 2.063 2.063 0 112.063 2.064zM7.119 20.452H3.553V9h3.566v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"></path> </svg> </a> </div> </div> <div> <h4 class="text-sm font-semibold text-ink-900">Product</h4> <ul class="mt-4 space-y-3"> <li> <a href="/product/builder" class="text-sm text-ink-500 hover:text-ink-900 transition-colors"> Product </a> </li><li> <a href="/product/developer" class="text-sm text-ink-500 hover:text-ink-900 transition-colors"> Developer </a> </li><li> <a href="/product/makers" class="text-sm text-ink-500 hover:text-ink-900 transition-colors"> Prototyping </a> </li> </ul> </div><div> <h4 class="text-sm font-semibold text-ink-900">Resources</h4> <ul class="mt-4 space-y-3"> <li> <a href="/docs/quickstart" class="text-sm text-ink-500 hover:text-ink-900 transition-colors"> Quick start </a> </li><li> <a href="/faq" class="text-sm text-ink-500 hover:text-ink-900 transition-colors"> FAQ </a> </li><li> <a href="/support" class="text-sm text-ink-500 hover:text-ink-900 transition-colors"> Support </a> </li><li> <a href="/security" class="text-sm text-ink-500 hover:text-ink-900 transition-colors"> Security </a> </li> </ul> </div><div> <h4 class="text-sm font-semibold text-ink-900">Company</h4> <ul class="mt-4 space-y-3"> <li> <a href="/about" class="text-sm text-ink-500 hover:text-ink-900 transition-colors"> About </a> </li> </ul> </div> </div> <div class="mt-14 pt-6 border-t border-ink-100 text-center text-xs text-ink-400"> 604© 2025 Ritual. All rights reserved. 605</div> </div> </footer> </body></html>
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.