A **journey** is a person's path through an app's pages to a goal: from where they start to where they are done. Open them at [/page/Journey](/page/Journey).
+## Review a journey with your AI {#review}
+Walk your app the way a person would, one step at a time, with your AI beside you. On any step, say what to change (*make Add to cart green, and say Buy*): your AI changes the page and you see it at once. Step on, and when it reads right say **lock it in**: your AI publishes the page and writes its spec and test lines. Open a journey on [/page/Journey](/page/Journey), start it with **Run now**, and talk to your AI in its chat.
+
+```embedhtml
+<style>
+.jr{--bg:#f6f4ef;--card:#fff;--card2:#eeebe4;--ink:#1d1d1b;--mute:#6b6a66;--line:#dcd8cf;--c:#2a8f86;--c2:#5b6ee0;--ok:#2f8f4e;--hi:#e0a43a;
+ font:14px/1.4 system-ui,-apple-system,'Segoe UI',sans-serif;color:var(--ink);background:var(--bg);border-radius:16px;padding:18px;max-width:980px;margin:18px auto;
+ display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.15fr);gap:16px;box-sizing:border-box}
+@media (prefers-color-scheme:dark){:root:not([data-theme=light]) .jr{--bg:#121417;--card:#1b1f24;--card2:#232830;--ink:#e8e6e1;--mute:#9aa0a6;--line:#2e343c;--c:#4fb3a9;--c2:#8d9bff;--ok:#4caf6a;--hi:#f2b84b}}
+:root[data-theme=dark] .jr{--bg:#121417;--card:#1b1f24;--card2:#232830;--ink:#e8e6e1;--mute:#9aa0a6;--line:#2e343c;--c:#4fb3a9;--c2:#8d9bff;--ok:#4caf6a;--hi:#f2b84b}
+@media (max-width:720px){.jr{grid-template-columns:1fr}}
+.jr *{box-sizing:border-box}
+.pane{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:12px;min-width:0}
+.cap{font:800 11px system-ui;letter-spacing:.06em;text-transform:uppercase;color:var(--mute);margin:0 0 8px;display:flex;gap:8px;align-items:center}
+.cap .tag{margin-left:auto;font-weight:700;letter-spacing:0;text-transform:none;border:1px solid var(--line);border-radius:999px;padding:1px 8px}
+.cap .tag.locked{color:var(--ok);border-color:var(--ok)}
+/* the journey's steps */
+.steps{list-style:none;margin:0 0 12px;padding:0;display:grid;gap:4px}
+.steps li{display:grid;grid-template-columns:22px 1fr 18px;gap:8px;align-items:center;padding:5px 8px;border-radius:8px;color:var(--mute);transition:background .3s,color .3s}
+.steps li b{width:22px;height:22px;border-radius:50%;display:grid;place-items:center;font-size:11px;border:1.5px solid var(--line)}
+.steps li.on{background:color-mix(in srgb,var(--c) 14%,transparent);color:var(--ink)}
+.steps li.on b{border-color:var(--c);color:var(--c)}
+.steps li.done b{background:var(--c);border-color:var(--c);color:var(--card)}
+.steps li .ck{opacity:0;color:var(--ok);font-weight:800;transition:opacity .3s}
+.steps li.locked .ck{opacity:1}
+/* the chat */
+.chat{display:flex;flex-direction:column;gap:6px;min-height:170px;justify-content:flex-end}
+.msg{max-width:88%;padding:7px 10px;border-radius:12px;opacity:0;transform:translateY(6px);transition:opacity .3s,transform .3s}
+.msg.in{opacity:1;transform:none}
+.msg.you{align-self:flex-end;background:color-mix(in srgb,var(--c2) 18%,var(--card));border-bottom-right-radius:4px}
+.msg.ai{align-self:flex-start;background:var(--card2);border-bottom-left-radius:4px}
+.msg .who{display:block;font:700 10px system-ui;color:var(--mute);text-transform:uppercase;letter-spacing:.05em}
+.typing::after{content:"▍";animation:blink .8s steps(1) infinite}@keyframes blink{50%{opacity:0}}
+/* the page at the right: a small shop */
+.browser{border:1px solid var(--line);border-radius:10px;overflow:hidden;background:var(--bg)}
+.bar{display:flex;gap:6px;align-items:center;padding:6px 8px;background:var(--card2);font:12px ui-monospace,Menlo,monospace;color:var(--mute)}
+.bar i{width:8px;height:8px;border-radius:50%;background:var(--line)}
+.bar span{margin-left:6px;background:var(--card);border-radius:6px;padding:1px 8px;flex:1;min-width:0;overflow:hidden;white-space:nowrap}
+.page{position:relative;height:300px;padding:14px;overflow:hidden}
+.scr{position:absolute;inset:14px;opacity:0;transform:translateX(16px);transition:opacity .35s,transform .35s}
+.scr.on{opacity:1;transform:none}
+.scr h3{margin:0 0 10px;font:800 17px system-ui}
+.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}
+.prod{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:8px}
+.prod .img{height:52px;border-radius:6px;background:color-mix(in srgb,var(--c) 25%,var(--card2));margin-bottom:6px}
+.prod .p{color:var(--mute);font-size:12px}
+.btn{display:inline-block;margin-top:6px;padding:4px 10px;border-radius:7px;font:700 12px system-ui;border:1px solid var(--line);background:var(--card2);color:var(--ink);transition:background .4s,color .4s,border-color .4s,transform .4s}
+.btn.buy{background:var(--ok);border-color:var(--ok);color:#fff}
+.flash{animation:flash 1.1s ease-out}@keyframes flash{0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--hi) 80%,transparent)}100%{box-shadow:0 0 0 14px transparent}}
+.row{display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px dashed var(--line)}
+.total{font-weight:800;margin-top:8px;display:flex;justify-content:space-between}
+.field{border:1px solid var(--line);border-radius:7px;padding:6px 8px;margin:6px 0;color:var(--mute);background:var(--card)}
+.big{display:block;text-align:center;margin-top:12px;padding:8px;border-radius:8px;background:var(--c);color:#fff;font-weight:800}
+.thanks{display:grid;place-items:center;height:100%;text-align:center}
+.thanks .k{font-size:38px;color:var(--ok)}
+/* the lock-in: a stamp */
+.stamp{position:absolute;right:16px;bottom:16px;padding:6px 12px;border:2px solid var(--ok);color:var(--ok);border-radius:10px;font:800 13px system-ui;transform:rotate(-6deg) scale(.6);opacity:0;transition:opacity .3s,transform .3s;background:var(--card)}
+.stamp.on{opacity:1;transform:rotate(-6deg) scale(1)}
+.out{margin-top:8px;font-size:12px;color:var(--mute);min-height:2.6em}
+.out b{color:var(--ok)}
+html,body{margin:0;background:transparent}
+.jr{margin:0 auto}
+</style>
+<div class="jr" id="jr">
+ <div class="pane">
+ <p class="cap">Journey · Buy something <span class="tag" id="state">reviewing</span></p>
+ <ol class="steps" id="steps">
+ <li><b>1</b><span>Open the shop</span><i class="ck">✓</i></li>
+ <li><b>2</b><span>Pick a product</span><i class="ck">✓</i></li>
+ <li><b>3</b><span>See the cart</span><i class="ck">✓</i></li>
+ <li><b>4</b><span>Check out</span><i class="ck">✓</i></li>
+ <li><b>5</b><span>Thank you</span><i class="ck">✓</i></li>
+ </ol>
+ <p class="cap">You and your AI</p>
+ <div class="chat" id="chat"></div>
+ </div>
+ <div class="pane">
+ <p class="cap">Your app, live</p>
+ <div class="browser">
+ <div class="bar"><i></i><i></i><i></i><span id="url">myshop/app/Shop</span></div>
+ <div class="page">
+ <div class="scr" data-s="1"><h3>My shop</h3><div class="grid"><div class="prod"><div class="img"></div>Mug<div class="p">$12</div></div><div class="prod"><div class="img"></div>Tote<div class="p">$18</div></div><div class="prod"><div class="img"></div>Cap<div class="p">$15</div></div></div></div>
+ <div class="scr" data-s="2"><h3>Mug</h3><div class="prod" style="max-width:220px"><div class="img" style="height:90px"></div>Stoneware mug, 350 ml<div class="p">$12 · 14 in stock</div><span class="btn" id="cta">Add to cart</span></div></div>
+ <div class="scr" data-s="3"><h3>Your cart</h3><div class="row"><span>Mug × 1</span><span>$12</span></div><div class="row"><span>Shipping</span><span>$4</span></div><div class="total"><span>Total</span><span>$16</span></div></div>
+ <div class="scr" data-s="4"><h3>Check out</h3><div class="field">Name</div><div class="field">Email</div><div class="field">Address</div><span class="big">Pay $16</span></div>
+ <div class="scr" data-s="5"><div class="thanks"><div><div class="k">✓</div><h3>Thank you!</h3><div class="p">Your order is on its way.</div></div></div></div>
+ <div class="stamp" id="stamp">Locked in ✓</div>
+ </div>
+ </div>
+ <p class="out" id="out"></p>
+ </div>
+</div>
+<script>
+(()=>{
+/* theme: an app frame gets d2's theme by message; put it on <html> so the data-theme rules apply */
+addEventListener("message",e=>{const t=e.data&&e.data.d2theme&&e.data.d2theme.theme;if(t)document.documentElement.setAttribute("data-theme",t)});
+const $=s=>document.querySelector(s),L=[...document.querySelectorAll("#steps li")],S=[...document.querySelectorAll(".scr")];
+const urls={1:"myshop/app/Shop",2:"myshop/app/Shop/mug",3:"myshop/app/Cart",4:"myshop/app/Checkout",5:"myshop/app/Thanks"};
+const wait=ms=>new Promise(r=>setTimeout(r,ms));
+const step=n=>{L.forEach((l,i)=>{l.classList.toggle("on",i===n-1);l.classList.toggle("done",i<n-1)});S.forEach(s=>s.classList.toggle("on",+s.dataset.s===n));$("#url").textContent=urls[n]};
+const say=async(who,text,type)=>{const c=$("#chat"),m=document.createElement("div");m.className="msg "+who;m.innerHTML='<span class="who">'+(who==="you"?"You":"Your AI")+'</span><span class="t"></span>';c.append(m);while(c.children.length>4)c.firstChild.remove();
+ await wait(30);m.classList.add("in");const t=m.querySelector(".t");
+ if(type){t.classList.add("typing");for(const ch of text){t.textContent+=ch;await wait(38)}t.classList.remove("typing")}else t.textContent=text};
+const reset=()=>{$("#chat").innerHTML="";$("#out").innerHTML="";$("#stamp").classList.remove("on");$("#state").textContent="reviewing";$("#state").classList.remove("locked");L.forEach(l=>l.classList.remove("locked","done","on"));const b=$("#cta");b.className="btn";b.textContent="Add to cart"};
+const reduce=matchMedia("(prefers-reduced-motion: reduce)").matches;
+async function run(){
+ for(;;){
+ reset();step(1);await wait(900);
+ await say("ai","Let's walk Buy something. Step 1: the shop.");await wait(1400);
+ await say("you","next",true);step(2);await wait(1200);
+ await say("you","make Add to cart green, and say Buy",true);await wait(500);
+ await say("ai","Done: SpecUi updated.");const b=$("#cta");b.classList.add("buy","flash");b.textContent="Buy";await wait(1900);
+ await say("you","next",true);step(3);await wait(1400);
+ await say("you","next",true);step(4);await wait(1300);
+ await say("you","next",true);step(5);await wait(1300);
+ await say("you","lock it in",true);await wait(500);
+ L.forEach(l=>{l.classList.remove("on");l.classList.add("done","locked")});$("#stamp").classList.add("on");$("#state").textContent="locked in";$("#state").classList.add("locked");
+ await say("ai","Locked in: the page is published, its spec and test lines are written.");
+ $("#out").innerHTML="<b>Published</b> App:Shop · <b>SpecUi</b> 1 line changed · <b>SpecTest</b> 1 test added";
+ await wait(4200);
+ }
+}
+if(reduce){step(5);L.forEach(l=>l.classList.add("done","locked"));$("#cta").className="btn buy";$("#cta").textContent="Buy";$("#stamp").classList.add("on");$("#state").textContent="locked in";$("#state").classList.add("locked")}
+else run();
+})();
+</script>
+```
+
## Find a journey {#find}
One search box finds **specified journeys**, **features** and **pages** (d2's built-in pages and its `Page:` and `App:` pages). Narrow it with *All · Journeys · Features · Pages*.
## Specified or reverse-engineered {#kinds}
-- **Specified** (green): written in SpecUi, in a `journey` fence. Each step names its page, the link that leads there and the behaviours it uses.
+- **Specified** (green): written as a `Story:` topic tagged `journey` (once SpecUi `journey` fences). Each step names its page, the link that leads there and the behaviours it uses.
- **Reverse-engineered** (amber): pick any page and d2 follows its links, two pages deep. It stops where a page moves on by script, and says so.
- A hop of a specified journey whose page has no link to the next one is marked **⚠ no link from the step before** (drift).