PageSourceSearch

https://vo.dev/product/makers/

html vo.dev collected 2026-10-04 13:33:20 UTC 65,851 bytes, 605 lines download raw bytes

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 &amp; 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 &amp; 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 &amp; 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 &amp; 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="{&quot;seed&quot;:[0,&quot;build a habit tracker for my team&quot;],&quot;source&quot;:[0,&quot;fold&quot;],&quot;data-astro-cid-2hrt3eon&quot;:[0,true]}" ssr client="load" opts="{&quot;name&quot;:&quot;PromptBuilder&quot;,&quot;value&quot;: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="{&quot;showPrompt&quot;:[0,false],&quot;after&quot;:[0,&quot;ritual:makers-seed-typed&quot;],&quot;data-astro-cid-2hrt3eon&quot;:[0,true]}" ssr client="load" opts="{&quot;name&quot;:&quot;BuildRace&quot;,&quot;value&quot;: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 &amp; 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 &amp; 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="{&quot;data-astro-cid-2hrt3eon&quot;:[0,true]}" ssr client="load" opts="{&quot;name&quot;:&quot;PromptCompare&quot;,&quot;value&quot;: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&#x27;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 &amp; 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&#x27;re making · Target user &amp; 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 &amp; 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 &amp; 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 &amp; 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&#x27;s clickable · States per screen · Transitions &amp; 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 &amp; 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 &amp; 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&#x27;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&#x27;s actually wired · What&#x27;s placeholder · Integrations &amp; 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 &amp; 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 &amp; 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="{&quot;source&quot;:[0,&quot;closing&quot;],&quot;data-astro-cid-2hrt3eon&quot;:[0,true]}" ssr client="load" opts="{&quot;name&quot;:&quot;PromptBuilder&quot;,&quot;value&quot;: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.