---
description: Journeys, a person's path through an app's pages, specified in SpecUi (journey fences) or reverse-engineered from the pages' links; served as /page/Journey (SpecUi 2.36, Design 69; built by designer-114 from Page:Mockup-flows, razie 2026-10-12)
tags: page, journeys
---
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"><title>Journeys</title>
<style>
:root{--paper:#fbfaf7;--card:#fff;--ink:#1d1d1b;--soft:#6b6a66;--line:#e4e1da;--accent:#9a4b12;--accent-bg:#f5e8dc;--sun:#d99a2b;--sun-bg:#fbf1dc;--leaf:#3f7d4e;--leaf-bg:#e6f1e8;--rose:#b23a48;--rose-bg:#f8e6e6;--violet:#7b3fb8;--violet-bg:#f2eafb;--teal:#1f7a8c;--teal-bg:#e3f2f5;--slate-bg:#f3f1ec}
@media (prefers-color-scheme:dark){:root{--paper:#141413;--card:#1d1d1b;--ink:#ecebe6;--soft:#9c9a93;--line:#33322f;--accent:#e0995c;--accent-bg:#3a2a1c;--sun:#e6b456;--sun-bg:#3a321c;--leaf:#7cc28d;--leaf-bg:#1d2f22;--rose:#e57d89;--rose-bg:#3a1f23;--violet:#b48be6;--violet-bg:#2b2236;--teal:#6cc3d4;--teal-bg:#1a2e33;--slate-bg:#252422}}
*{box-sizing:border-box}
body{margin:0;background:var(--paper);color:var(--ink);font:15px/1.45 system-ui,-apple-system,"Segoe UI",sans-serif}
a{color:var(--accent)}
header.top{padding:12px 18px 0;border-bottom:1px solid var(--line);background:var(--card)}
header.top h1{margin:0;font-size:20px;font-family:Manrope,system-ui,sans-serif}
header.top p{margin:2px 0 8px;color:var(--soft);font-size:13px}
.mock{display:inline-block;margin-left:8px;font-size:11px;padding:1px 8px;border-radius:999px;background:var(--sun);color:#fff;vertical-align:middle;font-family:system-ui}
.tabs{display:flex;gap:4px;flex-wrap:wrap}
.tabs button{border:1px solid var(--line);border-bottom:none;background:var(--slate-bg);color:var(--ink);padding:7px 14px;border-radius:8px 8px 0 0;font:inherit;font-size:14px;cursor:pointer}
.tabs button[aria-selected=true]{background:var(--paper);font-weight:600;border-color:var(--accent)}
section.view{display:none;padding:14px 18px 40px}
section.view.q{max-width:900px}
#fflow,#pgshot{max-height:calc(100vh - 230px)}#pgshot iframe{height:calc(100vh - 240px)}
section.view.on{display:block}
.crumb{font-size:13px;color:var(--soft);margin:0 0 10px}
.crumb a{text-decoration:none}
.bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:0 0 12px}
.bar h2{margin:0 12px 0 0;font-size:18px}
.chip{display:inline-flex;align-items:center;gap:4px;font-size:12px;padding:2px 9px;border-radius:999px;border:1px solid var(--line);background:var(--card);color:var(--ink);cursor:pointer;user-select:none}
.chip[aria-pressed=true]{border-color:currentColor}
.chip.f{color:var(--teal)}.chip.c{color:var(--violet)}.chip.t{color:var(--sun)}.chip.k{color:var(--rose)}
.legend{font-size:12px;color:var(--soft)}
/* the flow */
.flow{display:flex;flex-direction:column;align-items:center;gap:0;padding:6px 0 4px}
.arrow{width:2px;height:22px;background:var(--line);position:relative;margin:0 auto}
.arrow::after{content:"";position:absolute;bottom:-2px;left:-4px;border:5px solid transparent;border-top-color:var(--line)}
.arrow .lbl{position:absolute;left:10px;top:2px;font-size:11px;color:var(--soft);white-space:nowrap}
.row{display:flex;gap:14px;justify-content:center;flex-wrap:wrap;align-items:flex-start;width:100%}
.branch{display:flex;flex-direction:column;align-items:center;gap:0;min-width:0}
.branch .tag{font-size:11px;color:var(--soft);margin-bottom:4px}
.node{display:block;position:relative;background:var(--card);border:1px solid var(--line);border-radius:10px;padding:8px 12px;width:280px;max-width:86vw;cursor:pointer;text-align:left;font:inherit;color:inherit;box-shadow:0 1px 0 rgba(0,0,0,.03)}
.node:hover{border-color:var(--accent)}
.node.sel{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-bg)}
.node.start{border-top:3px solid var(--leaf)}.node.end{border-bottom:3px solid var(--leaf)}
.node.side{width:200px;background:var(--slate-bg)}
.node.gated{border-style:dashed}
.node .n{font-size:11px;color:var(--soft)}
.node b{display:block;font-size:14px}
.node code{font-size:11px;color:var(--soft)}
.node .counts{display:flex;gap:6px;margin-top:5px;flex-wrap:wrap}
.cnt{font-size:11px;padding:0 6px;border-radius:999px}
.cnt.f{background:var(--teal-bg);color:var(--teal)}.cnt.c{background:var(--violet-bg);color:var(--violet)}.cnt.t{background:var(--sun-bg);color:var(--sun)}.cnt.k{background:var(--rose-bg);color:var(--rose)}
.layer{display:none;margin-top:6px;border-top:1px dashed var(--line);padding-top:5px;font-size:12px}
.flow.show-f .layer.f,.flow.show-c .layer.c,.flow.show-t .layer.t,.flow.show-k .layer.k{display:block}
.layer span{display:inline-block;margin:1px 4px 1px 0;padding:0 6px;border-radius:6px}
.layer.f span{background:var(--teal-bg)}.layer.c span{background:var(--violet-bg)}.layer.t span{background:var(--sun-bg)}.layer.k span{background:var(--rose-bg)}
/* details below */
.below{margin-top:18px;border-top:1px solid var(--line);padding-top:14px}
.below h3{margin:0 0 2px;font-size:17px}
.below .sub{color:var(--soft);font-size:13px;margin:0 0 10px}
.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:10px}
.card{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:10px 12px;font-size:13px}
.card h4{margin:0 0 6px;font-size:13px;display:flex;gap:6px;align-items:center}
.card h4 .dot{width:8px;height:8px;border-radius:50%}
.card ul{margin:0;padding-left:16px}.card li{margin:2px 0}
.card .st{font-size:11px;color:var(--soft)}
.card.f .dot{background:var(--teal)}.card.c .dot{background:var(--violet)}.card.t .dot{background:var(--sun)}.card.k .dot{background:var(--rose)}.card.p .dot{background:var(--accent)}
.note{background:var(--sun-bg);border:1px solid var(--line);border-radius:10px;padding:10px 12px;font-size:13px;margin:14px 0}
.note b{color:var(--ink)}
/* entry: focus */
.focus{display:grid}
.focus{grid-template-columns:minmax(0,1.3fr) 28px minmax(0,1fr);gap:0}
.focus[data-show=left]{grid-template-columns:minmax(0,1fr) 28px}
.focus[data-show=right]{grid-template-columns:28px minmax(0,1fr)}
.focus[data-show=left]>.pane.notes,.focus[data-show=right]>.pane:first-child{display:none}
.fold{display:flex;flex-direction:column;align-items:center;gap:6px;padding-top:44px;width:28px}
.fold button{width:26px;height:44px;padding:0;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--soft);cursor:pointer;font-size:13px}
.fold button:hover{color:var(--accent);border-color:var(--accent)}
.focus[data-show=left] .fold [data-fold=r],.focus[data-show=right] .fold [data-fold=l],.fold [data-pick]{display:none}
@media (max-width:820px){.focus,.focus[data-show]{grid-template-columns:1fr}
.fold{flex-direction:row;padding:0;order:-1;gap:0;border:1px solid var(--line);border-radius:999px;overflow:hidden;justify-self:start}
.fold [data-fold]{display:none!important}.fold [data-pick]{display:block;width:auto;height:auto;border:0;border-radius:0;padding:6px 14px;font-size:14px}
.fold [data-pick][aria-pressed=true]{background:var(--accent-bg);color:var(--accent)}}
.pane{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:10px 12px;min-width:0}
.pane h3{margin:0 0 8px;font-size:15px;display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap}
.seg{display:inline-flex;border:1px solid var(--line);border-radius:999px;overflow:hidden}
.seg button{border:none;background:transparent;color:var(--ink);padding:4px 12px;font:inherit;font-size:13px;cursor:pointer}
.seg button[aria-pressed=true]{background:var(--accent);color:#fff}
.brain{position:relative;height:260px;border-radius:10px;background:var(--slate-bg);overflow:hidden}
.brain .bn{position:absolute;font-size:12px;padding:3px 9px;border-radius:999px;background:var(--card);border:1px solid var(--line)}
.brain svg{position:absolute;inset:0;width:100%;height:100%}
.brain svg line{stroke:var(--line);stroke-width:1.5}
.mini .node{width:220px;padding:6px 10px}.mini .node b{font-size:13px}.mini .arrow{height:14px}
.notes .th{border:1px solid var(--line);border-radius:8px;padding:7px 10px;margin:0 0 7px;font-size:13px;background:var(--paper)}
.notes textarea{width:100%;min-height:60px;border:1px solid var(--line);border-radius:8px;padding:7px;font:inherit;font-size:13px;background:var(--paper);color:var(--ink)}
/* entry: feature */
.feat h3{margin:12px 0 4px;font-size:16px}
.feat table{border-collapse:collapse;font-size:13px;width:100%}
.feat td{border-top:1px solid var(--line);padding:5px 6px;vertical-align:top}
.strip{display:flex;flex-wrap:wrap;gap:4px;align-items:center;font-size:12px;margin:6px 0}
.strip span{padding:2px 8px;border-radius:999px;border:1px solid var(--line);background:var(--card)}
.strip span.hit{border-color:var(--teal);background:var(--teal-bg)}
.strip i{color:var(--soft);font-style:normal}
.flowcard{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:10px 12px;margin:8px 0}
.flowcard .go{float:right;font-size:13px}
.q ol{margin:6px 0 0;padding-left:20px}.q li{margin:4px 0}
.node code{word-break:break-all}
#fflow{max-height:560px;overflow:auto}
.pghead{display:flex;flex-wrap:wrap;gap:6px;align-items:center;font-size:13px;margin:0 0 6px}.pghead code{font-size:11px;color:var(--soft)}
#pgshot{max-height:560px;overflow:auto;border:1px solid var(--line);border-radius:8px;background:var(--paper)}#pgshot img{display:block;width:100%;height:auto}.noshot{padding:30px 16px;color:var(--soft);font-size:14px;text-align:center}
#pgshot iframe{display:block;width:100%;height:640px;border:0;background:var(--paper)}
html.live #pgshot{max-height:none}
html.live .shotnote{display:none}html:not(.live) .livenote{display:none}
.fold [data-fold]{width:28px;height:28px;border:0;background:none;padding:0;filter:drop-shadow(0 2px 1.5px rgba(0,0,0,.35));transition:transform .12s}
.fold [data-fold]:hover{transform:scale(1.15);filter:drop-shadow(0 3px 2px rgba(0,0,0,.4))}
.fold [data-fold]:active{transform:scale(.95);filter:drop-shadow(0 1px 1px rgba(0,0,0,.3))}
:root{--fold-hi:#e6b37f;--fold-lo:#9a4b12;--fold-edge:#6e350c;--bn-f:#3f8f8a;--bn-c:#7a5cc9;--bn-p:#3b3fb6;--bn-k:#c2410c}
@media (prefers-color-scheme:dark){:root{--fold-hi:#f2c79a;--fold-lo:#c0712f;--fold-edge:#8a4a17;--bn-f:#5cc0b8;--bn-c:#a58bf0;--bn-p:#8f93f0;--bn-k:#f08a5d}}
.stepbrain{position:relative;display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);gap:0 34px;align-items:center;width:100%;padding:10px 0}
.bwires{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible}
.bwires path{fill:none;stroke-width:1.6;opacity:.6}.bwires .f{stroke:var(--bn-f)}.bwires .c{stroke:var(--bn-c)}.bwires .k{stroke:var(--bn-k)}.bwires .p{stroke:var(--bn-p)}.bwires .dim{opacity:.3}
.bcol{display:flex;flex-direction:column;gap:9px;min-width:0;position:relative}
.bcol.l{align-items:flex-end;text-align:right}.bcol.r{align-items:flex-start}
.bn{display:inline-flex;align-items:center;gap:7px;max-width:100%;color:var(--ink);text-decoration:none;font-size:12px;line-height:1.25}
.bcol.l .bn{flex-direction:row-reverse}
.bn span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bn:hover span{text-decoration:underline}
.bn.dim{opacity:.5}
.bs{flex:none;display:inline-block;width:14px;height:14px;border:2px solid;box-sizing:border-box}
.bs.f{border-radius:50%;border-color:var(--bn-f);background:color-mix(in srgb,var(--bn-f) 22%,var(--card))}
.bs.c{border-radius:50%;border-color:var(--bn-c);background:color-mix(in srgb,var(--bn-c) 22%,var(--card))}
.bs.p{border-radius:4px;border-color:var(--bn-p);background:var(--card)}
.bs.k{width:12px;height:12px;margin:1px;transform:rotate(45deg);border-radius:2px;border-color:var(--bn-k);background:color-mix(in srgb,var(--bn-k) 18%,var(--card))}
.bnone{font-size:12px;color:var(--soft)}
.bnlegend{display:flex;gap:12px;flex-wrap:wrap;font-size:12px;color:var(--soft);margin:6px 2px 0}.bnlegend span{display:inline-flex;align-items:center;gap:5px}
@media (max-width:820px){.stepbrain{grid-template-columns:1fr 1fr;gap:10px 14px}.bcard{grid-column:1/-1;grid-row:1;justify-self:center}.bwires{display:none}.bcol.l{align-items:flex-start;text-align:left}.bcol.l .bn{flex-direction:row}}
.bn.on span{font-weight:700;text-decoration:underline}.bn.on .bs{box-shadow:0 0 0 3px color-mix(in srgb,currentColor 18%,transparent)}
.mopen{--mk:var(--bn-k);border:1px solid var(--mk);border-radius:10px;padding:10px 12px;background:color-mix(in srgb,var(--mk) 5%,var(--card))}
.mopen.f{--mk:var(--bn-f)}.mopen.c{--mk:var(--bn-c)}
.mo-kind{font:600 11px/1.3 ui-monospace,monospace;letter-spacing:.06em;color:var(--mk)}
.mo-sub{margin:8px 0 4px;font-size:12px;color:var(--soft)}
.mo-list{margin:0;padding-left:18px;font-size:14px}.mo-list li{margin:3px 0}.mo-list .st{font-size:12px;color:var(--soft)}
.mo-title{margin:6px 0 6px;font-size:16px}
.mo-pills{display:flex;gap:5px;flex-wrap:wrap}.mo-pill{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--line);color:var(--soft);background:var(--card)}
.mo-pill.f{border-color:var(--bn-f);color:var(--bn-f)}.mo-pill[class*="s-new"],.mo-pill[class*="s-in-progress"]{border-color:var(--leaf);color:var(--leaf)}.mo-pill[class*="s-waiting"]{border-color:var(--sun);color:var(--sun)}.mo-pill[class*="s-parked"],.mo-pill[class*="s-paused"]{border-style:dashed}
.mo-sum{font-size:14px;line-height:1.5;margin:10px 0}
.mo-meta{display:grid;grid-template-columns:auto 1fr;gap:3px 12px;font-size:13px;margin:0 0 6px}.mo-meta dt{color:var(--soft)}.mo-meta dd{margin:0}
.mo-acts{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:10px}
.mo-btn{font-size:13px;padding:5px 12px;border:1px solid var(--mk);border-radius:8px;text-decoration:none;color:var(--mk);background:var(--card)}
.mo-content{--mk:var(--bn-f);margin-top:10px;border:1px solid var(--line);border-left:3px solid var(--mk);border-radius:10px;padding:10px 12px;background:var(--card);max-height:calc(100vh - 420px);min-height:120px;overflow:auto}
.mo-content.c{--mk:var(--bn-c)}.mo-content .mo-kind{position:sticky;top:-10px;background:var(--card);padding:2px 0 6px}
.mo-content .st{font-weight:400;letter-spacing:0;color:var(--soft)}
.mo-beh{margin:0;padding-left:22px;font-size:14px;line-height:1.5}.mo-beh li{margin:0 0 7px}
.mo-tip{font-size:14px;margin:0 0 8px}.mo-none{font-size:14px;color:var(--soft);margin:4px 0}
.mo-fields{border-collapse:collapse;font-size:13px;width:100%}.mo-fields td{padding:3px 8px;border-top:1px solid var(--line)}.mo-fields td:first-child{font-weight:600;width:40%}.mo-fields td:last-child{font-family:ui-monospace,monospace;color:var(--soft)}
.jfind{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin:10px 0}
#jq{flex:1;min-width:220px;font:inherit;font-size:16px;padding:9px 12px;border:1px solid var(--line);border-radius:10px;background:var(--card);color:var(--ink)}
#jq:focus{outline:2px solid var(--accent-bg);border-color:var(--accent)}
.jres h4{margin:14px 0 6px;font-size:13px;color:var(--soft);text-transform:uppercase;letter-spacing:.05em}
.jrow{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:8px 12px;border:1px solid var(--line);border-radius:10px;background:var(--card);margin:0 0 6px;color:var(--ink);text-decoration:none}
.jrow:hover{border-color:var(--accent)}.jrow.soon{opacity:.55;cursor:default}.jrow code{font-size:12px;color:var(--soft)}.jrow .st{margin-left:auto;font-size:12px;color:var(--soft)}
.jt{font-size:11px;padding:1px 7px;border-radius:999px;border:1px solid var(--line);color:var(--soft)}
.bs.j{width:16px;height:10px;border-radius:5px;border-color:var(--accent);background:var(--accent-bg)}
.jbanner{font-size:13px;padding:8px 10px;margin:0 0 8px;border-radius:8px;background:var(--slate-bg);line-height:1.5}
.jbanner .st{color:var(--soft)}
.flowcard.nojourney{border-style:dashed}
.jhead{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:2px 0 8px;padding:8px 12px;border:1px solid var(--line);border-radius:10px;background:var(--card)}
.jhead b{font-size:15px}.jhead .st{font-size:12px;color:var(--soft)}.jhead code{font-size:12px;color:var(--soft);margin-left:auto}.jhead a{font-size:13px}
.jkind{font-size:12px;font-weight:700;letter-spacing:.03em;padding:3px 10px;border-radius:999px;border:1.5px solid}
.jkind.spec{color:var(--leaf);border-color:var(--leaf);background:var(--leaf-bg)}
.jkind.rev{color:var(--sun);border-color:var(--sun);background:var(--sun-bg);border-style:dashed}
/* Page:Journey: drift marks, the status line */
.node.drift{border-color:var(--sun);border-style:dashed}
.driftnote{display:block;font-size:11px;color:var(--sun);margin-top:2px}
.drifts{color:var(--sun);font-weight:600}
#jstatus{margin:6px 18px 0;font-size:12px;color:var(--soft)}
.feat .mo-beh{max-width:900px}
</style></head><body>
<header class="top">
<h1>Journeys</h1>
<p>A journey is a person's path through an app's pages to a goal. <b>Specified</b> ones are written in SpecUi; any page can show the journey <b>reverse-engineered</b> from its links. <a href="/topics/SpecUi#ui-journeys">How journeys work</a></p>
</header>
<p class="legend" id="jstatus">Loading the journeys, features and pages…</p>
<section class="view" id="v-find">
<div class="jfind"><input id="jq" type="search" placeholder="Find a journey, a feature or a page…" autocomplete="off"><span class="seg"><button data-jk="all" aria-pressed="true">All</button><button data-jk="j" aria-pressed="false">Journeys</button><button data-jk="f" aria-pressed="false">Features</button><button data-jk="p" aria-pressed="false">Pages</button></span></div>
<div id="jres" class="jres"></div>
</section>
<section class="view" id="v-journey">
<div id="jhead" class="jhead"></div>
<div class="focus" data-show="both">
<div class="pane">
<h3><span id="ftitle"></span></h3>
<div id="jbanner" class="jbanner" hidden></div>
<div class="flow show-t show-k" id="miniflow"></div>
<div class="bnlegend"><span><i class="bs f"></i>features</span><span><i class="bs c"></i>classes</span><span><i class="bs k"></i>tickets</span><span><i class="bs p"></i>topics</span></div>
</div>
<div class="fold" role="group" aria-label="Show panes">
<button data-fold="l" title="Fold the left pane" aria-label="Fold the left pane"><svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true"><defs><linearGradient id="g3l" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="var(--fold-hi)"/><stop offset="1" stop-color="var(--fold-lo)"/></linearGradient></defs><path d="M17 3 L5 12 L17 21 Z" fill="url(#g3l)" stroke="var(--fold-edge)" stroke-width=".8" stroke-linejoin="round"/><path d="M16 5 L7 12" fill="none" stroke="#fff" stroke-opacity=".55" stroke-width="1.2" stroke-linecap="round"/></svg></button>
<button data-fold="r" title="Fold the right pane" aria-label="Fold the right pane"><svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true"><defs><linearGradient id="g3r" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="var(--fold-hi)"/><stop offset="1" stop-color="var(--fold-lo)"/></linearGradient></defs><path d="M7 3 L19 12 L7 21 Z" fill="url(#g3r)" stroke="var(--fold-edge)" stroke-width=".8" stroke-linejoin="round"/><path d="M8 5 L17 12" fill="none" stroke="#fff" stroke-opacity=".55" stroke-width="1.2" stroke-linecap="round"/></svg></button>
<button data-pick="left">Journey</button><button data-pick="right">Page · Item</button>
</div>
<div class="pane notes">
<h3><span class="seg"><button data-rp="page" aria-pressed="true">Page</button><button data-rp="item" aria-pressed="false" hidden>Item</button></span><span class="legend" id="rplbl">tap a step to see its page here</span></h3>
<div id="rp-item" hidden></div>
<div id="rp-page">
<div class="pghead" hidden><b id="pgtitle"></b> <code id="pgpath"></code> <a id="pgopen" target="_blank" rel="noopener">open ↗</a></div>
<div id="pgshot"><div class="noshot">Tap a step on the left: its page shows here.</div></div>
</div>
</div>
</div>
</section>
<section class="view feat" id="v-feature">
<p class="crumb" id="fcrumb"></p>
<div id="featbody"></div>
</section>
<script>
// Page:Journey (SpecUi 2.36 #ui-journeys, Design 69 #journeys): specified journeys from SpecUi's journey fences,
// reverse-engineered ones from the pages' own links, features from the brain, open tickets from the pipeline.
// Built by designer-114 from the mockup Page:Mockup-flows (razie, chats 6acd2ebb and 6acd4261, 2026-10-12).
(function(){
const D2=window.d2||window.D2||{},PARAMS=Object.assign({},window.d2params||{},D2.params||{});
const URLQ=()=>new URLSearchParams(location.search);
// d2's own journeys and features live on d2spec, its pages on base d2; a project of its own passes ?spec=&pages= (both its id)
const SPEC=URLQ().get("spec")||PARAMS.spec||"d2spec",PAGES=URLQ().get("pages")||PARAMS.pages||"d2";
const api=(m,u,b)=>D2.api?Promise.resolve(D2.api(m,u,b)):Promise.reject(new Error("E_BRIDGE: this page runs inside d2 (no d2.api here)"));
const inP=(u,p)=>u+(u.includes("?")?"&":"?")+"project="+encodeURIComponent(p);
const rawOf=j=>typeof j==="string"?j:(j&&(j.text||j.body||j.markdown))||"";
const $=s=>document.querySelector(s),$$=s=>[...document.querySelectorAll(s)];
const esc=s=>String(s==null?"":s).replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]));
const BUILTIN=["/","/home","/signup","/account/plan","/settings","/settings/plan","/projects","/pipeline","/thoughts","/agents","/dashboard","/system","/topics","/drafts","/toolbox","/features","/pad","/fiddle","/flows","/history","/ai"];
const SKIP=/^\/(api|styles|cdn|assets|toolbox|docs|features|help|favicon|apple-touch|sso|dom|domain)\b|^\/topics\/?$|^(Help:Toolbox|Skill:|Style:)/i;
const OKREF=/^(\/[a-z][a-z0-9\/_-]*|\/|[A-Z][A-Za-z]*:[A-Za-z0-9_-]+|[A-Z][A-Za-z0-9]+)$/;
const pathOf=n=>!n?"":n.startsWith("/")?n:n.startsWith("Page:")?"/page/"+n.slice(5):n.startsWith("App:")?"/app/"+n.slice(4):"/topics/"+n;
const kindOf=n=>n.startsWith("/")?"built-in":n.includes(":")?n.split(":")[0]:"topic";
const nice=n=>n==="/"?"Landing (/)":n.startsWith("/")?n:n.replace(/^[A-Za-z]+:/,"").replace(/([a-z])([A-Z])/g,"$1 $2").replace(/-/g," ");
const short=n=>n.replace(/^[A-Za-z]+:/,"").replace(/^\//,"");
let st={},SEQ=0,J=[],FEATS=[],FB={},CLASSES=[],TK=[],PAGELIST=[],LINKS={},SCRIPT={},TEXT={};
let JM=null,SHAPE=[],sel=null,cur=null,jk="all";
// ---- data
async function topicText(n,projects){if(n in TEXT)return TEXT[n];for(const p of projects){try{const t=rawOf(await api("GET",inP("/api/v2/topics/"+encodeURIComponent(n)+"?format=raw",p)));if(t&&!/^\{"ok":false/.test(t)){TEXT[n]=t;return t}}catch(e){}}TEXT[n]="";return ""}
function linksIn(n,t){const raw=[...t.matchAll(/href=["'](\/[^"'\s>]*)/g)].map(m=>m[1]).concat([...t.matchAll(/\]\((\/[^)\s]*)\)/g)].map(m=>m[1]),[...t.matchAll(/\[\[([^\]|#]+)/g)].map(m=>"/topics/"+m[1].trim()),[...t.matchAll(/location\.(?:href\s*=|assign\()\s*["'](\/[^"']+)/g)].map(m=>m[1]));
const out=[];for(let p of raw){if(SKIP.test(p))continue;p=p.split("#")[0].split("?")[0].replace(/\/$/,"")||"/";let q=p;let m;
if((m=p.match(/^\/(page|app)\/([A-Za-z0-9_-]+)$/)))q=(m[1]==="page"?"Page:":"App:")+m[2];else if((m=p.match(/^\/topics\/([^/]+)$/)))q=decodeURIComponent(m[1]);
if(q!==n&&OKREF.test(q)&&!SKIP.test(q)&&q!=="Topic"&&!out.includes(q))out.push(q)}
return out.slice(0,12)}
async function linksOf(n){if(n in LINKS)return LINKS[n];if(n.startsWith("/")){LINKS[n]=null;return null}
const t=await topicText(n,[PAGES,SPEC]);if(!t){LINKS[n]=null;return null}
const l=linksIn(n,t);LINKS[n]=l;if(!l.length&&/```home-button/.test(t))SCRIPT[n]="a home-button with no target";else if(!l.length&&/```embedhtml|<script/.test(t))SCRIPT[n]="next is set in script";return l}
async function load(){
const specUi=await api("GET",inP("/api/v2/drafts/SpecUi",SPEC)).then(rawOf).catch(()=>"")||await api("GET",inP("/api/v2/topics/SpecUi?format=raw",SPEC)).then(rawOf).catch(()=>"");
const [brain,pipe,tops]=await Promise.all([api("GET",inP("/api/v2/brain",SPEC)).catch(()=>({})),
api("GET",inP("/api/v2/pipeline?status=open&fields=id,title,summary,status,kind,size,feature,forRole,takenName,created,updated&limit=1000",SPEC)).catch(()=>({})),
api("GET",inP("/api/v2/topics?limit=1000&fields=name,category",PAGES)).catch(()=>({}))]);
FEATS=(brain.F||[]);FEATS.forEach(f=>FB[f.id]=f);CLASSES=brain.C||[];
TK=((pipe.data||pipe.items||[]).filter(i=>!/^(done|dropped|moved)$/.test(i.status)));
const names=(tops.data||[]).map(x=>x.name).filter(n=>/^(Page|App):/.test(n)&&!/Mockup|-try$|-mock$|^App:Landing1$/.test(n));
J=parseJourneys(specUi);
PAGELIST=[...new Set([...BUILTIN,...names,...J.flatMap(j=>j.ids.map(i=>st[i].page)).filter(Boolean)])];
$("#jstatus").textContent=`${J.length} specified journey${J.length==1?"":"s"} · ${FEATS.length} features · ${PAGELIST.length} pages · journeys and features from ${SPEC}, pages from ${PAGES}`}
// ---- the journey fence (SpecUi 2.36): journey <id> "<name>" { … }, steps, if/elseif/else { }, wait, done
function mkStep(page,why,codes,kind){const id="s"+(++SEQ);st[id]={page,title:kind==="wait"?"Wait: "+why:nice(page),path:pathOf(page),href:pathOf(page),why:kind==="wait"?"":why||"",codes:codes||[],kind:kind||"",f:[],c:[],t:[],k:[]};enrich(id);return id}
function stepLine(l){let m=l.match(/^wait\s+"([^"]*)"/);if(m)return mkStep("",m[1],[],"wait");
const page=l.split(/\s+/)[0],why=(l.match(/"([^"]*)"/)||[])[1]||"",codes=(l.match(/\^[a-z][a-z0-9-]*-\d+/gi)||[]).map(c=>c.slice(1));
return mkStep(page,why,codes,/\bdone\s*$/.test(l)?"end":"")}
function parseJourneys(text){const out=[];for(const m of text.matchAll(/^```journey[ \t]*\n([\s\S]*?)^```/gm)){try{out.push(parseOne(m[1]))}catch(e){out.push({id:"error-"+out.length,name:"A journey fence that doesn't parse",error:e.message,shape:[],ids:[]})}}return out}
function parseOne(src){const L=src.split("\n").map(l=>l.trim()).filter(l=>l&&!l.startsWith("//"));
const h=(L.shift()||"").match(/^journey\s+(\S+)\s+"([^"]+)"\s*\{$/);if(!h)throw new Error('line 1: expected journey <id> "<name>" {');
const j={id:h[1],name:h[2],who:"",goal:"",shape:[]};let block=null,branch=null;
L.forEach((l,i)=>{let m;
if(l==="}"){branch=null;return}
if((m=l.match(/^who\s+"([^"]*)"(?:\s+goal\s+"([^"]*)")?$/))){j.who=m[1];j.goal=m[2]||j.goal;return}
if((m=l.match(/^goal\s+"([^"]*)"$/))){j.goal=m[1];return}
if((m=l.match(/^(if|elseif)\s+"([^"]+)"\s*\{\s*(.*?)\s*\}?\s*$/))||(m=l.match(/^(else)()\s*\{\s*(.*?)\s*\}?\s*$/))){
if(m[1]==="if"){block={row:[],label:""};j.shape.push(block)}else if(!block)throw new Error(`line ${i+2}: ${m[1]} without an if`);
const br={tag:m[1]==="else"?"else":m[2],steps:[]};block.row.push(br);
if(m[3])m[3].split(/\s*;\s*/).filter(Boolean).forEach(x=>br.steps.push(stepLine(x)));
branch=/\}\s*$/.test(l)?null:br.steps;return}
const id=stepLine(l);if(branch)branch.push(id);else{block=null;j.shape.push([id,st[id].why])}});
j.ids=flat(j.shape);number(j.ids);if(j.ids[0]&&!st[j.ids[0]].kind)st[j.ids[0]].kind="start";return j}
const flat=shape=>shape.flatMap(x=>Array.isArray(x)?[x[0]]:x.row.flatMap(r=>r.steps));
const number=ids=>ids.forEach((id,i)=>st[id].n=i+1);
// what a step touches: features from its codes (a code's prefix is its feature, as the Focus does), classes named in those
// features' lines, open tickets naming its page or its codes
function enrich(id){const s=st[id],seen=new Set();s.f=[];
for(const c of s.codes){const p=c.replace(/-\d+$/,""),f=FB[p],num=+c.split("-").pop();s.f.push([p,f&&f.beh[num-1]?f.beh[num-1]:"^"+c]);seen.add(p)}
const words=s.f.map(x=>x[1]).join(" ");s.c=CLASSES.filter(c=>new RegExp("\\b"+c.name+"\\b").test(words)).slice(0,6).map(c=>[c.name,c.tip||c.group||""]);
const sh=short(s.page);s.k=s.page?TK.filter(t=>{const x=(t.title||"")+" "+(t.summary||"");return (sh.length>3&&x.includes(sh))||s.codes.some(c=>x.includes(c))}).slice(0,6).map(t=>[t.id,t.title,t.status]):[]}
async function enrichFromPage(id){const s=st[id];if(!s.page||s.page.startsWith("/")||s.pageRead)return;s.pageRead=true;
const t=await topicText(s.page,[PAGES,SPEC]);const more=[...new Set((t.match(/\^[a-z][a-z0-9-]*-\d+/gi)||[]).map(c=>c.slice(1)))].filter(c=>!s.codes.includes(c)&&FB[c.replace(/-\d+$/,"")]);
if(more.length){s.codes=s.codes.concat(more.slice(0,8));enrich(id);draw()}}
// ---- reverse-engineering from a page: its links, two pages deep (Design 69)
async function infer(n){const seen=new Set([n]),root=mkStep(n,"",[]),kids=((await linksOf(n))||[]).filter(x=>!seen.has(x)).slice(0,6);kids.forEach(x=>seen.add(x));
const shape=[[root,""]];if(kids.length){const row=[];for(const k of kids){const g=((await linksOf(k))||[]).find(x=>!seen.has(x));if(g)seen.add(g);
row.push({tag:SCRIPT[k]?"next: "+SCRIPT[k]:k.startsWith("/")?"built-in: links in code":"",steps:g?[mkStep(k,"",[]),mkStep(g,"",[])]:[mkStep(k,"",[])]})}
shape.push({label:"links on the page",row})}
number(flat(shape));return shape}
// drift: a hop of a specified journey whose earlier page doesn't link the next one
async function drift(j){const ids=j.ids;await Promise.all(ids.map(i=>st[i].page?linksOf(st[i].page):null));let prev=[];
for(const x of j.shape){const heads=Array.isArray(x)?[[x[0]]]:x.row.map(r=>r.steps);const ends=[];
for(const seq of heads){let p=prev;for(const id of seq){const s=st[id];if(s.page&&p.length){const ok=p.some(q=>{const pl=LINKS[st[q].page];return pl==null||SCRIPT[st[q].page]||pl.includes(s.page)||pl.includes(s.path)});s.drift=!ok}p=s.page?[id]:p}ends.push(...p)}
if(!Array.isArray(x)&&x.row.some(r=>!r.steps.length))ends.push(...prev);prev=[...new Set(ends)]}
return ids.filter(i=>st[i].drift).length}
// ---- drawing the steps (the mockup's tab 2)
function node(id){const s=st[id];if(!s)return"";
const cls=["node",s.kind||"",sel===id?"sel":"",s.drift?"drift":""].join(" ");
const cnt=(k,one,many,a)=>a.length?`<span class="cnt ${k}">${a.length} ${a.length==1?one:many}</span>`:"";
const lay=(k,a,f)=>a.length?`<div class="layer ${k}">${a.map(x=>`<span>${f(x)}</span>`).join("")}</div>`:"";
return `<button class="${cls}" data-step="${id}"><span class="n">${s.n} · <code>${esc(s.path||"no page")}</code></span><b>${esc(s.title)}</b>`+
(s.drift?`<span class="driftnote">⚠ no link from the step before</span>`:"")+
`<div class="counts">${cnt("f","behaviour","behaviours",s.f)}${cnt("c","class","classes",s.c)}${cnt("k","ticket","tickets",s.k)}</div>`+lay("k",s.k,x=>x[0])+`</button>`}
const arrow=l=>`<div class="arrow">${l?`<span class="lbl">${esc(l)}</span>`:""}</div>`;
function draw(){const el=$("#miniflow");let h="";const nd=id=>sel===id?stepBrain(id,node(id)):node(id);
SHAPE.forEach((x,i)=>{if(Array.isArray(x)){if(i)h+=arrow(x[1]);h+=nd(x[0])}
else{h+=arrow(x.label);h+=`<div class="row">`+x.row.map(b=>`<div class="branch"><span class="tag">${esc(b.tag)}</span>${b.steps.map(nd).join(arrow(""))}${b.steps.length?"":`<span class="tag">↓</span>`}</div>`).join("")+`</div>`}});
el.innerHTML=h;wire()}
function stepBrain(id,card){const s=st[id],uniq=a=>[...new Map(a.map(x=>[x[0],x])).values()];
const bn=(k,ref,label,tip,extra)=>`<a class="bn ${k}${extra||""}${cur&&cur[0]===k&&cur[1]===ref?" on":""}" href="#" data-bk="${k}" data-bid="${esc(ref)}" title="${esc(tip||label)}"><i class="bs ${k}"></i><span>${esc(label)}</span></a>`;
const feats=uniq(s.f).map(f=>bn("f",f[0],FB[f[0]]?f[0]:"^"+f[0],s.f.filter(x=>x[0]===f[0]).map(x=>x[1]).join(" · ")));
const cls=s.c.map(c=>bn("c",c[0],c[0],c[1]));
const tks=s.k.map(k=>bn("k",k[0],k[0]+" "+k[1],k[2]+": "+k[1],/parked|paused/.test(k[2])?" dim":""));
const tps=s.page?[bn("p",id,s.page,"the step's page")]:[];
const col=(side,a)=>`<div class="bcol ${side}">${a.length?a.join(""):`<span class="bnone">none</span>`}</div>`;
return `<div class="stepbrain"><svg class="bwires" aria-hidden="true"></svg>${col("l",feats.concat(cls))}<div class="bcard">${card}</div>${col("r",tks.concat(tps))}</div>`}
function wire(){document.querySelectorAll(".stepbrain").forEach(b=>{const sv=b.querySelector(".bwires"),R=b.getBoundingClientRect(),c=b.querySelector(".bcard .node").getBoundingClientRect();
sv.setAttribute("viewBox",`0 0 ${R.width} ${R.height}`);
sv.innerHTML=[...b.querySelectorAll(".bn")].map(n=>{const r=n.querySelector(".bs").getBoundingClientRect(),left=n.closest(".bcol.l"),
x1=(left?r.right:r.left)-R.left,y1=r.top+r.height/2-R.top,x2=(left?c.left:c.right)-R.left,y2=c.top+c.height/2-R.top,mx=(x1+x2)/2;
return `<path class="${[...n.classList].filter(k=>k.length==1||k==="dim").join(" ")}" d="M${x1} ${y1} C${mx} ${y1} ${mx} ${y2} ${x2} ${y2}"/>`}).join("")})}
addEventListener("resize",wire);
// ---- the right pane: the step's page, or a brain node as a Focus member
function nochat(e){try{const d=e.target.contentDocument;if(d&&!d.getElementById("jr-nochat")){const s=d.createElement("style");s.id="jr-nochat";s.textContent="#d2chat{display:none!important}";(d.head||d.documentElement).append(s)}}catch(x){}}
function page(id){const s=st[id];$(".pghead").hidden=!s.page;$("#pgtitle").textContent=s.n+" · "+s.title;$("#pgpath").textContent=s.path;$("#pgopen").href=s.path;
if(!s.page){$("#pgshot").innerHTML=`<div class="noshot">${esc(s.title)}: a step with no page.</div>`}
else{const f=$("#pgshot").querySelector("iframe")||Object.assign(document.createElement("iframe"),{title:"the step's page",onload:nochat});if(!f.isConnected){$("#pgshot").innerHTML="";$("#pgshot").append(f)}if(f.dataset.src!==s.path){f.dataset.src=s.path;f.src=s.path}}
$("#rplbl").textContent="step "+s.n+" of the journey";rp("page")}
function rp(w){if($(".focus").dataset.show==="left")fold(phone()?"right":"both");$$("[data-rp]").forEach(x=>x.setAttribute("aria-pressed",String(x.dataset.rp===w)));$("#rp-item").hidden=w!=="item";$("#rp-page").hidden=w!=="page";wire()}
const onSteps=(k,ref)=>flat(SHAPE).filter(i=>st[i][k].some(x=>x[0]===ref));
const stepLink=i=>`<a href="#" data-goto="${i}">${st[i].n} · ${esc(st[i].title)}</a>`;
function content(k,ref){
if(k==="f"){const f=FB[ref];if(!f)return `<section class="mo-content f"><div class="mo-kind">IN THE SPEC</div><p class="mo-none"><b>^${esc(ref)}</b> is a code with no feature of that name in the brain (a SpecUi or Spec section's own code).</p></section>`;
return `<section class="mo-content f"><div class="mo-kind">IN THE SPEC · ${esc(f.name)} <span class="st">(${esc(f.id)}, ${f.beh.length} behaviours)</span></div><ol class="mo-beh">${f.beh.map(b=>`<li>${esc(b)}</li>`).join("")}</ol></section>`}
if(k==="c"){const c=CLASSES.find(x=>x.name===ref);if(!c)return "";
return `<section class="mo-content c"><div class="mo-kind">IN THE MODEL · class ${esc(c.name)}${c.group?` <span class="st">· ${esc(c.group)}</span>`:""}</div>${c.tip?`<p class="mo-tip">${esc(c.tip)}</p>`:""}<table class="mo-fields">${(c.fields||[]).map(f=>`<tr><td>${esc(f.name)}</td><td>${esc(f.type)}${f.many?"*":""}</td></tr>`).join("")}</table></section>`}
return ""}
function openItem(k,ref){if(k==="p"){cur=null;page(ref);return}cur=[k,ref];
const KIND={f:"FEATURE",c:"CLASS",k:"TICKET"}[k],on=onSteps(k,ref);let body="",acts="";
if(k==="f"){body=`<p class="mo-sub">On this journey</p><ul class="mo-list">${on.map(i=>st[i].f.filter(x=>x[0]===ref).map(x=>`<li>${esc(x[1])} <span class="st">${stepLink(i)}</span></li>`).join("")).join("")}</ul>`;
acts=FB[ref]?`<a class="mo-btn" href="?feature=${encodeURIComponent(ref)}" data-feat="${esc(ref)}">Feature page →</a>`:""}
if(k==="c"){body=`<p class="mo-sub">On this journey</p><ul class="mo-list">${on.map(i=>`<li>${stepLink(i)}</li>`).join("")}</ul>`}
if(k==="k"){const x=TK.find(t=>t.id===ref)||{};const pill=(c,v)=>v?`<span class="mo-pill ${c}">${esc(v)}</span>`:"";
body=`<h4 class="mo-title">${esc(x.title||ref)}</h4><div class="mo-pills">${pill("s-"+String(x.status).replace(/\W+/g,"-"),x.status)}${pill("",x.kind)}${pill("",x.size)}${x.feature?pill("f",x.feature):""}</div>`+
`<p class="mo-sum">${x.summary?esc(x.summary):"<i>No summary yet.</i>"}</p>`+
`<dl class="mo-meta"><dt>lane</dt><dd>${esc(x.forRole||"–")}${x.takenName?" · held by "+esc(x.takenName):""}</dd><dt>filed</dt><dd>${esc((x.created||"–").slice(0,10))}</dd><dt>changed</dt><dd>${esc((x.updated||"–").slice(0,10))}</dd><dt>on steps</dt><dd>${on.map(stepLink).join(", ")||"–"}</dd></dl>`;
acts=`<a class="mo-btn" href="https://${SPEC==="d2spec"?"d2spec.aiputty.com":location.host}/pipeline/${ref}" target="_blank" rel="noopener">Open ticket ↗</a>`}
$("#rp-item").innerHTML=`<section class="mopen ${k}"><div class="mo-head"><span class="mo-kind">${KIND} · ${esc(ref)}</span></div>${body}<div class="mo-acts">${acts}</div></section>`+content(k,ref);
$('[data-rp="item"]').hidden=false;$("#rplbl").textContent=KIND.toLowerCase()+(sel?" from step "+st[sel].n:"");rp("item");
$$(".stepbrain .bn").forEach(n=>n.classList.toggle("on",n.dataset.bk===k&&n.dataset.bid===ref))}
function pick(id){sel=sel===id?null:id;cur=null;draw();if(sel){page(sel);enrichFromPage(sel)}}
// ---- panes
const phone=()=>matchMedia("(max-width:820px)").matches;
function fold(s){const f=$(".focus");if(phone()&&s==="both")s="left";f.dataset.show=s;$$("[data-pick]").forEach(x=>x.setAttribute("aria-pressed",String(x.dataset.pick===s)));requestAnimationFrame(wire)}
matchMedia("(max-width:820px)").addEventListener("change",()=>fold($(".focus").dataset.show));
// ---- views and addresses: ?journey=<id>, ?page=<page>, ?feature=<id>, none = the search
function show(v){$$("section.view").forEach(s=>s.classList.toggle("on",s.id==="v-"+v));window.scrollTo(0,0)}
function go(q){history.pushState(null,"",q?"?"+q:location.pathname);route()}
function jhead(){const j=JM,inf=j.inferred,q=inf?"page="+j.page:"journey="+j.id;
$("#jhead").innerHTML=`<span class="jkind ${inf?"rev":"spec"}">${inf?"Reverse-engineered":"Specified"}</span><b>${esc(j.title)}</b><span class="st">${inf?`from the links on ${esc(nice(j.page))}, two pages deep; it stops where a page moves on by script`:`in SpecUi${j.who?` · ${esc(j.who)} → ${esc(j.goal)}`:""}`}${j.drift?` · <span class="drifts">⚠ ${j.drift} hop${j.drift==1?"":"s"} without a link</span>`:""}</span><code>/journey?${esc(q)}</code>${j.back?`<a href="?feature=${encodeURIComponent(j.back)}" data-feat="${esc(j.back)}">← ${esc(j.back)}</a>`:""}<a href="?" data-search>← Search</a>`}
async function openJourney(id,back){const j=J.find(x=>x.id===id);if(!j){$("#jstatus").textContent=`No specified journey "${id}".`;show("find");return}
JM={title:j.name,id:j.id,who:j.who,goal:j.goal,back,inferred:false};SHAPE=j.shape;sel=null;cur=null;
$("#ftitle").textContent=j.name;$("#jbanner").hidden=!j.error;if(j.error)$("#jbanner").textContent="This journey doesn't parse: "+j.error;
show("journey");fold("both");draw();jhead();JM.drift=await drift(j);draw();jhead()}
async function openInferred(n){JM={title:"From "+nice(n),page:n,inferred:true};sel=null;cur=null;show("journey");$("#ftitle").textContent=JM.title;$("#miniflow").innerHTML=`<p class="legend">Following the links on ${esc(nice(n))}…</p>`;jhead();
SHAPE=await infer(n);const on=J.filter(j=>j.ids.some(i=>st[i].page===n));
const stops=flat(SHAPE).filter(i=>SCRIPT[st[i].page]).map(i=>st[i].title);const b=$("#jbanner");b.hidden=false;
b.innerHTML=`Reverse-engineered by following the links on <b>${esc(nice(n))}</b>.${LINKS[n]===null&&!n.startsWith("/")?` <span class="st">Its text couldn't be read here.</span>`:""}${n.startsWith("/")?` <span class="st">A built-in page keeps its links in code.</span>`:""}${stops.length?` <span class="st">Stops where the next page is set in script: ${stops.map(esc).join(", ")}.</span>`:""} ${on.length?on.map(j=>`<a href="?journey=${encodeURIComponent(j.id)}" data-journey="${esc(j.id)}">Also step ${j.ids.map(i=>st[i]).find(s=>s.page===n).n} of the specified <i>${esc(j.name)}</i> →</a>`).join(" "):`<span class="st">On no specified journey yet.</span>`}`;
fold("both");draw()}
function featPage(fid){const f=FB[fid];show("feature");if(!f){$("#featbody").innerHTML=`<p>No feature <b>${esc(fid)}</b>.</p>`;return}
const js=J.map(j=>({j,h:j.ids.filter(i=>st[i].f.some(x=>x[0]===fid))})).filter(x=>x.h.length);
$("#fcrumb").innerHTML=`<a href="?" data-search>← Search</a> › Features › <b>${esc(f.id)}</b>`;
$("#featbody").innerHTML=`<h3>${esc(f.id)} · ${esc(f.name)}</h3><ol class="mo-beh">${f.beh.map(b=>`<li>${esc(b)}</li>`).join("")}</ol><h3>Journeys</h3>`+
(js.length?js.map(({j,h})=>`<div class="flowcard"><a class="go" href="?journey=${encodeURIComponent(j.id)}" data-journey="${esc(j.id)}" data-from="${esc(fid)}">Open →</a><b>${esc(j.name)}</b> <span class="legend">· ${j.ids.length} steps · ${h.length} touch this feature</span><div class="strip">${j.ids.map(i=>`<span class="${h.includes(i)?"hit":""}">${esc(st[i].title)}</span>`).join("<i>→</i>")}</div></div>`).join(""):
`<div class="flowcard nojourney"><b>No journey yet.</b> <span class="legend">Every feature has at least one: this one is owed a specified journey in SpecUi.</span></div>`)}
function jlist(){const q=$("#jq").value.trim().toLowerCase(),m=x=>!q||String(x).toLowerCase().includes(q),want=k=>jk==="all"||jk===k;
const js=want("j")?J.filter(j=>m(j.name)||m(j.id)):[],fs=want("f")?FEATS.filter(f=>m(f.id)||m(f.name)):[],ps=want("p")?PAGELIST.filter(n=>m(n)||m(nice(n))||m(pathOf(n))):[];
const onJ=n=>J.filter(j=>j.ids.some(i=>st[i].page===n));
const jrow=j=>`<a href="?journey=${encodeURIComponent(j.id)}" class="jrow j" data-journey="${esc(j.id)}"><i class="bs j"></i><b>${esc(j.name)}</b><span class="jt">specified</span><span class="st">${j.error?"doesn't parse":j.ids.length+" steps"}</span></a>`;
const frow=f=>{const n=J.filter(j=>j.ids.some(i=>st[i].f.some(x=>x[0]===f.id))).length;return `<a href="?feature=${encodeURIComponent(f.id)}" class="jrow f" data-feat="${esc(f.id)}"><i class="bs f"></i><b>${esc(f.name)}</b><code>${esc(f.id)}</code><span class="jt">feature</span><span class="st">${f.beh.length} behaviours · ${n?`on ${n} journey${n==1?"":"s"}`:"no journey yet"}</span></a>`};
const prow=n=>{const o=onJ(n);return `<a href="?page=${encodeURIComponent(n)}" class="jrow p" data-page="${esc(n)}"><i class="bs p"></i><b>${esc(nice(n))}</b><code>${esc(pathOf(n))}</code><span class="jt">${esc(kindOf(n))}</span><span class="st">${o.length?`on ${o.map(j=>esc(j.name)).join(", ")}`:""}</span></a>`};
const grp=(h,a,f)=>a.length?`<h4>${h} <span class="st">${a.length}</span></h4>${a.map(f).join("")}`:"";
$("#jres").innerHTML=grp("Specified journeys",js,jrow)+grp("Features",fs,frow)+grp("Pages",ps,prow)||`<p class="st">Nothing matches.</p>`}
function route(){const q=URLQ();if(q.get("journey"))openJourney(q.get("journey"),q.get("from"));else if(q.get("page"))openInferred(q.get("page"));else if(q.get("feature"))featPage(q.get("feature"));else{show("find");jlist();$("#jq").focus()}}
addEventListener("popstate",route);
document.addEventListener("input",e=>{if(e.target.id==="jq")jlist()});
document.addEventListener("click",e=>{const t=e.target;let x;
if((x=t.closest("[data-jk]"))){jk=x.dataset.jk;$$("[data-jk]").forEach(b=>b.setAttribute("aria-pressed",String(b===x)));jlist();return}
if((x=t.closest("[data-journey]"))){e.preventDefault();go("journey="+encodeURIComponent(x.dataset.journey)+(x.dataset.from?"&from="+encodeURIComponent(x.dataset.from):""));return}
if((x=t.closest("[data-feat]"))){e.preventDefault();go("feature="+encodeURIComponent(x.dataset.feat));return}
if((x=t.closest("[data-page]"))){e.preventDefault();go("page="+encodeURIComponent(x.dataset.page));return}
if(t.closest("[data-search]")){e.preventDefault();go("");return}
if((x=t.closest(".stepbrain .bn"))){e.preventDefault();openItem(x.dataset.bk,x.dataset.bid);return}
if((x=t.closest("[data-goto]"))){e.preventDefault();if(sel!==x.dataset.goto)pick(x.dataset.goto);else page(sel);return}
if((x=t.closest("[data-step]"))){e.preventDefault();pick(x.dataset.step);return}
if((x=t.closest("[data-rp]"))){rp(x.dataset.rp);return}
if((x=t.closest("[data-pick]"))){fold(x.dataset.pick);return}
if((x=t.closest("[data-fold]"))){const s=$(".focus").dataset.show;fold(x.dataset.fold==="l"?(s==="left"?"both":"right"):(s==="right"?"both":"left"));return}});
load().then(route).catch(e=>{$("#jstatus").textContent="Couldn't load: "+e.message;show("find")});
})();
</script></body></html>