HTML 预览工具
粘贴或上传独立 HTML(含 Archify 架构图),在沙箱里预览并生成可分享短链。
示例
下面演示如何用 Archify 生成 HTML,再拿到本工具预览。展开 HTML 源码 可复制片段;或打开 HTML 预览 点击 加载 Archify 示例,查看官网 S·01 Agent Tool Call 工作流图。
安装 Archify 并出图
npx -y skills add tt-a1i/archify --skill archify --agent cursor --global --copy --yes
然后对 Agent 说:「用 Archify 把这个仓库画成架构图。」它会写 JSON 规格,用 node bin/archify.mjs validate architecture … --quality showcase --json 校验,再交付一份可离线打开的 HTML。
官方 Agent Tool Call 图
工具页用 Vite ?raw 引入 html-preview-example.html。这是 Archify 官网主推的 S·01 Agent Tool Call 工作流(Signal Flow):User → Chat → Planner → Tool Router → Approval Gate → Tool Call。
在线预览
html-preview-example.html
<!doctype html>
<html lang="en" data-theme="dark" data-preset="signal-flow">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="generator" content="archify 2.17.0-dev.1" />
<title>Agent Tool Call Workflow Diagram</title>
<script>
// Resolve the theme before first paint so light-preference users don't
// see a dark flash. The toolbar script below re-applies with UI state.
(function () {
try {
var theme = null;
try {
var param = new URLSearchParams(window.location.search).get("theme");
if (param === "light" || param === "dark") theme = param;
if (new URLSearchParams(window.location.search).get("embed") === "1") {
document.documentElement.setAttribute("data-embed", "true");
}
if (new URLSearchParams(window.location.search).get("present") === "1") {
document.documentElement.setAttribute("data-present", "true");
}
} catch (_) {}
if (!theme) {
try {
theme = localStorage.getItem("archify-theme");
} catch (_) {}
}
if (theme !== "light" && theme !== "dark") {
theme = "dark";
}
document.documentElement.setAttribute("data-theme", theme);
} catch (_) {}
})();
</script>
<!-- Async font load: a blackholed network must not block first paint.
The body font stack falls back to system monospace until it lands. -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap"
rel="stylesheet"
media="print"
onload="this.media = 'all'"
/>
<noscript>
<link
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
</noscript>
<style>
/* ==========================================================
THEME VARIABLES — switch by toggling [data-theme] on <html>
========================================================== */
:root,
[data-theme="dark"] {
--bg: #020617;
--grid: #1e293b;
--text: #ffffff;
--text-muted: #94a3b8;
--text-dim: #475569;
/* UI chrome and menu hints need more contrast than SVG .t-dim accents */
--text-faint: #7d8da1;
--panel: rgba(15, 23, 42, 0.5);
--panel-border: #1e293b;
--lane-fill: rgba(15, 23, 42, 0.22);
--lane-stroke: #334155;
--arrow: #64748b;
--arrow-emphasis: #34d399;
/* Opaque mask color used behind semi-transparent component fills
so the arrows drawn underneath are properly hidden. */
--mask: #0f172a;
--frontend-fill: rgba(8, 51, 68, 0.4);
--frontend-stroke: #22d3ee;
--backend-fill: rgba(6, 78, 59, 0.4);
--backend-stroke: #34d399;
--database-fill: rgba(76, 29, 149, 0.4);
--database-stroke: #a78bfa;
--cloud-fill: rgba(120, 53, 15, 0.3);
--cloud-stroke: #fbbf24;
--security-fill: rgba(136, 19, 55, 0.4);
--security-stroke: #fb7185;
--messagebus-fill: rgba(251, 146, 60, 0.3);
--messagebus-stroke: #fb923c;
--external-fill: rgba(30, 41, 59, 0.5);
--external-stroke: #94a3b8;
--toolbar-bg: rgba(15, 23, 42, 0.8);
--toolbar-border: #334155;
--toolbar-text: #e2e8f0;
--toolbar-hover: rgba(15, 23, 42, 0.95);
--toolbar-menu-bg: #0f172a;
}
[data-theme="light"] {
--bg: #f8fafc;
--grid: #e2e8f0;
--text: #0f172a;
--text-muted: #64748b;
--text-dim: #94a3b8;
--text-faint: #64748b;
--panel: #ffffff;
--panel-border: #e2e8f0;
--lane-fill: rgba(248, 250, 252, 0.65);
--lane-stroke: #cbd5e1;
--arrow: #94a3b8;
--arrow-emphasis: #059669;
--mask: #ffffff;
--frontend-fill: rgba(34, 211, 238, 0.15);
--frontend-stroke: #0891b2;
--backend-fill: rgba(52, 211, 153, 0.18);
--backend-stroke: #059669;
--database-fill: rgba(167, 139, 250, 0.2);
--database-stroke: #7c3aed;
--cloud-fill: rgba(251, 191, 36, 0.18);
--cloud-stroke: #d97706;
--security-fill: rgba(251, 113, 133, 0.15);
--security-stroke: #e11d48;
--messagebus-fill: rgba(251, 146, 60, 0.15);
--messagebus-stroke: #ea580c;
--external-fill: rgba(148, 163, 184, 0.18);
--external-stroke: #64748b;
--toolbar-bg: rgba(255, 255, 255, 0.92);
--toolbar-border: #cbd5e1;
--toolbar-text: #334155;
--toolbar-hover: #ffffff;
--toolbar-menu-bg: #ffffff;
}
/* ==========================================================
SIGNAL FLOW — an opt-in, motion-forward visual preset.
The variables also target a standalone exported SVG, whose
root carries data-preset and data-theme directly.
========================================================== */
[data-preset="signal-flow"][data-theme="dark"] {
--bg: #030711;
--grid: #15233a;
--panel: rgba(6, 14, 28, 0.78);
--panel-border: #1d3350;
--lane-fill: rgba(9, 22, 40, 0.5);
--lane-stroke: #2c4564;
--text: #f5fbff;
--text-muted: #9eb0c7;
--text-dim: #52667f;
--text-faint: #7890ad;
--mask: #07101e;
--frontend-fill: rgba(6, 182, 212, 0.14);
--frontend-stroke: #67e8f9;
--backend-fill: rgba(16, 185, 129, 0.14);
--backend-stroke: #5eead4;
--database-fill: rgba(139, 92, 246, 0.16);
--database-stroke: #c4b5fd;
--cloud-fill: rgba(245, 158, 11, 0.13);
--cloud-stroke: #fcd34d;
--security-fill: rgba(244, 63, 94, 0.13);
--security-stroke: #fda4af;
--messagebus-fill: rgba(249, 115, 22, 0.13);
--messagebus-stroke: #fdba74;
--external-fill: rgba(71, 85, 105, 0.24);
--external-stroke: #a5b4c7;
--arrow: #7890ad;
--arrow-emphasis: #2dd4bf;
--toolbar-bg: rgba(7, 16, 30, 0.86);
--toolbar-border: #29415f;
--toolbar-menu-bg: #07101e;
}
[data-preset="signal-flow"][data-theme="light"] {
--bg: #f4f9fc;
--grid: #d4e5ee;
--panel: rgba(255, 255, 255, 0.82);
--panel-border: #bfd5e2;
--lane-fill: rgba(232, 243, 248, 0.62);
--lane-stroke: #a9c5d5;
--text: #102638;
--text-muted: #587287;
--text-dim: #8aa2b4;
--text-faint: #668397;
--mask: #ffffff;
--frontend-fill: rgba(6, 182, 212, 0.09);
--frontend-stroke: #0789a1;
--backend-fill: rgba(5, 150, 105, 0.09);
--backend-stroke: #087f69;
--database-fill: rgba(124, 58, 237, 0.09);
--database-stroke: #7254c7;
--cloud-fill: rgba(217, 119, 6, 0.08);
--cloud-stroke: #b9670b;
--security-fill: rgba(225, 29, 72, 0.08);
--security-stroke: #c53a59;
--messagebus-fill: rgba(234, 88, 12, 0.08);
--messagebus-stroke: #c65f27;
--external-fill: rgba(100, 116, 139, 0.1);
--external-stroke: #607a8c;
--arrow: #7b97aa;
--arrow-emphasis: #0d9488;
}
/* ==========================================================
BLUEPRINT — a review-first drafting preset for deployment,
infrastructure, and technical design documentation. It keeps
the same semantic palette and geometry while trading glow for
precise grid contrast, squared materials, and drafting marks.
========================================================== */
[data-preset="blueprint"][data-theme="dark"] {
--bg: #06131f;
--grid: #17425a;
--panel: rgba(7, 27, 43, 0.9);
--panel-border: #27627f;
--lane-fill: rgba(10, 43, 66, 0.36);
--lane-stroke: #34799a;
--text: #e3f6ff;
--text-muted: #91b8ca;
--text-dim: #557e92;
--text-faint: #739caf;
--mask: #0a2031;
--frontend-fill: rgba(26, 157, 193, 0.14);
--frontend-stroke: #66d9ef;
--backend-fill: rgba(31, 155, 124, 0.13);
--backend-stroke: #69dfbd;
--database-fill: rgba(120, 105, 196, 0.14);
--database-stroke: #b4a8ff;
--cloud-fill: rgba(197, 145, 43, 0.13);
--cloud-stroke: #ffd166;
--security-fill: rgba(199, 76, 104, 0.13);
--security-stroke: #ff8da1;
--messagebus-fill: rgba(207, 112, 49, 0.13);
--messagebus-stroke: #ffad66;
--external-fill: rgba(84, 119, 139, 0.18);
--external-stroke: #a7cad9;
--arrow: #78a3b7;
--arrow-emphasis: #64dfc1;
--toolbar-bg: rgba(7, 28, 45, 0.92);
--toolbar-border: #34799a;
--toolbar-text: #d8f3ff;
--toolbar-hover: rgba(18, 58, 83, 0.95);
--toolbar-menu-bg: #0a2031;
}
[data-preset="blueprint"][data-theme="light"] {
--bg: #edf7fa;
--grid: #b5d5e1;
--panel: rgba(249, 253, 255, 0.94);
--panel-border: #78aabd;
--lane-fill: rgba(210, 232, 240, 0.42);
--lane-stroke: #83b2c4;
--text: #123344;
--text-muted: #4e7486;
--text-dim: #86a6b4;
--text-faint: #668c9d;
--mask: #f9fdff;
--frontend-fill: rgba(8, 145, 178, 0.08);
--frontend-stroke: #087f9c;
--backend-fill: rgba(5, 128, 101, 0.08);
--backend-stroke: #08755f;
--database-fill: rgba(100, 75, 180, 0.08);
--database-stroke: #6757a8;
--cloud-fill: rgba(181, 120, 15, 0.08);
--cloud-stroke: #a86609;
--security-fill: rgba(190, 48, 80, 0.07);
--security-stroke: #b32f50;
--messagebus-fill: rgba(196, 81, 22, 0.07);
--messagebus-stroke: #b65120;
--external-fill: rgba(79, 112, 128, 0.08);
--external-stroke: #506f7e;
--arrow: #6d93a5;
--arrow-emphasis: #087f69;
--toolbar-bg: rgba(247, 252, 254, 0.94);
--toolbar-border: #8ab5c5;
--toolbar-text: #294f61;
--toolbar-hover: #ffffff;
--toolbar-menu-bg: #f9fdff;
}
/* ==========================================================
EDITORIAL — a warm, publication-minded preset for design
reviews, launch notes, and documentation. The diagram keeps
the same semantic palette and exact geometry, but trades
software chrome for paper, ink, ruled structure, and one
restrained vermilion accent.
========================================================== */
[data-preset="editorial"][data-theme="dark"] {
--bg: #181611;
--grid: #39342a;
--panel: rgba(35, 31, 24, 0.96);
--panel-border: #625a4a;
--lane-fill: rgba(52, 46, 35, 0.46);
--lane-stroke: #726957;
--text: #f4eddf;
--text-muted: #b9ae9b;
--text-dim: #776e60;
--text-faint: #9d917e;
--mask: #231f18;
--frontend-fill: rgba(43, 133, 142, 0.16);
--frontend-stroke: #7fc6c7;
--backend-fill: rgba(63, 132, 92, 0.16);
--backend-stroke: #8fc29e;
--database-fill: rgba(117, 91, 141, 0.17);
--database-stroke: #c0a4d0;
--cloud-fill: rgba(170, 119, 49, 0.16);
--cloud-stroke: #d8ad68;
--security-fill: rgba(157, 69, 65, 0.17);
--security-stroke: #df9085;
--messagebus-fill: rgba(174, 79, 42, 0.16);
--messagebus-stroke: #df946f;
--external-fill: rgba(126, 115, 96, 0.18);
--external-stroke: #b8ad99;
--arrow: #948978;
--arrow-emphasis: #dd6b3d;
--toolbar-bg: rgba(35, 31, 24, 0.92);
--toolbar-border: #625a4a;
--toolbar-text: #eee5d5;
--toolbar-hover: #302a20;
--toolbar-menu-bg: #231f18;
}
[data-preset="editorial"][data-theme="light"] {
--bg: #f2eee5;
--grid: #d8d0c2;
--panel: rgba(251, 248, 241, 0.97);
--panel-border: #c4b9a6;
--lane-fill: rgba(229, 221, 207, 0.42);
--lane-stroke: #b8aa94;
--text: #242018;
--text-muted: #6f6658;
--text-dim: #a09788;
--text-faint: #817767;
--mask: #fbf8f1;
--frontend-fill: rgba(31, 117, 126, 0.09);
--frontend-stroke: #287e84;
--backend-fill: rgba(42, 119, 75, 0.09);
--backend-stroke: #397b53;
--database-fill: rgba(105, 75, 130, 0.09);
--database-stroke: #765d86;
--cloud-fill: rgba(157, 103, 31, 0.1);
--cloud-stroke: #9a671f;
--security-fill: rgba(153, 58, 52, 0.09);
--security-stroke: #9e463f;
--messagebus-fill: rgba(182, 70, 27, 0.09);
--messagebus-stroke: #ad4b25;
--external-fill: rgba(101, 91, 75, 0.09);
--external-stroke: #746b5e;
--arrow: #8a806f;
--arrow-emphasis: #bb4c23;
--toolbar-bg: rgba(251, 248, 241, 0.94);
--toolbar-border: #c4b9a6;
--toolbar-text: #40392f;
--toolbar-hover: #fffdf8;
--toolbar-menu-bg: #fbf8f1;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family:
"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, "DejaVu Sans Mono",
"Liberation Mono", "Noto Sans Mono CJK SC", "PingFang SC", "Hiragino Sans GB",
"Microsoft YaHei", monospace;
background: var(--bg);
min-height: 100vh;
padding: 2rem;
color: var(--text);
transition:
background 0.2s ease,
color 0.2s ease;
}
html[data-preset="signal-flow"] body {
background-image:
radial-gradient(circle at 18% -8%, rgba(34, 211, 238, 0.14), transparent 34rem),
radial-gradient(circle at 92% 18%, rgba(139, 92, 246, 0.12), transparent 30rem);
background-attachment: fixed;
}
html[data-preset="signal-flow"][data-theme="light"] body {
background-image:
radial-gradient(circle at 18% -8%, rgba(6, 182, 212, 0.12), transparent 34rem),
radial-gradient(circle at 92% 18%, rgba(139, 92, 246, 0.08), transparent 30rem);
}
html[data-preset="blueprint"] body {
background-image:
linear-gradient(var(--grid) 1px, transparent 1px),
linear-gradient(90deg, var(--grid) 1px, transparent 1px);
background-size: 32px 32px;
background-position: -1px -1px;
background-attachment: fixed;
}
html[data-preset="editorial"] body {
background-image:
linear-gradient(
90deg,
transparent 0,
transparent 5.4rem,
color-mix(in srgb, var(--security-stroke) 18%, transparent) 5.4rem,
color-mix(in srgb, var(--security-stroke) 18%, transparent) calc(5.4rem + 1px),
transparent calc(5.4rem + 1px)
),
repeating-linear-gradient(
0deg,
transparent 0,
transparent 31px,
color-mix(in srgb, var(--grid) 36%, transparent) 31px,
color-mix(in srgb, var(--grid) 36%, transparent) 32px
);
background-attachment: fixed;
}
/* Gallery/embed mode keeps the generated artifact as the source of truth
while presenting only its diagram surface inside a proof card. */
html[data-embed="true"] body {
min-height: 0;
padding: 0;
overflow: hidden;
background: var(--bg);
background-image: none;
}
html[data-embed="true"] .toolbar,
html[data-embed="true"] .header,
html[data-embed="true"] .cards,
html[data-embed="true"] .diagram-nav,
html[data-embed="true"] .overview-map,
html[data-embed="true"] .focus-chip,
html[data-embed="true"] .route-probe,
html[data-embed="true"] .semantic-lens,
html[data-embed="true"] .diagram-guide,
html[data-embed="true"] .node-finder,
html[data-embed="true"] .guided-views {
display: none !important;
}
html[data-embed="true"] .container {
max-width: none;
}
html[data-embed="true"] .diagram-container {
padding: 0.5rem;
border: 0;
border-radius: 0;
box-shadow: none;
}
html[data-embed="true"] .diagram-container svg {
min-width: 0;
}
/* Share Chapter Cue lives in the HTML viewer layer, not inside the SVG.
It gives one-shot embeds a title, authored note, truthful stop order,
and lifecycle state while canonical diagram geometry stays untouched. */
.share-chapter-cue {
display: none;
}
html[data-embed="true"][data-share-playback="true"] .share-chapter-cue:not([hidden]),
html[data-embed="true"][data-share-moment="true"] .share-chapter-cue:not([hidden]) {
position: absolute;
z-index: 18;
top: 0.85rem;
left: 50%;
display: grid;
grid-template-columns: auto minmax(9rem, 13.5rem) minmax(0, 1fr);
align-items: center;
gap: 0.45rem 0.85rem;
width: min(52rem, calc(100% - 1.5rem));
min-height: 3.35rem;
padding: 0.55rem 0.75rem 0.65rem;
overflow: hidden;
border: 1px solid color-mix(in srgb, var(--frontend-stroke) 42%, var(--toolbar-border));
border-radius: 0.75rem;
background:
linear-gradient(
115deg,
color-mix(in srgb, var(--frontend-fill) 52%, transparent),
transparent 45%
),
color-mix(in srgb, var(--toolbar-menu-bg) 92%, transparent);
color: var(--toolbar-text);
box-shadow:
0 14px 42px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.045);
backdrop-filter: blur(16px);
pointer-events: none;
animation: archify-share-cue-enter 280ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
html[data-preset="blueprint"] .share-chapter-cue {
border-radius: 0.2rem;
background:
linear-gradient(
90deg,
color-mix(in srgb, var(--frontend-fill) 46%, transparent),
transparent 36%
),
var(--toolbar-menu-bg);
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.2);
backdrop-filter: none;
}
html[data-preset="editorial"] .share-chapter-cue {
border-color: var(--panel-border);
background:
linear-gradient(
90deg,
color-mix(in srgb, var(--security-fill) 58%, transparent),
transparent 42%
),
var(--toolbar-menu-bg);
box-shadow: 0 10px 30px color-mix(in srgb, #000 18%, transparent);
backdrop-filter: none;
}
html[data-preset="editorial"] .share-chapter-state {
color: var(--arrow-emphasis);
}
html[data-preset="editorial"] .share-chapter-state::before {
border-radius: 0;
box-shadow: none;
transform: rotate(45deg) scale(0.8);
}
.share-chapter-index {
display: grid;
gap: 0.12rem;
min-width: 5rem;
padding-right: 0.75rem;
border-right: 1px solid var(--toolbar-border);
}
.share-chapter-state {
display: inline-flex;
align-items: center;
gap: 0.36rem;
color: var(--frontend-stroke);
font-size: 0.5rem;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.share-chapter-state::before {
width: 0.38rem;
height: 0.38rem;
border-radius: 50%;
background: currentColor;
box-shadow: 0 0 10px currentColor;
content: "";
}
.share-chapter-cue[data-state="playing"] .share-chapter-state::before {
animation: archify-share-cue-pulse 1.15s ease-in-out infinite;
}
.share-chapter-count {
color: var(--text-faint);
font-size: 0.5rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.share-chapter-copy {
min-width: 0;
}
.share-chapter-copy strong,
.share-chapter-copy small,
.share-chapter-route {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.share-chapter-copy strong {
color: var(--toolbar-text);
font-size: 0.6875rem;
line-height: 1.35;
}
.share-chapter-copy small {
margin-top: 0.08rem;
color: var(--text-muted);
font-size: 0.5rem;
line-height: 1.35;
}
.share-chapter-route {
color: color-mix(in srgb, var(--frontend-stroke) 76%, var(--text-muted));
font-size: 0.5625rem;
font-weight: 650;
letter-spacing: 0.015em;
text-align: right;
}
.share-chapter-progress {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 2px;
overflow: hidden;
background: color-mix(in srgb, var(--toolbar-border) 72%, transparent);
}
.share-chapter-progress span {
display: block;
width: 100%;
height: 100%;
background: linear-gradient(90deg, var(--frontend-stroke), var(--database-stroke));
box-shadow: 0 0 12px var(--frontend-stroke);
transform: scaleX(0);
transform-origin: left center;
}
/* Presentation Stage is a viewer-only layer: it gives the live diagram
the viewport while keeping guided views, theme, focus, and exports real.
Embed mode wins when both query parameters are present. */
html[data-present="true"]:not([data-embed="true"]) body {
height: 100dvh;
min-height: 0;
padding: 1rem;
overflow: hidden;
}
html[data-present="true"]:not([data-embed="true"]) .container {
display: flex;
flex-direction: column;
gap: 0.75rem;
width: 100%;
max-width: none;
height: calc(100dvh - 2rem);
}
html[data-present="true"]:not([data-embed="true"]) .header {
flex: none;
min-width: 0;
margin: 0;
padding-right: 23rem;
}
html[data-present="true"]:not([data-embed="true"]) .header-row {
margin-bottom: 0.25rem;
}
html[data-present="true"]:not([data-embed="true"]) .subtitle {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
html[data-present="true"]:not([data-embed="true"]) .guided-views {
flex: none;
margin-bottom: 0;
}
html[data-present="true"]:not([data-embed="true"]) .diagram-container {
display: flex;
flex: 1 1 auto;
align-items: center;
justify-content: center;
min-height: 0;
padding: 0.75rem;
padding-bottom: calc(0.75rem + var(--archify-nav-reserve));
}
html[data-present="true"]:not([data-embed="true"]) .diagram-container > svg {
flex: 1 1 auto;
height: 100%;
min-height: 0;
width: 100%;
min-width: 0;
}
html[data-present="true"]:not([data-embed="true"]) .cards {
display: none;
}
html[data-present="true"]:not([data-embed="true"]) #btn-present {
border-color: var(--frontend-stroke);
color: var(--frontend-stroke);
}
/* The reader owns only the outer scale. Wide diagrams keep one authored
SVG/viewBox while the runtime chooses a desktop width from the actual
vertical budget. This avoids device-specific files and breakpoint
jumps between a laptop and a tall monitor. */
.container {
width: 100%;
max-width: var(--archify-reader-width, 1440px);
margin: 0 auto;
}
@media (min-width: 768px) and (max-height: 920px) {
.header {
margin-bottom: 1rem;
}
.diagram-container {
padding: 1rem;
}
.cards {
margin-top: 1rem;
}
.card {
padding: 1rem;
}
}
.header {
margin-bottom: 1.5rem;
padding-right: 29.5rem;
}
html[data-preset="signal-flow"] .header,
html[data-preset="blueprint"] .header,
html[data-preset="editorial"] .header {
padding-right: 29.5rem;
}
.header-row {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 0.5rem;
}
.pulse-dot {
width: 12px;
height: 12px;
background: var(--frontend-stroke);
border-radius: 50%;
animation: none;
}
html[data-motion-capable="true"] .pulse-dot {
animation: pulse 2s infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
h1 {
font-size: 1.5rem;
font-weight: 700;
letter-spacing: -0.025em;
}
.subtitle {
color: var(--text-muted);
font-size: 0.875rem;
margin-left: 1.75rem;
}
html[data-preset="signal-flow"] .header-row::after {
content: attr(data-preset-badge-signal-flow);
margin-left: auto;
color: var(--frontend-stroke);
border: 1px solid var(--frontend-stroke);
border-radius: 999px;
padding: 0.28rem 0.6rem;
font-size: 0.625rem;
letter-spacing: 0.12em;
opacity: 0.82;
box-shadow: 0 0 20px rgba(34, 211, 238, 0.12);
white-space: nowrap;
}
html[data-preset="blueprint"] .pulse-dot {
width: 10px;
height: 10px;
border: 1px solid var(--frontend-stroke);
border-radius: 1px;
background: transparent;
box-shadow: inset 0 0 0 2px var(--bg);
transform: rotate(45deg);
animation: none;
}
html[data-preset="blueprint"] .header-row::after {
content: attr(data-preset-badge-blueprint);
margin-left: auto;
padding: 0.28rem 0.55rem;
border-top: 1px solid var(--frontend-stroke);
border-bottom: 1px solid var(--frontend-stroke);
color: var(--frontend-stroke);
font-size: 0.5625rem;
font-weight: 700;
letter-spacing: 0.14em;
white-space: nowrap;
}
html[data-preset="editorial"] h1,
html[data-preset="editorial"] .card h3 {
font-family: Georgia, "Times New Roman", "Songti SC", STSong, serif;
letter-spacing: -0.02em;
}
html[data-preset="editorial"] h1 {
font-size: 1.72rem;
font-weight: 600;
}
html[data-preset="editorial"] .pulse-dot {
width: 9px;
height: 9px;
border: 1px solid var(--arrow-emphasis);
border-radius: 0;
background: var(--arrow-emphasis);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--arrow-emphasis) 14%, transparent);
transform: rotate(45deg);
animation: none;
}
html[data-preset="editorial"] .header-row::after {
content: attr(data-preset-badge-editorial);
margin-left: auto;
padding: 0.32rem 0 0.24rem;
border-bottom: 2px solid var(--arrow-emphasis);
color: var(--arrow-emphasis);
font-family: Georgia, "Times New Roman", serif;
font-size: 0.625rem;
font-style: italic;
font-weight: 600;
letter-spacing: 0.08em;
white-space: nowrap;
}
.diagram-container {
--archify-nav-reserve: 0px;
background: var(--panel);
border-radius: 1rem;
border: 1px solid var(--panel-border);
padding: 1.5rem;
padding-bottom: calc(1.5rem + var(--archify-nav-reserve));
overflow: hidden;
position: relative;
}
html[data-preset="signal-flow"] .diagram-container {
position: relative;
overflow: hidden;
background: linear-gradient(180deg, rgba(8, 20, 38, 0.82), rgba(3, 8, 18, 0.94));
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.04),
0 28px 80px rgba(0, 0, 0, 0.34);
}
html[data-preset="signal-flow"][data-theme="light"] .diagram-container {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(237, 246, 250, 0.94));
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.72),
0 24px 60px rgba(45, 78, 99, 0.12);
}
html[data-preset="blueprint"] .diagram-container {
border-color: var(--panel-border);
border-radius: 0.35rem;
background: var(--panel);
box-shadow:
inset 0 0 0 1px color-mix(in srgb, var(--panel-border) 34%, transparent),
0 18px 48px rgba(0, 0, 0, 0.16);
}
html[data-preset="blueprint"] .diagram-container::after {
content: "";
position: absolute;
z-index: 10;
inset: 0.7rem;
pointer-events: none;
background:
linear-gradient(var(--frontend-stroke), var(--frontend-stroke)) left top / 1.35rem 1px
no-repeat,
linear-gradient(var(--frontend-stroke), var(--frontend-stroke)) left top / 1px 1.35rem
no-repeat,
linear-gradient(var(--frontend-stroke), var(--frontend-stroke)) right top / 1.35rem 1px
no-repeat,
linear-gradient(var(--frontend-stroke), var(--frontend-stroke)) right top / 1px 1.35rem
no-repeat,
linear-gradient(var(--frontend-stroke), var(--frontend-stroke)) left bottom / 1.35rem 1px
no-repeat,
linear-gradient(var(--frontend-stroke), var(--frontend-stroke)) left bottom / 1px 1.35rem
no-repeat,
linear-gradient(var(--frontend-stroke), var(--frontend-stroke)) right bottom / 1.35rem 1px
no-repeat,
linear-gradient(var(--frontend-stroke), var(--frontend-stroke)) right bottom / 1px 1.35rem
no-repeat;
opacity: 0.58;
}
html[data-preset="editorial"] .diagram-container {
overflow: hidden;
border-color: var(--panel-border);
border-radius: 0.38rem;
background:
linear-gradient(
90deg,
color-mix(in srgb, var(--security-fill) 58%, transparent),
transparent 12%
),
var(--panel);
box-shadow:
0 1px 0 color-mix(in srgb, var(--text) 6%, transparent),
0 18px 48px color-mix(in srgb, #000 19%, transparent);
}
html[data-preset="editorial"] .diagram-container::after {
position: absolute;
z-index: 10;
top: 0.72rem;
right: 1.25rem;
padding: 0.2rem 0.52rem;
border: 1px solid var(--panel-border);
background: var(--panel);
color: var(--arrow-emphasis);
content: attr(data-preset-badge-editorial-plate);
font-family: Georgia, "Times New Roman", serif;
font-size: 0.52rem;
font-style: italic;
font-weight: 600;
letter-spacing: 0.09em;
pointer-events: none;
}
html[data-embed="true"][data-preset="editorial"] .diagram-container::after {
display: none;
}
html[data-motion-capable="true"][data-preset="signal-flow"][data-ambient-motion="running"]
.diagram-container::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(
115deg,
transparent 28%,
rgba(103, 232, 249, 0.035) 48%,
transparent 68%
);
transform: translateX(-70%);
animation: archify-signal-scan 4.8s ease-in-out 1 both;
}
/* Motion Governor gives the reader a real static state and gives the
strongest semantic action the only motion budget. Atmosphere resets to
its authored base instead of freezing mid-cycle. Static artifacts never
start a decorative pulse or scan. */
html:not([data-motion-capable="true"]) svg[data-animation="trace"] [data-animate] {
animation: none !important;
}
html[data-motion="still"] .pulse-dot,
html[data-motion="still"] .diagram-container::before,
html[data-motion="still"] svg[data-animation="trace"] [data-animate],
html[data-motion-owner] .pulse-dot,
html[data-motion-owner] .diagram-container::before,
html[data-motion-owner] svg[data-animation="trace"] [data-animate],
html[data-embed="true"] .pulse-dot,
html[data-embed="true"] .diagram-container::before,
html[data-share-playback="true"] .pulse-dot,
html[data-share-playback="true"] .diagram-container::before,
html[data-document-hidden="true"] .pulse-dot,
html[data-document-hidden="true"] .diagram-container::before,
html[data-document-hidden="true"] svg[data-animation="trace"] [data-animate] {
animation: none !important;
}
html[data-motion="still"] .diagram-container::before,
html[data-motion-owner] .diagram-container::before,
html[data-embed="true"] .diagram-container::before,
html[data-share-playback="true"] .diagram-container::before {
opacity: 0;
}
html[data-motion="still"] .guided-view-progress span,
html[data-motion="still"] .guided-story-caption,
html[data-motion="still"] .story-trail-flow,
html[data-motion="still"] .intent-trace-flow,
html[data-motion="still"] .route-probe-flow,
html[data-motion="still"] .route-journey-flow,
html[data-motion="still"] .semantic-lens-flow,
html[data-motion="still"] .diagram-guide,
html[data-motion="still"] [data-story-step],
html[data-motion="still"] .overview-map-live,
html[data-motion="still"] .guided-view-stop,
html[data-motion="still"] .share-chapter-cue,
html[data-motion="still"] .share-chapter-state::before,
html[data-motion="still"] .share-chapter-progress span,
html[data-document-hidden="true"] .guided-view-progress span,
html[data-document-hidden="true"] .story-trail-flow,
html[data-document-hidden="true"] .intent-trace-flow,
html[data-document-hidden="true"] .route-probe-flow,
html[data-document-hidden="true"] .route-journey-flow,
html[data-document-hidden="true"] .semantic-lens-flow,
html[data-document-hidden="true"] .diagram-guide,
html[data-document-hidden="true"] [data-story-step],
html[data-document-hidden="true"] .overview-map-live,
html[data-document-hidden="true"] .guided-view-stop,
html[data-document-hidden="true"] .share-chapter-cue,
html[data-document-hidden="true"] .share-chapter-state::before,
html[data-document-hidden="true"] .share-chapter-progress span {
animation: none !important;
}
html[data-motion="still"] .guided-view-progress span {
transform: scaleX(1) !important;
}
html[data-motion="still"] svg[data-story-playing="true"] .story-trail-flow {
stroke-dasharray: none;
stroke-dashoffset: 0;
opacity: 0.55;
}
html[data-motion="still"] .intent-trace-flow,
html[data-motion="still"] .route-probe-flow {
stroke-dasharray: none;
stroke-dashoffset: 0;
opacity: 0.72;
}
html[data-motion="still"] .route-journey-overlay,
html[data-document-hidden="true"] .route-journey-overlay {
display: none !important;
}
html[data-motion="still"] .relationship-pulse-overlay,
html[data-motion="still"] .story-carrier-overlay,
html[data-document-hidden="true"] .story-carrier-overlay {
display: none !important;
}
html[data-motion="still"] .chapter-handoff-overlay {
display: none !important;
}
html[data-motion="still"] .chapter-handoff-anchor {
animation: none !important;
}
html[data-motion="still"] .guided-view-chapter,
html[data-motion="still"] svg [data-node-id],
html[data-motion="still"] svg [data-edge-from],
html[data-motion="still"] svg [data-detail],
html[data-motion="still"] svg [data-detail-anchor],
html[data-motion="still"] svg [data-legend-hit],
html[data-motion="still"] svg {
transition: none !important;
}
svg {
width: 100%;
min-width: min(900px, 100%);
display: block;
transform-origin: 0 0;
transition: transform 0.18s ease;
}
.diagram-container.is-camera-moving svg {
transition: transform 0.42s cubic-bezier(0.22, 1, 0.36, 1);
}
.diagram-container.is-camera-transaction svg {
transition: none;
}
.diagram-container.is-pannable {
cursor: grab;
}
.diagram-container.is-panning {
cursor: grabbing;
user-select: none;
}
.diagram-container.is-panning svg {
transition: none;
}
.diagram-nav {
position: absolute;
right: 1rem;
bottom: 1rem;
z-index: 12;
display: inline-flex;
align-items: center;
max-width: calc(100% - 2rem);
padding: 0.15rem;
gap: 0;
overflow-x: auto;
border: 1px solid color-mix(in srgb, var(--toolbar-border) 48%, transparent);
border-radius: 0.58rem;
background: color-mix(in srgb, var(--toolbar-bg) 82%, transparent);
box-shadow: 0 5px 14px rgba(0, 0, 0, 0.07);
backdrop-filter: blur(10px);
scrollbar-width: none;
}
.diagram-nav::-webkit-scrollbar {
display: none;
}
.diagram-nav button,
.focus-chip button,
.guided-views button {
border: 0;
background: transparent;
color: var(--toolbar-text);
font: inherit;
cursor: pointer;
}
.diagram-nav button {
min-width: 2rem;
height: 2rem;
padding: 0 0.35rem;
border-radius: 0.35rem;
color: color-mix(in srgb, var(--toolbar-text) 68%, transparent);
font-size: 0.56rem;
font-weight: 600;
letter-spacing: 0.02em;
transition:
background 0.15s,
color 0.15s;
}
.diagram-nav button:hover,
.diagram-nav button:focus-visible,
.focus-chip button:hover,
.focus-chip button:focus-visible,
.guided-views button:hover,
.guided-views button:focus-visible {
background: var(--toolbar-hover);
}
.diagram-nav button:focus-visible,
.focus-chip button:focus-visible,
.guided-views button:focus-visible {
outline: 2px solid var(--frontend-stroke);
outline-offset: 1px;
}
.diagram-nav button:disabled {
opacity: 0.38;
cursor: default;
}
.diagram-nav button:disabled:hover {
background: transparent;
}
.diagram-nav #btn-route-probe,
.diagram-nav #btn-overview-map,
.diagram-nav #btn-semantic-lens {
min-width: auto;
padding-inline: 0.5rem;
color: color-mix(in srgb, var(--toolbar-text) 86%, transparent);
font-weight: 650;
}
.diagram-nav #btn-node-finder,
.diagram-nav [data-view="out"] {
position: relative;
margin-left: 0.25rem;
}
.diagram-nav #btn-node-finder::before,
.diagram-nav [data-view="out"]::before {
position: absolute;
top: 0.4rem;
bottom: 0.4rem;
left: -0.13rem;
width: 1px;
background: color-mix(in srgb, var(--toolbar-border) 62%, transparent);
content: "";
pointer-events: none;
}
.diagram-nav-icon {
display: block;
width: 0.72rem;
height: 0.72rem;
margin: auto;
background: currentColor;
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: contain;
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
}
.diagram-nav-icon.find {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Ccircle cx='8.5' cy='8.5' r='4.5' fill='none' stroke='black' stroke-width='1.8'/%3E%3Cpath d='m12 12 4 4' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Ccircle cx='8.5' cy='8.5' r='4.5' fill='none' stroke='black' stroke-width='1.8'/%3E%3Cpath d='m12 12 4 4' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");
}
.diagram-nav-icon.guide {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Ccircle cx='10' cy='10' r='7' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M7.8 7.3a2.4 2.4 0 014.6.9c0 1.8-2.4 2-2.4 3.7M10 14.9h.01' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Ccircle cx='10' cy='10' r='7' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M7.8 7.3a2.4 2.4 0 014.6.9c0 1.8-2.4 2-2.4 3.7M10 14.9h.01' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round'/%3E%3C/svg%3E");
}
.diagram-nav-icon.minus {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M5 10h10' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M5 10h10' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");
}
.diagram-nav-icon.plus {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 5v10M5 10h10' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 5v10M5 10h10' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");
}
.diagram-nav [data-view="reset"] {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.2rem;
min-width: 3.1rem;
padding-inline: 0.35rem;
}
.diagram-nav [data-view="reset"][data-detail-visible] {
min-width: 3.8rem;
}
.diagram-nav-detail {
color: color-mix(in srgb, var(--toolbar-text) 52%, transparent);
font-size: 0.44rem;
font-weight: 700;
line-height: 1;
letter-spacing: 0.07em;
}
.diagram-nav-percent {
color: color-mix(in srgb, var(--toolbar-text) 84%, transparent);
font-size: 0.55rem;
font-weight: 650;
line-height: 1;
}
.diagram-container[data-camera-indicator="true"] .diagram-nav-detail,
.diagram-container[data-camera-indicator="true"] .diagram-nav-percent {
color: var(--frontend-stroke);
}
.diagram-nav #btn-diagram-guide[aria-expanded="true"] {
color: var(--cloud-stroke);
background: var(--toolbar-hover);
}
/* Semantic Radar is a deliberately simplified, viewer-only overview. It
uses authored node bounds and the live camera viewport rather than
cloning the canonical SVG, so IDs, filters, animation, and export state
stay singular and deterministic. */
.overview-map {
position: absolute;
right: 1rem;
bottom: calc(4.15rem + var(--archify-nav-reserve));
z-index: 12;
width: 11.25rem;
overflow: hidden;
border: 1px solid color-mix(in srgb, var(--frontend-stroke) 52%, var(--toolbar-border));
border-radius: 0.78rem;
background: var(--toolbar-menu-bg);
color: var(--toolbar-text);
box-shadow: 0 16px 42px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(16px);
font-size: 0.625rem;
}
.overview-map[hidden] {
display: none;
}
.overview-map[data-docked="true"] {
position: fixed;
right: var(--archify-radar-right, 1rem);
top: var(--archify-radar-top, 1rem);
bottom: auto;
}
.overview-map[data-docked="true"][data-dock-side="left"] {
right: auto;
left: var(--archify-radar-left, 1rem);
}
html[data-preset="blueprint"] .overview-map {
border-radius: 0.28rem;
}
.overview-map-head {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 0.45rem;
min-height: 2.25rem;
padding: 0.42rem 0.48rem 0.38rem 0.55rem;
border-bottom: 1px solid var(--toolbar-border);
background: linear-gradient(
110deg,
color-mix(in srgb, var(--frontend-fill) 45%, transparent),
transparent 68%
);
cursor: grab;
touch-action: none;
user-select: none;
}
.overview-map[data-panel-dragging="true"] .overview-map-head {
cursor: grabbing;
}
.overview-map[data-placement-invalid="true"] {
border-color: var(--security-stroke);
box-shadow:
0 0 0 2px color-mix(in srgb, var(--security-stroke) 34%, transparent),
0 16px 42px rgba(0, 0, 0, 0.3);
}
.overview-map[data-compact="true"] {
width: 7.4rem;
}
.overview-map[data-compact="true"] .overview-map-head {
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.24rem;
min-height: 1.5rem;
padding: 0.05rem 0.16rem 0.05rem 0.32rem;
border-bottom: 0;
}
.overview-map[data-compact="true"] .overview-map-live,
.overview-map[data-compact="true"] .overview-map-copy,
.overview-map[data-compact="true"] .overview-map-surface,
.overview-map[data-compact="true"] .overview-map-hint {
display: none;
}
.overview-map[data-compact="true"] .overview-map-expand {
display: inline-flex;
}
.overview-map[data-compact="true"] .overview-map-close {
width: 1.15rem;
height: 1.15rem;
font-size: 0.72rem;
}
.overview-map-live {
width: 0.42rem;
height: 0.42rem;
border-radius: 50%;
background: var(--backend-stroke);
box-shadow: 0 0 0 0 color-mix(in srgb, var(--backend-stroke) 52%, transparent);
animation: archify-radar-live 2.4s ease-out infinite;
}
.overview-map-copy {
min-width: 0;
line-height: 1.1;
}
.overview-map-copy strong {
display: block;
overflow: hidden;
color: var(--frontend-stroke);
font-size: 0.59rem;
letter-spacing: 0.12em;
text-transform: uppercase;
white-space: nowrap;
text-overflow: ellipsis;
}
.overview-map-copy small {
display: block;
margin-top: 0.18rem;
overflow: hidden;
color: var(--text-faint);
font-size: 0.52rem;
letter-spacing: 0.04em;
white-space: nowrap;
text-overflow: ellipsis;
}
.overview-map-expand {
display: none;
align-items: center;
justify-content: center;
min-width: 0;
height: 1.2rem;
padding: 0 0.34rem;
overflow: hidden;
border: 0;
border-radius: 0.3rem;
background: transparent;
color: var(--frontend-stroke);
font: inherit;
font-size: 0.49rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
white-space: nowrap;
cursor: pointer;
}
.overview-map-close {
width: 1.55rem;
height: 1.55rem;
padding: 0;
border: 0;
border-radius: 0.34rem;
background: transparent;
color: var(--toolbar-text);
font: inherit;
font-size: 0.9rem;
cursor: pointer;
}
.overview-map-expand:hover,
.overview-map-expand:focus-visible,
.overview-map-close:hover,
.overview-map-close:focus-visible {
background: var(--toolbar-hover);
}
.overview-map-expand:focus-visible,
.overview-map-close:focus-visible,
.overview-map-surface:focus-visible,
.overview-map-node:focus-visible {
outline: 2px solid var(--frontend-stroke);
outline-offset: 1px;
}
.overview-map-surface {
position: relative;
height: 7rem;
margin: 0.42rem;
overflow: hidden;
border: 1px solid color-mix(in srgb, var(--panel-border) 72%, transparent);
border-radius: 0.42rem;
background:
linear-gradient(
color-mix(in srgb, var(--panel-border) 26%, transparent) 1px,
transparent 1px
),
linear-gradient(
90deg,
color-mix(in srgb, var(--panel-border) 26%, transparent) 1px,
transparent 1px
),
color-mix(in srgb, var(--bg) 86%, transparent);
background-size: 12px 12px;
cursor: crosshair;
touch-action: none;
}
html[data-preset="blueprint"] .overview-map-surface {
border-radius: 0.1rem;
}
.overview-map-surface > svg {
width: 100%;
height: 100%;
min-width: 0;
transform: none !important;
transition: none !important;
display: block;
overflow: visible;
}
.overview-map-node {
fill: var(--backend-stroke);
fill-opacity: 0.52;
stroke: var(--backend-stroke);
stroke-opacity: 0.86;
vector-effect: non-scaling-stroke;
cursor: pointer;
transition:
fill-opacity 140ms ease,
stroke-width 140ms ease,
filter 140ms ease;
}
.overview-map-node[data-kind="frontend"],
.overview-map-node[data-kind="start"] {
fill: var(--frontend-stroke);
stroke: var(--frontend-stroke);
}
.overview-map-node[data-kind="database"],
.overview-map-node[data-kind="success"] {
fill: var(--database-stroke);
stroke: var(--database-stroke);
}
.overview-map-node[data-kind="cloud"],
.overview-map-node[data-kind="waiting"] {
fill: var(--cloud-stroke);
stroke: var(--cloud-stroke);
}
.overview-map-node[data-kind="messagebus"] {
fill: var(--messagebus-stroke);
stroke: var(--messagebus-stroke);
}
.overview-map-node[data-kind="security"],
.overview-map-node[data-kind="decision"],
.overview-map-node[data-kind="failure"] {
fill: var(--security-stroke);
stroke: var(--security-stroke);
}
.overview-map-node[data-kind="external"],
.overview-map-node[data-kind="neutral"] {
fill: var(--external-stroke);
stroke: var(--external-stroke);
}
.overview-map-node[data-radar-active="true"],
.overview-map-node:hover,
.overview-map-node:focus-visible {
fill-opacity: 0.92;
stroke-width: 2.2;
filter: drop-shadow(0 0 3px currentColor);
}
.overview-map-viewport {
fill: color-mix(in srgb, var(--frontend-fill) 56%, transparent);
stroke: var(--frontend-stroke);
stroke-width: 2;
vector-effect: non-scaling-stroke;
pointer-events: none;
filter: drop-shadow(0 0 3px color-mix(in srgb, var(--frontend-stroke) 54%, transparent));
}
.overview-map-hint {
display: flex;
justify-content: space-between;
gap: 0.5rem;
padding: 0 0.52rem 0.46rem;
color: var(--text-faint);
font-size: 0.49rem;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.diagram-nav #btn-overview-map[aria-expanded="true"] {
color: var(--frontend-stroke);
background: var(--toolbar-hover);
}
.overview-map-feedback {
position: absolute;
right: 1rem;
bottom: 4.15rem;
z-index: 13;
max-width: min(16rem, calc(100% - 2rem));
padding: 0.38rem 0.55rem;
border: 1px solid color-mix(in srgb, var(--frontend-stroke) 52%, var(--toolbar-border));
border-radius: 0.5rem;
background: var(--toolbar-menu-bg);
color: var(--toolbar-text);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
font-size: 0.52rem;
line-height: 1.35;
pointer-events: none;
}
.overview-map-feedback[hidden] {
display: none;
}
.focus-chip[data-radar-yielded="true"] {
display: none !important;
}
.diagram-nav #btn-route-probe[aria-pressed="true"] {
color: var(--backend-stroke);
background: var(--toolbar-hover);
}
.diagram-nav #btn-semantic-lens[aria-expanded="true"],
.diagram-nav #btn-semantic-lens[aria-pressed="true"] {
color: var(--database-stroke);
background: var(--toolbar-hover);
}
/* Semantic Lens turns the compiled node-kind palette into an analytical
legend. One kind reveals its real traffic; two kinds compare only their
direct authored relationships. Geometry and the canonical SVG stay put. */
.semantic-lens {
position: absolute;
right: 1rem;
bottom: calc(4.15rem + var(--archify-nav-reserve));
z-index: 24;
width: min(20rem, calc(100% - 2rem));
overflow: hidden;
border: 1px solid color-mix(in srgb, var(--database-stroke) 58%, var(--toolbar-border));
border-radius: 0.82rem;
background:
linear-gradient(
135deg,
color-mix(in srgb, var(--database-fill) 34%, transparent),
transparent 48%
),
var(--toolbar-menu-bg);
color: var(--toolbar-text);
box-shadow: 0 18px 52px rgba(0, 0, 0, 0.36);
backdrop-filter: blur(18px);
animation: archify-lens-in 180ms cubic-bezier(0.22, 1, 0.36, 1);
}
.semantic-lens[hidden] {
display: none;
}
.semantic-lens[data-dock-side="left"] {
left: 1rem;
right: auto;
}
.semantic-lens[data-dock-side="right"] {
left: auto;
right: 1rem;
}
html[data-preset="blueprint"] .semantic-lens {
border-radius: 0.28rem;
}
.semantic-lens-head {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: start;
gap: 0.75rem;
padding: 0.72rem 0.78rem 0.62rem;
border-bottom: 1px solid var(--toolbar-border);
}
.semantic-lens-eyebrow {
display: block;
color: var(--database-stroke);
font-size: 0.5rem;
font-weight: 800;
letter-spacing: 0.16em;
text-transform: uppercase;
}
.semantic-lens-title {
display: block;
margin-top: 0.16rem;
color: var(--text);
font-size: 0.82rem;
letter-spacing: -0.02em;
}
.semantic-lens-close {
width: 1.75rem;
height: 1.75rem;
padding: 0;
border: 0;
border-radius: 0.35rem;
background: transparent;
color: var(--toolbar-text);
font: inherit;
font-size: 1rem;
cursor: pointer;
}
.semantic-lens-close:hover,
.semantic-lens-close:focus-visible {
background: var(--toolbar-hover);
}
.semantic-lens-instruction {
margin: 0;
padding: 0.52rem 0.78rem 0.48rem;
color: var(--text-faint);
font-size: 0.55rem;
line-height: 1.45;
}
.semantic-lens-kinds {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.3rem;
max-height: min(15rem, 40vh);
padding: 0 0.5rem 0.5rem;
overflow-y: auto;
scrollbar-width: thin;
}
.semantic-lens-kind {
--lens-color: var(--external-stroke);
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 0.42rem;
min-width: 0;
min-height: 2.15rem;
padding: 0.35rem 0.42rem;
border: 1px solid color-mix(in srgb, var(--lens-color) 24%, var(--toolbar-border));
border-radius: 0.45rem;
background: color-mix(in srgb, var(--panel) 76%, transparent);
color: var(--toolbar-text);
font: inherit;
text-align: left;
cursor: pointer;
transition:
border-color 140ms ease,
background 140ms ease,
opacity 140ms ease;
}
html[data-preset="blueprint"] .semantic-lens-kind {
border-radius: 0.12rem;
}
.semantic-lens-kind[data-kind="frontend"],
.semantic-lens-kind[data-kind="start"] {
--lens-color: var(--frontend-stroke);
}
.semantic-lens-kind[data-kind="backend"],
.semantic-lens-kind[data-kind="active"] {
--lens-color: var(--backend-stroke);
}
.semantic-lens-kind[data-kind="database"],
.semantic-lens-kind[data-kind="success"] {
--lens-color: var(--database-stroke);
}
.semantic-lens-kind[data-kind="cloud"],
.semantic-lens-kind[data-kind="waiting"] {
--lens-color: var(--cloud-stroke);
}
.semantic-lens-kind[data-kind="messagebus"] {
--lens-color: var(--messagebus-stroke);
}
.semantic-lens-kind[data-kind="security"],
.semantic-lens-kind[data-kind="decision"],
.semantic-lens-kind[data-kind="failure"] {
--lens-color: var(--security-stroke);
}
.semantic-lens-kind:hover,
.semantic-lens-kind:focus-visible,
.semantic-lens-kind[aria-pressed="true"] {
border-color: color-mix(in srgb, var(--lens-color) 78%, var(--toolbar-border));
background: color-mix(in srgb, var(--lens-color) 11%, var(--panel));
}
.semantic-lens-kind:focus-visible,
.semantic-lens-close:focus-visible,
.semantic-lens-actions button:focus-visible {
outline: 2px solid var(--database-stroke);
outline-offset: 1px;
}
.semantic-lens-kind:disabled {
opacity: 0.34;
cursor: default;
}
.semantic-lens-swatch {
width: 0.48rem;
height: 0.48rem;
border-radius: 50%;
background: var(--lens-color);
box-shadow: 0 0 8px color-mix(in srgb, var(--lens-color) 58%, transparent);
}
.semantic-lens-kind strong {
overflow: hidden;
color: var(--text);
font-size: 0.61rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.semantic-lens-kind em {
color: var(--lens-color);
font-size: 0.54rem;
font-style: normal;
font-weight: 800;
}
.semantic-lens-status {
min-height: 2.5rem;
margin: 0;
padding: 0.55rem 0.78rem;
border-top: 1px solid var(--toolbar-border);
color: var(--text-faint);
font-size: 0.55rem;
line-height: 1.45;
}
.semantic-lens-status strong {
color: var(--database-stroke);
font-weight: 800;
}
.semantic-lens-actions {
display: flex;
justify-content: flex-end;
gap: 0.28rem;
padding: 0 0.62rem 0.62rem;
}
.semantic-lens-actions button {
min-height: 1.8rem;
padding: 0.28rem 0.48rem;
border: 0;
border-radius: 0.35rem;
background: transparent;
color: var(--toolbar-text);
font: inherit;
font-size: 0.56rem;
cursor: pointer;
}
.semantic-lens-actions button:hover {
background: var(--toolbar-hover);
}
.semantic-lens-actions #semantic-lens-copy {
color: var(--database-stroke);
}
@keyframes archify-lens-in {
from {
opacity: 0;
transform: translateY(5px) scale(0.985);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* Route Probe answers a reader question over the compiled semantic graph:
choose two stable nodes and reveal the shortest authored directed route.
The receipt and signal live entirely in the viewer layer. */
.route-probe {
position: absolute;
left: 1rem;
bottom: calc(1rem + var(--archify-nav-reserve));
z-index: 12;
width: min(46rem, calc(100% - 25rem));
min-width: 18rem;
overflow: hidden;
border: 1px solid color-mix(in srgb, var(--backend-stroke) 62%, var(--toolbar-border));
border-radius: 0.82rem;
background:
linear-gradient(
115deg,
color-mix(in srgb, var(--backend-fill) 50%, transparent),
transparent 62%
),
var(--toolbar-menu-bg);
color: var(--toolbar-text);
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(16px);
font-size: 0.625rem;
}
.route-probe[hidden] {
display: none;
}
.route-probe[data-route-dock="top"] {
top: 1rem;
bottom: auto;
}
html[data-preset="blueprint"] .route-probe {
border-radius: 0.25rem;
}
.route-probe-head {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: start;
gap: 0.65rem;
padding: 0.52rem 0.58rem 0.44rem;
border-bottom: 1px solid color-mix(in srgb, var(--toolbar-border) 80%, transparent);
}
.route-probe-copy {
min-width: 0;
}
.route-probe-eyebrow {
display: block;
color: var(--backend-stroke);
font-size: 0.48rem;
font-weight: 800;
letter-spacing: 0.15em;
text-transform: uppercase;
}
.route-probe-title {
display: block;
margin-top: 0.14rem;
overflow: hidden;
color: var(--toolbar-text);
font-size: 0.72rem;
line-height: 1.25;
white-space: nowrap;
text-overflow: ellipsis;
}
.route-probe-actions {
display: flex;
gap: 0.2rem;
}
.route-probe button {
min-height: 1.55rem;
padding: 0.25rem 0.42rem;
border: 0;
border-radius: 0.36rem;
background: transparent;
color: var(--toolbar-text);
font: inherit;
cursor: pointer;
}
.route-probe button:hover,
.route-probe button:focus-visible {
background: var(--toolbar-hover);
}
.route-probe button:focus-visible {
outline: 2px solid var(--backend-stroke);
outline-offset: 1px;
}
.route-probe-copy-link {
color: var(--frontend-stroke) !important;
}
.route-probe-find {
color: var(--backend-stroke) !important;
}
.route-probe-copy-link[hidden] {
display: none;
}
.route-probe-find[hidden] {
display: none;
}
.route-probe-path {
display: flex;
align-items: center;
min-height: 2.2rem;
padding: 0.42rem 0.58rem 0.24rem;
overflow-x: auto;
scrollbar-width: thin;
white-space: nowrap;
}
.route-probe-node,
.route-probe button.route-probe-node {
flex: none;
max-width: 9.5rem;
overflow: hidden;
min-height: 1.75rem !important;
padding: 0.22rem 0.38rem;
border: 1px solid color-mix(in srgb, var(--backend-stroke) 46%, var(--toolbar-border));
border-radius: 999px;
background: color-mix(in srgb, var(--backend-fill) 52%, transparent);
color: var(--text);
font-size: 0.56rem;
font-weight: 700;
text-overflow: ellipsis;
white-space: nowrap;
}
.route-probe-node[data-endpoint="start"] {
border-color: var(--frontend-stroke);
color: var(--frontend-stroke);
}
.route-probe-node[data-endpoint="end"] {
border-color: var(--security-stroke);
color: var(--security-stroke);
}
.route-probe-node[data-route-journey-state="past"] {
opacity: 0.66;
}
.route-probe-node[data-route-journey-state="future"] {
opacity: 0.48;
}
.route-probe-node[aria-current="step"] {
border-color: var(--backend-stroke);
background: color-mix(in srgb, var(--backend-fill) 82%, var(--toolbar-menu-bg));
color: var(--text);
box-shadow:
0 0 0 1px color-mix(in srgb, var(--backend-stroke) 42%, transparent),
0 0 14px color-mix(in srgb, var(--backend-stroke) 34%, transparent);
opacity: 1;
}
html[data-preset="blueprint"] .route-probe-node[aria-current="step"] {
border-style: double;
border-width: 3px;
border-radius: 0.2rem;
box-shadow: none;
}
.route-probe-arrow {
flex: none;
padding: 0 0.28rem;
color: var(--text-faint);
font-size: 0.65rem;
}
.route-probe-placeholder {
color: var(--text-faint);
font-size: 0.56rem;
letter-spacing: 0.03em;
}
.route-probe-status {
display: block;
min-height: 1.45rem;
padding: 0.08rem 0.58rem 0.44rem;
color: var(--text-faint);
font-size: 0.52rem;
line-height: 1.35;
}
.route-journey-controls {
display: grid;
grid-template-columns: 2rem minmax(5.8rem, auto) 2rem minmax(4.8rem, auto);
justify-content: start;
gap: 0.22rem;
padding: 0.08rem 0.58rem 0.28rem;
}
.route-journey-controls[hidden] {
display: none;
}
.route-journey-controls button {
min-height: 1.8rem;
border: 1px solid color-mix(in srgb, var(--toolbar-border) 86%, transparent);
background: color-mix(in srgb, var(--toolbar-bg) 72%, transparent);
font-size: 0.54rem;
font-weight: 700;
}
.route-journey-controls button:disabled {
cursor: default;
opacity: 0.34;
}
.route-journey-play {
color: var(--backend-stroke) !important;
}
.route-journey-overview {
color: var(--frontend-stroke) !important;
}
.route-journey-controls[data-playing="true"] .route-journey-play {
border-color: color-mix(in srgb, var(--backend-stroke) 64%, var(--toolbar-border));
background: color-mix(in srgb, var(--backend-fill) 62%, transparent);
}
.route-probe[data-state="error"] {
border-color: color-mix(in srgb, var(--security-stroke) 72%, var(--toolbar-border));
}
.route-probe[data-state="error"] .route-probe-status {
color: var(--security-stroke);
}
.focus-chip {
position: absolute;
left: 1rem;
top: 1rem;
z-index: 12;
display: block;
width: min(22rem, calc(100% - 13rem));
max-width: calc(100% - 13rem);
overflow: hidden;
border: 1px solid var(--frontend-stroke);
border-radius: 0.85rem;
background: var(--toolbar-menu-bg);
color: var(--toolbar-text);
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(16px);
font-size: 0.6875rem;
}
.focus-chip[hidden] {
display: none;
}
html[data-preset="blueprint"] .focus-chip {
border-radius: 0.3rem;
}
.focus-chip button {
flex: none;
padding: 0.3rem 0.45rem;
border-radius: 0.35rem;
font-size: 0.625rem;
}
.relationship-lens-head {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: start;
gap: 0.75rem;
padding: 0.7rem 0.75rem 0.65rem;
border-bottom: 1px solid var(--toolbar-border);
background: linear-gradient(
120deg,
color-mix(in srgb, var(--frontend-fill) 34%, transparent),
transparent 72%
);
}
.relationship-lens-copy {
min-width: 0;
}
.relationship-lens-actions {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.18rem;
}
.relationship-lens-actions button {
white-space: nowrap;
}
.relationship-lens-actions #btn-focus-clear {
display: grid;
width: 1.75rem;
height: 1.75rem;
padding: 0;
place-items: center;
color: var(--text-muted);
font-size: 1rem;
line-height: 1;
}
.relationship-lens-actions #btn-focus-clear:hover,
.relationship-lens-actions #btn-focus-clear:focus-visible {
background: var(--toolbar-hover);
color: var(--text);
}
.relationship-lens-actions #btn-focus-copy {
color: var(--frontend-stroke);
}
.relationship-lens-actions #btn-focus-relations {
display: none;
}
.relationship-lens-eyebrow {
display: block;
margin-bottom: 0.18rem;
color: var(--frontend-stroke);
font-size: 0.5rem;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.relationship-lens-title {
display: block;
overflow: hidden;
color: var(--text);
font-size: 0.8rem;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}
.semantic-passport-detail {
display: block;
margin-top: 0.12rem;
overflow: hidden;
color: var(--text-muted);
font-size: 0.625rem;
line-height: 1.4;
text-overflow: ellipsis;
white-space: nowrap;
}
.semantic-passport-detail[hidden] {
display: none;
}
.semantic-passport-meta {
display: flex;
flex-wrap: wrap;
gap: 0.24rem;
margin-top: 0.42rem;
}
.semantic-passport-meta span,
.semantic-passport-meta code {
max-width: 100%;
overflow: hidden;
padding: 0.14rem 0.34rem;
border: 1px solid color-mix(in srgb, var(--toolbar-border) 82%, transparent);
border-radius: 999px;
background: color-mix(in srgb, var(--panel) 68%, transparent);
color: var(--text-faint);
font: inherit;
font-size: 0.5rem;
letter-spacing: 0.04em;
text-overflow: ellipsis;
white-space: nowrap;
}
.semantic-passport-meta [data-passport="kind"] {
border-color: color-mix(in srgb, var(--frontend-stroke) 50%, var(--toolbar-border));
color: var(--frontend-stroke);
font-weight: 800;
text-transform: uppercase;
}
.semantic-passport-meta [data-passport="tag"] {
color: var(--cloud-stroke);
}
.semantic-passport-meta [data-passport="brand"] {
border-color: color-mix(in srgb, var(--messagebus-stroke) 52%, var(--toolbar-border));
color: var(--messagebus-stroke);
font-weight: 700;
}
.semantic-passport-meta [hidden] {
display: none;
}
html[data-preset="blueprint"] .semantic-passport-meta span,
html[data-preset="blueprint"] .semantic-passport-meta code {
border-radius: 0.1rem;
}
.semantic-passport-evidence {
margin-top: 0.52rem;
padding: 0.5rem;
border: 1px solid color-mix(in srgb, var(--backend-stroke) 32%, var(--toolbar-border));
border-radius: 0.55rem;
background: color-mix(in srgb, var(--backend-fill) 18%, var(--panel));
}
.semantic-passport-evidence[hidden] {
display: none;
}
html[data-preset="blueprint"] .semantic-passport-evidence {
border-radius: 0.18rem;
}
.semantic-passport-evidence-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.55rem;
margin-bottom: 0.35rem;
}
.semantic-passport-evidence-status {
display: inline-flex;
align-items: center;
gap: 0.28rem;
color: var(--backend-stroke);
font-size: 0.5rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.semantic-passport-evidence-status::before {
content: "";
width: 0.38rem;
height: 0.38rem;
border-radius: 50%;
background: var(--backend-stroke);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--backend-stroke) 14%, transparent);
}
.semantic-passport-repository {
min-width: 0;
overflow: hidden;
color: var(--text-faint);
font-size: 0.5rem;
text-decoration: none;
text-overflow: ellipsis;
white-space: nowrap;
}
.semantic-passport-repository:hover,
.semantic-passport-repository:focus-visible {
color: var(--backend-stroke);
}
.semantic-passport-evidence-links {
display: grid;
gap: 0.22rem;
}
.semantic-passport-source {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.15rem 0.45rem;
align-items: center;
padding: 0.34rem 0.4rem;
border: 1px solid transparent;
border-radius: 0.38rem;
color: var(--text-muted);
text-decoration: none;
}
.semantic-passport-source:hover,
.semantic-passport-source:focus-visible {
border-color: color-mix(in srgb, var(--backend-stroke) 42%, transparent);
background: color-mix(in srgb, var(--backend-fill) 32%, transparent);
color: var(--text);
}
.semantic-passport-source strong {
overflow: hidden;
font-size: 0.5625rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.semantic-passport-source code {
color: var(--backend-stroke);
font: inherit;
font-size: 0.5rem;
}
.semantic-passport-source small {
grid-column: 1 / -1;
overflow: hidden;
color: var(--text-faint);
font-size: 0.5rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.relationship-lens-summary {
display: block;
margin-top: 0.12rem;
color: var(--text-faint);
font-size: 0.5625rem;
}
.semantic-passport-reach {
display: grid;
gap: 0.28rem;
margin-top: 0.52rem;
padding-top: 0.48rem;
border-top: 1px solid color-mix(in srgb, var(--toolbar-border) 76%, transparent);
}
.semantic-passport-reach[hidden] {
display: none;
}
.semantic-passport-reach-label {
color: var(--text-faint);
font-size: 0.5rem;
font-weight: 800;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.semantic-passport-reach-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.25rem;
}
.semantic-passport-reach-actions button {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
min-width: 0;
min-height: 2rem;
padding: 0.32rem 0.42rem;
border: 1px solid color-mix(in srgb, var(--toolbar-border) 86%, transparent);
background: color-mix(in srgb, var(--panel) 70%, transparent);
color: var(--text-muted);
text-align: left;
}
.semantic-passport-reach-actions button:hover:not(:disabled),
.semantic-passport-reach-actions button:focus-visible {
border-color: color-mix(in srgb, var(--frontend-stroke) 62%, var(--toolbar-border));
color: var(--text);
}
.semantic-passport-reach-actions button:focus-visible {
outline: 2px solid var(--frontend-stroke);
outline-offset: 1px;
}
.semantic-passport-reach-actions button:disabled {
cursor: default;
opacity: 0.5;
}
.semantic-passport-reach-actions button[aria-pressed="true"] {
border-color: color-mix(
in srgb,
var(--reach-color, var(--frontend-stroke)) 72%,
var(--toolbar-border)
);
background: color-mix(in srgb, var(--reach-fill, var(--frontend-fill)) 48%, transparent);
color: var(--text);
}
#btn-reach-upstream {
--reach-color: var(--database-stroke);
--reach-fill: var(--database-fill);
}
#btn-reach-downstream {
--reach-color: var(--backend-stroke);
--reach-fill: var(--backend-fill);
}
.semantic-passport-reach-actions strong {
color: var(--reach-color, var(--frontend-stroke));
font-size: 0.625rem;
}
.semantic-passport-reach-status {
color: var(--text-faint);
font-size: 0.525rem;
line-height: 1.4;
}
.semantic-passport-reach-status[hidden] {
display: none;
}
.relationship-lens-list {
max-height: min(18rem, 46vh);
overflow: auto;
padding: 0.4rem;
scrollbar-width: thin;
scrollbar-color: var(--frontend-stroke) transparent;
}
.relationship-lens-list::-webkit-scrollbar {
width: 4px;
}
.relationship-lens-list::-webkit-scrollbar-thumb {
border-radius: 999px;
background: color-mix(in srgb, var(--frontend-stroke) 68%, transparent);
}
.relationship-lens-group + .relationship-lens-group {
margin-top: 0.35rem;
}
.relationship-lens-group-title {
display: block;
padding: 0.24rem 0.38rem 0.16rem;
color: var(--text-faint);
font-size: 0.5rem;
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.relationship-lens-row {
display: grid;
grid-template-columns: 2.1rem minmax(0, 1fr);
gap: 0.06rem 0.45rem;
width: 100%;
padding: 0.45rem 0.5rem !important;
text-align: left;
}
.relationship-lens-row:hover,
.relationship-lens-row:focus-visible,
.relationship-lens-row[data-preview-active="true"] {
background: var(--toolbar-hover);
box-shadow: inset 2px 0 0 var(--frontend-stroke);
}
.relationship-lens-row:focus-visible {
outline: 2px solid var(--frontend-stroke);
outline-offset: -2px;
}
.relationship-lens-direction {
grid-row: 1 / 3;
align-self: center;
color: var(--frontend-stroke);
font-size: 0.5rem;
font-weight: 800;
letter-spacing: 0.08em;
}
.relationship-lens-row[data-direction="in"] .relationship-lens-direction {
color: var(--database-stroke);
}
.relationship-lens-row[data-direction="loop"] .relationship-lens-direction {
color: var(--security-stroke);
}
.relationship-lens-row strong {
overflow: hidden;
color: var(--text);
font-size: 0.6875rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.relationship-lens-row small {
overflow: hidden;
color: var(--text-faint);
font-size: 0.5625rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.relationship-lens-empty {
margin: 0;
padding: 0.75rem;
color: var(--text-faint);
font-size: 0.625rem;
text-align: center;
}
/* Diagram Guide keeps the artifact's growing reader vocabulary
discoverable without widening the permanent control bar. It derives
honest counts at runtime and delegates every action to an existing
production interaction instead of creating a second command system. */
.diagram-guide {
position: absolute;
top: 1rem;
right: 1rem;
z-index: 26;
width: min(32rem, calc(100% - 2rem));
max-height: min(42rem, calc(100% - 2rem));
overflow: auto;
border: 1px solid color-mix(in srgb, var(--cloud-stroke) 58%, var(--toolbar-border));
border-radius: 0.82rem;
background:
linear-gradient(
135deg,
color-mix(in srgb, var(--cloud-fill) 32%, transparent),
transparent 42%
),
var(--toolbar-menu-bg);
color: var(--toolbar-text);
box-shadow: 0 22px 64px rgba(0, 0, 0, 0.4);
backdrop-filter: blur(18px);
animation: archify-guide-in 180ms cubic-bezier(0.22, 1, 0.36, 1);
}
.diagram-guide[hidden] {
display: none;
}
html[data-preset="blueprint"] .diagram-guide {
border-radius: 0.28rem;
}
.diagram-guide-head {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: start;
gap: 1rem;
padding: 0.82rem 0.9rem 0.68rem;
border-bottom: 1px solid var(--toolbar-border);
}
.diagram-guide-eyebrow {
display: block;
color: var(--cloud-stroke);
font-size: 0.5rem;
font-weight: 800;
letter-spacing: 0.16em;
text-transform: uppercase;
}
.diagram-guide-title {
display: block;
margin-top: 0.16rem;
color: var(--text);
font-size: 0.86rem;
letter-spacing: -0.02em;
}
.diagram-guide-close {
width: 1.8rem;
height: 1.8rem;
border: 0;
border-radius: 0.35rem;
background: transparent;
color: var(--toolbar-text);
font: inherit;
font-size: 1rem;
cursor: pointer;
}
.diagram-guide-close:hover,
.diagram-guide-close:focus-visible {
background: var(--toolbar-hover);
}
.diagram-guide-stats {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0;
padding: 0.54rem 0.9rem;
border-bottom: 1px solid color-mix(in srgb, var(--toolbar-border) 74%, transparent);
color: var(--text-faint);
font-size: 0.56rem;
letter-spacing: 0.025em;
}
.diagram-guide-stats::before {
content: "";
width: 0.42rem;
height: 0.42rem;
flex: none;
border-radius: 50%;
background: var(--backend-stroke);
box-shadow: 0 0 10px color-mix(in srgb, var(--backend-stroke) 72%, transparent);
}
.diagram-guide-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.38rem;
padding: 0.62rem;
}
.diagram-guide-action {
--guide-accent: var(--frontend-stroke);
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 0.55rem;
min-height: 4.15rem;
padding: 0.55rem 0.58rem;
border: 1px solid color-mix(in srgb, var(--guide-accent) 22%, var(--toolbar-border));
border-radius: 0.55rem;
background: color-mix(in srgb, var(--panel) 78%, transparent);
color: var(--toolbar-text);
font: inherit;
text-align: left;
cursor: pointer;
transition:
border-color 140ms ease,
background 140ms ease,
transform 140ms ease;
}
.diagram-guide-action[data-guide-action="route"] {
--guide-accent: var(--backend-stroke);
}
.diagram-guide-action[data-guide-action="map"] {
--guide-accent: var(--database-stroke);
}
.diagram-guide-action[data-guide-action="lens"] {
--guide-accent: var(--messagebus-stroke);
}
.diagram-guide-action[data-guide-action="story"] {
--guide-accent: var(--security-stroke);
}
.diagram-guide-action[data-guide-action="present"] {
--guide-accent: var(--cloud-stroke);
}
.diagram-guide-action:hover,
.diagram-guide-action:focus-visible {
border-color: color-mix(in srgb, var(--guide-accent) 76%, var(--toolbar-border));
background: color-mix(in srgb, var(--guide-accent) 9%, var(--panel));
transform: translateY(-1px);
}
.diagram-guide-action:focus-visible,
.diagram-guide-close:focus-visible {
outline: 2px solid var(--cloud-stroke);
outline-offset: 1px;
}
.diagram-guide-action:disabled {
opacity: 0.42;
cursor: default;
transform: none;
}
.diagram-guide-index {
display: grid;
place-items: center;
width: 1.65rem;
height: 1.65rem;
border: 1px solid color-mix(in srgb, var(--guide-accent) 64%, var(--toolbar-border));
border-radius: 0.35rem;
color: var(--guide-accent);
font-size: 0.5rem;
font-weight: 800;
letter-spacing: 0.06em;
}
.diagram-guide-action-copy {
min-width: 0;
}
.diagram-guide-action strong {
display: block;
overflow: hidden;
color: var(--text);
font-size: 0.66rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.diagram-guide-action small {
display: -webkit-box;
margin-top: 0.18rem;
overflow: hidden;
color: var(--text-faint);
font-size: 0.5rem;
line-height: 1.35;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.diagram-guide kbd {
align-self: start;
min-width: 1.45rem;
padding: 0.12rem 0.3rem;
border: 1px solid var(--toolbar-border);
border-radius: 0.25rem;
color: var(--text-faint);
font: inherit;
font-size: 0.5rem;
text-align: center;
}
.diagram-guide-shortcuts {
display: flex;
flex-wrap: wrap;
gap: 0.34rem 0.62rem;
padding: 0.58rem 0.9rem 0.68rem;
border-top: 1px solid var(--toolbar-border);
color: var(--text-faint);
font-size: 0.5rem;
}
.diagram-guide-shortcuts span {
display: inline-flex;
align-items: center;
gap: 0.28rem;
white-space: nowrap;
}
.diagram-guide-feedback {
min-height: 1rem;
margin: -0.18rem 0.9rem 0.62rem;
color: var(--security-stroke);
font-size: 0.5rem;
}
.diagram-guide-feedback:empty {
display: none;
}
@keyframes archify-guide-in {
from {
opacity: 0;
transform: translateY(-5px) scale(0.985);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.node-finder {
position: absolute;
top: 1rem;
left: 1rem;
z-index: 24;
display: flex;
flex-direction: column;
width: min(22rem, calc(100% - 2rem));
max-height: calc(100% - 2rem);
overflow: hidden;
border: 1px solid var(--toolbar-border);
border-radius: 0.85rem;
background: var(--toolbar-menu-bg);
color: var(--toolbar-text);
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.22);
backdrop-filter: blur(16px);
}
.node-finder[hidden] {
display: none;
}
.node-finder[data-context="route-source"] {
border-color: color-mix(in srgb, var(--frontend-stroke) 68%, var(--toolbar-border));
}
.node-finder[data-context="route-target"] {
border-color: color-mix(in srgb, var(--security-stroke) 68%, var(--toolbar-border));
}
html[data-preset="blueprint"] .node-finder {
border-radius: 0.3rem;
}
.node-finder-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.7rem 0.8rem 0.45rem;
}
.node-finder-head strong {
color: var(--text);
font-size: 0.6875rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.node-finder[data-context="route-source"] .node-finder-head strong {
color: var(--frontend-stroke);
}
.node-finder[data-context="route-target"] .node-finder-head strong {
color: var(--security-stroke);
}
.node-finder-close {
width: 1.75rem;
height: 1.75rem;
border: 0;
border-radius: 0.35rem;
background: transparent;
color: var(--toolbar-text);
font: inherit;
font-size: 1rem;
cursor: pointer;
}
.node-finder-close:hover,
.node-finder-close:focus-visible {
background: var(--toolbar-hover);
}
.node-finder-close:focus-visible,
.node-finder-result:focus-visible {
outline: 2px solid var(--frontend-stroke);
outline-offset: -2px;
}
.node-finder-search {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 0.55rem;
margin: 0 0.55rem 0.55rem;
padding: 0 0.7rem;
min-height: 2.6rem;
border: 1px solid var(--toolbar-border);
border-radius: 0.55rem;
background: color-mix(in srgb, var(--panel) 78%, transparent);
transition:
border-color 160ms ease-out,
box-shadow 160ms ease-out;
}
html[data-preset="blueprint"] .node-finder-search {
border-radius: 0.18rem;
}
.node-finder-search > span {
color: var(--frontend-stroke);
font-size: 0.875rem;
}
.node-finder-search:focus-within {
border-color: var(--frontend-stroke);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--frontend-stroke) 20%, transparent);
}
.node-finder-input {
appearance: none;
width: 100%;
min-width: 0;
border: 0;
outline: 0;
background: transparent;
color: var(--text);
font: inherit;
font-size: 0.75rem;
}
.node-finder-input:focus-visible {
outline: none;
}
.node-finder-input::placeholder {
color: var(--text-faint);
}
.node-finder kbd {
padding: 0.1rem 0.32rem;
border: 1px solid var(--toolbar-border);
border-radius: 0.25rem;
color: var(--text-faint);
font: inherit;
font-size: 0.5625rem;
}
.node-finder-results {
display: grid;
flex: 1 1 auto;
gap: 0;
min-height: 0;
max-height: min(18rem, 48vh);
margin: 0 0.55rem 0.45rem;
padding: 0;
overflow-y: auto;
overscroll-behavior: contain;
border: 1px solid color-mix(in srgb, var(--toolbar-border) 78%, transparent);
border-radius: 0.55rem;
background: color-mix(in srgb, var(--panel) 54%, transparent);
scroll-padding-block: 0.25rem;
scrollbar-width: thin;
}
html[data-preset="blueprint"] .node-finder-results {
border-radius: 0.18rem;
}
.node-finder-result {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.2rem 0.75rem;
width: 100%;
min-height: 3.25rem;
padding: 0.58rem 0.65rem;
border: 0;
border-radius: 0;
background: transparent;
color: var(--text);
font: inherit;
text-align: left;
cursor: pointer;
transition: background-color 160ms ease-out;
}
.node-finder-result:not(:last-child) {
border-bottom: 1px solid color-mix(in srgb, var(--toolbar-border) 62%, transparent);
}
.node-finder-result:hover,
.node-finder-result:focus-visible {
position: relative;
z-index: 1;
background: color-mix(in srgb, var(--frontend-fill) 48%, var(--toolbar-hover));
}
.node-finder-result strong {
overflow: hidden;
font-size: 0.75rem;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
}
.node-finder-result small {
grid-column: 1 / -1;
overflow: hidden;
color: var(--text-faint);
font-size: 0.5625rem;
letter-spacing: 0.015em;
text-overflow: ellipsis;
white-space: nowrap;
}
.node-finder-result em {
align-self: center;
color: var(--frontend-stroke);
font-size: 0.625rem;
font-style: normal;
}
.node-finder[data-context^="route-"] .node-finder-result em {
padding: 0.1rem 0.3rem;
border: 1px solid color-mix(in srgb, currentColor 52%, transparent);
border-radius: 999px;
font-size: 0.5rem;
font-weight: 800;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.node-finder[data-context="route-target"] .node-finder-result em {
color: var(--security-stroke);
}
.node-finder-empty {
padding: 1.1rem 0.75rem 1.25rem;
color: var(--text-faint);
font-size: 0.6875rem;
text-align: center;
}
.node-finder-status {
margin: 0;
padding: 0.1rem 0.8rem 0.65rem;
color: var(--text-faint);
font-size: 0.5625rem;
}
.guided-views {
display: grid;
grid-template-columns: 2.75rem minmax(0, 1fr) 2.75rem auto;
align-items: stretch;
gap: 0.35rem;
min-width: 0;
margin: 0 0 0.75rem;
padding: 0.35rem;
border: 1px solid var(--toolbar-border);
border-radius: 0.85rem;
background: var(--toolbar-bg);
background: linear-gradient(
120deg,
var(--toolbar-bg),
color-mix(in srgb, var(--toolbar-bg) 82%, var(--frontend-fill))
);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.16);
backdrop-filter: blur(12px);
}
.guided-views[hidden] {
display: none;
}
html[data-preset="blueprint"] .guided-views {
border-color: var(--panel-border);
border-radius: 0.35rem;
background:
linear-gradient(
90deg,
color-mix(in srgb, var(--frontend-fill) 36%, transparent),
transparent 24%
),
var(--toolbar-bg);
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.12);
}
html[data-preset="blueprint"] .guided-views button {
border-radius: 0.2rem;
}
html[data-preset="blueprint"] .guided-view-progress {
border-radius: 0;
}
html[data-preset="blueprint"] .guided-view-progress span {
border-radius: 0;
}
.guided-views button {
min-width: 0;
padding: 0.45rem 0.6rem;
border-radius: 0.55rem;
font-size: 0.75rem;
}
.guided-views > #guided-view-prev,
.guided-views > #guided-view-next {
min-width: 2.75rem;
min-height: 2.75rem;
}
.guided-views button:disabled {
opacity: 0.3;
cursor: default;
}
.guided-view-copy {
min-width: 0;
padding: 0.2rem 0.5rem;
border-left: 1px solid var(--toolbar-border);
overflow: hidden;
}
.guided-view-meta {
display: flex;
align-items: center;
gap: 0.5rem;
min-height: 1rem;
margin-bottom: 0.12rem;
color: var(--frontend-stroke);
font-size: 0.5625rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.guided-view-handoff {
box-sizing: border-box;
height: 1rem;
min-width: 0;
max-width: 15rem;
overflow: hidden;
padding: 0 0.38rem;
border: 1px solid color-mix(in srgb, var(--frontend-stroke) 46%, transparent);
border-radius: 999px;
color: var(--toolbar-text);
background: color-mix(in srgb, var(--frontend-fill) 44%, transparent);
font-size: 0.5rem;
line-height: calc(1rem - 2px);
letter-spacing: 0.06em;
text-overflow: ellipsis;
white-space: nowrap;
}
.guided-view-handoff[hidden] {
display: none;
}
html[data-preset="blueprint"] .guided-view-handoff {
border-radius: 0.15rem;
}
.chapter-handoff-overlay {
pointer-events: none;
}
.chapter-handoff-anchor {
fill: color-mix(in srgb, var(--frontend-fill) 10%, transparent);
stroke: var(--frontend-stroke);
stroke-width: 2;
stroke-dasharray: 4 4;
vector-effect: non-scaling-stroke;
opacity: 0.96;
animation: archify-chapter-anchor 0.42s cubic-bezier(0.22, 1, 0.36, 1) both;
}
svg[data-chapter-handoff] [data-node-id][data-chapter-role="stay"] {
opacity: 1;
}
svg[data-chapter-handoff] [data-node-id][data-chapter-role="enter"] {
opacity: 0.78;
}
svg[data-chapter-handoff] [data-node-id][data-chapter-role="leave"] {
opacity: 0.26;
}
@keyframes archify-chapter-anchor {
0% {
opacity: 0.35;
stroke-dashoffset: 8;
}
45% {
opacity: 1;
}
100% {
opacity: 0.72;
stroke-dashoffset: 0;
}
}
.guided-view-progress {
width: 3.5rem;
height: 2px;
overflow: hidden;
border-radius: 999px;
background: var(--toolbar-border);
}
.guided-view-progress span {
display: block;
width: 100%;
height: 100%;
border-radius: inherit;
background: var(--frontend-stroke);
transform: scaleX(0);
transform-origin: left center;
}
.guided-view-copy strong,
.guided-view-copy small {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.guided-view-copy strong {
color: var(--toolbar-text);
font-size: 0.75rem;
line-height: 1.35;
}
.guided-view-copy small {
margin-top: 0;
color: var(--text-muted);
font-size: 0.625rem;
line-height: 1.35;
}
.guided-story-caption {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 0.12rem 0.55rem;
align-items: center;
min-width: 0;
margin-top: 0.18rem;
padding: 0.3rem 0.5rem;
overflow: hidden;
border: 1px solid color-mix(in srgb, var(--frontend-stroke) 32%, var(--toolbar-border));
border-radius: 0.58rem;
background: color-mix(in srgb, var(--frontend-fill) 26%, transparent);
animation: archify-story-caption-in 140ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
.guided-story-caption[hidden] {
display: none;
}
.guided-story-caption-index {
grid-row: 1 / span 2;
align-self: stretch;
display: inline-grid;
min-width: 3.3rem;
place-items: center;
padding-right: 0.52rem;
border-right: 1px solid
color-mix(in srgb, var(--frontend-stroke) 28%, var(--toolbar-border));
color: var(--frontend-stroke);
font-size: 0.5625rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.guided-story-caption strong,
.guided-story-caption small {
display: block;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.guided-story-caption strong {
color: var(--toolbar-text);
font-size: 0.6875rem;
line-height: 1.3;
}
.guided-story-caption small {
color: var(--text-muted);
font-size: 0.5625rem;
line-height: 1.35;
}
.guided-story-caption-next {
grid-column: 3;
grid-row: 1 / span 2;
display: grid;
min-width: 6.4rem;
max-width: 9.5rem;
align-self: stretch;
align-content: center;
padding-left: 0.58rem;
border-left: 1px solid color-mix(in srgb, var(--frontend-stroke) 22%, var(--toolbar-border));
}
.guided-story-caption-next[hidden] {
display: none;
}
.guided-story-caption-next small {
color: color-mix(in srgb, var(--frontend-stroke) 72%, var(--text-muted));
font-weight: 800;
letter-spacing: 0.09em;
text-transform: uppercase;
}
.guided-story-caption-next strong {
color: var(--text-muted);
font-weight: 700;
}
html[data-preset="blueprint"] .guided-story-caption {
border-radius: 0.2rem;
}
.guided-views[data-story-beat] .guided-view-copy > #guided-view-label,
.guided-views[data-story-beat] .guided-view-copy > #guided-view-note {
display: none;
}
@keyframes archify-story-caption-in {
from {
opacity: 0;
transform: translateY(3px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.guided-view-trail {
display: flex;
align-items: center;
gap: 0.28rem;
min-width: 0;
min-height: 1.7rem;
margin-top: 0;
padding: 0;
overflow-x: auto;
color: var(--text-faint);
overscroll-behavior-x: contain;
touch-action: pan-x;
scrollbar-width: none;
white-space: nowrap;
}
.guided-view-trail[hidden] {
display: none;
}
.guided-view-trail::-webkit-scrollbar {
display: none;
}
.guided-view-trail .guided-view-stop {
appearance: none;
box-sizing: border-box;
display: inline-flex;
flex: 0 0 auto;
align-items: center;
gap: 0.28rem;
min-height: 1.5rem;
padding: 0.1rem 0.3rem 0.1rem 0.18rem;
border: 1px solid transparent;
border-radius: 999px;
background: transparent;
color: var(--text-muted);
cursor: pointer;
font-family: inherit;
font-size: 0.5625rem;
font-weight: 650;
letter-spacing: 0.02em;
line-height: 1;
transition:
background 140ms ease,
border-color 140ms ease,
color 140ms ease,
opacity 140ms ease;
}
.guided-view-stop:hover {
border-color: color-mix(in srgb, var(--frontend-stroke) 36%, transparent);
background: color-mix(in srgb, var(--frontend-fill) 34%, transparent);
}
.guided-view-stop:focus-visible {
outline: 2px solid var(--frontend-stroke);
outline-offset: 1px;
}
.guided-view-stop:disabled {
cursor: wait;
}
.guided-view-stop::before {
display: inline-grid;
width: 1rem;
height: 1rem;
place-items: center;
border: 1px solid color-mix(in srgb, var(--frontend-stroke) 54%, var(--toolbar-border));
border-radius: 50%;
background: color-mix(in srgb, var(--frontend-fill) 58%, var(--toolbar-bg));
color: var(--frontend-stroke);
content: attr(data-story-number);
font-size: 0.5rem;
line-height: 1;
}
.guided-view-stop + .guided-view-stop::after {
order: -1;
color: color-mix(in srgb, var(--frontend-stroke) 58%, var(--text-faint));
content: "\00b7";
font-size: 0.65rem;
}
.guided-view-stop[data-story-link="forward"]::after {
content: "\2192";
}
.guided-view-stop[data-story-link="reverse"]::after {
content: "\2190";
}
.guided-view-stop[data-story-link="multiple"]::after {
content: "\21c4";
}
.guided-view-stop[data-story-beat-state="pending"] {
opacity: 0.38;
}
.guided-view-stop[data-story-beat-state="next"] {
border-style: dashed;
border-color: color-mix(in srgb, var(--frontend-stroke) 42%, var(--toolbar-border));
color: color-mix(in srgb, var(--toolbar-text) 68%, var(--text-muted));
opacity: 0.58;
}
.guided-view-stop[data-story-beat-state="past"] {
color: var(--text-muted);
opacity: 0.76;
}
.guided-view-stop[data-story-beat-state="active"] {
border-color: color-mix(in srgb, var(--frontend-stroke) 72%, var(--toolbar-border));
background: color-mix(in srgb, var(--frontend-fill) 42%, transparent);
color: var(--toolbar-text);
font-weight: 800;
opacity: 1;
}
.guided-view-stop[data-story-beat-state="active"]::before {
border-color: var(--frontend-stroke);
box-shadow: 0 0 10px color-mix(in srgb, var(--frontend-stroke) 54%, transparent);
}
html[data-preset="blueprint"] .guided-view-stop,
html[data-preset="blueprint"] .guided-view-stop::before {
border-radius: 0.1rem;
}
html[data-preset="blueprint"] .guided-view-stop[data-story-beat-state="active"]::before {
box-shadow: none;
}
.guided-view-actions {
display: flex;
align-items: stretch;
gap: 0.2rem;
}
.guided-view-actions button {
white-space: nowrap;
}
.guided-view-play {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.38rem;
min-width: 6.25rem !important;
color: var(--frontend-stroke) !important;
font-size: 0.625rem !important;
}
.guided-view-play[aria-pressed="true"] {
background: var(--toolbar-hover);
}
.guided-view-all {
min-width: 3.25rem !important;
color: var(--text-muted) !important;
font-size: 0.625rem !important;
}
.guided-view-beat-link {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.3rem;
min-width: 5.4rem !important;
min-height: 1.5rem;
color: var(--text-muted) !important;
font-size: 0.5625rem !important;
letter-spacing: 0.025em;
transition:
background 140ms ease,
border-color 140ms ease,
color 140ms ease;
}
.guided-view-beat-link:not(:disabled):hover {
border-color: color-mix(in srgb, var(--frontend-stroke) 52%, var(--toolbar-border));
color: var(--frontend-stroke) !important;
}
.guided-view-beat-link[data-copy-state="copied"] {
border-color: color-mix(in srgb, var(--backend-stroke) 58%, var(--toolbar-border));
background: color-mix(in srgb, var(--backend-fill) 46%, transparent);
color: var(--backend-stroke) !important;
}
.guided-view-index {
grid-column: 1 / -1;
min-width: 0;
padding: 0.28rem 0.12rem 0.08rem;
overflow: hidden;
border-top: 1px solid color-mix(in srgb, var(--toolbar-border) 72%, transparent);
}
@media (min-width: 721px) {
html[data-present="true"]:not([data-embed="true"])
.guided-views[data-playing="true"]
.guided-view-index,
html[data-present="true"]:not([data-embed="true"])
.guided-views[data-playing="true"]
.guided-view-beat-link,
html[data-present="true"]:not([data-embed="true"])
.guided-views[data-playing="true"]
.guided-view-all,
html[data-present="true"]:not([data-embed="true"])
.guided-views[data-playing="true"]
.guided-view-trail,
html[data-present="true"]:not([data-embed="true"])
.guided-views[data-playing="true"]
.guided-view-copy
> #guided-view-label,
html[data-present="true"]:not([data-embed="true"])
.guided-views[data-playing="true"]
.guided-view-copy
> #guided-view-note {
display: none;
}
html[data-present="true"]:not([data-embed="true"])
.guided-views[data-playing="true"]
.guided-story-caption {
margin-top: 0.08rem;
}
}
.guided-view-chapters {
display: flex;
gap: 0.28rem;
min-width: 0;
margin: 0;
padding: 0.08rem 0.05rem 0.2rem;
overflow-x: auto;
list-style: none;
scroll-padding-inline: 0.05rem;
scroll-snap-type: x proximity;
scrollbar-width: none;
}
.guided-view-chapters::-webkit-scrollbar {
display: none;
}
.guided-view-chapters li {
display: flex;
flex: 1 1 0;
min-width: 9.5rem;
scroll-snap-align: center;
}
.guided-view-chapter {
position: relative;
display: grid !important;
grid-template-columns: 1.55rem minmax(0, 1fr) auto;
align-items: center;
width: 100%;
min-height: 2.75rem;
padding: 0.36rem 0.48rem !important;
overflow: hidden;
border: 1px solid color-mix(in srgb, var(--toolbar-border) 86%, transparent) !important;
border-radius: 0.48rem !important;
background: color-mix(in srgb, var(--toolbar-bg) 88%, transparent) !important;
color: var(--text-muted) !important;
text-align: left;
transition:
background 0.16s ease,
border-color 0.16s ease,
opacity 0.16s ease;
}
.guided-view-chapter:hover {
border-color: color-mix(
in srgb,
var(--frontend-stroke) 58%,
var(--toolbar-border)
) !important;
background: color-mix(in srgb, var(--frontend-fill) 42%, var(--toolbar-bg)) !important;
}
.guided-view-chapter:focus-visible {
outline: 2px solid var(--frontend-stroke);
outline-offset: 1px;
}
.guided-view-chapter-index {
display: inline-grid;
width: 1.32rem;
height: 1.32rem;
place-items: center;
border: 1px solid color-mix(in srgb, var(--frontend-stroke) 48%, var(--toolbar-border));
border-radius: 50%;
color: var(--frontend-stroke);
font-size: 0.5rem;
font-weight: 800;
letter-spacing: 0.03em;
}
.guided-view-chapter-title {
min-width: 0;
overflow: hidden;
color: var(--toolbar-text);
font-size: 0.625rem;
font-weight: 700;
line-height: 1.3;
text-overflow: ellipsis;
white-space: nowrap;
}
.guided-view-chapter-count {
grid-column: 3;
grid-row: 1;
margin-left: 0.38rem;
color: var(--text-faint);
font-size: 0.5rem;
font-style: normal;
font-weight: 650;
white-space: nowrap;
}
.guided-view-chapter-delta {
display: inline-flex;
grid-column: 3;
grid-row: 1;
align-items: center;
gap: 0.22rem;
margin-left: 0.38rem;
color: var(--text-faint);
font-size: 0.5rem;
font-weight: 750;
letter-spacing: 0.01em;
white-space: nowrap;
}
.guided-view-chapter-delta[hidden] {
display: none;
}
.guided-view-chapter-delta [data-delta-kind="stay"] {
color: var(--text-muted);
}
.guided-view-chapter-delta [data-delta-kind="enter"] {
color: var(--frontend-stroke);
}
.guided-view-chapter-delta [data-delta-kind="leave"] {
color: var(--security-stroke);
}
.guided-view-chapter[data-preview-active="true"] {
border-color: color-mix(
in srgb,
var(--frontend-stroke) 72%,
var(--toolbar-border)
) !important;
background: color-mix(in srgb, var(--frontend-fill) 54%, var(--toolbar-bg)) !important;
}
.guided-view-chapter[data-chapter-position="current"] {
border: 2px solid var(--frontend-stroke) !important;
padding: calc(0.36rem - 1px) calc(0.48rem - 1px) !important;
box-shadow: inset 3px 0 0 var(--frontend-stroke);
color: var(--toolbar-text) !important;
}
.guided-view-chapter[data-chapter-position="current"] .guided-view-chapter-index {
background: var(--frontend-stroke);
color: var(--bg);
}
.guided-view-chapter[data-chapter-position="before"] {
border-style: solid !important;
opacity: 0.72;
}
.guided-view-chapter[data-chapter-position="after"] {
border-style: dashed !important;
}
html[data-preset="signal-flow"] .guided-view-chapter[data-chapter-position="current"] {
box-shadow:
inset 3px 0 0 var(--frontend-stroke),
0 0 18px color-mix(in srgb, var(--frontend-stroke) 18%, transparent);
}
html[data-preset="blueprint"] .guided-view-chapter,
html[data-preset="blueprint"] .guided-view-chapter-index {
border-radius: 0.12rem !important;
}
/* Story Shelf — keep every authored chapter and the Play action visible,
but do not spend a second row on unavailable director controls before
the reader enters a chapter. Existing data-active-view state expands
the full guided surface without a new toggle or stored preference. */
.guided-views[data-active-view="all"] {
grid-template-columns: minmax(11rem, 0.72fr) auto minmax(0, 2fr);
align-items: center;
padding: 0.28rem 0.35rem;
}
.guided-views[data-active-view="all"] > #guided-view-prev,
.guided-views[data-active-view="all"] > #guided-view-next,
.guided-views[data-active-view="all"] .guided-view-beat-link,
.guided-views[data-active-view="all"] .guided-view-all {
display: none;
}
.guided-views[data-active-view="all"] .guided-view-copy {
grid-column: 1;
grid-row: 1;
padding: 0.1rem 0.55rem;
}
.guided-views[data-active-view="all"] .guided-view-copy > #guided-view-note {
display: none;
}
.guided-views[data-active-view="all"] .guided-view-actions {
display: block;
grid-column: 2;
grid-row: 1;
}
.guided-views[data-active-view="all"] .guided-view-play {
min-height: 2.75rem;
}
.guided-views[data-active-view="all"] .guided-view-index {
grid-column: 3;
grid-row: 1;
padding: 0 0.08rem 0 0.42rem;
border-top: 0;
border-left: 1px solid color-mix(in srgb, var(--toolbar-border) 72%, transparent);
}
.guided-views[data-active-view="all"] .guided-view-chapters {
gap: 0.3rem;
padding: 0;
}
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
margin-top: 2rem;
}
.card {
background: var(--panel);
border-radius: 0.75rem;
border: 1px solid var(--panel-border);
padding: 1.25rem;
}
html[data-preset="signal-flow"] .card {
background: linear-gradient(145deg, rgba(10, 24, 43, 0.86), rgba(5, 12, 24, 0.92));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.035);
}
html[data-preset="signal-flow"][data-theme="light"] .card {
background: linear-gradient(145deg, rgba(255, 255, 255, 0.92), rgba(237, 246, 250, 0.9));
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.82),
0 12px 32px rgba(45, 78, 99, 0.08);
}
/* Preserve the one-screen guarantee on ordinary laptop displays. The
high-screen expansion above must not trade a fuller canvas for page
scrolling at 900–1080px heights. */
@media (min-width: 768px) and (max-height: 1100px) {
.header {
margin-bottom: 1rem;
}
.diagram-container {
padding: 0.875rem;
padding-bottom: calc(0.875rem + var(--archify-nav-reserve));
}
.cards {
margin-top: 1rem;
}
.card {
padding: 1rem;
}
}
@media (min-width: 768px) and (max-height: 920px) {
body {
padding-block: 1.25rem;
}
.header {
margin-bottom: 0.875rem;
}
.cards {
margin-top: 0.75rem;
}
html[data-nav-stage-rail="true"] body {
padding-block: 0.375rem;
}
html[data-nav-stage-rail="true"] .header {
margin-bottom: 0.5rem;
}
html[data-nav-stage-rail="true"] .guided-views {
margin-bottom: 0.375rem;
}
html[data-nav-stage-rail="true"] .diagram-container[data-nav-stage-rail="true"] {
padding-top: 0.5rem;
}
html[data-nav-stage-rail="true"] .cards {
margin-top: 0.375rem;
}
}
html[data-preset="blueprint"] .card {
position: relative;
border-color: var(--panel-border);
border-radius: 0.3rem;
background: var(--panel);
box-shadow: none;
}
html[data-preset="blueprint"] .card::before {
content: "";
position: absolute;
top: -1px;
left: 1rem;
width: 2.5rem;
border-top: 2px solid var(--frontend-stroke);
}
html[data-preset="editorial"] .card {
position: relative;
border-radius: 0.28rem;
background: var(--panel);
box-shadow: 0 10px 28px color-mix(in srgb, #000 10%, transparent);
}
html[data-preset="editorial"] .card::before {
position: absolute;
top: -1px;
left: 1.25rem;
width: 3.25rem;
border-top: 3px solid var(--arrow-emphasis);
content: "";
}
html[data-preset="editorial"] .card-dot {
border-radius: 0;
transform: rotate(45deg) scale(0.78);
}
.card-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.card-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.card-dot.cyan {
background: var(--frontend-stroke);
}
.card-dot.emerald {
background: var(--backend-stroke);
}
.card-dot.violet {
background: var(--database-stroke);
}
.card-dot.amber {
background: var(--cloud-stroke);
}
.card-dot.rose {
background: var(--security-stroke);
}
.card-dot.orange {
background: var(--messagebus-stroke);
}
.card-dot.slate {
background: var(--external-stroke);
}
.card h3 {
font-size: 0.875rem;
font-weight: 600;
color: var(--text);
}
.card ul {
list-style: none;
color: var(--text-muted);
font-size: 0.75rem;
}
.card li {
margin-bottom: 0.375rem;
}
/* ==========================================================
Print stylesheet — hide interactive controls, force light,
drop the grid so the diagram doesn't waste ink, use landscape
so wide diagrams fit, pack summary cards into 2 columns to
avoid an orphan card on a trailing page.
========================================================== */
@page {
size: landscape;
margin: 1.5cm;
}
@media print {
/* Force the FULL light palette — including component fills/strokes and
lane/arrow colors — so printing from dark theme doesn't put neon
strokes and translucent dark fills on white paper. */
:root,
[data-theme="dark"],
[data-theme="light"] {
--bg: #ffffff;
--grid: transparent;
--panel: #ffffff;
--panel-border: #e2e8f0;
--text: #0f172a;
--text-muted: #475569;
--text-dim: #94a3b8;
--text-faint: #64748b;
--mask: #ffffff;
--lane-fill: rgba(248, 250, 252, 0.65);
--lane-stroke: #cbd5e1;
--arrow: #94a3b8;
--arrow-emphasis: #059669;
--frontend-fill: rgba(34, 211, 238, 0.15);
--frontend-stroke: #0891b2;
--backend-fill: rgba(52, 211, 153, 0.18);
--backend-stroke: #059669;
--database-fill: rgba(167, 139, 250, 0.2);
--database-stroke: #7c3aed;
--cloud-fill: rgba(251, 191, 36, 0.18);
--cloud-stroke: #d97706;
--security-fill: rgba(251, 113, 133, 0.15);
--security-stroke: #e11d48;
--messagebus-fill: rgba(251, 146, 60, 0.15);
--messagebus-stroke: #ea580c;
--external-fill: rgba(148, 163, 184, 0.18);
--external-stroke: #64748b;
}
body {
background: #ffffff !important;
padding: 0;
}
.container {
max-width: none;
}
.toolbar,
.diagram-nav,
.focus-chip,
.guided-views,
.archify-toast,
.no-print {
display: none !important;
}
.chapter-handoff-overlay {
display: none !important;
}
svg [data-node-id][data-chapter-role] {
opacity: 1 !important;
}
svg[data-chapter-preview] [data-node-id],
svg[data-chapter-preview] [data-edge-from] {
opacity: 1 !important;
filter: none !important;
}
.diagram-container,
.card {
box-shadow: none;
border-color: #e2e8f0;
}
.diagram-container {
--archify-nav-reserve: 0px !important;
}
html[data-preset="signal-flow"] .diagram-container,
html[data-preset="signal-flow"] .card {
background: #ffffff;
}
.cards {
grid-template-columns: 1fr 1fr;
}
/* Ensure elements break sensibly across pages */
.card {
break-inside: avoid;
page-break-inside: avoid;
}
.diagram-container {
break-inside: avoid;
page-break-inside: avoid;
}
.diagram-container svg [data-detail] {
opacity: 1 !important;
pointer-events: auto !important;
}
.diagram-container svg [data-detail-anchor] {
transform: none !important;
}
.diagram-container svg[data-lens-active] [data-node-id],
.diagram-container svg[data-lens-active] [data-edge-from],
.diagram-container svg[data-legend-preview-active] [data-node-id],
.diagram-container svg[data-legend-preview-active] [data-edge-from] {
opacity: 1 !important;
filter: none !important;
}
[data-legend-bridge-runtime] {
display: none !important;
}
.semantic-lens-overlay {
display: none !important;
}
.relationship-hit-overlay {
display: none !important;
}
.relationship-pulse-overlay {
display: none !important;
}
.route-probe-overlay,
.route-journey-overlay {
display: none !important;
}
.story-trail-overlay,
.story-carrier-overlay {
display: none !important;
}
.diagram-container svg[data-focus-active] [data-node-id],
.diagram-container svg[data-focus-active] [data-edge-from],
.diagram-container svg[data-reach-active] [data-node-id],
.diagram-container svg[data-reach-active] [data-edge-from],
.diagram-container svg[data-story-beat] [data-story-step],
.diagram-container svg[data-story-beat] [data-edge-from][data-story-beat-step],
.diagram-container svg[data-route-active] [data-node-id],
.diagram-container svg[data-route-active] [data-edge-from] {
opacity: 1 !important;
filter: none !important;
animation: none !important;
}
h1,
.subtitle {
color: #0f172a;
}
}
/* ==========================================================
TOOLBAR (theme toggle + export menu)
========================================================== */
.toolbar {
position: fixed;
top: 1rem;
right: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0;
border: 0;
background: transparent;
box-shadow: none;
z-index: 100;
}
.toolbar button {
background: var(--toolbar-bg);
color: var(--toolbar-text);
border: 1px solid var(--toolbar-border);
padding: 0.5rem 0.875rem;
border-radius: 0.625rem;
font-family: inherit;
font-size: 0.75rem;
font-weight: 500;
cursor: pointer;
white-space: nowrap;
backdrop-filter: blur(10px);
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);
transition:
background 0.15s,
border-color 0.15s,
color 0.15s;
display: inline-flex;
align-items: center;
gap: 0.375rem;
min-height: 2.75rem;
}
.toolbar button:hover {
background: var(--toolbar-hover);
border-color: color-mix(in srgb, var(--arrow) 62%, var(--toolbar-border));
}
.toolbar button[aria-expanded="true"],
.toolbar button[aria-pressed="true"]:not(#btn-theme) {
background: color-mix(in srgb, var(--frontend-stroke) 10%, var(--toolbar-hover));
border-color: color-mix(in srgb, var(--frontend-stroke) 48%, var(--toolbar-border));
}
.toolbar #btn-export {
padding-right: 0.82rem;
padding-left: 0.82rem;
}
.toolbar #btn-export:hover,
.toolbar #btn-export[aria-expanded="true"] {
color: var(--toolbar-text);
background: color-mix(in srgb, var(--frontend-stroke) 10%, var(--toolbar-hover));
border-color: color-mix(in srgb, var(--frontend-stroke) 54%, var(--toolbar-border));
}
.toolbar button:focus-visible {
outline: 2px solid var(--frontend-stroke);
outline-offset: 2px;
}
.toolbar #btn-motion[hidden] {
display: none !important;
}
.toolbar #btn-motion {
min-width: 4.4rem;
}
.toolbar #btn-motion:disabled {
cursor: default;
opacity: 0.58;
}
.toolbar .preset-wrap,
.toolbar .export-wrap {
position: relative;
}
.toolbar-icon {
display: inline-block;
width: 0.95rem;
height: 0.95rem;
flex: 0 0 auto;
background: currentColor;
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: contain;
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
}
.toolbar-chevron {
width: 0.65rem;
height: 0.65rem;
transition: transform 0.15s ease;
}
[aria-expanded="true"] > .toolbar-chevron {
transform: rotate(180deg);
}
[data-theme="light"] #theme-icon {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cg fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round'%3E%3Ccircle cx='10' cy='10' r='3.1'/%3E%3Cpath d='M10 2.2v1.4M10 16.4v1.4M2.2 10h1.4M16.4 10h1.4M4.5 4.5l1 1M14.5 14.5l1 1M15.5 4.5l-1 1M5.5 14.5l-1 1'/%3E%3C/g%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cg fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round'%3E%3Ccircle cx='10' cy='10' r='3.1'/%3E%3Cpath d='M10 2.2v1.4M10 16.4v1.4M2.2 10h1.4M16.4 10h1.4M4.5 4.5l1 1M14.5 14.5l1 1M15.5 4.5l-1 1M5.5 14.5l-1 1'/%3E%3C/g%3E%3C/svg%3E");
}
[data-theme="dark"] #theme-icon {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M15.2 12.7A6.3 6.3 0 017.3 4.8a6.3 6.3 0 107.9 7.9Z' fill='none' stroke='black' stroke-width='1.7' stroke-linejoin='round'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M15.2 12.7A6.3 6.3 0 017.3 4.8a6.3 6.3 0 107.9 7.9Z' fill='none' stroke='black' stroke-width='1.7' stroke-linejoin='round'/%3E%3C/svg%3E");
}
#btn-present[aria-pressed="false"] #present-icon {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M7.2 3.2h-4v4M12.8 3.2h4v4M7.2 16.8h-4v-4M12.8 16.8h4v-4' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M7.2 3.2h-4v4M12.8 3.2h4v4M7.2 16.8h-4v-4M12.8 16.8h4v-4' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round'/%3E%3C/svg%3E");
}
#btn-present[aria-pressed="true"] #present-icon {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M7.2 7.2h-4v-4M12.8 7.2h4v-4M7.2 12.8h-4v4M12.8 12.8h4v4' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M7.2 7.2h-4v-4M12.8 7.2h4v-4M7.2 12.8h-4v4M12.8 12.8h4v4' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round'/%3E%3C/svg%3E");
}
.toolbar-chevron {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='m3 4.5 3 3 3-3' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='m3 4.5 3 3 3-3' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.toolbar #btn-preset {
min-width: 5.4rem;
}
.preset-control-mark {
width: 0.58rem;
height: 0.58rem;
flex: 0 0 auto;
border: 1px solid var(--frontend-stroke);
border-radius: 0.2rem;
background: color-mix(in srgb, var(--frontend-stroke) 28%, transparent);
transition:
border-radius 0.15s,
box-shadow 0.15s,
transform 0.15s;
}
#btn-preset[data-preset-option="signal-flow"] .preset-control-mark {
border-radius: 50%;
box-shadow: 0 0 10px color-mix(in srgb, var(--frontend-stroke) 78%, transparent);
}
#btn-preset[data-preset-option="blueprint"] .preset-control-mark {
border-radius: 0.06rem;
background: transparent;
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--frontend-stroke) 32%, transparent);
transform: rotate(45deg) scale(0.84);
}
#btn-preset[data-preset-option="editorial"] .preset-control-mark {
border-color: var(--arrow-emphasis);
border-radius: 0;
background: var(--arrow-emphasis);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--arrow-emphasis) 14%, transparent);
transform: rotate(45deg) scale(0.72);
}
.toolbar .preset-menu {
position: absolute;
top: calc(100% + 0.375rem);
right: 0;
display: none;
width: 18.5rem;
max-width: calc(100vw - 2rem);
padding: 0.45rem;
border: 1px solid var(--toolbar-border);
border-radius: 0.875rem;
background: var(--toolbar-menu-bg);
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.24);
}
.toolbar .preset-menu.open {
display: block;
}
.toolbar .preset-menu.open,
.toolbar .export-menu.open {
animation: toolbar-menu-in 0.16s cubic-bezier(0.2, 0.8, 0.2, 1);
transform-origin: top right;
}
@keyframes toolbar-menu-in {
from {
opacity: 0;
transform: translateY(-0.25rem) scale(0.985);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.preset-menu-heading {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
padding: 0.42rem 0.6rem 0.55rem;
color: var(--text-faint);
font-size: 0.56rem;
letter-spacing: 0.13em;
text-transform: uppercase;
}
.preset-menu-heading span:last-child {
font-size: 0.52rem;
letter-spacing: 0.04em;
text-transform: none;
}
.toolbar .preset-option {
display: grid;
grid-template-columns: 2rem minmax(0, 1fr) 1rem;
align-items: center;
gap: 0.68rem;
width: 100%;
min-height: 3.35rem;
padding: 0.5rem 0.62rem;
border: 0;
border-radius: 0.5rem;
background: transparent;
box-shadow: none;
backdrop-filter: none;
text-align: left;
}
.toolbar .preset-option:hover,
.toolbar .preset-option:focus-visible {
background: var(--toolbar-hover);
}
.toolbar .preset-option[aria-checked="true"] {
background: color-mix(in srgb, var(--frontend-stroke) 9%, transparent);
border-color: color-mix(in srgb, var(--frontend-stroke) 36%, transparent);
}
.preset-option-swatch {
position: relative;
width: 2rem;
height: 1.55rem;
overflow: hidden;
border: 1px solid #64748b;
border-radius: 0.24rem;
background: #0f172a;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04);
}
.preset-option-swatch::before,
.preset-option-swatch::after {
position: absolute;
content: "";
}
.preset-option-swatch.classic::before {
top: 0.42rem;
right: 0.35rem;
bottom: 0.42rem;
left: 0.35rem;
border: 1px solid #22d3ee;
border-radius: 0.16rem;
background: rgba(34, 211, 238, 0.14);
}
.preset-option-swatch.signal-flow {
border-color: #29415f;
background: linear-gradient(135deg, #07101e 10%, #10243c 100%);
}
.preset-option-swatch.signal-flow::before {
top: 50%;
right: 0.2rem;
left: 0.2rem;
height: 1px;
background: linear-gradient(90deg, #67e8f9, #c4b5fd);
box-shadow: 0 0 8px #67e8f9;
}
.preset-option-swatch.signal-flow::after {
top: calc(50% - 0.13rem);
right: 0.28rem;
width: 0.26rem;
height: 0.26rem;
border-radius: 50%;
background: #67e8f9;
box-shadow: 0 0 7px #67e8f9;
}
.preset-option-swatch.blueprint {
border-color: #34799a;
border-radius: 0.08rem;
background-color: #0a2031;
background-image:
linear-gradient(rgba(102, 217, 239, 0.18) 1px, transparent 1px),
linear-gradient(90deg, rgba(102, 217, 239, 0.18) 1px, transparent 1px);
background-size: 0.42rem 0.42rem;
}
.preset-option-swatch.blueprint::before {
top: 0.34rem;
right: 0.38rem;
bottom: 0.34rem;
left: 0.38rem;
border: 1px solid #66d9ef;
}
.preset-option-swatch.editorial {
border-color: #b8aa94;
border-radius: 0.08rem;
background: repeating-linear-gradient(
0deg,
#f2eee5 0 0.34rem,
#d8d0c2 0.36rem,
#f2eee5 0.38rem
);
}
.preset-option-swatch.editorial::before {
top: 0;
bottom: 0;
left: 0.42rem;
width: 1px;
background: #c9502d;
opacity: 0.72;
}
.preset-option-swatch.editorial::after {
top: 0.52rem;
right: 0.45rem;
width: 0.38rem;
height: 0.38rem;
background: #c9502d;
transform: rotate(45deg);
}
.preset-option-copy {
min-width: 0;
}
.preset-option-copy strong,
.preset-option-copy small {
display: block;
}
.preset-option-copy strong {
color: var(--toolbar-text);
font-size: 0.72rem;
font-weight: 600;
}
.preset-option-copy small {
margin-top: 0.16rem;
overflow: hidden;
color: var(--text-faint);
font-size: 0.57rem;
font-weight: 400;
white-space: nowrap;
text-overflow: ellipsis;
}
.preset-option-check {
display: inline-block;
width: 0.9rem;
height: 0.9rem;
color: var(--frontend-stroke);
background: currentColor;
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='m3 8.5 3 3 7-7' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: contain;
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='m3 8.5 3 3 7-7' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
opacity: 0;
}
.preset-option[aria-checked="true"] .preset-option-check {
opacity: 1;
}
html[data-preset="editorial"] .toolbar button,
html[data-preset="editorial"] .preset-menu,
html[data-preset="editorial"] .export-menu,
html[data-preset="editorial"] .overview-map,
html[data-preset="editorial"] .semantic-lens,
html[data-preset="editorial"] .route-probe,
html[data-preset="editorial"] .focus-chip,
html[data-preset="editorial"] .diagram-guide,
html[data-preset="editorial"] .node-finder,
html[data-preset="editorial"] .guided-views,
html[data-preset="editorial"] .share-chapter-cue {
border-radius: 0.3rem;
}
.motion-control-dot {
width: 0.46rem;
height: 0.46rem;
flex: 0 0 auto;
border: 1px solid var(--frontend-stroke);
border-radius: 50%;
background: var(--frontend-stroke);
box-shadow: 0 0 9px color-mix(in srgb, var(--frontend-stroke) 72%, transparent);
}
#btn-motion[aria-pressed="false"] .motion-control-dot {
background: transparent;
box-shadow: none;
}
.toolbar .export-menu {
position: absolute;
top: calc(100% + 0.375rem);
right: 0;
background: var(--toolbar-menu-bg);
border: 1px solid var(--toolbar-border);
width: 19rem;
max-width: calc(100vw - 2rem);
max-height: calc(100vh - 5.5rem);
overflow-y: auto;
border-radius: 0.875rem;
padding: 0.55rem;
display: none;
flex-direction: column;
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.24);
}
.toolbar .export-menu.open {
display: flex;
}
.export-menu-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
padding: 0.5rem 0.55rem 0.72rem;
}
.export-menu-header strong,
.export-menu-header span {
display: block;
}
.export-menu-header strong {
color: var(--toolbar-text);
font-size: 0.78rem;
font-weight: 650;
letter-spacing: -0.015em;
}
.export-menu-header span {
margin-top: 0.16rem;
color: var(--text-faint);
font-size: 0.56rem;
}
.export-menu-header kbd {
padding: 0.16rem 0.36rem;
border: 1px solid var(--toolbar-border);
border-radius: 0.3rem;
color: var(--text-faint);
background: color-mix(in srgb, var(--toolbar-border) 16%, transparent);
font-family: inherit;
font-size: 0.54rem;
font-weight: 600;
}
.export-menu-section {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 0.18rem;
}
.export-menu-section + .export-menu-section {
margin-top: 0.42rem;
padding-top: 0.42rem;
border-top: 1px solid color-mix(in srgb, var(--toolbar-border) 68%, transparent);
}
.export-menu-heading {
grid-column: 1 / -1;
display: block;
padding: 0.24rem 0.55rem 0.2rem;
color: var(--text-faint);
font-size: 0.54rem;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.toolbar .export-menu button {
display: grid;
grid-template-columns: 1.15rem minmax(0, 1fr);
align-items: center;
column-gap: 0.52rem;
background: transparent;
border: 1px solid transparent;
box-shadow: none;
backdrop-filter: none;
width: 100%;
min-height: 3rem;
text-align: left;
padding: 0.48rem 0.55rem;
border-radius: 0.5rem;
white-space: nowrap;
}
.export-item-copy {
display: block;
min-width: 0;
}
.export-item-copy strong,
.export-item-copy small {
display: block;
}
.export-item-copy strong {
overflow: hidden;
color: inherit;
font-size: 0.7rem;
font-weight: 550;
text-overflow: ellipsis;
}
.export-item-copy small {
margin-top: 0.12rem;
overflow: hidden;
color: var(--text-faint);
font-size: 0.55rem;
font-weight: 450;
text-overflow: ellipsis;
}
.toolbar .export-menu button::before {
width: 1rem;
height: 1rem;
background: currentColor;
content: "";
opacity: 0.72;
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: contain;
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
}
.toolbar .export-menu button[data-format="share-card"]::before,
.toolbar .export-menu button[data-action="route-share-card"]::before,
.toolbar .export-menu button[data-action="reach-share-card"]::before {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath fill='none' stroke='black' stroke-width='1.7' d='M3 4.5h14v11H3zM6 12l2.5-2.5 2 2 1.5-1.5 2 2'/%3E%3Ccircle cx='13.8' cy='7.3' r='1.2' fill='black'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath fill='none' stroke='black' stroke-width='1.7' d='M3 4.5h14v11H3zM6 12l2.5-2.5 2 2 1.5-1.5 2 2'/%3E%3Ccircle cx='13.8' cy='7.3' r='1.2' fill='black'/%3E%3C/svg%3E");
}
.toolbar .export-menu button[data-action^="copy"]::before {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='6' y='6' width='10' height='10' rx='1.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M4 13H3.5A1.5 1.5 0 012 11.5v-8A1.5 1.5 0 013.5 2h8A1.5 1.5 0 0113 3.5V4' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='6' y='6' width='10' height='10' rx='1.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M4 13H3.5A1.5 1.5 0 012 11.5v-8A1.5 1.5 0 013.5 2h8A1.5 1.5 0 0113 3.5V4' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E");
}
.toolbar .export-menu button[data-format="png"]::before,
.toolbar .export-menu button[data-format="jpeg"]::before,
.toolbar .export-menu button[data-format="webp"]::before {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M5 2.5h7l3 3v12H5zM12 2.5v3h3' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M7.5 14l2-2 1.5 1.5 1.5-1.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M5 2.5h7l3 3v12H5zM12 2.5v3h3' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M7.5 14l2-2 1.5 1.5 1.5-1.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E");
}
.toolbar .export-menu button[data-format="svg"]::before,
.toolbar .export-menu button[data-format="webm"]::before {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E");
}
.toolbar .export-menu button[data-format="share-card"],
.toolbar .export-menu button[data-action="route-share-card"],
.toolbar .export-menu button[data-action="reach-share-card"] {
min-height: 3.35rem;
color: var(--toolbar-text);
background: color-mix(in srgb, var(--frontend-stroke) 8%, transparent);
border-color: color-mix(in srgb, var(--frontend-stroke) 24%, var(--toolbar-border));
}
.toolbar .export-menu button[hidden] {
display: none;
}
.toolbar .export-menu button:hover {
background: var(--toolbar-hover);
}
.toolbar .export-menu button:focus-visible {
background: var(--toolbar-hover);
outline-offset: -2px;
}
.toolbar .export-menu button:disabled {
color: var(--text-faint);
background: color-mix(in srgb, var(--toolbar-border) 18%, transparent);
cursor: not-allowed;
}
.toolbar .export-menu button:disabled:hover {
border-color: transparent;
}
.toolbar .export-menu .hint {
color: var(--text-faint);
font-size: 0.55rem;
}
/* Toast — brief feedback for actions like "copied" */
.archify-toast {
position: fixed;
top: 1rem;
left: 50%;
transform: translateX(-50%) translateY(-8px);
background: var(--toolbar-menu-bg);
color: var(--text);
border: 1px solid var(--toolbar-border);
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-family: inherit;
font-size: 0.75rem;
opacity: 0;
transition:
opacity 0.2s,
transform 0.2s;
z-index: 200;
pointer-events: none;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
}
.archify-toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
@media (max-width: 720px) {
body {
padding: 1rem;
}
.toolbar .preset-menu,
.toolbar .export-menu {
position: fixed;
top: 4.5rem;
right: 1rem;
left: 1rem;
width: auto;
max-width: none;
}
.toolbar {
position: relative;
justify-content: flex-end;
width: max-content;
max-width: 100%;
margin-left: auto;
margin-bottom: 1rem;
}
.header {
margin-bottom: 1.25rem;
padding-right: 0;
}
html[data-preset="signal-flow"] .header,
html[data-preset="blueprint"] .header,
html[data-preset="editorial"] .header {
padding-right: 0;
}
.header-row {
gap: 0.75rem;
align-items: flex-start;
}
html[data-preset="signal-flow"] .header-row,
html[data-preset="blueprint"] .header-row,
html[data-preset="editorial"] .header-row {
flex-wrap: wrap;
}
html[data-preset="signal-flow"] .header-row::after {
margin-left: 0;
order: 3;
}
html[data-preset="blueprint"] .header-row::after {
margin-left: 0;
order: 3;
}
html[data-preset="editorial"] .header-row::after {
margin-left: 0;
order: 3;
}
h1 {
font-size: 1.25rem;
line-height: 1.25;
}
.subtitle {
margin-left: 0;
font-size: 0.8125rem;
line-height: 1.55;
}
.diagram-container {
padding: 0.75rem;
border-radius: 0.75rem;
}
html:not([data-embed="true"]) .diagram-container {
padding: 0.75rem 0.75rem 4.25rem;
}
html:not([data-embed="true"]) .diagram-container[data-wide-diagram="true"] {
overflow-x: auto;
overflow-y: hidden;
overscroll-behavior-x: contain;
scrollbar-width: thin;
scroll-behavior: smooth;
}
html:not([data-embed="true"]) .diagram-container[data-wide-diagram="true"] > svg {
min-width: 720px;
}
html:not([data-embed="true"]) .diagram-container[data-wide-diagram="true"] .diagram-nav,
html:not([data-embed="true"]) .diagram-container[data-wide-diagram="true"] .overview-map,
html:not([data-embed="true"]) .diagram-container[data-wide-diagram="true"] .focus-chip,
html:not([data-embed="true"]) .diagram-container[data-wide-diagram="true"] .route-probe,
html:not([data-embed="true"]) .diagram-container[data-wide-diagram="true"] .semantic-lens,
html:not([data-embed="true"]) .diagram-container[data-wide-diagram="true"] .diagram-guide,
html:not([data-embed="true"]) .diagram-container[data-wide-diagram="true"] .node-finder {
transform: translateX(var(--archify-scroll-x, 0px));
}
.diagram-nav {
right: 0.5rem;
bottom: 0.5rem;
}
.diagram-nav button {
min-width: 2.75rem;
height: 2.75rem;
}
.diagram-nav [data-view="reset"] {
min-width: 3.75rem;
}
.diagram-nav [data-view="reset"][data-detail-visible] {
min-width: 4.35rem;
}
.overview-map {
right: 0.5rem;
bottom: 3.7rem;
width: min(10.25rem, calc(100% - 1rem));
}
.overview-map[data-docked="true"] {
right: var(--archify-radar-right, 0.5rem);
top: var(--archify-radar-top, 0.5rem);
bottom: auto;
transform: none !important;
}
.overview-map[data-docked="true"][data-dock-side="left"] {
right: auto;
left: var(--archify-radar-left, 0.5rem);
}
.overview-map-surface {
height: 4rem;
}
.overview-map-feedback {
right: 0.5rem;
bottom: 3.7rem;
}
.semantic-lens {
right: 0.5rem;
bottom: 3.7rem;
width: calc(100% - 1rem);
max-height: calc(100% - 4.2rem);
}
.semantic-lens[data-dock-side] {
left: auto;
right: 0.5rem;
}
.semantic-lens-kinds {
max-height: min(12rem, 34vh);
}
.focus-chip {
left: 0.5rem;
top: 0.5rem;
width: calc(100% - 1rem);
max-width: none;
}
.relationship-lens-list {
max-height: min(12rem, 38vh);
}
.relationship-lens-actions #btn-focus-relations {
display: block;
color: var(--database-stroke);
}
.focus-chip:not([data-relations-expanded="true"]) .relationship-lens-head {
border-bottom-color: transparent;
}
.focus-chip:not([data-relations-expanded="true"]) .relationship-lens-list {
display: none;
}
/* While a mobile reader previews one relationship, turn the full Lens
into a compact peek card. This keeps the exact source and target on
canvas instead of covering them with a long, scrollable list. */
.focus-chip[data-relationship-previewing="true"] .relationship-lens-list {
max-height: none;
padding: 0.35rem;
overflow: hidden;
}
.focus-chip[data-relationship-previewing="true"] .relationship-lens-group {
margin: 0;
}
.focus-chip[data-relationship-previewing="true"] .relationship-lens-group-title,
.focus-chip[data-relationship-previewing="true"]
.relationship-lens-row:not([data-preview-active="true"]) {
display: none;
}
.node-finder {
top: 0.5rem;
left: 0.5rem;
width: min(22rem, calc(100% - 1rem));
max-height: calc(100% - 1rem);
}
.diagram-guide {
top: 0.5rem;
right: 0.5rem;
left: 0.5rem;
width: auto;
max-height: calc(100% - 1rem);
}
.diagram-guide-head {
padding: 0.62rem 0.7rem 0.52rem;
}
.diagram-guide-stats {
padding: 0.42rem 0.7rem;
}
.diagram-guide-actions {
grid-template-columns: 1fr;
gap: 0.28rem;
padding: 0.42rem;
}
.diagram-guide-action {
min-height: 3.1rem;
padding: 0.42rem 0.48rem;
}
.diagram-guide-action small {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.diagram-guide-index {
width: 1.5rem;
height: 1.5rem;
}
.diagram-guide-shortcuts {
padding: 0.46rem 0.7rem 0.52rem;
}
.diagram-guide-action[data-guide-action="present"] {
grid-column: auto;
}
.route-probe[data-guide-open="true"] {
visibility: hidden;
pointer-events: none;
}
.node-finder-results {
max-height: min(16rem, 38vh);
}
.route-probe {
left: 0.5rem;
bottom: 3.7rem;
width: calc(100% - 1rem);
min-width: 0;
}
.route-probe[data-route-dock="top"] {
top: 0.5rem;
bottom: auto;
}
.route-probe[data-finder-open="true"] {
visibility: hidden;
pointer-events: none;
}
.route-probe-head {
padding: 0.45rem 0.48rem 0.38rem;
}
.route-probe-path {
padding: 0.35rem 0.48rem 0.2rem;
}
.route-probe-node {
min-height: 2.75rem !important;
}
.route-journey-controls {
grid-template-columns: 2.75rem minmax(0, 1fr) 2.75rem minmax(0, 1fr);
padding: 0.08rem 0.48rem 0.32rem;
}
.route-journey-controls button {
min-height: 2.75rem;
}
.route-probe-status {
padding: 0.05rem 0.48rem 0.38rem;
}
.route-probe-node {
max-width: 7.5rem;
}
.guided-views {
grid-template-columns: 2.75rem minmax(0, 1fr) 2.75rem;
gap: 0.2rem;
padding: 0.25rem;
}
.guided-view-actions {
grid-column: 1 / -1;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.guided-views > #guided-view-prev,
.guided-views > #guided-view-next {
align-self: center;
height: 2.75rem;
}
.guided-view-play,
.guided-view-all,
.guided-view-beat-link {
min-height: 2.75rem;
}
.guided-view-copy small {
white-space: normal;
}
.guided-story-caption {
padding: 0.32rem 0.45rem;
}
.guided-story-caption-index {
min-width: 2.9rem;
padding-right: 0.42rem;
}
.guided-story-caption strong,
.guided-story-caption small {
white-space: normal;
}
.guided-story-caption-next {
display: none;
}
.guided-view-trail {
min-height: 2rem;
}
.guided-view-trail .guided-view-stop {
min-height: 2rem;
padding: 0.22rem 0.42rem 0.22rem 0.28rem;
}
.guided-view-handoff {
max-width: min(9rem, 32vw);
}
.guided-view-index {
padding-top: 0.35rem;
}
.guided-view-chapters {
gap: 0.35rem;
scroll-padding-inline: 0.2rem;
}
.guided-view-chapters li {
flex: 0 0 min(14rem, 78vw);
min-width: 0;
}
.guided-view-chapter {
min-height: 2.75rem;
}
.guided-views[data-active-view="all"] {
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.2rem;
padding: 0.25rem;
}
.guided-views[data-active-view="all"] .guided-view-copy {
grid-column: 1;
grid-row: 1;
}
.guided-views[data-active-view="all"] .guided-view-actions {
grid-column: 2;
grid-row: 1;
}
.guided-views[data-active-view="all"] .guided-view-play {
min-width: 6rem !important;
min-height: 2.75rem;
}
.guided-views[data-active-view="all"] .guided-view-index {
grid-column: 1 / -1;
grid-row: 2;
padding: 0.3rem 0.08rem 0;
border-top: 1px solid color-mix(in srgb, var(--toolbar-border) 72%, transparent);
border-left: 0;
}
html[data-embed="true"][data-share-playback="true"] .share-chapter-cue:not([hidden]),
html[data-embed="true"][data-share-moment="true"] .share-chapter-cue:not([hidden]) {
top: 0.5rem;
grid-template-columns: auto minmax(0, 1fr);
gap: 0.28rem 0.55rem;
width: calc(100% - 1rem);
min-height: 0;
padding: 0.45rem 0.55rem 0.55rem;
}
html[data-embed="true"][data-share-playback="true"] .diagram-container {
padding-top: 4.25rem;
}
html[data-embed="true"][data-share-moment="true"] .diagram-container {
padding-top: 4.25rem;
}
.share-chapter-index {
min-width: 4.4rem;
padding-right: 0.5rem;
}
.share-chapter-copy small {
font-size: 0.46rem;
}
.share-chapter-route {
grid-column: 1 / -1;
font-size: 0.5rem;
text-align: left;
}
.cards {
grid-template-columns: 1fr;
margin-top: 1rem;
}
html[data-present="true"]:not([data-embed="true"]) body {
padding: 0.5rem;
}
html[data-present="true"]:not([data-embed="true"]) .container {
gap: 0.5rem;
height: calc(100dvh - 1rem);
}
html[data-present="true"]:not([data-embed="true"]) .toolbar {
position: fixed;
top: 0.5rem;
right: 0.5rem;
margin: 0;
gap: 0.25rem;
}
html[data-present="true"]:not([data-embed="true"]) .toolbar button {
min-height: 2.75rem;
padding: 0.42rem 0.58rem;
}
html[data-present="true"]:not([data-embed="true"]) .header {
min-height: 2.25rem;
padding: 3.5rem 0 0;
}
html[data-present="true"]:not([data-embed="true"]) .header-row {
min-height: 2.25rem;
margin: 0;
align-items: center;
flex-wrap: nowrap;
}
html[data-present="true"]:not([data-embed="true"]) .header-row::after,
html[data-present="true"]:not([data-embed="true"]) .pulse-dot,
html[data-present="true"]:not([data-embed="true"]) .subtitle {
display: none;
}
html[data-present="true"]:not([data-embed="true"]) h1 {
flex: 1 1 auto;
min-width: 0;
max-width: 100%;
overflow: hidden;
font-size: 0.875rem;
text-overflow: ellipsis;
white-space: nowrap;
}
html[data-present="true"]:not([data-embed="true"]) .guided-views {
padding: 0.2rem;
}
html[data-present="true"]:not([data-embed="true"]) .guided-view-copy small {
display: none;
}
html[data-present="true"]:not([data-embed="true"]) .guided-story-caption small {
display: block;
}
html[data-present="true"]:not([data-embed="true"]) .diagram-container {
padding: 0.5rem 0.5rem 3.75rem;
}
html[data-present="true"]:not([data-embed="true"])
.diagram-container[data-wide-diagram="true"]
> svg {
width: 720px;
height: auto;
min-width: 720px;
}
}
@media (max-width: 420px) {
.export-menu-section {
grid-template-columns: 1fr;
}
.export-menu-heading,
.toolbar .export-menu button[data-format="share-card"],
.toolbar .export-menu button[data-action="route-share-card"],
.toolbar .export-menu button[data-action="reach-share-card"] {
grid-column: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.guided-story-caption {
animation: none !important;
}
.toolbar .preset-menu.open,
.toolbar .export-menu.open {
animation: none;
}
.toolbar-chevron {
transition: none;
}
}
@media (max-width: 360px) {
.toolbar {
gap: 0.25rem;
}
.toolbar button {
padding-right: 0.58rem;
padding-left: 0.58rem;
}
.toolbar #btn-motion {
min-width: 4.4rem;
padding-right: 0.58rem;
padding-left: 0.58rem;
}
#theme-label,
#preset-label,
#present-label {
display: none;
}
.guided-view-handoff {
max-width: 6.5rem;
}
}
/* ==========================================================
SVG SEMANTIC CLASSES — use these instead of inline fill/stroke
========================================================== */
.c-grid {
stroke: var(--grid);
fill: none;
}
.c-mask {
fill: var(--mask);
stroke: none;
}
.c-frontend {
fill: var(--frontend-fill);
stroke: var(--frontend-stroke);
}
.c-backend {
fill: var(--backend-fill);
stroke: var(--backend-stroke);
}
.c-database {
fill: var(--database-fill);
stroke: var(--database-stroke);
}
.c-cloud {
fill: var(--cloud-fill);
stroke: var(--cloud-stroke);
}
.c-security {
fill: var(--security-fill);
stroke: var(--security-stroke);
}
.c-messagebus {
fill: var(--messagebus-fill);
stroke: var(--messagebus-stroke);
}
.c-external {
fill: var(--external-fill);
stroke: var(--external-stroke);
}
/* Text helpers */
.t-primary {
fill: var(--text);
}
.t-muted {
fill: var(--text-muted);
}
.t-dim {
fill: var(--text-dim);
}
.t-frontend {
fill: var(--frontend-stroke);
}
.t-backend {
fill: var(--backend-stroke);
}
.t-database {
fill: var(--database-stroke);
}
.t-cloud {
fill: var(--cloud-stroke);
}
.t-security {
fill: var(--security-stroke);
}
.t-messagebus {
fill: var(--messagebus-stroke);
}
.t-external {
fill: var(--external-stroke);
}
/* Semantic Sigils are small authored role stamps, not icons from a brand
pack and not viewer controls. Prefix selectors with svg so the canonical
export's SVG-only stylesheet collector keeps them automatically. */
svg .semantic-sigil {
fill: none;
stroke: currentColor;
stroke-width: 1.35;
stroke-linecap: round;
stroke-linejoin: round;
opacity: 0.76;
pointer-events: none;
}
svg .semantic-sigil > * {
vector-effect: non-scaling-stroke;
}
svg .semantic-sigil .sigil-fill {
fill: currentColor;
stroke: none;
}
svg .s-frontend {
color: var(--frontend-stroke);
}
svg .s-backend {
color: var(--backend-stroke);
}
svg .s-database {
color: var(--database-stroke);
}
svg .s-cloud {
color: var(--cloud-stroke);
}
svg .s-security {
color: var(--security-stroke);
}
svg .s-messagebus {
color: var(--messagebus-stroke);
}
svg .s-external {
color: var(--external-stroke);
}
/* Brand Marks are authored identity badges. Their color is contained
inside one neutral plate so vendor color never replaces Archify's
semantic node and relationship vocabulary. */
svg .brand-mark {
pointer-events: none;
}
svg .brand-mark-badge {
fill: #fff;
stroke: none;
}
svg .brand-mark-frame {
fill: none;
stroke: #cbd5e1;
stroke-width: 0.8;
vector-effect: non-scaling-stroke;
}
svg .brand-mark-fallback {
fill: none;
stroke: #475569;
stroke-width: 1.15;
stroke-linecap: round;
stroke-linejoin: round;
}
svg[data-preset="blueprint"] .brand-mark-badge,
svg[data-preset="blueprint"] .brand-mark-frame {
rx: 1px;
}
/* Reading Depth is a viewer-only level-of-detail layer. MAP preserves the
authored structure and primary labels, READ restores relationship labels
and node context, and FULL restores tags and fine annotations. Semantic
intent can reveal the relevant detail without moving any geometry. */
svg [data-detail] {
transition: opacity 160ms ease;
}
svg [data-detail-anchor] {
transform-box: fill-box;
transform-origin: center;
transition: transform 160ms ease;
}
.diagram-container[data-detail-level="map"] svg [data-detail="context"],
.diagram-container[data-detail-level="map"] svg [data-detail="fine"],
.diagram-container[data-detail-level="read"] svg [data-detail="fine"] {
opacity: 0;
pointer-events: none;
}
.diagram-container[data-detail-level="map"] svg [data-detail-anchor] {
transform: translateY(8px);
}
/* Focus, lens, preview, route, and story states are stronger reader intent than
the global zoom level. Reveal only their exact semantic matches. */
svg[data-focus-active] [data-focus-match][data-detail],
svg[data-focus-active] [data-focus-match] [data-detail],
svg[data-reach-active] [data-reach-match][data-detail],
svg[data-reach-active] [data-reach-match] [data-detail],
svg[data-lens-active] [data-lens-match][data-detail],
svg[data-lens-active] [data-lens-match] [data-detail],
svg[data-legend-preview-active] [data-legend-preview-match][data-detail],
svg[data-legend-preview-active] [data-legend-preview-match] [data-detail],
svg[data-intent-trace-active] [data-intent-trace-match][data-detail],
svg[data-intent-trace-active] [data-intent-trace-match] [data-detail],
svg[data-route-active] [data-route-match][data-detail],
svg[data-route-active] [data-route-match] [data-detail],
svg[data-story-active] [data-story-step][data-detail],
svg[data-story-active] [data-story-step] [data-detail],
svg[data-relationship-preview-active] [data-relationship-preview][data-detail],
svg[data-relationship-preview-active] [data-relationship-preview] [data-detail],
svg [data-node-id]:hover [data-detail],
svg [data-node-id]:focus-visible [data-detail] {
opacity: 1;
pointer-events: auto;
}
svg[data-focus-active] [data-focus-match] [data-detail-anchor],
svg[data-reach-active] [data-reach-match] [data-detail-anchor],
svg[data-lens-active] [data-lens-match] [data-detail-anchor],
svg[data-legend-preview-active] [data-legend-preview-match] [data-detail-anchor],
svg[data-intent-trace-active] [data-intent-trace-match] [data-detail-anchor],
svg[data-route-active] [data-route-match] [data-detail-anchor],
svg[data-story-active] [data-story-step] [data-detail-anchor],
svg [data-node-id]:hover [data-detail-anchor],
svg [data-node-id]:focus-visible [data-detail-anchor] {
transform: none;
}
/* Arrows */
.a-default {
stroke: var(--arrow);
fill: none;
}
.a-emphasis {
stroke: var(--arrow-emphasis);
fill: none;
}
.a-security {
stroke: var(--security-stroke);
fill: none;
stroke-dasharray: 5, 5;
}
.a-dashed {
stroke: var(--database-stroke);
fill: none;
stroke-dasharray: 4, 4;
}
/* Arrowhead markers reference these */
.m-default {
fill: var(--arrow);
}
.m-emphasis {
fill: var(--arrow-emphasis);
}
.m-security {
fill: var(--security-stroke);
}
.m-dashed {
fill: var(--database-stroke);
}
/* Stable semantic exploration hooks emitted by every renderer. */
svg [data-node-id] {
cursor: pointer;
transition:
opacity 0.18s ease,
filter 0.18s ease;
}
svg [data-edge-from] {
transition:
opacity 0.18s ease,
filter 0.18s ease;
}
svg [data-node-id]:hover,
svg [data-node-id]:focus-visible {
filter: drop-shadow(0 0 7px var(--frontend-stroke));
outline: none;
}
/* Verified Source Beacons are installed at runtime only for nodes whose
repository evidence passed the local revision/blob/line checks. They
live inside the owning node so every focus, route, story, and guided
view keeps one coherent opacity state. Export serialization removes
them before producing the canonical SVG or any derived visual. */
.source-evidence-beacon {
pointer-events: none;
opacity: 0.76;
transition:
opacity 160ms ease,
filter 160ms ease;
}
.source-evidence-beacon rect {
fill: color-mix(in srgb, var(--mask) 92%, var(--backend-stroke));
stroke: color-mix(in srgb, var(--backend-stroke) 82%, var(--panel-border));
stroke-width: 1;
vector-effect: non-scaling-stroke;
}
.source-evidence-beacon text {
fill: var(--backend-stroke);
font-size: 5.5px;
font-weight: 800;
letter-spacing: 0.06em;
text-anchor: middle;
text-transform: uppercase;
}
svg [data-node-id]:hover .source-evidence-beacon,
svg [data-node-id]:focus-visible .source-evidence-beacon,
svg [data-node-id][data-focus-selected] .source-evidence-beacon {
opacity: 1;
filter: drop-shadow(0 0 4px color-mix(in srgb, var(--backend-stroke) 72%, transparent));
}
svg[data-preset="signal-flow"] .source-evidence-beacon {
opacity: 0.9;
}
svg[data-preset="blueprint"] .source-evidence-beacon rect {
rx: 0.8px;
filter: none;
}
svg [data-legend-kind][role="button"] {
cursor: pointer;
}
svg [data-legend-kind]:focus {
outline: none;
}
svg [data-legend-hit] {
fill: transparent;
stroke: transparent;
stroke-width: 1.4;
pointer-events: all;
vector-effect: non-scaling-stroke;
transition:
stroke 150ms ease,
stroke-width 150ms ease,
filter 150ms ease;
}
svg [data-legend-count-badge] {
pointer-events: none;
}
svg [data-legend-count-badge] rect {
fill: var(--mask);
stroke: var(--panel-border);
stroke-width: 1;
vector-effect: non-scaling-stroke;
}
svg [data-legend-count-badge] text {
fill: var(--text-muted);
font-size: 8px;
font-weight: 700;
text-anchor: middle;
}
svg [data-legend-kind][role="button"]:hover [data-legend-hit] {
stroke: color-mix(in srgb, var(--database-stroke) 58%, transparent);
}
svg [data-legend-kind][role="button"]:focus-visible [data-legend-hit] {
stroke: var(--frontend-stroke);
stroke-width: 1.8;
filter: drop-shadow(0 0 3px var(--frontend-stroke));
}
svg [data-legend-kind][data-legend-selected] [data-legend-hit] {
stroke: var(--database-stroke);
stroke-width: 2.2;
stroke-dasharray: 3 2;
}
svg [data-legend-kind][data-legend-selected] [data-legend-count-badge] rect {
stroke: var(--database-stroke);
stroke-width: 1.8;
}
svg [data-legend-kind][data-legend-zero] {
opacity: 0.44;
}
svg[data-preset="signal-flow"] [data-legend-kind][data-legend-selected] [data-legend-hit] {
filter: drop-shadow(0 0 4px var(--database-stroke));
}
svg[data-preset="blueprint"] [data-legend-hit],
svg[data-preset="blueprint"] [data-legend-count-badge] rect {
rx: 0;
filter: none;
}
svg[data-legend-preview-active] [data-node-id],
svg[data-legend-preview-active] [data-edge-from] {
opacity: 0.11;
}
svg[data-legend-preview-active] [data-legend-preview-match] {
opacity: 1;
}
svg[data-legend-preview-active] [data-legend-preview-peer] {
opacity: 0.62;
}
svg[data-legend-preview-active] [data-legend-preview-selected] {
filter: drop-shadow(0 0 8px var(--database-stroke));
}
svg[data-lens-active] [data-node-id],
svg[data-lens-active] [data-edge-from] {
opacity: 0.11;
}
svg[data-lens-active] [data-lens-match] {
opacity: 1;
}
svg[data-lens-active] [data-lens-peer] {
opacity: 0.62;
}
svg[data-lens-active] [data-lens-selected] {
filter: drop-shadow(0 0 10px var(--database-stroke));
}
.semantic-lens-overlay {
pointer-events: none;
}
.semantic-lens-flow {
fill: none;
stroke-width: 3.05;
stroke-linecap: round;
stroke-dasharray: 0.075 0.925;
stroke-dashoffset: 0;
vector-effect: non-scaling-stroke;
opacity: 0.9;
animation: archify-semantic-lens-flow 1.35s linear 1 both;
animation-delay: var(--lens-flow-delay, 0s);
}
.semantic-lens-flow[data-direction="out"],
.semantic-lens-flow[data-direction="forward"] {
stroke: var(--frontend-stroke);
filter: drop-shadow(0 0 3px var(--frontend-stroke));
}
.semantic-lens-flow[data-direction="in"],
.semantic-lens-flow[data-direction="reverse"] {
stroke: var(--database-stroke);
filter: drop-shadow(0 0 3px var(--database-stroke));
}
.semantic-lens-flow[data-direction="within"] {
stroke: var(--messagebus-stroke);
filter: drop-shadow(0 0 3px var(--messagebus-stroke));
}
svg[data-preset="signal-flow"] .semantic-lens-flow {
stroke-width: 3.7;
opacity: 1;
}
svg[data-preset="signal-flow"] .semantic-lens-flow[data-direction="out"],
svg[data-preset="signal-flow"] .semantic-lens-flow[data-direction="forward"] {
filter: drop-shadow(0 0 6px var(--frontend-stroke));
}
svg[data-preset="signal-flow"] .semantic-lens-flow[data-direction="in"],
svg[data-preset="signal-flow"] .semantic-lens-flow[data-direction="reverse"] {
filter: drop-shadow(0 0 6px var(--database-stroke));
}
svg[data-preset="signal-flow"] .semantic-lens-flow[data-direction="within"] {
filter: drop-shadow(0 0 6px var(--messagebus-stroke));
}
svg[data-preset="blueprint"] .semantic-lens-flow {
stroke-width: 2.45;
stroke-linecap: square;
filter: none;
opacity: 0.78;
}
html[data-embed="true"] .semantic-lens-overlay {
display: none;
}
svg[data-focus-active] [data-node-id],
svg[data-focus-active] [data-edge-from] {
opacity: 0.13;
}
svg[data-focus-active] [data-focus-match] {
opacity: 1;
}
svg[data-focus-active] [data-focus-selected] {
filter: drop-shadow(0 0 10px var(--frontend-stroke));
}
/* Authored Reachability is a bounded graph query over the relationships
already present in the artifact. Upstream walks incoming edges;
downstream walks outgoing edges. It never claims runtime causality and
changes only viewer emphasis, not canonical geometry or export bytes. */
svg[data-reach-active] [data-node-id],
svg[data-reach-active] [data-edge-from] {
opacity: 0.09;
}
svg[data-reach-active] [data-reach-match] {
opacity: 1;
}
svg[data-reach-active="upstream"] [data-reach-origin] {
filter: drop-shadow(0 0 11px var(--database-stroke));
}
svg[data-reach-active="downstream"] [data-reach-origin] {
filter: drop-shadow(0 0 11px var(--backend-stroke));
}
svg[data-reach-active="upstream"] [data-edge-from][data-reach-match] {
filter: drop-shadow(0 0 4px color-mix(in srgb, var(--database-stroke) 72%, transparent));
}
svg[data-reach-active="downstream"] [data-edge-from][data-reach-match] {
filter: drop-shadow(0 0 4px color-mix(in srgb, var(--backend-stroke) 72%, transparent));
}
svg[data-preset="blueprint"][data-reach-active] [data-reach-origin],
svg[data-preset="blueprint"][data-reach-active] [data-edge-from][data-reach-match] {
filter: none;
}
/* Direct Relationship Explorer makes authored paths a real input surface
without changing their visible stroke or canonical SVG. A transparent
runtime rail supplies forgiving pointer geometry; the exact authored
path and endpoints continue to carry every visible state. */
.relationship-hit-overlay {
pointer-events: none;
}
.relationship-hit-target {
outline: none;
}
.relationship-hit-rail {
fill: none;
stroke: transparent;
stroke-width: 24;
stroke-linecap: round;
stroke-linejoin: round;
vector-effect: non-scaling-stroke;
pointer-events: stroke;
cursor: pointer;
}
.relationship-focus-rail {
fill: none;
stroke: color-mix(in srgb, var(--frontend-stroke) 34%, transparent);
stroke-width: 6;
stroke-dasharray: 2 4;
stroke-linecap: round;
stroke-linejoin: round;
vector-effect: non-scaling-stroke;
pointer-events: none;
opacity: 0;
}
.relationship-hit-target:focus-visible .relationship-focus-rail {
opacity: 1;
}
svg[data-preset="blueprint"] .relationship-hit-target:focus-visible .relationship-focus-rail {
stroke-linecap: square;
filter: none;
}
svg[data-relationship-direct-active] [data-node-id],
svg[data-relationship-direct-active] [data-edge-from] {
opacity: 0.16;
}
svg[data-relationship-direct-active] [data-relationship-preview],
svg[data-relationship-direct-active] [data-relationship-preview-node] {
opacity: 1;
}
svg[data-focus-active]:not([data-relationship-pin-active]) .relationship-hit-rail,
svg[data-story-active] .relationship-hit-rail,
svg[data-route-picking] .relationship-hit-rail,
svg[data-route-active] .relationship-hit-rail,
svg[data-lens-active] .relationship-hit-rail,
svg[data-chapter-preview] .relationship-hit-rail {
pointer-events: none;
}
html[data-embed="true"] .relationship-hit-overlay {
display: none;
}
@media (hover: none), (pointer: coarse) {
.relationship-hit-rail {
stroke-width: 24;
}
}
/* Relationship Preview is temporary exploration state layered on top of
neighborhood focus. It changes emphasis only; exported geometry and the
relationship's original solid/dashed visual language stay untouched. */
svg[data-relationship-preview-active] [data-focus-match] {
opacity: 0.18;
}
svg[data-relationship-preview-active] [data-relationship-preview],
svg[data-relationship-preview-active] [data-relationship-preview-node] {
opacity: 1;
}
svg[data-relationship-preview-active] [data-relationship-preview] {
filter: drop-shadow(0 0 5px var(--arrow-emphasis));
}
svg[data-relationship-preview-active] [data-relationship-preview] path,
svg[data-relationship-preview-active] path[data-relationship-preview],
svg[data-relationship-preview-active] [data-relationship-preview] line,
svg[data-relationship-preview-active] line[data-relationship-preview],
svg[data-relationship-preview-active] [data-relationship-preview] polyline,
svg[data-relationship-preview-active] polyline[data-relationship-preview] {
stroke-width: 2.75;
}
svg[data-relationship-preview-active] [data-relationship-preview-source] {
filter: drop-shadow(0 0 7px var(--frontend-stroke));
}
svg[data-relationship-preview-active] [data-relationship-preview-target] {
filter: drop-shadow(0 0 7px var(--database-stroke));
}
.relationship-pulse-overlay {
pointer-events: none;
}
.relationship-flow-pulse {
fill: none;
stroke: var(--arrow-emphasis);
stroke-width: 3.35;
stroke-linecap: round;
stroke-dasharray: 0.085 0.915;
stroke-dashoffset: 0;
vector-effect: non-scaling-stroke;
opacity: 0;
filter: drop-shadow(0 0 4px var(--arrow-emphasis));
animation: archify-relationship-pulse 1.2s linear 1 both;
}
svg[data-preset="signal-flow"] .relationship-flow-pulse {
stroke: var(--frontend-stroke);
stroke-width: 3.85;
filter: drop-shadow(0 0 7px var(--frontend-stroke));
}
svg[data-preset="blueprint"] .relationship-flow-pulse {
stroke-width: 2.5;
stroke-linecap: square;
filter: none;
}
svg[data-preset="editorial"] .relationship-flow-pulse {
stroke: var(--arrow-emphasis);
stroke-width: 2.75;
filter: none;
}
.semantic-flow-token {
color: var(--frontend-stroke);
opacity: 0;
pointer-events: none;
}
.relationship-flow-token {
animation: archify-relationship-token-life 1.2s linear 1 both;
}
.story-flow-token {
animation: archify-relationship-token-life 0.78s linear 1 both;
}
.semantic-flow-token-halo {
fill: var(--mask);
stroke: currentColor;
stroke-width: 1.1;
vector-effect: non-scaling-stroke;
opacity: 0.96;
}
.relationship-flow-token-shape {
fill: var(--mask);
stroke: currentColor;
stroke-width: 1.35;
stroke-linecap: round;
stroke-linejoin: round;
vector-effect: non-scaling-stroke;
}
.relationship-flow-token-ink {
fill: none;
stroke: currentColor;
stroke-width: 1.05;
stroke-linecap: round;
vector-effect: non-scaling-stroke;
}
.relationship-flow-token-dot {
fill: currentColor;
stroke: none;
}
.semantic-flow-token[data-token-kind="data"] {
color: var(--database-stroke);
}
.semantic-flow-token[data-token-kind="event"] {
color: var(--messagebus-stroke);
}
.semantic-flow-token[data-token-kind="security"] {
color: var(--security-stroke);
}
.semantic-flow-token[data-token-kind="state"] {
color: var(--cloud-stroke);
}
svg[data-preset="signal-flow"] .semantic-flow-token {
filter: drop-shadow(0 0 5px currentColor);
}
svg[data-preset="blueprint"] .semantic-flow-token {
filter: none;
}
svg[data-preset="editorial"] .semantic-flow-token {
filter: none;
}
html[data-embed="true"] .relationship-pulse-overlay {
display: none;
}
/* Intent Trace is a temporary, pre-click exploration layer. Related
topology stays readable while a short viewer-only signal runs over
cloned edge geometry; the authored edge remains untouched underneath. */
svg[data-intent-trace-active] [data-node-id],
svg[data-intent-trace-active] [data-edge-from] {
opacity: 0.2;
}
svg[data-intent-trace-active] [data-intent-trace-match] {
opacity: 1;
}
svg[data-intent-trace-active] [data-intent-trace-selected] {
filter: drop-shadow(0 0 10px var(--frontend-stroke));
}
.intent-trace-overlay {
pointer-events: none;
}
.intent-trace-flow {
fill: none;
stroke-width: 3.1;
stroke-linecap: round;
stroke-dasharray: 0.1 0.9;
stroke-dashoffset: 0;
vector-effect: non-scaling-stroke;
opacity: 0.94;
animation: archify-intent-trace-flow 1.15s linear 1 both;
}
/* Direction names describe each edge relative to the focused node only.
The cloned geometry already runs from the authored source to target, so
incoming and outgoing signals must share the same playback direction. */
.intent-trace-flow[data-direction="out"] {
stroke: var(--frontend-stroke);
filter: drop-shadow(0 0 4px var(--frontend-stroke));
}
.intent-trace-flow[data-direction="in"] {
stroke: var(--database-stroke);
filter: drop-shadow(0 0 4px var(--database-stroke));
animation-direction: normal;
}
.intent-trace-flow[data-direction="loop"] {
stroke: var(--security-stroke);
filter: drop-shadow(0 0 4px var(--security-stroke));
}
svg[data-preset="blueprint"] [data-intent-trace-selected],
svg[data-preset="blueprint"] .intent-trace-flow {
filter: none;
stroke-linecap: square;
}
svg[data-preset="editorial"] [data-intent-trace-selected],
svg[data-preset="editorial"] .intent-trace-flow {
filter: none;
}
.intent-trace-status {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
/* Route Probe separates a durable two-point query from selection and
one-hop hover. Only the chosen shortest directed path stays prominent;
the traveling signal is cloned viewer geometry and never authored SVG. */
svg[data-route-picking="target"] [data-node-id] {
opacity: 0.24;
}
svg[data-route-picking="target"] [data-route-candidate],
svg[data-route-picking="target"] [data-route-start] {
opacity: 1;
}
svg[data-route-picking] [data-node-id] {
cursor: crosshair;
}
svg[data-route-picking] [data-route-start] {
filter: drop-shadow(0 0 10px var(--frontend-stroke));
}
svg[data-route-active] [data-node-id],
svg[data-route-active] [data-edge-from] {
opacity: 0.11;
}
svg[data-route-active] [data-route-match] {
opacity: 1;
}
svg[data-route-active] [data-route-start] {
filter: drop-shadow(0 0 11px var(--frontend-stroke));
}
svg[data-route-active] [data-route-end] {
filter: drop-shadow(0 0 11px var(--security-stroke));
}
svg[data-route-active] [data-route-step]:not([data-route-start]):not([data-route-end]) {
filter: drop-shadow(0 0 7px var(--backend-stroke));
}
svg[data-route-journey] [data-route-match][data-route-journey-state="past"] {
opacity: 0.62;
}
svg[data-route-journey] [data-route-match][data-route-journey-state="future"] {
opacity: 0.34;
}
svg[data-route-journey] [data-route-match][data-route-journey-state="current"] {
opacity: 1;
}
svg[data-route-journey] [data-node-id][data-route-journey-current] {
filter: drop-shadow(0 0 10px var(--backend-stroke));
}
svg[data-route-journey] [data-edge-from][data-route-journey-current] {
opacity: 1;
filter: drop-shadow(0 0 6px var(--backend-stroke));
}
.route-probe-overlay {
pointer-events: none;
}
.route-journey-overlay {
pointer-events: none;
}
.route-probe-flow {
fill: none;
stroke: var(--backend-stroke);
stroke-width: 3.25;
stroke-linecap: round;
stroke-dasharray: 0.085 0.915;
stroke-dashoffset: 0;
vector-effect: non-scaling-stroke;
opacity: 0.96;
filter: drop-shadow(0 0 5px var(--backend-stroke));
animation: archify-route-probe-flow 1.1s cubic-bezier(0.22, 1, 0.36, 1) 1 both;
animation-delay: calc(var(--route-step, 0) * 0.16s);
}
.route-journey-flow {
fill: none;
stroke: var(--backend-stroke);
stroke-width: 3.5;
stroke-linecap: round;
stroke-dasharray: 0.1 0.9;
stroke-dashoffset: 0;
vector-effect: non-scaling-stroke;
opacity: 0;
filter: drop-shadow(0 0 6px var(--backend-stroke));
animation: archify-route-journey-flow 780ms cubic-bezier(0.22, 1, 0.36, 1) 1 both;
}
svg[data-preset="blueprint"] [data-route-start],
svg[data-preset="blueprint"] [data-route-end],
svg[data-preset="blueprint"] [data-route-step],
svg[data-preset="blueprint"] .route-probe-flow {
filter: none;
stroke-linecap: square;
}
svg[data-preset="blueprint"] .route-journey-flow {
filter: none;
stroke-linecap: square;
}
svg[data-preset="editorial"] [data-route-start],
svg[data-preset="editorial"] [data-route-end],
svg[data-preset="editorial"] [data-route-step],
svg[data-preset="editorial"] .route-probe-flow,
svg[data-preset="editorial"] .route-journey-flow {
filter: none;
}
svg[data-preset="editorial"] .c-grid {
stroke-dasharray: 1 4;
opacity: 0.48;
}
svg[data-preset="editorial"] .c-region {
fill: color-mix(in srgb, var(--cloud-fill) 48%, transparent);
stroke-dasharray: 9 4;
}
svg[data-preset="editorial"] [data-node-id]:hover,
svg[data-preset="editorial"] [data-node-id]:focus-visible,
svg[data-preset="editorial"][data-focus-active] [data-focus-selected] {
filter: drop-shadow(0 2px 2px color-mix(in srgb, var(--text) 18%, transparent));
}
svg[data-preset="blueprint"] .c-grid {
stroke-dasharray: 1 3;
opacity: 0.78;
}
svg[data-preset="blueprint"] .c-region {
fill: color-mix(in srgb, var(--cloud-fill) 58%, transparent);
stroke-dasharray: 12 4 2 4;
}
svg[data-preset="blueprint"] .c-security-group,
svg[data-preset="blueprint"] .c-lane {
stroke-dasharray: 7 3;
}
svg[data-preset="blueprint"] [data-node-id]:hover,
svg[data-preset="blueprint"] [data-node-id]:focus-visible {
filter: drop-shadow(0 0 3px var(--frontend-stroke));
}
svg[data-preset="blueprint"][data-focus-active] [data-focus-selected] {
filter: drop-shadow(0 0 3px var(--frontend-stroke));
}
svg[data-preset="blueprint"][data-focus-active][data-reach-active]
[data-focus-selected][data-reach-origin] {
filter: none;
}
svg[data-preset="blueprint"][data-relationship-preview-active] [data-relationship-preview],
svg[data-preset="blueprint"][data-relationship-preview-active]
[data-relationship-preview-source],
svg[data-preset="blueprint"][data-relationship-preview-active]
[data-relationship-preview-target] {
filter: drop-shadow(0 0 3px var(--frontend-stroke));
}
/* Story Trail is a viewer-only overlay derived from the ordered focus IDs
of the active guided view. The authored edge stays underneath with its
original color, dash pattern, marker, and geometry intact. */
.story-trail-overlay {
pointer-events: none;
}
.story-carrier-overlay {
pointer-events: none;
}
.story-trail-flow {
fill: none;
stroke: var(--frontend-stroke);
stroke-width: 2.8;
stroke-linecap: round;
vector-effect: non-scaling-stroke;
opacity: 0.34;
filter: drop-shadow(0 0 2px var(--frontend-stroke));
}
svg[data-story-beat] [data-story-step] {
opacity: 0.22;
filter: saturate(0.42);
transition:
opacity 180ms ease,
filter 180ms ease;
}
svg[data-story-beat] [data-story-step][data-story-beat-state="past"] {
opacity: 0.72;
filter: saturate(0.82);
}
svg[data-story-beat] [data-story-step][data-story-beat-state="next"] {
opacity: 0.5;
filter: saturate(0.66);
}
svg[data-story-beat] [data-story-step][data-story-beat-state="active"] {
opacity: 1;
filter: drop-shadow(0 0 7px var(--frontend-stroke));
animation: archify-story-beat-node 360ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
svg[data-story-beat] [data-edge-from][data-story-beat-step] {
opacity: 0.16;
transition: opacity 160ms ease;
}
svg[data-story-beat] [data-edge-from][data-story-beat-step][data-story-beat-state="past"] {
opacity: 0.58;
}
svg[data-story-beat] [data-edge-from][data-story-beat-step][data-story-beat-state="next"] {
opacity: 0.34;
}
svg[data-story-beat] [data-edge-from][data-story-beat-step][data-story-beat-state="active"] {
opacity: 1;
}
svg[data-story-beat] .story-trail-flow {
opacity: 0;
transition: opacity 160ms ease;
}
svg[data-story-beat] .story-trail-flow[data-story-beat-state="past"] {
opacity: 0.34;
}
svg[data-story-beat] .story-trail-flow[data-story-beat-state="next"] {
opacity: 0.2;
}
svg[data-story-beat] .story-trail-flow[data-story-beat-state="active"] {
opacity: 0.92;
}
svg[data-story-beat] .story-trail-flow[data-story-pulse="true"] {
stroke-dasharray: 2 13;
stroke-dashoffset: 30;
opacity: 0.92;
animation: archify-story-flow 0.78s linear 1 both;
}
svg[data-preset="blueprint"] .story-trail-flow {
stroke-linecap: square;
filter: none;
opacity: 0.5;
}
svg[data-preset="blueprint"][data-story-beat]
[data-story-step][data-story-beat-state="active"] {
filter: none;
animation: none;
}
svg[data-preset="editorial"] .story-trail-flow {
filter: none;
}
svg[data-preset="editorial"][data-story-beat]
[data-story-step][data-story-beat-state="active"] {
filter: drop-shadow(0 2px 2px color-mix(in srgb, var(--text) 16%, transparent));
}
/* Chapter Delta Preview is a static, pre-commit comparison between two
chapter focus sets. Symbols in the rail carry the meaning without color;
these SVG rules only make exact stable-ID membership easier to scan. */
svg[data-chapter-preview] [data-node-id],
svg[data-chapter-preview] [data-edge-from] {
opacity: 0.1 !important;
filter: none !important;
transition: none !important;
}
svg[data-chapter-preview] .story-trail-overlay {
opacity: 0.08 !important;
}
svg[data-chapter-preview] [data-node-id][data-chapter-preview-role="stay"] {
opacity: 0.76 !important;
filter: saturate(0.7) !important;
}
svg[data-chapter-preview] [data-node-id][data-chapter-preview-role="enter"] {
opacity: 1 !important;
filter: saturate(1.2) !important;
}
svg[data-chapter-preview] [data-node-id][data-chapter-preview-role="leave"] {
opacity: 0.28 !important;
filter: grayscale(0.72) !important;
}
svg[data-preset="blueprint"][data-chapter-preview] [data-node-id] {
filter: none !important;
}
/* Dashed boundary variants */
.c-security-group {
fill: transparent;
stroke: var(--security-stroke);
stroke-dasharray: 4, 4;
}
.c-region {
fill: rgba(251, 191, 36, 0.05);
stroke: var(--cloud-stroke);
stroke-dasharray: 8, 4;
}
.c-lane {
fill: var(--lane-fill);
stroke: var(--lane-stroke);
stroke-dasharray: 6, 6;
}
/* Optional trace animation, enabled only by meta.animation = "trace".
The ordinary viewer performs one ambient pass and then restores the
authored solid/security/async line language. WebM samples the same
authored geometry into an explicit canvas timeline. */
html[data-ambient-motion="running"] svg[data-animation="trace"] [data-animate="edge"] {
animation: archify-edge-flow 2.4s linear 1;
animation-delay: calc(var(--step, 0) * 160ms);
}
html[data-ambient-motion="running"] svg[data-animation="trace"] [data-animate="node"] {
transform-box: fill-box;
transform-origin: center;
animation: archify-node-pulse 3.6s ease-in-out 1;
animation-delay: calc(var(--step, 0) * 160ms);
}
svg[data-preset="signal-flow"][data-animation="trace"] [data-animate="edge"] {
stroke-linecap: round;
filter: drop-shadow(0 0 3px var(--arrow-emphasis));
animation-duration: 1.75s;
}
svg[data-preset="signal-flow"][data-animation="trace"] [data-animate="node"] {
animation-duration: 3.1s;
}
svg[data-preset="blueprint"][data-animation="trace"] [data-animate="edge"] {
stroke-linecap: square;
filter: none;
animation-duration: 2.15s;
}
svg[data-preset="blueprint"][data-animation="trace"] [data-animate="node"] {
animation-name: archify-blueprint-node-pulse;
animation-duration: 3.8s;
}
svg[data-preset="editorial"][data-animation="trace"] [data-animate="edge"] {
filter: none;
animation-duration: 2.2s;
}
svg[data-preset="editorial"][data-animation="trace"] [data-animate="node"] {
animation-name: archify-editorial-node-pulse;
animation-duration: 3.5s;
}
/* Embedded proofs are static until a named Story Trail chapter is played.
This keeps proof grids calm and ensures ?play=1 stops all viewer motion
after one 3.2 second chapter instead of leaving ambient trace loops on. */
html[data-embed="true"] svg[data-animation="trace"] [data-animate],
html[data-share-playback="true"] svg[data-animation="trace"] [data-animate] {
animation: none !important;
stroke-dashoffset: 0;
}
@keyframes archify-edge-flow {
0% {
stroke-dasharray: 10 8;
stroke-dashoffset: 54;
opacity: 0.42;
}
88% {
stroke-dasharray: 10 8;
stroke-dashoffset: 0;
opacity: 1;
}
99.9% {
stroke-dasharray: 10 8;
stroke-dashoffset: 0;
opacity: 1;
}
100% {
stroke-dashoffset: 0;
opacity: 1;
}
}
@keyframes archify-node-pulse {
0%,
72%,
100% {
filter: none;
stroke-width: 1.5;
}
18%,
36% {
filter: drop-shadow(0 0 8px var(--arrow-emphasis));
stroke-width: 2.4;
}
}
@keyframes archify-blueprint-node-pulse {
0%,
72%,
100% {
opacity: 1;
filter: none;
}
18%,
36% {
opacity: 0.72;
filter: drop-shadow(0 0 2px var(--frontend-stroke));
}
}
@keyframes archify-editorial-node-pulse {
0%,
72%,
100% {
opacity: 1;
filter: none;
}
18%,
36% {
opacity: 0.78;
filter: drop-shadow(0 2px 2px color-mix(in srgb, var(--text) 18%, transparent));
}
}
@keyframes archify-signal-scan {
0%,
18% {
opacity: 1;
transform: translateX(-70%);
}
70% {
opacity: 1;
transform: translateX(70%);
}
100% {
opacity: 0;
transform: translateX(70%);
}
}
@keyframes archify-radar-live {
0% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--backend-stroke) 52%, transparent);
}
55%,
100% {
box-shadow: 0 0 0 0.32rem transparent;
}
}
@keyframes archify-guided-progress {
from {
transform: scaleX(var(--guided-progress-start, 0));
}
to {
transform: scaleX(1);
}
}
@keyframes archify-story-flow {
to {
stroke-dashoffset: 0;
}
}
@keyframes archify-intent-trace-flow {
0% {
opacity: 0.28;
stroke-dashoffset: 0;
}
12%,
78% {
opacity: 0.94;
}
100% {
opacity: 0;
stroke-dashoffset: -1;
}
}
@keyframes archify-route-probe-flow {
0% {
stroke-dashoffset: 0;
opacity: 0.96;
}
78% {
stroke-dashoffset: -1;
opacity: 0.96;
}
100% {
stroke-dasharray: none;
stroke-dashoffset: 0;
opacity: 0.58;
}
}
@keyframes archify-route-journey-flow {
0% {
opacity: 0.16;
stroke-dashoffset: 0;
}
14%,
76% {
opacity: 1;
}
100% {
opacity: 0;
stroke-dashoffset: -1;
}
}
@keyframes archify-semantic-lens-flow {
0% {
opacity: 0.2;
stroke-dashoffset: 0;
}
12%,
78% {
opacity: 0.9;
}
100% {
opacity: 0;
stroke-dashoffset: -1;
}
}
@keyframes archify-relationship-pulse {
0% {
opacity: 0.18;
stroke-dashoffset: 0;
}
10%,
76% {
opacity: 0.98;
}
100% {
opacity: 0;
stroke-dashoffset: -1;
}
}
@keyframes archify-relationship-token-life {
0%,
7% {
opacity: 0;
}
13%,
82% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes archify-story-beat-node {
from {
opacity: 0.48;
filter: saturate(0.6);
}
58% {
opacity: 1;
filter: drop-shadow(0 0 10px var(--frontend-stroke));
}
to {
opacity: 1;
filter: drop-shadow(0 0 7px var(--frontend-stroke));
}
}
@keyframes archify-share-cue-enter {
from {
opacity: 0;
transform: translate(-50%, -0.45rem);
}
to {
opacity: 1;
transform: translate(-50%, 0);
}
}
@keyframes archify-share-cue-pulse {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.48;
transform: scale(0.78);
}
}
@media (prefers-reduced-motion: reduce) {
html[data-preset="signal-flow"] .diagram-container::before {
animation: none !important;
}
.pulse-dot {
animation: none !important;
}
svg[data-animation="trace"] [data-animate] {
animation: none !important;
stroke-dashoffset: 0;
}
.guided-view-progress span {
animation: none !important;
transform: scaleX(1) !important;
}
.story-trail-flow,
.intent-trace-flow,
.route-probe-flow,
.semantic-lens,
.diagram-guide,
[data-story-step],
.overview-map-live,
.guided-view-stop,
.share-chapter-cue,
.chapter-handoff-anchor,
.share-chapter-state::before,
.share-chapter-progress span {
animation: none !important;
}
.guided-view-chapter,
svg[data-chapter-preview] [data-node-id],
svg[data-chapter-preview] [data-edge-from] {
transition: none !important;
}
svg[data-story-playing="true"] .story-trail-flow {
stroke-dasharray: none;
stroke-dashoffset: 0;
opacity: 0.55;
}
.intent-trace-flow {
animation: none !important;
stroke-dasharray: none;
stroke-dashoffset: 0;
opacity: 0.72;
}
.route-probe-flow {
animation: none !important;
stroke-dasharray: none;
stroke-dashoffset: 0;
opacity: 0.78;
}
.route-journey-overlay,
.story-carrier-overlay {
display: none !important;
}
.semantic-lens-flow {
animation: none !important;
stroke-dasharray: none;
stroke-width: 2.2;
opacity: 0.34;
filter: none;
}
.relationship-pulse-overlay {
display: none !important;
}
.guided-view-chapter {
transition: none !important;
}
.source-evidence-beacon,
svg [data-node-id],
svg [data-edge-from],
svg [data-detail],
svg [data-detail-anchor],
svg [data-legend-hit],
svg {
transition: none !important;
}
}
</style>
</head>
<body>
<!-- Toolbar: theme, live visual style, trace motion, presentation stage, and export.
Motion appears only for trace-enabled artifacts and has no shortcut;
Keyboard: T toggles theme, S cycles style, F toggles the stage, E opens export. -->
<div class="toolbar" role="toolbar" aria-label="Diagram actions">
<button
id="btn-theme"
type="button"
title="Toggle theme (T)"
aria-label="Toggle color theme"
aria-pressed="false"
>
<span id="theme-icon" class="toolbar-icon" aria-hidden="true"></span>
<span id="theme-label">Dark</span>
</button>
<div class="preset-wrap">
<button
id="btn-preset"
type="button"
title="Choose visual style (S cycles)"
aria-label="Choose visual style"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="preset-menu"
aria-keyshortcuts="S"
>
<span class="preset-control-mark" aria-hidden="true"></span>
<span id="preset-label">Style</span>
<span class="toolbar-icon toolbar-chevron" aria-hidden="true"></span>
</button>
<div class="preset-menu" id="preset-menu" role="menu" aria-label="Visual style">
<div class="preset-menu-heading" role="presentation">
<span>Visual identity</span><span>S cycles</span>
</div>
<button
class="preset-option"
data-preset-value="classic"
type="button"
role="menuitemradio"
aria-checked="false"
tabindex="-1"
>
<span class="preset-option-swatch classic" aria-hidden="true"></span>
<span class="preset-option-copy"
><strong>Classic</strong><small>Stable technical default</small></span
>
<span class="preset-option-check" aria-hidden="true"></span>
</button>
<button
class="preset-option"
data-preset-value="signal-flow"
type="button"
role="menuitemradio"
aria-checked="false"
tabindex="-1"
>
<span class="preset-option-swatch signal-flow" aria-hidden="true"></span>
<span class="preset-option-copy"
><strong>Signal Flow</strong><small>Motion-forward presentation</small></span
>
<span class="preset-option-check" aria-hidden="true"></span>
</button>
<button
class="preset-option"
data-preset-value="blueprint"
type="button"
role="menuitemradio"
aria-checked="false"
tabindex="-1"
>
<span class="preset-option-swatch blueprint" aria-hidden="true"></span>
<span class="preset-option-copy"
><strong>Blueprint</strong><small>Engineering review</small></span
>
<span class="preset-option-check" aria-hidden="true"></span>
</button>
<button
class="preset-option"
data-preset-value="editorial"
type="button"
role="menuitemradio"
aria-checked="false"
tabindex="-1"
>
<span class="preset-option-swatch editorial" aria-hidden="true"></span>
<span class="preset-option-copy"
><strong>Editorial</strong><small>Publication and launch notes</small></span
>
<span class="preset-option-check" aria-hidden="true"></span>
</button>
</div>
</div>
<button
id="btn-motion"
type="button"
hidden
title="Pause motion"
aria-label="Pause motion"
aria-pressed="true"
>
<span class="motion-control-dot" aria-hidden="true"></span>
<span id="motion-label">Live</span>
</button>
<button
id="btn-present"
type="button"
title="Presentation stage (F)"
aria-label="Enter presentation stage"
aria-pressed="false"
>
<span id="present-icon" class="toolbar-icon" aria-hidden="true"></span>
<span id="present-label">Present</span>
</button>
<div class="export-wrap">
<button
id="btn-export"
type="button"
title="Export diagram (E)"
aria-label="Export diagram"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="export-menu"
>
Export <span class="toolbar-icon toolbar-chevron" aria-hidden="true"></span>
</button>
<div class="export-menu" id="export-menu" role="menu" aria-label="Export">
<div class="export-menu-header" role="presentation">
<div><strong>Export diagram</strong><span>Portable, clean outputs</span></div>
<kbd>E</kbd>
</div>
<div class="export-menu-section" role="group" aria-label="Share">
<span class="export-menu-heading" aria-hidden="true">Share</span>
<button data-format="share-card" type="button" role="menuitem" tabindex="-1">
<span class="export-item-copy"
><strong>Share Card</strong><small class="hint">1200×630 PNG</small></span
>
</button>
<button
data-action="route-share-card"
type="button"
role="menuitem"
tabindex="-1"
hidden
disabled
>
<span class="export-item-copy"
><strong>Route Share Card</strong
><small class="hint">1200×630 PNG</small></span
>
</button>
<button
data-action="reach-share-card"
type="button"
role="menuitem"
tabindex="-1"
hidden
disabled
>
<span class="export-item-copy"
><strong>Reach Share Card</strong
><small class="hint">1200×630 PNG</small></span
>
</button>
<button data-action="copy-share-card" type="button" role="menuitem" tabindex="-1">
<span class="export-item-copy"
><strong>Copy Share Card</strong><small class="hint">PNG to clipboard</small></span
>
</button>
<button data-action="copy" type="button" role="menuitem" tabindex="-1">
<span class="export-item-copy"
><strong>Copy diagram</strong><small class="hint">PNG to clipboard</small></span
>
</button>
</div>
<div class="export-menu-section" role="group" aria-label="Raster images">
<span class="export-menu-heading" aria-hidden="true">Image</span>
<button data-format="png" type="button" role="menuitem" tabindex="-1">
<span class="export-item-copy"
><strong>PNG</strong><small class="hint">Lossless image</small></span
>
</button>
<button data-format="jpeg" type="button" role="menuitem" tabindex="-1">
<span class="export-item-copy"
><strong>JPEG</strong><small class="hint">Compact image</small></span
>
</button>
<button data-format="webp" type="button" role="menuitem" tabindex="-1">
<span class="export-item-copy"
><strong>WebP</strong><small class="hint">Modern image</small></span
>
</button>
</div>
<div class="export-menu-section" role="group" aria-label="Vector and motion">
<span class="export-menu-heading" aria-hidden="true">Vector & motion</span>
<button data-format="svg" type="button" role="menuitem" tabindex="-1">
<span class="export-item-copy"
><strong>SVG</strong><small class="hint">Editable vector</small></span
>
</button>
<button data-format="webm" type="button" role="menuitem" tabindex="-1">
<span class="export-item-copy"
><strong>WebM</strong><small class="hint">6s motion</small></span
>
</button>
</div>
</div>
</div>
</div>
<div class="container">
<!-- Header -->
<div class="header">
<div
class="header-row"
data-preset-badge-signal-flow="SIGNAL FLOW"
data-preset-badge-blueprint="BLUEPRINT / REV 01"
data-preset-badge-editorial="EDITORIAL / FIELD NOTE"
>
<div class="pulse-dot"></div>
<h1>Agent Tool Call Workflow</h1>
</div>
</div>
<script id="archify-guided-views-data" type="application/json">
[
{
"id": "happy-path",
"label": "Request to result",
"focus": ["user", "chat", "planner", "router", "approval", "tool", "external", "final"],
"note": "Follow the successful request from user intent to the final reply."
},
{
"id": "safety-gate",
"label": "Policy and recovery",
"focus": ["router", "approval", "blocked", "retry"],
"note": "See where risky work stops, waits for consent, or returns for revision."
},
{
"id": "evidence-loop",
"label": "Evidence and memory",
"focus": ["external", "store", "trace"],
"note": "Isolate the durable trace and context path behind the visible answer."
}
]
</script>
<script id="archify-i18n-data" type="application/json">
{
"locale": "en",
"messages": {
"viewer.kind.frontend": "Frontend",
"viewer.kind.backend": "Backend",
"viewer.kind.database": "Database",
"viewer.kind.cloud": "Cloud",
"viewer.kind.security": "Security",
"viewer.kind.messagebus": "Message bus",
"viewer.kind.external": "External",
"viewer.kind.neutral": "Neutral",
"viewer.kind.node": "Node",
"viewer.kind.start": "Start",
"viewer.kind.active": "Active",
"viewer.kind.waiting": "Waiting",
"viewer.kind.decision": "Decision",
"viewer.kind.success": "Success",
"viewer.kind.failure": "Failure",
"viewer.toolbar.actions": "Diagram actions",
"viewer.theme.toggle.title": "Toggle theme (T)",
"viewer.theme.toggle": "Toggle color theme",
"viewer.theme.dark": "Dark",
"viewer.theme.light": "Light",
"viewer.preset.choose.title": "Choose visual style (S cycles)",
"viewer.preset.choose": "Choose visual style",
"viewer.preset.style": "Style",
"viewer.preset.menu": "Visual style",
"viewer.preset.identity": "Visual identity",
"viewer.preset.cycles": "S cycles",
"viewer.preset.classic": "Classic",
"viewer.preset.classic.short": "Classic",
"viewer.preset.classic.hint": "Stable technical default",
"viewer.preset.flow": "Signal Flow",
"viewer.preset.flow.short": "Flow",
"viewer.preset.flow.hint": "Motion-forward presentation",
"viewer.preset.blueprint": "Blueprint",
"viewer.preset.blueprint.hint": "Engineering review",
"viewer.preset.editorial": "Editorial",
"viewer.preset.editorial.hint": "Publication and launch notes",
"viewer.preset.badge.signalFlow": "SIGNAL FLOW",
"viewer.preset.badge.blueprint": "BLUEPRINT / REV 01",
"viewer.preset.badge.editorial": "EDITORIAL / FIELD NOTE",
"viewer.preset.badge.editorialPlate": "ARCHIFY / PLATE 04",
"viewer.preset.current": "Visual style: {style}. Choose visual style",
"viewer.motion.live": "Live",
"viewer.motion.still": "Still",
"viewer.motion.pause": "Pause motion",
"viewer.motion.resume": "Resume motion",
"viewer.motion.reduced": "Motion paused by reduced-motion preference",
"viewer.motion.hidden": "Motion paused while this page is hidden",
"viewer.motion.yielding": "Pause motion; currently yielding to {owner}",
"viewer.motion.yielding.title": "Live preview enabled · yielding to {owner}",
"viewer.owner.story": "the guided story",
"viewer.owner.chapter": "the active chapter",
"viewer.owner.chapterPreview": "the chapter delta preview",
"viewer.owner.handoff": "the chapter handoff",
"viewer.owner.route": "Route Probe",
"viewer.owner.lens": "Semantic Lens",
"viewer.owner.relationship": "Relationship Preview",
"viewer.owner.intent": "Intent Trace",
"viewer.owner.focus": "semantic focus",
"viewer.owner.legend": "legend preview",
"viewer.owner.reader": "reader interaction",
"viewer.present.enter": "Enter presentation stage",
"viewer.present.enter.title": "Presentation stage (F)",
"viewer.present.exit": "Exit presentation stage",
"viewer.present.exit.title": "Exit presentation stage (F or Escape)",
"viewer.present.present": "Present",
"viewer.present.exit.label": "Exit",
"viewer.export.button": "Export",
"viewer.export.button.title": "Export diagram (E)",
"viewer.export.diagram": "Export diagram",
"viewer.export.menu": "Export",
"viewer.export.subtitle": "Portable, clean outputs",
"viewer.export.share": "Share",
"viewer.export.shareCard": "Share Card",
"viewer.export.routeShareCard": "Route Share Card",
"viewer.export.reachShareCard": "Reach Share Card",
"viewer.export.copyShareCard": "Copy Share Card",
"viewer.export.copyDiagram": "Copy diagram",
"viewer.export.clipboardPng": "PNG to clipboard",
"viewer.export.raster": "Raster images",
"viewer.export.image": "Image",
"viewer.export.lossless": "Lossless image",
"viewer.export.compact": "Compact image",
"viewer.export.modern": "Modern image",
"viewer.export.vectorMotion": "Vector and motion",
"viewer.export.vectorMotion.heading": "Vector \u0026 motion",
"viewer.export.editable": "Editable vector",
"viewer.export.motion6s": "6s motion",
"viewer.export.unsupported": "Not supported by this browser",
"viewer.export.clipboardUnsupported": "Clipboard image write not supported by this browser",
"viewer.export.clipboardUnsupported.period": "Clipboard image write not supported by this browser.",
"viewer.export.clipboardUnsupported.short": "Clipboard image write not supported in this browser.",
"viewer.export.motionUnavailable": "Motion capture unavailable in this browser",
"viewer.export.webmUnavailable": "WebM unavailable in this browser",
"viewer.export.failed": "Export failed: {message}",
"viewer.export.unknownVariant": "Unknown Share Card variant: {variant}",
"viewer.export.routeRequired": "Trace a route before exporting a Route Share Card",
"viewer.export.reachRequired": "Trace authored reach before exporting a Reach Share Card",
"viewer.export.unknown": "unknown",
"viewer.export.routeFailed": "Route Share Card export failed: {message}",
"viewer.export.reachFailed": "Reach Share Card export failed: {message}",
"viewer.export.copyFailed": "Copy failed: {message}",
"viewer.export.copiedPng": "Copied PNG to clipboard",
"viewer.export.copiedShare": "Copied Share Card",
"viewer.export.downloadedShare": "Downloaded Share Card",
"viewer.export.downloadedRoute": "Downloaded Route Share Card",
"viewer.export.downloadedReach": "Downloaded Reach Share Card",
"viewer.export.downloadedWebm": "Downloaded WebM",
"viewer.export.recording": "Recording 6 seconds of motion…",
"viewer.export.card.routeSummary.one": "Route: {source} → {target} · {count} directed hop",
"viewer.export.card.routeSummary.other": "Route: {source} → {target} · {count} directed hops",
"viewer.export.card.reachSummary": "Authored {direction} from {origin} · {nodes} · {links} · max {hops}",
"viewer.export.card.node.one": "{count} node",
"viewer.export.card.node.other": "{count} nodes",
"viewer.export.card.link.one": "{count} link",
"viewer.export.card.link.other": "{count} links",
"viewer.export.card.hop.one": "{count} hop",
"viewer.export.card.hop.other": "{count} hops",
"viewer.export.card.routeBadge": "ARCHIFY · ROUTE · {hops}",
"viewer.export.card.reachBadge": "ARCHIFY · {direction} REACH",
"viewer.export.card.defaultBadge": "ARCHIFY · {preset} · {theme}",
"viewer.export.direction.upstream": "Upstream",
"viewer.export.direction.downstream": "Downstream",
"viewer.export.error.canvasUnavailable": "Canvas unavailable for {label}",
"viewer.export.error.contextUnavailable": "2D canvas context unavailable for {label}",
"viewer.export.error.toBlobUnavailable": "canvas.toBlob unavailable for {label}",
"viewer.export.error.toBlobNull": "canvas.toBlob returned no data for {label}",
"viewer.export.error.variantsCombined": "Share Card variants cannot be combined",
"viewer.export.error.viewerState": "Share Card export could not remove temporary viewer state",
"viewer.export.error.routeState": "Route Card export could not preserve the resolved route safely",
"viewer.export.error.reachState": "Reach Card export could not preserve authored reach safely",
"viewer.export.error.webmRequirements": "WebM motion export requires a trace animation and browser MediaRecorder support",
"viewer.export.error.mediaRecorder": "MediaRecorder failed",
"viewer.export.error.emptyWebm": "MediaRecorder produced an empty WebM",
"viewer.export.error.webmBackground": "SVG background could not be loaded for WebM export",
"viewer.guided.region": "Guided diagram views",
"viewer.guided.previous": "Previous guided view",
"viewer.guided.previous.title": "Previous guided view ([)",
"viewer.guided.next": "Next guided view",
"viewer.guided.next.title": "Next guided view (])",
"viewer.guided.views": "Guided views",
"viewer.guided.explore": "Explore this system",
"viewer.guided.intro": "Step through curated paths without changing the source diagram.",
"viewer.guided.trail": "Story trail",
"viewer.guided.beat": "Beat",
"viewer.guided.nextBeat": "Next",
"viewer.guided.play": "Play guided story",
"viewer.guided.play.title": "Play guided story (P)",
"viewer.guided.pause": "Pause guided story",
"viewer.guided.pause.title": "Pause guided story (P)",
"viewer.guided.replay": "Replay guided story",
"viewer.guided.replay.title": "Replay guided story (P)",
"viewer.guided.playStory": "Play story",
"viewer.guided.pauseStory": "Pause",
"viewer.guided.replayStory": "Replay story",
"viewer.guided.motionUnavailable": "Story playback unavailable while motion is Still",
"viewer.guided.enableMotion": "Switch motion to Live to play the guided story",
"viewer.guided.selectBeatLink": "Select a Story Beat to copy its exact link",
"viewer.guided.copyMoment": "Copy moment",
"viewer.guided.momentCopied": "Moment link copied",
"viewer.guided.momentCopyFailed": "Could not copy story moment link",
"viewer.guided.copied": "Copied",
"viewer.guided.copyFailed": "Copy failed",
"viewer.guided.showAll": "Show all",
"viewer.guided.showAll.aria": "Show entire diagram",
"viewer.guided.chapters": "Story chapters",
"viewer.guided.storyTrail": "Story trail for {label}: {count} beats",
"viewer.guided.chapter.open": "Open chapter {index} of {total}: {label}, {count} stops",
"viewer.guided.chapter.current": "Current chapter {index} of {total}: {label}, {count} stops",
"viewer.guided.chapter.selectedNodes": "{count} selected nodes",
"viewer.guided.chapter.stops": "{count} stops",
"viewer.guided.chapter.stop.one": "{count} stop",
"viewer.guided.chapter.stop.other": "{count} stops",
"viewer.guided.chapter.current.title": "{label} — current chapter, {count} stops",
"viewer.guided.chapter.delta.expanded": "{stay} stay, {enter} enter, {leave} leave",
"viewer.guided.chapter.delta.aria": "Open chapter {index} of {total}: {label}. Chapter focus delta: {delta}",
"viewer.guided.chapter.delta.title": "{label} — {delta} chapter focus",
"viewer.guided.handoff": "{from} → {to} · via {label}",
"viewer.guided.share.chapter": "Chapter {index} / {total}",
"viewer.guided.share.initial": "Chapter 01 / 01",
"viewer.guided.share.default": "Guided chapter",
"viewer.guided.state.ready": "Ready",
"viewer.guided.state.playing": "Playing",
"viewer.guided.state.settled": "Settled",
"viewer.guided.state.paused": "Paused",
"viewer.guided.state.pinned": "Pinned",
"viewer.guided.state.still": "Still",
"viewer.guided.share.step": "Step {index} / {total} · {label}",
"viewer.guided.share.staticMoment": "{step} · Static moment",
"viewer.guided.share.complete": "{count} steps complete · {note}",
"viewer.guided.share.settled": "Path settled for reading.",
"viewer.guided.share.staticPath": "{count} steps · Static path",
"viewer.guided.share.ready": "{count} steps · Ready",
"viewer.guided.share.aria": "{state} chapter {index} of {total}: {label}. {beat}. {route}",
"viewer.guided.beat.start": "Beat {index} / {total} · {label} · starting point",
"viewer.guided.beat.forward": "Beat {index} / {total} · {from} → {to}",
"viewer.guided.beat.reverse": "Beat {index} / {total} · {from} → {to} · reverse authored link",
"viewer.guided.beat.multiple": "Beat {index} / {total} · {from} ⇄ {to} · {count} authored links",
"viewer.guided.beat.group": "Beat {index} / {total} · {from} · {to} · grouped · no direct link",
"viewer.guided.beat.aria.prefix": "Story beat {index} of {total}: {label}. ",
"viewer.guided.beat.aria.start": "Starting point.",
"viewer.guided.beat.aria.forward": "From {from} through one authored forward relationship.",
"viewer.guided.beat.aria.reverse": "From {from}; the authored relationship points from {to} to {from}.",
"viewer.guided.beat.aria.multiple": "From {from} through {count} authored relationships; shown without arbitrary motion.",
"viewer.guided.beat.aria.group": "Grouped from {from} with no direct authored relationship.",
"viewer.guided.caption.start": "Starting point",
"viewer.guided.caption.grouped": "Grouped transition · no direct authored link",
"viewer.guided.caption.more": " +{count} more",
"viewer.guided.caption.reverse": "Reverse authored relationship",
"viewer.guided.caption.relationships": "{count} authored relationships",
"viewer.guided.caption.relationship": "Authored relationship",
"viewer.guided.caption.direction": "authored direction: {from} → {to}",
"viewer.guided.caption.starting": "Authored starting point",
"viewer.guided.beatLink": "Copy link to current story moment: Beat {index} of {total}: {label}",
"viewer.guided.noStory": "This diagram has no authored guided story.",
"viewer.guide.eyebrow": "Diagram guide",
"viewer.guide.close": "Close diagram guide",
"viewer.guide.inspecting": "Inspecting compiled semantics",
"viewer.guide.actions": "Diagram exploration actions",
"viewer.guide.find": "Find any node",
"viewer.guide.find.hint": "Search labels, responsibilities, kinds, and stable IDs.",
"viewer.guide.route": "Trace a route",
"viewer.guide.route.aria": "Trace a directed route",
"viewer.guide.route.hint": "Ask how two semantic nodes connect in authored direction.",
"viewer.guide.map": "See the whole system",
"viewer.guide.map.hint": "Open Semantic Radar with a live viewport and stable nodes.",
"viewer.guide.lens": "Compare semantic kinds",
"viewer.guide.lens.hint": "Count roles, reveal their traffic, and compare direct authored links.",
"viewer.guide.story": "Play the guided story",
"viewer.guide.story.hint": "Walk the authored chapters and real relationships.",
"viewer.guide.present": "Enter Presentation Stage",
"viewer.guide.present.hint": "Give the live diagram the viewport without changing export.",
"viewer.guide.shortcuts": "Additional keyboard shortcuts",
"viewer.guide.shortcut.export": "Export",
"viewer.guide.shortcut.theme": "Theme",
"viewer.guide.shortcut.style": "Style",
"viewer.guide.shortcut.reset": "Reset",
"viewer.guide.shortcut.zoomIn": "Zoom in",
"viewer.guide.shortcut.zoomOut": "Zoom out",
"viewer.guide.shortcut.close": "Close",
"viewer.guide.facts": "{nodes} · {relationships} · {views}",
"viewer.guide.fact.node.one": "{count} semantic node",
"viewer.guide.fact.node.other": "{count} semantic nodes",
"viewer.guide.fact.relationship.one": "{count} relationship",
"viewer.guide.fact.relationship.other": "{count} relationships",
"viewer.guide.fact.view.one": "{count} guided view",
"viewer.guide.fact.view.other": "{count} guided views",
"viewer.guide.story.available.one": "Walk {count} authored chapter and its real relationships.",
"viewer.guide.story.available.other": "Walk {count} authored chapters and their real relationships.",
"viewer.guide.story.unavailable": "No authored guided story in this diagram.",
"viewer.guide.open": "Open diagram guide",
"viewer.guide.noStory": "This diagram has no authored guided story.",
"viewer.finder.title": "Find a node",
"viewer.finder.close": "Close node finder",
"viewer.finder.placeholder": "Search labels or IDs",
"viewer.finder.search": "Search diagram nodes",
"viewer.finder.results": "Diagram nodes",
"viewer.finder.empty": "No matching nodes",
"viewer.finder.result.focus": "Focus {label}",
"viewer.finder.result.routeStart": "Choose {label} as route start",
"viewer.finder.result.routeTarget": "Choose {label} as route destination, {links}",
"viewer.finder.status.empty": "No matching nodes",
"viewer.finder.status.count.one": "{count} matching node",
"viewer.finder.status.count.other": "{count} matching nodes",
"viewer.finder.noun.nodes": "nodes",
"viewer.finder.link.one": "{count} link",
"viewer.finder.link.other": "{count} links",
"viewer.finder.result.focus.one": "Focus {label}, {count} related connection",
"viewer.finder.result.focus.other": "Focus {label}, {count} related connections",
"viewer.finder.status.filtered": "{visible} of {available} {noun}",
"viewer.finder.status.all": "{available} {noun}",
"viewer.passport.eyebrow": "Semantic passport",
"viewer.passport.metadata": "Node metadata",
"viewer.passport.evidence": "Verified source evidence",
"viewer.passport.verified": "Verified source",
"viewer.passport.reach": "Authored reach",
"viewer.passport.reach.trace": "Trace authored reachability",
"viewer.passport.upstream": "Upstream",
"viewer.passport.downstream": "Downstream",
"viewer.passport.upstream.trace": "Trace upstream authored reachability",
"viewer.passport.downstream.trace": "Trace downstream authored reachability",
"viewer.passport.close": "Close semantic passport",
"viewer.passport.copy": "Copy link",
"viewer.passport.copy.focus": "Copy link to focused node",
"viewer.passport.relations": "Relations",
"viewer.passport.relations.show": "Show connected relationships",
"viewer.passport.relations.hide": "Hide connected relationships",
"viewer.passport.relations.list": "Connected relationships",
"viewer.passport.copyRelation": "Copy relation",
"viewer.passport.copyNode": "Copy node",
"viewer.passport.copyPinned": "Copy link to pinned relationship",
"viewer.passport.copySource": "Copy link to source node",
"viewer.passport.copy.focused.success": "Focused node link copied",
"viewer.passport.copy.pinned.success": "Pinned relationship link copied",
"viewer.passport.copy.focused.failed": "Could not copy focused node link",
"viewer.passport.copy.pinned.failed": "Could not copy pinned relationship link",
"viewer.passport.relationship.none": "No connected relationships",
"viewer.passport.relationship.count.one": "{count} relation",
"viewer.passport.relationship.count.other": "{count} relations",
"viewer.passport.relationship.show.one": "Show {count} connected relationship",
"viewer.passport.relationship.show.other": "Show {count} connected relationships",
"viewer.passport.relationship.summary": "{out} outgoing · {in} incoming{loops}",
"viewer.passport.relationship.loops": " · {count} loop",
"viewer.passport.relationship.explorer": "Direct relationship explorer",
"viewer.passport.relationship.help": "Use arrow keys to explore relationships. Press Enter or Space to pin details; Escape clears.",
"viewer.passport.relationship.loopsBack": "loops back",
"viewer.passport.relationship.connectsTo": "connects to",
"viewer.passport.relationship.connectsFrom": "connects from",
"viewer.passport.relationship.pinned": "Pinned relationship · {from} → {to} · {label}",
"viewer.passport.relationship.inspect": "Inspect relationship {index} of {total}: {from} to {to}, {label}. Press Enter for details.",
"viewer.passport.relationship.group.out": "Outgoing",
"viewer.passport.relationship.group.in": "Incoming",
"viewer.passport.relationship.group.loop": "Self loops",
"viewer.passport.relationship.row": "{group}: {relationship}, {neighbor}",
"viewer.passport.relationship.direction.out": "OUT →",
"viewer.passport.relationship.direction.in": "← IN",
"viewer.passport.relationship.direction.loop": "LOOP",
"viewer.passport.sourceCount.one": "{count} verified source reference",
"viewer.passport.sourceCount.other": "{count} verified source references",
"viewer.passport.sourceMarker": "SRC",
"viewer.passport.beacon.one": "{count} verified source; focus this node to inspect",
"viewer.passport.beacon.other": "{count} verified sources; focus this node to inspect",
"viewer.passport.repository.open": "Open verified repository revision {revision}",
"viewer.passport.source.open": "Open verified source {path} at revision {revision}",
"viewer.passport.source.openLink": "Open ↗",
"viewer.passport.reach.upstream.one": "Trace {count} upstream authored node",
"viewer.passport.reach.upstream.other": "Trace {count} upstream authored nodes",
"viewer.passport.reach.downstream.one": "Trace {count} downstream authored node",
"viewer.passport.reach.downstream.other": "Trace {count} downstream authored nodes",
"viewer.passport.reach.noUpstream": "No upstream authored nodes",
"viewer.passport.reach.noDownstream": "No downstream authored nodes",
"viewer.passport.reach.status": "{direction} · {nodes} nodes · {links} links · max {hops} hops",
"viewer.route.eyebrow": "Route probe",
"viewer.route.start": "Choose a start node",
"viewer.route.start.find": "Find start",
"viewer.route.start.find.aria": "Find a route start",
"viewer.route.copy": "Copy link",
"viewer.route.copy.aria": "Copy link to traced route",
"viewer.route.clear": "Clear",
"viewer.route.clear.aria": "Clear route probe",
"viewer.route.traced": "Traced route",
"viewer.route.pickTwo": "Pick two semantic nodes on the diagram",
"viewer.route.pickOne": "Pick a semantic node on the diagram",
"viewer.route.controls": "Route journey controls",
"viewer.route.previous": "Previous route position",
"viewer.route.play": "Play route journey",
"viewer.route.pause": "Pause route journey",
"viewer.route.replay": "Replay route journey",
"viewer.route.next": "Next route position",
"viewer.route.journey": "Journey",
"viewer.route.pause.label": "Pause",
"viewer.route.replay.label": "Replay",
"viewer.route.overview": "Overview",
"viewer.route.overview.aria": "Show complete route overview",
"viewer.route.instructions": "Choose the source, then the destination. Direction matters.",
"viewer.route.destination": "Choose a destination from {label}",
"viewer.route.destination.find": "Find target",
"viewer.route.destination.find.aria": "Find a reachable route destination",
"viewer.route.differentDestination": "Choose a different destination",
"viewer.route.distinct": "A route needs two distinct semantic nodes.",
"viewer.route.unreachable": "No directed route to {label}",
"viewer.route.unreachable.detail": "{target} is not reachable from {source}. Pick a highlighted destination.",
"viewer.route.start.instructions": "Select the source. The next step will reveal only directed destinations.",
"viewer.route.copy.success": "Traced route link copied",
"viewer.route.copy.failed": "Could not copy traced route link",
"viewer.route.position": "Route position {index} of {total}: {label}",
"viewer.route.step": "Step {index} of {total} · {phase} · {label}",
"viewer.route.motionRequired": "Automatic journey requires Live motion",
"viewer.route.trigger.clear": "Clear traced route",
"viewer.route.overview.status": "{nodes} · {hops} · shortest authored route",
"viewer.route.overview.node.one": "{count} node",
"viewer.route.overview.node.other": "{count} nodes",
"viewer.route.overview.hop.one": "{count} directed hop",
"viewer.route.overview.hop.other": "{count} directed hops",
"viewer.route.phase.playing": "Playing",
"viewer.route.phase.complete": "Complete",
"viewer.route.phase.inspecting": "Inspecting",
"viewer.route.destination.count.one": "{count} directed destination available. Pick a highlighted node.",
"viewer.route.destination.count.other": "{count} directed destinations available. Pick a highlighted node.",
"viewer.route.noOutgoing": "No outgoing route starts here. Clear and choose another source.",
"viewer.route.result.title": "{source} to {target}",
"viewer.route.finder.source.title": "Choose route start",
"viewer.route.finder.source.placeholder": "Search route sources",
"viewer.route.finder.source.empty": "No matching route sources",
"viewer.route.finder.source.results": "Nodes that can start a route",
"viewer.route.finder.source.noun": "route sources",
"viewer.route.finder.source.badge": "start",
"viewer.route.finder.target.title": "Destination from {label}",
"viewer.route.finder.target.placeholder": "Search reachable destinations",
"viewer.route.finder.target.empty": "No matching reachable destinations",
"viewer.route.finder.target.results": "Reachable route destinations",
"viewer.route.finder.target.noun": "reachable destinations",
"viewer.route.hop.one": "{count} hop",
"viewer.route.hop.other": "{count} hops",
"viewer.lens.eyebrow": "Semantic lens",
"viewer.lens.title": "Compare system roles",
"viewer.lens.close": "Close semantic lens",
"viewer.lens.instruction": "Choose up to two semantic kinds. One reveals its real traffic; two compare only direct authored relationships.",
"viewer.lens.kinds": "Semantic kinds",
"viewer.lens.choose": "Choose a kind to inspect its nodes and touching relationships.",
"viewer.lens.copy": "Copy link to semantic lens",
"viewer.lens.clear": "Clear semantic lens",
"viewer.lens.open": "Open semantic lens",
"viewer.lens.openActive": "Open active semantic lens",
"viewer.lens.legend": "Semantic legend",
"viewer.lens.legend.inspect.one": "Inspect {label}, {count} node",
"viewer.lens.legend.inspect.other": "Inspect {label}, {count} nodes",
"viewer.lens.kind.count.one": "{label}, {count} node",
"viewer.lens.kind.count.other": "{label}, {count} nodes",
"viewer.lens.compare.one": "{first} → {second}: {forward} · {second} → {first}: {reverse} · {count} direct relationship",
"viewer.lens.compare.other": "{first} → {second}: {forward} · {second} → {first}: {reverse} · {count} direct relationships",
"viewer.lens.single": "{nodes} · {relationships} · connected peers remain visible",
"viewer.lens.node.one": "{count} {label} node",
"viewer.lens.node.other": "{count} {label} nodes",
"viewer.lens.relationship.one": "{count} touching relationship",
"viewer.lens.relationship.other": "{count} touching relationships",
"viewer.radar.title": "Semantic radar",
"viewer.radar.building": "Building overview",
"viewer.radar.openFull": "Open full semantic radar",
"viewer.radar.open": "Open radar",
"viewer.radar.close": "Close semantic radar",
"viewer.radar.surface": "Diagram overview. Click a node to focus it, or use arrow keys to pan.",
"viewer.radar.click": "Click node",
"viewer.radar.drag": "Drag to pan",
"viewer.radar.space": "Semantic radar needs more MAP space.",
"viewer.radar.nodes": "Semantic diagram radar nodes",
"viewer.radar.focus": "Focus {label} from Semantic Radar",
"viewer.radar.status": "{count} nodes · {viewport}",
"viewer.radar.fullMap": "{count} nodes · full map",
"viewer.radar.compacted": "Radar compacted to avoid covering the Semantic Passport or MAP controls.",
"viewer.radar.cancelWaiting": "Cancel semantic radar waiting for more MAP space",
"viewer.radar.needsSpace": "Semantic radar needs more visible MAP space",
"viewer.radar.viewport.full": "full map",
"viewer.radar.viewport.width": "{percent}% width",
"viewer.radar.viewport.scale": "{percent}% viewport",
"viewer.nav.controls": "Diagram view controls",
"viewer.nav.route": "Trace a directed route",
"viewer.nav.route.title": "Trace route (R)",
"viewer.nav.route.short": "PATH",
"viewer.nav.radar": "Open semantic radar",
"viewer.nav.radar.title": "Semantic radar (M)",
"viewer.nav.radar.short": "MAP",
"viewer.nav.lens": "Open semantic lens",
"viewer.nav.lens.title": "Semantic lens (L)",
"viewer.nav.lens.short": "LENS",
"viewer.nav.find": "Find a node",
"viewer.nav.find.title": "Find a node (/)",
"viewer.nav.guide": "Open diagram guide",
"viewer.nav.guide.title": "Diagram guide (?)",
"viewer.nav.zoomOut": "Zoom out",
"viewer.nav.zoomOut.title": "Zoom out (-)",
"viewer.nav.reset": "Reset diagram view",
"viewer.nav.reset.title": "Reset view (0)",
"viewer.nav.read": "READ",
"viewer.nav.zoomIn": "Zoom in",
"viewer.nav.zoomIn.title": "Zoom in (+)",
"viewer.nav.camera": "{hint}. Reset diagram view",
"viewer.nav.camera.title": "{semantic}{hint} · reset view (0)",
"viewer.nav.camera.semantic": "Semantic camera active · ",
"viewer.nav.level.map": "MAP",
"viewer.nav.level.read": "READ",
"viewer.nav.level.full": "FULL",
"viewer.nav.level.auto": "AUTO",
"viewer.nav.detail.map": "Zoom in to reveal relationship labels and node context",
"viewer.nav.detail.read": "Zoom in again to reveal tags and annotations",
"viewer.nav.detail.full": "Full diagram detail",
"viewer.intent.summary": "{label}. {out} outgoing, {in} incoming{loops}. {total} connections. Press Enter for details.",
"viewer.intent.loops": ", {count} self loop",
"viewer.common.copied": "Copied",
"viewer.common.copyFailed": "Copy failed",
"viewer.common.copyLink": "Copy link",
"viewer.common.clear": "Clear",
"viewer.common.close": "Close"
}
}
</script>
<div class="guided-views no-print" id="guided-views" hidden aria-label="Guided diagram views">
<button
id="guided-view-prev"
type="button"
aria-label="Previous guided view"
title="Previous guided view ([)"
>
←
</button>
<div class="guided-view-copy">
<div class="guided-view-meta">
<span>Guided views</span>
<span id="guided-view-count"></span>
<span class="guided-view-progress" aria-hidden="true"
><span id="guided-view-progress-bar"></span
></span>
<span
class="guided-view-handoff"
id="guided-view-handoff"
hidden
aria-hidden="true"
></span>
</div>
<strong id="guided-view-label">Explore this system</strong>
<small id="guided-view-note" aria-live="polite" aria-atomic="true"
>Step through curated paths without changing the source diagram.</small
>
<div
class="guided-view-trail"
id="guided-view-trail"
hidden
role="group"
aria-label="Story trail"
></div>
<div
class="guided-story-caption"
id="guided-story-caption"
hidden
aria-live="polite"
aria-atomic="true"
>
<span class="guided-story-caption-index" id="guided-story-caption-index">Beat</span>
<strong id="guided-story-caption-route"></strong>
<small id="guided-story-caption-detail"></small>
<span
class="guided-story-caption-next"
id="guided-story-caption-next"
hidden
aria-hidden="true"
>
<small>Next</small>
<strong id="guided-story-caption-next-label"></strong>
</span>
</div>
</div>
<button
id="guided-view-next"
type="button"
aria-label="Next guided view"
title="Next guided view (])"
>
→
</button>
<div class="guided-view-actions">
<button
class="guided-view-play"
id="guided-view-play"
type="button"
aria-label="Play guided story"
aria-pressed="false"
title="Play guided story (P)"
>
<span id="guided-view-play-icon" aria-hidden="true">▶</span>
<span id="guided-view-play-label">Play story</span>
</button>
<button
class="guided-view-beat-link"
id="guided-view-beat-link"
type="button"
aria-label="Select a Story Beat to copy its exact link"
title="Select a Story Beat to copy its exact link"
disabled
>
<span aria-hidden="true">#</span>
<span id="guided-view-beat-link-label">Copy moment</span>
</button>
<button
class="guided-view-all"
id="guided-view-all"
type="button"
aria-label="Show entire diagram"
>
Show all
</button>
</div>
<nav class="guided-view-index" id="guided-view-index" aria-label="Story chapters">
<ol class="guided-view-chapters" id="guided-view-chapters"></ol>
</nav>
</div>
<!-- Main Diagram -->
<div
class="diagram-container"
data-detail-level="read"
data-preset-badge-editorial-plate="ARCHIFY / PLATE 04"
>
<svg
viewBox="0 0 1096 684"
role="img"
lang="en"
aria-labelledby="archify-diagram-title archify-diagram-description"
data-animation="trace"
data-preset="signal-flow"
data-quality-profile="showcase"
>
<title id="archify-diagram-title">Agent Tool Call Workflow</title>
<desc id="archify-diagram-description">A workflow diagram generated by Archify.</desc>
<!-- Definitions -->
<defs>
<marker
id="arrowhead"
markerWidth="10"
markerHeight="7"
refX="9"
refY="3.5"
orient="auto"
>
<polygon points="0 0, 10 3.5, 0 7" class="m-default" />
</marker>
<marker
id="arrowhead-emphasis"
markerWidth="10"
markerHeight="7"
refX="9"
refY="3.5"
orient="auto"
>
<polygon points="0 0, 10 3.5, 0 7" class="m-emphasis" />
</marker>
<marker
id="arrowhead-security"
markerWidth="10"
markerHeight="7"
refX="9"
refY="3.5"
orient="auto"
>
<polygon points="0 0, 10 3.5, 0 7" class="m-security" />
</marker>
<marker
id="arrowhead-dashed"
markerWidth="10"
markerHeight="7"
refX="9"
refY="3.5"
orient="auto"
>
<polygon points="0 0, 10 3.5, 0 7" class="m-dashed" />
</marker>
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" class="c-grid" stroke-width="0.5" />
</pattern>
</defs>
<!-- Background Grid -->
<rect width="100%" height="100%" fill="url(#grid)" />
<!-- Swimlanes -->
<rect
data-graph-role="structural-frame"
data-composition-frame-kind="lane"
data-composition-frame-id="lane-0"
x="40"
y="52"
width="1028"
height="106"
rx="10"
class="c-lane"
stroke-width="1"
/>
<text x="54" y="74" class="t-dim" font-size="10" font-weight="600">
01 / User Interface
</text>
<rect
data-graph-role="structural-frame"
data-composition-frame-kind="lane"
data-composition-frame-id="lane-1"
x="40"
y="178"
width="1028"
height="114"
rx="10"
class="c-lane"
stroke-width="1"
/>
<text x="54" y="200" class="t-dim" font-size="10" font-weight="600">
02 / Agent Runtime
</text>
<rect
data-graph-role="structural-frame"
data-composition-frame-kind="lane"
data-composition-frame-id="lane-2"
x="40"
y="312"
width="1028"
height="114"
rx="10"
class="c-lane"
stroke-width="1"
/>
<rect
data-graph-role="structural-frame"
data-composition-frame-kind="exception-lane"
data-composition-frame-id="lane-2-exception"
x="46"
y="318"
width="1016"
height="102"
rx="8"
class="c-security-group"
stroke-width="1"
/>
<text x="54" y="334" class="t-security" font-size="10" font-weight="600">
EX / Policy & Recovery
</text>
<rect
data-graph-role="structural-frame"
data-composition-frame-kind="lane"
data-composition-frame-id="lane-3"
x="40"
y="446"
width="1028"
height="114"
rx="10"
class="c-lane"
stroke-width="1"
/>
<text x="54" y="468" class="t-dim" font-size="10" font-weight="600">
04 / Tool Execution & Evidence
</text>
<!-- Phase headers -->
<line x1="68" y1="35" x2="320" y2="35" class="a-default" stroke-width="1.1" />
<rect x="68" y="27" width="252" height="16" rx="4" class="c-mask" />
<text x="194" y="39" class="t-muted" font-size="8" font-weight="600" text-anchor="middle">
Intake
</text>
<line
x1="445.20000000000005"
y1="35"
x2="697.2"
y2="35"
class="a-emphasis"
stroke-width="1.1"
/>
<rect
x="445.20000000000005"
y="27"
width="252.00000000000006"
height="16"
rx="4"
class="c-mask"
/>
<text
x="571.2"
y="39"
class="t-backend"
font-size="8"
font-weight="600"
text-anchor="middle"
>
Plan + route
</text>
<line x1="784" y1="35" x2="1036" y2="35" class="a-dashed" stroke-width="1.1" />
<rect x="784" y="27" width="252" height="16" rx="4" class="c-mask" />
<text
x="910"
y="39"
class="t-messagebus"
font-size="8"
font-weight="600"
text-anchor="middle"
>
Execute + report
</text>
<!-- Workflow groups -->
<rect
data-graph-role="structural-frame"
data-composition-frame-kind="group"
data-composition-frame-id="group-0"
x="421.2"
y="216"
width="300.00000000000006"
height="72"
rx="9"
class="c-lane"
stroke-width="1"
/>
<text x="431.2" y="214" class="t-backend" font-size="7" font-weight="600">
Planning loop
</text>
<rect
data-graph-role="structural-frame"
data-composition-frame-kind="group"
data-composition-frame-id="group-1"
x="581.2"
y="350"
width="478.79999999999995"
height="72"
rx="9"
class="c-security-group"
stroke-width="1"
/>
<text x="591.2" y="348" class="t-security" font-size="7" font-weight="600">
Human or policy stop
</text>
<rect
data-graph-role="structural-frame"
data-composition-frame-kind="group"
data-composition-frame-id="group-2"
x="204"
y="484"
width="357.20000000000005"
height="72"
rx="9"
class="c-lane"
stroke-width="1"
/>
<text x="214" y="482" class="t-messagebus" font-size="7" font-weight="600">
Evidence path
</text>
<rect
data-graph-role="structural-frame"
data-composition-frame-kind="group"
data-composition-frame-id="group-3"
x="760"
y="484"
width="300"
height="72"
rx="9"
class="c-lane"
stroke-width="1"
/>
<text x="770" y="482" class="t-messagebus" font-size="7" font-weight="600">
Tool work
</text>
<!-- Edge paths -->
<path
data-edge-from="router"
data-edge-to="approval"
data-edge-label="needs approval?"
data-edge-key="0"
data-edge-id="approval-check"
data-composition-points="651.2,279;651.2,302;569.2,302;569.2,387;585.2,387"
d="M 651.2 279 L 651.2 302 L 569.2 302 L 569.2 387 L 585.2 387"
class="a-security"
data-animate="edge"
style="--step: 3"
stroke-width="1.4"
marker-end="url(#arrowhead-security)"
/>
<path
data-edge-from="approval"
data-edge-to="blocked"
data-edge-label="denied"
data-edge-key="1"
data-edge-id="approval-denied"
data-composition-points="651.2,421;651.2,442;830,442;830,413"
d="M 651.2 421 L 651.2 442 L 830 442 L 830 413"
class="a-security"
data-animate="edge"
style="--step: 9"
stroke-width="1.4"
marker-end="url(#arrowhead-security)"
/>
<path
data-edge-from="approval"
data-edge-to="tool"
data-edge-key="2"
data-edge-id="approved-tool"
data-composition-points="651.2,421;651.2,521;764,521"
d="M 651.2 421 L 651.2 521 L 764 521"
class="a-emphasis"
data-animate="edge"
style="--step: 4"
stroke-width="1.8"
marker-end="url(#arrowhead-emphasis)"
/>
<path
data-edge-from="external"
data-edge-to="final"
data-edge-key="3"
data-edge-id="external-reply"
data-composition-points="1056,521;1080,521;1080,120;1056,120"
d="M 1056 521 L 1080 521 L 1080 120 L 1056 120"
class="a-emphasis"
data-animate="edge"
style="--step: 6"
stroke-width="1.2"
marker-end="url(#arrowhead-emphasis)"
/>
<path
data-edge-from="chat"
data-edge-to="planner"
data-edge-label="plan"
data-edge-key="4"
data-edge-id="plan-request"
data-composition-points="274,146;274,253;425.2,253"
d="M 274 146 L 274 253 L 425.2 253"
class="a-emphasis"
data-animate="edge"
style="--step: 1"
stroke-width="1.8"
marker-end="url(#arrowhead-emphasis)"
/>
<path
data-edge-from="planner"
data-edge-to="router"
data-edge-key="5"
data-edge-id="planner-route"
data-composition-points="557.2,253;585.2,253"
d="M 557.2 253 L 585.2 253"
class="a-default"
data-animate="edge"
style="--step: 2"
stroke-width="1.4"
marker-end="url(#arrowhead)"
/>
<path
data-edge-from="external"
data-edge-to="trace"
data-edge-label="record result"
data-edge-key="6"
data-edge-id="record-result"
data-composition-points="990,547;990,579;491.2,579;491.2,547"
d="M 990 547 L 990 579 L 491.2 579 L 491.2 547"
class="a-dashed"
data-animate="edge"
style="--step: 12"
stroke-width="1.4"
marker-end="url(#arrowhead-dashed)"
/>
<path
data-edge-from="user"
data-edge-to="chat"
data-edge-key="7"
data-edge-id="request-chat"
data-composition-points="180,120;208,120"
d="M 180 120 L 208 120"
class="a-default"
data-animate="edge"
style="--step: 0"
stroke-width="1.4"
marker-end="url(#arrowhead)"
/>
<path
data-edge-from="blocked"
data-edge-to="retry"
data-edge-key="8"
data-edge-id="retry-request"
data-composition-points="896,387;924,387"
d="M 896 387 L 924 387"
class="a-dashed"
data-animate="edge"
style="--step: 12"
stroke-width="1.4"
marker-end="url(#arrowhead-dashed)"
/>
<path
data-edge-from="tool"
data-edge-to="external"
data-edge-key="9"
data-edge-id="tool-external-call"
data-composition-points="896,521;924,521"
d="M 896 521 L 924 521"
class="a-default"
data-animate="edge"
style="--step: 5"
stroke-width="1.4"
marker-end="url(#arrowhead)"
/>
<path
data-edge-from="store"
data-edge-to="trace"
data-edge-label="trace + memory"
data-edge-key="10"
data-edge-id="write-trace-memory"
data-composition-points="340,521;425.2,521"
d="M 340 521 L 425.2 521"
class="a-dashed"
data-animate="edge"
style="--step: 12"
stroke-width="1.4"
marker-end="url(#arrowhead-dashed)"
/>
<!-- Nodes -->
<g
id="node-user"
data-node-id="user"
data-node-label="User"
tabindex="0"
role="button"
aria-label="Focus User, asks for work, User Interface › Intake"
aria-pressed="false"
data-node-kind="external"
data-node-sublabel="asks for work"
data-node-context="User Interface › Intake"
>
<title>User · asks for work · User Interface › Intake</title>
<rect x="48" y="94" width="132" height="52" rx="6" class="c-mask" />
<rect
x="48"
y="94"
width="132"
height="52"
rx="6"
class="c-external"
data-animate="node"
style="--step: 0"
stroke-width="1.5"
/>
<g
aria-hidden="true"
data-semantic-sigil="external"
class="semantic-sigil s-external"
transform="translate(54 100) scale(0.6875)"
>
<rect x="2.5" y="5" width="8.5" height="8" rx="1.5" />
<path d="M8 2.5h5.5V8M13.5 2.5 7.5 8.5" />
</g>
<text
data-node-label=""
data-detail-anchor=""
x="114"
y="115"
class="t-primary"
font-size="11"
font-weight="600"
text-anchor="middle"
>
User
</text>
<text
data-detail="context"
x="114"
y="132"
class="t-muted"
font-size="8"
text-anchor="middle"
>
asks for work
</text>
</g>
<g
id="node-chat"
data-node-id="chat"
data-node-label="Chat Surface"
tabindex="0"
role="button"
aria-label="Focus Chat Surface, thread + files, User Interface › Intake"
aria-pressed="false"
data-node-kind="frontend"
data-node-sublabel="thread + files"
data-node-context="User Interface › Intake"
>
<title>Chat Surface · thread + files · User Interface › Intake</title>
<rect x="208" y="94" width="132" height="52" rx="6" class="c-mask" />
<rect
x="208"
y="94"
width="132"
height="52"
rx="6"
class="c-frontend"
data-animate="node"
style="--step: 1"
stroke-width="1.5"
/>
<g
aria-hidden="true"
data-semantic-sigil="frontend"
class="semantic-sigil s-frontend"
transform="translate(214 100) scale(0.6875)"
>
<rect x="2" y="3" width="12" height="10" rx="2" />
<path d="M2 6.5h12" />
<circle cx="4.1" cy="4.8" r=".7" class="sigil-fill" />
<circle cx="6.3" cy="4.8" r=".7" class="sigil-fill" />
</g>
<text
data-node-label=""
data-detail-anchor=""
x="274"
y="115"
class="t-primary"
font-size="11"
font-weight="600"
text-anchor="middle"
>
Chat Surface
</text>
<text
data-detail="context"
x="274"
y="132"
class="t-muted"
font-size="8"
text-anchor="middle"
>
thread + files
</text>
</g>
<g
id="node-final"
data-node-id="final"
data-node-label="Final Reply"
tabindex="0"
role="button"
aria-label="Focus Final Reply, answer + changes, User Interface › Execute + report"
aria-pressed="false"
data-node-kind="backend"
data-node-sublabel="answer + changes"
data-node-context="User Interface › Execute + report"
>
<title>Final Reply · answer + changes · User Interface › Execute + report</title>
<rect x="924" y="94" width="132" height="52" rx="6" class="c-mask" />
<rect
x="924"
y="94"
width="132"
height="52"
rx="6"
class="c-backend"
data-animate="node"
style="--step: 7"
stroke-width="1.5"
/>
<g
aria-hidden="true"
data-semantic-sigil="backend"
class="semantic-sigil s-backend"
transform="translate(930 100) scale(0.6875)"
>
<path d="M6 3 3 8l3 5M10 3l3 5-3 5" />
</g>
<text
data-node-label=""
data-detail-anchor=""
x="990"
y="115"
class="t-primary"
font-size="11"
font-weight="600"
text-anchor="middle"
>
Final Reply
</text>
<text
data-detail="context"
x="990"
y="132"
class="t-muted"
font-size="8"
text-anchor="middle"
>
answer + changes
</text>
</g>
<g
id="node-planner"
data-node-id="planner"
data-node-label="Agent Planner"
tabindex="0"
role="button"
aria-label="Focus Agent Planner, plan next step, Agent Runtime › Planning loop › Plan + route"
aria-pressed="false"
data-node-kind="backend"
data-node-sublabel="plan next step"
data-node-tag="context aware"
data-node-context="Agent Runtime › Planning loop › Plan + route"
>
<title>
Agent Planner · plan next step · Agent Runtime › Planning loop › Plan + route ·
context aware
</title>
<rect x="425.2" y="219" width="132" height="68" rx="6" class="c-mask" />
<rect
x="425.2"
y="219"
width="132"
height="68"
rx="6"
class="c-backend"
data-animate="node"
style="--step: 2"
stroke-width="1.5"
/>
<g
aria-hidden="true"
data-semantic-sigil="backend"
class="semantic-sigil s-backend"
transform="translate(431.2 225) scale(0.6875)"
>
<path d="M6 3 3 8l3 5M10 3l3 5-3 5" />
</g>
<text
data-node-label=""
data-detail-anchor=""
x="491.2"
y="240"
class="t-primary"
font-size="11"
font-weight="600"
text-anchor="middle"
>
Agent Planner
</text>
<text
data-detail="context"
x="491.2"
y="257"
class="t-muted"
font-size="8"
text-anchor="middle"
>
plan next step
</text>
<text
data-detail="fine"
x="491.2"
y="275"
class="t-backend"
font-size="7"
text-anchor="middle"
>
context aware
</text>
</g>
<g
id="node-router"
data-node-id="router"
data-node-label="Tool Router"
tabindex="0"
role="button"
aria-label="Focus Tool Router, choose capability, Agent Runtime › Planning loop › Plan + route"
aria-pressed="false"
data-node-kind="backend"
data-node-sublabel="choose capability"
data-node-context="Agent Runtime › Planning loop › Plan + route"
>
<title>
Tool Router · choose capability · Agent Runtime › Planning loop › Plan + route
</title>
<rect x="585.2" y="227" width="132" height="52" rx="6" class="c-mask" />
<rect
x="585.2"
y="227"
width="132"
height="52"
rx="6"
class="c-backend"
data-animate="node"
style="--step: 3"
stroke-width="1.5"
/>
<g
aria-hidden="true"
data-semantic-sigil="backend"
class="semantic-sigil s-backend"
transform="translate(591.2 233) scale(0.6875)"
>
<path d="M6 3 3 8l3 5M10 3l3 5-3 5" />
</g>
<text
data-node-label=""
data-detail-anchor=""
x="651.2"
y="248"
class="t-primary"
font-size="11"
font-weight="600"
text-anchor="middle"
>
Tool Router
</text>
<text
data-detail="context"
x="651.2"
y="265"
class="t-muted"
font-size="8"
text-anchor="middle"
>
choose capability
</text>
</g>
<g
id="node-approval"
data-node-id="approval"
data-node-label="Approval Gate"
tabindex="0"
role="button"
aria-label="Focus Approval Gate, scope + consent, Policy & Recovery › Human or policy stop › Plan + route"
aria-pressed="false"
data-node-kind="security"
data-node-sublabel="scope + consent"
data-node-tag="block risky ops"
data-node-context="Policy & Recovery › Human or policy stop › Plan + route"
>
<title>
Approval Gate · scope + consent · Policy & Recovery › Human or policy stop › Plan
+ route · block risky ops
</title>
<rect x="585.2" y="353" width="132" height="68" rx="6" class="c-mask" />
<rect
x="585.2"
y="353"
width="132"
height="68"
rx="6"
class="c-security"
data-animate="node"
style="--step: 4"
stroke-width="1.5"
/>
<g
aria-hidden="true"
data-semantic-sigil="security"
class="semantic-sigil s-security"
transform="translate(591.2 359) scale(0.6875)"
>
<path d="M8 2.2 13 4v3.5c0 3.1-1.8 5.4-5 6.5-3.2-1.1-5-3.4-5-6.5V4Z" />
<path d="m5.8 8 1.5 1.5 3-3" />
</g>
<text
data-node-label=""
data-detail-anchor=""
x="651.2"
y="374"
class="t-primary"
font-size="11"
font-weight="600"
text-anchor="middle"
>
Approval Gate
</text>
<text
data-detail="context"
x="651.2"
y="391"
class="t-muted"
font-size="8"
text-anchor="middle"
>
scope + consent
</text>
<text
data-detail="fine"
x="651.2"
y="409"
class="t-security"
font-size="7"
text-anchor="middle"
>
block risky ops
</text>
</g>
<g
id="node-blocked"
data-node-id="blocked"
data-node-label="Blocked"
tabindex="0"
role="button"
aria-label="Focus Blocked, wait or reject, Policy & Recovery › Human or policy stop › Execute + report"
aria-pressed="false"
data-node-kind="security"
data-node-sublabel="wait or reject"
data-node-context="Policy & Recovery › Human or policy stop › Execute + report"
>
<title>
Blocked · wait or reject · Policy & Recovery › Human or policy stop › Execute +
report
</title>
<rect x="764" y="361" width="132" height="52" rx="6" class="c-mask" />
<rect
x="764"
y="361"
width="132"
height="52"
rx="6"
class="c-security"
data-animate="node"
style="--step: 12"
stroke-width="1.5"
/>
<g
aria-hidden="true"
data-semantic-sigil="security"
class="semantic-sigil s-security"
transform="translate(770 367) scale(0.6875)"
>
<path d="M8 2.2 13 4v3.5c0 3.1-1.8 5.4-5 6.5-3.2-1.1-5-3.4-5-6.5V4Z" />
<path d="m5.8 8 1.5 1.5 3-3" />
</g>
<text
data-node-label=""
data-detail-anchor=""
x="830"
y="382"
class="t-primary"
font-size="11"
font-weight="600"
text-anchor="middle"
>
Blocked
</text>
<text
data-detail="context"
x="830"
y="399"
class="t-muted"
font-size="8"
text-anchor="middle"
>
wait or reject
</text>
</g>
<g
id="node-retry"
data-node-id="retry"
data-node-label="Retry Path"
tabindex="0"
role="button"
aria-label="Focus Retry Path, revise request, Policy & Recovery › Human or policy stop › Execute + report"
aria-pressed="false"
data-node-kind="messagebus"
data-node-sublabel="revise request"
data-node-context="Policy & Recovery › Human or policy stop › Execute + report"
>
<title>
Retry Path · revise request · Policy & Recovery › Human or policy stop › Execute +
report
</title>
<rect x="924" y="361" width="132" height="52" rx="6" class="c-mask" />
<rect
x="924"
y="361"
width="132"
height="52"
rx="6"
class="c-messagebus"
data-animate="node"
style="--step: 12"
stroke-width="1.5"
/>
<g
aria-hidden="true"
data-semantic-sigil="messagebus"
class="semantic-sigil s-messagebus"
transform="translate(930 367) scale(0.6875)"
>
<path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11" />
<circle cx="5" cy="4.5" r="1" class="sigil-fill" />
<circle cx="10.5" cy="8" r="1" class="sigil-fill" />
<circle cx="7" cy="11.5" r="1" class="sigil-fill" />
</g>
<text
data-node-label=""
data-detail-anchor=""
x="990"
y="382"
class="t-primary"
font-size="11"
font-weight="600"
text-anchor="middle"
>
Retry Path
</text>
<text
data-detail="context"
x="990"
y="399"
class="t-muted"
font-size="8"
text-anchor="middle"
>
revise request
</text>
</g>
<g
id="node-store"
data-node-id="store"
data-node-label="Context Store"
tabindex="0"
role="button"
aria-label="Focus Context Store, repo + memory, Tool Execution & Evidence › Evidence path › Intake"
aria-pressed="false"
data-node-kind="database"
data-node-sublabel="repo + memory"
data-node-context="Tool Execution & Evidence › Evidence path › Intake"
>
<title>
Context Store · repo + memory · Tool Execution & Evidence › Evidence path › Intake
</title>
<rect x="208" y="495" width="132" height="52" rx="6" class="c-mask" />
<rect
x="208"
y="495"
width="132"
height="52"
rx="6"
class="c-database"
data-animate="node"
style="--step: 12"
stroke-width="1.5"
/>
<g
aria-hidden="true"
data-semantic-sigil="database"
class="semantic-sigil s-database"
transform="translate(214 501) scale(0.6875)"
>
<ellipse cx="8" cy="4" rx="5" ry="2" />
<path d="M3 4v8c0 1.1 2.2 2 5 2s5-.9 5-2V4M3 8c0 1.1 2.2 2 5 2s5-.9 5-2" />
</g>
<text
data-node-label=""
data-detail-anchor=""
x="274"
y="516"
class="t-primary"
font-size="11"
font-weight="600"
text-anchor="middle"
>
Context Store
</text>
<text
data-detail="context"
x="274"
y="533"
class="t-muted"
font-size="8"
text-anchor="middle"
>
repo + memory
</text>
</g>
<g
id="node-trace"
data-node-id="trace"
data-node-label="Trace Log"
tabindex="0"
role="button"
aria-label="Focus Trace Log, events + output, Tool Execution & Evidence › Evidence path › Plan + route"
aria-pressed="false"
data-node-kind="database"
data-node-sublabel="events + output"
data-node-context="Tool Execution & Evidence › Evidence path › Plan + route"
>
<title>
Trace Log · events + output · Tool Execution & Evidence › Evidence path › Plan +
route
</title>
<rect x="425.2" y="495" width="132" height="52" rx="6" class="c-mask" />
<rect
x="425.2"
y="495"
width="132"
height="52"
rx="6"
class="c-database"
data-animate="node"
style="--step: 12"
stroke-width="1.5"
/>
<g
aria-hidden="true"
data-semantic-sigil="database"
class="semantic-sigil s-database"
transform="translate(431.2 501) scale(0.6875)"
>
<ellipse cx="8" cy="4" rx="5" ry="2" />
<path d="M3 4v8c0 1.1 2.2 2 5 2s5-.9 5-2V4M3 8c0 1.1 2.2 2 5 2s5-.9 5-2" />
</g>
<text
data-node-label=""
data-detail-anchor=""
x="491.2"
y="516"
class="t-primary"
font-size="11"
font-weight="600"
text-anchor="middle"
>
Trace Log
</text>
<text
data-detail="context"
x="491.2"
y="533"
class="t-muted"
font-size="8"
text-anchor="middle"
>
events + output
</text>
</g>
<g
id="node-tool"
data-node-id="tool"
data-node-label="Tool Call"
tabindex="0"
role="button"
aria-label="Focus Tool Call, shell / browser / MCP, Tool Execution & Evidence › Tool work › Execute + report"
aria-pressed="false"
data-node-kind="messagebus"
data-node-sublabel="shell / browser / MCP"
data-node-tag="structured result"
data-node-context="Tool Execution & Evidence › Tool work › Execute + report"
>
<title>
Tool Call · shell / browser / MCP · Tool Execution & Evidence › Tool work ›
Execute + report · structured result
</title>
<rect x="764" y="487" width="132" height="68" rx="6" class="c-mask" />
<rect
x="764"
y="487"
width="132"
height="68"
rx="6"
class="c-messagebus"
data-animate="node"
style="--step: 5"
stroke-width="1.5"
/>
<g
aria-hidden="true"
data-semantic-sigil="messagebus"
class="semantic-sigil s-messagebus"
transform="translate(770 493) scale(0.6875)"
>
<path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11" />
<circle cx="5" cy="4.5" r="1" class="sigil-fill" />
<circle cx="10.5" cy="8" r="1" class="sigil-fill" />
<circle cx="7" cy="11.5" r="1" class="sigil-fill" />
</g>
<text
data-node-label=""
data-detail-anchor=""
x="830"
y="508"
class="t-primary"
font-size="11"
font-weight="600"
text-anchor="middle"
>
Tool Call
</text>
<text
data-detail="context"
x="830"
y="525"
class="t-muted"
font-size="8"
text-anchor="middle"
>
shell / browser / MCP
</text>
<text
data-detail="fine"
x="830"
y="543"
class="t-messagebus"
font-size="7"
text-anchor="middle"
>
structured result
</text>
</g>
<g
id="node-external"
data-node-id="external"
data-node-label="External API"
tabindex="0"
role="button"
aria-label="Focus External API, network service, Tool Execution & Evidence › Tool work › Execute + report"
aria-pressed="false"
data-node-kind="cloud"
data-node-sublabel="network service"
data-node-context="Tool Execution & Evidence › Tool work › Execute + report"
>
<title>
External API · network service · Tool Execution & Evidence › Tool work › Execute +
report
</title>
<rect x="924" y="495" width="132" height="52" rx="6" class="c-mask" />
<rect
x="924"
y="495"
width="132"
height="52"
rx="6"
class="c-cloud"
data-animate="node"
style="--step: 6"
stroke-width="1.5"
/>
<g
aria-hidden="true"
data-semantic-sigil="cloud"
class="semantic-sigil s-cloud"
transform="translate(930 501) scale(0.6875)"
>
<path
d="M4.3 12.5h7.3a2.4 2.4 0 0 0 .2-4.8 4 4 0 0 0-7.5-1.3A3.1 3.1 0 0 0 4.3 12.5Z"
/>
</g>
<text
data-node-label=""
data-detail-anchor=""
x="990"
y="516"
class="t-primary"
font-size="11"
font-weight="600"
text-anchor="middle"
>
External API
</text>
<text
data-detail="context"
x="990"
y="533"
class="t-muted"
font-size="8"
text-anchor="middle"
>
network service
</text>
</g>
<!-- Edge labels -->
<g
data-detail="context"
data-edge-from="router"
data-edge-to="approval"
data-edge-label="needs approval?"
data-edge-key="0"
data-edge-id="approval-check"
>
<rect x="569.2" y="282" width="82" height="14" rx="3" class="c-mask" />
<text x="610.2" y="292" class="t-security" font-size="8" text-anchor="middle">
needs approval?
</text>
</g>
<g
data-detail="context"
data-edge-from="approval"
data-edge-to="blocked"
data-edge-label="denied"
data-edge-key="1"
data-edge-id="approval-denied"
>
<rect x="721.2" y="422" width="38.8" height="14" rx="3" class="c-mask" />
<text x="740.6" y="432" class="t-security" font-size="8" text-anchor="middle">
denied
</text>
</g>
<g
data-detail="context"
data-edge-from="chat"
data-edge-to="planner"
data-edge-label="plan"
data-edge-key="4"
data-edge-id="plan-request"
>
<rect x="334.6" y="233" width="30" height="14" rx="3" class="c-mask" />
<text x="349.6" y="243" class="t-backend" font-size="8" text-anchor="middle">plan</text>
</g>
<g
data-detail="context"
data-edge-from="external"
data-edge-to="trace"
data-edge-label="record result"
data-edge-key="6"
data-edge-id="record-result"
>
<rect x="704.4" y="559" width="72.4" height="14" rx="3" class="c-mask" />
<text x="740.6" y="569" class="t-database" font-size="8" text-anchor="middle">
record result
</text>
</g>
<g
data-detail="context"
data-edge-from="store"
data-edge-to="trace"
data-edge-label="trace + memory"
data-edge-key="10"
data-edge-id="write-trace-memory"
>
<rect x="344" y="501" width="77.2" height="14" rx="3" class="c-mask" />
<text x="382.6" y="511" class="t-database" font-size="8" text-anchor="middle">
trace + memory
</text>
</g>
<!-- Legend -->
<g data-legend="" data-legend-bridge="">
<text x="20" y="584" class="t-primary" font-size="12" font-weight="650">Legend</text>
<g
data-legend-semantic-kind="frontend"
data-legend-kind="frontend"
data-legend-label="User UI"
data-legend-x="20"
data-legend-baseline="604"
data-legend-width="74"
>
<rect
x="20"
y="596"
width="14"
height="9"
rx="2"
class="c-frontend"
stroke-width="1"
/>
<text x="42" y="604" class="t-muted" font-size="7.5" font-weight="500">User UI</text>
</g>
<g
data-legend-semantic-kind="backend"
data-legend-kind="backend"
data-legend-label="Agent logic"
data-legend-x="101"
data-legend-baseline="604"
data-legend-width="91"
>
<rect
x="101"
y="596"
width="14"
height="9"
rx="2"
class="c-backend"
stroke-width="1"
/>
<text x="123" y="604" class="t-muted" font-size="7.5" font-weight="500">
Agent logic
</text>
</g>
<g
data-legend-semantic-kind="security"
data-legend-kind="security"
data-legend-label="Policy"
data-legend-x="199"
data-legend-baseline="604"
data-legend-width="70"
>
<rect
x="199"
y="596"
width="14"
height="9"
rx="2"
class="c-security"
stroke-width="1"
/>
<text x="221" y="604" class="t-muted" font-size="7.5" font-weight="500">Policy</text>
</g>
<g
data-legend-semantic-kind="messagebus"
data-legend-kind="messagebus"
data-legend-label="Tool action"
data-legend-x="276"
data-legend-baseline="604"
data-legend-width="91"
>
<rect
x="276"
y="596"
width="14"
height="9"
rx="2"
class="c-messagebus"
stroke-width="1"
/>
<text x="298" y="604" class="t-muted" font-size="7.5" font-weight="500">
Tool action
</text>
</g>
<g
data-legend-semantic-kind="database"
data-legend-kind="database"
data-legend-label="Context / trace"
data-legend-x="374"
data-legend-baseline="604"
data-legend-width="109"
>
<rect
x="374"
y="596"
width="14"
height="9"
rx="2"
class="c-database"
stroke-width="1"
/>
<text x="396" y="604" class="t-muted" font-size="7.5" font-weight="500">
Context / trace
</text>
</g>
<g
data-legend-semantic-kind="cloud"
data-legend-kind="cloud"
data-legend-label="Cloud service"
data-legend-x="490"
data-legend-baseline="604"
data-legend-width="100"
>
<rect x="490" y="596" width="14" height="9" rx="2" class="c-cloud" stroke-width="1" />
<text x="512" y="604" class="t-muted" font-size="7.5" font-weight="500">
Cloud service
</text>
</g>
<g
data-legend-semantic-kind="external"
data-legend-kind="external"
data-legend-label="External system"
data-legend-x="597"
data-legend-baseline="604"
data-legend-width="109"
>
<rect
x="597"
y="596"
width="14"
height="9"
rx="2"
class="c-external"
stroke-width="1"
/>
<text x="619" y="604" class="t-muted" font-size="7.5" font-weight="500">
External system
</text>
</g>
</g>
</svg>
<p
class="intent-trace-status no-print"
id="intent-trace-status"
role="status"
aria-live="polite"
aria-atomic="true"
></p>
<div
class="share-chapter-cue no-print"
id="share-chapter-cue"
hidden
role="status"
aria-live="polite"
>
<div class="share-chapter-index">
<span class="share-chapter-state" id="share-chapter-state">Ready</span>
<span class="share-chapter-count" id="share-chapter-count">Chapter 01 / 01</span>
</div>
<div class="share-chapter-copy">
<strong id="share-chapter-label">Guided chapter</strong>
<small id="share-chapter-note"></small>
</div>
<span class="share-chapter-route" id="share-chapter-route"></span>
<span class="share-chapter-progress" aria-hidden="true"
><span id="share-chapter-progress-bar"></span
></span>
</div>
<div
class="diagram-guide no-print"
id="diagram-guide"
hidden
role="dialog"
aria-modal="false"
aria-labelledby="diagram-guide-title"
>
<div class="diagram-guide-head">
<span>
<span class="diagram-guide-eyebrow">Diagram guide</span>
<strong class="diagram-guide-title" id="diagram-guide-title"
>Explore this system</strong
>
</span>
<button
class="diagram-guide-close"
id="diagram-guide-close"
type="button"
aria-label="Close diagram guide"
>
×
</button>
</div>
<p class="diagram-guide-stats" id="diagram-guide-stats">Inspecting compiled semantics</p>
<div
class="diagram-guide-actions"
id="diagram-guide-actions"
role="group"
aria-label="Diagram exploration actions"
>
<button
class="diagram-guide-action"
type="button"
data-guide-action="find"
aria-label="Find any node"
>
<span class="diagram-guide-index" aria-hidden="true">01</span>
<span class="diagram-guide-action-copy"
><strong>Find any node</strong
><small>Search labels, responsibilities, kinds, and stable IDs.</small></span
>
<kbd aria-hidden="true">/</kbd>
</button>
<button
class="diagram-guide-action"
type="button"
data-guide-action="route"
aria-label="Trace a directed route"
>
<span class="diagram-guide-index" aria-hidden="true">02</span>
<span class="diagram-guide-action-copy"
><strong>Trace a route</strong
><small>Ask how two semantic nodes connect in authored direction.</small></span
>
<kbd aria-hidden="true">R</kbd>
</button>
<button
class="diagram-guide-action"
type="button"
data-guide-action="map"
aria-label="See the whole system"
>
<span class="diagram-guide-index" aria-hidden="true">03</span>
<span class="diagram-guide-action-copy"
><strong>See the whole system</strong
><small>Open Semantic Radar with a live viewport and stable nodes.</small></span
>
<kbd aria-hidden="true">M</kbd>
</button>
<button
class="diagram-guide-action"
type="button"
data-guide-action="lens"
aria-label="Compare semantic kinds"
>
<span class="diagram-guide-index" aria-hidden="true">04</span>
<span class="diagram-guide-action-copy"
><strong>Compare semantic kinds</strong
><small
>Count roles, reveal their traffic, and compare direct authored links.</small
></span
>
<kbd aria-hidden="true">L</kbd>
</button>
<button
class="diagram-guide-action"
type="button"
data-guide-action="story"
aria-label="Play the guided story"
>
<span class="diagram-guide-index" aria-hidden="true">05</span>
<span class="diagram-guide-action-copy"
><strong>Play the guided story</strong
><small id="diagram-guide-story-copy"
>Walk the authored chapters and real relationships.</small
></span
>
<kbd aria-hidden="true">P</kbd>
</button>
<button
class="diagram-guide-action"
type="button"
data-guide-action="present"
aria-label="Enter presentation stage"
>
<span class="diagram-guide-index" aria-hidden="true">06</span>
<span class="diagram-guide-action-copy"
><strong>Enter Presentation Stage</strong
><small>Give the live diagram the viewport without changing export.</small></span
>
<kbd aria-hidden="true">F</kbd>
</button>
</div>
<div class="diagram-guide-shortcuts" aria-label="Additional keyboard shortcuts">
<span><kbd>E</kbd> Export</span><span><kbd>T</kbd> Theme</span
><span><kbd>S</kbd> Style</span><span><kbd>0</kbd> Reset</span
><span><kbd>+</kbd> Zoom in</span><span><kbd>-</kbd> Zoom out</span
><span><kbd>Esc</kbd> Close</span>
</div>
<p class="diagram-guide-feedback" id="diagram-guide-feedback" aria-live="polite"></p>
</div>
<div
class="node-finder no-print"
id="node-finder"
hidden
role="dialog"
aria-modal="false"
aria-labelledby="node-finder-title"
>
<div class="node-finder-head">
<strong id="node-finder-title">Find a node</strong>
<button
class="node-finder-close"
id="node-finder-close"
type="button"
aria-label="Close node finder"
>
×
</button>
</div>
<div class="node-finder-search">
<span aria-hidden="true">⌕</span>
<input
class="node-finder-input"
id="node-finder-input"
type="search"
autocomplete="off"
spellcheck="false"
placeholder="Search labels or IDs"
aria-label="Search diagram nodes"
aria-describedby="node-finder-status"
/>
<kbd aria-hidden="true">/</kbd>
</div>
<div
class="node-finder-results"
id="node-finder-results"
role="group"
aria-label="Diagram nodes"
></div>
<p class="node-finder-empty" id="node-finder-empty" hidden>No matching nodes</p>
<p class="node-finder-status" id="node-finder-status" aria-live="polite"></p>
</div>
<div
class="focus-chip relationship-lens no-print"
id="focus-chip"
hidden
role="region"
aria-labelledby="relationship-lens-title"
>
<div class="relationship-lens-head">
<div class="relationship-lens-copy">
<span class="relationship-lens-eyebrow">Semantic passport</span>
<strong class="relationship-lens-title" id="relationship-lens-title"
><span id="focus-label"></span
></strong>
<span class="semantic-passport-detail" id="focus-detail" hidden></span>
<div
class="semantic-passport-meta"
id="focus-passport-meta"
aria-label="Node metadata"
>
<span id="focus-kind" data-passport="kind"></span>
<span id="focus-context" data-passport="context" hidden></span>
<span id="focus-tag" data-passport="tag" hidden></span>
<span id="focus-brand" data-passport="brand" hidden></span>
<code id="focus-id" data-passport="id"></code>
</div>
<div
class="semantic-passport-evidence"
id="focus-evidence"
hidden
aria-label="Verified source evidence"
>
<div class="semantic-passport-evidence-head">
<span class="semantic-passport-evidence-status">Verified source</span>
<a
class="semantic-passport-repository"
id="focus-repository"
target="_blank"
rel="noopener noreferrer"
referrerpolicy="no-referrer"
></a>
</div>
<div class="semantic-passport-evidence-links" id="focus-evidence-links"></div>
</div>
<span class="relationship-lens-summary" id="focus-summary" aria-live="polite"></span>
<div class="semantic-passport-reach" id="focus-reach" hidden>
<span class="semantic-passport-reach-label">Authored reach</span>
<div
class="semantic-passport-reach-actions"
role="group"
aria-label="Trace authored reachability"
>
<button
id="btn-reach-upstream"
type="button"
aria-label="Trace upstream authored reachability"
aria-pressed="false"
>
<span>Upstream</span><strong id="focus-reach-upstream-count">0</strong>
</button>
<button
id="btn-reach-downstream"
type="button"
aria-label="Trace downstream authored reachability"
aria-pressed="false"
>
<span>Downstream</span><strong id="focus-reach-downstream-count">0</strong>
</button>
</div>
<small
class="semantic-passport-reach-status"
id="focus-reach-status"
hidden
aria-live="polite"
></small>
</div>
</div>
<div class="relationship-lens-actions">
<button
id="btn-focus-clear"
type="button"
aria-label="Close semantic passport"
title="Close"
>
×
</button>
<button id="btn-focus-copy" type="button" aria-label="Copy link to focused node">
Copy link
</button>
<button
id="btn-focus-relations"
type="button"
aria-label="Show connected relationships"
aria-expanded="false"
aria-controls="relationship-lens-list"
>
Relations
</button>
</div>
</div>
<div
class="relationship-lens-list"
id="relationship-lens-list"
aria-label="Connected relationships"
></div>
</div>
<div
class="route-probe no-print"
id="route-probe"
hidden
role="region"
aria-labelledby="route-probe-title"
data-state="idle"
>
<div class="route-probe-head">
<span class="route-probe-copy">
<span class="route-probe-eyebrow">Route probe</span>
<strong class="route-probe-title" id="route-probe-title">Choose a start node</strong>
</span>
<span class="route-probe-actions">
<button
class="route-probe-find"
id="route-probe-find"
type="button"
aria-label="Find a route start"
data-node-finder-trigger
>
Find start
</button>
<button
class="route-probe-copy-link"
id="route-probe-copy"
type="button"
aria-label="Copy link to traced route"
hidden
>
Copy link
</button>
<button id="route-probe-clear" type="button" aria-label="Clear route probe">
Clear
</button>
</span>
</div>
<div class="route-probe-path" id="route-probe-path" aria-label="Traced route">
<span class="route-probe-placeholder">Pick two semantic nodes on the diagram</span>
</div>
<div
class="route-journey-controls"
id="route-journey-controls"
hidden
role="group"
aria-label="Route journey controls"
data-playing="false"
>
<button
id="route-journey-prev"
type="button"
aria-label="Previous route position"
title="Previous route position"
>
←
</button>
<button
class="route-journey-play"
id="route-journey-play"
type="button"
aria-label="Play route journey"
aria-pressed="false"
title="Play route journey"
>
<span id="route-journey-play-icon" aria-hidden="true">▶</span>
<span id="route-journey-play-label">Journey</span>
</button>
<button
id="route-journey-next"
type="button"
aria-label="Next route position"
title="Next route position"
>
→
</button>
<button
class="route-journey-overview"
id="route-journey-overview"
type="button"
aria-label="Show complete route overview"
title="Show complete route overview"
>
Overview
</button>
</div>
<small class="route-probe-status" id="route-probe-status" aria-live="polite"
>Choose the source, then the destination. Direction matters.</small
>
</div>
<div
class="semantic-lens no-print"
id="semantic-lens"
hidden
role="dialog"
aria-modal="false"
aria-labelledby="semantic-lens-title"
>
<div class="semantic-lens-head">
<span>
<span class="semantic-lens-eyebrow">Semantic lens</span>
<strong class="semantic-lens-title" id="semantic-lens-title"
>Compare system roles</strong
>
</span>
<button
class="semantic-lens-close"
id="semantic-lens-close"
type="button"
aria-label="Close semantic lens"
>
×
</button>
</div>
<p class="semantic-lens-instruction">
Choose up to two semantic kinds. One reveals its real traffic; two compare only direct
authored relationships.
</p>
<div
class="semantic-lens-kinds"
id="semantic-lens-kinds"
role="group"
aria-label="Semantic kinds"
></div>
<p class="semantic-lens-status" id="semantic-lens-status" aria-live="polite">
Choose a kind to inspect its nodes and touching relationships.
</p>
<div class="semantic-lens-actions">
<button
id="semantic-lens-copy"
type="button"
aria-label="Copy link to semantic lens"
hidden
>
Copy link
</button>
<button id="semantic-lens-clear" type="button" aria-label="Clear semantic lens">
Clear
</button>
</div>
</div>
<div
class="overview-map no-print"
id="overview-map"
hidden
role="region"
aria-labelledby="overview-map-title"
>
<div class="overview-map-head">
<span class="overview-map-live" aria-hidden="true"></span>
<span class="overview-map-copy">
<strong id="overview-map-title">Semantic radar</strong>
<small id="overview-map-status" aria-live="polite">Building overview</small>
</span>
<button
class="overview-map-expand"
id="overview-map-expand"
type="button"
aria-label="Open full semantic radar"
>
Open radar
</button>
<button
class="overview-map-close"
id="overview-map-close"
type="button"
aria-label="Close semantic radar"
>
×
</button>
</div>
<div
class="overview-map-surface"
id="overview-map-surface"
tabindex="0"
role="group"
aria-label="Diagram overview. Click a node to focus it, or use arrow keys to pan."
></div>
<div class="overview-map-hint" aria-hidden="true">
<span>Click node</span><span>Drag to pan</span>
</div>
</div>
<p
class="overview-map-feedback no-print"
id="overview-map-feedback"
role="status"
aria-live="polite"
hidden
>
Semantic radar needs more MAP space.
</p>
<div class="diagram-nav no-print" role="toolbar" aria-label="Diagram view controls">
<button
id="btn-route-probe"
type="button"
aria-label="Trace a directed route"
aria-pressed="false"
aria-controls="route-probe"
title="Trace route (R)"
>
PATH
</button>
<button
id="btn-overview-map"
type="button"
aria-label="Open semantic radar"
aria-expanded="false"
aria-controls="overview-map"
title="Semantic radar (M)"
>
MAP
</button>
<button
id="btn-semantic-lens"
type="button"
aria-label="Open semantic lens"
aria-haspopup="dialog"
aria-expanded="false"
aria-pressed="false"
aria-controls="semantic-lens"
title="Semantic lens (L)"
>
LENS
</button>
<button
id="btn-node-finder"
type="button"
aria-label="Find a node"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="node-finder"
title="Find a node (/)"
data-node-finder-trigger
>
<span class="diagram-nav-icon find" aria-hidden="true"></span>
</button>
<button
id="btn-diagram-guide"
type="button"
aria-label="Open diagram guide"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="diagram-guide"
title="Diagram guide (?)"
>
<span class="diagram-nav-icon guide" aria-hidden="true"></span>
</button>
<button data-view="out" type="button" aria-label="Zoom out" title="Zoom out (-)">
<span class="diagram-nav-icon minus" aria-hidden="true"></span>
</button>
<button
data-view="reset"
type="button"
aria-label="Reset diagram view"
title="Reset view (0)"
>
<span class="diagram-nav-detail" data-view-detail hidden>READ</span
><span class="diagram-nav-percent" data-view-percent>100%</span>
</button>
<button data-view="in" type="button" aria-label="Zoom in" title="Zoom in (+)">
<span class="diagram-nav-icon plus" aria-hidden="true"></span>
</button>
</div>
</div>
<!-- Info Cards -->
<div class="cards">
<div class="card">
<div class="card-header">
<div class="card-dot cyan"></div>
<h3>Compiler Contract</h3>
</div>
<ul>
<li>• Lanes and columns determine node placement</li>
<li>• Labels reserve clearance; routes stay orthogonal</li>
</ul>
</div>
<div class="card">
<div class="card-header">
<div class="card-dot rose"></div>
<h3>Runtime Semantics</h3>
</div>
<ul>
<li>• Approval gates risky work before tool execution</li>
<li>• Evidence returns through isolated trace and memory</li>
</ul>
</div>
</div>
</div>
<script>
var Archify = {};
var archifyI18nData = (function () {
var node = document.getElementById("archify-i18n-data");
try {
return JSON.parse(node ? node.textContent : "{}");
} catch (_) {
return { locale: "en", messages: {} };
}
})();
Archify.locale = archifyI18nData.locale || "en";
function viewerText(key, values) {
var messages = archifyI18nData.messages || {};
var template = Object.prototype.hasOwnProperty.call(messages, key) ? messages[key] : key;
return String(template).replace(/\{([a-zA-Z0-9_]+)\}/g, function (match, name) {
return values && Object.prototype.hasOwnProperty.call(values, name)
? String(values[name])
: match;
});
}
function viewerCount(key, count, values) {
var suffix = Number(count) === 1 ? ".one" : ".other";
var payload = Object.assign({}, values || {}, { count: count });
return viewerText(key + suffix, payload);
}
function viewerKindLabel(value) {
var normalized = String(value || "node").toLowerCase();
var knownKey = "viewer.kind." + normalized;
var known = viewerText(knownKey);
if (known !== knownKey) return known;
return normalized
.replace(/messagebus/gi, "message bus")
.replace(/[-_]+/g, " ")
.replace(/\b\w/g, function (letter) {
return letter.toUpperCase();
});
}
function hasDrawableGeometry(element) {
if (!element) return false;
var geometries = /^(path|line|polyline)$/i.test(element.tagName)
? [element]
: Array.prototype.slice.call(element.querySelectorAll("path, line, polyline"));
return geometries.some(function (geometry) {
var source =
geometry.tagName.toLowerCase() === "path"
? geometry.getAttribute("d")
: geometry.tagName.toLowerCase() === "polyline"
? geometry.getAttribute("points")
: [
geometry.getAttribute("x1"),
geometry.getAttribute("y1"),
geometry.getAttribute("x2"),
geometry.getAttribute("y2"),
].join(" ");
if (
!source ||
/(?:^|[^a-z])(?:nan|infinity)(?:[^a-z]|$)/i.test(source) ||
typeof geometry.getTotalLength !== "function"
)
return false;
try {
var length = Number(geometry.getTotalLength());
return Number.isFinite(length) && length > 0;
} catch (_) {
return false;
}
});
}
/* ============================================================
Visual style try-on — one topology, four bundled presets.
The reader's choice is intentionally session-only: it updates the
live page and canonical SVG together, but never rewrites the source,
URL, or a later diagram's authored default.
============================================================ */
Archify.preset = (function () {
var PRESETS = ["classic", "signal-flow", "blueprint", "editorial"];
var LABELS = {
classic: viewerText("viewer.preset.classic.short"),
"signal-flow": viewerText("viewer.preset.flow.short"),
blueprint: viewerText("viewer.preset.blueprint"),
editorial: viewerText("viewer.preset.editorial"),
};
var html = document.documentElement;
var svg = document.querySelector(".diagram-container svg");
var btn = document.getElementById("btn-preset");
var label = document.getElementById("preset-label");
var menu = document.getElementById("preset-menu");
var options = function () {
return Array.prototype.slice.call(menu.querySelectorAll("[data-preset-value]"));
};
var authored =
PRESETS.indexOf(html.getAttribute("data-preset")) >= 0
? html.getAttribute("data-preset")
: "classic";
function current() {
var value = html.getAttribute("data-preset");
return PRESETS.indexOf(value) >= 0 ? value : authored;
}
function nextAfter(preset) {
return PRESETS[(PRESETS.indexOf(preset) + 1) % PRESETS.length];
}
function apply(preset) {
if (html.getAttribute("data-embed") === "true") return false;
if (PRESETS.indexOf(preset) < 0) return false;
html.setAttribute("data-preset", preset);
svg.setAttribute("data-preset", preset);
btn.setAttribute("data-preset-option", preset);
label.textContent = LABELS[preset];
btn.setAttribute(
"aria-label",
viewerText("viewer.preset.current", { style: LABELS[preset] }),
);
btn.title = viewerText("viewer.preset.choose.title");
options().forEach(function (option) {
var selected = option.getAttribute("data-preset-value") === preset;
option.setAttribute("aria-checked", String(selected));
});
return true;
}
function cycle() {
return apply(nextAfter(current()));
}
function isOpen() {
return menu.classList.contains("open");
}
function open(focusLast) {
if (html.getAttribute("data-embed") === "true") return false;
if (Archify.exportMenu && Archify.exportMenu.isOpen()) Archify.exportMenu.close(false);
menu.classList.add("open");
btn.setAttribute("aria-expanded", "true");
var available = options();
var selected = available.find(function (option) {
return option.getAttribute("aria-checked") === "true";
});
var target = focusLast ? available[available.length - 1] : selected || available[0];
if (target) target.focus();
return true;
}
function close(focusTrigger) {
menu.classList.remove("open");
btn.setAttribute("aria-expanded", "false");
if (focusTrigger) btn.focus();
return false;
}
apply(authored);
btn.addEventListener("click", function (e) {
e.stopPropagation();
if (isOpen()) close(false);
else open(false);
});
btn.addEventListener("keydown", function (e) {
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
if (!isOpen()) open(e.key === "ArrowUp");
}
});
document.addEventListener("click", function (e) {
if (!menu.contains(e.target) && e.target !== btn) close(false);
});
menu.addEventListener("click", function (e) {
var option = e.target.closest("[data-preset-value]");
if (!option || !menu.contains(option)) return;
if (apply(option.getAttribute("data-preset-value"))) close(true);
});
menu.addEventListener("keydown", function (e) {
if (e.key === "Escape") {
e.preventDefault();
close(true);
return;
}
if (e.key === "Tab") {
close(false);
return;
}
var available = options();
var active = available.indexOf(document.activeElement);
switch (e.key) {
case "ArrowDown":
e.preventDefault();
if (available.length)
available[(active + 1 + available.length) % available.length].focus();
break;
case "ArrowUp":
e.preventDefault();
if (available.length)
available[(active - 1 + available.length) % available.length].focus();
break;
case "Home":
e.preventDefault();
if (available[0]) available[0].focus();
break;
case "End":
e.preventDefault();
if (available.length) available[available.length - 1].focus();
break;
}
});
return {
cycle: cycle,
apply: apply,
current: current,
authored: authored,
open: open,
close: close,
isOpen: isOpen,
};
})();
/* ============================================================
Theme toggle — persists to localStorage, defaults to dark
============================================================ */
Archify.theme = (function () {
var STORAGE_KEY = "archify-theme";
var html = document.documentElement;
var btn = document.getElementById("btn-theme");
var label = document.getElementById("theme-label");
// localStorage can throw (blocked cookies, sandboxed iframes); the whole
// script element dies on an uncaught error, so guard every access.
function readStored() {
try {
return localStorage.getItem(STORAGE_KEY);
} catch (_) {
return null;
}
}
function writeStored(value) {
try {
localStorage.setItem(STORAGE_KEY, value);
} catch (_) {}
}
function urlOverride() {
// URL param wins (useful for deterministic screenshots / share links)
try {
var param = new URLSearchParams(window.location.search).get("theme");
if (param === "light" || param === "dark") return param;
} catch (_) {}
return null;
}
function resolveInitial() {
var fromUrl = urlOverride();
if (fromUrl) return fromUrl;
var saved = readStored();
if (saved === "light" || saved === "dark") return saved;
return "dark";
}
function apply(theme) {
html.setAttribute("data-theme", theme);
label.textContent = viewerText(
theme === "dark" ? "viewer.theme.dark" : "viewer.theme.light",
);
btn.setAttribute("aria-pressed", theme === "light" ? "true" : "false");
}
function toggle() {
var next = html.getAttribute("data-theme") === "dark" ? "light" : "dark";
apply(next);
writeStored(next);
}
apply(resolveInitial());
btn.addEventListener("click", toggle);
return { toggle: toggle };
})();
/* ============================================================
Export — Share Card / PNG / JPEG / WebP / SVG / WebM and clipboard
Raster exports are always rendered at 4x source resolution for
maximum sharpness. The trick: we set the serialized SVG's
`width`/`height` to viewBox * 4 so the browser rasterizes the
vectors at that resolution natively. drawImage then draws at
the image's natural size (no upscaling = no blur).
JPEG/WebP paint the current theme's background explicitly since
those formats have no alpha channel.
============================================================ */
(function () {
var RASTER_SCALE = 4;
var MOTION_DURATION = 6000;
var MOTION_FPS = 30;
var SHARE_CARD_WIDTH = 1200;
var SHARE_CARD_HEIGHT = 630;
var SHARE_CARD_PADDING = 36;
var SHARE_CARD_HEADER = 112;
function exportError(key, values) {
var error = new Error(viewerText(key, values));
error.archifyViewerMessage = true;
return error;
}
function exportMessage(error) {
return error && error.archifyViewerMessage
? error.message
: viewerText("viewer.export.unknown");
}
function diagramFilename() {
var title = (document.title || "diagram")
.replace(/\s+Architecture(\s+Diagram)?$/i, "")
.replace(/\s+Diagram$/i, "")
.trim();
return (
title
.replace(/[^a-z0-9_\-]+/gi, "-")
.toLowerCase()
.replace(/^-+|-+$/g, "") || "diagram"
);
}
function currentBg() {
return getComputedStyle(document.body).backgroundColor || "#ffffff";
}
/**
* Serialize the main SVG into a standalone document.
*
* Two modes:
* - Default (opts.autoTheme=false) — locks the serialized SVG to the
* viewer's current theme. Used by the raster pipeline
* (PNG/JPEG/WebP/clipboard) because canvas rasterization needs
* deterministic colors; a raster cannot react to
* prefers-color-scheme after encoding.
* - autoTheme=true — emits BOTH dark and light variable sets plus a
* `@media (prefers-color-scheme)` rule so the resulting SVG
* self-themes when embedded in GitHub READMEs or other hosts that
* expose a color scheme. Used for "Download SVG".
*/
function applyRouteSnapshot(clone, snapshot) {
if (
!snapshot ||
!Array.isArray(snapshot.nodeIds) ||
!Array.isArray(snapshot.edges) ||
snapshot.nodeIds.length < 2 ||
snapshot.edges.length !== snapshot.nodeIds.length - 1 ||
snapshot.hops !== snapshot.edges.length ||
!snapshot.source ||
!snapshot.target ||
snapshot.source.id !== snapshot.nodeIds[0] ||
snapshot.target.id !== snapshot.nodeIds[snapshot.nodeIds.length - 1]
)
return false;
clone.removeAttribute("data-animation");
Array.prototype.forEach.call(
clone.querySelectorAll("[data-animate]"),
function (element) {
element.removeAttribute("data-animate");
element.style.removeProperty("--step");
},
);
var cloneNodes = Array.prototype.slice.call(clone.querySelectorAll("[data-node-id]"));
var cloneEdges = Array.prototype.slice.call(clone.querySelectorAll("[data-edge-key]"));
var nodeIds = Object.create(null);
var edgeKeys = Object.create(null);
var nodeMatches = [];
var edgeMatches = [];
for (var nodeIndex = 0; nodeIndex < snapshot.nodeIds.length; nodeIndex++) {
var nodeId = snapshot.nodeIds[nodeIndex];
if (typeof nodeId !== "string" || !nodeId || nodeIds[nodeId]) return false;
var matchedNodes = cloneNodes.filter(function (candidate) {
return candidate.getAttribute("data-node-id") === nodeId;
});
if (matchedNodes.length !== 1) return false;
nodeIds[nodeId] = true;
nodeMatches.push(matchedNodes);
}
for (var edgeIndex = 0; edgeIndex < snapshot.edges.length; edgeIndex++) {
var edge = snapshot.edges[edgeIndex];
var fromId = snapshot.nodeIds[edgeIndex];
var toId = snapshot.nodeIds[edgeIndex + 1];
if (
!edge ||
typeof edge.key !== "string" ||
!edge.key ||
edgeKeys[edge.key] ||
edge.from !== fromId ||
edge.to !== toId
)
return false;
var matchedEdges = cloneEdges.filter(function (candidate) {
return candidate.getAttribute("data-edge-key") === edge.key;
});
var drawableMatches = matchedEdges.filter(hasDrawableGeometry);
if (
!matchedEdges.length ||
drawableMatches.length !== 1 ||
!matchedEdges.every(function (candidate) {
return (
candidate.getAttribute("data-edge-from") === edge.from &&
candidate.getAttribute("data-edge-to") === edge.to &&
(candidate.getAttribute("data-edge-id") || "") === (edge.id || "")
);
})
)
return false;
edgeKeys[edge.key] = true;
edgeMatches.push(matchedEdges);
}
clone.setAttribute("data-share-route", "");
nodeMatches.forEach(function (matches, step) {
matches.forEach(function (element) {
element.setAttribute("data-share-route-match", "");
element.setAttribute("data-share-route-step", String(step));
if (step === 0) element.setAttribute("data-share-route-start", "");
else if (step === snapshot.nodeIds.length - 1)
element.setAttribute("data-share-route-end", "");
else element.setAttribute("data-share-route-middle", "");
});
});
edgeMatches.forEach(function (matches, step) {
matches.forEach(function (element) {
element.setAttribute("data-share-route-match", "");
element.setAttribute("data-share-route-step", String(step));
});
});
return (
clone.hasAttribute("data-share-route") &&
!clone.hasAttribute("data-animation") &&
!clone.hasAttribute("data-route-active") &&
!clone.hasAttribute("data-route-journey") &&
clone.querySelectorAll(
"[data-animate], [data-route-match], [data-route-step], [data-route-start], [data-route-end], [data-route-journey-state], [data-route-journey-current], [data-route-journey-overlay]",
).length === 0 &&
clone.querySelectorAll("[data-share-route-match]").length ===
nodeMatches.reduce(function (count, matches) {
return count + matches.length;
}, 0) +
edgeMatches.reduce(function (count, matches) {
return count + matches.length;
}, 0)
);
}
function applyReachSnapshot(clone, snapshot) {
if (
!snapshot ||
(snapshot.direction !== "upstream" && snapshot.direction !== "downstream") ||
!snapshot.origin ||
typeof snapshot.origin.id !== "string" ||
!snapshot.origin.id ||
typeof snapshot.origin.label !== "string" ||
!snapshot.origin.label.trim() ||
!Array.isArray(snapshot.nodeIds) ||
snapshot.nodeIds.length < 2 ||
!Array.isArray(snapshot.edges) ||
!snapshot.edges.length ||
!snapshot.depths ||
typeof snapshot.depths !== "object" ||
!Number.isInteger(snapshot.maxDepth) ||
snapshot.maxDepth < 1 ||
snapshot.nodeIds[0] !== snapshot.origin.id
)
return false;
clone.removeAttribute("data-animation");
Array.prototype.forEach.call(
clone.querySelectorAll("[data-animate]"),
function (element) {
element.removeAttribute("data-animate");
element.style.removeProperty("--step");
},
);
var cloneNodes = Array.prototype.slice.call(clone.querySelectorAll("[data-node-id]"));
var cloneEdges = Array.prototype.slice.call(clone.querySelectorAll("[data-edge-key]"));
var nodeIds = Object.create(null);
var edgeKeys = Object.create(null);
var nodeMatches = [];
var edgeMatches = [];
var measuredMaxDepth = 0;
for (var nodeIndex = 0; nodeIndex < snapshot.nodeIds.length; nodeIndex++) {
var nodeId = snapshot.nodeIds[nodeIndex];
var depth = snapshot.depths[nodeId];
if (
typeof nodeId !== "string" ||
!nodeId ||
nodeIds[nodeId] ||
!Number.isInteger(depth) ||
depth < 0 ||
depth > snapshot.maxDepth ||
(nodeId === snapshot.origin.id ? depth !== 0 : depth < 1)
)
return false;
var matchedNodes = cloneNodes.filter(function (candidate) {
return candidate.getAttribute("data-node-id") === nodeId;
});
if (matchedNodes.length !== 1) return false;
nodeIds[nodeId] = true;
measuredMaxDepth = Math.max(measuredMaxDepth, depth);
nodeMatches.push({ elements: matchedNodes, id: nodeId, depth: depth });
}
if (measuredMaxDepth !== snapshot.maxDepth) return false;
for (var edgeIndex = 0; edgeIndex < snapshot.edges.length; edgeIndex++) {
var edge = snapshot.edges[edgeIndex];
if (
!edge ||
typeof edge.key !== "string" ||
!edge.key ||
edgeKeys[edge.key] ||
!nodeIds[edge.from] ||
!nodeIds[edge.to] ||
!Number.isInteger(edge.depth) ||
edge.depth < 1 ||
edge.depth > snapshot.maxDepth ||
edge.depth !== Math.max(snapshot.depths[edge.from], snapshot.depths[edge.to])
)
return false;
var matchedEdges = cloneEdges.filter(function (candidate) {
return candidate.getAttribute("data-edge-key") === edge.key;
});
var drawableMatches = matchedEdges.filter(hasDrawableGeometry);
if (
!matchedEdges.length ||
drawableMatches.length !== 1 ||
!matchedEdges.every(function (candidate) {
return (
candidate.getAttribute("data-edge-from") === edge.from &&
candidate.getAttribute("data-edge-to") === edge.to &&
(candidate.getAttribute("data-edge-id") || "") === (edge.id || "")
);
})
)
return false;
edgeKeys[edge.key] = true;
edgeMatches.push({ elements: matchedEdges, depth: edge.depth });
}
clone.setAttribute("data-share-reach", snapshot.direction);
nodeMatches.forEach(function (match) {
match.elements.forEach(function (element) {
element.setAttribute("data-share-reach-match", "");
element.setAttribute("data-share-reach-depth", String(match.depth));
if (match.id === snapshot.origin.id)
element.setAttribute("data-share-reach-origin", "");
});
});
edgeMatches.forEach(function (match) {
match.elements.forEach(function (element) {
element.setAttribute("data-share-reach-match", "");
element.setAttribute("data-share-reach-depth", String(match.depth));
});
});
return (
clone.getAttribute("data-share-reach") === snapshot.direction &&
!clone.hasAttribute("data-animation") &&
!clone.hasAttribute("data-reach-active") &&
clone.querySelectorAll(
"[data-animate], [data-reach-match], [data-reach-origin], [data-reach-depth]",
).length === 0 &&
clone.querySelectorAll("[data-share-reach-origin]").length === 1 &&
clone.querySelectorAll("[data-share-reach-match]").length ===
nodeMatches.reduce(function (count, match) {
return count + match.elements.length;
}, 0) +
edgeMatches.reduce(function (count, match) {
return count + match.elements.length;
}, 0)
);
}
function serializeSvg(scale, opts) {
// scale: integer multiplier for intrinsic SVG pixel dimensions used by
// the raster path. Defaults to 1 (natural size) for SVG download.
scale = scale || 1;
opts = opts || {};
var autoTheme = opts.autoTheme === true;
var svg = document.querySelector(".diagram-container svg");
var clone = svg.cloneNode(true);
// View transforms and neighborhood focus are HTML exploration state,
// never part of a downloaded full-diagram artifact.
clone.style.removeProperty("transform");
clone.style.removeProperty("clip-path");
clone.removeAttribute("data-view-scale");
clone.removeAttribute("data-focus-active");
clone.removeAttribute("data-reach-active");
clone.removeAttribute("data-lens-active");
clone.removeAttribute("data-lens-flow-count");
clone.removeAttribute("data-lens-flow-density");
clone.removeAttribute("data-legend-preview-active");
clone.removeAttribute("data-relationship-preview-active");
clone.removeAttribute("data-relationship-direct-active");
clone.removeAttribute("data-relationship-pin-active");
clone.removeAttribute("data-intent-trace-active");
clone.removeAttribute("data-route-picking");
clone.removeAttribute("data-route-active");
clone.removeAttribute("data-route-journey");
clone.removeAttribute("data-share-route");
clone.removeAttribute("data-share-reach");
clone.removeAttribute("data-story-active");
clone.removeAttribute("data-story-playing");
clone.removeAttribute("data-story-beat");
clone.removeAttribute("data-story-next");
clone.removeAttribute("data-story-follow");
clone.removeAttribute("data-chapter-handoff");
clone.removeAttribute("data-chapter-anchor");
clone.removeAttribute("data-chapter-preview");
Array.prototype.forEach.call(
clone.querySelectorAll("[data-chapter-handoff-overlay]"),
function (el) {
el.remove();
},
);
Array.prototype.forEach.call(
clone.querySelectorAll("[data-chapter-role]"),
function (el) {
el.removeAttribute("data-chapter-role");
},
);
Array.prototype.forEach.call(
clone.querySelectorAll("[data-chapter-preview-role]"),
function (el) {
el.removeAttribute("data-chapter-preview-role");
},
);
Array.prototype.forEach.call(
clone.querySelectorAll("[data-story-overlay], [data-story-carrier-overlay]"),
function (el) {
el.remove();
},
);
Array.prototype.forEach.call(
clone.querySelectorAll("[data-intent-trace-overlay]"),
function (el) {
el.remove();
},
);
Array.prototype.forEach.call(
clone.querySelectorAll("[data-route-probe-overlay]"),
function (el) {
el.remove();
},
);
Array.prototype.forEach.call(
clone.querySelectorAll("[data-route-journey-overlay]"),
function (el) {
el.remove();
},
);
Array.prototype.forEach.call(
clone.querySelectorAll("[data-semantic-lens-overlay]"),
function (el) {
el.remove();
},
);
Array.prototype.forEach.call(
clone.querySelectorAll("[data-legend-bridge-runtime]"),
function (el) {
el.remove();
},
);
Array.prototype.forEach.call(clone.querySelectorAll("[data-legend-kind]"), function (el) {
el.removeAttribute("data-legend-kind");
el.removeAttribute("data-legend-label");
el.removeAttribute("data-legend-count");
el.removeAttribute("data-legend-zero");
el.removeAttribute("data-legend-selected");
el.removeAttribute("role");
el.removeAttribute("tabindex");
el.removeAttribute("aria-label");
el.removeAttribute("aria-pressed");
el.removeAttribute("aria-haspopup");
el.removeAttribute("aria-controls");
el.removeAttribute("aria-expanded");
});
Array.prototype.forEach.call(
clone.querySelectorAll("[data-legend-bridge]"),
function (el) {
el.removeAttribute("data-legend-bridge");
el.removeAttribute("role");
el.removeAttribute("aria-label");
},
);
Array.prototype.forEach.call(
clone.querySelectorAll("[data-relationship-pulse-overlay]"),
function (el) {
el.remove();
},
);
Array.prototype.forEach.call(
clone.querySelectorAll("[data-relationship-hit-overlay]"),
function (el) {
el.remove();
},
);
Array.prototype.forEach.call(
clone.querySelectorAll("[data-source-evidence-beacon]"),
function (el) {
el.remove();
},
);
Array.prototype.forEach.call(
clone.querySelectorAll("[data-source-evidence-count]"),
function (el) {
var originalLabel = el.getAttribute("data-source-evidence-original-label");
if (originalLabel == null || originalLabel === "") el.removeAttribute("aria-label");
else el.setAttribute("aria-label", originalLabel);
el.removeAttribute("data-source-evidence-count");
el.removeAttribute("data-source-evidence-original-label");
},
);
Array.prototype.forEach.call(
clone.querySelectorAll(
"[data-story-step], [data-story-beat-state], [data-story-beat-step]",
),
function (el) {
el.removeAttribute("data-story-step");
el.removeAttribute("data-story-beat-state");
el.removeAttribute("data-story-beat-step");
el.style.removeProperty("--story-step");
},
);
Array.prototype.forEach.call(
clone.querySelectorAll("[data-focus-match], [data-focus-selected]"),
function (el) {
el.removeAttribute("data-focus-match");
el.removeAttribute("data-focus-selected");
},
);
Array.prototype.forEach.call(
clone.querySelectorAll("[data-reach-match], [data-reach-origin], [data-reach-depth]"),
function (el) {
el.removeAttribute("data-reach-match");
el.removeAttribute("data-reach-origin");
el.removeAttribute("data-reach-depth");
},
);
Array.prototype.forEach.call(
clone.querySelectorAll("[data-lens-match], [data-lens-selected], [data-lens-peer]"),
function (el) {
el.removeAttribute("data-lens-match");
el.removeAttribute("data-lens-selected");
el.removeAttribute("data-lens-peer");
},
);
Array.prototype.forEach.call(
clone.querySelectorAll(
"[data-legend-preview-match], [data-legend-preview-selected], [data-legend-preview-peer]",
),
function (el) {
el.removeAttribute("data-legend-preview-match");
el.removeAttribute("data-legend-preview-selected");
el.removeAttribute("data-legend-preview-peer");
},
);
Array.prototype.forEach.call(
clone.querySelectorAll(
"[data-relationship-preview], [data-relationship-preview-node], [data-relationship-preview-source], [data-relationship-preview-target]",
),
function (el) {
el.removeAttribute("data-relationship-preview");
el.removeAttribute("data-relationship-preview-node");
el.removeAttribute("data-relationship-preview-source");
el.removeAttribute("data-relationship-preview-target");
},
);
Array.prototype.forEach.call(
clone.querySelectorAll("[data-intent-trace-match], [data-intent-trace-selected]"),
function (el) {
el.removeAttribute("data-intent-trace-match");
el.removeAttribute("data-intent-trace-selected");
},
);
Array.prototype.forEach.call(
clone.querySelectorAll(
"[data-route-match], [data-route-start], [data-route-end], [data-route-step], [data-route-candidate], [data-route-journey-state], [data-route-journey-current]",
),
function (el) {
el.removeAttribute("data-route-match");
el.removeAttribute("data-route-start");
el.removeAttribute("data-route-end");
el.removeAttribute("data-route-step");
el.removeAttribute("data-route-candidate");
el.removeAttribute("data-route-journey-state");
el.removeAttribute("data-route-journey-current");
el.style.removeProperty("--route-step");
},
);
Array.prototype.forEach.call(
clone.querySelectorAll(
"[data-share-route-match], [data-share-route-step], [data-share-route-start], [data-share-route-end], [data-share-route-middle]",
),
function (el) {
el.removeAttribute("data-share-route-match");
el.removeAttribute("data-share-route-step");
el.removeAttribute("data-share-route-start");
el.removeAttribute("data-share-route-end");
el.removeAttribute("data-share-route-middle");
},
);
Array.prototype.forEach.call(
clone.querySelectorAll(
"[data-share-reach-match], [data-share-reach-origin], [data-share-reach-depth]",
),
function (el) {
el.removeAttribute("data-share-reach-match");
el.removeAttribute("data-share-reach-origin");
el.removeAttribute("data-share-reach-depth");
},
);
Array.prototype.forEach.call(
clone.querySelectorAll("[data-detail], [data-detail-anchor]"),
function (el) {
el.removeAttribute("data-detail");
el.removeAttribute("data-detail-anchor");
},
);
Array.prototype.forEach.call(
clone.querySelectorAll("[data-node-id][aria-pressed]"),
function (el) {
el.setAttribute("aria-pressed", "false");
},
);
var canonicalStateClean =
!clone.hasAttribute("data-view-scale") &&
!clone.hasAttribute("data-focus-active") &&
!clone.hasAttribute("data-reach-active") &&
!clone.hasAttribute("data-lens-active") &&
!clone.hasAttribute("data-lens-flow-count") &&
!clone.hasAttribute("data-lens-flow-density") &&
!clone.hasAttribute("data-legend-preview-active") &&
!clone.hasAttribute("data-relationship-preview-active") &&
!clone.hasAttribute("data-relationship-direct-active") &&
!clone.hasAttribute("data-intent-trace-active") &&
!clone.hasAttribute("data-route-picking") &&
!clone.hasAttribute("data-route-active") &&
!clone.hasAttribute("data-route-journey") &&
!clone.hasAttribute("data-share-route") &&
!clone.hasAttribute("data-share-reach") &&
!clone.hasAttribute("data-story-active") &&
!clone.hasAttribute("data-story-playing") &&
!clone.hasAttribute("data-story-beat") &&
!clone.hasAttribute("data-story-next") &&
!clone.hasAttribute("data-story-follow") &&
!clone.hasAttribute("data-chapter-handoff") &&
!clone.hasAttribute("data-chapter-anchor") &&
!clone.hasAttribute("data-chapter-preview") &&
!clone.style.getPropertyValue("transform") &&
!clone.style.getPropertyValue("clip-path") &&
clone.querySelectorAll(
"[data-story-overlay], [data-story-carrier-overlay], [data-story-carrier-token], [data-story-step], [data-story-beat-state], [data-story-beat-step], [data-chapter-handoff-overlay], [data-chapter-role], [data-chapter-preview-role], [data-focus-match], [data-focus-selected], [data-reach-match], [data-reach-origin], [data-reach-depth], [data-semantic-lens-overlay], [data-lens-match], [data-lens-selected], [data-lens-peer], [data-legend-bridge], [data-legend-kind], [data-legend-bridge-runtime], [data-legend-count], [data-legend-zero], [data-legend-selected], [data-legend-preview-match], [data-legend-preview-selected], [data-legend-preview-peer], [data-relationship-hit-overlay], [data-relationship-pulse-overlay], [data-relationship-preview], [data-relationship-preview-node], [data-relationship-preview-source], [data-relationship-preview-target], [data-intent-trace-overlay], [data-intent-trace-match], [data-intent-trace-selected], [data-route-probe-overlay], [data-route-journey-overlay], [data-route-match], [data-route-start], [data-route-end], [data-route-step], [data-route-candidate], [data-route-journey-state], [data-route-journey-current], [data-share-route-match], [data-share-route-step], [data-share-route-start], [data-share-route-end], [data-share-route-middle], [data-share-reach-match], [data-share-reach-origin], [data-share-reach-depth], [data-source-evidence-beacon], [data-source-evidence-count], [data-source-evidence-original-label], [data-detail], [data-detail-anchor]",
).length === 0;
var vb = svg.viewBox.baseVal;
var finiteSvgDimensions =
Number.isFinite(vb.x) &&
Number.isFinite(vb.y) &&
Number.isFinite(vb.width) &&
Number.isFinite(vb.height) &&
vb.width > 0 &&
vb.height > 0;
var routeStateClean = opts.routeSnapshot
? canonicalStateClean &&
finiteSvgDimensions &&
applyRouteSnapshot(clone, opts.routeSnapshot)
: null;
var reachStateClean = opts.reachSnapshot
? canonicalStateClean &&
finiteSvgDimensions &&
!opts.routeSnapshot &&
applyReachSnapshot(clone, opts.reachSnapshot)
: null;
// Scale width/height so the browser rasterizes the vectors at target
// resolution directly. viewBox stays unchanged so coordinates don't
// shift. This is the key to sharp rasters — do NOT scale later via
// canvas upscaling.
clone.setAttribute("width", vb.width * scale);
clone.setAttribute("height", vb.height * scale);
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
// Only the SVG-relevant rules: semantic classes, markers, and the
// theme variable blocks. Toolbar/cards/print CSS can never apply
// inside a standalone SVG and would only bloat the export.
var hostStyle = (function () {
var out = [];
Array.prototype.forEach.call(document.styleSheets, function (sheet) {
var rules;
try {
rules = sheet.cssRules;
} catch (_) {
return;
} // cross-origin
if (!rules) return;
Array.prototype.forEach.call(rules, function (rule) {
if (rule.type === 7 && /^archify-/.test(rule.name || "")) {
out.push(rule.cssText);
return;
}
if (rule.type !== 1) return; // plain style rules only
var sel = rule.selectorText || "";
if (
/(^|,)\s*(svg|:root|\[data-theme|\[data-preset|\.c-|\.t-|\.a-|\.m-)/.test(sel)
) {
out.push(rule.cssText);
}
});
});
return out.join("\n");
})();
// Derive the variable list from the stylesheet so newly added theme
// variables can never be missed by the export pipeline again
// (--lane-fill/--lane-stroke once were).
var varNames = (function () {
var seen = {};
var names = [];
(hostStyle.match(/--[a-zA-Z0-9-]+(?=\s*:)/g) || []).forEach(function (n) {
if (!seen[n]) {
seen[n] = true;
names.push(n);
}
});
return names;
})();
// Resolve the full variable set for a given data-theme via an
// off-DOM probe, independent of what the viewer is currently set to.
function resolveVars(themeAttr) {
var probe = document.createElement("div");
probe.setAttribute("data-theme", themeAttr);
probe.setAttribute(
"data-preset",
document.documentElement.getAttribute("data-preset") || "classic",
);
probe.style.cssText = "position:absolute;width:0;height:0;visibility:hidden;";
document.body.appendChild(probe);
try {
var c = getComputedStyle(probe);
return varNames
.map(function (n) {
return n + ": " + c.getPropertyValue(n).trim() + ";";
})
.join(" ");
} finally {
document.body.removeChild(probe);
}
}
// Prepend a local()-only @font-face block for each weight so that the
// sandboxed image-rendering context used during PNG/JPEG/WebP export
// can find JetBrains Mono if the user has it installed locally, and
// falls through cleanly to the monospace stack otherwise. The usual
// Google-Fonts <link> in the host document is unreachable from the
// serialized SVG, so url()-sourced faces would just hang.
var fontFallback = [400, 500, 600, 700]
.map(function (w) {
return (
"@font-face { font-family: 'JetBrains Mono'; font-weight: " +
w +
"; src: local('JetBrains Mono'), local('JetBrainsMono-Regular'); }"
);
})
.join("\n");
var style = document.createElementNS("http://www.w3.org/2000/svg", "style");
var bgRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
bgRect.setAttribute("width", "100%");
bgRect.setAttribute("height", "100%");
if (autoTheme) {
// Dual-theme SVG. Dark is the default (so hosts without
// prefers-color-scheme still render), light swaps in via media
// query, and svg[data-theme="..."] still lets downstream
// consumers force a specific theme.
var darkVars = resolveVars("dark");
var lightVars = resolveVars("light");
style.textContent =
fontFallback +
"\n" +
"svg { font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, 'DejaVu Sans Mono', 'Liberation Mono', 'Noto Sans Mono CJK SC', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', monospace; }\n" +
hostStyle +
"\n" +
":root, svg { " +
darkVars +
" }\n" +
"@media (prefers-color-scheme: light) { :root, svg { " +
lightVars +
" } }\n" +
'svg[data-theme="light"] { ' +
lightVars +
" }\n" +
'svg[data-theme="dark"] { ' +
darkVars +
" }\n" +
"rect.c-bg-rect { fill: var(--bg); }\n";
// Don't lock the serialized SVG to the viewer's current theme.
clone.removeAttribute("data-theme");
// Background follows the CSS variable, not a fixed color, so it
// swaps with the media query.
bgRect.setAttribute("class", "c-bg-rect");
} else {
// Raster path: lock to the viewer's current theme.
var theme = document.documentElement.getAttribute("data-theme") || "dark";
var themeHost =
document.querySelector('[data-theme="' + theme + '"]') || document.documentElement;
var computed = getComputedStyle(themeHost);
var vars = varNames
.map(function (n) {
return n + ": " + computed.getPropertyValue(n).trim() + ";";
})
.join(" ");
// IMPORTANT: inject the resolved variables AFTER hostStyle,
// otherwise hostStyle's ":root, [data-theme=\"dark\"] { ... }" rule
// overrides our chosen theme via later-in-cascade equal-specificity.
// Keep this order.
style.textContent =
fontFallback +
"\n" +
"svg { font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, 'DejaVu Sans Mono', 'Liberation Mono', 'Noto Sans Mono CJK SC', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', monospace; }\n" +
hostStyle +
"\n" +
":root, svg { " +
vars +
" }\n";
bgRect.setAttribute("fill", computed.getPropertyValue("--bg").trim() || "#ffffff");
}
if (opts.routeSnapshot) {
style.textContent +=
"\nsvg[data-share-route] [data-node-id], svg[data-share-route] [data-edge-from] { opacity: 0.18; }\n" +
"svg[data-share-route] [data-share-route-match] { opacity: 1; }\n" +
"svg[data-share-route] [data-share-route-start] > :is(rect, circle, polygon):not(.c-mask) { stroke-width: 3; stroke-dasharray: 5 3; }\n" +
"svg[data-share-route] [data-share-route-middle] > :is(rect, circle, polygon):not(.c-mask) { stroke-width: 2.2; }\n" +
"svg[data-share-route] [data-share-route-end] > :is(rect, circle, polygon):not(.c-mask) { stroke-width: 3.4; stroke-dasharray: 1 0; }\n";
}
if (opts.reachSnapshot) {
style.textContent +=
"\nsvg[data-share-reach] [data-node-id], svg[data-share-reach] [data-edge-from] { opacity: 0.14; }\n" +
"svg[data-share-reach] [data-share-reach-match] { opacity: 1; }\n" +
"svg[data-share-reach] [data-edge-from][data-share-reach-match] { stroke-width: 1.55; }\n" +
'svg[data-share-reach="upstream"] [data-share-reach-origin] > :is(rect, circle, polygon):not(.c-mask) { stroke: var(--database-stroke); stroke-width: 3.4; stroke-dasharray: 5 3; }\n' +
'svg[data-share-reach="downstream"] [data-share-reach-origin] > :is(rect, circle, polygon):not(.c-mask) { stroke: var(--backend-stroke); stroke-width: 3.4; stroke-dasharray: 1 0; }\n' +
'svg[data-preset="blueprint"][data-share-reach] [data-share-reach-origin], svg[data-preset="blueprint"][data-share-reach] [data-edge-from][data-share-reach-match] { filter: none; }\n';
}
clone.insertBefore(style, clone.firstChild);
clone.insertBefore(bgRect, style.nextSibling);
return {
svgString: new XMLSerializer().serializeToString(clone),
width: vb.width * scale,
height: vb.height * scale,
canonicalStateClean: canonicalStateClean,
routeStateClean: routeStateClean,
reachStateClean: reachStateClean,
};
}
function download(blob, filename) {
var url = URL.createObjectURL(blob);
var a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(function () {
URL.revokeObjectURL(url);
}, 1000);
}
// Conservative upper bound on total canvas pixels. Older Safari on iOS
// silently produces a blank canvas above ~16 Mpx; Chrome / Firefox /
// desktop Safari are far higher but start failing on memory-constrained
// devices. We pick the largest integer scale in {4,3,2,1} whose target
// pixel count fits under this cap.
var MAX_CANVAS_PIXELS = 16 * 1024 * 1024;
function pickSafeScale(vbW, vbH) {
for (var s = RASTER_SCALE; s >= 1; s--) {
if (vbW * s * vbH * s <= MAX_CANVAS_PIXELS) return s;
}
return 1;
}
function rasterize(format) {
// Serialize at a safe scale (RASTER_SCALE=4 by default, reduced if the
// resulting canvas would exceed MAX_CANVAS_PIXELS). The SVG itself is
// rasterized at target resolution natively; drawImage draws at natural
// size — no upsampling blur.
var svg = document.querySelector(".diagram-container svg");
var vb = svg.viewBox.baseVal;
var scale = pickSafeScale(vb.width, vb.height);
var data = serializeSvg(scale);
var svgBlob = new Blob([data.svgString], { type: "image/svg+xml;charset=utf-8" });
var svgUrl = URL.createObjectURL(svgBlob);
return new Promise(function (resolve, reject) {
var img = new Image();
img.onload = function () {
try {
var canvas = document.createElement("canvas");
canvas.width = data.width;
canvas.height = data.height;
var ctx = canvas2dOrThrow(canvas, format);
if (format === "jpeg") {
ctx.fillStyle = currentBg();
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// Draw at natural size — SVG was already rasterized at target res.
ctx.drawImage(img, 0, 0);
URL.revokeObjectURL(svgUrl);
var mime =
format === "jpeg" ? "image/jpeg" : format === "webp" ? "image/webp" : "image/png";
var quality = format === "png" ? undefined : 0.95;
canvas.toBlob(
function (blob) {
if (!blob)
reject(exportError("viewer.export.error.toBlobNull", { label: format }));
else resolve(blob);
},
mime,
quality,
);
} catch (error) {
URL.revokeObjectURL(svgUrl);
reject(error);
}
};
img.onerror = function (e) {
URL.revokeObjectURL(svgUrl);
reject(e);
};
img.src = svgUrl;
});
}
function fitCanvasText(ctx, text, maxWidth, startSize, minSize, weight) {
var value = String(text || "").trim();
var size = startSize;
var family = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
while (size > minSize) {
ctx.font = (weight || "600") + " " + size + "px " + family;
if (ctx.measureText(value).width <= maxWidth) return value;
size -= 1;
}
ctx.font = (weight || "600") + " " + minSize + "px " + family;
if (ctx.measureText(value).width <= maxWidth) return value;
var suffix = "\u2026";
while (value.length > 1 && ctx.measureText(value + suffix).width > maxWidth) {
value = value.slice(0, -1);
}
return value + suffix;
}
function canvas2dOrThrow(canvas, label) {
if (!canvas || typeof canvas.getContext !== "function") {
throw exportError("viewer.export.error.canvasUnavailable", { label: label });
}
var ctx = canvas.getContext("2d");
if (!ctx) throw exportError("viewer.export.error.contextUnavailable", { label: label });
if (typeof canvas.toBlob !== "function")
throw exportError("viewer.export.error.toBlobUnavailable", { label: label });
return ctx;
}
function renderShareCard(options) {
options = options || {};
var routeSnapshot = options.routeSnapshot || null;
var reachSnapshot = options.reachSnapshot || null;
if (routeSnapshot && reachSnapshot)
return Promise.reject(exportError("viewer.export.error.variantsCombined"));
var svg = document.querySelector(".diagram-container svg");
var vb = svg.viewBox.baseVal;
var sourceScale = Math.min(2, pickSafeScale(vb.width, vb.height));
var data = serializeSvg(sourceScale, {
routeSnapshot: routeSnapshot,
reachSnapshot: reachSnapshot,
});
if (!data.canonicalStateClean)
return Promise.reject(exportError("viewer.export.error.viewerState"));
if (routeSnapshot && !data.routeStateClean)
return Promise.reject(exportError("viewer.export.error.routeState"));
if (reachSnapshot && !data.reachStateClean)
return Promise.reject(exportError("viewer.export.error.reachState"));
var svgBlob = new Blob([data.svgString], { type: "image/svg+xml;charset=utf-8" });
var svgUrl = URL.createObjectURL(svgBlob);
return new Promise(function (resolve, reject) {
var img = new Image();
img.onload = function () {
try {
var canvas = document.createElement("canvas");
canvas.width = SHARE_CARD_WIDTH;
canvas.height = SHARE_CARD_HEIGHT;
var ctx = canvas2dOrThrow(canvas, viewerText("viewer.export.shareCard"));
var computed = getComputedStyle(document.documentElement);
var bg = computed.getPropertyValue("--bg").trim() || currentBg();
var text = computed.getPropertyValue("--text").trim() || "#ffffff";
var muted = computed.getPropertyValue("--text-muted").trim() || "#94a3b8";
var border = computed.getPropertyValue("--panel-border").trim() || "#334155";
var accentProperty = reachSnapshot
? reachSnapshot.direction === "upstream"
? "--database-stroke"
: "--backend-stroke"
: "--frontend-stroke";
var accent = computed.getPropertyValue(accentProperty).trim() || "#22d3ee";
var titleNode = document.querySelector(".header h1");
var subtitleNode = document.querySelector(".header .subtitle");
var title = titleNode ? titleNode.textContent : document.title;
var directionLabel = reachSnapshot
? viewerText("viewer.export.direction." + reachSnapshot.direction)
: "";
var subtitle = routeSnapshot
? viewerCount("viewer.export.card.routeSummary", routeSnapshot.hops, {
source: routeSnapshot.source.label,
target: routeSnapshot.target.label,
})
: reachSnapshot
? viewerText("viewer.export.card.reachSummary", {
direction: directionLabel,
origin: reachSnapshot.origin.label,
nodes: viewerCount(
"viewer.export.card.node",
reachSnapshot.nodeIds.length - 1,
),
links: viewerCount("viewer.export.card.link", reachSnapshot.edges.length),
hops: viewerCount("viewer.export.card.hop", reachSnapshot.maxDepth),
})
: subtitleNode
? subtitleNode.textContent
: "";
var preset = document.documentElement.getAttribute("data-preset") || "classic";
var theme = document.documentElement.getAttribute("data-theme") || "dark";
var presetKey =
preset === "signal-flow" ? "viewer.preset.flow.short" : "viewer.preset." + preset;
var presetLabel = viewerText(presetKey).toUpperCase();
var themeLabel = viewerText("viewer.theme." + theme).toUpperCase();
var cardLabel = routeSnapshot
? viewerText("viewer.export.card.routeBadge", {
hops: viewerCount("viewer.export.card.hop", routeSnapshot.hops).toUpperCase(),
})
: reachSnapshot
? viewerText("viewer.export.card.reachBadge", {
direction: directionLabel.toUpperCase(),
})
: viewerText("viewer.export.card.defaultBadge", {
preset: presetLabel,
theme: themeLabel,
});
ctx.fillStyle = bg;
ctx.fillRect(0, 0, SHARE_CARD_WIDTH, SHARE_CARD_HEIGHT);
ctx.fillStyle = accent;
ctx.fillRect(SHARE_CARD_PADDING, 27, 42, 3);
ctx.textBaseline = "alphabetic";
ctx.fillStyle = text;
var fittedTitle = fitCanvasText(
ctx,
title,
SHARE_CARD_WIDTH - SHARE_CARD_PADDING * 2 - 330,
29,
18,
"700",
);
ctx.fillText(fittedTitle, SHARE_CARD_PADDING, 62);
ctx.fillStyle = muted;
var fittedSubtitle = fitCanvasText(
ctx,
subtitle,
SHARE_CARD_WIDTH - SHARE_CARD_PADDING * 2 - 280,
13,
11,
"500",
);
ctx.fillText(fittedSubtitle, SHARE_CARD_PADDING, 87);
ctx.font =
"600 12px 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
ctx.textAlign = "right";
ctx.fillStyle = accent;
ctx.fillText(cardLabel, SHARE_CARD_WIDTH - SHARE_CARD_PADDING, 50);
ctx.textAlign = "left";
var availableWidth = SHARE_CARD_WIDTH - SHARE_CARD_PADDING * 2;
var availableHeight = SHARE_CARD_HEIGHT - SHARE_CARD_HEADER - SHARE_CARD_PADDING;
var fit = Math.min(availableWidth / data.width, availableHeight / data.height);
var drawWidth = data.width * fit;
var drawHeight = data.height * fit;
var drawX = SHARE_CARD_PADDING + (availableWidth - drawWidth) / 2;
var drawY = SHARE_CARD_HEADER + (availableHeight - drawHeight) / 2;
ctx.strokeStyle = border;
ctx.lineWidth = 1;
ctx.strokeRect(drawX - 0.5, drawY - 0.5, drawWidth + 1, drawHeight + 1);
ctx.drawImage(img, drawX, drawY, drawWidth, drawHeight);
URL.revokeObjectURL(svgUrl);
canvas.toBlob(function (blob) {
if (!blob)
reject(
exportError("viewer.export.error.toBlobNull", {
label: viewerText("viewer.export.shareCard"),
}),
);
else resolve(blob);
}, "image/png");
} catch (error) {
URL.revokeObjectURL(svgUrl);
reject(error);
}
};
img.onerror = function (e) {
URL.revokeObjectURL(svgUrl);
reject(e);
};
img.src = svgUrl;
});
}
function rasterizeShareCard(options) {
options = options || {};
if (!options.variant) return renderShareCard();
if (options.variant !== "route" && options.variant !== "reach") {
return Promise.reject(
exportError("viewer.export.unknownVariant", { variant: options.variant }),
);
}
if (options.variant === "route") {
var routeSnapshot = Archify.routeProbe && Archify.routeProbe.exportSnapshot();
if (!routeSnapshot) return Promise.reject(exportError("viewer.export.routeRequired"));
return renderShareCard({ routeSnapshot: routeSnapshot });
}
var snapshot =
Archify.focus && typeof Archify.focus.reachabilitySnapshot === "function"
? Archify.focus.reachabilitySnapshot()
: null;
if (!snapshot) return Promise.reject(exportError("viewer.export.reachRequired"));
return renderShareCard({ reachSnapshot: snapshot });
}
function motionMimeType() {
if (typeof MediaRecorder === "undefined") return "";
var candidates = ["video/webm;codecs=vp9", "video/webm;codecs=vp8", "video/webm"];
for (var i = 0; i < candidates.length; i++) {
if (!MediaRecorder.isTypeSupported || MediaRecorder.isTypeSupported(candidates[i]))
return candidates[i];
}
return "";
}
function canRecordMotion() {
var svg = document.querySelector(".diagram-container svg");
return !!(
svg &&
svg.getAttribute("data-animation") === "trace" &&
typeof MediaRecorder !== "undefined" &&
motionMimeType() &&
typeof HTMLCanvasElement !== "undefined" &&
typeof HTMLCanvasElement.prototype.captureStream === "function"
);
}
// Record the live CSS animation without Puppeteer, ffmpeg, or a network
// dependency. SVG files loaded through Image are commonly rasterized as
// one cached bitmap, so repeatedly drawing that Image does not reliably
// advance its CSS animation. Keep one crisp static SVG background, then
// render an explicit time-varying signal scene over the real authored
// relationship geometry on every captured canvas frame.
function recordWebm(options) {
options = options || {};
if (!canRecordMotion()) {
return Promise.reject(exportError("viewer.export.error.webmRequirements"));
}
var duration = Math.max(250, Number(options.duration) || MOTION_DURATION);
var fps = Math.max(1, Number(options.fps) || MOTION_FPS);
var svg = document.querySelector(".diagram-container svg");
var vb = svg.viewBox.baseVal;
var scale = Math.min(1, 1280 / vb.width);
var data = serializeSvg(scale);
var sourceUrl = URL.createObjectURL(
new Blob([data.svgString], { type: "image/svg+xml;charset=utf-8" }),
);
function createMotionScene(root) {
var rootMatrix = root.getCTM ? root.getCTM() : null;
function pointInRoot(element, point) {
if (!rootMatrix || !element.getCTM || !point.matrixTransform)
return { x: point.x, y: point.y };
try {
var elementMatrix = element.getCTM();
if (!elementMatrix) return { x: point.x, y: point.y };
var mapped = point.matrixTransform(rootMatrix.inverse().multiply(elementMatrix));
return { x: mapped.x, y: mapped.y };
} catch (_) {
return { x: point.x, y: point.y };
}
}
function samplesFor(element) {
if (
typeof element.getTotalLength !== "function" ||
typeof element.getPointAtLength !== "function"
)
return [];
var length;
try {
length = element.getTotalLength();
} catch (_) {
return [];
}
if (!Number.isFinite(length) || length <= 0) return [];
var count = Math.max(12, Math.min(72, Math.ceil(length / 12)));
var points = [];
for (var i = 0; i <= count; i++) {
points.push(pointInRoot(element, element.getPointAtLength((length * i) / count)));
}
return points;
}
function authoredStep(element, fallback) {
var raw =
element.style.getPropertyValue("--step") ||
getComputedStyle(element).getPropertyValue("--step");
var value = Number(raw);
return Number.isFinite(value) ? value : fallback;
}
var edges = Array.prototype.slice
.call(root.querySelectorAll('[data-animate="edge"]'))
.map(function (element, index) {
var computed = getComputedStyle(element);
return {
points: samplesFor(element),
color:
computed.stroke && computed.stroke !== "none" ? computed.stroke : "#22d3ee",
width: Math.max(1.5, parseFloat(computed.strokeWidth) || 1.5),
delay: Math.min(12, authoredStep(element, index)) * 0.16,
};
})
.filter(function (edge) {
return edge.points.length > 1;
});
var nodes = Array.prototype.slice
.call(root.querySelectorAll('[data-node-id][data-animate="node"]'))
.map(function (element, index) {
var box;
try {
box = element.getBBox();
} catch (_) {
return null;
}
var painted = element.querySelector('[class*="c-"]') || element;
var computed = getComputedStyle(painted);
return {
x: box.x,
y: box.y,
width: box.width,
height: box.height,
color:
computed.stroke && computed.stroke !== "none" ? computed.stroke : "#22d3ee",
delay: Math.min(12, authoredStep(element, index)) * 0.16,
};
})
.filter(Boolean);
return {
edges: edges,
nodes: nodes,
x: vb.x,
y: vb.y,
width: vb.width,
height: vb.height,
};
}
function pointAlong(points, progress) {
var scaled = Math.max(0, Math.min(1, progress)) * (points.length - 1);
var index = Math.min(points.length - 2, Math.floor(scaled));
var mix = scaled - index;
return {
x: points[index].x + (points[index + 1].x - points[index].x) * mix,
y: points[index].y + (points[index + 1].y - points[index].y) * mix,
};
}
function roundedRectPath(ctx, x, y, width, height, radius) {
var r = Math.max(0, Math.min(radius, width / 2, height / 2));
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + width - r, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + r);
ctx.lineTo(x + width, y + height - r);
ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
ctx.lineTo(x + r, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
function drawMotionFrame(ctx, backgroundImage, motionScene, elapsed) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.drawImage(backgroundImage, 0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.save();
ctx.scale(ctx.canvas.width / motionScene.width, ctx.canvas.height / motionScene.height);
ctx.translate(-motionScene.x, -motionScene.y);
motionScene.edges.forEach(function (edge) {
var duration = 1.75;
var progress = (elapsed - edge.delay) / duration;
if (progress < 0 || progress > 1) return;
var head = Math.max(0, Math.min(1, progress));
var tail = Math.max(0, head - 0.24);
var opacity = Math.sin(Math.PI * Math.min(1, progress));
ctx.save();
ctx.strokeStyle = edge.color;
ctx.fillStyle = edge.color;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.lineWidth = Math.max(3.2, edge.width * 2.2);
ctx.globalAlpha = 0.42 + opacity * 0.5;
ctx.shadowColor = edge.color;
ctx.shadowBlur = 10;
ctx.beginPath();
for (var i = 0; i <= 14; i++) {
var trailPoint = pointAlong(edge.points, tail + ((head - tail) * i) / 14);
if (i === 0) ctx.moveTo(trailPoint.x, trailPoint.y);
else ctx.lineTo(trailPoint.x, trailPoint.y);
}
ctx.stroke();
var point = pointAlong(edge.points, head);
ctx.globalAlpha = 0.95;
ctx.beginPath();
ctx.arc(point.x, point.y, 4.8, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
});
motionScene.nodes.forEach(function (node) {
var progress = (elapsed - node.delay) / 2.6;
if (progress < 0 || progress > 1) return;
var pulse = Math.sin(Math.PI * progress);
if (pulse <= 0.02) return;
ctx.save();
ctx.strokeStyle = node.color;
ctx.lineWidth = 1.4 + pulse * 1.8;
ctx.globalAlpha = pulse * 0.5;
ctx.shadowColor = node.color;
ctx.shadowBlur = 12 * pulse;
var inset = 2 + pulse * 3;
roundedRectPath(
ctx,
node.x - inset,
node.y - inset,
node.width + inset * 2,
node.height + inset * 2,
8,
);
ctx.stroke();
ctx.restore();
});
ctx.restore();
}
var motionScene = createMotionScene(svg);
return new Promise(function (resolve, reject) {
var backgroundImage = new Image();
backgroundImage.onload = function () {
var canvas = document.createElement("canvas");
canvas.width = Math.max(2, Math.round(data.width / 2) * 2);
canvas.height = Math.max(2, Math.round(data.height / 2) * 2);
var ctx = canvas.getContext("2d");
var stream = canvas.captureStream(fps);
var mime = motionMimeType();
var recorder;
try {
recorder = new MediaRecorder(stream, {
mimeType: mime,
videoBitsPerSecond: 6000000,
});
} catch (err) {
URL.revokeObjectURL(sourceUrl);
stream.getTracks().forEach(function (track) {
track.stop();
});
reject(err);
return;
}
var chunks = [];
var raf = 0;
var stopped = false;
var startedAt = performance.now();
function cleanup() {
if (stopped) return;
stopped = true;
cancelAnimationFrame(raf);
URL.revokeObjectURL(sourceUrl);
stream.getTracks().forEach(function (track) {
track.stop();
});
}
function draw(now) {
var elapsed = Math.max(0, ((Number(now) || performance.now()) - startedAt) / 1000);
drawMotionFrame(ctx, backgroundImage, motionScene, elapsed);
raf = requestAnimationFrame(draw);
}
recorder.ondataavailable = function (event) {
if (event.data && event.data.size) chunks.push(event.data);
};
recorder.onerror = function (event) {
cleanup();
reject(event.error || exportError("viewer.export.error.mediaRecorder"));
};
recorder.onstop = function () {
cleanup();
var blob = new Blob(chunks, { type: recorder.mimeType || mime });
if (!blob.size) reject(exportError("viewer.export.error.emptyWebm"));
else resolve(blob);
};
drawMotionFrame(ctx, backgroundImage, motionScene, 0);
recorder.start(250);
startedAt = performance.now();
raf = requestAnimationFrame(draw);
setTimeout(function () {
if (recorder.state === "inactive") return;
// Chromium normally emits the final chunk during stop(), but
// some embedded browser shells need an explicit encoder flush
// first. Keep the short grace window bounded and harmless for
// browsers that already delivered timeslice chunks.
try {
recorder.requestData();
} catch (_) {}
setTimeout(function () {
if (recorder.state !== "inactive") recorder.stop();
}, 120);
}, duration);
};
backgroundImage.onerror = function () {
URL.revokeObjectURL(sourceUrl);
reject(exportError("viewer.export.error.webmBackground"));
};
backgroundImage.src = sourceUrl;
});
}
var menu = document.getElementById("export-menu");
var btn = document.getElementById("btn-export");
var routeShareItem = menu.querySelector('button[data-action="route-share-card"]');
var reachShareItem = menu.querySelector('button[data-action="reach-share-card"]');
var items = function () {
return Array.prototype.slice.call(menu.querySelectorAll('button[role="menuitem"]'));
};
function syncRouteShareItem() {
var snapshot =
Archify.routeProbe && typeof Archify.routeProbe.exportSnapshot === "function"
? Archify.routeProbe.exportSnapshot()
: null;
routeShareItem.hidden = !snapshot;
routeShareItem.disabled = !snapshot;
return snapshot;
}
function syncReachShareItem() {
var snapshot =
Archify.focus && typeof Archify.focus.reachabilitySnapshot === "function"
? Archify.focus.reachabilitySnapshot()
: null;
reachShareItem.hidden = !snapshot;
reachShareItem.disabled = !snapshot;
return snapshot;
}
// ---- Clipboard support ----------------------------------------------
function canCopyImage() {
return (
typeof ClipboardItem !== "undefined" &&
navigator.clipboard &&
typeof navigator.clipboard.write === "function"
);
}
// ---- Raster format detection ----------------------------------------
// canvas.toBlob('image/webp') silently returns a PNG on browsers without
// WebP encoding (older Safari), so detect explicitly.
function supports(format) {
if (format === "share-card") return true;
if (format === "svg" || format === "png") return true;
if (format === "webm") return canRecordMotion();
var mime = format === "jpeg" ? "image/jpeg" : "image/webp";
try {
var c = document.createElement("canvas");
c.width = c.height = 2;
return c.toDataURL(mime).indexOf("data:" + mime) === 0;
} catch (_) {
return false;
}
}
// Gray out unsupported items.
items().forEach(function (it) {
if (it.dataset.format && !supports(it.dataset.format)) {
it.disabled = true;
it.title = viewerText("viewer.export.unsupported");
}
if (
(it.dataset.action === "copy" || it.dataset.action === "copy-share-card") &&
!canCopyImage()
) {
it.disabled = true;
it.title = viewerText("viewer.export.clipboardUnsupported");
}
});
// ---- Toast ----------------------------------------------------------
// A live region only announces CHANGES to an existing node, so the toast
// element is created once up front and updated in place.
var toastEl = document.createElement("div");
toastEl.className = "archify-toast";
toastEl.setAttribute("role", "status");
document.body.appendChild(toastEl);
var toastTimer = null;
function toast(msg) {
toastEl.textContent = "";
requestAnimationFrame(function () {
toastEl.textContent = msg;
toastEl.classList.add("show");
});
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 1500);
}
function open(focusLast) {
if (Archify.preset && Archify.preset.isOpen()) Archify.preset.close(false);
if (Archify.semanticLens && typeof Archify.semanticLens.clearPreview === "function")
Archify.semanticLens.clearPreview();
if (Archify.semanticLens && Archify.semanticLens.isOpen())
Archify.semanticLens.close({ restoreFocus: false });
syncRouteShareItem();
syncReachShareItem();
menu.classList.add("open");
btn.setAttribute("aria-expanded", "true");
var available = items().filter(function (i) {
return !i.hidden && !i.disabled;
});
var target = focusLast ? available[available.length - 1] : available[0];
if (target) target.focus();
}
function close(focusTrigger) {
menu.classList.remove("open");
btn.setAttribute("aria-expanded", "false");
if (focusTrigger) btn.focus();
}
function isOpen() {
return menu.classList.contains("open");
}
btn.addEventListener("click", function (e) {
e.stopPropagation();
if (isOpen()) close(false);
else open();
});
// APG menu-button pattern: ArrowDown/ArrowUp on the trigger opens the menu.
btn.addEventListener("keydown", function (e) {
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
if (!isOpen()) open(e.key === "ArrowUp");
}
});
document.addEventListener("click", function (e) {
if (!menu.contains(e.target) && e.target !== btn) close(false);
});
// Keyboard navigation inside the menu.
// Menu items: Up/Down, Home/End.
// Esc closes + returns focus to trigger; Tab closes.
menu.addEventListener("keydown", function (e) {
if (e.key === "Escape") {
e.preventDefault();
close(true);
return;
}
if (e.key === "Tab") {
close(false);
return;
}
var available = items().filter(function (i) {
return !i.hidden && !i.disabled;
});
var current = available.indexOf(document.activeElement);
switch (e.key) {
case "ArrowDown":
e.preventDefault();
if (available.length)
available[(current + 1 + available.length) % available.length].focus();
break;
case "ArrowUp":
e.preventDefault();
if (available.length)
available[(current - 1 + available.length) % available.length].focus();
break;
case "Home":
e.preventDefault();
if (available[0]) available[0].focus();
break;
case "End":
e.preventDefault();
if (available.length) available[available.length - 1].focus();
break;
}
});
function runExport(format) {
var base = diagramFilename();
close(true);
clearExportReceipt();
if (format === "webm") toast(viewerText("viewer.export.recording"));
return (
format === "share-card"
? rasterizeShareCard().then(function (blob) {
recordExportReceipt("share-card", blob, true, {
width: SHARE_CARD_WIDTH,
height: SHARE_CARD_HEIGHT,
});
download(blob, base + "-share-card.png");
toast(viewerText("viewer.export.downloadedShare"));
})
: format === "svg"
? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) {
var blob = new Blob([d.svgString], { type: "image/svg+xml;charset=utf-8" });
recordExportReceipt("svg", blob, d.canonicalStateClean);
download(blob, base + ".svg");
})
: format === "webm"
? recordWebm().then(function (blob) {
document.documentElement.setAttribute(
"data-last-motion-bytes",
String(blob.size),
);
recordExportReceipt("webm", blob, true);
download(blob, base + ".webm");
toast(viewerText("viewer.export.downloadedWebm"));
})
: rasterize(format).then(function (blob) {
recordExportReceipt(format, blob, true);
download(blob, base + "." + format);
})
).catch(function (err) {
console.error(err);
var technicalMessage = err && err.message ? err.message : format;
var message = exportMessage(err);
document.documentElement.setAttribute("data-last-export-error-format", format);
document.documentElement.setAttribute("data-last-export-error", technicalMessage);
if (format === "webm") {
var motionItem = menu.querySelector('button[data-format="webm"]');
if (motionItem) {
motionItem.disabled = true;
motionItem.title = viewerText("viewer.export.motionUnavailable");
motionItem.style.opacity = "0.5";
}
toast(viewerText("viewer.export.webmUnavailable"));
return;
}
alert(viewerText("viewer.export.failed", { message: message }));
});
}
function runRouteShareCard() {
close(false);
clearExportReceipt();
var snapshot = Archify.routeProbe && Archify.routeProbe.exportSnapshot();
var blobPromise = snapshot
? renderShareCard({ routeSnapshot: snapshot })
: Promise.reject(exportError("viewer.export.routeRequired"));
return blobPromise
.then(function (blob) {
recordExportReceipt(
"share-card",
blob,
false,
{ width: SHARE_CARD_WIDTH, height: SHARE_CARD_HEIGHT },
"route",
true,
);
download(blob, diagramFilename() + "-route-share-card.png");
toast(viewerText("viewer.export.downloadedRoute"));
return blob;
})
.catch(function (err) {
console.error(err);
var technicalMessage = err && err.message ? err.message : "share-card";
var message = exportMessage(err);
document.documentElement.setAttribute("data-last-export-error-format", "share-card");
document.documentElement.setAttribute("data-last-export-error", technicalMessage);
alert(viewerText("viewer.export.routeFailed", { message: message }));
});
}
function runReachShareCard() {
close(false);
clearExportReceipt();
var snapshot =
Archify.focus && typeof Archify.focus.reachabilitySnapshot === "function"
? Archify.focus.reachabilitySnapshot()
: null;
var blobPromise = snapshot
? renderShareCard({ reachSnapshot: snapshot })
: Promise.reject(exportError("viewer.export.reachRequired"));
return blobPromise
.then(function (blob) {
recordExportReceipt(
"share-card",
blob,
false,
{ width: SHARE_CARD_WIDTH, height: SHARE_CARD_HEIGHT },
"reach",
false,
true,
);
download(
blob,
diagramFilename() + "-" + snapshot.direction + "-reach-share-card.png",
);
toast(viewerText("viewer.export.downloadedReach"));
return blob;
})
.catch(function (err) {
console.error(err);
var technicalMessage = err && err.message ? err.message : "share-card";
var message = exportMessage(err);
document.documentElement.setAttribute("data-last-export-error-format", "share-card");
document.documentElement.setAttribute("data-last-export-error", technicalMessage);
alert(viewerText("viewer.export.reachFailed", { message: message }));
});
}
// A compact, machine-readable receipt for local QA and generated proof
// galleries. It exposes no diagram contents, only the completed format,
// byte count, and whether temporary viewer state was excluded.
function recordExportReceipt(
format,
blob,
canonical,
dimensions,
variant,
routeStateClean,
reachStateClean,
) {
document.documentElement.setAttribute("data-last-export-format", format);
document.documentElement.setAttribute("data-last-export-bytes", String(blob.size));
document.documentElement.setAttribute(
"data-last-export-canonical",
canonical ? "true" : "false",
);
if (variant) {
document.documentElement.setAttribute("data-last-export-variant", variant);
} else {
document.documentElement.removeAttribute("data-last-export-variant");
}
if (routeStateClean === true) {
document.documentElement.setAttribute("data-last-export-route-state-clean", "true");
} else {
document.documentElement.removeAttribute("data-last-export-route-state-clean");
}
if (reachStateClean === true) {
document.documentElement.setAttribute("data-last-export-reach-state-clean", "true");
} else {
document.documentElement.removeAttribute("data-last-export-reach-state-clean");
}
if (dimensions) {
document.documentElement.setAttribute(
"data-last-export-width",
String(dimensions.width),
);
document.documentElement.setAttribute(
"data-last-export-height",
String(dimensions.height),
);
} else {
document.documentElement.removeAttribute("data-last-export-width");
document.documentElement.removeAttribute("data-last-export-height");
}
}
function clearExportReceipt() {
document.documentElement.removeAttribute("data-last-export-format");
document.documentElement.removeAttribute("data-last-export-bytes");
document.documentElement.removeAttribute("data-last-export-canonical");
document.documentElement.removeAttribute("data-last-export-width");
document.documentElement.removeAttribute("data-last-export-height");
document.documentElement.removeAttribute("data-last-export-variant");
document.documentElement.removeAttribute("data-last-export-route-state-clean");
document.documentElement.removeAttribute("data-last-export-reach-state-clean");
document.documentElement.removeAttribute("data-last-export-error-format");
document.documentElement.removeAttribute("data-last-export-error");
}
function writePngToClipboard(blobPromise) {
// WebKit requires ClipboardItem to be constructed synchronously inside
// the user gesture. Chromium accepts the same pending Promise<Blob>;
// engines that reject promise values fall back to an awaited Blob.
try {
return navigator.clipboard.write([new ClipboardItem({ "image/png": blobPromise })]);
} catch (_) {
return blobPromise.then(function (blob) {
return navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
});
}
}
function runCopyShareCard() {
close(true);
clearExportReceipt();
if (!canCopyImage()) {
alert(viewerText("viewer.export.clipboardUnsupported.period"));
return;
}
var blobPromise = rasterizeShareCard();
return writePngToClipboard(blobPromise)
.then(function () {
return blobPromise.then(function (blob) {
recordExportReceipt("share-card", blob, true, {
width: SHARE_CARD_WIDTH,
height: SHARE_CARD_HEIGHT,
});
toast(viewerText("viewer.export.copiedShare"));
});
})
.catch(function (err) {
console.error(err);
var technicalMessage = err && err.message ? err.message : "share-card";
var message = exportMessage(err);
document.documentElement.setAttribute("data-last-export-error-format", "share-card");
document.documentElement.setAttribute("data-last-export-error", technicalMessage);
alert(viewerText("viewer.export.copyFailed", { message: message }));
});
}
function runCopy() {
close(true);
clearExportReceipt();
if (!canCopyImage()) {
alert(viewerText("viewer.export.clipboardUnsupported.short"));
return;
}
var blobPromise = rasterize("png");
return writePngToClipboard(blobPromise)
.then(function () {
toast(viewerText("viewer.export.copiedPng"));
})
.catch(function (err) {
console.error(err);
alert(
viewerText("viewer.export.copyFailed", {
message: exportMessage(err),
}),
);
});
}
menu.addEventListener("click", function (e) {
var routeShareCardBtn = e.target.closest('button[data-action="route-share-card"]');
if (routeShareCardBtn && !routeShareCardBtn.disabled && !routeShareCardBtn.hidden) {
runRouteShareCard();
return;
}
var reachShareCardBtn = e.target.closest('button[data-action="reach-share-card"]');
if (reachShareCardBtn && !reachShareCardBtn.disabled && !reachShareCardBtn.hidden) {
runReachShareCard();
return;
}
var copyShareCardBtn = e.target.closest('button[data-action="copy-share-card"]');
if (copyShareCardBtn && !copyShareCardBtn.disabled) {
runCopyShareCard();
return;
}
var copyBtn = e.target.closest('button[data-action="copy"]');
if (copyBtn && !copyBtn.disabled) {
runCopy();
return;
}
var formatBtn = e.target.closest("button[data-format]");
if (formatBtn && !formatBtn.disabled) {
runExport(formatBtn.dataset.format);
}
});
Archify.motion = { canRecord: canRecordMotion, recordWebm: recordWebm };
Archify.exportMenu = {
open: open,
close: close,
isOpen: isOpen,
run: runExport,
shareCard: rasterizeShareCard,
downloadRouteShareCard: runRouteShareCard,
downloadReachShareCard: runReachShareCard,
syncRouteShare: syncRouteShareItem,
syncReachShare: syncReachShareItem,
copyShareCard: runCopyShareCard,
};
// Auto-open on page load for demo/screenshot purposes: ?openExport=1
// Wait for fonts (so the menu doesn't flash before typography lands)
// and paint before opening. Fallback timeout for browsers without the
// Font Loading API.
try {
if (new URLSearchParams(window.location.search).get("openExport") === "1") {
var openWhenReady = function () {
requestAnimationFrame(function () {
requestAnimationFrame(open);
});
};
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(openWhenReady);
} else {
setTimeout(openWhenReady, 200);
}
}
} catch (_) {}
})();
/* ============================================================
Motion Governor — one reader switch and one motion budget. Ambient
trace yields to the strongest semantic owner; Still also parks bounded
viewer signals without discarding their static meaning.
============================================================ */
Archify.motionGovernor = (function () {
var STORAGE_KEY = "archify-motion";
var html = document.documentElement;
var svg = document.querySelector(".diagram-container svg");
var btn = document.getElementById("btn-motion");
var label = document.getElementById("motion-label");
var motionQuery = window.matchMedia
? window.matchMedia("(prefers-reduced-motion: reduce)")
: null;
var capable = !!(svg && svg.getAttribute("data-animation") === "trace");
var readerPaused = false;
var suspensions = Object.create(null);
var explicitOwner = "";
var owner = "";
var ownerToken = 0;
var ownerCleanup = null;
var lastEffectivePaused = null;
var ambientStarted = false;
var ambientPending = new Set();
function detachAmbientBoundary() {
if (!svg) return;
svg.removeEventListener("animationend", onAmbientBoundary, true);
svg.removeEventListener("animationcancel", onAmbientBoundary, true);
}
function settleAmbient(reason) {
if (!capable) return false;
ambientStarted = true;
ambientPending.clear();
detachAmbientBoundary();
html.setAttribute("data-ambient-motion", "settled");
html.setAttribute("data-ambient-settle-reason", reason || "complete");
return true;
}
function onAmbientBoundary(event) {
if (!ambientPending.has(event.target)) return;
ambientPending.delete(event.target);
if (!ambientPending.size) settleAmbient("complete");
}
function startAmbient() {
if (ambientStarted || !capable) return false;
ambientStarted = true;
ambientPending = new Set(
Array.prototype.slice.call(
svg.querySelectorAll('[data-animate="edge"], [data-animate="node"]'),
),
);
if (!ambientPending.size) return settleAmbient("empty");
svg.addEventListener("animationend", onAmbientBoundary, true);
svg.addEventListener("animationcancel", onAmbientBoundary, true);
html.setAttribute("data-ambient-motion", "running");
html.removeAttribute("data-ambient-settle-reason");
return true;
}
function readStored() {
try {
return localStorage.getItem(STORAGE_KEY);
} catch (_) {
return null;
}
}
function writeStored() {
try {
if (readerPaused) localStorage.setItem(STORAGE_KEY, "still");
else localStorage.removeItem(STORAGE_KEY);
} catch (_) {}
}
function reducedMotion() {
return !!(motionQuery && motionQuery.matches);
}
function hasSuspension() {
return Object.keys(suspensions).length > 0;
}
function effectivePaused() {
return readerPaused || reducedMotion() || hasSuspension();
}
function ownerLabel(value) {
if (value === "story") return viewerText("viewer.owner.story");
if (value === "chapter") return viewerText("viewer.owner.chapter");
if (value === "chapter-preview") return viewerText("viewer.owner.chapterPreview");
if (value === "handoff") return viewerText("viewer.owner.handoff");
if (value === "route") return viewerText("viewer.owner.route");
if (value === "lens") return viewerText("viewer.owner.lens");
if (value === "relationship") return viewerText("viewer.owner.relationship");
if (value === "intent") return viewerText("viewer.owner.intent");
if (value === "focus") return viewerText("viewer.owner.focus");
if (value === "legend") return viewerText("viewer.owner.legend");
return viewerText("viewer.owner.reader");
}
function render() {
if (!capable) return;
var systemPaused = reducedMotion();
var paused = effectivePaused();
html.setAttribute("data-motion", paused ? "still" : "live");
if (
paused ||
owner ||
html.hasAttribute("data-embed") ||
html.hasAttribute("data-share-playback") ||
html.hasAttribute("data-document-hidden")
) {
settleAmbient("suppressed");
} else if (!ambientStarted) {
startAmbient();
}
btn.setAttribute("aria-pressed", paused ? "false" : "true");
btn.disabled = systemPaused;
label.textContent = viewerText(paused ? "viewer.motion.still" : "viewer.motion.live");
if (
paused &&
lastEffectivePaused !== true &&
Archify.guidedViews &&
Archify.guidedViews.isPlaying()
) {
Archify.guidedViews.pause();
}
if (
paused &&
lastEffectivePaused !== true &&
Archify.guidedViews &&
Archify.guidedViews.settleHandoff
) {
Archify.guidedViews.settleHandoff(
systemPaused ? "reduced-motion" : hasSuspension() ? "hidden" : "still",
);
}
if (
paused &&
lastEffectivePaused !== true &&
Archify.routeProbe &&
Archify.routeProbe.isJourneyPlaying &&
Archify.routeProbe.isJourneyPlaying()
) {
Archify.routeProbe.pauseJourney({
preserveElapsed: true,
reason: systemPaused ? "reduced-motion" : hasSuspension() ? "hidden" : "still",
});
}
lastEffectivePaused = paused;
if (Archify.routeProbe && typeof Archify.routeProbe.syncMotion === "function")
Archify.routeProbe.syncMotion();
if (systemPaused) {
btn.setAttribute("aria-label", viewerText("viewer.motion.reduced"));
btn.title = viewerText("viewer.motion.reduced");
} else if (hasSuspension()) {
btn.setAttribute("aria-label", viewerText("viewer.motion.hidden"));
btn.title = viewerText("viewer.motion.hidden");
} else if (paused) {
btn.setAttribute("aria-label", viewerText("viewer.motion.resume"));
btn.title = viewerText("viewer.motion.resume");
} else if (owner) {
btn.setAttribute(
"aria-label",
viewerText("viewer.motion.yielding", { owner: ownerLabel(owner) }),
);
btn.title = viewerText("viewer.motion.yielding.title", { owner: ownerLabel(owner) });
} else {
btn.setAttribute("aria-label", viewerText("viewer.motion.pause"));
btn.title = viewerText("viewer.motion.pause");
}
}
function setPaused(next, options) {
options = options || {};
readerPaused = !!next;
if (options.persist !== false) writeStored();
render();
return readerPaused;
}
function publishOwner() {
owner = explicitOwner || deriveOwner();
if (owner) html.setAttribute("data-motion-owner", owner);
else html.removeAttribute("data-motion-owner");
render();
return owner;
}
function deriveOwner() {
if (!svg) return "";
if (svg.hasAttribute("data-story-playing") || svg.hasAttribute("data-story-follow"))
return "story";
if (svg.hasAttribute("data-story-active")) return "chapter";
if (svg.hasAttribute("data-route-picking") || svg.hasAttribute("data-route-active"))
return "route";
if (svg.hasAttribute("data-lens-active")) return "lens";
if (svg.hasAttribute("data-relationship-preview-active")) return "relationship";
if (svg.hasAttribute("data-intent-trace-active")) return "intent";
if (svg.hasAttribute("data-focus-active")) return "focus";
if (svg.hasAttribute("data-legend-preview-active")) return "legend";
return "";
}
function clearClaim(preempted) {
var cleanup = ownerCleanup;
ownerCleanup = null;
explicitOwner = "";
if (preempted && cleanup) {
try {
cleanup();
} catch (_) {}
}
}
function claim(next, cleanup) {
if (!capable || !next) return 0;
clearClaim(true);
ownerToken += 1;
explicitOwner = next;
ownerCleanup = typeof cleanup === "function" ? cleanup : null;
publishOwner();
return ownerToken;
}
function release(token) {
if (!capable || token !== ownerToken || !explicitOwner) return false;
clearClaim(false);
ownerToken += 1;
publishOwner();
return true;
}
function suspend(reason) {
var key = String(reason || "runtime");
var active = true;
suspensions[key] = (suspensions[key] || 0) + 1;
render();
return function () {
if (!active) return false;
active = false;
if (suspensions[key] > 1) suspensions[key] -= 1;
else delete suspensions[key];
render();
return true;
};
}
function syncVisibility() {
if (document.hidden) {
suspensions.visibility = true;
html.setAttribute("data-document-hidden", "true");
} else {
delete suspensions.visibility;
html.removeAttribute("data-document-hidden");
}
render();
}
if (!capable) {
btn.hidden = true;
html.removeAttribute("data-motion-capable");
html.removeAttribute("data-motion");
html.removeAttribute("data-motion-owner");
html.removeAttribute("data-ambient-motion");
html.removeAttribute("data-ambient-settle-reason");
return {
capable: false,
pause: function () {
return false;
},
resume: function () {
return false;
},
toggle: function () {
return false;
},
setMode: function () {
return "still";
},
mode: function () {
return "still";
},
claim: function () {
return 0;
},
release: function () {
return false;
},
suspend: function () {
return function () {
return false;
};
},
isPaused: function () {
return true;
},
owner: function () {
return "";
},
};
}
html.setAttribute("data-motion-capable", "true");
btn.hidden = false;
readerPaused = readStored() === "still";
btn.addEventListener("click", function () {
setPaused(!readerPaused);
});
if (motionQuery) {
if (typeof motionQuery.addEventListener === "function")
motionQuery.addEventListener("change", render);
else if (typeof motionQuery.addListener === "function") motionQuery.addListener(render);
}
document.addEventListener("visibilitychange", syncVisibility);
if (
document.documentElement.getAttribute("data-embed") !== "true" &&
typeof MutationObserver !== "undefined" &&
typeof Node !== "undefined" &&
svg instanceof Node
) {
var ownerObserver = new MutationObserver(function () {
publishOwner();
});
ownerObserver.observe(svg, {
attributes: true,
attributeFilter: [
"data-story-playing",
"data-story-follow",
"data-story-active",
"data-route-picking",
"data-route-active",
"data-lens-active",
"data-relationship-preview-active",
"data-intent-trace-active",
"data-focus-active",
"data-legend-preview-active",
],
});
}
syncVisibility();
publishOwner();
render();
return {
capable: true,
pause: function () {
return setPaused(true);
},
resume: function () {
return setPaused(false);
},
toggle: function () {
return setPaused(!readerPaused);
},
setMode: function (next, options) {
setPaused(next === "still", options);
return effectivePaused() ? "still" : "live";
},
mode: function () {
return effectivePaused() ? "still" : "live";
},
claim: claim,
release: release,
suspend: suspend,
isPaused: effectivePaused,
owner: function () {
return owner;
},
};
})();
/* Verified repository evidence is emitted only after the renderer checks
the authored GitHub revision, source blobs, and optional line ranges
against the explicit --repo-root. It remains outside the SVG so every
canonical visual export stays free of repository paths. */
Archify.sourceEvidence = (function () {
var element = document.getElementById("archify-source-evidence-data");
var payload = null;
var svgNamespace = "http://www.w3.org/2000/svg";
if (element) {
try {
var parsed = JSON.parse(element.textContent || "null");
if (parsed && parsed.verified === true && parsed.repository && parsed.nodes)
payload = parsed;
} catch (_) {}
}
function installBeacons() {
if (!payload) return 0;
var svg = document.querySelector(".diagram-container svg");
if (!svg) return 0;
var installed = 0;
Array.prototype.forEach.call(svg.querySelectorAll("[data-node-id]"), function (node) {
var id = node.getAttribute("data-node-id");
var sources = payload.nodes[id];
if (
!Array.isArray(sources) ||
!sources.length ||
node.querySelector("[data-source-evidence-beacon]")
)
return;
var shape =
node.querySelector('[data-animate="node"]') || node.querySelector('[class*="c-"]');
var box;
try {
box = (shape || node).getBBox();
} catch (_) {
return;
}
if (
!box ||
!Number.isFinite(box.x) ||
!Number.isFinite(box.y) ||
!Number.isFinite(box.width) ||
box.width < 36
)
return;
var count = sources.length;
var label = viewerCount("viewer.passport.beacon", count);
var beacon = document.createElementNS(svgNamespace, "g");
beacon.classList.add("source-evidence-beacon");
beacon.setAttribute("data-source-evidence-beacon", "");
beacon.setAttribute("aria-hidden", "true");
var brandOffset = node.hasAttribute("data-node-brand") ? 24 : 0;
beacon.setAttribute(
"transform",
"translate(" + (box.x + box.width - 35 - brandOffset) + " " + (box.y + 5) + ")",
);
var title = document.createElementNS(svgNamespace, "title");
title.textContent = label;
var plate = document.createElementNS(svgNamespace, "rect");
plate.setAttribute("width", "30");
plate.setAttribute("height", "12");
plate.setAttribute("rx", "6");
var text = document.createElementNS(svgNamespace, "text");
text.setAttribute("x", "15");
text.setAttribute("y", "8.25");
text.textContent = viewerText("viewer.passport.sourceMarker") + " " + count;
beacon.appendChild(title);
beacon.appendChild(plate);
beacon.appendChild(text);
node.appendChild(beacon);
var originalLabel = node.getAttribute("aria-label") || "";
node.setAttribute("data-source-evidence-count", String(count));
node.setAttribute("data-source-evidence-original-label", originalLabel);
node.setAttribute(
"aria-label",
(originalLabel ? originalLabel + ", " : "") +
viewerCount("viewer.passport.sourceCount", count),
);
installed += 1;
});
return installed;
}
return {
available: function () {
return Boolean(payload);
},
repository: function () {
return payload ? payload.repository : null;
},
node: function (id) {
var sources = payload && payload.nodes ? payload.nodes[id] : null;
return Array.isArray(sources) ? sources.slice() : [];
},
installBeacons: installBeacons,
};
})();
Archify.sourceEvidence.installBeacons();
/* ============================================================
Explore — stable-ID neighborhood focus + dependency-free pan/zoom.
Renderer IDs become deep-linkable semantic hooks without turning the
standalone artifact into a canvas editor.
============================================================ */
Archify.focus = (function () {
var html = document.documentElement;
var container = document.querySelector(".diagram-container");
var svg = container.querySelector("svg");
var chip = document.getElementById("focus-chip");
var label = document.getElementById("focus-label");
var detail = document.getElementById("focus-detail");
var kind = document.getElementById("focus-kind");
var context = document.getElementById("focus-context");
var tag = document.getElementById("focus-tag");
var semanticId = document.getElementById("focus-id");
var evidence = document.getElementById("focus-evidence");
var repositoryLink = document.getElementById("focus-repository");
var evidenceLinks = document.getElementById("focus-evidence-links");
var summary = document.getElementById("focus-summary");
var reachSection = document.getElementById("focus-reach");
var reachStatus = document.getElementById("focus-reach-status");
var upstreamBtn = document.getElementById("btn-reach-upstream");
var downstreamBtn = document.getElementById("btn-reach-downstream");
var upstreamCount = document.getElementById("focus-reach-upstream-count");
var downstreamCount = document.getElementById("focus-reach-downstream-count");
var relationshipList = document.getElementById("relationship-lens-list");
var copyBtn = document.getElementById("btn-focus-copy");
var relationsBtn = document.getElementById("btn-focus-relations");
var clearBtn = document.getElementById("btn-focus-clear");
var activeIds = [];
var hoveredRelationship = null;
var focusedRelationship = null;
var pinnedRelationship = null;
var pinnedRelationshipKey = null;
var activeRelationshipPreview = null;
var relationshipHitOverlay = null;
var relationshipHitTargets = [];
var directPreviewTimer = null;
var reachabilityMode = null;
var activeReachability = null;
var svgNamespace = "http://www.w3.org/2000/svg";
var reducedMotionQuery = window.matchMedia
? window.matchMedia("(prefers-reduced-motion: reduce)")
: null;
var finePointerQuery = window.matchMedia
? window.matchMedia("(hover: hover) and (pointer: fine)")
: null;
function nodes() {
return Array.prototype.slice.call(svg.querySelectorAll("[data-node-id]"));
}
function edges() {
return Array.prototype.slice.call(svg.querySelectorAll("[data-edge-from][data-edge-to]"));
}
function nodeLabel(node, fallback) {
return (
node.getAttribute("data-node-label") ||
(node.getAttribute("aria-label") || fallback).replace(/^Focus\s+/, "")
);
}
function reachabilityRelationships() {
var seen = Object.create(null);
var relationships = [];
edges().forEach(function (edge) {
var from = edge.getAttribute("data-edge-from");
var to = edge.getAttribute("data-edge-to");
var key =
edge.getAttribute("data-edge-key") ||
from + "\u0000" + to + "\u0000" + (edge.getAttribute("data-edge-label") || "");
if (!from || !to || seen[key]) return;
seen[key] = true;
relationships.push({ key: key, from: from, to: to });
});
return relationships;
}
function computeReachability(originId, direction, relationships) {
if (
typeof originId !== "string" ||
!originId ||
(direction !== "upstream" && direction !== "downstream") ||
!Array.isArray(relationships)
)
return null;
var records = [];
var seenKeys = Object.create(null);
relationships.forEach(function (relationship, index) {
if (
!relationship ||
typeof relationship.from !== "string" ||
!relationship.from ||
typeof relationship.to !== "string" ||
!relationship.to
)
return;
var key =
typeof relationship.key === "string" && relationship.key
? relationship.key
: String(index);
if (seenKeys[key]) return;
seenKeys[key] = true;
records.push({ key: key, from: relationship.from, to: relationship.to });
});
var depths = Object.create(null);
var order = [];
var queue = [originId];
depths[originId] = 0;
for (var cursor = 0; cursor < queue.length; cursor += 1) {
var current = queue[cursor];
records.forEach(function (relationship) {
var next = null;
if (direction === "downstream" && relationship.from === current)
next = relationship.to;
else if (direction === "upstream" && relationship.to === current)
next = relationship.from;
if (next == null || Object.prototype.hasOwnProperty.call(depths, next)) return;
depths[next] = depths[current] + 1;
queue.push(next);
});
}
queue.forEach(function (id) {
order.push(id);
});
var edgeKeys = [];
records.forEach(function (relationship) {
if (
Object.prototype.hasOwnProperty.call(depths, relationship.from) &&
Object.prototype.hasOwnProperty.call(depths, relationship.to)
)
edgeKeys.push(relationship.key);
});
var maxDepth = order.reduce(function (maximum, id) {
return Math.max(maximum, depths[id]);
}, 0);
return {
direction: direction,
originId: originId,
nodeIds: order,
edgeKeys: edgeKeys,
depths: depths,
maxDepth: maxDepth,
};
}
function reachabilityFor(id, direction) {
return computeReachability(id, direction, reachabilityRelationships());
}
function resetReachabilityButtons() {
upstreamBtn.setAttribute("aria-pressed", "false");
downstreamBtn.setAttribute("aria-pressed", "false");
}
function clearReachability(options) {
options = options || {};
reachabilityMode = null;
activeReachability = null;
svg.removeAttribute("data-reach-active");
chip.removeAttribute("data-reach-mode");
nodes().forEach(function (node) {
node.removeAttribute("data-reach-match");
node.removeAttribute("data-reach-origin");
node.removeAttribute("data-reach-depth");
});
edges().forEach(function (edge) {
edge.removeAttribute("data-reach-match");
edge.removeAttribute("data-reach-depth");
});
resetReachabilityButtons();
reachStatus.textContent = "";
reachStatus.hidden = true;
if (Archify.exportMenu && typeof Archify.exportMenu.syncReachShare === "function") {
Archify.exportMenu.syncReachShare();
}
if (options.updateUrl === true && activeIds.length === 1) {
try {
history.replaceState(
null,
"",
location.pathname + location.search + "#focus=" + encodeURIComponent(activeIds[0]),
);
} catch (_) {}
}
}
function renderReachabilityControls(id) {
var upstream = reachabilityFor(id, "upstream");
var downstream = reachabilityFor(id, "downstream");
var upstreamReach = upstream ? Math.max(0, upstream.nodeIds.length - 1) : 0;
var downstreamReach = downstream ? Math.max(0, downstream.nodeIds.length - 1) : 0;
upstreamCount.textContent = String(upstreamReach);
downstreamCount.textContent = String(downstreamReach);
upstreamBtn.disabled = upstreamReach === 0;
downstreamBtn.disabled = downstreamReach === 0;
upstreamBtn.setAttribute(
"aria-label",
upstreamReach
? viewerCount("viewer.passport.reach.upstream", upstreamReach)
: viewerText("viewer.passport.reach.noUpstream"),
);
downstreamBtn.setAttribute(
"aria-label",
downstreamReach
? viewerCount("viewer.passport.reach.downstream", downstreamReach)
: viewerText("viewer.passport.reach.noDownstream"),
);
reachSection.hidden = false;
}
function applyReachability(direction, options) {
options = options || {};
if (activeIds.length !== 1 || (direction !== "upstream" && direction !== "downstream"))
return false;
if (reachabilityMode === direction && options.toggle !== false) {
clearReachability({ updateUrl: options.updateUrl !== false });
return true;
}
var result = reachabilityFor(activeIds[0], direction);
if (!result || result.nodeIds.length <= 1) return false;
if (Archify.guidedViews && typeof Archify.guidedViews.showAll === "function") {
Archify.guidedViews.showAll({ clearFocus: false, updateUrl: false, resetView: false });
}
clearRelationshipPreview({ clearPin: true });
clearReachability({ updateUrl: false });
reachabilityMode = direction;
activeReachability = result;
var edgeKeySet = Object.create(null);
result.edgeKeys.forEach(function (key) {
edgeKeySet[key] = true;
});
svg.setAttribute("data-reach-active", direction);
chip.setAttribute("data-reach-mode", direction);
nodes().forEach(function (node) {
var id = node.getAttribute("data-node-id");
if (!Object.prototype.hasOwnProperty.call(result.depths, id)) return;
node.setAttribute("data-reach-match", "");
node.setAttribute("data-reach-depth", String(result.depths[id]));
if (id === result.originId) node.setAttribute("data-reach-origin", "");
});
edges().forEach(function (edge) {
var key =
edge.getAttribute("data-edge-key") ||
edge.getAttribute("data-edge-from") +
"\u0000" +
edge.getAttribute("data-edge-to") +
"\u0000" +
(edge.getAttribute("data-edge-label") || "");
if (!edgeKeySet[key]) return;
edge.setAttribute("data-reach-match", "");
var fromDepth = result.depths[edge.getAttribute("data-edge-from")];
var toDepth = result.depths[edge.getAttribute("data-edge-to")];
edge.setAttribute("data-reach-depth", String(Math.max(fromDepth || 0, toDepth || 0)));
});
resetReachabilityButtons();
var activeButton = direction === "upstream" ? upstreamBtn : downstreamBtn;
activeButton.setAttribute("aria-pressed", "true");
var reachableCount = result.nodeIds.length - 1;
var directionLabel = viewerText(
direction === "upstream" ? "viewer.passport.upstream" : "viewer.passport.downstream",
);
reachStatus.textContent = viewerText("viewer.passport.reach.status", {
direction: directionLabel,
nodes: reachableCount,
links: result.edgeKeys.length,
hops: result.maxDepth,
});
reachStatus.hidden = false;
if (Archify.exportMenu && typeof Archify.exportMenu.syncReachShare === "function") {
Archify.exportMenu.syncReachShare();
}
if (options.updateUrl !== false) {
try {
history.replaceState(
null,
"",
location.pathname +
location.search +
"#focus=" +
encodeURIComponent(activeIds[0]) +
"&reach=" +
direction,
);
} catch (_) {}
}
if (
options.reveal !== false &&
Archify.view &&
typeof Archify.view.reveal === "function"
) {
Archify.view.reveal(result.nodeIds, {
includeNeighbors: false,
reason: "reachability",
});
}
requestLensPlacement();
return true;
}
function reachabilitySnapshot() {
if (
!activeReachability ||
activeIds.length !== 1 ||
(reachabilityMode !== "upstream" && reachabilityMode !== "downstream") ||
activeReachability.direction !== reachabilityMode ||
activeReachability.originId !== activeIds[0] ||
svg.getAttribute("data-reach-active") !== reachabilityMode
)
return null;
var originId = activeReachability.originId;
var nodeIds = activeReachability.nodeIds.slice();
var edgeKeys = activeReachability.edgeKeys.slice();
if (
nodeIds.length < 2 ||
nodeIds[0] !== originId ||
!edgeKeys.length ||
!activeReachability.depths ||
!Number.isInteger(activeReachability.maxDepth) ||
activeReachability.maxDepth < 1
)
return null;
var allNodes = nodes();
var allEdges = edges();
var seenNodeIds = Object.create(null);
var seenEdgeKeys = Object.create(null);
var nodeIdSet = Object.create(null);
var depths = Object.create(null);
var originNode = null;
var measuredMaxDepth = 0;
if (
nodeIds.some(function (id) {
var depth = activeReachability.depths[id];
var matches = allNodes.filter(function (node) {
return node.getAttribute("data-node-id") === id;
});
if (
typeof id !== "string" ||
!id ||
seenNodeIds[id] ||
matches.length !== 1 ||
!matches[0].hasAttribute("data-reach-match") ||
!Number.isInteger(depth) ||
depth < 0 ||
depth > activeReachability.maxDepth ||
(id === originId
? !matches[0].hasAttribute("data-reach-origin") || depth !== 0
: depth < 1)
)
return true;
seenNodeIds[id] = true;
nodeIdSet[id] = true;
depths[id] = depth;
measuredMaxDepth = Math.max(measuredMaxDepth, depth);
if (id === originId) originNode = matches[0];
return false;
}) ||
!originNode ||
measuredMaxDepth !== activeReachability.maxDepth ||
allNodes.filter(function (node) {
return node.hasAttribute("data-reach-match");
}).length !== nodeIds.length
)
return null;
var edgeRecords = [];
if (
edgeKeys.some(function (key) {
if (typeof key !== "string" || !key || seenEdgeKeys[key]) return true;
var fragments = allEdges.filter(function (edge) {
return edge.getAttribute("data-edge-key") === key;
});
var drawableFragments = fragments.filter(hasDrawableGeometry);
if (
!fragments.length ||
drawableFragments.length !== 1 ||
!fragments.every(function (fragment) {
return fragment.hasAttribute("data-reach-match");
})
)
return true;
var first = fragments[0];
var from = first.getAttribute("data-edge-from");
var to = first.getAttribute("data-edge-to");
var id = first.getAttribute("data-edge-id") || "";
var labelValue = first.getAttribute("data-edge-label") || "";
if (
!nodeIdSet[from] ||
!nodeIdSet[to] ||
!fragments.every(function (fragment) {
return (
fragment.getAttribute("data-edge-from") === from &&
fragment.getAttribute("data-edge-to") === to &&
(fragment.getAttribute("data-edge-id") || "") === id
);
})
)
return true;
seenEdgeKeys[key] = true;
edgeRecords.push({
key: key,
id: id,
from: from,
to: to,
label: labelValue,
depth: Math.max(depths[from], depths[to]),
});
return false;
})
)
return null;
var liveEdgeKeys = Object.create(null);
if (
allEdges.some(function (edge) {
if (!edge.hasAttribute("data-reach-match")) return false;
var key = edge.getAttribute("data-edge-key");
if (!key || !seenEdgeKeys[key]) return true;
liveEdgeKeys[key] = true;
return false;
}) ||
Object.keys(liveEdgeKeys).length !== edgeKeys.length
)
return null;
return {
direction: reachabilityMode,
origin: { id: originId, label: nodeLabel(originNode, originId) },
nodeIds: nodeIds,
depths: depths,
maxDepth: activeReachability.maxDepth,
edges: edgeRecords,
};
}
function setPassportValue(element, value) {
var normalized = value == null ? "" : String(value).trim();
element.textContent = normalized;
element.hidden = !normalized;
}
function renderSourceEvidence(id) {
evidenceLinks.textContent = "";
repositoryLink.removeAttribute("href");
repositoryLink.textContent = "";
var sources = Archify.sourceEvidence.node(id);
var repository = Archify.sourceEvidence.repository();
if (!repository || !sources.length) {
evidence.hidden = true;
return;
}
var slug = repository.url.replace(/^https:\/\/github\.com\//, "").replace(/\/$/, "");
repositoryLink.href = repository.url + "/tree/" + repository.revision;
repositoryLink.textContent = slug + " @ " + repository.shortRevision;
repositoryLink.setAttribute(
"aria-label",
viewerText("viewer.passport.repository.open", { revision: repository.revision }),
);
sources.forEach(function (source) {
var link = document.createElement("a");
link.className = "semantic-passport-source";
link.href = source.href;
link.target = "_blank";
link.rel = "noopener noreferrer";
link.referrerPolicy = "no-referrer";
link.setAttribute(
"aria-label",
viewerText("viewer.passport.source.open", {
path: source.path,
revision: repository.shortRevision,
}),
);
var name = document.createElement("strong");
name.textContent = source.label || source.path.split("/").pop() || source.path;
var location = document.createElement("code");
location.textContent = source.line
? "L" +
source.line +
(source.endLine && source.endLine !== source.line ? "–" + source.endLine : "") +
" ↗"
: viewerText("viewer.passport.source.openLink");
var sourcePath = document.createElement("small");
sourcePath.textContent = source.path;
link.appendChild(name);
link.appendChild(location);
link.appendChild(sourcePath);
evidenceLinks.appendChild(link);
});
evidence.hidden = false;
}
function renderPassport(id, node) {
setPassportValue(detail, node.getAttribute("data-node-sublabel"));
setPassportValue(kind, viewerKindLabel(node.getAttribute("data-node-kind") || "node"));
setPassportValue(context, node.getAttribute("data-node-context"));
setPassportValue(tag, node.getAttribute("data-node-tag"));
setPassportValue(
document.getElementById("focus-brand"),
node.getAttribute("data-node-brand"),
);
semanticId.textContent = id;
semanticId.hidden = false;
renderSourceEvidence(id);
}
function relationshipsFor(id, byId) {
var seen = {};
var relationships = [];
edges().forEach(function (edge) {
var from = edge.getAttribute("data-edge-from");
var to = edge.getAttribute("data-edge-to");
if (from !== id && to !== id) return;
var edgeLabel = edge.getAttribute("data-edge-label") || "";
var edgeKey =
edge.getAttribute("data-edge-key") || from + "\u0000" + to + "\u0000" + edgeLabel;
var edgeId = edge.getAttribute("data-edge-id") || "";
if (seen[edgeKey]) return;
seen[edgeKey] = true;
var direction = from === id && to === id ? "loop" : from === id ? "out" : "in";
var neighborId = direction === "in" ? from : to;
var neighbor = byId[neighborId];
relationships.push({
key: edgeKey,
id: edgeId,
from: from,
to: to,
direction: direction,
neighborId: neighborId,
neighborLabel: neighbor ? nodeLabel(neighbor, neighborId) : neighborId,
label:
edgeLabel ||
viewerText(
direction === "loop"
? "viewer.passport.relationship.loopsBack"
: direction === "out"
? "viewer.passport.relationship.connectsTo"
: "viewer.passport.relationship.connectsFrom",
),
});
});
return relationships;
}
function relationshipEdgeShapes(edge) {
if (!edge) return [];
if (/^(path|line|polyline)$/i.test(edge.tagName || "")) return [edge];
return Array.prototype.slice.call(edge.querySelectorAll("path, line, polyline"));
}
function relationshipHitRecords() {
var recordsByKey = {};
var recordsById = {};
var byId = {};
nodes().forEach(function (node) {
byId[node.getAttribute("data-node-id")] = node;
});
var records = [];
edges().forEach(function (edge) {
var shapes = relationshipEdgeShapes(edge);
if (!shapes.length) return;
var from = edge.getAttribute("data-edge-from");
var to = edge.getAttribute("data-edge-to");
var labelValue = edge.getAttribute("data-edge-label") || "";
var edgeId = edge.getAttribute("data-edge-id") || "";
var key =
edge.getAttribute("data-edge-key") || from + "\u0000" + to + "\u0000" + labelValue;
var existing = recordsByKey[key];
if (existing) {
if (
existing.from !== from ||
existing.to !== to ||
existing.labelValue !== labelValue ||
existing.id !== edgeId
) {
existing.invalid = true;
return;
}
shapes.forEach(function (shape) {
if (existing.shapes.indexOf(shape) === -1) existing.shapes.push(shape);
});
return;
}
var record = {
key: key,
id: edgeId,
from: from,
to: to,
fromLabel: byId[from] ? nodeLabel(byId[from], from) : from,
toLabel: byId[to] ? nodeLabel(byId[to], to) : to,
labelValue: labelValue,
label:
labelValue ||
viewerText(
from === to
? "viewer.passport.relationship.loopsBack"
: "viewer.passport.relationship.connectsTo",
),
edge: edge,
shapes: shapes,
invalid: false,
};
recordsByKey[key] = record;
if (edgeId) {
if (recordsById[edgeId]) {
recordsById[edgeId].invalid = true;
record.invalid = true;
} else {
recordsById[edgeId] = record;
}
}
records.push(record);
});
return records.filter(function (record) {
return !record.invalid && record.from && record.to;
});
}
function relationshipRecordForKey(key) {
return (
relationshipHitRecords().find(function (record) {
return record.key === key;
}) || null
);
}
function pinnedRelationshipRecord() {
return pinnedRelationshipKey ? relationshipRecordForKey(pinnedRelationshipKey) : null;
}
function renderRelationshipCopyAction() {
var record = pinnedRelationshipRecord();
if (record && record.id) {
copyBtn.textContent = viewerText("viewer.passport.copyRelation");
copyBtn.setAttribute("aria-label", viewerText("viewer.passport.copyPinned"));
} else if (pinnedRelationshipKey) {
copyBtn.textContent = viewerText("viewer.passport.copyNode");
copyBtn.setAttribute("aria-label", viewerText("viewer.passport.copySource"));
} else {
copyBtn.textContent = viewerText("viewer.passport.copy");
copyBtn.setAttribute("aria-label", viewerText("viewer.passport.copy.focus"));
}
}
function relationshipHitGeometry(shape, className) {
var clone = shape.cloneNode(false);
clone.removeAttribute("id");
clone.removeAttribute("class");
clone.removeAttribute("style");
clone.removeAttribute("filter");
clone.removeAttribute("marker-start");
clone.removeAttribute("marker-mid");
clone.removeAttribute("marker-end");
clone.removeAttribute("role");
clone.removeAttribute("tabindex");
clone.removeAttribute("aria-label");
clone.removeAttribute("aria-labelledby");
clone.removeAttribute("aria-hidden");
clone.removeAttribute("data-animate");
clone.removeAttribute("data-edge-from");
clone.removeAttribute("data-edge-to");
clone.removeAttribute("data-edge-key");
clone.removeAttribute("data-edge-id");
clone.removeAttribute("data-edge-label");
clone.setAttribute("class", className || "relationship-hit-rail");
return clone;
}
function removeRelationshipPulse() {
Array.prototype.forEach.call(
svg.querySelectorAll("[data-relationship-pulse-overlay]"),
function (element) {
element.remove();
},
);
}
function relationshipPulseGeometry(shape) {
var clone = shape.cloneNode(false);
clone.removeAttribute("id");
clone.removeAttribute("class");
clone.removeAttribute("style");
clone.removeAttribute("transform");
clone.removeAttribute("filter");
clone.removeAttribute("marker-start");
clone.removeAttribute("marker-mid");
clone.removeAttribute("marker-end");
clone.removeAttribute("role");
clone.removeAttribute("tabindex");
clone.removeAttribute("aria-label");
clone.removeAttribute("aria-hidden");
clone.removeAttribute("data-animate");
clone.removeAttribute("data-edge-from");
clone.removeAttribute("data-edge-to");
clone.removeAttribute("data-edge-key");
clone.removeAttribute("data-edge-id");
clone.removeAttribute("data-edge-label");
clone.removeAttribute("data-focus-match");
clone.removeAttribute("data-relationship-preview");
clone.setAttribute("class", "relationship-flow-pulse");
clone.setAttribute("pathLength", "1");
return clone;
}
function relationshipTokenKind(edge) {
var shapes = relationshipEdgeShapes(edge);
var classEvidence = [edge.getAttribute("class") || ""]
.concat(
shapes.map(function (shape) {
return shape.getAttribute("class") || "";
}),
)
.join(" ");
var from = edge.getAttribute("data-edge-from") || "";
var to = edge.getAttribute("data-edge-to") || "";
var source = nodes().find(function (node) {
return node.getAttribute("data-node-id") === from;
});
var target = nodes().find(function (node) {
return node.getAttribute("data-node-id") === to;
});
var sourceKind = source ? source.getAttribute("data-node-kind") || "neutral" : "neutral";
var targetKind = target ? target.getAttribute("data-node-kind") || "neutral" : "neutral";
if (
/\ba-security\b/.test(classEvidence) ||
sourceKind === "security" ||
targetKind === "security" ||
targetKind === "failure"
)
return "security";
if (
/\ba-dashed\b/.test(classEvidence) ||
sourceKind === "messagebus" ||
targetKind === "messagebus"
)
return "event";
if (sourceKind === "database" || targetKind === "database") return "data";
if (targetKind === "waiting" || targetKind === "success") return "state";
return "call";
}
function relationshipTokenPath(shape) {
if (!shape) return "";
var tagName = String(shape.tagName || "").toLowerCase();
if (tagName === "path") return shape.getAttribute("d") || "";
if (tagName === "line") {
return (
"M " +
shape.getAttribute("x1") +
" " +
shape.getAttribute("y1") +
" L " +
shape.getAttribute("x2") +
" " +
shape.getAttribute("y2")
);
}
if (tagName === "polyline" && shape.points && shape.points.numberOfItems > 1) {
var commands = [];
for (var i = 0; i < shape.points.numberOfItems; i += 1) {
var point = shape.points.getItem(i);
commands.push((i === 0 ? "M " : "L ") + point.x + " " + point.y);
}
return commands.join(" ");
}
return "";
}
function relationshipTokenPart(tagName, className, attrs) {
var part = document.createElementNS(svgNamespace, tagName);
part.setAttribute("class", className);
Object.keys(attrs || {}).forEach(function (name) {
part.setAttribute(name, attrs[name]);
});
return part;
}
function relationshipTokenGeometry(shape, kind, key, options) {
options = options || {};
var pathData = relationshipTokenPath(shape);
if (!pathData) return null;
var token = document.createElementNS(svgNamespace, "g");
token.setAttribute(
"class",
"semantic-flow-token " + (options.className || "relationship-flow-token"),
);
token.setAttribute("data-token-kind", kind);
token.setAttribute("data-token-edge-key", key);
token.setAttribute("aria-hidden", "true");
token.appendChild(
relationshipTokenPart("circle", "semantic-flow-token-halo", {
cx: "0",
cy: "0",
r: "7",
}),
);
if (kind === "data") {
token.appendChild(
relationshipTokenPart("rect", "relationship-flow-token-shape", {
x: "-5",
y: "-4",
width: "10",
height: "8",
rx: "2",
}),
);
token.appendChild(
relationshipTokenPart("path", "relationship-flow-token-ink", {
d: "M -2.8 -1.2 h 5.6 M -2.8 1.4 h 3.8",
}),
);
} else if (kind === "event") {
token.appendChild(
relationshipTokenPart("rect", "relationship-flow-token-shape", {
x: "-7",
y: "-3",
width: "4",
height: "6",
rx: "1",
}),
);
token.appendChild(
relationshipTokenPart("rect", "relationship-flow-token-shape", {
x: "-2",
y: "-3",
width: "4",
height: "6",
rx: "1",
}),
);
token.appendChild(
relationshipTokenPart("rect", "relationship-flow-token-shape", {
x: "3",
y: "-3",
width: "4",
height: "6",
rx: "1",
}),
);
} else if (kind === "security") {
token.appendChild(
relationshipTokenPart("path", "relationship-flow-token-shape", {
d: "M 0 -5 L 4 -3.4 V 0 c 0 3 -1.6 4.5 -4 5.5 C -2.4 4.5 -4 3 -4 0 v -3.4 Z",
}),
);
token.appendChild(
relationshipTokenPart("path", "relationship-flow-token-ink", {
d: "m -2 .2 1.4 1.4 L 2 -1.4",
}),
);
} else if (kind === "state") {
token.appendChild(
relationshipTokenPart("circle", "relationship-flow-token-shape", {
cx: "0",
cy: "0",
r: "5",
}),
);
token.appendChild(
relationshipTokenPart("circle", "relationship-flow-token-dot", {
cx: "0",
cy: "0",
r: "1.35",
}),
);
} else {
token.appendChild(
relationshipTokenPart("path", "relationship-flow-token-ink", {
d: "M -5 -3 L -1 0 L -5 3 M 0 -3 L 4 0 L 0 3",
}),
);
}
var motion = document.createElementNS(svgNamespace, "animateMotion");
motion.setAttribute("path", pathData);
motion.setAttribute("dur", options.duration || "1.2s");
motion.setAttribute("begin", "0s");
motion.setAttribute("fill", "freeze");
motion.setAttribute("rotate", "auto");
motion.setAttribute("calcMode", "spline");
motion.setAttribute("keyTimes", "0;1");
motion.setAttribute("keySplines", ".2 0 .2 1");
token.appendChild(motion);
return token;
}
function createSemanticFlowToken(edge, shape, options) {
if (!edge || !shape) return null;
var key =
edge.getAttribute("data-edge-key") ||
edge.getAttribute("data-edge-from") +
"\u0000" +
edge.getAttribute("data-edge-to") +
"\u0000" +
(edge.getAttribute("data-edge-label") || "");
return relationshipTokenGeometry(shape, relationshipTokenKind(edge), key, options);
}
Archify.flowTokens = {
create: createSemanticFlowToken,
kind: function (edge) {
return relationshipTokenKind(edge);
},
path: relationshipTokenPath,
};
function renderRelationshipPulse(key) {
removeRelationshipPulse();
if (!key || document.documentElement.getAttribute("data-embed") === "true") return false;
if (document.hidden || (Archify.motionGovernor && Archify.motionGovernor.isPaused()))
return false;
if (reducedMotionQuery && reducedMotionQuery.matches) return false;
var matchingEdges = edges().filter(function (edge) {
return edge.getAttribute("data-edge-key") === key;
});
if (!matchingEdges.length) return false;
var overlay = document.createElementNS(svgNamespace, "g");
overlay.setAttribute("class", "relationship-pulse-overlay");
overlay.setAttribute("data-relationship-pulse-overlay", "");
overlay.setAttribute("data-relationship-pulse-key", key);
overlay.setAttribute("aria-hidden", "true");
var tokenAdded = false;
matchingEdges.forEach(function (edge) {
var wrapper = document.createElementNS(svgNamespace, "g");
if (edge.hasAttribute("transform"))
wrapper.setAttribute("transform", edge.getAttribute("transform"));
var shapes = relationshipEdgeShapes(edge);
shapes.forEach(function (shape) {
wrapper.appendChild(relationshipPulseGeometry(shape));
});
if (!tokenAdded && shapes.length) {
var tokenKind = relationshipTokenKind(edge);
var token = relationshipTokenGeometry(shapes[0], tokenKind, key);
if (token) {
wrapper.appendChild(token);
overlay.setAttribute("data-relationship-token-kind", tokenKind);
tokenAdded = true;
}
}
if (wrapper.childNodes.length) overlay.appendChild(wrapper);
});
if (!overlay.childNodes.length) return false;
var finishPulse = function () {
if (overlay.parentNode) overlay.remove();
};
overlay.addEventListener("animationend", finishPulse, { once: true });
overlay.addEventListener("animationcancel", finishPulse, { once: true });
var firstNode = svg.querySelector("[data-node-id]");
if (firstNode) svg.insertBefore(overlay, firstNode);
else svg.appendChild(overlay);
return true;
}
function clearRelationshipPreview(options) {
options = options || {};
if (directPreviewTimer) window.clearTimeout(directPreviewTimer);
directPreviewTimer = null;
removeRelationshipPulse();
activeRelationshipPreview = null;
svg.removeAttribute("data-relationship-preview-active");
svg.removeAttribute("data-relationship-direct-active");
if (options.clearPin === true) {
pinnedRelationship = null;
pinnedRelationshipKey = null;
svg.removeAttribute("data-relationship-pin-active");
}
chip.removeAttribute("data-relationship-previewing");
edges().forEach(function (edge) {
edge.removeAttribute("data-relationship-preview");
});
nodes().forEach(function (node) {
node.removeAttribute("data-relationship-preview-node");
node.removeAttribute("data-relationship-preview-source");
node.removeAttribute("data-relationship-preview-target");
});
Array.prototype.forEach.call(
relationshipList.querySelectorAll("[data-preview-active]"),
function (button) {
button.removeAttribute("data-preview-active");
},
);
relationshipHitTargets.forEach(function (target) {
target.setAttribute(
"aria-pressed",
pinnedRelationshipKey &&
target.getAttribute("data-relationship-key") === pinnedRelationshipKey
? "true"
: "false",
);
target.removeAttribute("data-preview-active");
});
renderRelationshipCopyAction();
requestLensPlacement();
}
function previewRelationship(button, options) {
options = options || {};
if (pinnedRelationshipKey && pinnedRelationship && button !== pinnedRelationship) return;
clearRelationshipPreview();
if (!button) return;
var key = button.getAttribute("data-relationship-key");
var from = button.getAttribute("data-relationship-from");
var to = button.getAttribute("data-relationship-to");
if (!key || !from || !to) return;
svg.setAttribute("data-relationship-preview-active", key);
if (options.direct === true) svg.setAttribute("data-relationship-direct-active", key);
edges().forEach(function (edge) {
if (edge.getAttribute("data-edge-key") === key)
edge.setAttribute("data-relationship-preview", "");
});
nodes().forEach(function (node) {
var id = node.getAttribute("data-node-id");
if (id !== from && id !== to) return;
node.setAttribute("data-relationship-preview-node", "");
if (id === from) node.setAttribute("data-relationship-preview-source", "");
if (id === to) node.setAttribute("data-relationship-preview-target", "");
});
button.setAttribute("data-preview-active", "true");
if (!chip.hidden && options.direct !== true)
chip.setAttribute("data-relationship-previewing", "true");
activeRelationshipPreview = button;
renderRelationshipPulse(key);
requestLensPlacement();
}
function syncRelationshipPreview() {
var next = pinnedRelationship || focusedRelationship || hoveredRelationship;
if (next === activeRelationshipPreview) return;
previewRelationship(next, {
direct: !!(next && next.hasAttribute("data-relationship-hit-key")),
});
}
function directRelationshipBlocked() {
return (
html.getAttribute("data-embed") === "true" ||
html.getAttribute("data-guide-open") === "true" ||
container.classList.contains("is-panning") ||
(activeIds.length > 0 && !pinnedRelationshipKey) ||
svg.hasAttribute("data-story-active") ||
svg.hasAttribute("data-route-picking") ||
svg.hasAttribute("data-route-active") ||
svg.hasAttribute("data-lens-active") ||
svg.hasAttribute("data-chapter-preview")
);
}
function scheduleDirectRelationshipPreview(target) {
if (directPreviewTimer) window.clearTimeout(directPreviewTimer);
if (pinnedRelationshipKey) return;
directPreviewTimer = window.setTimeout(
function () {
directPreviewTimer = null;
if (
pinnedRelationshipKey ||
hoveredRelationship !== target ||
directRelationshipBlocked()
)
return;
previewRelationship(target, { direct: true });
},
reducedMotionQuery && reducedMotionQuery.matches ? 0 : 90,
);
}
function relationshipHitTarget(key) {
return (
relationshipHitTargets.find(function (target) {
return target.getAttribute("data-relationship-key") === key;
}) || null
);
}
function revealPinnedRelationship(record) {
function reveal() {
if (!record || pinnedRelationshipKey !== record.key) return true;
if (!Archify.view || typeof Archify.view.reveal !== "function") return false;
Archify.view.reveal([record.from, record.to], { reason: "relationship-direct" });
return true;
}
if (!reveal()) requestAnimationFrame(reveal);
}
function inspectRelationship(key, options) {
options = options || {};
if (html.getAttribute("data-embed") === "true") return false;
if (directPreviewTimer) window.clearTimeout(directPreviewTimer);
directPreviewTimer = null;
hoveredRelationship = null;
focusedRelationship = null;
if (pinnedRelationshipKey === key) {
if (options.toggle === false) return true;
clear({ updateUrl: options.updateUrl !== false });
return true;
}
var record = relationshipRecordForKey(key);
if (!record) return false;
if (Archify.guidedViews && typeof Archify.guidedViews.showAll === "function") {
Archify.guidedViews.showAll({ clearFocus: false, updateUrl: false });
}
set(record.from, { toggle: false, updateUrl: false });
var row = Array.prototype.slice
.call(relationshipList.querySelectorAll("[data-relationship-key]"))
.find(function (candidate) {
return candidate.getAttribute("data-relationship-key") === key;
});
if (!row) return false;
previewRelationship(row);
pinnedRelationship = row;
pinnedRelationshipKey = key;
svg.setAttribute("data-relationship-pin-active", key);
var target = relationshipHitTarget(key);
if (target) target.setAttribute("aria-pressed", "true");
renderRelationshipCopyAction();
summary.textContent = viewerText("viewer.passport.relationship.pinned", {
from: record.fromLabel,
to: record.toLabel,
label: record.label,
});
revealPinnedRelationship(record);
if (options.updateUrl !== false && record.id) {
try {
history.replaceState(
null,
"",
location.pathname + location.search + "#relation=" + encodeURIComponent(record.id),
);
} catch (_) {}
}
return true;
}
function inspectRelationshipById(id, options) {
var record = relationshipHitRecords().find(function (item) {
return item.id === id;
});
return record ? inspectRelationship(record.key, options) : false;
}
function installRelationshipHitTargets() {
if (html.getAttribute("data-embed") === "true") return 0;
var records = relationshipHitRecords();
if (!records.length) return 0;
relationshipHitOverlay = document.createElementNS(svgNamespace, "g");
relationshipHitOverlay.setAttribute("class", "relationship-hit-overlay");
relationshipHitOverlay.setAttribute("data-relationship-hit-overlay", "");
relationshipHitOverlay.setAttribute("role", "group");
relationshipHitOverlay.setAttribute(
"aria-label",
viewerText("viewer.passport.relationship.explorer"),
);
var relationshipHelp = document.createElementNS(svgNamespace, "desc");
relationshipHelp.id = "archify-relationship-help";
relationshipHelp.textContent = viewerText("viewer.passport.relationship.help");
relationshipHitOverlay.appendChild(relationshipHelp);
records.forEach(function (record, index) {
var target = document.createElementNS(svgNamespace, "g");
target.setAttribute("class", "relationship-hit-target");
target.setAttribute("data-relationship-hit-key", record.key);
target.setAttribute("data-relationship-key", record.key);
target.setAttribute("data-relationship-from", record.from);
target.setAttribute("data-relationship-to", record.to);
if (record.id) target.setAttribute("data-relationship-id", record.id);
target.setAttribute("role", "button");
target.setAttribute("tabindex", index === 0 ? "0" : "-1");
target.setAttribute("aria-pressed", "false");
target.setAttribute("aria-describedby", relationshipHelp.id);
var description = viewerText("viewer.passport.relationship.inspect", {
index: index + 1,
total: records.length,
from: record.fromLabel,
to: record.toLabel,
label: record.label,
});
target.setAttribute("aria-label", description);
var title = document.createElementNS(svgNamespace, "title");
title.textContent =
record.fromLabel + " \u2192 " + record.toLabel + " \u00b7 " + record.label;
target.appendChild(title);
record.shapes.forEach(function (shape) {
target.appendChild(relationshipHitGeometry(shape));
target.appendChild(relationshipHitGeometry(shape, "relationship-focus-rail"));
});
if (target.childNodes.length > 1) {
relationshipHitTargets.push(target);
relationshipHitOverlay.appendChild(target);
}
});
if (!relationshipHitTargets.length) return 0;
var firstNode = svg.querySelector("[data-node-id]");
var nodeLayer = firstNode;
while (nodeLayer && nodeLayer.parentNode && nodeLayer.parentNode !== svg)
nodeLayer = nodeLayer.parentNode;
if (nodeLayer && nodeLayer.parentNode === svg)
svg.insertBefore(relationshipHitOverlay, nodeLayer);
else svg.appendChild(relationshipHitOverlay);
relationshipHitOverlay.addEventListener("pointerdown", function (event) {
if (event.target.closest("[data-relationship-hit-key]")) event.stopPropagation();
});
relationshipHitOverlay.addEventListener("pointerover", function (event) {
if (event.pointerType === "touch") return;
if (finePointerQuery && !finePointerQuery.matches) return;
var target = event.target.closest("[data-relationship-hit-key]");
if (!target || directRelationshipBlocked() || pinnedRelationshipKey) return;
if (event.relatedTarget && target.contains(event.relatedTarget)) return;
hoveredRelationship = target;
scheduleDirectRelationshipPreview(target);
});
relationshipHitOverlay.addEventListener("pointerout", function (event) {
var target = event.target.closest("[data-relationship-hit-key]");
if (!target || (event.relatedTarget && target.contains(event.relatedTarget))) return;
if (hoveredRelationship === target) hoveredRelationship = null;
syncRelationshipPreview();
});
relationshipHitOverlay.addEventListener("focusin", function (event) {
var target = event.target.closest("[data-relationship-hit-key]");
if (!target || directRelationshipBlocked()) return;
focusedRelationship = target;
if (!pinnedRelationshipKey) previewRelationship(target, { direct: true });
});
relationshipHitOverlay.addEventListener("focusout", function (event) {
var target = event.target.closest("[data-relationship-hit-key]");
if (!target || (event.relatedTarget && target.contains(event.relatedTarget))) return;
if (focusedRelationship === target) focusedRelationship = null;
syncRelationshipPreview();
});
relationshipHitOverlay.addEventListener("click", function (event) {
var target = event.target.closest("[data-relationship-hit-key]");
if (!target || directRelationshipBlocked()) return;
event.preventDefault();
event.stopPropagation();
focusedRelationship = null;
hoveredRelationship = null;
inspectRelationship(target.getAttribute("data-relationship-key"));
});
relationshipHitOverlay.addEventListener("keydown", function (event) {
var target = event.target.closest("[data-relationship-hit-key]");
if (!target) return;
if (event.key === "Escape" && pinnedRelationshipKey) {
event.preventDefault();
clear({ updateUrl: false });
return;
}
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
focusedRelationship = null;
hoveredRelationship = null;
inspectRelationship(target.getAttribute("data-relationship-key"));
return;
}
if (
event.key !== "ArrowRight" &&
event.key !== "ArrowLeft" &&
event.key !== "ArrowDown" &&
event.key !== "ArrowUp" &&
event.key !== "Home" &&
event.key !== "End"
)
return;
var index = relationshipHitTargets.indexOf(target);
if (event.key === "Home") index = 0;
else if (event.key === "End") index = relationshipHitTargets.length - 1;
else if (event.key === "ArrowRight" || event.key === "ArrowDown")
index = (index + 1) % relationshipHitTargets.length;
else
index = (index - 1 + relationshipHitTargets.length) % relationshipHitTargets.length;
event.preventDefault();
relationshipHitTargets.forEach(function (item, itemIndex) {
item.setAttribute("tabindex", itemIndex === index ? "0" : "-1");
});
try {
relationshipHitTargets[index].focus({ preventScroll: true });
} catch (_) {
try {
relationshipHitTargets[index].focus();
} catch (_) {}
}
});
return relationshipHitTargets.length;
}
function renderRelationshipLens(id, byId) {
hoveredRelationship = null;
focusedRelationship = null;
clearRelationshipPreview({ clearPin: true });
renderPassport(id, byId[id]);
var relationships = relationshipsFor(id, byId);
chip.removeAttribute("data-relations-expanded");
relationsBtn.setAttribute("aria-expanded", "false");
relationsBtn.textContent = viewerCount(
"viewer.passport.relationship.count",
relationships.length,
);
relationsBtn.setAttribute(
"aria-label",
viewerCount("viewer.passport.relationship.show", relationships.length),
);
var counts = { out: 0, in: 0, loop: 0 };
relationships.forEach(function (relationship) {
counts[relationship.direction] += 1;
});
summary.textContent = viewerText("viewer.passport.relationship.summary", {
out: counts.out,
in: counts.in,
loops: counts.loop
? viewerText("viewer.passport.relationship.loops", { count: counts.loop })
: "",
});
renderReachabilityControls(id);
relationshipList.textContent = "";
if (!relationships.length) {
var empty = document.createElement("p");
empty.className = "relationship-lens-empty";
empty.textContent = viewerText("viewer.passport.relationship.none");
relationshipList.appendChild(empty);
return;
}
[
{ id: "out", label: viewerText("viewer.passport.relationship.group.out") },
{ id: "in", label: viewerText("viewer.passport.relationship.group.in") },
{ id: "loop", label: viewerText("viewer.passport.relationship.group.loop") },
].forEach(function (group) {
var items = relationships.filter(function (relationship) {
return relationship.direction === group.id;
});
if (!items.length) return;
var section = document.createElement("div");
section.className = "relationship-lens-group";
var heading = document.createElement("span");
heading.className = "relationship-lens-group-title";
heading.textContent = group.label + " · " + items.length;
section.appendChild(heading);
items.forEach(function (relationship) {
var button = document.createElement("button");
button.type = "button";
button.className = "relationship-lens-row";
button.setAttribute("data-direction", relationship.direction);
button.setAttribute("data-relationship-target", relationship.neighborId);
button.setAttribute("data-relationship-key", relationship.key);
button.setAttribute("data-relationship-from", relationship.from);
button.setAttribute("data-relationship-to", relationship.to);
if (relationship.id) button.setAttribute("data-relationship-id", relationship.id);
button.setAttribute(
"aria-label",
viewerText("viewer.passport.relationship.row", {
group: group.label,
relationship: relationship.label,
neighbor: relationship.neighborLabel,
}),
);
var direction = document.createElement("span");
direction.className = "relationship-lens-direction";
direction.setAttribute("aria-hidden", "true");
direction.textContent = viewerText(
relationship.direction === "out"
? "viewer.passport.relationship.direction.out"
: relationship.direction === "in"
? "viewer.passport.relationship.direction.in"
: "viewer.passport.relationship.direction.loop",
);
var target = document.createElement("strong");
target.textContent = relationship.neighborLabel;
var relation = document.createElement("small");
relation.textContent = relationship.label;
button.appendChild(direction);
button.appendChild(target);
button.appendChild(relation);
section.appendChild(button);
});
relationshipList.appendChild(section);
});
}
var lensFrame = 0;
function placeRelationshipLens() {
lensFrame = 0;
if (chip.hidden || activeIds.length !== 1) return;
var node = svg.querySelector('[data-node-id="' + activeIds[0] + '"]');
if (!node) return;
var containerRect = container.getBoundingClientRect();
var nodeRect = node.getBoundingClientRect();
if (containerRect.bottom <= 0 || containerRect.top >= window.innerHeight) return;
var padding = window.innerWidth <= 720 ? 8 : 16;
var visibleTop = Math.max(padding, -containerRect.top + padding);
var visibleBottom = Math.min(
containerRect.height - padding,
window.innerHeight - containerRect.top - padding,
);
var maxTop = Math.max(padding, visibleBottom - chip.offsetHeight);
var minTop = Math.min(visibleTop, maxTop);
var nodeCenter = nodeRect.top - containerRect.top + nodeRect.height / 2;
var mobile = window.innerWidth <= 720;
var previewingOnMobile =
mobile && chip.getAttribute("data-relationship-previewing") === "true";
var compactOnMobile = mobile && chip.getAttribute("data-relations-expanded") !== "true";
var preferred;
if (compactOnMobile) {
var nodeTop = nodeRect.top - containerRect.top;
var nodeBottom = nodeRect.bottom - containerRect.top;
var gap = 10;
var above = nodeTop - chip.offsetHeight - gap;
var below = nodeBottom + gap;
if (above >= visibleTop) preferred = above;
else if (below + chip.offsetHeight <= visibleBottom - 56) preferred = below;
else
preferred =
nodeCenter < (visibleTop + visibleBottom) / 2
? Math.max(minTop, visibleBottom - chip.offsetHeight - 56)
: visibleTop;
} else if (previewingOnMobile) {
var pinnedTop = visibleTop;
var pinnedBottom = Math.max(minTop, visibleBottom - chip.offsetHeight - 56);
preferred = nodeCenter < (visibleTop + visibleBottom) / 2 ? pinnedBottom : pinnedTop;
} else {
preferred = nodeCenter - chip.offsetHeight / 2;
}
var top = Math.max(minTop, Math.min(maxTop, preferred));
var chipRect = chip.getBoundingClientRect();
var safeGap = 10;
var protectViewerChrome = !mobile || compactOnMobile || previewingOnMobile;
var protectedRects = (
protectViewerChrome
? [svg.querySelector("[data-legend]"), container.querySelector(".diagram-nav")]
: []
)
.filter(function (element) {
if (!element || element.hidden) return false;
var style = window.getComputedStyle(element);
return style.display !== "none" && style.visibility !== "hidden";
})
.map(function (element) {
return element.getBoundingClientRect();
})
.filter(function (rect) {
return (
rect.width > 0 &&
rect.height > 0 &&
chipRect.left < rect.right + safeGap &&
chipRect.right > rect.left - safeGap
);
});
if (protectedRects.length) {
var candidates = [top, minTop, maxTop];
protectedRects.forEach(function (rect) {
candidates.push(
rect.top - containerRect.top - chip.offsetHeight - safeGap,
rect.bottom - containerRect.top + safeGap,
);
});
var valid = candidates
.map(function (candidate) {
return Math.max(minTop, Math.min(maxTop, candidate));
})
.filter(function (candidate, index, all) {
if (all.indexOf(candidate) !== index) return false;
var candidateTop = containerRect.top + candidate;
var candidateBottom = candidateTop + chip.offsetHeight;
return protectedRects.every(function (rect) {
return (
candidateBottom <= rect.top - safeGap || candidateTop >= rect.bottom + safeGap
);
});
});
valid.sort(function (first, second) {
return Math.abs(first - preferred) - Math.abs(second - preferred);
});
if (valid.length) top = valid[0];
}
chip.style.top = Math.round(top) + "px";
if (Archify.radar && typeof Archify.radar.sync === "function") Archify.radar.sync();
}
function requestLensPlacement() {
if (lensFrame) return;
lensFrame = requestAnimationFrame(placeRelationshipLens);
}
function clear(options) {
options = options || {};
var restoreNode =
options.restoreFocus === true && activeIds.length === 1
? svg.querySelector('[data-node-id="' + activeIds[0] + '"]')
: null;
clearReachability({ updateUrl: false });
if (Archify.intentTrace && typeof Archify.intentTrace.clear === "function") {
Archify.intentTrace.clear({ announce: false });
}
hoveredRelationship = null;
focusedRelationship = null;
clearRelationshipPreview({ clearPin: true });
activeIds = [];
svg.removeAttribute("data-focus-active");
nodes().forEach(function (node) {
node.removeAttribute("data-focus-match");
node.removeAttribute("data-focus-selected");
node.setAttribute("aria-pressed", "false");
});
edges().forEach(function (edge) {
edge.removeAttribute("data-focus-match");
});
chip.hidden = true;
label.textContent = "";
detail.textContent = "";
detail.hidden = true;
kind.textContent = "";
kind.hidden = true;
context.textContent = "";
context.hidden = true;
tag.textContent = "";
tag.hidden = true;
semanticId.textContent = "";
semanticId.hidden = true;
evidence.hidden = true;
evidenceLinks.textContent = "";
repositoryLink.removeAttribute("href");
repositoryLink.textContent = "";
summary.textContent = "";
reachSection.hidden = true;
upstreamCount.textContent = "0";
downstreamCount.textContent = "0";
upstreamBtn.disabled = true;
downstreamBtn.disabled = true;
relationshipList.textContent = "";
copyBtn.textContent = viewerText("viewer.passport.copy");
copyBtn.setAttribute("aria-label", viewerText("viewer.passport.copy.focus"));
relationsBtn.textContent = viewerText("viewer.passport.relations");
relationsBtn.setAttribute("aria-label", viewerText("viewer.passport.relations.show"));
relationsBtn.setAttribute("aria-expanded", "false");
chip.removeAttribute("data-relations-expanded");
chip.style.removeProperty("top");
if (
options.preserveView !== true &&
Archify.view &&
typeof Archify.view.reset === "function"
) {
Archify.view.reset({ automatic: true });
}
if (options.updateUrl !== false) {
try {
history.replaceState(null, "", location.pathname + location.search);
} catch (_) {}
}
if (restoreNode) {
try {
restoreNode.focus({ preventScroll: true });
} catch (_) {
try {
restoreNode.focus();
} catch (_) {}
}
}
}
function setMany(ids, options) {
options = options || {};
if (Archify.semanticLens && typeof Archify.semanticLens.clearPreview === "function")
Archify.semanticLens.clearPreview();
if (Archify.semanticLens && Archify.semanticLens.active()) {
Archify.semanticLens.clear({ updateUrl: false, preserveView: true, closePanel: true });
}
if (
options.preserveRoute !== true &&
Archify.routeProbe &&
typeof Archify.routeProbe.clear === "function"
) {
Archify.routeProbe.clear({ updateUrl: false, restoreFocus: false });
}
var nodeList = nodes();
var byId = {};
nodeList.forEach(function (node) {
byId[node.getAttribute("data-node-id")] = node;
});
var normalized = [];
(ids || []).forEach(function (id) {
if (byId[id] && normalized.indexOf(id) === -1) normalized.push(id);
});
if (!normalized.length) return false;
if (
normalized.length === activeIds.length &&
normalized.every(function (id, index) {
return activeIds[index] === id;
}) &&
options.toggle !== false
) {
clear();
return true;
}
clear({ updateUrl: false, preserveView: true });
activeIds = normalized;
var selected = {};
var related = {};
var seenEdges = {};
var matchedEdges = 0;
normalized.forEach(function (id) {
selected[id] = true;
related[id] = true;
});
var selectionMode = options.mode === "selection" || normalized.length > 1;
edges().forEach(function (edge) {
var from = edge.getAttribute("data-edge-from");
var to = edge.getAttribute("data-edge-to");
var match = selectionMode
? selected[from] && selected[to]
: selected[from] || selected[to];
if (!match) return;
edge.setAttribute("data-focus-match", "");
if (!selectionMode) {
related[from] = true;
related[to] = true;
}
var edgeKey =
edge.getAttribute("data-edge-key") ||
from + "\u0000" + to + "\u0000" + (edge.getAttribute("data-edge-label") || "");
if (!seenEdges[edgeKey]) {
seenEdges[edgeKey] = true;
matchedEdges += 1;
}
});
nodeList.forEach(function (node) {
var nodeId = node.getAttribute("data-node-id");
if (related[nodeId]) node.setAttribute("data-focus-match", "");
if (selected[nodeId]) {
node.setAttribute("data-focus-selected", "");
node.setAttribute("aria-pressed", "true");
}
});
svg.setAttribute("data-focus-active", normalized.join(" "));
var defaultLabel =
normalized.length === 1
? nodeLabel(byId[normalized[0]], normalized[0])
: viewerText("viewer.guided.chapter.selectedNodes", { count: normalized.length });
label.textContent = options.label || defaultLabel;
chip.hidden = options.hideChip === true || normalized.length !== 1 || selectionMode;
if (!chip.hidden) {
renderRelationshipLens(normalized[0], byId);
requestLensPlacement();
}
if (options.updateUrl !== false) {
var key = options.urlKey || "focus";
var value = options.urlValue || normalized[0];
try {
history.replaceState(
null,
"",
location.pathname + location.search + "#" + key + "=" + encodeURIComponent(value),
);
} catch (_) {}
}
return true;
}
function set(id, options) {
options = options || {};
options.mode = "neighborhood";
return setMany([id], options);
}
function fallbackCopy(value) {
var field = document.createElement("textarea");
field.value = value;
field.setAttribute("readonly", "");
field.style.position = "fixed";
field.style.opacity = "0";
document.body.appendChild(field);
field.select();
var copied = false;
try {
copied = document.execCommand("copy");
} catch (_) {}
field.remove();
return copied;
}
function copyFocusLink() {
if (activeIds.length !== 1) return Promise.resolve(false);
var record = pinnedRelationshipRecord();
var relationId = record && record.id;
var value =
location.href.replace(/#.*$/, "") +
(relationId
? "#relation=" + encodeURIComponent(relationId)
: "#focus=" +
encodeURIComponent(activeIds[0]) +
(reachabilityMode ? "&reach=" + reachabilityMode : ""));
var copy =
navigator.clipboard && typeof navigator.clipboard.writeText === "function"
? navigator.clipboard
.writeText(value)
.then(function () {
return true;
})
.catch(function () {
return fallbackCopy(value);
})
: Promise.resolve(fallbackCopy(value));
return copy.then(function (copied) {
copyBtn.textContent = viewerText(
copied ? "viewer.common.copied" : "viewer.common.copyFailed",
);
copyBtn.setAttribute(
"aria-label",
copied
? viewerText(
relationId
? "viewer.passport.copy.pinned.success"
: "viewer.passport.copy.focused.success",
)
: viewerText(
relationId
? "viewer.passport.copy.pinned.failed"
: "viewer.passport.copy.focused.failed",
),
);
window.setTimeout(function () {
renderRelationshipCopyAction();
}, 1600);
return copied;
});
}
svg.addEventListener("click", function (event) {
if (container.getAttribute("data-just-panned") === "true") return;
var node = event.target.closest("[data-node-id]");
if (node) {
var id = node.getAttribute("data-node-id");
set(id);
if (
activeIds.indexOf(id) !== -1 &&
Archify.view &&
typeof Archify.view.reveal === "function"
) {
Archify.view.reveal([id], { includeNeighbors: true, reason: "focus" });
}
} else if (activeIds.length) clear();
});
svg.addEventListener("keydown", function (event) {
var node = event.target.closest("[data-node-id]");
if (!node || (event.key !== "Enter" && event.key !== " ")) return;
event.preventDefault();
var id = node.getAttribute("data-node-id");
set(id);
if (
activeIds.indexOf(id) !== -1 &&
Archify.view &&
typeof Archify.view.reveal === "function"
) {
Archify.view.reveal([id], { includeNeighbors: true, reason: "focus" });
}
});
clearBtn.addEventListener("click", function () {
clear({ restoreFocus: true });
});
copyBtn.addEventListener("click", copyFocusLink);
upstreamBtn.addEventListener("click", function () {
applyReachability("upstream");
});
downstreamBtn.addEventListener("click", function () {
applyReachability("downstream");
});
relationsBtn.addEventListener("click", function () {
var expanded = chip.getAttribute("data-relations-expanded") === "true";
if (expanded) chip.removeAttribute("data-relations-expanded");
else chip.setAttribute("data-relations-expanded", "true");
relationsBtn.setAttribute("aria-expanded", expanded ? "false" : "true");
relationsBtn.setAttribute(
"aria-label",
viewerText(
expanded ? "viewer.passport.relations.show" : "viewer.passport.relations.hide",
),
);
requestLensPlacement();
});
relationshipList.addEventListener("click", function (event) {
var button = event.target.closest("[data-relationship-target]");
if (!button) return;
var id = button.getAttribute("data-relationship-target");
if (Archify.guidedViews && typeof Archify.guidedViews.showAll === "function") {
Archify.guidedViews.showAll({ clearFocus: false, updateUrl: false });
}
set(id, { toggle: false });
if (Archify.view && typeof Archify.view.reveal === "function") {
Archify.view.reveal([id], { includeNeighbors: true, reason: "relationship" });
}
var node = svg.querySelector('[data-node-id="' + id + '"]');
if (node) {
try {
node.focus({ preventScroll: true });
} catch (_) {
try {
node.focus();
} catch (_) {}
}
}
});
relationshipList.addEventListener("pointerover", function (event) {
if (event.pointerType === "touch") return;
if (finePointerQuery && !finePointerQuery.matches) return;
var button = event.target.closest("[data-relationship-key]");
if (!button || !relationshipList.contains(button)) return;
if (event.relatedTarget && button.contains(event.relatedTarget)) return;
hoveredRelationship = button;
syncRelationshipPreview();
});
relationshipList.addEventListener("pointerout", function (event) {
var button = event.target.closest("[data-relationship-key]");
if (!button || (event.relatedTarget && button.contains(event.relatedTarget))) return;
if (hoveredRelationship === button) hoveredRelationship = null;
syncRelationshipPreview();
});
relationshipList.addEventListener("focusin", function (event) {
var button = event.target.closest("[data-relationship-key]");
if (!button) return;
focusedRelationship = button;
syncRelationshipPreview();
});
relationshipList.addEventListener("focusout", function (event) {
var button = event.target.closest("[data-relationship-key]");
if (!button || (event.relatedTarget && button.contains(event.relatedTarget))) return;
if (focusedRelationship === button) focusedRelationship = null;
syncRelationshipPreview();
});
relationshipList.addEventListener("keydown", function (event) {
if (
event.key !== "ArrowDown" &&
event.key !== "ArrowUp" &&
event.key !== "Home" &&
event.key !== "End"
)
return;
var buttons = Array.prototype.slice.call(
relationshipList.querySelectorAll("[data-relationship-target]"),
);
if (!buttons.length) return;
var index = buttons.indexOf(document.activeElement);
if (event.key === "Home") index = 0;
else if (event.key === "End") index = buttons.length - 1;
else if (event.key === "ArrowDown")
index = Math.min(buttons.length - 1, Math.max(0, index + 1));
else index = Math.max(0, index < 0 ? 0 : index - 1);
event.preventDefault();
buttons[index].focus();
});
document.addEventListener(
"click",
function (event) {
var target = event.target;
if (
chip.hidden ||
!target ||
typeof target.closest !== "function" ||
chip.contains(target)
)
return;
if (container.getAttribute("data-just-panned") === "true") return;
if (target.closest("[data-node-id], [data-relationship-hit-key], .overview-map"))
return;
clear();
},
true,
);
window.addEventListener("scroll", requestLensPlacement, { passive: true });
window.addEventListener("resize", requestLensPlacement);
container.addEventListener("scroll", requestLensPlacement, { passive: true });
document.addEventListener("visibilitychange", function () {
if (document.hidden) removeRelationshipPulse();
});
function syncRelationshipMotionPreference(event) {
if (event.matches) removeRelationshipPulse();
}
if (reducedMotionQuery) {
if (typeof reducedMotionQuery.addEventListener === "function") {
reducedMotionQuery.addEventListener("change", syncRelationshipMotionPreference);
} else if (typeof reducedMotionQuery.addListener === "function") {
reducedMotionQuery.addListener(syncRelationshipMotionPreference);
}
}
installRelationshipHitTargets();
function syncFocusFromHash() {
try {
var params = new URLSearchParams(location.hash.replace(/^#/, ""));
var relation = params.get("relation");
var initial = params.get("focus");
var reach = params.get("reach");
if (relation) {
if (
html.getAttribute("data-embed") === "true" ||
!inspectRelationshipById(relation, { updateUrl: false, toggle: false })
)
clear({ updateUrl: false });
} else if (initial) {
if (
set(initial, { updateUrl: false, toggle: false }) &&
(reach === "upstream" || reach === "downstream")
) {
applyReachability(reach, { updateUrl: false, toggle: false, reveal: false });
}
} else if (!params.get("view")) clear({ updateUrl: false });
} catch (_) {}
}
window.addEventListener("hashchange", syncFocusFromHash);
syncFocusFromHash();
return {
set: set,
setMany: setMany,
clear: clear,
copyLink: copyFocusLink,
reach: applyReachability,
clearReach: clearReachability,
reachabilitySnapshot: reachabilitySnapshot,
inspectRelationship: inspectRelationship,
inspectRelationshipById: inspectRelationshipById,
reposition: requestLensPlacement,
relationship: function () {
var record = pinnedRelationshipRecord();
return record
? {
id: record.id || null,
key: record.key,
from: record.from,
to: record.to,
label: record.label,
}
: null;
},
reachability: function () {
return activeReachability
? {
direction: activeReachability.direction,
originId: activeReachability.originId,
nodeIds: activeReachability.nodeIds.slice(),
edgeKeys: activeReachability.edgeKeys.slice(),
maxDepth: activeReachability.maxDepth,
}
: null;
},
active: function () {
return activeIds.length === 0
? null
: activeIds.length === 1
? activeIds[0]
: activeIds.slice();
},
};
})();
/* ============================================================
Intent Trace — temporary one-hop topology before durable focus.
Fine-pointer hover and keyboard focus share the same stable-ID preview;
touch continues directly to the existing click-to-focus contract.
============================================================ */
Archify.intentTrace = (function () {
var html = document.documentElement;
var container = document.querySelector(".diagram-container");
var svg = container.querySelector(":scope > svg");
var status = document.getElementById("intent-trace-status");
var namespace = "http://www.w3.org/2000/svg";
var activeId = null;
var hoveredNode = null;
var focusedNode = null;
var enterTimer = null;
function nodes() {
return Array.prototype.slice.call(svg.querySelectorAll("[data-node-id]"));
}
function edges() {
return Array.prototype.slice.call(svg.querySelectorAll("[data-edge-from][data-edge-to]"));
}
function nodeLabel(node, fallback) {
return (
node.getAttribute("data-node-label") ||
(node.getAttribute("aria-label") || fallback).replace(/^Focus\s+/, "").split(",")[0]
);
}
function finePointer() {
return (
!window.matchMedia || window.matchMedia("(hover: hover) and (pointer: fine)").matches
);
}
function reducedMotion() {
return !!(
window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches
);
}
function blocked() {
return (
html.getAttribute("data-embed") === "true" ||
html.getAttribute("data-guide-open") === "true" ||
container.classList.contains("is-panning") ||
svg.hasAttribute("data-lens-active") ||
svg.hasAttribute("data-story-active") ||
svg.hasAttribute("data-relationship-preview-active") ||
!!(
Archify.routeProbe &&
typeof Archify.routeProbe.active === "function" &&
Archify.routeProbe.active()
) ||
!!(
Archify.focus &&
typeof Archify.focus.active === "function" &&
Archify.focus.active()
)
);
}
function edgeShapes(edge) {
if (/^(path|line|polyline)$/i.test(edge.tagName)) return [edge];
return Array.prototype.slice.call(edge.querySelectorAll("path, line, polyline"));
}
function removeOverlay() {
Array.prototype.forEach.call(
svg.querySelectorAll("[data-intent-trace-overlay]"),
function (overlay) {
overlay.remove();
},
);
}
function clear(options) {
options = options || {};
if (enterTimer) window.clearTimeout(enterTimer);
enterTimer = null;
activeId = null;
svg.removeAttribute("data-intent-trace-active");
removeOverlay();
nodes().forEach(function (node) {
node.removeAttribute("data-intent-trace-match");
node.removeAttribute("data-intent-trace-selected");
});
edges().forEach(function (edge) {
edge.removeAttribute("data-intent-trace-match");
});
if (options.announce !== false) status.textContent = "";
}
function traceGeometry(shape, direction) {
var clone = shape.cloneNode(false);
clone.removeAttribute("id");
clone.removeAttribute("class");
clone.removeAttribute("style");
clone.removeAttribute("marker-start");
clone.removeAttribute("marker-mid");
clone.removeAttribute("marker-end");
clone.removeAttribute("role");
clone.removeAttribute("aria-label");
clone.removeAttribute("aria-labelledby");
clone.removeAttribute("data-animate");
clone.removeAttribute("data-edge-from");
clone.removeAttribute("data-edge-to");
clone.removeAttribute("data-edge-key");
clone.removeAttribute("data-edge-id");
clone.removeAttribute("data-edge-label");
clone.removeAttribute("data-intent-trace-match");
clone.removeAttribute("data-intent-trace-selected");
clone.setAttribute("class", "intent-trace-flow");
clone.setAttribute("data-direction", direction);
clone.setAttribute("pathLength", "1");
return clone;
}
function show(id, options) {
options = options || {};
if (!id || blocked()) {
clear({ announce: false });
return false;
}
if (activeId === id) return true;
clear({ announce: false });
var nodeList = nodes();
var edgeList = edges();
var byId = {};
nodeList.forEach(function (node) {
byId[node.getAttribute("data-node-id")] = node;
});
var selected = byId[id];
if (!selected) return false;
var overlay = document.createElementNS(namespace, "g");
overlay.setAttribute("class", "intent-trace-overlay");
overlay.setAttribute("data-intent-trace-overlay", "");
overlay.setAttribute("aria-hidden", "true");
var related = {};
var seen = {};
var counts = { out: 0, in: 0, loop: 0 };
related[id] = true;
edgeList.forEach(function (edge) {
var from = edge.getAttribute("data-edge-from");
var to = edge.getAttribute("data-edge-to");
if (from !== id && to !== id) return;
var direction = from === id && to === id ? "loop" : from === id ? "out" : "in";
var key =
edge.getAttribute("data-edge-key") ||
from + "\u0000" + to + "\u0000" + (edge.getAttribute("data-edge-label") || "");
if (!seen[key]) {
seen[key] = true;
counts[direction] += 1;
}
related[from] = true;
related[to] = true;
edge.setAttribute("data-intent-trace-match", "");
var wrapper = document.createElementNS(namespace, "g");
if (edge.hasAttribute("transform"))
wrapper.setAttribute("transform", edge.getAttribute("transform"));
edgeShapes(edge).forEach(function (shape) {
wrapper.appendChild(traceGeometry(shape, direction));
});
if (wrapper.childNodes.length) overlay.appendChild(wrapper);
});
nodeList.forEach(function (node) {
var nodeId = node.getAttribute("data-node-id");
if (related[nodeId]) node.setAttribute("data-intent-trace-match", "");
if (nodeId === id) node.setAttribute("data-intent-trace-selected", "");
});
if (overlay.childNodes.length) {
var firstEdge = edgeList[0];
var firstNode = svg.querySelector("[data-node-id]");
if (firstEdge && firstEdge.parentNode)
firstEdge.parentNode.insertBefore(overlay, firstEdge);
else if (firstNode) svg.insertBefore(overlay, firstNode);
else svg.appendChild(overlay);
}
activeId = id;
svg.setAttribute("data-intent-trace-active", id);
if (options.announce === true) {
var total = counts.out + counts.in + counts.loop;
status.textContent = viewerText("viewer.intent.summary", {
label: nodeLabel(selected, id),
out: counts.out,
in: counts.in,
loops: counts.loop ? viewerText("viewer.intent.loops", { count: counts.loop }) : "",
total: total,
});
}
return true;
}
function schedule(node) {
if (enterTimer) window.clearTimeout(enterTimer);
enterTimer = window.setTimeout(
function () {
enterTimer = null;
if (hoveredNode === node)
show(node.getAttribute("data-node-id"), { announce: false });
},
reducedMotion() ? 0 : 90,
);
}
function sync() {
var candidate = focusedNode || hoveredNode;
if (!candidate) {
clear();
return;
}
show(candidate.getAttribute("data-node-id"), { announce: candidate === focusedNode });
}
svg.addEventListener("pointerover", function (event) {
var node = event.target.closest("[data-node-id]");
if (!node || !finePointer() || event.pointerType === "touch") return;
if (event.relatedTarget && node.contains(event.relatedTarget)) return;
hoveredNode = node;
schedule(node);
});
svg.addEventListener("pointerout", function (event) {
var node = event.target.closest("[data-node-id]");
if (!node || (event.relatedTarget && node.contains(event.relatedTarget))) return;
if (hoveredNode === node) hoveredNode = null;
sync();
});
svg.addEventListener("focusin", function (event) {
var node = event.target.closest("[data-node-id]");
if (!node) return;
focusedNode = node;
show(node.getAttribute("data-node-id"), { announce: true });
});
svg.addEventListener("focusout", function (event) {
var node = event.target.closest("[data-node-id]");
if (!node || (event.relatedTarget && node.contains(event.relatedTarget))) return;
if (focusedNode === node) focusedNode = null;
sync();
});
container.addEventListener("pointerdown", function (event) {
if (!event.target.closest("[data-node-id]")) clear({ announce: false });
});
window.addEventListener("blur", function () {
clear({ announce: false });
});
return {
show: show,
clear: clear,
active: function () {
return activeId;
},
};
})();
Archify.guidedViews = (function () {
var data = document.getElementById("archify-guided-views-data");
var panel = document.getElementById("guided-views");
var prev = document.getElementById("guided-view-prev");
var next = document.getElementById("guided-view-next");
var all = document.getElementById("guided-view-all");
var play = document.getElementById("guided-view-play");
var playIcon = document.getElementById("guided-view-play-icon");
var playLabel = document.getElementById("guided-view-play-label");
var beatLink = document.getElementById("guided-view-beat-link");
var beatLinkLabel = document.getElementById("guided-view-beat-link-label");
var progressBar = document.getElementById("guided-view-progress-bar");
var count = document.getElementById("guided-view-count");
var handoffReceipt = document.getElementById("guided-view-handoff");
var label = document.getElementById("guided-view-label");
var note = document.getElementById("guided-view-note");
var trail = document.getElementById("guided-view-trail");
var storyCaption = document.getElementById("guided-story-caption");
var storyCaptionIndex = document.getElementById("guided-story-caption-index");
var storyCaptionRoute = document.getElementById("guided-story-caption-route");
var storyCaptionDetail = document.getElementById("guided-story-caption-detail");
var storyCaptionNext = document.getElementById("guided-story-caption-next");
var storyCaptionNextLabel = document.getElementById("guided-story-caption-next-label");
var chapterIndex = document.getElementById("guided-view-index");
var chapterList = document.getElementById("guided-view-chapters");
var shareCue = document.getElementById("share-chapter-cue");
var shareCueState = document.getElementById("share-chapter-state");
var shareCueCount = document.getElementById("share-chapter-count");
var shareCueLabel = document.getElementById("share-chapter-label");
var shareCueNote = document.getElementById("share-chapter-note");
var shareCueRoute = document.getElementById("share-chapter-route");
var shareCueProgress = document.getElementById("share-chapter-progress-bar");
var svg = document.querySelector(".diagram-container svg");
var views = [];
var activeIndex = -1;
var playing = false;
var storyBeatTimer = null;
var storyBeatIndex = -1;
var storyBeatStartedAt = 0;
var storyBeatElapsedMs = 0;
var storyBeatDwellMs = 0;
var storyPlaybackGeneration = 0;
var storyFollowGeneration = 0;
var storyPlaybackScope = "story";
var storyPlaybackComplete = false;
var beatLinkFeedbackTimer = null;
var momentRestoreGeneration = 0;
var storySteps = [];
var storyPulseOwnerToken = 0;
var storyPulseGeneration = 0;
var autoplayPending = false;
var VIEW_INTERVAL_MS = 3200;
var STORY_FOLLOW_MIN_DWELL_MS = 1100;
var STORY_FOLLOW_DURATION_MS = 320;
var chapterButtons = [];
var handoffGeneration = 0;
var currentHandoff = null;
var previewGeneration = 0;
var pointerPreviewIntent = null;
var focusPreviewIntent = null;
var activePreviewIndex = -1;
var previewOwnerToken = 0;
try {
views = JSON.parse(data.textContent || "[]");
} catch (_) {
views = [];
}
if (!views.length)
return {
count: 0,
active: function () {
return null;
},
};
var canonicalNodeIds = {};
Array.prototype.forEach.call(svg.querySelectorAll("[data-node-id]"), function (node) {
canonicalNodeIds[node.getAttribute("data-node-id")] = true;
});
views.forEach(function (view) {
var seen = {};
view.focus = (view.focus || []).filter(function (id) {
if (!canonicalNodeIds[id] || seen[id]) return false;
seen[id] = true;
return true;
});
});
// Establish the compact cold state before exposing the panel so the
// full Director cannot flash during first paint. Hash restoration runs
// in the same task and replaces this state before paint when valid.
panel.setAttribute("data-active-view", "all");
panel.hidden = false;
panel.setAttribute("data-view-interval-ms", String(VIEW_INTERVAL_MS));
panel.setAttribute("data-story-follow-min-dwell-ms", String(STORY_FOLLOW_MIN_DWELL_MS));
buildChapterIndex();
function reducedMotion() {
return !!(
window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches
);
}
function findNode(id) {
return (
Array.prototype.find.call(svg.querySelectorAll("[data-node-id]"), function (node) {
return node.getAttribute("data-node-id") === id;
}) || null
);
}
function chapterDelta(previous, destination) {
var previousFocus = previous ? previous.focus : [];
var destinationFocus = destination ? destination.focus : [];
var previousIds = {};
var destinationIds = {};
previousFocus.forEach(function (id) {
previousIds[id] = true;
});
destinationFocus.forEach(function (id) {
destinationIds[id] = true;
});
return {
stay: previousFocus.filter(function (id) {
return destinationIds[id];
}),
enter: destinationFocus.filter(function (id) {
return !previousIds[id];
}),
leave: previousFocus.filter(function (id) {
return !destinationIds[id];
}),
};
}
function chapterAnchor(previous, destination, outgoingBeatIndex, delta) {
if (!previous || !destination) return "";
var stayIds = {};
delta.stay.forEach(function (id) {
stayIds[id] = true;
});
var activeBeat = outgoingBeatIndex >= 0 ? previous.focus[outgoingBeatIndex] : "";
if (activeBeat && stayIds[activeBeat]) return activeBeat;
for (var index = previous.focus.length - 1; index >= 0; index -= 1) {
if (stayIds[previous.focus[index]]) return previous.focus[index];
}
return "";
}
function handoffMotionAllowed() {
if (document.hidden || reducedMotion()) return false;
if (document.documentElement.getAttribute("data-embed") === "true") return false;
if (window.matchMedia && window.matchMedia("print").matches) return false;
return !(
Archify.motionGovernor &&
Archify.motionGovernor.capable &&
Archify.motionGovernor.isPaused()
);
}
function classifyHandoff(delta, anchor) {
var roles = {};
delta.stay.forEach(function (id) {
roles[id] = "stay";
});
delta.enter.forEach(function (id) {
roles[id] = "enter";
});
delta.leave.forEach(function (id) {
roles[id] = "leave";
});
Array.prototype.forEach.call(svg.querySelectorAll("[data-node-id]"), function (node) {
var id = node.getAttribute("data-node-id");
var role = roles[id] || "";
if (role) node.setAttribute("data-chapter-role", role);
else node.removeAttribute("data-chapter-role");
});
svg.setAttribute("data-chapter-handoff", anchor ? "anchor" : "no-anchor");
if (anchor) svg.setAttribute("data-chapter-anchor", anchor);
else svg.removeAttribute("data-chapter-anchor");
}
function drawHandoffAnchor(id) {
var node = findNode(id);
if (!node) return null;
var box;
try {
box = node.getBBox();
} catch (_) {
return null;
}
var overlay = document.createElementNS("http://www.w3.org/2000/svg", "g");
var ring = document.createElementNS("http://www.w3.org/2000/svg", "rect");
overlay.setAttribute("class", "chapter-handoff-overlay");
overlay.setAttribute("data-chapter-handoff-overlay", "");
overlay.setAttribute("aria-hidden", "true");
ring.setAttribute("class", "chapter-handoff-anchor");
ring.setAttribute("x", String(box.x - 8));
ring.setAttribute("y", String(box.y - 8));
ring.setAttribute("width", String(box.width + 16));
ring.setAttribute("height", String(box.height + 16));
ring.setAttribute("rx", "10");
overlay.appendChild(ring);
svg.appendChild(overlay);
return overlay;
}
function clearHandoffPresentation() {
svg.removeAttribute("data-chapter-handoff");
svg.removeAttribute("data-chapter-anchor");
Array.prototype.forEach.call(
svg.querySelectorAll("[data-chapter-handoff-overlay]"),
function (overlay) {
overlay.remove();
},
);
Array.prototype.forEach.call(
svg.querySelectorAll("[data-chapter-role]"),
function (node) {
node.removeAttribute("data-chapter-role");
},
);
if (handoffReceipt) {
handoffReceipt.hidden = true;
handoffReceipt.textContent = "";
}
}
function completeHandoff(handoff, outcome) {
if (!handoff || currentHandoff !== handoff) return false;
if (handoff.holdTimer) clearTimeout(handoff.holdTimer);
handoff.holdTimer = null;
currentHandoff = null;
clearHandoffPresentation();
if (handoff.ownerToken && Archify.motionGovernor)
Archify.motionGovernor.release(handoff.ownerToken);
handoff.resolve({
id: handoff.id,
state: outcome || "complete",
anchor: handoff.anchor || null,
});
syncStoryControlsDisabled();
syncChapterPreview();
return true;
}
function settleHandoff(reason) {
var handoff = currentHandoff;
if (!handoff) return false;
if (handoff.holdTimer) clearTimeout(handoff.holdTimer);
handoff.holdTimer = null;
if (handoff.camera && handoff.camera.cancel)
handoff.camera.cancel(reason || "settled", true);
else if (Archify.view && Archify.view.reveal)
Archify.view.reveal(handoff.focus, { instant: true, reason: reason || "settled" });
completeHandoff(handoff, reason || "settled");
return true;
}
function cancelHandoff(reason) {
var handoff = currentHandoff;
if (!handoff) return false;
if (handoff.holdTimer) clearTimeout(handoff.holdTimer);
handoff.holdTimer = null;
if (handoff.camera && handoff.camera.cancel)
handoff.camera.cancel(reason || "manual", false);
completeHandoff(handoff, reason || "manual");
return true;
}
function afterHandoff(callback) {
if (!currentHandoff) {
callback();
return;
}
currentHandoff.finished.then(callback);
}
function beginHandoff(
previousIndex,
nextIndex,
previous,
destination,
outgoingBeatIndex,
reason,
) {
if (!Archify.view || !Archify.view.reveal) return null;
if (!previous || previousIndex === nextIndex) {
Archify.view.reveal(destination.focus, { reason: reason });
return null;
}
var delta = chapterDelta(previous, destination);
var anchor = chapterAnchor(previous, destination, outgoingBeatIndex, delta);
if (!handoffMotionAllowed()) {
Archify.view.reveal(destination.focus, { instant: true, reason: reason });
return null;
}
var resolver;
var handoff = {
id: ++handoffGeneration,
mode: anchor ? "holding" : "no-anchor",
anchor: anchor,
focus: destination.focus.slice(),
holdTimer: null,
camera: null,
ownerToken: 0,
finished: new Promise(function (resolve) {
resolver = resolve;
}),
resolve: resolver,
};
currentHandoff = handoff;
classifyHandoff(delta, anchor);
if (anchor) {
drawHandoffAnchor(anchor);
var node = findNode(anchor);
var nodeLabel = node ? node.getAttribute("data-node-label") || anchor : anchor;
handoffReceipt.textContent = viewerText("viewer.guided.handoff", {
from: (previousIndex + 1 < 10 ? "0" : "") + (previousIndex + 1),
to: (nextIndex + 1 < 10 ? "0" : "") + (nextIndex + 1),
label: nodeLabel,
});
handoffReceipt.hidden = false;
}
if (Archify.motionGovernor) {
handoff.ownerToken = Archify.motionGovernor.claim("handoff", function () {
if (currentHandoff === handoff) settleHandoff("preempted");
});
}
var startCamera = function () {
if (currentHandoff !== handoff) return;
handoff.holdTimer = null;
handoff.mode = "settling";
svg.setAttribute("data-chapter-handoff", anchor ? "settling" : "no-anchor");
handoff.camera = Archify.view.reveal(destination.focus, {
reason: reason,
duration: 420,
});
if (handoff.camera && handoff.camera.finished) {
handoff.camera.finished.then(function (outcome) {
if (currentHandoff === handoff)
completeHandoff(handoff, (outcome && outcome.state) || "complete");
});
} else {
completeHandoff(handoff, "complete");
}
};
if (anchor) handoff.holdTimer = setTimeout(startCamera, 110);
else startCamera();
return handoff;
}
function centerChapterButton(button) {
if (!button || !chapterList) return false;
var target = Math.max(
0,
button.offsetLeft - (chapterList.clientWidth - button.offsetWidth) / 2,
);
if (typeof chapterList.scrollTo === "function")
chapterList.scrollTo({ left: target, behavior: "auto" });
else chapterList.scrollLeft = target;
return true;
}
function focusChapterButton(index) {
if (!chapterButtons.length) return false;
index = Math.max(0, Math.min(chapterButtons.length - 1, index));
chapterButtons.forEach(function (button, buttonIndex) {
button.setAttribute("tabindex", buttonIndex === index ? "0" : "-1");
});
chapterButtons[index].focus();
centerChapterButton(chapterButtons[index]);
return true;
}
function buildChapterIndex() {
chapterButtons = [];
chapterList.textContent = "";
views.forEach(function (view, index) {
var item = document.createElement("li");
var button = document.createElement("button");
button.type = "button";
button.className = "guided-view-chapter";
button.setAttribute("data-guided-view-id", view.id);
button.setAttribute("aria-pressed", "false");
button.setAttribute("tabindex", index === 0 ? "0" : "-1");
button.setAttribute(
"aria-label",
viewerText("viewer.guided.chapter.open", {
index: index + 1,
total: views.length,
label: view.label,
count: view.focus.length,
}),
);
button.title =
view.label +
" — " +
(view.note ||
viewerText("viewer.guided.chapter.selectedNodes", { count: view.focus.length }));
var position = document.createElement("span");
position.className = "guided-view-chapter-index";
position.setAttribute("aria-hidden", "true");
position.textContent = (index + 1 < 10 ? "0" : "") + (index + 1);
var title = document.createElement("span");
title.className = "guided-view-chapter-title";
title.textContent = view.label;
var stops = document.createElement("em");
stops.className = "guided-view-chapter-count";
stops.textContent = viewerCount("viewer.guided.chapter.stop", view.focus.length);
var delta = document.createElement("span");
delta.className = "guided-view-chapter-delta";
delta.hidden = true;
delta.setAttribute("aria-hidden", "true");
["stay", "enter", "leave"].forEach(function (kind) {
var value = document.createElement("span");
value.setAttribute("data-delta-kind", kind);
delta.appendChild(value);
});
button.appendChild(position);
button.appendChild(title);
button.appendChild(stops);
button.appendChild(delta);
item.appendChild(button);
chapterList.appendChild(item);
chapterButtons.push(button);
});
}
function syncChapterIndex() {
chapterButtons.forEach(function (button, index) {
var current = index === activeIndex;
var stops = button.querySelector(".guided-view-chapter-count");
var deltaLabel = button.querySelector(".guided-view-chapter-delta");
var position =
activeIndex < 0
? "available"
: current
? "current"
: index < activeIndex
? "before"
: "after";
button.setAttribute("data-chapter-position", position);
button.setAttribute("aria-pressed", current ? "true" : "false");
button.setAttribute(
"tabindex",
current || (activeIndex < 0 && index === 0) ? "0" : "-1",
);
if (current) {
stops.hidden = false;
deltaLabel.hidden = true;
button.removeAttribute("data-chapter-delta");
button.setAttribute(
"aria-label",
viewerText("viewer.guided.chapter.current", {
index: index + 1,
total: views.length,
label: views[index].label,
count: views[index].focus.length,
}),
);
button.title = viewerText("viewer.guided.chapter.current.title", {
label: views[index].label,
count: views[index].focus.length,
});
} else {
var delta = chapterDelta(activeIndex >= 0 ? views[activeIndex] : null, views[index]);
var compact =
"=" +
delta.stay.length +
" +" +
delta.enter.length +
" \u2212" +
delta.leave.length;
var expanded = viewerText("viewer.guided.chapter.delta.expanded", {
stay: delta.stay.length,
enter: delta.enter.length,
leave: delta.leave.length,
});
stops.hidden = true;
deltaLabel.hidden = false;
deltaLabel.querySelector('[data-delta-kind="stay"]').textContent =
"=" + delta.stay.length;
deltaLabel.querySelector('[data-delta-kind="enter"]').textContent =
"+" + delta.enter.length;
deltaLabel.querySelector('[data-delta-kind="leave"]').textContent =
"\u2212" + delta.leave.length;
button.setAttribute("data-chapter-delta", compact);
button.setAttribute(
"aria-label",
viewerText("viewer.guided.chapter.delta.aria", {
index: index + 1,
total: views.length,
label: views[index].label,
delta: expanded,
}),
);
button.title = viewerText("viewer.guided.chapter.delta.title", {
label: views[index].label,
delta: compact,
});
}
if (current) button.setAttribute("aria-current", "step");
else button.removeAttribute("aria-current");
if (button.parentElement)
button.parentElement.setAttribute("data-chapter-position", position);
});
if (activeIndex >= 0) centerChapterButton(chapterButtons[activeIndex]);
}
function chapterPreviewBlocked() {
if (document.hidden || playing || currentHandoff) return true;
if (document.documentElement.getAttribute("data-embed") === "true") return true;
if (window.matchMedia && window.matchMedia("print").matches) return true;
if (svg.hasAttribute("data-route-picking") || svg.hasAttribute("data-route-active"))
return true;
if (
svg.hasAttribute("data-lens-active") ||
svg.hasAttribute("data-legend-preview-active")
)
return true;
if (
svg.hasAttribute("data-relationship-preview-active") ||
svg.hasAttribute("data-intent-trace-active")
)
return true;
return activeIndex < 0 && svg.hasAttribute("data-focus-active");
}
function clearChapterPreviewPresentation() {
svg.removeAttribute("data-chapter-preview");
Array.prototype.forEach.call(
svg.querySelectorAll("[data-chapter-preview-role]"),
function (node) {
node.removeAttribute("data-chapter-preview-role");
},
);
chapterButtons.forEach(function (button) {
button.removeAttribute("data-preview-active");
});
activePreviewIndex = -1;
if (previewOwnerToken && Archify.motionGovernor) {
var token = previewOwnerToken;
previewOwnerToken = 0;
Archify.motionGovernor.release(token);
}
}
function clearChapterPreview(options) {
options = options || {};
previewGeneration += 1;
if (options.clearIntents !== false) {
pointerPreviewIntent = null;
focusPreviewIntent = null;
}
clearChapterPreviewPresentation();
return true;
}
function winningChapterPreviewIntent() {
return (
[pointerPreviewIntent, focusPreviewIntent]
.filter(function (intent) {
return (
intent &&
intent.index >= 0 &&
intent.index < views.length &&
intent.index !== activeIndex
);
})
.sort(function (left, right) {
return right.generation - left.generation;
})[0] || null
);
}
function showChapterPreview(index) {
if (
index < 0 ||
index >= views.length ||
index === activeIndex ||
chapterPreviewBlocked()
) {
clearChapterPreviewPresentation();
return false;
}
if (
activePreviewIndex === index &&
svg.getAttribute("data-chapter-preview") === views[index].id
)
return true;
clearChapterPreviewPresentation();
var delta = chapterDelta(activeIndex >= 0 ? views[activeIndex] : null, views[index]);
var roles = {};
delta.stay.forEach(function (id) {
roles[id] = "stay";
});
delta.enter.forEach(function (id) {
roles[id] = "enter";
});
delta.leave.forEach(function (id) {
roles[id] = "leave";
});
Array.prototype.forEach.call(svg.querySelectorAll("[data-node-id]"), function (node) {
var role = roles[node.getAttribute("data-node-id")];
if (role) node.setAttribute("data-chapter-preview-role", role);
});
svg.setAttribute("data-chapter-preview", views[index].id);
chapterButtons[index].setAttribute("data-preview-active", "true");
activePreviewIndex = index;
if (Archify.motionGovernor && Archify.motionGovernor.capable) {
previewOwnerToken = Archify.motionGovernor.claim("chapter-preview", function () {
previewOwnerToken = 0;
clearChapterPreviewPresentation();
});
}
return true;
}
function syncChapterPreview() {
var intent = winningChapterPreviewIntent();
if (!intent || chapterPreviewBlocked()) {
clearChapterPreviewPresentation();
return false;
}
return showChapterPreview(intent.index);
}
function setChapterPreviewIntent(source, index) {
var intent = { index: index, generation: ++previewGeneration };
if (source === "pointer") pointerPreviewIntent = intent;
else focusPreviewIntent = intent;
if (playing) pausePlayback();
return syncChapterPreview();
}
function clearChapterPreviewIntent(source, index) {
var intent = source === "pointer" ? pointerPreviewIntent : focusPreviewIntent;
if (intent && (typeof index !== "number" || intent.index === index)) {
if (source === "pointer") pointerPreviewIntent = null;
else focusPreviewIntent = null;
previewGeneration += 1;
}
return syncChapterPreview();
}
function hoverCapable() {
return !!(window.matchMedia && window.matchMedia("(hover: hover)").matches);
}
function sharePlaybackRequested() {
try {
return new URLSearchParams(location.search).get("play") === "1";
} catch (_) {
return false;
}
}
autoplayPending = sharePlaybackRequested();
if (autoplayPending) {
document.documentElement.setAttribute("data-share-playback", "true");
setAutoplayState("pending");
}
function setAutoplayState(state) {
if (state) panel.setAttribute("data-autoplay", state);
else panel.removeAttribute("data-autoplay");
renderShareCue();
}
function shareCueStatus(state) {
return (
{
pending: viewerText("viewer.guided.state.ready"),
playing: viewerText("viewer.guided.state.playing"),
complete: viewerText("viewer.guided.state.settled"),
interrupted: viewerText("viewer.guided.state.paused"),
pinned: viewerText("viewer.guided.state.pinned"),
"reduced-motion": viewerText("viewer.guided.state.still"),
}[state] || viewerText("viewer.guided.state.ready")
);
}
function shareCueBeatCopy(state, view, stops) {
var total = stops.length;
var activeStop = storyBeatIndex >= 0 ? stops[storyBeatIndex] : null;
if ((state === "playing" || state === "interrupted") && activeStop) {
return viewerText("viewer.guided.share.step", {
index: (storyBeatIndex + 1 < 10 ? "0" : "") + (storyBeatIndex + 1),
total: (total < 10 ? "0" : "") + total,
label: activeStop.textContent,
});
}
if (state === "pinned" && activeStop) {
return storyBeatCopy(storySteps[storyBeatIndex], total);
}
if (state === "reduced-motion" && activeStop && hashBeatMatchesCurrent()) {
return viewerText("viewer.guided.share.staticMoment", {
step: viewerText("viewer.guided.share.step", {
index: (storyBeatIndex + 1 < 10 ? "0" : "") + (storyBeatIndex + 1),
total: (total < 10 ? "0" : "") + total,
label: activeStop.textContent,
}),
});
}
if (state === "complete")
return viewerText("viewer.guided.share.complete", {
count: total,
note: view.note || viewerText("viewer.guided.share.settled"),
});
if (state === "reduced-motion")
return viewerText("viewer.guided.share.staticPath", { count: total });
if (state === "pending") return viewerText("viewer.guided.share.ready", { count: total });
return (
view.note ||
viewerText("viewer.guided.chapter.selectedNodes", { count: view.focus.length })
);
}
function renderShareCue() {
if (!shareCue) return;
var view = activeIndex >= 0 ? views[activeIndex] : null;
var shareMode = document.documentElement.getAttribute("data-share-playback") === "true";
var embedMode = document.documentElement.getAttribute("data-embed") === "true";
var pinnedMode = !shareMode && embedMode && hashBeatMatchesCurrent();
if (pinnedMode) document.documentElement.setAttribute("data-share-moment", "true");
else document.documentElement.removeAttribute("data-share-moment");
shareCue.hidden = !(embedMode && view && (shareMode || pinnedMode));
if (shareCue.hidden) return;
var state = pinnedMode ? "pinned" : panel.getAttribute("data-autoplay") || "pending";
var stops = Array.prototype.slice.call(trail.querySelectorAll("[data-story-node]"));
var route = stops
.map(function (stop, index) {
if (index === 0) return stop.textContent;
var kind = stop.getAttribute("data-story-link");
var separator =
kind === "forward"
? "\u2192"
: kind === "reverse"
? "\u2190"
: kind === "multiple"
? "\u21c4"
: "\u00b7";
return separator + " " + stop.textContent;
})
.join(" ");
var beatCopy = shareCueBeatCopy(state, view, stops);
shareCue.setAttribute("data-state", state);
shareCue.setAttribute("aria-live", state === "playing" ? "off" : "polite");
shareCueState.textContent = shareCueStatus(state);
shareCueCount.textContent = viewerText("viewer.guided.share.chapter", {
index: (activeIndex + 1 < 10 ? "0" : "") + (activeIndex + 1),
total: (views.length < 10 ? "0" : "") + views.length,
});
shareCueLabel.textContent = view.label;
shareCueNote.textContent = beatCopy;
shareCueRoute.textContent = route;
shareCue.setAttribute(
"aria-label",
viewerText("viewer.guided.share.aria", {
state: shareCueStatus(state),
index: activeIndex + 1,
total: views.length,
label: view.label,
beat: beatCopy,
route: route,
}),
);
}
function setShareCueProgress(fraction) {
if (!shareCueProgress) return;
shareCueProgress.style.animation = "none";
shareCueProgress.style.setProperty(
"--guided-progress-start",
String(Math.max(0, Math.min(1, fraction))),
);
shareCueProgress.style.transform = "scaleX(" + Math.max(0, Math.min(1, fraction)) + ")";
}
function startShareCueProgress(fraction, duration) {
if (!shareCueProgress) return;
setShareCueProgress(fraction || 0);
void shareCueProgress.offsetWidth;
shareCueProgress.style.animation =
"archify-guided-progress " +
Math.max(1, duration || VIEW_INTERVAL_MS) +
"ms linear forwards";
}
function storyNodeLabel(node, fallback) {
if (!node) return fallback;
return (
node.getAttribute("data-node-label") ||
(node.getAttribute("aria-label") || fallback).replace(/^Focus\s+/, "")
);
}
function storyEdgeKey(edge) {
return (
edge.getAttribute("data-edge-key") ||
edge.getAttribute("data-edge-from") +
"\u0000" +
edge.getAttribute("data-edge-to") +
"\u0000" +
(edge.getAttribute("data-edge-label") || "")
);
}
function uniqueStoryEdges(edgeList) {
var positions = {};
var unique = [];
edgeList.forEach(function (edge) {
var key = storyEdgeKey(edge);
if (!Object.prototype.hasOwnProperty.call(positions, key)) {
positions[key] = unique.length;
unique.push(edge);
return;
}
var index = positions[key];
if (!storyGeometry(unique[index]).length && storyGeometry(edge).length)
unique[index] = edge;
});
return unique;
}
function storyStep(view, index, edgeList, byId) {
var id = view.focus[index];
var node = byId[id];
var previousId = index > 0 ? view.focus[index - 1] : "";
var forward = [];
var reverse = [];
if (previousId) {
edgeList.forEach(function (edge) {
var from = edge.getAttribute("data-edge-from");
var to = edge.getAttribute("data-edge-to");
if (from === previousId && to === id) forward.push(edge);
else if (from === id && to === previousId) reverse.push(edge);
});
}
forward = uniqueStoryEdges(forward);
reverse = uniqueStoryEdges(reverse);
var edges = forward.concat(reverse).sort(function (left, right) {
return edgeList.indexOf(left) - edgeList.indexOf(right);
});
var relation =
index === 0
? "start"
: !edges.length
? "group"
: edges.length === 1 && forward.length === 1
? "forward"
: edges.length === 1 && reverse.length === 1
? "reverse"
: "multiple";
return {
index: index,
nodeId: id,
nodeLabel: storyNodeLabel(byId[id], id),
previousId: previousId,
previousLabel: previousId ? storyNodeLabel(byId[previousId], previousId) : "",
relation: relation,
edges: edges,
edgeKeys: edges.map(storyEdgeKey),
edgeLabels: edges
.map(function (edge) {
return edge.getAttribute("data-edge-label") || "";
})
.filter(function (value, edgeIndex, values) {
return value && values.indexOf(value) === edgeIndex;
}),
responsibility: node ? node.getAttribute("data-node-sublabel") || "" : "",
context: node ? node.getAttribute("data-node-context") || "" : "",
};
}
function storyBeatCopy(step, total) {
if (!step) return "";
var position = (step.index + 1 < 10 ? "0" : "") + (step.index + 1);
var countValue = (total < 10 ? "0" : "") + total;
if (step.relation === "start")
return viewerText("viewer.guided.beat.start", {
index: position,
total: countValue,
label: step.nodeLabel,
});
if (step.relation === "forward")
return viewerText("viewer.guided.beat.forward", {
index: position,
total: countValue,
from: step.previousLabel,
to: step.nodeLabel,
});
if (step.relation === "reverse")
return viewerText("viewer.guided.beat.reverse", {
index: position,
total: countValue,
from: step.nodeLabel,
to: step.previousLabel,
});
if (step.relation === "multiple")
return viewerText("viewer.guided.beat.multiple", {
index: position,
total: countValue,
from: step.previousLabel,
to: step.nodeLabel,
count: step.edges.length,
});
return viewerText("viewer.guided.beat.group", {
index: position,
total: countValue,
from: step.previousLabel,
to: step.nodeLabel,
});
}
function storyBeatAria(step, total) {
var prefix = viewerText("viewer.guided.beat.aria.prefix", {
index: step.index + 1,
total: total,
label: step.nodeLabel,
});
if (step.relation === "start")
return prefix + viewerText("viewer.guided.beat.aria.start");
if (step.relation === "forward")
return (
prefix + viewerText("viewer.guided.beat.aria.forward", { from: step.previousLabel })
);
if (step.relation === "reverse")
return (
prefix +
viewerText("viewer.guided.beat.aria.reverse", {
from: step.previousLabel,
to: step.nodeLabel,
})
);
if (step.relation === "multiple")
return (
prefix +
viewerText("viewer.guided.beat.aria.multiple", {
from: step.previousLabel,
count: step.edges.length,
})
);
return prefix + viewerText("viewer.guided.beat.aria.group", { from: step.previousLabel });
}
function storyCaptionRouteCopy(step) {
if (step.relation === "start")
return step.nodeLabel + " · " + viewerText("viewer.guided.caption.start");
if (step.relation === "reverse") return step.previousLabel + " ← " + step.nodeLabel;
if (step.relation === "multiple") return step.previousLabel + " ⇄ " + step.nodeLabel;
if (step.relation === "group") return step.previousLabel + " · " + step.nodeLabel;
return step.previousLabel + " → " + step.nodeLabel;
}
function storyCaptionDetailCopy(step) {
var facts = [];
if (step.relation === "group") facts.push(viewerText("viewer.guided.caption.grouped"));
else if (step.edgeLabels.length)
facts.push(
step.edgeLabels.slice(0, 3).join(" + ") +
(step.edgeLabels.length > 3
? viewerText("viewer.guided.caption.more", { count: step.edgeLabels.length - 3 })
: ""),
);
else if (step.relation === "reverse")
facts.push(viewerText("viewer.guided.caption.reverse"));
else if (step.relation === "multiple")
facts.push(
viewerText("viewer.guided.caption.relationships", { count: step.edges.length }),
);
else if (step.relation !== "start")
facts.push(viewerText("viewer.guided.caption.relationship"));
if (step.relation === "reverse")
facts.push(
viewerText("viewer.guided.caption.direction", {
from: step.nodeLabel,
to: step.previousLabel,
}),
);
if (step.responsibility) facts.push(step.responsibility);
if (step.context) facts.push(step.context);
if (!facts.length) facts.push(viewerText("viewer.guided.caption.starting"));
return facts.join(" · ");
}
function renderStoryCaption(step, total, nextStep) {
if (!storyCaption) return;
if (!step) {
storyCaption.hidden = true;
storyCaption.removeAttribute("data-story-caption");
storyCaptionNext.hidden = true;
storyCaptionNextLabel.textContent = "";
return;
}
storyCaption.hidden = false;
storyCaption.setAttribute("data-story-caption", step.relation);
storyCaption.setAttribute("aria-live", playing ? "off" : "polite");
storyCaptionIndex.textContent =
(step.index + 1 < 10 ? "0" : "") +
(step.index + 1) +
" / " +
(total < 10 ? "0" : "") +
total;
storyCaptionRoute.textContent = storyCaptionRouteCopy(step);
storyCaptionDetail.textContent = storyCaptionDetailCopy(step);
storyCaptionNext.hidden = !nextStep;
storyCaptionNextLabel.textContent = nextStep
? (nextStep.index + 1 < 10 ? "0" : "") +
(nextStep.index + 1) +
" · " +
nextStep.nodeLabel
: "";
storyCaption.style.animation = "none";
void storyCaption.offsetWidth;
storyCaption.style.removeProperty("animation");
}
function storyMomentLink() {
var view = activeIndex >= 0 ? views[activeIndex] : null;
var step = storyBeatIndex >= 0 ? storySteps[storyBeatIndex] : null;
if (!view || !step) return "";
var url = new URL(location.href);
url.searchParams.delete("play");
url.hash =
"view=" + encodeURIComponent(view.id) + "&beat=" + encodeURIComponent(step.nodeId);
return url.href;
}
function fallbackCopyStoryMoment(value) {
var field = document.createElement("textarea");
field.value = value;
field.setAttribute("readonly", "");
field.style.position = "fixed";
field.style.opacity = "0";
document.body.appendChild(field);
field.select();
var copied = false;
try {
copied = document.execCommand("copy");
} catch (_) {}
field.remove();
return copied;
}
function resetBeatLinkFeedback() {
if (beatLinkFeedbackTimer) clearTimeout(beatLinkFeedbackTimer);
beatLinkFeedbackTimer = null;
beatLink.removeAttribute("data-copy-state");
beatLinkLabel.textContent = viewerText("viewer.guided.copyMoment");
}
function syncBeatLink() {
var step = storyBeatIndex >= 0 ? storySteps[storyBeatIndex] : null;
var available = !!(activeIndex >= 0 && step && !currentHandoff);
beatLink.disabled = !available;
if (!beatLink.hasAttribute("data-copy-state"))
beatLinkLabel.textContent = viewerText("viewer.guided.copyMoment");
var description = available
? viewerText("viewer.guided.beatLink", {
index: step.index + 1,
total: storySteps.length,
label: step.nodeLabel,
})
: viewerText("viewer.guided.selectBeatLink");
beatLink.setAttribute("aria-label", description);
beatLink.title = description;
}
function setBeatLinkFeedback(copied) {
resetBeatLinkFeedback();
beatLink.setAttribute("data-copy-state", copied ? "copied" : "failed");
beatLinkLabel.textContent = viewerText(
copied ? "viewer.guided.copied" : "viewer.guided.copyFailed",
);
beatLink.setAttribute(
"aria-label",
viewerText(copied ? "viewer.guided.momentCopied" : "viewer.guided.momentCopyFailed"),
);
beatLinkFeedbackTimer = setTimeout(function () {
resetBeatLinkFeedback();
syncBeatLink();
}, 1600);
}
function copyStoryMomentLink() {
if (playing) pausePlayback();
var value = storyMomentLink();
if (!value) return Promise.resolve(false);
var copy =
navigator.clipboard && typeof navigator.clipboard.writeText === "function"
? navigator.clipboard
.writeText(value)
.then(function () {
return true;
})
.catch(function () {
return fallbackCopyStoryMoment(value);
})
: Promise.resolve(fallbackCopyStoryMoment(value));
return copy.then(function (copied) {
setBeatLinkFeedback(copied);
return copied;
});
}
function hashBeatMatchesCurrent() {
var view = activeIndex >= 0 ? views[activeIndex] : null;
var step = storyBeatIndex >= 0 ? storySteps[storyBeatIndex] : null;
if (!view || !step) return false;
try {
var params = new URLSearchParams(location.hash.replace(/^#/, ""));
return params.get("view") === view.id && params.get("beat") === step.nodeId;
} catch (_) {
return false;
}
}
function storyMotionAllowed() {
if (document.hidden || reducedMotion()) return false;
if (document.documentElement.getAttribute("data-motion") !== "live") return false;
if (
document.documentElement.getAttribute("data-embed") === "true" &&
document.documentElement.getAttribute("data-share-playback") !== "true"
)
return false;
if (window.matchMedia && window.matchMedia("print").matches) return false;
return !(Archify.motionGovernor && Archify.motionGovernor.isPaused());
}
function storyAutomaticPlaybackAllowed() {
if (document.hidden || reducedMotion()) return false;
if (window.matchMedia && window.matchMedia("print").matches) return false;
if (Archify.motionGovernor && Archify.motionGovernor.capable)
return !Archify.motionGovernor.isPaused();
return true;
}
function clearStoryPulse(options) {
options = options || {};
storyPulseGeneration += 1;
Array.prototype.forEach.call(
svg.querySelectorAll(".story-trail-flow[data-story-pulse]"),
function (flow) {
flow.removeAttribute("data-story-pulse");
},
);
Array.prototype.forEach.call(
svg.querySelectorAll("[data-story-carrier-overlay]"),
function (overlay) {
overlay.remove();
},
);
if (storyPulseOwnerToken && options.release !== false && Archify.motionGovernor) {
var token = storyPulseOwnerToken;
storyPulseOwnerToken = 0;
Archify.motionGovernor.release(token);
} else if (options.release === false) storyPulseOwnerToken = 0;
}
function pulseStoryStep(step) {
clearStoryPulse();
if (
!step ||
(step.relation !== "forward" && step.relation !== "reverse") ||
step.edges.length !== 1 ||
!storyMotionAllowed()
)
return false;
var flows = Array.prototype.slice.call(
svg.querySelectorAll('.story-trail-flow[data-story-beat-step="' + step.index + '"]'),
);
if (!flows.length) return false;
var pulseGeneration = storyPulseGeneration;
if (Archify.motionGovernor && Archify.motionGovernor.capable) {
storyPulseOwnerToken = Archify.motionGovernor.claim("story", function () {
clearStoryPulse({ release: false });
});
}
flows.forEach(function (flow) {
flow.setAttribute("data-story-pulse", "true");
});
if (Archify.flowTokens && typeof Archify.flowTokens.create === "function") {
var edge = step.edges[0];
var shapes = storyGeometry(edge);
var carrier = shapes.length
? Archify.flowTokens.create(edge, shapes[0], {
className: "story-flow-token",
duration: "0.78s",
})
: null;
if (carrier) {
var carrierOverlay = document.createElementNS("http://www.w3.org/2000/svg", "g");
var carrierWrapper = document.createElementNS("http://www.w3.org/2000/svg", "g");
carrierOverlay.setAttribute("class", "story-carrier-overlay");
carrierOverlay.setAttribute("data-story-carrier-overlay", "");
carrierOverlay.setAttribute("aria-hidden", "true");
if (edge.hasAttribute("transform"))
carrierWrapper.setAttribute("transform", edge.getAttribute("transform"));
carrier.setAttribute("data-story-carrier-token", "");
carrier.setAttribute("data-story-beat-step", String(step.index));
carrierWrapper.appendChild(carrier);
carrierOverlay.appendChild(carrierWrapper);
var firstNode = svg.querySelector("[data-node-id]");
while (firstNode && firstNode.parentNode !== svg) firstNode = firstNode.parentNode;
if (firstNode) svg.insertBefore(carrierOverlay, firstNode);
else svg.appendChild(carrierOverlay);
}
}
void flows[0].getBoundingClientRect();
flows[0].addEventListener(
"animationend",
function () {
if (pulseGeneration === storyPulseGeneration) clearStoryPulse();
},
{ once: true },
);
return true;
}
function stopStoryBeatTimer(options) {
options = options || {};
storyPlaybackGeneration += 1;
if (storyBeatTimer && options.preserveElapsed === true && storyBeatStartedAt) {
storyBeatElapsedMs = Math.min(
storyBeatDwellMs,
storyBeatElapsedMs + Math.max(0, Date.now() - storyBeatStartedAt),
);
}
if (storyBeatTimer) clearTimeout(storyBeatTimer);
storyBeatTimer = null;
storyBeatStartedAt = 0;
}
function centerStoryStop(stop) {
if (!stop || !trail) return false;
var target = Math.max(0, stop.offsetLeft - (trail.clientWidth - stop.offsetWidth) / 2);
trail.scrollLeft = target;
return true;
}
function storyBeatDwell(total) {
return Math.max(STORY_FOLLOW_MIN_DWELL_MS, VIEW_INTERVAL_MS / Math.max(1, total));
}
function clearStoryFollow() {
storyFollowGeneration += 1;
svg.removeAttribute("data-story-follow");
panel.removeAttribute("data-story-follow");
panel.removeAttribute("data-story-follow-node");
}
function storyFrameIds(step) {
if (!step) return [];
var ids = [];
if (step.index > 0 && storySteps[step.index - 1])
ids.push(storySteps[step.index - 1].nodeId);
ids.push(step.nodeId);
if (step.index + 1 < storySteps.length) ids.push(storySteps[step.index + 1].nodeId);
return ids;
}
function followStoryStep(step, options) {
options = options || {};
if (!step || document.hidden) return false;
if (!Archify.view || typeof Archify.view.reveal !== "function") {
var deferredIndex = step.index;
var deferredGeneration = ++storyFollowGeneration;
requestAnimationFrame(function () {
if (deferredGeneration !== storyFollowGeneration || storyBeatIndex !== deferredIndex)
return;
if (Archify.view && typeof Archify.view.reveal === "function")
followStoryStep(step, options);
});
return true;
}
if (window.matchMedia && window.matchMedia("print").matches) return false;
var embed = document.documentElement.getAttribute("data-embed") === "true";
var explicitEmbedPlayback =
document.documentElement.getAttribute("data-share-playback") === "true";
if (embed && !explicitEmbedPlayback && options.linked !== true) return false;
var ids = storyFrameIds(step);
var generation = ++storyFollowGeneration;
svg.setAttribute("data-story-follow", step.nodeId);
panel.setAttribute("data-story-follow", "moving");
panel.setAttribute("data-story-follow-node", step.nodeId);
var receipt = Archify.view.reveal(ids, {
reason: options.manual === true ? "story-beat" : "story-follow",
padding: 64,
maxScale: 1.65,
duration: STORY_FOLLOW_DURATION_MS,
instant:
options.instant === true ||
reducedMotion() ||
document.documentElement.getAttribute("data-motion") !== "live",
});
if (receipt && receipt.finished && typeof receipt.finished.then === "function") {
receipt.finished.then(function (result) {
if (generation !== storyFollowGeneration || storyBeatIndex !== step.index) return;
svg.removeAttribute("data-story-follow");
panel.setAttribute(
"data-story-follow",
result && result.state === "complete" ? "settled" : "interrupted",
);
});
} else if (generation === storyFollowGeneration) {
panel.setAttribute("data-story-follow", "settled");
}
return receipt;
}
function setStoryBeat(index, options) {
options = options || {};
var stops = Array.prototype.slice.call(trail.querySelectorAll("[data-story-node]"));
if (!stops.length) return false;
resetBeatLinkFeedback();
storyBeatIndex = Math.max(0, Math.min(stops.length - 1, index));
var nextStep =
storyBeatIndex + 1 < storySteps.length ? storySteps[storyBeatIndex + 1] : null;
svg.setAttribute("data-story-beat", storyBeatIndex + 1 + "/" + stops.length);
panel.setAttribute("data-story-beat", storyBeatIndex + 1 + "/" + stops.length);
if (nextStep) {
svg.setAttribute("data-story-next", nextStep.nodeId);
panel.setAttribute("data-story-next", nextStep.nodeId);
} else {
svg.removeAttribute("data-story-next");
panel.removeAttribute("data-story-next");
}
function storyBeatState(step) {
if (step < storyBeatIndex) return "past";
if (step === storyBeatIndex) return "active";
if (step === storyBeatIndex + 1) return "next";
return "pending";
}
Array.prototype.forEach.call(svg.querySelectorAll("[data-story-step]"), function (node) {
var step = Number(node.getAttribute("data-story-step"));
node.setAttribute("data-story-beat-state", storyBeatState(step));
});
Array.prototype.forEach.call(
svg.querySelectorAll("[data-story-beat-step]"),
function (edge) {
var step = Number(edge.getAttribute("data-story-beat-step"));
edge.setAttribute("data-story-beat-state", storyBeatState(step));
},
);
stops.forEach(function (stop, step) {
stop.setAttribute("data-story-beat-state", storyBeatState(step));
if (step === storyBeatIndex) stop.setAttribute("aria-current", "step");
else stop.removeAttribute("aria-current");
});
note.setAttribute("aria-live", "off");
note.textContent = storyBeatCopy(storySteps[storyBeatIndex], stops.length);
renderStoryCaption(storySteps[storyBeatIndex], stops.length, nextStep);
if (options.center === true) centerStoryStop(stops[storyBeatIndex]);
if (options.pulse === true) pulseStoryStep(storySteps[storyBeatIndex]);
else clearStoryPulse();
if (options.follow === true)
followStoryStep(storySteps[storyBeatIndex], {
manual: options.manual === true,
linked: options.linked === true,
instant: options.followInstant === true,
});
renderShareCue();
syncBeatLink();
return true;
}
function settleStoryBeats() {
stopStoryBeatTimer();
clearStoryPulse();
clearStoryFollow();
resetBeatLinkFeedback();
storyBeatIndex = -1;
storyBeatElapsedMs = 0;
storyBeatDwellMs = 0;
svg.removeAttribute("data-story-beat");
svg.removeAttribute("data-story-next");
panel.removeAttribute("data-story-beat");
panel.removeAttribute("data-story-next");
Array.prototype.forEach.call(
svg.querySelectorAll("[data-story-beat-state]"),
function (el) {
el.removeAttribute("data-story-beat-state");
},
);
Array.prototype.forEach.call(
trail.querySelectorAll("[data-story-beat-state]"),
function (stop) {
stop.removeAttribute("data-story-beat-state");
stop.removeAttribute("aria-current");
},
);
note.setAttribute("aria-live", "polite");
if (activeIndex >= 0)
note.textContent =
views[activeIndex].note ||
viewerText("viewer.guided.chapter.selectedNodes", {
count: views[activeIndex].focus.length,
});
renderStoryCaption(null, 0);
renderShareCue();
syncBeatLink();
}
function clearStoryTrail() {
stopStoryBeatTimer();
clearStoryPulse();
clearStoryFollow();
resetBeatLinkFeedback();
storyBeatIndex = -1;
storyBeatElapsedMs = 0;
storyBeatDwellMs = 0;
storySteps = [];
svg.removeAttribute("data-story-active");
svg.removeAttribute("data-story-playing");
svg.removeAttribute("data-story-beat");
svg.removeAttribute("data-story-next");
panel.removeAttribute("data-story-beat");
panel.removeAttribute("data-story-next");
Array.prototype.forEach.call(
svg.querySelectorAll("[data-story-overlay]"),
function (overlay) {
overlay.remove();
},
);
Array.prototype.forEach.call(
svg.querySelectorAll("[data-story-step], [data-story-beat-state]"),
function (node) {
node.removeAttribute("data-story-step");
node.removeAttribute("data-story-beat-state");
node.style.removeProperty("--story-step");
},
);
Array.prototype.forEach.call(
svg.querySelectorAll("[data-edge-from][data-story-beat-step]"),
function (edge) {
edge.removeAttribute("data-story-beat-step");
edge.removeAttribute("data-story-beat-state");
},
);
trail.textContent = "";
trail.hidden = true;
trail.removeAttribute("aria-label");
renderStoryCaption(null, 0);
syncBeatLink();
}
function storyGeometry(edge) {
if (/^(path|line|polyline)$/i.test(edge.tagName)) return [edge];
return Array.prototype.slice.call(edge.querySelectorAll("path, line, polyline"));
}
function renderStoryTrail(view) {
if (Archify.semanticLens && typeof Archify.semanticLens.clearPreview === "function")
Archify.semanticLens.clearPreview();
clearStoryTrail();
if (!view) return;
var nodeList = Array.prototype.slice.call(svg.querySelectorAll("[data-node-id]"));
var edgeList = Array.prototype.slice.call(
svg.querySelectorAll("[data-edge-from][data-edge-to]"),
);
var byId = {};
nodeList.forEach(function (node) {
byId[node.getAttribute("data-node-id")] = node;
});
storySteps = view.focus.map(function (_, index) {
return storyStep(view, index, edgeList, byId);
});
var labels = [];
storySteps.forEach(function (step, index) {
var id = step.nodeId;
var node = byId[id];
if (!node) return;
node.setAttribute("data-story-step", String(index));
node.style.setProperty("--story-step", String(index));
var nodeLabel = step.nodeLabel;
labels.push(nodeLabel);
var stop = document.createElement("button");
stop.type = "button";
stop.className = "guided-view-stop";
stop.setAttribute("data-story-node", id);
stop.setAttribute("data-story-index", String(index));
stop.setAttribute("data-story-relation", step.relation);
stop.setAttribute("data-story-number", (index + 1 < 10 ? "0" : "") + (index + 1));
if (index > 0) stop.setAttribute("data-story-link", step.relation);
stop.style.setProperty("--story-step", String(index));
stop.setAttribute("aria-label", storyBeatAria(step, storySteps.length));
stop.title = storyBeatCopy(step, storySteps.length);
stop.textContent = nodeLabel;
trail.appendChild(stop);
});
trail.hidden = labels.length === 0;
trail.setAttribute(
"aria-label",
viewerText("viewer.guided.storyTrail", { label: view.label, count: labels.length }),
);
trail.scrollLeft = 0;
svg.setAttribute("data-story-active", view.id);
var overlay = document.createElementNS("http://www.w3.org/2000/svg", "g");
overlay.setAttribute("class", "story-trail-overlay");
overlay.setAttribute("data-story-overlay", "");
overlay.setAttribute("aria-hidden", "true");
var copied = 0;
storySteps.forEach(function (step) {
step.edges.forEach(function (edge) {
var edgeBeat = step.index;
edge.setAttribute("data-story-beat-step", String(edgeBeat));
var wrapper = document.createElementNS("http://www.w3.org/2000/svg", "g");
if (edge.hasAttribute("transform"))
wrapper.setAttribute("transform", edge.getAttribute("transform"));
storyGeometry(edge).forEach(function (shape) {
var clone = shape.cloneNode(false);
clone.removeAttribute("id");
clone.removeAttribute("marker-start");
clone.removeAttribute("marker-mid");
clone.removeAttribute("marker-end");
clone.removeAttribute("aria-label");
clone.removeAttribute("role");
clone.removeAttribute("data-animate");
clone.removeAttribute("data-edge-from");
clone.removeAttribute("data-edge-to");
clone.removeAttribute("data-edge-key");
clone.removeAttribute("data-edge-id");
clone.removeAttribute("data-edge-label");
clone.setAttribute("class", "story-trail-flow");
clone.setAttribute("data-story-beat-step", String(edgeBeat));
clone.style.setProperty("--story-step", String(edgeBeat));
wrapper.appendChild(clone);
copied += 1;
});
if (wrapper.childNodes.length) overlay.appendChild(wrapper);
});
});
if (copied) {
var firstEdge = edgeList[0];
if (firstEdge && firstEdge.parentNode)
firstEdge.parentNode.insertBefore(overlay, firstEdge);
else svg.insertBefore(overlay, svg.firstChild);
}
}
function syncStoryPlayback() {
var shouldPlay = playing && activeIndex >= 0;
if (shouldPlay && svg.getAttribute("data-story-playing") !== "true")
svg.setAttribute("data-story-playing", "true");
else if (!shouldPlay && svg.hasAttribute("data-story-playing"))
svg.removeAttribute("data-story-playing");
}
function resetProgress(fraction) {
fraction = Math.max(0, Math.min(1, Number(fraction) || 0));
progressBar.style.animation = "none";
progressBar.style.setProperty("--guided-progress-start", String(fraction));
progressBar.style.transform = "scaleX(" + fraction + ")";
}
function startProgress(fraction, duration) {
resetProgress(fraction);
void progressBar.offsetWidth;
progressBar.style.animation =
"archify-guided-progress " +
Math.max(1, duration || VIEW_INTERVAL_MS) +
"ms linear forwards";
}
function currentStoryProgress() {
var total = storySteps.length;
if (!total || storyBeatIndex < 0) return 0;
var elapsed = storyBeatElapsedMs;
if (playing && storyBeatTimer && storyBeatStartedAt)
elapsed += Math.max(0, Date.now() - storyBeatStartedAt);
var dwell = storyBeatDwellMs || storyBeatDwell(total);
return Math.max(0, Math.min(1, (storyBeatIndex + Math.min(1, elapsed / dwell)) / total));
}
function renderPlayback() {
var automaticPlaybackAllowed = storyAutomaticPlaybackAllowed();
panel.setAttribute("data-playing", playing ? "true" : "false");
syncStoryPlayback();
play.disabled = !playing && !automaticPlaybackAllowed;
play.setAttribute("aria-pressed", playing ? "true" : "false");
play.setAttribute(
"aria-label",
viewerText(
playing
? "viewer.guided.pause"
: !automaticPlaybackAllowed
? "viewer.guided.motionUnavailable"
: storyPlaybackComplete
? "viewer.guided.replay"
: "viewer.guided.play",
),
);
play.title = viewerText(
playing
? "viewer.guided.pause.title"
: !automaticPlaybackAllowed
? "viewer.guided.enableMotion"
: storyPlaybackComplete
? "viewer.guided.replay.title"
: "viewer.guided.play.title",
);
playIcon.textContent = playing ? "\u2016" : "\u25b6";
playLabel.textContent = viewerText(
playing
? "viewer.guided.pauseStory"
: storyPlaybackComplete
? "viewer.guided.replayStory"
: "viewer.guided.playStory",
);
if (storyCaption && !storyCaption.hidden)
storyCaption.setAttribute("aria-live", playing ? "off" : "polite");
}
function render() {
var view = views[activeIndex];
count.textContent =
activeIndex < 0 ? "0 / " + views.length : activeIndex + 1 + " / " + views.length;
label.textContent = view ? view.label : viewerText("viewer.guided.explore");
note.setAttribute("aria-live", storyBeatIndex >= 0 ? "off" : "polite");
note.textContent =
storyBeatIndex >= 0
? storyBeatCopy(storySteps[storyBeatIndex], storySteps.length)
: view
? view.note ||
viewerText("viewer.guided.chapter.selectedNodes", { count: view.focus.length })
: viewerText("viewer.guided.intro");
prev.disabled = activeIndex < 0;
next.disabled = activeIndex >= views.length - 1;
all.disabled = activeIndex < 0;
panel.setAttribute("data-active-view", view ? view.id : "all");
syncChapterIndex();
renderShareCue();
renderPlayback();
syncBeatLink();
}
function pausePlayback(options) {
options = options || {};
var progress = options.complete === true ? 1 : currentStoryProgress();
if (panel.getAttribute("data-autoplay") === "playing") {
setShareCueProgress(progress);
setAutoplayState(options.complete === true ? "complete" : "interrupted");
}
stopStoryBeatTimer({ preserveElapsed: options.complete !== true });
clearStoryPulse();
clearStoryFollow();
playing = false;
storyPlaybackComplete = options.complete === true;
resetProgress(progress);
renderPlayback();
renderShareCue();
}
function finishStoryChapter() {
if (!playing) return false;
if (storyPlaybackScope === "chapter") {
pausePlayback({ complete: true });
return true;
}
if (activeIndex < views.length - 1) {
var destinationIndex = activeIndex + 1;
activate(destinationIndex, { playback: true });
afterHandoff(function () {
if (!playing || activeIndex !== destinationIndex) return;
storyBeatElapsedMs = 0;
setStoryBeat(0, { pulse: true, follow: true });
scheduleStoryPlayback();
});
return true;
}
pausePlayback({ complete: true });
return true;
}
function scheduleStoryPlayback() {
var total = storySteps.length;
if (!playing || !total || reducedMotion()) {
if (playing) pausePlayback();
return false;
}
storyBeatDwellMs = storyBeatDwell(total);
if (storyBeatIndex < 0) {
storyBeatElapsedMs = 0;
setStoryBeat(0, { pulse: true, follow: true });
}
storyBeatElapsedMs = Math.max(0, Math.min(storyBeatDwellMs, storyBeatElapsedMs));
var remainingDwell = Math.max(1, storyBeatDwellMs - storyBeatElapsedMs);
var progress = (storyBeatIndex + storyBeatElapsedMs / storyBeatDwellMs) / total;
var remainingChapter =
remainingDwell + Math.max(0, total - storyBeatIndex - 1) * storyBeatDwellMs;
startProgress(progress, remainingChapter);
if (panel.getAttribute("data-autoplay") === "playing")
startShareCueProgress(progress, remainingChapter);
var generation = ++storyPlaybackGeneration;
storyBeatStartedAt = Date.now();
storyBeatTimer = setTimeout(function () {
if (!playing || generation !== storyPlaybackGeneration) return;
storyBeatTimer = null;
storyBeatStartedAt = 0;
storyBeatElapsedMs = 0;
if (storyBeatIndex < total - 1) {
setStoryBeat(storyBeatIndex + 1, { pulse: true, follow: true });
scheduleStoryPlayback();
} else finishStoryChapter();
}, remainingDwell);
return true;
}
function startPlayback() {
if (!storyAutomaticPlaybackAllowed()) {
renderPlayback();
return false;
}
autoplayPending = false;
setAutoplayState(null);
setShareCueProgress(0);
document.documentElement.removeAttribute("data-share-playback");
storyPlaybackScope = "story";
if (storyPlaybackComplete) showAll({ updateUrl: false });
storyPlaybackComplete = false;
if (activeIndex < 0) activate(0, { playback: true });
playing = true;
renderPlayback();
afterHandoff(function () {
if (!playing) return;
if (storyBeatIndex >= 0) followStoryStep(storySteps[storyBeatIndex]);
scheduleStoryPlayback();
});
return true;
}
function startCurrentViewPlayback() {
if (document.hidden) return false;
autoplayPending = false;
document.documentElement.setAttribute("data-share-playback", "true");
storyPlaybackScope = "chapter";
storyPlaybackComplete = false;
if (activeIndex < 0) activate(0, { playback: true, updateUrl: false });
if (!storyAutomaticPlaybackAllowed()) {
var preserveMoment = hashBeatMatchesCurrent();
setShareCueProgress(
preserveMoment && storySteps.length ? (storyBeatIndex + 1) / storySteps.length : 1,
);
setAutoplayState("reduced-motion");
playing = false;
if (preserveMoment) {
clearStoryPulse();
renderShareCue();
} else settleStoryBeats();
renderPlayback();
return false;
}
playing = true;
setAutoplayState("playing");
renderPlayback();
afterHandoff(function () {
if (!playing) return;
if (storyBeatIndex >= 0) followStoryStep(storySteps[storyBeatIndex]);
scheduleStoryPlayback();
});
return true;
}
function maybeStartSharePlayback() {
if (!autoplayPending || document.hidden) return false;
return startCurrentViewPlayback();
}
function togglePlayback() {
return playing ? (pausePlayback(), false) : startPlayback();
}
function syncStoryControlsDisabled() {
Array.prototype.forEach.call(
trail.querySelectorAll("[data-story-node]"),
function (stop) {
stop.disabled = !!currentHandoff;
},
);
syncBeatLink();
}
function selectStoryBeat(index) {
if (currentHandoff || activeIndex < 0 || index < 0 || index >= storySteps.length)
return false;
momentRestoreGeneration += 1;
clearChapterPreview({ clearIntents: true });
if (playing) pausePlayback();
else {
stopStoryBeatTimer();
clearStoryPulse();
}
storyPlaybackComplete = false;
storyPlaybackScope = "story";
storyBeatElapsedMs = 0;
storyBeatDwellMs = storyBeatDwell(storySteps.length);
var view = views[activeIndex];
Archify.focus.setMany(view.focus, {
toggle: false,
updateUrl: false,
mode: "selection",
label: view.label,
hideChip: true,
});
setStoryBeat(index, { manual: true, center: true, pulse: true, follow: true });
resetProgress(index / storySteps.length);
renderPlayback();
return true;
}
function selectStoryBeatById(id, options) {
options = options || {};
var index = storySteps.findIndex(function (step) {
return step.nodeId === id;
});
if (index < 0) return false;
if (playing) pausePlayback();
else {
stopStoryBeatTimer();
clearStoryPulse();
}
storyPlaybackComplete = false;
storyPlaybackScope = "story";
storyBeatElapsedMs = 0;
storyBeatDwellMs = storyBeatDwell(storySteps.length);
setStoryBeat(index, {
manual: false,
center: true,
pulse: false,
follow: options.follow === true,
linked: options.linked === true,
followInstant: options.followInstant === true,
});
resetProgress(index / storySteps.length);
renderPlayback();
return true;
}
function updateUrl(view) {
try {
history.replaceState(
null,
"",
location.pathname +
location.search +
(view ? "#view=" + encodeURIComponent(view.id) : ""),
);
} catch (_) {}
}
function showAll(options) {
options = options || {};
if (options.restore !== true) momentRestoreGeneration += 1;
clearChapterPreview({ clearIntents: true });
if (playing && options.playback !== true) pausePlayback();
if (options.playback !== true) storyPlaybackComplete = false;
cancelHandoff("overview");
activeIndex = -1;
renderStoryTrail(null);
if (options.clearFocus !== false) Archify.focus.clear({ updateUrl: false });
if (
options.resetView !== false &&
Archify.view &&
typeof Archify.view.reset === "function"
) {
Archify.view.reset({ automatic: true });
}
render();
if (options.updateUrl !== false) updateUrl(null);
}
function activate(index, options) {
options = options || {};
if (options.restore !== true) momentRestoreGeneration += 1;
if (playing && options.playback !== true) pausePlayback();
if (options.playback !== true) storyPlaybackComplete = false;
if (index < 0) {
showAll(options);
return true;
}
if (index >= views.length) return false;
clearChapterPreview({ clearIntents: true });
var previousIndex = activeIndex;
var previous = previousIndex >= 0 ? views[previousIndex] : null;
var outgoingBeatIndex = storyBeatIndex;
cancelHandoff("replaced");
activeIndex = index;
var view = views[index];
Archify.focus.setMany(view.focus, {
toggle: false,
updateUrl: false,
mode: "selection",
label: view.label,
hideChip: true,
});
renderStoryTrail(view);
beginHandoff(
previousIndex,
index,
previous,
view,
outgoingBeatIndex,
options.playback === true ? "playback" : "guided",
);
syncStoryControlsDisabled();
render();
if (options.updateUrl !== false) updateUrl(view);
return true;
}
function activateById(id, options) {
var index = views.findIndex(function (view) {
return view.id === id;
});
return index >= 0 && activate(index, options);
}
prev.addEventListener("click", function () {
activate(activeIndex - 1);
});
next.addEventListener("click", function () {
activate(activeIndex + 1);
});
all.addEventListener("click", function () {
showAll();
});
play.addEventListener("click", togglePlayback);
beatLink.addEventListener("click", copyStoryMomentLink);
trail.addEventListener("focusin", function (event) {
if (!event.target.closest("[data-story-node]")) return;
if (playing) pausePlayback();
});
trail.addEventListener("click", function (event) {
var stop = event.target.closest("[data-story-node]");
if (!stop || stop.disabled) return;
selectStoryBeat(Number(stop.getAttribute("data-story-index")));
});
trail.addEventListener("keydown", function (event) {
if (event.key !== "Enter" && event.key !== " ") return;
var stop = event.target.closest("[data-story-node]");
if (!stop || stop.disabled) return;
event.preventDefault();
selectStoryBeat(Number(stop.getAttribute("data-story-index")));
});
chapterList.addEventListener("click", function (event) {
var button = event.target.closest("[data-guided-view-id]");
if (!button) return;
activateById(button.getAttribute("data-guided-view-id"));
});
chapterList.addEventListener("pointerover", function (event) {
var button = event.target.closest("[data-guided-view-id]");
if (!button || !hoverCapable() || event.pointerType === "touch") return;
if (event.relatedTarget && button.contains(event.relatedTarget)) return;
setChapterPreviewIntent("pointer", chapterButtons.indexOf(button));
});
chapterList.addEventListener("pointerout", function (event) {
var button = event.target.closest("[data-guided-view-id]");
if (!button || (event.relatedTarget && button.contains(event.relatedTarget))) return;
clearChapterPreviewIntent("pointer", chapterButtons.indexOf(button));
});
chapterIndex.addEventListener("focusin", function (event) {
var button = event.target.closest("[data-guided-view-id]");
if (!button) return;
setChapterPreviewIntent("focus", chapterButtons.indexOf(button));
});
chapterIndex.addEventListener("focusout", function (event) {
var button = event.target.closest("[data-guided-view-id]");
if (!button || (event.relatedTarget && button.contains(event.relatedTarget))) return;
clearChapterPreviewIntent("focus", chapterButtons.indexOf(button));
});
chapterList.addEventListener("keydown", function (event) {
var button = event.target.closest("[data-guided-view-id]");
var index = chapterButtons.indexOf(button);
if (index < 0) return;
var target = null;
if (event.key === "ArrowRight") target = Math.min(chapterButtons.length - 1, index + 1);
else if (event.key === "ArrowLeft") target = Math.max(0, index - 1);
else if (event.key === "Home") target = 0;
else if (event.key === "End") target = chapterButtons.length - 1;
if (target === null) return;
event.preventDefault();
focusChapterButton(target);
});
// A direct node exploration supersedes the curated view. Capture phase
// releases the view before the focus explorer writes its own deep link.
function releaseForNode(event) {
if (activeIndex >= 0 && event.target.closest("[data-node-id]")) {
pausePlayback();
showAll({ clearFocus: false, updateUrl: false });
}
}
svg.addEventListener("click", releaseForNode, true);
svg.addEventListener(
"keydown",
function (event) {
if (event.key === "Enter" || event.key === " ") releaseForNode(event);
},
true,
);
document.addEventListener(
"keydown",
function (event) {
if (event.metaKey || event.ctrlKey || event.altKey) return;
if (document.documentElement.getAttribute("data-guide-open") === "true") return;
var target = event.target;
if (
target &&
(target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
target.isContentEditable)
)
return;
if (event.key === "]") {
event.preventDefault();
if (activeIndex < views.length - 1) activate(activeIndex + 1);
} else if (event.key === "[") {
event.preventDefault();
if (activeIndex >= 0) activate(activeIndex - 1);
} else if (event.key.toLowerCase() === "p") {
event.preventDefault();
togglePlayback();
} else if (event.key === "Escape" && activePreviewIndex >= 0) {
event.preventDefault();
event.stopImmediatePropagation();
clearChapterPreview({ clearIntents: true });
} else if (event.key === "Escape" && activeIndex >= 0) {
event.preventDefault();
showAll();
}
},
true,
);
function syncViewFromHash() {
try {
var params = new URLSearchParams(location.hash.replace(/^#/, ""));
var initial = params.get("view");
var requestedBeat = params.get("beat");
var restoreGeneration = ++momentRestoreGeneration;
if (initial) {
var activated = activateById(initial, { updateUrl: false, restore: true });
if (!activated) {
showAll({ updateUrl: false, restore: true });
return;
}
if (requestedBeat)
afterHandoff(function () {
if (restoreGeneration !== momentRestoreGeneration) return;
var latest = new URLSearchParams(location.hash.replace(/^#/, ""));
if (latest.get("view") !== initial || latest.get("beat") !== requestedBeat)
return;
if (activeIndex < 0 || views[activeIndex].id !== initial) return;
selectStoryBeatById(requestedBeat, {
linked: true,
follow: true,
followInstant: true,
});
});
} else if (params.get("focus") || params.get("relation")) {
activeIndex = -1;
render();
} else {
showAll({ updateUrl: false, restore: true });
}
} catch (_) {
showAll({ updateUrl: false, restore: true });
}
}
window.addEventListener("hashchange", syncViewFromHash);
document.addEventListener("visibilitychange", function () {
if (document.hidden) {
clearChapterPreview({ clearIntents: true });
if (playing) pausePlayback();
else clearStoryPulse();
settleHandoff("hidden");
} else if (!document.hidden) maybeStartSharePlayback();
});
if (window.matchMedia) {
var guidedMotionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
var onGuidedMotionChange = function () {
if (guidedMotionQuery.matches) {
if (playing) pausePlayback();
else clearStoryPulse();
settleHandoff("reduced-motion");
}
};
if (typeof guidedMotionQuery.addEventListener === "function")
guidedMotionQuery.addEventListener("change", onGuidedMotionChange);
else if (typeof guidedMotionQuery.addListener === "function")
guidedMotionQuery.addListener(onGuidedMotionChange);
}
if (
document.documentElement.getAttribute("data-embed") !== "true" &&
typeof MutationObserver !== "undefined" &&
typeof Node !== "undefined" &&
svg instanceof Node &&
document.documentElement instanceof Node
) {
var chapterPreviewOwnerObserver = new MutationObserver(syncChapterPreview);
chapterPreviewOwnerObserver.observe(svg, {
attributes: true,
attributeFilter: [
"data-route-picking",
"data-route-active",
"data-lens-active",
"data-legend-preview-active",
"data-relationship-preview-active",
"data-intent-trace-active",
"data-focus-active",
],
});
var storyMotionObserver = new MutationObserver(function () {
if (document.documentElement.getAttribute("data-motion") !== "live") clearStoryPulse();
renderPlayback();
});
storyMotionObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-motion"],
});
}
window.addEventListener("beforeprint", function () {
clearChapterPreview({ clearIntents: true });
if (playing) pausePlayback();
else clearStoryPulse();
settleHandoff("print");
});
syncViewFromHash();
if (autoplayPending) {
requestAnimationFrame(function () {
requestAnimationFrame(maybeStartSharePlayback);
});
}
return {
count: views.length,
activate: activateById,
showAll: showAll,
play: startPlayback,
playCurrent: startCurrentViewPlayback,
pause: pausePlayback,
beatLink: storyMomentLink,
copyBeatLink: copyStoryMomentLink,
cancelHandoff: cancelHandoff,
settleHandoff: settleHandoff,
clearPreview: function () {
return clearChapterPreview({ clearIntents: true });
},
isPlaying: function () {
return playing;
},
handoff: function () {
return currentHandoff
? {
id: currentHandoff.id,
mode: currentHandoff.mode,
anchor: currentHandoff.anchor || null,
}
: null;
},
active: function () {
return activeIndex < 0 ? null : views[activeIndex].id;
},
preview: function () {
return activePreviewIndex < 0 ? null : views[activePreviewIndex].id;
},
delta: function (id) {
var index = views.findIndex(function (view) {
return view.id === id;
});
if (index < 0) return null;
var value = chapterDelta(activeIndex >= 0 ? views[activeIndex] : null, views[index]);
return {
stay: value.stay.slice(),
enter: value.enter.slice(),
leave: value.leave.slice(),
};
},
beat: function () {
var step = storyBeatIndex >= 0 ? storySteps[storyBeatIndex] : null;
return step
? {
index: step.index,
position: step.index + 1,
total: storySteps.length,
nodeId: step.nodeId,
relation: step.relation,
edgeKeys: step.edgeKeys.slice(),
}
: null;
},
focus: function () {
return activeIndex < 0 ? [] : views[activeIndex].focus.slice();
},
};
})();
/* ============================================================
Adaptive Reader Shell — one diagram across laptop and monitor.
The canonical SVG and viewBox never change. On ordinary desktop pages,
wide diagrams receive only enough outer width to use the available
height without pushing summary cards below the viewport. Mobile,
embed, presentation, print, and non-wide diagrams retain their own
established layout contracts.
============================================================ */
Archify.waitForStableLayout = function (options) {
options = options || {};
var maximumFrames = Math.max(1, Number(options.maximumFrames) || 240);
var fontsReady =
document.fonts && document.fonts.ready
? document.fonts.ready.catch(function () {})
: Promise.resolve();
return fontsReady.then(function () {
if (typeof options.schedule === "function") options.schedule();
return new Promise(function (resolve, reject) {
var previous = "";
var stableFrames = 0;
var sampledFrames = 0;
function sample() {
sampledFrames += 1;
if (typeof options.pending === "function" && options.pending()) {
previous = "";
stableFrames = 0;
} else {
var current = typeof options.snapshot === "function" ? options.snapshot() : "";
if (current === previous) stableFrames += 1;
else {
previous = current;
stableFrames = 0;
}
if (stableFrames >= 3) {
resolve({ stable: true, snapshot: current, sampledFrames: sampledFrames });
return;
}
}
if (sampledFrames >= maximumFrames) {
reject(
new Error(options.timeoutMessage || "Layout did not reach stable dimensions."),
);
return;
}
requestAnimationFrame(sample);
}
requestAnimationFrame(sample);
});
});
};
Archify.readerLayout = (function () {
var html = document.documentElement;
var body = document.body;
var shell = document.querySelector(".container");
var diagram = document.querySelector(".diagram-container");
var svg = diagram && diagram.querySelector(":scope > svg");
var header = shell && shell.querySelector(".header");
var guided = shell && shell.querySelector(".guided-views");
var cards = shell && shell.querySelector(".cards");
var viewBox = svg && svg.viewBox && svg.viewBox.baseVal;
var ratio = viewBox && viewBox.height > 0 ? viewBox.width / viewBox.height : 0;
var frame = 0;
var settleFrame = 0;
var lastWidth = 0;
var WIDE_RATIO = 1.55;
var MIN_DESKTOP_WIDTH = 1024;
var MIN_READER_WIDTH = 960;
var MAX_READER_WIDTH = 1920;
var SAFE_BOTTOM_GAP = 12;
if (diagram && ratio >= WIDE_RATIO) {
diagram.setAttribute("data-wide-diagram", "true");
html.setAttribute("data-diagram-shape", "wide");
}
function number(value) {
var parsed = parseFloat(value);
return Number.isFinite(parsed) ? parsed : 0;
}
function visible(element) {
return Boolean(
element && !element.hidden && window.getComputedStyle(element).display !== "none",
);
}
function outerHeight(element) {
if (!visible(element)) return 0;
var style = window.getComputedStyle(element);
return (
element.getBoundingClientRect().height +
number(style.marginTop) +
number(style.marginBottom)
);
}
function eligible() {
return Boolean(
shell &&
diagram &&
svg &&
ratio >= WIDE_RATIO &&
window.innerWidth >= MIN_DESKTOP_WIDTH &&
html.getAttribute("data-embed") !== "true" &&
html.getAttribute("data-present") !== "true" &&
(!window.matchMedia || !window.matchMedia("print").matches),
);
}
function clear() {
html.style.removeProperty("--archify-reader-width");
html.removeAttribute("data-reader-layout");
html.removeAttribute("data-reader-overflow");
lastWidth = 0;
}
function chromeMetrics() {
var bodyStyle = window.getComputedStyle(body);
var diagramStyle = window.getComputedStyle(diagram);
return {
bodyX: number(bodyStyle.paddingLeft) + number(bodyStyle.paddingRight),
bodyY: number(bodyStyle.paddingTop) + number(bodyStyle.paddingBottom),
diagramX:
number(diagramStyle.paddingLeft) +
number(diagramStyle.paddingRight) +
number(diagramStyle.borderLeftWidth) +
number(diagramStyle.borderRightWidth),
diagramY:
number(diagramStyle.paddingTop) +
number(diagramStyle.paddingBottom) +
number(diagramStyle.borderTopWidth) +
number(diagramStyle.borderBottomWidth),
};
}
function applyWidth(width) {
var rounded = Math.round(width);
if (Math.abs(rounded - lastWidth) < 1) return false;
lastWidth = rounded;
html.style.setProperty("--archify-reader-width", rounded + "px");
html.setAttribute("data-reader-layout", "adaptive");
return true;
}
function settleOverflow(minWidth) {
if (settleFrame) cancelAnimationFrame(settleFrame);
settleFrame = requestAnimationFrame(function () {
settleFrame = 0;
if (!eligible() || !lastWidth) return;
var overflow =
Math.max(document.documentElement.scrollHeight, document.body.scrollHeight) -
window.innerHeight;
if (overflow > 1 && lastWidth > minWidth) {
applyWidth(Math.max(minWidth, lastWidth - overflow * ratio - 4));
html.setAttribute("data-reader-overflow", "reduced");
} else if (overflow > 1) {
html.setAttribute("data-reader-overflow", "authored");
} else {
html.removeAttribute("data-reader-overflow");
}
});
}
function measure() {
frame = 0;
if (!eligible()) {
clear();
return null;
}
var chrome = chromeMetrics();
var viewportCap = Math.max(0, window.innerWidth - chrome.bodyX);
var minWidth = Math.min(MIN_READER_WIDTH, viewportCap);
var maxWidth = Math.min(MAX_READER_WIDTH, viewportCap);
var fixedHeight =
chrome.bodyY +
chrome.diagramY +
SAFE_BOTTOM_GAP +
outerHeight(header) +
outerHeight(guided) +
outerHeight(cards);
var availableSvgHeight = Math.max(1, window.innerHeight - fixedHeight);
var desiredWidth = availableSvgHeight * ratio + chrome.diagramX;
var width = Math.max(minWidth, Math.min(maxWidth, desiredWidth));
applyWidth(width);
settleOverflow(minWidth);
return {
ratio: ratio,
width: Math.round(width),
availableSvgHeight: Math.round(availableSvgHeight),
fixedHeight: Math.round(fixedHeight),
};
}
function schedule() {
if (frame) return;
frame = requestAnimationFrame(measure);
}
function stableSnapshot() {
var shellRect = shell ? shell.getBoundingClientRect() : { width: 0, height: 0 };
var diagramRect = diagram ? diagram.getBoundingClientRect() : { width: 0, height: 0 };
return [
lastWidth,
html.getAttribute("data-reader-layout") || "",
html.getAttribute("data-reader-overflow") || "",
Math.ceil(document.documentElement.scrollWidth),
Math.ceil(document.documentElement.scrollHeight),
Math.ceil(document.body.scrollWidth),
Math.ceil(document.body.scrollHeight),
Math.round(shellRect.width * 100) / 100,
Math.round(shellRect.height * 100) / 100,
Math.round(diagramRect.width * 100) / 100,
Math.round(diagramRect.height * 100) / 100,
].join("|");
}
function whenStable() {
return Archify.waitForStableLayout({
schedule: schedule,
pending: function () {
return Boolean(frame || settleFrame);
},
snapshot: stableSnapshot,
timeoutMessage: "Adaptive reader layout did not reach stable dimensions.",
});
}
window.addEventListener("resize", schedule, { passive: true });
window.addEventListener("load", schedule, { once: true });
if (document.fonts && document.fonts.ready)
document.fonts.ready.then(schedule).catch(function () {});
if (typeof ResizeObserver === "function") {
var resizeObserver = new ResizeObserver(schedule);
[header, guided, cards].forEach(function (element) {
if (element) resizeObserver.observe(element);
});
}
if (typeof MutationObserver === "function") {
var contentObserver = new MutationObserver(schedule);
if (guided)
contentObserver.observe(guided, { attributes: true, childList: true, subtree: true });
if (cards)
contentObserver.observe(cards, { attributes: true, childList: true, subtree: true });
contentObserver.observe(html, {
attributes: true,
attributeFilter: ["data-embed", "data-present"],
});
}
schedule();
return {
measure: measure,
schedule: schedule,
whenStable: whenStable,
active: function () {
return html.getAttribute("data-reader-layout") === "adaptive";
},
receipt: function () {
return { ratio: ratio, width: lastWidth };
},
};
})();
/* ============================================================
Viewer Chrome Layout — keep HTML controls clear of the canonical SVG
stage without changing authored geometry. The dock keeps its floating
appearance while a small bottom stage rail becomes part of the reader
budget whenever its zero-reserve position would enter the stage.
============================================================ */
Archify.viewerChromeLayout = (function () {
var html = document.documentElement;
var container = document.querySelector(".diagram-container");
var svg = container && container.querySelector(":scope > svg");
var nav = container && container.querySelector(".diagram-nav");
var legend = svg && svg.querySelector("[data-legend]");
var frame = 0;
var settleFrame = 0;
var reserve = 0;
var railLatched = false;
var probingBaseline = false;
var probePromise = null;
var baselineIntersectionArea = 0;
var baselineStageGap = null;
var restorableReserve = 0;
var probeFallbackReserve = 0;
var lastReceipt = null;
var SAFE_GAP = 10;
function visible(element) {
if (!element || element.hidden) return false;
var style = window.getComputedStyle(element);
return style.display !== "none" && style.visibility !== "hidden";
}
function usable(rect) {
return Boolean(rect && rect.width > 0 && rect.height > 0);
}
function intersectionArea(a, b) {
var width = Math.max(0, Math.min(a.right, b.right) - Math.max(a.left, b.left));
var height = Math.max(0, Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top));
return width * height;
}
function protectedStageRect() {
if (!svg) return null;
var rect = svg.getBoundingClientRect();
var transform = "";
try {
transform = window.getComputedStyle(svg).transform || "";
} catch (_) {}
var scaleX = 1;
var scaleY = 1;
var translateX = 0;
var translateY = 0;
var matrix = transform.match(/^matrix\(([^)]+)\)$/);
var matrix3d = transform.match(/^matrix3d\(([^)]+)\)$/);
if (matrix) {
var values = matrix[1].split(",").map(Number);
if (values.length === 6 && values.every(Number.isFinite)) {
scaleX = Math.abs(values[0]) || 1;
scaleY = Math.abs(values[3]) || 1;
translateX = values[4];
translateY = values[5];
}
} else if (matrix3d) {
var values3d = matrix3d[1].split(",").map(Number);
if (values3d.length === 16 && values3d.every(Number.isFinite)) {
scaleX = Math.abs(values3d[0]) || 1;
scaleY = Math.abs(values3d[5]) || 1;
translateX = values3d[12];
translateY = values3d[13];
}
}
var width = rect.width / scaleX;
var height = rect.height / scaleY;
var left = rect.left - translateX;
var top = rect.top - translateY;
return {
x: left,
y: top,
left: left,
top: top,
right: left + width,
bottom: top + height,
width: width,
height: height,
};
}
function eligible() {
return Boolean(
container &&
svg &&
nav &&
window.innerWidth > 720 &&
html.getAttribute("data-embed") !== "true" &&
(!window.matchMedia || !window.matchMedia("print").matches) &&
visible(nav),
);
}
function cameraAtBaseline() {
var scale = Number(svg && svg.getAttribute("data-view-scale"));
return !Number.isFinite(scale) || Math.abs(scale - 1) < 0.001;
}
function writeReserve(next, options) {
options = options || {};
next = Math.max(0, Math.ceil(next));
if (Math.abs(next - reserve) < 1) return false;
reserve = next;
if (reserve) {
if (options.remember !== false) restorableReserve = reserve;
container.style.setProperty("--archify-nav-reserve", reserve + "px");
container.setAttribute("data-nav-stage-rail", "true");
html.setAttribute("data-nav-stage-rail", "true");
} else {
container.style.removeProperty("--archify-nav-reserve");
container.removeAttribute("data-nav-stage-rail");
html.removeAttribute("data-nav-stage-rail");
}
if (Archify.readerLayout && typeof Archify.readerLayout.schedule === "function") {
Archify.readerLayout.schedule();
}
if (options.quiet !== true) {
if (settleFrame) cancelAnimationFrame(settleFrame);
settleFrame = requestAnimationFrame(function () {
settleFrame = 0;
schedule();
});
}
return true;
}
function clear(options) {
options = options || {};
railLatched = false;
if (options.preserveBaseline !== true) {
baselineIntersectionArea = 0;
baselineStageGap = null;
restorableReserve = 0;
}
writeReserve(0, { quiet: probingBaseline });
lastReceipt = {
eligible: false,
active: false,
reserve: 0,
gap: SAFE_GAP,
baselineStageGap: null,
stageGap: null,
stageIntersectionArea: 0,
baselineIntersectionArea: 0,
intersectionArea: 0,
};
return lastReceipt;
}
function measure() {
frame = 0;
if (probingBaseline) return null;
if (!eligible()) return clear({ preserveBaseline: !cameraAtBaseline() });
/* Camera transforms enlarge and translate authored paint inside the
fixed, clipped root SVG viewport. They must not redefine the
reader's baseline rail.
Restore that baseline after temporary mobile/embed/print states.
Camera Reset preserves the established rail while viewport, mode,
and content changes remain responsible for baseline reprobes. */
if (!cameraAtBaseline()) {
if (reserve === 0 && restorableReserve > 0) {
if (writeReserve(restorableReserve)) return null;
}
var cameraNavRect = nav.getBoundingClientRect();
var cameraLegendRect = visible(legend) ? legend.getBoundingClientRect() : null;
var cameraStageRect = protectedStageRect();
var cameraIntersectionArea =
usable(cameraLegendRect) && intersectionArea(cameraNavRect, cameraStageRect) > 0
? intersectionArea(cameraNavRect, cameraLegendRect)
: 0;
lastReceipt = {
eligible: true,
active: reserve > 0,
reserve: reserve,
gap: SAFE_GAP,
baselineStageGap:
baselineStageGap == null ? null : Math.round(baselineStageGap * 100) / 100,
stageGap: Math.round((cameraNavRect.top - cameraStageRect.bottom) * 100) / 100,
stageIntersectionArea:
Math.round(intersectionArea(cameraNavRect, cameraStageRect) * 100) / 100,
baselineIntersectionArea: Math.round(baselineIntersectionArea * 100) / 100,
intersectionArea: Math.round(cameraIntersectionArea * 100) / 100,
};
return lastReceipt;
}
var navRect = nav.getBoundingClientRect();
var legendRect = visible(legend) ? legend.getBoundingClientRect() : null;
var stageRect = protectedStageRect();
if (!usable(navRect) || !usable(stageRect)) return clear();
var actualIntersectionArea = usable(legendRect)
? intersectionArea(navRect, legendRect)
: 0;
var stageGap = navRect.top - stageRect.bottom;
if (!railLatched && reserve === 0) {
baselineIntersectionArea = actualIntersectionArea;
baselineStageGap = stageGap;
if (stageGap < SAFE_GAP) {
railLatched = true;
if (writeReserve(Math.max(0, SAFE_GAP - stageGap))) return null;
}
} else if (railLatched && reserve > 0 && stageGap < SAFE_GAP) {
/* Keep the decision latched while Adaptive Reader incorporates the
rail. Presentation and rounded layout values can require one
bounded follow-up before the full stage gap is established. */
var remaining = Math.max(1, SAFE_GAP - stageGap);
if (writeReserve(reserve + remaining)) return null;
}
navRect = nav.getBoundingClientRect();
legendRect = visible(legend) ? legend.getBoundingClientRect() : null;
stageRect = protectedStageRect();
stageGap = navRect.top - stageRect.bottom;
lastReceipt = {
eligible: true,
active: reserve > 0,
reserve: reserve,
gap: SAFE_GAP,
baselineStageGap:
baselineStageGap == null ? null : Math.round(baselineStageGap * 100) / 100,
stageGap: Math.round(stageGap * 100) / 100,
stageIntersectionArea: Math.round(intersectionArea(navRect, stageRect) * 100) / 100,
baselineIntersectionArea: Math.round(baselineIntersectionArea * 100) / 100,
intersectionArea:
Math.round((usable(legendRect) ? intersectionArea(navRect, legendRect) : 0) * 100) /
100,
};
return lastReceipt;
}
function reprobe() {
if (probingBaseline) return probePromise || Promise.resolve(false);
if (!cameraAtBaseline()) {
schedule();
return Promise.resolve(false);
}
if (!reserve && !railLatched && !restorableReserve) {
schedule();
return Promise.resolve(false);
}
probeFallbackReserve = restorableReserve || reserve;
probingBaseline = true;
railLatched = false;
baselineIntersectionArea = 0;
baselineStageGap = null;
restorableReserve = 0;
writeReserve(0, { quiet: true });
var readerReady =
Archify.readerLayout && typeof Archify.readerLayout.whenStable === "function"
? Archify.readerLayout.whenStable().catch(function () {})
: Promise.resolve();
probePromise = readerReady.then(function () {
probingBaseline = false;
probePromise = null;
if (!cameraAtBaseline() && probeFallbackReserve > 0) {
restorableReserve = probeFallbackReserve;
}
probeFallbackReserve = 0;
schedule();
return true;
});
return probePromise;
}
function schedule() {
if (frame) return;
frame = requestAnimationFrame(measure);
}
function stableSnapshot() {
var containerRect = container
? container.getBoundingClientRect()
: { width: 0, height: 0 };
var navRect = nav ? nav.getBoundingClientRect() : { top: 0, left: 0 };
var legendRect = legend ? legend.getBoundingClientRect() : { top: 0, left: 0 };
return [
reserve,
Math.round(containerRect.width * 100) / 100,
Math.round(containerRect.height * 100) / 100,
Math.round(navRect.left * 100) / 100,
Math.round(navRect.top * 100) / 100,
Math.round(legendRect.left * 100) / 100,
Math.round(legendRect.top * 100) / 100,
railLatched ? "latched" : "clear",
lastReceipt ? lastReceipt.stageGap : "",
].join("|");
}
function whenStable() {
return Archify.waitForStableLayout({
schedule: schedule,
pending: function () {
return Boolean(frame || settleFrame || probingBaseline);
},
snapshot: stableSnapshot,
timeoutMessage: "Viewer chrome layout did not reach stable dimensions.",
});
}
window.addEventListener("resize", reprobe, { passive: true });
window.addEventListener("load", schedule, { once: true });
window.addEventListener("beforeprint", schedule);
window.addEventListener("afterprint", reprobe);
if (document.fonts && document.fonts.ready)
document.fonts.ready.then(reprobe).catch(function () {});
if (typeof ResizeObserver === "function") {
var resizeObserver = new ResizeObserver(schedule);
[nav, svg, legend].forEach(function (element) {
if (element) resizeObserver.observe(element);
});
}
if (typeof MutationObserver === "function") {
var contentObserver = new MutationObserver(function (records) {
var viewerModeChanged = records.some(function (record) {
return record.target === html;
});
if (viewerModeChanged) reprobe();
else schedule();
});
if (legend)
contentObserver.observe(legend, { attributes: true, childList: true, subtree: true });
contentObserver.observe(html, {
attributes: true,
attributeFilter: ["data-embed", "data-present", "data-preset", "data-theme"],
});
}
schedule();
return {
measure: measure,
schedule: schedule,
reprobe: reprobe,
whenStable: whenStable,
stageRect: protectedStageRect,
active: function () {
return reserve > 0;
},
receipt: function () {
return lastReceipt || measure();
},
};
})();
Archify.view = (function () {
var container = document.querySelector(".diagram-container");
var svg = container.querySelector("svg");
var outBtn = container.querySelector('[data-view="out"]');
var resetBtn = container.querySelector('[data-view="reset"]');
var resetDetailLabel = resetBtn.querySelector("[data-view-detail]");
var resetPercentLabel = resetBtn.querySelector("[data-view-percent]");
var inBtn = container.querySelector('[data-view="in"]');
var state = { scale: 1, x: 0, y: 0, mode: "overview" };
var drag = null;
var cameraTimer = null;
var cameraFrame = null;
var cameraGeneration = 0;
var cameraTransaction = null;
var clipFrame = 0;
var resizeFrame = 0;
var autoScrollUntil = 0;
var viewBox = svg.viewBox && svg.viewBox.baseVal;
function clamp() {
var width = svg.clientWidth || 1;
var height = svg.clientHeight || 1;
state.x = Math.min(0, Math.max(width - width * state.scale, state.x));
state.y = Math.min(0, Math.max(height - height * state.scale, state.y));
}
function reducedMotion() {
return window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}
function contentMetrics() {
if (!viewBox || viewBox.width <= 0 || viewBox.height <= 0) return null;
var width = svg.clientWidth || 1;
var height = svg.clientHeight || 1;
var scale = Math.min(width / viewBox.width, height / viewBox.height);
return {
width: width,
height: height,
scale: scale,
offsetX: (width - viewBox.width * scale) / 2,
offsetY: (height - viewBox.height * scale) / 2,
};
}
function logicalViewport() {
var metrics = contentMetrics();
if (!metrics) return null;
var x;
var y;
var width;
var height;
if (window.innerWidth <= 720 && container.hasAttribute("data-wide-diagram")) {
x = viewBox.x + container.scrollLeft / metrics.scale;
y = viewBox.y;
width = Math.min(viewBox.width, Math.max(1, container.clientWidth / metrics.scale));
height = viewBox.height;
} else {
x = viewBox.x + (-state.x / state.scale - metrics.offsetX) / metrics.scale;
y = viewBox.y + (-state.y / state.scale - metrics.offsetY) / metrics.scale;
width = Math.min(viewBox.width, metrics.width / state.scale / metrics.scale);
height = Math.min(viewBox.height, metrics.height / state.scale / metrics.scale);
}
width = Math.max(1, Math.min(viewBox.width, width));
height = Math.max(1, Math.min(viewBox.height, height));
x = Math.max(viewBox.x, Math.min(viewBox.x + viewBox.width - width, x));
y = Math.max(viewBox.y, Math.min(viewBox.y + viewBox.height - height, y));
return { x: x, y: y, width: width, height: height, scale: state.scale };
}
function detailLevel() {
if (state.mode === "semantic") return "full";
if (state.scale >= 1.75) return "full";
if (state.scale >= 1) return "read";
return "map";
}
function renderControls() {
var semantic = state.mode === "semantic" && state.scale > 1.01;
var detail = detailLevel();
var percent = Math.round(state.scale * 100) + "%";
var levelLabel = viewerText("viewer.nav.level." + detail);
var detailHint =
detail === "map"
? viewerText("viewer.nav.detail.map")
: detail === "read"
? viewerText("viewer.nav.detail.read")
: viewerText("viewer.nav.detail.full");
var resolvedLevel = semantic ? viewerText("viewer.nav.level.auto") : levelLabel;
var showDetailLevel = semantic || detail !== "read";
if (resetDetailLabel) {
resetDetailLabel.textContent = resolvedLevel;
resetDetailLabel.hidden = !showDetailLevel;
}
if (resetPercentLabel) resetPercentLabel.textContent = percent;
resetBtn.toggleAttribute("data-detail-visible", showDetailLevel);
resetBtn.title = viewerText("viewer.nav.camera.title", {
semantic: semantic ? viewerText("viewer.nav.camera.semantic") : "",
hint: detailHint,
});
resetBtn.setAttribute(
"aria-label",
viewerText("viewer.nav.camera", { hint: detailHint }),
);
resetBtn.setAttribute("data-detail-level", detail);
container.setAttribute("data-detail-level", detail);
container.setAttribute("data-camera-mode", state.mode);
container.setAttribute("data-camera-indicator", semantic ? "true" : "false");
}
function clipToViewport(camera) {
camera = camera || state;
if (camera.scale <= 1.001) {
svg.style.removeProperty("clip-path");
return;
}
var width = svg.clientWidth || 1;
var height = svg.clientHeight || 1;
var scale = camera.scale;
var top = Math.max(0, Math.min(height, -camera.y / scale));
var left = Math.max(0, Math.min(width, -camera.x / scale));
var right = Math.max(0, Math.min(width, width - (width - camera.x) / scale));
var bottom = Math.max(0, Math.min(height, height - (height - camera.y) / scale));
svg.style.clipPath =
"inset(" +
[top, right, bottom, left]
.map(function (value) {
return Math.round(value * 1000) / 1000 + "px";
})
.join(" ") +
")";
}
function cameraSettled(rendered) {
return (
Math.abs(rendered.scale - state.scale) < 0.001 &&
Math.abs(rendered.x - state.x) < 0.05 &&
Math.abs(rendered.y - state.y) < 0.05
);
}
function syncViewportClip() {
if (clipFrame) cancelAnimationFrame(clipFrame);
clipFrame = 0;
function sample() {
clipFrame = 0;
var rendered = sampleRenderedState();
clipToViewport(rendered);
if (!cameraSettled(rendered)) clipFrame = requestAnimationFrame(sample);
}
sample();
}
function apply() {
clamp();
svg.style.transform =
"translate(" + state.x + "px," + state.y + "px) scale(" + state.scale + ")";
syncViewportClip();
renderControls();
outBtn.disabled = state.scale <= 1;
inBtn.disabled = state.scale >= 3;
container.classList.toggle("is-pannable", state.scale > 1);
svg.setAttribute("data-view-scale", String(state.scale));
if (Archify.radar && typeof Archify.radar.sync === "function") Archify.radar.sync();
if (
Archify.viewerChromeLayout &&
typeof Archify.viewerChromeLayout.schedule === "function"
) {
Archify.viewerChromeLayout.schedule();
}
}
function sampleRenderedState() {
var transform = "";
try {
transform = getComputedStyle(svg).transform || "";
} catch (_) {}
var match = transform.match(/^matrix\(([^)]+)\)$/);
if (!match) return { scale: state.scale, x: state.x, y: state.y, mode: state.mode };
var values = match[1].split(",").map(Number);
if (values.length !== 6 || !values.every(Number.isFinite)) {
return { scale: state.scale, x: state.x, y: state.y, mode: state.mode };
}
return { scale: values[0], x: values[4], y: values[5], mode: state.mode };
}
function finishCameraTransaction(transaction, outcome) {
if (!transaction || transaction.settled) return false;
transaction.settled = true;
transaction.state = outcome || "complete";
if (transaction.frame) cancelAnimationFrame(transaction.frame);
if (transaction.timer) clearTimeout(transaction.timer);
transaction.frame = null;
transaction.timer = null;
if (cameraTransaction === transaction) cameraTransaction = null;
cameraFrame = null;
cameraTimer = null;
container.classList.remove("is-camera-moving");
container.classList.remove("is-camera-transaction");
container.removeAttribute("data-camera-transaction");
if (Archify.focus && Archify.focus.reposition) Archify.focus.reposition();
transaction.resolve({ id: transaction.id, state: transaction.state });
return true;
}
function cameraReceipt(target, options) {
var resolver;
var transaction = {
id: ++cameraGeneration,
state: "running",
target: target,
settled: false,
frame: null,
timer: null,
finished: new Promise(function (resolve) {
resolver = resolve;
}),
resolve: resolver,
cancel: function (reason, commitTarget) {
if (transaction.settled) return false;
if (commitTarget && transaction.target) {
if (Object.prototype.hasOwnProperty.call(transaction.target, "scrollLeft")) {
container.scrollLeft = transaction.target.scrollLeft;
} else {
state = {
scale: transaction.target.scale,
x: transaction.target.x,
y: transaction.target.y,
mode: transaction.target.mode,
};
apply();
}
}
return finishCameraTransaction(transaction, reason || "cancelled");
},
};
return transaction;
}
function stopCameraMotion(reason, commitTarget) {
if (cameraTransaction && !cameraTransaction.settled) {
cameraTransaction.cancel(reason || "cancelled", commitTarget === true);
return;
}
if (cameraTimer) clearTimeout(cameraTimer);
if (cameraFrame) cancelAnimationFrame(cameraFrame);
cameraTimer = null;
cameraFrame = null;
container.classList.remove("is-camera-moving");
container.classList.remove("is-camera-transaction");
container.removeAttribute("data-camera-transaction");
}
function interruptCamera(reason) {
if (Archify.guidedViews && Archify.guidedViews.cancelHandoff) {
Archify.guidedViews.cancelHandoff(reason || "manual");
}
var rendered = sampleRenderedState();
stopCameraMotion(reason || "manual", false);
state = rendered;
state.mode = "manual";
apply();
renderControls();
if (
Archify.guidedViews &&
Archify.guidedViews.isPlaying &&
Archify.guidedViews.isPlaying()
) {
Archify.guidedViews.pause();
}
if (
Archify.routeProbe &&
Archify.routeProbe.isJourneyPlaying &&
Archify.routeProbe.isJourneyPlaying()
) {
Archify.routeProbe.pauseJourney({ preserveElapsed: true, reason: reason || "manual" });
}
}
function zoom(next, options) {
options = options || {};
if (options.manual !== false) interruptCamera();
var previous = state.scale;
next = Math.max(1, Math.min(3, Math.round(next * 4) / 4));
if (next === previous) return;
var centerX = (svg.clientWidth || 1) / 2;
var centerY = (svg.clientHeight || 1) / 2;
var contentX = (centerX - state.x) / previous;
var contentY = (centerY - state.y) / previous;
state.scale = next;
state.x = centerX - contentX * next;
state.y = centerY - contentY * next;
apply();
}
function reset(options) {
options = options || {};
if (options.automatic !== true) interruptCamera();
else stopCameraMotion("reset", false);
state = { scale: 1, x: 0, y: 0, mode: "overview" };
apply();
}
function centerAt(logicalX, logicalY, options) {
options = options || {};
logicalX = Number(logicalX);
logicalY = Number(logicalY);
var metrics = contentMetrics();
if (!metrics || !Number.isFinite(logicalX) || !Number.isFinite(logicalY)) return false;
interruptCamera();
if (window.innerWidth <= 720 && container.hasAttribute("data-wide-diagram")) {
state.scale = 1;
state.x = 0;
state.y = 0;
state.mode = "manual";
apply();
var mobileTarget = (logicalX - viewBox.x) * metrics.scale - container.clientWidth / 2;
mobileTarget = Math.max(
0,
Math.min(svg.clientWidth - container.clientWidth, mobileTarget),
);
autoScrollUntil = Date.now() + 80;
try {
container.scrollTo({
left: mobileTarget,
behavior: options.instant ? "auto" : "smooth",
});
} catch (_) {
container.scrollLeft = mobileTarget;
}
return true;
}
var minimumScale = Math.max(1, Math.min(3, Number(options.minimumScale) || 1));
var requestedScale = Number(options.scale);
state.scale = Math.max(
minimumScale,
Math.min(3, Number.isFinite(requestedScale) ? requestedScale : state.scale),
);
var contentX = metrics.offsetX + (logicalX - viewBox.x) * metrics.scale;
var contentY = metrics.offsetY + (logicalY - viewBox.y) * metrics.scale;
state.x = metrics.width / 2 - contentX * state.scale;
state.y = metrics.height / 2 - contentY * state.scale;
state.mode = "manual";
apply();
if (Archify.focus && Archify.focus.reposition) Archify.focus.reposition();
return true;
}
function semanticIds(ids, includeNeighbors) {
var seeds = {};
var wanted = {};
(ids || []).forEach(function (id) {
seeds[id] = true;
wanted[id] = true;
});
if (includeNeighbors) {
Array.prototype.forEach.call(
svg.querySelectorAll("[data-edge-from][data-edge-to]"),
function (edge) {
var from = edge.getAttribute("data-edge-from");
var to = edge.getAttribute("data-edge-to");
if (seeds[from] || seeds[to]) {
wanted[from] = true;
wanted[to] = true;
}
},
);
}
return wanted;
}
function boxesFor(ids, includeNeighbors) {
var wanted = semanticIds(ids, includeNeighbors);
return Array.prototype.slice
.call(svg.querySelectorAll("[data-node-id]"))
.filter(function (node) {
return wanted[node.getAttribute("data-node-id")];
})
.map(function (node) {
try {
return node.getBBox();
} catch (_) {
return null;
}
})
.filter(Boolean);
}
function frameDesktop(ids, options) {
options = options || {};
var boxes = boxesFor(ids, options.includeNeighbors === true);
if (!boxes.length || !viewBox || viewBox.width <= 0 || viewBox.height <= 0) return false;
var svgWidth = svg.clientWidth || 1;
var svgHeight = svg.clientHeight || 1;
var contentScale = Math.min(svgWidth / viewBox.width, svgHeight / viewBox.height);
var contentOffsetX = (svgWidth - viewBox.width * contentScale) / 2;
var contentOffsetY = (svgHeight - viewBox.height * contentScale) / 2;
var minX = Math.min.apply(
Math,
boxes.map(function (box) {
return box.x;
}),
);
var minY = Math.min.apply(
Math,
boxes.map(function (box) {
return box.y;
}),
);
var maxX = Math.max.apply(
Math,
boxes.map(function (box) {
return box.x + box.width;
}),
);
var maxY = Math.max.apply(
Math,
boxes.map(function (box) {
return box.y + box.height;
}),
);
var bounds = {
x: contentOffsetX + minX * contentScale,
y: contentOffsetY + minY * contentScale,
width: Math.max(1, (maxX - minX) * contentScale),
height: Math.max(1, (maxY - minY) * contentScale),
};
var padding = options.padding || 48;
var left = padding;
var right = svgWidth - padding;
var top = padding;
var bottom = svgHeight - Math.max(padding, 72);
var containerRect = container.getBoundingClientRect();
var visibleTop = Math.max(0, -containerRect.top);
var visibleBottom = Math.min(svgHeight, window.innerHeight - containerRect.top);
if (visibleBottom - visibleTop >= 240) {
top = Math.max(top, visibleTop + padding);
bottom = Math.min(bottom, visibleBottom - Math.max(padding, 72));
}
var chip = document.getElementById("focus-chip");
if (chip && !chip.hidden) {
var lensEnd = chip.offsetLeft + chip.offsetWidth + 24 - (svg.offsetLeft || 0);
left = Math.max(left, Math.min(svgWidth * 0.42, lensEnd));
}
var routeReceipt = document.getElementById("route-probe");
if (
routeReceipt &&
!routeReceipt.hidden &&
routeReceipt.hasAttribute("data-route-journey")
) {
var receiptTop = routeReceipt.offsetTop;
var receiptBottom = receiptTop + routeReceipt.offsetHeight;
if (receiptTop < svgHeight / 2) top = Math.max(top, receiptBottom + 24);
else bottom = Math.min(bottom, receiptTop - 24);
}
if (right <= left || bottom <= top) return false;
var maxScale = options.maxScale || (options.includeNeighbors ? 1.9 : 2.15);
var targetScale =
Math.min((right - left) / bounds.width, (bottom - top) / bounds.height) * 0.9;
targetScale = Math.max(1, Math.min(maxScale, targetScale));
if (targetScale < 1.08) targetScale = 1;
var target = {
scale: Math.round(targetScale * 100) / 100,
x: 0,
y: 0,
mode: "semantic",
};
target.x = (left + right) / 2 - (bounds.x + bounds.width / 2) * target.scale;
target.y = (top + bottom) / 2 - (bounds.y + bounds.height / 2) * target.scale;
var start = sampleRenderedState();
stopCameraMotion("replaced", false);
var transaction = cameraReceipt(target, options);
cameraTransaction = transaction;
var instant = options.instant === true || reducedMotion() || document.hidden;
if (instant) {
state = target;
apply();
finishCameraTransaction(
transaction,
reducedMotion() ? "reduced-motion" : document.hidden ? "hidden" : "complete",
);
return transaction;
}
var duration = Math.max(180, Math.min(520, Number(options.duration) || 420));
var startedAt = 0;
state = start;
state.mode = "semantic";
apply();
container.classList.add("is-camera-moving");
container.classList.add("is-camera-transaction");
container.setAttribute("data-camera-transaction", String(transaction.id));
var step = function (timestamp) {
if (cameraTransaction !== transaction || transaction.settled) return;
if (!startedAt) startedAt = timestamp;
var fraction = Math.max(0, Math.min(1, (timestamp - startedAt) / duration));
var eased = 1 - Math.pow(1 - fraction, 3);
state = {
scale: start.scale + (target.scale - start.scale) * eased,
x: start.x + (target.x - start.x) * eased,
y: start.y + (target.y - start.y) * eased,
mode: "semantic",
};
apply();
if (fraction < 1) {
transaction.frame = requestAnimationFrame(step);
cameraFrame = transaction.frame;
} else {
state = target;
apply();
finishCameraTransaction(transaction, "complete");
}
};
transaction.frame = requestAnimationFrame(step);
cameraFrame = transaction.frame;
return transaction;
}
function reveal(ids, options) {
options = options || {};
if (window.innerWidth > 720) return frameDesktop(ids, options);
stopCameraMotion("replaced", false);
state.scale = 1;
state.x = 0;
state.y = 0;
state.mode = "semantic";
apply();
if (!container.hasAttribute("data-wide-diagram")) {
var contained = cameraReceipt({ scale: 1, x: 0, y: 0, mode: "semantic" }, options);
cameraTransaction = contained;
finishCameraTransaction(contained, "complete");
return contained;
}
var boxes = boxesFor(ids, options.includeNeighbors === true);
if (!boxes.length || !viewBox || viewBox.width <= 0) return false;
var minX = Math.min.apply(
Math,
boxes.map(function (box) {
return box.x;
}),
);
var maxX = Math.max.apply(
Math,
boxes.map(function (box) {
return box.x + box.width;
}),
);
var center = ((minX + maxX) / 2 / viewBox.width) * (svg.clientWidth || 1);
var target = Math.max(
0,
Math.min(svg.clientWidth - container.clientWidth, center - container.clientWidth / 2),
);
var transaction = cameraReceipt({ scrollLeft: target }, options);
cameraTransaction = transaction;
var instant = options.instant === true || reducedMotion() || document.hidden;
autoScrollUntil = Date.now() + (instant ? 50 : 470);
try {
container.scrollTo({ left: target, behavior: instant ? "auto" : "smooth" });
} catch (_) {
container.scrollLeft = target;
}
if (instant)
finishCameraTransaction(
transaction,
reducedMotion() ? "reduced-motion" : document.hidden ? "hidden" : "complete",
);
else {
transaction.timer = setTimeout(function () {
finishCameraTransaction(transaction, "complete");
}, 460);
cameraTimer = transaction.timer;
container.classList.add("is-camera-moving");
container.setAttribute("data-camera-transaction", String(transaction.id));
}
return transaction;
}
function syncSemantic() {
var guided =
Archify.guidedViews && typeof Archify.guidedViews.focus === "function"
? Archify.guidedViews.focus()
: [];
if (guided && guided.length) return reveal(guided, { reason: "guided-sync" });
var active =
Archify.focus && typeof Archify.focus.active === "function"
? Archify.focus.active()
: null;
if (typeof active === "string")
return reveal([active], { includeNeighbors: true, reason: "focus-sync" });
if (Array.isArray(active) && active.length)
return reveal(active, { reason: "selection-sync" });
return false;
}
function pinControls() {
container.style.setProperty("--archify-scroll-x", container.scrollLeft + "px");
}
function onScroll() {
pinControls();
if (Archify.radar && typeof Archify.radar.sync === "function") Archify.radar.sync();
if (
window.innerWidth <= 720 &&
container.hasAttribute("data-wide-diagram") &&
Date.now() > autoScrollUntil
) {
interruptCamera();
}
}
function onPointerEnd(event) {
if (!drag) return;
var moved = drag.moved;
drag = null;
container.classList.remove("is-panning");
try {
container.releasePointerCapture(event.pointerId);
} catch (_) {}
if (moved) {
container.setAttribute("data-just-panned", "true");
setTimeout(function () {
container.removeAttribute("data-just-panned");
}, 80);
}
}
inBtn.addEventListener("click", function () {
zoom(state.scale + 0.25);
});
outBtn.addEventListener("click", function () {
zoom(state.scale - 0.25);
});
resetBtn.addEventListener("click", reset);
container.addEventListener("pointerdown", function (event) {
if (
state.scale <= 1 ||
event.button !== 0 ||
event.target.closest(
".diagram-nav, .focus-chip, .node-finder, .diagram-guide, .overview-map, .route-probe, .semantic-lens",
) ||
event.target.closest("[data-node-id]") ||
event.target.closest("[data-relationship-hit-key]")
)
return;
interruptCamera();
drag = {
startX: event.clientX,
startY: event.clientY,
x: state.x,
y: state.y,
moved: false,
};
container.classList.add("is-panning");
try {
container.setPointerCapture(event.pointerId);
} catch (_) {}
});
container.addEventListener("pointermove", function (event) {
if (!drag) return;
var dx = event.clientX - drag.startX;
var dy = event.clientY - drag.startY;
if (Math.abs(dx) + Math.abs(dy) > 3) drag.moved = true;
state.x = drag.x + dx;
state.y = drag.y + dy;
apply();
});
container.addEventListener("pointerup", onPointerEnd);
container.addEventListener("pointercancel", onPointerEnd);
container.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", function () {
if (resizeFrame) cancelAnimationFrame(resizeFrame);
resizeFrame = requestAnimationFrame(function () {
resizeFrame = 0;
if (state.mode === "semantic") syncSemantic();
else apply();
});
});
window.addEventListener("hashchange", function () {
requestAnimationFrame(syncSemantic);
});
apply();
pinControls();
requestAnimationFrame(syncSemantic);
return {
zoomIn: function () {
zoom(state.scale + 0.25);
},
zoomOut: function () {
zoom(state.scale - 0.25);
},
reset: reset,
reveal: reveal,
centerAt: centerAt,
logicalViewport: logicalViewport,
sync: syncSemantic,
state: function () {
return { scale: state.scale, x: state.x, y: state.y, mode: state.mode };
},
};
})();
/* ============================================================
Semantic Radar — simplified semantic bounds + live viewport.
The radar is built at runtime so the checked artifact still contains
one canonical SVG block, and export serialization remains untouched.
============================================================ */
Archify.radar = (function () {
var container = document.querySelector(".diagram-container");
var diagram = container.querySelector(":scope > svg");
var panel = document.getElementById("overview-map");
var panelHead = panel.querySelector(".overview-map-head");
var surface = document.getElementById("overview-map-surface");
var status = document.getElementById("overview-map-status");
var trigger = document.getElementById("btn-overview-map");
var closeBtn = document.getElementById("overview-map-close");
var expandBtn = document.getElementById("overview-map-expand");
var feedback = document.getElementById("overview-map-feedback");
var navigation = container.querySelector(".diagram-nav");
var passport = document.getElementById("focus-chip");
var namespace = "http://www.w3.org/2000/svg";
var viewBox = diagram.viewBox && diagram.viewBox.baseVal;
var mapSvg = document.createElementNS(namespace, "svg");
var nodeLayer = document.createElementNS(namespace, "g");
var viewport = document.createElementNS(namespace, "rect");
var nodes = [];
var viewportDrag = null;
var panelDrag = null;
var manualPosition = null;
var lastPlacement = null;
var requestedOpen = false;
var passportYielded = false;
var passportPreviousAriaHidden = null;
var syncFrame = 0;
var spaceRetryTimer = 0;
var spaceRetryCount = 0;
var placementGap = 16;
mapSvg.setAttribute("role", "group");
mapSvg.setAttribute("aria-label", viewerText("viewer.radar.nodes"));
mapSvg.setAttribute("preserveAspectRatio", "xMidYMid meet");
nodeLayer.setAttribute("class", "overview-map-nodes");
viewport.setAttribute("class", "overview-map-viewport");
viewport.setAttribute("rx", "3");
viewport.setAttribute("ry", "3");
mapSvg.appendChild(nodeLayer);
mapSvg.appendChild(viewport);
surface.appendChild(mapSvg);
function nodeLabel(node, fallback) {
return (
node.getAttribute("data-node-label") ||
(node.getAttribute("aria-label") || fallback).replace(/^Focus\s+/, "").split(",")[0]
);
}
function build() {
if (!viewBox || viewBox.width <= 0 || viewBox.height <= 0) return false;
mapSvg.setAttribute(
"viewBox",
[viewBox.x, viewBox.y, viewBox.width, viewBox.height].join(" "),
);
nodeLayer.textContent = "";
nodes = [];
Array.prototype.forEach.call(diagram.querySelectorAll("[data-node-id]"), function (node) {
var box;
try {
box = node.getBBox();
} catch (_) {
box = null;
}
if (!box || box.width <= 0 || box.height <= 0) return;
var id = node.getAttribute("data-node-id");
var rect = document.createElementNS(namespace, "rect");
rect.setAttribute("class", "overview-map-node");
rect.setAttribute("x", String(box.x));
rect.setAttribute("y", String(box.y));
rect.setAttribute("width", String(Math.max(3, box.width)));
rect.setAttribute("height", String(Math.max(3, box.height)));
rect.setAttribute(
"rx",
String(Math.max(2, Math.min(8, Math.min(box.width, box.height) * 0.1))),
);
rect.setAttribute("data-radar-node-id", id);
rect.setAttribute("data-kind", node.getAttribute("data-node-kind") || "neutral");
rect.setAttribute("tabindex", "0");
rect.setAttribute("role", "button");
rect.setAttribute(
"aria-label",
viewerText("viewer.radar.focus", { label: nodeLabel(node, id) }),
);
nodeLayer.appendChild(rect);
nodes.push({ id: id, node: node, rect: rect });
});
status.textContent = viewerText("viewer.radar.fullMap", { count: nodes.length });
return true;
}
function visibleRect(element) {
if (!element || element.hidden) return null;
var rect = element.getBoundingClientRect();
if (
rect.width <= 0 ||
rect.height <= 0 ||
rect.right <= 0 ||
rect.bottom <= 0 ||
rect.left >= window.innerWidth ||
rect.top >= window.innerHeight
)
return null;
return {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
width: rect.width,
height: rect.height,
};
}
function panelRectAt(position) {
return {
left: position.left,
top: position.top,
right: position.left + panel.offsetWidth,
bottom: position.top + panel.offsetHeight,
width: panel.offsetWidth,
height: panel.offsetHeight,
};
}
function rectsIntersect(first, second, gap) {
gap = Number(gap) || 0;
return (
first.left < second.right + gap &&
first.right > second.left - gap &&
first.top < second.bottom + gap &&
first.bottom > second.top - gap
);
}
function intersectionArea(first, second) {
var width = Math.max(
0,
Math.min(first.right, second.right) - Math.max(first.left, second.left),
);
var height = Math.max(
0,
Math.min(first.bottom, second.bottom) - Math.max(first.top, second.top),
);
return width * height;
}
function clamp(value, minimum, maximum) {
if (maximum < minimum) return minimum;
return Math.max(minimum, Math.min(maximum, value));
}
function placementContext() {
var containerRect = visibleRect(container);
if (!containerRect) return null;
var controlRect = visibleRect(navigation);
var left = Math.max(placementGap, containerRect.left + placementGap);
var top = Math.max(placementGap, containerRect.top + placementGap);
var right = Math.min(
window.innerWidth - placementGap,
containerRect.right - placementGap,
);
var bottom = Math.min(
window.innerHeight - placementGap,
containerRect.bottom - placementGap,
);
if (controlRect) bottom = Math.min(bottom, controlRect.top - placementGap);
var lens = document.getElementById("focus-chip");
var lensRect = visibleRect(lens);
var legendRect = visibleRect(diagram.querySelector("[data-legend]"));
var active = diagram.querySelector("[data-focus-selected]");
var activeRect = visibleRect(active);
return {
bounds: { left: left, top: top, right: right, bottom: bottom },
hardBlockers: [lensRect, controlRect, legendRect].filter(Boolean),
softBlockers: [activeRect].filter(Boolean),
preferredSide:
!activeRect || activeRect.left + activeRect.width / 2 > window.innerWidth / 2
? "left"
: "right",
};
}
function positionIsValid(position, context) {
if (!position || !context) return false;
var rect = panelRectAt(position);
var bounds = context.bounds;
if (
rect.left < bounds.left ||
rect.top < bounds.top ||
rect.right > bounds.right ||
rect.bottom > bounds.bottom
)
return false;
return context.hardBlockers.every(function (blocker) {
return !rectsIntersect(rect, blocker, placementGap);
});
}
function cornerCandidates(context) {
var bounds = context.bounds;
var left = bounds.left;
var right = Math.max(left, bounds.right - panel.offsetWidth);
var top = bounds.top;
var bottom = Math.max(top, bounds.bottom - panel.offsetHeight);
var preferredLeft = context.preferredSide === "left" ? left : right;
var alternateLeft = context.preferredSide === "left" ? right : left;
return [
{ left: preferredLeft, top: bottom },
{ left: alternateLeft, top: bottom },
{ left: preferredLeft, top: top },
{ left: alternateLeft, top: top },
].filter(function (candidate, index, all) {
return (
all.findIndex(function (other) {
return other.left === candidate.left && other.top === candidate.top;
}) === index
);
});
}
function nearbyCandidates(context, reference) {
var bounds = context.bounds;
var maximumLeft = bounds.right - panel.offsetWidth;
var maximumTop = bounds.bottom - panel.offsetHeight;
var requested = reference
? {
left: clamp(reference.left, bounds.left, maximumLeft),
top: clamp(reference.top, bounds.top, maximumTop),
}
: null;
var horizontal = [bounds.left, maximumLeft];
var vertical = [bounds.top, maximumTop];
if (requested) {
horizontal.push(requested.left);
vertical.push(requested.top);
}
context.hardBlockers.forEach(function (blocker) {
horizontal.push(
blocker.left - placementGap - panel.offsetWidth,
blocker.right + placementGap,
);
vertical.push(
blocker.top - placementGap - panel.offsetHeight,
blocker.bottom + placementGap,
);
});
horizontal = horizontal.map(function (left) {
return clamp(left, bounds.left, maximumLeft);
});
vertical = vertical.map(function (top) {
return clamp(top, bounds.top, maximumTop);
});
var candidates = [];
horizontal.forEach(function (left) {
vertical.forEach(function (top) {
candidates.push({ left: left, top: top });
});
});
return candidates.filter(function (candidate, index, all) {
return (
all.findIndex(function (other) {
return other.left === candidate.left && other.top === candidate.top;
}) === index
);
});
}
function placementScore(position, context, reference, softWeight) {
var rect = panelRectAt(position);
var referenceLeft = reference
? reference.left
: context.preferredSide === "left"
? context.bounds.left
: context.bounds.right - panel.offsetWidth;
var referenceTop = reference ? reference.top : context.bounds.bottom - panel.offsetHeight;
var distance =
Math.pow(position.left - referenceLeft, 2) + Math.pow(position.top - referenceTop, 2);
var softOverlap = context.softBlockers.reduce(function (total, blocker) {
return total + intersectionArea(rect, blocker);
}, 0);
return distance + softOverlap * softWeight;
}
function chooseRadarPlacement(context, reference, options) {
options = options || {};
var softWeight = Number.isFinite(options.softWeight) ? options.softWeight : 100;
var candidates = nearbyCandidates(context, reference).concat(cornerCandidates(context));
var valid = candidates.filter(function (candidate) {
return positionIsValid(candidate, context);
});
valid.sort(function (first, second) {
return (
placementScore(first, context, reference, softWeight) -
placementScore(second, context, reference, softWeight)
);
});
return valid.length ? valid[0] : null;
}
function applyPlacement(position, remember) {
var useLeft = position.left + panel.offsetWidth / 2 <= window.innerWidth / 2;
panel.setAttribute("data-docked", "true");
panel.setAttribute("data-dock-side", useLeft ? "left" : "right");
if (useLeft) {
panel.style.setProperty("--archify-radar-left", Math.round(position.left) + "px");
panel.style.removeProperty("--archify-radar-right");
} else {
panel.style.setProperty(
"--archify-radar-right",
Math.round(window.innerWidth - position.left - panel.offsetWidth) + "px",
);
panel.style.removeProperty("--archify-radar-left");
}
panel.style.setProperty("--archify-radar-top", Math.round(position.top) + "px");
if (remember !== false) lastPlacement = { left: position.left, top: position.top };
}
function resetDockingStyles() {
panel.removeAttribute("data-docked");
panel.removeAttribute("data-dock-side");
panel.removeAttribute("data-panel-dragging");
panel.removeAttribute("data-placement-invalid");
panel.removeAttribute("data-placement-degraded");
panel.removeAttribute("data-placement-unavailable");
panel.removeAttribute("data-compact");
panel.removeAttribute("title");
panel.style.removeProperty("visibility");
panel.style.removeProperty("--archify-radar-right");
panel.style.removeProperty("--archify-radar-left");
panel.style.removeProperty("--archify-radar-top");
}
function updateDocking() {
var options = arguments[0] || {};
if (panel.hidden) return false;
if (panelDrag) return true;
panel.removeAttribute("data-compact");
panel.removeAttribute("data-placement-degraded");
panel.removeAttribute("data-placement-invalid");
panel.removeAttribute("data-placement-unavailable");
panel.removeAttribute("title");
panel.style.removeProperty("visibility");
var context = placementContext();
if (!context) {
resetDockingStyles();
return false;
}
var reference = manualPosition || lastPlacement;
var placementOptions = { softWeight: manualPosition ? 0 : 100 };
var placement =
manualPosition && positionIsValid(manualPosition, context)
? manualPosition
: chooseRadarPlacement(context, reference, placementOptions);
var compact = false;
if (!placement && options.allowCompact !== false) {
compact = true;
panel.setAttribute("data-compact", "true");
panel.setAttribute("data-placement-degraded", "true");
panel.setAttribute("title", viewerText("viewer.radar.compacted"));
context = placementContext();
placement = chooseRadarPlacement(context, reference, placementOptions);
}
if (!placement) {
resetDockingStyles();
panel.setAttribute("data-placement-unavailable", "true");
return false;
}
if (manualPosition && !compact)
manualPosition = { left: placement.left, top: placement.top };
applyPlacement(placement, true);
return true;
}
function clearSpaceRetry() {
if (spaceRetryTimer) window.clearTimeout(spaceRetryTimer);
spaceRetryTimer = 0;
}
function yieldPassport() {
if (!passport || passport.hidden || passportYielded) return passportYielded;
passportPreviousAriaHidden = passport.getAttribute("aria-hidden");
passportYielded = true;
passport.setAttribute("data-radar-yielded", "true");
passport.setAttribute("aria-hidden", "true");
return true;
}
function restorePassport() {
if (!passport || !passportYielded) return;
passportYielded = false;
passport.removeAttribute("data-radar-yielded");
if (passportPreviousAriaHidden === null) passport.removeAttribute("aria-hidden");
else passport.setAttribute("aria-hidden", passportPreviousAriaHidden);
passportPreviousAriaHidden = null;
}
function reflectVisible() {
panel.hidden = false;
panel.removeAttribute("data-placement-unavailable");
trigger.setAttribute("aria-expanded", "true");
trigger.removeAttribute("data-radar-space-limited");
trigger.setAttribute("aria-label", viewerText("viewer.radar.close"));
trigger.title = viewerText("viewer.nav.radar.title");
feedback.hidden = true;
}
function scheduleSpaceRetry() {
if (!requestedOpen || spaceRetryTimer || spaceRetryCount >= 4) return;
spaceRetryCount += 1;
spaceRetryTimer = window.setTimeout(function () {
spaceRetryTimer = 0;
attemptRequestedOpen();
}, 60);
}
function reflectUnavailable() {
restorePassport();
panel.hidden = true;
panel.setAttribute("data-placement-unavailable", "true");
trigger.setAttribute("aria-expanded", "false");
trigger.setAttribute("aria-label", viewerText("viewer.radar.cancelWaiting"));
trigger.setAttribute("data-radar-space-limited", "true");
trigger.title = viewerText("viewer.radar.needsSpace");
feedback.hidden = false;
scheduleSpaceRetry();
}
function attemptRequestedOpen(options) {
options = options || {};
if (!requestedOpen) return false;
panel.hidden = false;
if (!updateDocking(options)) {
reflectUnavailable();
return false;
}
clearSpaceRetry();
spaceRetryCount = 0;
reflectVisible();
sync();
if (options.focus === true && !panel.hasAttribute("data-compact")) surface.focus();
return true;
}
function expandCompactRadar() {
if (!requestedOpen || panel.hidden || !panel.hasAttribute("data-compact")) return false;
yieldPassport();
if (!updateDocking({ allowCompact: false })) {
restorePassport();
if (!updateDocking()) {
reflectUnavailable();
return false;
}
}
reflectVisible();
sync();
if (!panel.hasAttribute("data-compact")) surface.focus();
return !panel.hasAttribute("data-compact");
}
function syncNow() {
syncFrame = 0;
if (panel.hidden || !Archify.view || typeof Archify.view.logicalViewport !== "function")
return;
if (!updateDocking()) {
reflectUnavailable();
return;
}
if (passportYielded && panel.hasAttribute("data-compact")) {
restorePassport();
if (!updateDocking()) {
reflectUnavailable();
return;
}
}
reflectVisible();
var visible = Archify.view.logicalViewport();
if (!visible) return;
viewport.setAttribute("x", String(visible.x));
viewport.setAttribute("y", String(visible.y));
viewport.setAttribute("width", String(visible.width));
viewport.setAttribute("height", String(visible.height));
var full =
visible.width >= viewBox.width * 0.98 && visible.height >= viewBox.height * 0.98;
var mobileWide = window.innerWidth <= 720 && container.hasAttribute("data-wide-diagram");
var viewportCopy = full
? viewerText("viewer.radar.viewport.full")
: mobileWide
? viewerText("viewer.radar.viewport.width", {
percent: Math.round((visible.width / viewBox.width) * 100),
})
: viewerText("viewer.radar.viewport.scale", {
percent: Math.round(visible.scale * 100),
});
status.textContent = viewerText("viewer.radar.status", {
count: nodes.length,
viewport: viewportCopy,
});
nodes.forEach(function (item) {
var active =
item.node.hasAttribute("data-focus-selected") ||
item.node.getAttribute("data-story-beat-state") === "active";
if (active) item.rect.setAttribute("data-radar-active", "true");
else item.rect.removeAttribute("data-radar-active");
});
}
function sync() {
if (requestedOpen && panel.hidden) {
attemptRequestedOpen();
return;
}
if (syncFrame) return;
syncFrame = requestAnimationFrame(syncNow);
}
function setOpen(next, options) {
options = options || {};
next = Boolean(next);
if (
next &&
Archify.semanticLens &&
typeof Archify.semanticLens.clearPreview === "function"
)
Archify.semanticLens.clearPreview();
if (next && Archify.semanticLens && Archify.semanticLens.isOpen()) {
Archify.semanticLens.close({ restoreFocus: false });
}
requestedOpen = next;
if (next) {
clearSpaceRetry();
spaceRetryCount = 0;
build();
attemptRequestedOpen(options);
} else {
clearSpaceRetry();
spaceRetryCount = 0;
viewportDrag = null;
panelDrag = null;
container.classList.remove("is-panning");
panel.hidden = true;
resetDockingStyles();
restorePassport();
feedback.hidden = true;
trigger.setAttribute("aria-expanded", "false");
trigger.removeAttribute("data-radar-space-limited");
trigger.setAttribute("aria-label", viewerText("viewer.nav.radar"));
trigger.title = viewerText("viewer.nav.radar.title");
if (options.restoreFocus === true) trigger.focus();
}
return next;
}
function toggle() {
return setOpen(!requestedOpen, { focus: false });
}
function close(options) {
return setOpen(false, options);
}
function bringNodeIntoWindow(node) {
var delay =
window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches
? 0
: 540;
window.setTimeout(function () {
var rect = node.getBoundingClientRect();
var safeTop = 64;
var safeBottom = window.innerHeight - 64;
if (rect.top >= safeTop && rect.bottom <= safeBottom) return;
var top = Math.max(
0,
window.scrollY + rect.top + rect.height / 2 - window.innerHeight / 2,
);
try {
window.scrollTo({ top: top, behavior: delay ? "smooth" : "auto" });
} catch (_) {
window.scrollTo(0, top);
}
}, delay);
}
function focusNode(id) {
var main = diagram.querySelector('[data-node-id="' + id + '"]');
if (!main) return false;
if (Archify.guidedViews && typeof Archify.guidedViews.showAll === "function") {
Archify.guidedViews.showAll({ clearFocus: false, updateUrl: false });
}
if (Archify.focus && typeof Archify.focus.set === "function") {
Archify.focus.set(id, { toggle: false });
}
if (Archify.view && typeof Archify.view.reveal === "function") {
Archify.view.reveal([id], { includeNeighbors: true, reason: "radar" });
}
bringNodeIntoWindow(main);
try {
main.focus({ preventScroll: true });
} catch (_) {
try {
main.focus();
} catch (_) {}
}
sync();
return true;
}
function diagramPoint(event) {
var matrix = mapSvg.getScreenCTM();
if (!matrix) return null;
var point = mapSvg.createSVGPoint();
point.x = event.clientX;
point.y = event.clientY;
return point.matrixTransform(matrix.inverse());
}
function navigate(event) {
var point = diagramPoint(event);
if (!point || !Archify.view || typeof Archify.view.centerAt !== "function") return;
Archify.view.centerAt(point.x, point.y, { minimumScale: 1.5, instant: true });
sync();
}
function endViewportDrag(event) {
if (!viewportDrag) return;
viewportDrag = null;
panel.removeAttribute("data-dragging");
container.classList.remove("is-panning");
try {
surface.releasePointerCapture(event.pointerId);
} catch (_) {}
}
function beginPanelDrag(event) {
if (event.button !== 0 || event.target.closest('button, a, input, [role="button"]'))
return;
var context = placementContext();
if (!context) return;
var rect = panel.getBoundingClientRect();
event.preventDefault();
event.stopPropagation();
panelDrag = {
pointerId: event.pointerId,
originX: event.clientX,
originY: event.clientY,
start: { left: rect.left, top: rect.top },
current: { left: rect.left, top: rect.top },
previousManual: manualPosition
? { left: manualPosition.left, top: manualPosition.top }
: null,
};
panel.setAttribute("data-panel-dragging", "true");
try {
panelHead.setPointerCapture(event.pointerId);
} catch (_) {}
}
function movePanelDrag(event) {
if (!panelDrag || panelDrag.pointerId !== event.pointerId) return;
var context = placementContext();
if (!context) return;
event.preventDefault();
event.stopPropagation();
var bounds = context.bounds;
var position = {
left: clamp(
panelDrag.start.left + event.clientX - panelDrag.originX,
bounds.left,
bounds.right - panel.offsetWidth,
),
top: clamp(
panelDrag.start.top + event.clientY - panelDrag.originY,
bounds.top,
bounds.bottom - panel.offsetHeight,
),
};
panelDrag.current = position;
if (positionIsValid(position, context)) panel.removeAttribute("data-placement-invalid");
else panel.setAttribute("data-placement-invalid", "true");
applyPlacement(position, false);
}
function finishPanelDrag(event, cancel) {
if (!panelDrag || panelDrag.pointerId !== event.pointerId) return;
event.preventDefault();
event.stopPropagation();
var activeDrag = panelDrag;
panelDrag = null;
panel.removeAttribute("data-panel-dragging");
panel.removeAttribute("data-placement-invalid");
try {
panelHead.releasePointerCapture(event.pointerId);
} catch (_) {}
var context = placementContext();
if (!context) return;
if (cancel) {
manualPosition = activeDrag.previousManual;
lastPlacement = { left: activeDrag.start.left, top: activeDrag.start.top };
updateDocking();
return;
}
var requested = activeDrag.current;
manualPosition = { left: requested.left, top: requested.top };
updateDocking();
}
trigger.addEventListener("click", toggle);
closeBtn.addEventListener("click", function () {
close({ restoreFocus: true });
});
expandBtn.addEventListener("click", expandCompactRadar);
panelHead.addEventListener("pointerdown", beginPanelDrag);
panelHead.addEventListener("pointermove", movePanelDrag);
panelHead.addEventListener("pointerup", function (event) {
finishPanelDrag(event, false);
});
panelHead.addEventListener("pointercancel", function (event) {
finishPanelDrag(event, true);
});
surface.addEventListener("pointerdown", function (event) {
if (event.button !== 0 || event.target.closest("[data-radar-node-id]")) return;
event.preventDefault();
viewportDrag = { pointerId: event.pointerId };
panel.setAttribute("data-dragging", "true");
container.classList.add("is-panning");
try {
surface.setPointerCapture(event.pointerId);
} catch (_) {}
navigate(event);
});
surface.addEventListener("pointermove", function (event) {
if (!viewportDrag || viewportDrag.pointerId !== event.pointerId) return;
navigate(event);
});
surface.addEventListener("pointerup", endViewportDrag);
surface.addEventListener("pointercancel", endViewportDrag);
surface.addEventListener("click", function (event) {
var node = event.target.closest("[data-radar-node-id]");
if (node) focusNode(node.getAttribute("data-radar-node-id"));
});
surface.addEventListener("keydown", function (event) {
if (event.key === "Escape") {
event.preventDefault();
event.stopPropagation();
close({ restoreFocus: true });
return;
}
var node = event.target.closest("[data-radar-node-id]");
if (node && (event.key === "Enter" || event.key === " ")) {
event.preventDefault();
focusNode(node.getAttribute("data-radar-node-id"));
return;
}
if (
event.key !== "ArrowLeft" &&
event.key !== "ArrowRight" &&
event.key !== "ArrowUp" &&
event.key !== "ArrowDown"
)
return;
var visible =
Archify.view && Archify.view.logicalViewport ? Archify.view.logicalViewport() : null;
if (!visible) return;
event.preventDefault();
var x = visible.x + visible.width / 2;
var y = visible.y + visible.height / 2;
var stepX = Math.max(12, visible.width * 0.24);
var stepY = Math.max(12, visible.height * 0.24);
if (event.key === "ArrowLeft") x -= stepX;
else if (event.key === "ArrowRight") x += stepX;
else if (event.key === "ArrowUp") y -= stepY;
else y += stepY;
Archify.view.centerAt(x, y, { minimumScale: 1.5, instant: true });
sync();
});
document.addEventListener(
"keydown",
function (event) {
if (!panelDrag || event.key !== "Escape") return;
finishPanelDrag(
{
pointerId: panelDrag.pointerId,
preventDefault: function () {
event.preventDefault();
},
stopPropagation: function () {
event.stopPropagation();
},
},
true,
);
},
true,
);
function reflow() {
if (!requestedOpen) return;
clearSpaceRetry();
spaceRetryCount = 0;
sync();
}
window.addEventListener("resize", reflow);
window.addEventListener("scroll", reflow, { passive: true });
if (typeof ResizeObserver === "function") {
var radarResizeObserver = new ResizeObserver(function (entries) {
if (!requestedOpen) return;
if (
panel.hidden &&
entries.every(function (entry) {
return entry.target === panel;
})
)
return;
reflow();
});
[container, navigation, document.getElementById("focus-chip"), panel]
.filter(Boolean)
.forEach(function (element) {
radarResizeObserver.observe(element);
});
}
requestAnimationFrame(build);
return {
open: function () {
return setOpen(true);
},
close: close,
toggle: toggle,
sync: sync,
focus: focusNode,
isOpen: function () {
return requestedOpen;
},
count: function () {
return nodes.length;
},
};
})();
/* ============================================================
Presentation Stage — a shareable, viewport-filling live view.
It changes only HTML layout and URL state; SVG semantics and export
serialization remain untouched. Escape first clears an active guided
view/focus, then exits the stage.
============================================================ */
Archify.presentation = (function () {
var html = document.documentElement;
var btn = document.getElementById("btn-present");
var label = document.getElementById("present-label");
var previousScrollY = 0;
function active() {
return html.getAttribute("data-present") === "true";
}
function updateUrl(next) {
try {
var url = new URL(window.location.href);
if (next) url.searchParams.set("present", "1");
else url.searchParams.delete("present");
history.replaceState(null, "", url.pathname + url.search + url.hash);
} catch (_) {}
}
function render(next) {
btn.setAttribute("aria-pressed", next ? "true" : "false");
btn.setAttribute(
"aria-label",
viewerText(next ? "viewer.present.exit" : "viewer.present.enter"),
);
btn.title = viewerText(next ? "viewer.present.exit.title" : "viewer.present.enter.title");
label.textContent = viewerText(
next ? "viewer.present.exit.label" : "viewer.present.present",
);
}
function setActive(next, options) {
options = options || {};
next = Boolean(next);
if (next === active()) {
render(next);
return next;
}
if (next) {
if (Archify.semanticLens && typeof Archify.semanticLens.clearPreview === "function")
Archify.semanticLens.clearPreview();
previousScrollY = window.scrollY || 0;
html.setAttribute("data-present", "true");
try {
window.scrollTo(0, 0);
} catch (_) {}
} else {
html.removeAttribute("data-present");
}
render(next);
if (options.updateUrl !== false) updateUrl(next);
requestAnimationFrame(function () {
if (Archify.view && typeof Archify.view.reset === "function") {
Archify.view.reset({ automatic: true });
requestAnimationFrame(function () {
if (Archify.view && typeof Archify.view.sync === "function") Archify.view.sync();
});
}
if (!next && previousScrollY) {
try {
window.scrollTo(0, previousScrollY);
} catch (_) {}
}
});
return next;
}
function toggle() {
return setActive(!active());
}
render(active());
btn.addEventListener("click", toggle);
return {
enter: function () {
return setActive(true);
},
exit: function () {
return setActive(false);
},
toggle: toggle,
active: active,
};
})();
/* ============================================================
Node Finder — stable-ID search over the existing semantic SVG.
Search never changes the IR or SVG geometry: selecting a result resets
the viewport, releases a guided view, and delegates to semantic focus.
============================================================ */
Archify.finder = (function () {
var html = document.documentElement;
var container = document.querySelector(".diagram-container");
var svg = container.querySelector("svg");
var trigger = document.getElementById("btn-node-finder");
var panel = document.getElementById("node-finder");
var heading = document.getElementById("node-finder-title");
var closeBtn = document.getElementById("node-finder-close");
var input = document.getElementById("node-finder-input");
var results = document.getElementById("node-finder-results");
var empty = document.getElementById("node-finder-empty");
var status = document.getElementById("node-finder-status");
var visibleItems = [];
function defaultContext() {
return {
kind: "focus",
title: viewerText("viewer.finder.title"),
placeholder: viewerText("viewer.finder.placeholder"),
empty: viewerText("viewer.finder.empty"),
resultsLabel: viewerText("viewer.finder.results"),
availableNoun: viewerText("viewer.finder.noun.nodes"),
allowedIds: null,
badges: null,
};
}
var context = defaultContext();
function semanticType(node) {
var authored = node.getAttribute("data-node-kind");
if (authored) return authored;
var types = [
"frontend",
"backend",
"database",
"cloud",
"security",
"messagebus",
"external",
];
return (
types.find(function (type) {
return node.querySelector(".c-" + type);
}) || "node"
);
}
function connectionsFor(id) {
var count = 0;
var seen = {};
Array.prototype.forEach.call(
svg.querySelectorAll("[data-edge-from][data-edge-to]"),
function (edge) {
var from = edge.getAttribute("data-edge-from");
var to = edge.getAttribute("data-edge-to");
var key = from + "\u0000" + to;
if ((from === id || to === id) && !seen[key]) {
seen[key] = true;
count += 1;
}
},
);
return count;
}
var items = Array.prototype.map.call(
svg.querySelectorAll("[data-node-id]"),
function (node) {
var id = node.getAttribute("data-node-id");
var label =
node.getAttribute("data-node-label") ||
(node.getAttribute("aria-label") || id).replace(/^Focus\s+/, "");
var text = (node.textContent || "").replace(/\s+/g, " ").trim();
var sublabel = node.getAttribute("data-node-sublabel") || "";
var context = node.getAttribute("data-node-context") || "";
var tag = node.getAttribute("data-node-tag") || "";
var brand = node.getAttribute("data-node-brand") || "";
var type = semanticType(node);
var sources = Archify.sourceEvidence.node(id);
var sourceSearch = sources
.map(function (source) {
return [source.path, source.label, source.line, source.endLine]
.filter(Boolean)
.join(" ");
})
.join(" ");
return {
id: id,
label: label,
type: type,
sublabel: sublabel,
context: context,
tag: tag,
brand: brand,
sources: sources,
links: connectionsFor(id),
search:
(
id +
" " +
label +
" " +
type +
" " +
sublabel +
" " +
context +
" " +
tag +
" " +
sourceSearch +
" " +
text
).toLowerCase() +
" " +
brand.toLowerCase(),
node: node,
};
},
);
function resultButtons() {
return Array.prototype.slice.call(results.querySelectorAll(".node-finder-result"));
}
function resolveContext(options) {
var requested = options && options.context ? options.context : null;
if (
!requested &&
Archify.routeProbe &&
typeof Archify.routeProbe.finderContext === "function"
) {
requested = Archify.routeProbe.finderContext();
}
if (!requested) return defaultContext();
var resolved = defaultContext();
Object.keys(requested).forEach(function (key) {
resolved[key] = requested[key];
});
return resolved;
}
function availableItems() {
if (!context.allowedIds) return items.slice();
return items.filter(function (item) {
return context.allowedIds.indexOf(item.id) !== -1;
});
}
function applyContext() {
panel.setAttribute("data-context", context.kind);
heading.textContent = context.title;
input.placeholder = context.placeholder;
empty.textContent = context.empty;
results.setAttribute("aria-label", context.resultsLabel);
}
function select(id) {
var item = items.find(function (candidate) {
return candidate.id === id;
});
if (!item) return false;
var routeSelection = context.kind === "route-source" || context.kind === "route-target";
if (routeSelection) {
if (
!Archify.routeProbe ||
typeof Archify.routeProbe.choose !== "function" ||
!Archify.routeProbe.choose(id)
)
return false;
if (
Archify.routeProbe.active() === "target" &&
Archify.view &&
typeof Archify.view.reveal === "function"
) {
Archify.view.reveal([id], { includeNeighbors: true, reason: "route-pick" });
}
close({ restoreFocus: false });
try {
item.node.focus({ preventScroll: true });
} catch (_) {
try {
item.node.focus();
} catch (_) {}
}
return true;
}
if (Archify.guidedViews && typeof Archify.guidedViews.showAll === "function") {
Archify.guidedViews.showAll({ clearFocus: false, updateUrl: false });
}
if (Archify.view && typeof Archify.view.reset === "function")
Archify.view.reset({ automatic: true });
Archify.focus.set(id, { toggle: false });
if (Archify.view && typeof Archify.view.reveal === "function") {
Archify.view.reveal([id], { includeNeighbors: true, reason: "finder" });
}
close({ restoreFocus: false });
try {
item.node.focus({ preventScroll: true });
} catch (_) {
try {
item.node.focus();
} catch (_) {}
}
return true;
}
function render(query) {
query = (query || "").trim().toLowerCase();
var available = availableItems();
visibleItems = available.filter(function (item) {
return !query || item.search.indexOf(query) !== -1;
});
results.textContent = "";
visibleItems.forEach(function (item) {
var button = document.createElement("button");
button.type = "button";
button.className = "node-finder-result";
button.setAttribute("data-node-id", item.id);
var badge =
context.badges && context.badges[item.id]
? context.badges[item.id]
: context.kind === "focus"
? viewerCount("viewer.finder.link", item.links)
: String(item.links);
var action =
context.kind === "route-source"
? viewerText("viewer.finder.result.routeStart", { label: item.label })
: context.kind === "route-target"
? viewerText("viewer.finder.result.routeTarget", {
label: item.label,
links: badge,
})
: viewerCount("viewer.finder.result.focus", item.links, { label: item.label });
button.setAttribute("aria-label", action);
var name = document.createElement("strong");
name.textContent = item.label;
var links = document.createElement("em");
links.textContent = badge;
links.title = context.kind === "focus" ? badge : action;
var meta = document.createElement("small");
meta.textContent = [viewerKindLabel(item.type), item.id, item.sublabel, item.tag]
.filter(Boolean)
.join(" \u00b7 ");
meta.title = [
viewerKindLabel(item.type),
item.id,
item.context,
item.sublabel,
item.tag,
]
.filter(Boolean)
.join(" \u00b7 ");
button.appendChild(name);
button.appendChild(links);
button.appendChild(meta);
button.addEventListener("click", function () {
select(item.id);
});
results.appendChild(button);
});
empty.hidden = visibleItems.length !== 0;
status.textContent = query
? viewerText("viewer.finder.status.filtered", {
visible: visibleItems.length,
available: available.length,
noun: context.availableNoun,
})
: viewerText("viewer.finder.status.all", {
available: available.length,
noun: context.availableNoun,
});
}
function open(options) {
if (html.getAttribute("data-embed") === "true") return false;
if (Archify.semanticLens && typeof Archify.semanticLens.clearPreview === "function")
Archify.semanticLens.clearPreview();
if (Archify.exportMenu && Archify.exportMenu.isOpen()) Archify.exportMenu.close(false);
if (Archify.semanticLens && Archify.semanticLens.isOpen())
Archify.semanticLens.close({ restoreFocus: false });
context = resolveContext(options || {});
applyContext();
panel.hidden = false;
trigger.setAttribute("aria-expanded", "true");
if (
context.kind.indexOf("route-") === 0 &&
Archify.routeProbe &&
typeof Archify.routeProbe.finderOpening === "function"
) {
Archify.routeProbe.finderOpening();
}
input.value = "";
render("");
requestAnimationFrame(function () {
input.focus();
});
return true;
}
function close(options) {
options = options || {};
var routeContext = context.kind.indexOf("route-") === 0;
panel.hidden = true;
trigger.setAttribute("aria-expanded", "false");
input.value = "";
if (
routeContext &&
Archify.routeProbe &&
typeof Archify.routeProbe.finderClosed === "function"
) {
Archify.routeProbe.finderClosed({ restoreFocus: options.restoreFocus !== false });
} else if (options.restoreFocus !== false) {
trigger.focus();
}
}
function toggle() {
return panel.hidden ? open() : (close(), false);
}
trigger.addEventListener("click", toggle);
closeBtn.addEventListener("click", function () {
close();
});
input.addEventListener("input", function () {
render(input.value);
});
input.addEventListener("keydown", function (event) {
var buttons = resultButtons();
if (event.key === "ArrowDown" && buttons.length) {
event.preventDefault();
buttons[0].focus();
} else if (event.key === "Enter" && visibleItems.length) {
event.preventDefault();
select(visibleItems[0].id);
}
});
results.addEventListener("keydown", function (event) {
var buttons = resultButtons();
var index = buttons.indexOf(document.activeElement);
if (index < 0 || !buttons.length) return;
var next = null;
if (event.key === "ArrowDown") next = (index + 1) % buttons.length;
else if (event.key === "ArrowUp") next = (index - 1 + buttons.length) % buttons.length;
else if (event.key === "Home") next = 0;
else if (event.key === "End") next = buttons.length - 1;
if (next !== null) {
event.preventDefault();
buttons[next].focus();
}
});
panel.addEventListener("keydown", function (event) {
if (event.key !== "Escape") return;
event.preventDefault();
event.stopPropagation();
close();
});
document.addEventListener("click", function (event) {
if (
!panel.hidden &&
!panel.contains(event.target) &&
!event.target.closest("[data-node-finder-trigger]")
)
close({ restoreFocus: false });
});
render("");
return {
open: open,
close: close,
toggle: toggle,
select: select,
isOpen: function () {
return !panel.hidden;
},
context: function () {
return context.kind;
},
count: items.length,
};
})();
/* ============================================================
Route Probe — shortest directed path over compiled semantics.
The graph is read from renderer-owned stable IDs and relationship
endpoints. BFS order follows authored DOM edge order, so equal-length
choices are deterministic without adding weights or a graph runtime.
============================================================ */
Archify.routeProbe = (function () {
var html = document.documentElement;
var container = document.querySelector(".diagram-container");
var svg = container.querySelector(":scope > svg");
var trigger = document.getElementById("btn-route-probe");
var panel = document.getElementById("route-probe");
var title = document.getElementById("route-probe-title");
var path = document.getElementById("route-probe-path");
var status = document.getElementById("route-probe-status");
var findBtn = document.getElementById("route-probe-find");
var copyBtn = document.getElementById("route-probe-copy");
var clearBtn = document.getElementById("route-probe-clear");
var journeyControls = document.getElementById("route-journey-controls");
var journeyPrevBtn = document.getElementById("route-journey-prev");
var journeyPlayBtn = document.getElementById("route-journey-play");
var journeyPlayIcon = document.getElementById("route-journey-play-icon");
var journeyPlayLabel = document.getElementById("route-journey-play-label");
var journeyNextBtn = document.getElementById("route-journey-next");
var journeyOverviewBtn = document.getElementById("route-journey-overview");
var namespace = "http://www.w3.org/2000/svg";
var mode = "idle";
var startId = null;
var endId = null;
var activeNodeIds = [];
var activeEdges = [];
var journeyIndex = -1;
var journeyPlaying = false;
var journeyComplete = false;
var journeyGeneration = 0;
var journeyTimer = null;
var journeyStartedAt = 0;
var journeyElapsedMs = 0;
var journeyOwnerToken = 0;
var JOURNEY_DWELL_MS = 1100;
function nodes() {
return Array.prototype.slice.call(svg.querySelectorAll("[data-node-id]"));
}
function edges() {
return Array.prototype.slice.call(svg.querySelectorAll("[data-edge-from][data-edge-to]"));
}
function nodesById() {
var out = Object.create(null);
nodes().forEach(function (node) {
out[node.getAttribute("data-node-id")] = node;
});
return out;
}
function nodeLabel(node, fallback) {
return (
node.getAttribute("data-node-label") ||
(node.getAttribute("aria-label") || fallback).replace(/^Focus\s+/, "").split(",")[0]
);
}
function exportSnapshot() {
if (
mode !== "result" ||
activeNodeIds.length < 2 ||
activeEdges.length !== activeNodeIds.length - 1
)
return null;
var allNodes = nodes();
var byId = nodesById();
var seenNodeIds = Object.create(null);
var seenEdgeKeys = Object.create(null);
if (
startId !== activeNodeIds[0] ||
endId !== activeNodeIds[activeNodeIds.length - 1] ||
activeNodeIds.some(function (id) {
if (
seenNodeIds[id] ||
allNodes.filter(function (node) {
return node.getAttribute("data-node-id") === id;
}).length !== 1
)
return true;
seenNodeIds[id] = true;
return !byId[id];
}) ||
activeEdges.some(function (edge, index) {
if (!edge || !svg.contains(edge)) return true;
var edgeKey = edge.getAttribute("data-edge-key");
var edgeId = edge.getAttribute("data-edge-id") || "";
if (!edgeKey || seenEdgeKeys[edgeKey]) return true;
seenEdgeKeys[edgeKey] = true;
var fragments = Array.prototype.slice
.call(svg.querySelectorAll("[data-edge-key]"))
.filter(function (candidate) {
return candidate.getAttribute("data-edge-key") === edgeKey;
});
var drawableFragments = fragments.filter(hasDrawableGeometry);
return (
!fragments.length ||
!fragments.every(function (fragment) {
return (
fragment.getAttribute("data-edge-from") === activeNodeIds[index] &&
fragment.getAttribute("data-edge-to") === activeNodeIds[index + 1] &&
(fragment.getAttribute("data-edge-id") || "") === edgeId
);
}) ||
drawableFragments.length !== 1 ||
drawableFragments[0] !== edge ||
edge.getAttribute("data-edge-from") !== activeNodeIds[index] ||
edge.getAttribute("data-edge-to") !== activeNodeIds[index + 1]
);
})
)
return null;
return {
source: { id: startId, label: nodeLabel(byId[startId], startId) },
target: { id: endId, label: nodeLabel(byId[endId], endId) },
nodeIds: activeNodeIds.slice(),
hops: activeEdges.length,
edges: activeEdges.map(function (edge) {
return {
key: edge.getAttribute("data-edge-key"),
id: edge.getAttribute("data-edge-id") || "",
from: edge.getAttribute("data-edge-from"),
to: edge.getAttribute("data-edge-to"),
label: edge.getAttribute("data-edge-label") || "",
};
}),
};
}
function edgeShapes(edge) {
if (/^(path|line|polyline)$/i.test(edge.tagName)) return [edge];
return Array.prototype.slice.call(edge.querySelectorAll("path, line, polyline"));
}
function removeOverlay() {
Array.prototype.forEach.call(
svg.querySelectorAll("[data-route-probe-overlay]"),
function (overlay) {
overlay.remove();
},
);
}
function removeJourneyPulse(options) {
options = options || {};
Array.prototype.forEach.call(
svg.querySelectorAll("[data-route-journey-overlay]"),
function (overlay) {
overlay.remove();
},
);
if (journeyOwnerToken && options.release !== false && Archify.motionGovernor) {
var token = journeyOwnerToken;
journeyOwnerToken = 0;
Archify.motionGovernor.release(token);
}
}
function stopJourneyTimer(options) {
options = options || {};
journeyGeneration += 1;
if (journeyTimer) {
if (options.preserveElapsed === true && journeyStartedAt) {
journeyElapsedMs = Math.min(
JOURNEY_DWELL_MS,
journeyElapsedMs + Math.max(0, Date.now() - journeyStartedAt),
);
}
window.clearTimeout(journeyTimer);
}
journeyTimer = null;
journeyStartedAt = 0;
if (options.preserveElapsed !== true) journeyElapsedMs = 0;
}
function clearJourneyPresentation(options) {
options = options || {};
removeJourneyPulse();
svg.removeAttribute("data-route-journey");
panel.removeAttribute("data-route-journey");
nodes().forEach(function (node) {
node.removeAttribute("data-route-journey-state");
node.removeAttribute("data-route-journey-current");
});
edges().forEach(function (edge) {
edge.removeAttribute("data-route-journey-state");
edge.removeAttribute("data-route-journey-current");
});
Array.prototype.forEach.call(
path.querySelectorAll("[data-route-journey-index]"),
function (button, index) {
button.removeAttribute("data-route-journey-state");
button.removeAttribute("aria-current");
button.setAttribute("tabindex", index === 0 ? "0" : "-1");
},
);
if (options.keepControls !== true) journeyControls.hidden = true;
journeyControls.setAttribute("data-playing", "false");
}
function resetJourneyState() {
stopJourneyTimer();
journeyPlaying = false;
journeyComplete = false;
journeyIndex = -1;
clearJourneyPresentation();
}
function cleanSvgState() {
resetJourneyState();
svg.removeAttribute("data-route-picking");
svg.removeAttribute("data-route-active");
removeOverlay();
nodes().forEach(function (node) {
node.removeAttribute("data-route-match");
node.removeAttribute("data-route-start");
node.removeAttribute("data-route-end");
node.removeAttribute("data-route-step");
node.removeAttribute("data-route-candidate");
node.style.removeProperty("--route-step");
});
edges().forEach(function (edge) {
edge.removeAttribute("data-route-match");
edge.removeAttribute("data-route-step");
edge.style.removeProperty("--route-step");
});
panel.removeAttribute("data-route-dock");
}
function replaceRouteHash(value) {
try {
history.replaceState(
null,
"",
location.pathname + location.search + (value ? "#route=" + value : ""),
);
} catch (_) {}
}
function renderPlaceholder(copy) {
path.textContent = "";
var placeholder = document.createElement("span");
placeholder.className = "route-probe-placeholder";
placeholder.textContent = copy;
path.appendChild(placeholder);
}
function renderPath(ids, options) {
options = options || {};
var byId = nodesById();
path.textContent = "";
ids.forEach(function (id, index) {
if (index) {
var arrow = document.createElement("span");
arrow.className = "route-probe-arrow";
arrow.setAttribute("aria-hidden", "true");
arrow.textContent = "\u2192";
path.appendChild(arrow);
}
var item = document.createElement(options.interactive === true ? "button" : "span");
if (options.interactive === true) {
item.type = "button";
item.setAttribute("data-route-journey-index", String(index));
item.setAttribute("data-route-node-id", id);
item.setAttribute("tabindex", index === 0 ? "0" : "-1");
item.setAttribute(
"aria-label",
viewerText("viewer.route.position", {
index: index + 1,
total: ids.length,
label: nodeLabel(byId[id], id),
}),
);
}
item.className = "route-probe-node";
item.textContent = nodeLabel(byId[id], id);
item.title = nodeLabel(byId[id], id) + " · " + id;
if (index === 0) item.setAttribute("data-endpoint", "start");
if (index === ids.length - 1 && ids.length > 1)
item.setAttribute("data-endpoint", "end");
path.appendChild(item);
});
}
function setTrigger(active) {
trigger.setAttribute("aria-pressed", active ? "true" : "false");
trigger.setAttribute(
"aria-label",
viewerText(active ? "viewer.route.trigger.clear" : "viewer.guide.route.aria"),
);
}
function clear(options) {
options = options || {};
var wasActive = mode !== "idle";
if (
Archify.finder &&
Archify.finder.isOpen() &&
typeof Archify.finder.context === "function" &&
Archify.finder.context().indexOf("route-") === 0
) {
Archify.finder.close({ restoreFocus: false });
}
mode = "idle";
startId = null;
endId = null;
activeNodeIds = [];
activeEdges = [];
cleanSvgState();
panel.hidden = true;
panel.setAttribute("data-state", "idle");
panel.removeAttribute("data-finder-open");
title.textContent = viewerText("viewer.route.start");
renderPlaceholder(viewerText("viewer.route.pickTwo"));
status.textContent = viewerText("viewer.route.instructions");
findBtn.hidden = true;
findBtn.textContent = viewerText("viewer.route.start.find");
findBtn.setAttribute("aria-label", viewerText("viewer.route.start.find.aria"));
copyBtn.hidden = true;
copyBtn.textContent = viewerText("viewer.route.copy");
if (Archify.exportMenu && typeof Archify.exportMenu.syncRouteShare === "function")
Archify.exportMenu.syncRouteShare();
setTrigger(false);
if (
wasActive &&
options.preserveView !== true &&
Archify.view &&
typeof Archify.view.reset === "function"
) {
Archify.view.reset({ automatic: true });
}
if (options.updateUrl !== false) replaceRouteHash("");
if (options.restoreFocus === true) trigger.focus();
}
function outgoingByNode() {
var byId = nodesById();
var outgoing = {};
edges().forEach(function (edge) {
var from = edge.getAttribute("data-edge-from");
var to = edge.getAttribute("data-edge-to");
if (!byId[from] || !byId[to] || from === to) return;
if (!outgoing[from]) outgoing[from] = [];
outgoing[from].push({ edge: edge, to: to });
});
return outgoing;
}
function reachableFrom(source) {
var outgoing = outgoingByNode();
var reached = {};
var queue = [source];
reached[source] = true;
for (var cursor = 0; cursor < queue.length; cursor += 1) {
var links = outgoing[queue[cursor]] || [];
links.forEach(function (link) {
if (reached[link.to]) return;
reached[link.to] = true;
queue.push(link.to);
});
}
return reached;
}
function hopDistancesFrom(source) {
var outgoing = outgoingByNode();
var distances = {};
var queue = [source];
distances[source] = 0;
for (var cursor = 0; cursor < queue.length; cursor += 1) {
var links = outgoing[queue[cursor]] || [];
links.forEach(function (link) {
if (Object.prototype.hasOwnProperty.call(distances, link.to)) return;
distances[link.to] = distances[queue[cursor]] + 1;
queue.push(link.to);
});
}
return distances;
}
function shortestDirectedPath(source, target) {
if (source === target) return null;
var outgoing = outgoingByNode();
var previous = {};
var queue = [source];
previous[source] = null;
for (
var cursor = 0;
cursor < queue.length && !Object.prototype.hasOwnProperty.call(previous, target);
cursor += 1
) {
var links = outgoing[queue[cursor]] || [];
links.some(function (link) {
if (Object.prototype.hasOwnProperty.call(previous, link.to)) return false;
previous[link.to] = { from: queue[cursor], edge: link.edge };
queue.push(link.to);
return link.to === target;
});
}
if (!Object.prototype.hasOwnProperty.call(previous, target)) return null;
var nodeIds = [target];
var routeEdges = [];
var current = target;
while (current !== source) {
var step = previous[current];
if (!step) return null;
routeEdges.unshift(step.edge);
nodeIds.unshift(step.from);
current = step.from;
}
return { nodes: nodeIds, edges: routeEdges };
}
function traceGeometry(shape, step) {
var clone = shape.cloneNode(false);
clone.removeAttribute("id");
clone.removeAttribute("class");
clone.removeAttribute("style");
clone.removeAttribute("marker-start");
clone.removeAttribute("marker-mid");
clone.removeAttribute("marker-end");
clone.removeAttribute("role");
clone.removeAttribute("aria-label");
clone.removeAttribute("aria-labelledby");
clone.removeAttribute("data-animate");
clone.removeAttribute("data-edge-from");
clone.removeAttribute("data-edge-to");
clone.removeAttribute("data-edge-key");
clone.removeAttribute("data-edge-id");
clone.removeAttribute("data-edge-label");
clone.removeAttribute("data-route-match");
clone.removeAttribute("data-route-step");
clone.setAttribute("class", "route-probe-flow");
clone.setAttribute("pathLength", "1");
clone.style.setProperty("--route-step", String(step));
return clone;
}
function renderOverlay(routeEdges) {
removeOverlay();
if (!routeEdges.length) return;
var overlay = document.createElementNS(namespace, "g");
overlay.setAttribute("class", "route-probe-overlay");
overlay.setAttribute("data-route-probe-overlay", "");
overlay.setAttribute("aria-hidden", "true");
routeEdges.forEach(function (edge, step) {
var wrapper = document.createElementNS(namespace, "g");
if (edge.hasAttribute("transform"))
wrapper.setAttribute("transform", edge.getAttribute("transform"));
edgeShapes(edge).forEach(function (shape) {
wrapper.appendChild(traceGeometry(shape, step));
});
if (wrapper.childNodes.length) overlay.appendChild(wrapper);
});
var firstNode = svg.querySelector("[data-node-id]");
if (firstNode) svg.insertBefore(overlay, firstNode);
else svg.appendChild(overlay);
}
function journeyButtons() {
return Array.prototype.slice.call(path.querySelectorAll("[data-route-journey-index]"));
}
function journeyMotionAllowed() {
return (
html.getAttribute("data-embed") !== "true" &&
!document.hidden &&
Archify.motionGovernor &&
Archify.motionGovernor.capable === true &&
!Archify.motionGovernor.isPaused()
);
}
function routeOverviewStatus() {
return viewerText("viewer.route.overview.status", {
nodes: viewerCount("viewer.route.overview.node", activeNodeIds.length),
hops: viewerCount("viewer.route.overview.hop", activeEdges.length),
});
}
function centerJourneyButton(button) {
if (!button) return;
var target = button.offsetLeft - Math.max(0, (path.clientWidth - button.offsetWidth) / 2);
path.scrollLeft = Math.max(0, target);
}
function focusJourneyButton(index) {
var buttons = journeyButtons();
if (!buttons.length) return false;
var next = Math.max(0, Math.min(buttons.length - 1, index));
buttons.forEach(function (button, buttonIndex) {
button.setAttribute("tabindex", buttonIndex === next ? "0" : "-1");
});
try {
buttons[next].focus({ preventScroll: true });
} catch (_) {
buttons[next].focus();
}
centerJourneyButton(buttons[next]);
return true;
}
function renderJourneyControls() {
var hasRoute = mode === "result" && activeNodeIds.length > 1;
var canPlay = hasRoute && journeyMotionAllowed();
journeyControls.hidden = !hasRoute;
journeyControls.setAttribute("data-playing", journeyPlaying ? "true" : "false");
journeyPrevBtn.disabled = !hasRoute || journeyIndex <= 0;
journeyNextBtn.disabled = !hasRoute || journeyIndex >= activeNodeIds.length - 1;
journeyOverviewBtn.disabled = !hasRoute || journeyIndex < 0;
journeyPlayBtn.disabled = !canPlay;
journeyPlayBtn.setAttribute("aria-pressed", journeyPlaying ? "true" : "false");
if (journeyPlaying) {
journeyPlayIcon.textContent = "\u275a\u275a";
journeyPlayLabel.textContent = viewerText("viewer.route.pause.label");
journeyPlayBtn.setAttribute("aria-label", viewerText("viewer.route.pause"));
journeyPlayBtn.title = viewerText("viewer.route.pause");
} else if (journeyComplete || journeyIndex === activeNodeIds.length - 1) {
journeyPlayIcon.textContent = "\u21bb";
journeyPlayLabel.textContent = viewerText("viewer.route.replay.label");
journeyPlayBtn.setAttribute("aria-label", viewerText("viewer.route.replay"));
journeyPlayBtn.title = viewerText(
canPlay ? "viewer.route.replay" : "viewer.route.motionRequired",
);
} else {
journeyPlayIcon.textContent = "\u25b6";
journeyPlayLabel.textContent = viewerText("viewer.route.journey");
journeyPlayBtn.setAttribute("aria-label", viewerText("viewer.route.play"));
journeyPlayBtn.title = viewerText(
canPlay ? "viewer.route.play" : "viewer.route.motionRequired",
);
}
}
function journeyGeometry(shape) {
var clone = shape.cloneNode(false);
clone.removeAttribute("id");
clone.removeAttribute("class");
clone.removeAttribute("style");
clone.removeAttribute("marker-start");
clone.removeAttribute("marker-mid");
clone.removeAttribute("marker-end");
clone.removeAttribute("role");
clone.removeAttribute("aria-label");
clone.removeAttribute("aria-labelledby");
clone.removeAttribute("data-animate");
clone.removeAttribute("data-edge-from");
clone.removeAttribute("data-edge-to");
clone.removeAttribute("data-edge-key");
clone.removeAttribute("data-edge-id");
clone.removeAttribute("data-edge-label");
clone.removeAttribute("data-route-match");
clone.removeAttribute("data-route-step");
clone.removeAttribute("data-route-journey-state");
clone.removeAttribute("data-route-journey-current");
clone.setAttribute("class", "route-journey-flow");
clone.setAttribute("pathLength", "1");
return clone;
}
function renderJourneyPulse(edge) {
removeJourneyPulse();
if (!edge || !journeyMotionAllowed()) return false;
var overlay = document.createElementNS(namespace, "g");
overlay.setAttribute("class", "route-journey-overlay");
overlay.setAttribute("data-route-journey-overlay", "");
overlay.setAttribute("aria-hidden", "true");
var wrapper = document.createElementNS(namespace, "g");
if (edge.hasAttribute("transform"))
wrapper.setAttribute("transform", edge.getAttribute("transform"));
edgeShapes(edge).forEach(function (shape) {
wrapper.appendChild(journeyGeometry(shape));
});
if (!wrapper.childNodes.length) return false;
overlay.appendChild(wrapper);
var firstNode = svg.querySelector("[data-node-id]");
if (firstNode) svg.insertBefore(overlay, firstNode);
else svg.appendChild(overlay);
if (Archify.motionGovernor && Archify.motionGovernor.capable) {
var token = 0;
token = Archify.motionGovernor.claim("route", function () {
if (journeyOwnerToken === token) journeyOwnerToken = 0;
removeJourneyPulse({ release: false });
});
journeyOwnerToken = token;
}
overlay.addEventListener(
"animationend",
function () {
if (overlay.isConnected) removeJourneyPulse();
},
{ once: true },
);
window.setTimeout(function () {
if (overlay.isConnected) removeJourneyPulse();
}, 860);
return true;
}
function applyJourneyState(index, options) {
options = options || {};
if (mode !== "result" || !activeNodeIds.length) return false;
var byId = nodesById();
journeyIndex = Math.max(0, Math.min(activeNodeIds.length - 1, index));
removeJourneyPulse();
svg.setAttribute("data-route-journey", journeyIndex + 1 + "/" + activeNodeIds.length);
panel.setAttribute("data-route-journey", String(journeyIndex));
activeNodeIds.forEach(function (id, step) {
var node = byId[id];
if (!node) return;
var state = step < journeyIndex ? "past" : step === journeyIndex ? "current" : "future";
node.setAttribute("data-route-journey-state", state);
if (step === journeyIndex) node.setAttribute("data-route-journey-current", "");
else node.removeAttribute("data-route-journey-current");
});
activeEdges.forEach(function (edge, step) {
var destination = step + 1;
var state =
destination < journeyIndex
? "past"
: destination === journeyIndex
? "current"
: "future";
edge.setAttribute("data-route-journey-state", state);
if (destination === journeyIndex) edge.setAttribute("data-route-journey-current", "");
else edge.removeAttribute("data-route-journey-current");
});
var buttons = journeyButtons();
buttons.forEach(function (button, step) {
var state = step < journeyIndex ? "past" : step === journeyIndex ? "current" : "future";
button.setAttribute("data-route-journey-state", state);
button.setAttribute("tabindex", step === journeyIndex ? "0" : "-1");
if (step === journeyIndex) button.setAttribute("aria-current", "step");
else button.removeAttribute("aria-current");
});
if (options.center !== false) centerJourneyButton(buttons[journeyIndex]);
var phase = viewerText(
journeyPlaying
? "viewer.route.phase.playing"
: journeyComplete
? "viewer.route.phase.complete"
: "viewer.route.phase.inspecting",
);
status.textContent = viewerText("viewer.route.step", {
index: journeyIndex + 1,
total: activeNodeIds.length,
phase: phase,
label: nodeLabel(byId[activeNodeIds[journeyIndex]], activeNodeIds[journeyIndex]),
});
if (options.pulse === true && journeyIndex > 0)
renderJourneyPulse(activeEdges[journeyIndex - 1]);
if (
options.reveal !== false &&
Archify.view &&
typeof Archify.view.reveal === "function"
) {
Archify.view.reveal(
activeNodeIds.slice(
Math.max(0, journeyIndex - 1),
Math.min(activeNodeIds.length, journeyIndex + 2),
),
{
includeNeighbors: false,
reason: "route-journey",
maxScale: 1.65,
padding: 64,
duration: 360,
instant: !journeyMotionAllowed(),
},
);
}
renderJourneyControls();
requestDocking();
return true;
}
function pauseJourney(options) {
options = options || {};
if (!journeyPlaying && options.complete !== true) {
renderJourneyControls();
return false;
}
stopJourneyTimer({
preserveElapsed: options.complete !== true && options.preserveElapsed !== false,
});
journeyPlaying = false;
journeyComplete = options.complete === true;
if (journeyComplete) journeyElapsedMs = 0;
removeJourneyPulse();
if (journeyIndex >= 0)
applyJourneyState(journeyIndex, { center: false, pulse: false, reveal: false });
else renderJourneyControls();
return true;
}
function selectJourneyIndex(index) {
if (mode !== "result") return false;
stopJourneyTimer();
journeyPlaying = false;
journeyComplete = false;
return applyJourneyState(index, { pulse: true, reveal: true });
}
function showJourneyOverview(options) {
options = options || {};
if (mode !== "result") return false;
stopJourneyTimer();
journeyPlaying = false;
journeyComplete = false;
journeyIndex = -1;
clearJourneyPresentation({ keepControls: true });
status.textContent = routeOverviewStatus();
renderJourneyControls();
if (
options.reveal !== false &&
Archify.view &&
typeof Archify.view.reveal === "function"
) {
Archify.view.reveal(activeNodeIds, {
includeNeighbors: false,
reason: "route",
instant: !journeyMotionAllowed(),
});
}
requestDocking();
return true;
}
function scheduleJourney() {
if (!journeyPlaying || !journeyMotionAllowed()) {
pauseJourney({ preserveElapsed: true });
return false;
}
var generation = ++journeyGeneration;
var remaining = Math.max(0, JOURNEY_DWELL_MS - journeyElapsedMs);
journeyStartedAt = Date.now();
journeyTimer = window.setTimeout(function () {
if (generation !== journeyGeneration || !journeyPlaying) return;
journeyTimer = null;
journeyStartedAt = 0;
journeyElapsedMs = 0;
if (journeyIndex >= activeNodeIds.length - 1) {
pauseJourney({ complete: true, preserveElapsed: false });
return;
}
applyJourneyState(journeyIndex + 1, { pulse: true, reveal: true });
// Camera and motion-owner handoff are allowed to synchronously
// settle the previous step. The new step always starts with a fresh
// dwell; never carry cleanup time into the next scheduler lease.
journeyElapsedMs = 0;
journeyStartedAt = 0;
scheduleJourney();
}, remaining);
return true;
}
function playJourney() {
if (mode !== "result" || activeNodeIds.length < 2 || !journeyMotionAllowed()) {
renderJourneyControls();
return false;
}
if (journeyPlaying) return true;
if (journeyComplete || journeyIndex >= activeNodeIds.length - 1) {
journeyIndex = -1;
journeyElapsedMs = 0;
journeyComplete = false;
}
journeyPlaying = true;
if (journeyIndex < 0) applyJourneyState(0, { pulse: false, reveal: true });
else applyJourneyState(journeyIndex, { center: false, pulse: false, reveal: false });
scheduleJourney();
return true;
}
function toggleJourney() {
return journeyPlaying ? pauseJourney({ preserveElapsed: true }) : playJourney();
}
function previousJourney() {
return selectJourneyIndex(Math.max(0, journeyIndex < 0 ? 0 : journeyIndex - 1));
}
function nextJourney() {
return selectJourneyIndex(Math.min(activeNodeIds.length - 1, journeyIndex + 1));
}
function overlapArea(a, b) {
var width = Math.max(0, Math.min(a.right, b.right) - Math.max(a.left, b.left));
var height = Math.max(0, Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top));
return width * height;
}
function updateDocking() {
if (panel.hidden) {
panel.removeAttribute("data-route-dock");
return;
}
var containerRect = container.getBoundingClientRect();
var width = panel.offsetWidth;
var height = panel.offsetHeight;
if (!width || !height) return;
var currentRect = panel.getBoundingClientRect();
var left = currentRect.left;
var topCandidate = {
left: left,
right: left + width,
top: containerRect.top + 8,
bottom: containerRect.top + 8 + height,
};
var nav = container.querySelector(".diagram-nav");
var navRect = nav ? nav.getBoundingClientRect() : null;
var bottom = navRect ? navRect.top - 9 : containerRect.bottom - 8;
var bottomCandidate = {
left: left,
right: left + width,
top: bottom - height,
bottom: bottom,
};
var byId = nodesById();
var relevant = activeNodeIds.length ? activeNodeIds : startId ? [startId] : [];
var blockers = relevant
.map(function (id) {
return byId[id];
})
.filter(Boolean)
.map(function (node) {
return node.getBoundingClientRect();
});
function score(candidate) {
var value = blockers.reduce(function (sum, rect) {
return sum + overlapArea(candidate, rect);
}, 0);
if (navRect) value += overlapArea(candidate, navRect) * 4;
if (candidate.top < containerRect.top || candidate.bottom > containerRect.bottom)
value += 1000000;
return value;
}
if (score(topCandidate) <= score(bottomCandidate))
panel.setAttribute("data-route-dock", "top");
else panel.setAttribute("data-route-dock", "bottom");
}
function requestDocking() {
requestAnimationFrame(updateDocking);
window.setTimeout(updateDocking, 120);
window.setTimeout(updateDocking, 560);
}
function chooseStart(id) {
var byId = nodesById();
if (!byId[id]) return false;
cleanSvgState();
startId = id;
endId = null;
mode = "target";
var reached = reachableFrom(id);
byId[id].setAttribute("data-route-start", "");
byId[id].setAttribute("data-route-match", "");
Object.keys(reached).forEach(function (candidateId) {
if (candidateId !== id && byId[candidateId])
byId[candidateId].setAttribute("data-route-candidate", "");
});
svg.setAttribute("data-route-picking", "target");
panel.setAttribute("data-state", "target");
title.textContent = viewerText("viewer.route.destination", {
label: nodeLabel(byId[id], id),
});
renderPath([id]);
var count = Math.max(0, Object.keys(reached).length - 1);
status.textContent = count
? viewerCount("viewer.route.destination.count", count)
: viewerText("viewer.route.noOutgoing");
findBtn.hidden = false;
findBtn.textContent = viewerText("viewer.route.destination.find");
findBtn.setAttribute("aria-label", viewerText("viewer.route.destination.find.aria"));
requestDocking();
return true;
}
function showResult(result, options) {
options = options || {};
var byId = nodesById();
cleanSvgState();
mode = "result";
startId = result.nodes[0];
endId = result.nodes[result.nodes.length - 1];
activeNodeIds = result.nodes.slice();
activeEdges = result.edges.slice();
result.nodes.forEach(function (id, step) {
var node = byId[id];
if (!node) return;
node.setAttribute("data-route-match", "");
node.setAttribute("data-route-step", String(step));
node.style.setProperty("--route-step", String(step));
if (step === 0) node.setAttribute("data-route-start", "");
if (step === result.nodes.length - 1) node.setAttribute("data-route-end", "");
});
result.edges.forEach(function (edge, step) {
edge.setAttribute("data-route-match", "");
edge.setAttribute("data-route-step", String(step));
edge.style.setProperty("--route-step", String(step));
});
svg.setAttribute("data-route-active", startId + "~" + endId);
renderOverlay(result.edges);
renderPath(result.nodes, { interactive: true });
panel.setAttribute("data-state", "result");
panel.hidden = false;
title.textContent = viewerText("viewer.route.result.title", {
source: nodeLabel(byId[startId], startId),
target: nodeLabel(byId[endId], endId),
});
findBtn.hidden = true;
copyBtn.hidden = false;
setTrigger(true);
if (Archify.exportMenu && typeof Archify.exportMenu.syncRouteShare === "function")
Archify.exportMenu.syncRouteShare();
if (options.updateUrl !== false) {
replaceRouteHash(encodeURIComponent(startId) + "~" + encodeURIComponent(endId));
}
showJourneyOverview({ reveal: false });
if (Archify.view && typeof Archify.view.reveal === "function") {
Archify.view.reveal(result.nodes, { includeNeighbors: false, reason: "route" });
}
return true;
}
function choose(id, options) {
options = options || {};
var byId = nodesById();
if (!byId[id]) return false;
if (mode === "source") return chooseStart(id);
if (mode !== "target") return false;
if (id === startId) {
panel.setAttribute("data-state", "error");
title.textContent = viewerText("viewer.route.differentDestination");
status.textContent = viewerText("viewer.route.distinct");
return false;
}
var result = shortestDirectedPath(startId, id);
if (!result) {
panel.setAttribute("data-state", "error");
title.textContent = viewerText("viewer.route.unreachable", {
label: nodeLabel(byId[id], id),
});
status.textContent = viewerText("viewer.route.unreachable.detail", {
target: nodeLabel(byId[id], id),
source: nodeLabel(byId[startId], startId),
});
return false;
}
return showResult(result, options);
}
function begin(options) {
options = options || {};
if (html.getAttribute("data-embed") === "true") return false;
if (Archify.semanticLens && typeof Archify.semanticLens.clearPreview === "function")
Archify.semanticLens.clearPreview();
if (Archify.semanticLens && Archify.semanticLens.active()) {
Archify.semanticLens.clear({ updateUrl: false, preserveView: true, closePanel: true });
}
var focused =
options.source ||
(Archify.focus && typeof Archify.focus.active === "function"
? Archify.focus.active()
: null);
if (Array.isArray(focused)) focused = null;
clear({ updateUrl: false, preserveView: true, restoreFocus: false });
if (Archify.intentTrace && typeof Archify.intentTrace.clear === "function")
Archify.intentTrace.clear({ announce: false });
if (Archify.guidedViews && typeof Archify.guidedViews.showAll === "function") {
Archify.guidedViews.showAll({ clearFocus: false, updateUrl: false });
}
if (Archify.focus && typeof Archify.focus.clear === "function") {
Archify.focus.clear({ updateUrl: false, preserveView: true });
}
if (Archify.finder && Archify.finder.isOpen())
Archify.finder.close({ restoreFocus: false });
if (Archify.radar && Archify.radar.isOpen()) Archify.radar.close({ restoreFocus: false });
mode = "source";
panel.hidden = false;
panel.setAttribute("data-state", "source");
svg.setAttribute("data-route-picking", "source");
title.textContent = viewerText("viewer.route.start");
renderPlaceholder(viewerText("viewer.route.pickOne"));
status.textContent = viewerText("viewer.route.start.instructions");
findBtn.hidden = false;
findBtn.textContent = viewerText("viewer.route.start.find");
findBtn.setAttribute("aria-label", viewerText("viewer.route.start.find.aria"));
copyBtn.hidden = true;
setTrigger(true);
if (focused && nodesById()[focused]) chooseStart(focused);
if (options.focusNode === true && !focused) {
var first = nodes()[0];
if (first) {
try {
first.focus({ preventScroll: true });
} catch (_) {
try {
first.focus();
} catch (_) {}
}
}
}
return true;
}
function toggle(options) {
if (mode !== "idle") {
clear({ restoreFocus: true });
return false;
}
return begin(options);
}
function finderContext() {
var byId = nodesById();
if (mode === "source") {
var outgoing = outgoingByNode();
var sourceIds = nodes()
.map(function (node) {
return node.getAttribute("data-node-id");
})
.filter(function (id) {
return outgoing[id] && outgoing[id].length;
});
var sourceBadges = {};
sourceIds.forEach(function (id) {
sourceBadges[id] = viewerText("viewer.route.finder.source.badge");
});
return {
kind: "route-source",
title: viewerText("viewer.route.finder.source.title"),
placeholder: viewerText("viewer.route.finder.source.placeholder"),
empty: viewerText("viewer.route.finder.source.empty"),
resultsLabel: viewerText("viewer.route.finder.source.results"),
availableNoun: viewerText("viewer.route.finder.source.noun"),
allowedIds: sourceIds,
badges: sourceBadges,
};
}
if (mode === "target" && startId && byId[startId]) {
var distances = hopDistancesFrom(startId);
var targetIds = Object.keys(distances).filter(function (id) {
return id !== startId && byId[id];
});
var targetBadges = {};
targetIds.forEach(function (id) {
targetBadges[id] = viewerCount("viewer.route.hop", distances[id]);
});
return {
kind: "route-target",
title: viewerText("viewer.route.finder.target.title", {
label: nodeLabel(byId[startId], startId),
}),
placeholder: viewerText("viewer.route.finder.target.placeholder"),
empty: viewerText("viewer.route.finder.target.empty"),
resultsLabel: viewerText("viewer.route.finder.target.results"),
availableNoun: viewerText("viewer.route.finder.target.noun"),
allowedIds: targetIds,
badges: targetBadges,
};
}
return null;
}
function finderOpening() {
panel.setAttribute("data-finder-open", "true");
}
function finderClosed(options) {
options = options || {};
panel.removeAttribute("data-finder-open");
requestDocking();
if (options.restoreFocus === true && !findBtn.hidden) findBtn.focus();
}
function openFinder() {
var context = finderContext();
if (!context || !Archify.finder) return false;
return Archify.finder.open({ context: context });
}
function fallbackCopy(value) {
var field = document.createElement("textarea");
field.value = value;
field.setAttribute("readonly", "");
field.style.position = "fixed";
field.style.opacity = "0";
document.body.appendChild(field);
field.select();
var copied = false;
try {
copied = document.execCommand("copy");
} catch (_) {}
field.remove();
return copied;
}
function copyLink() {
if (mode !== "result") return Promise.resolve(false);
var value =
location.href.replace(/#.*$/, "") +
"#route=" +
encodeURIComponent(startId) +
"~" +
encodeURIComponent(endId);
var copy =
navigator.clipboard && typeof navigator.clipboard.writeText === "function"
? navigator.clipboard
.writeText(value)
.then(function () {
return true;
})
.catch(function () {
return fallbackCopy(value);
})
: Promise.resolve(fallbackCopy(value));
return copy.then(function (copied) {
copyBtn.textContent = viewerText(
copied ? "viewer.common.copied" : "viewer.common.copyFailed",
);
copyBtn.setAttribute(
"aria-label",
viewerText(copied ? "viewer.route.copy.success" : "viewer.route.copy.failed"),
);
window.setTimeout(function () {
copyBtn.textContent = viewerText("viewer.route.copy");
copyBtn.setAttribute("aria-label", viewerText("viewer.route.copy.aria"));
}, 1600);
return copied;
});
}
function interceptSelection(event) {
if (mode !== "source" && mode !== "target") return;
if (container.getAttribute("data-just-panned") === "true") return;
var node = event.target.closest("[data-node-id]");
if (!node) return;
if (event.type === "keydown" && event.key !== "Enter" && event.key !== " ") return;
event.preventDefault();
event.stopImmediatePropagation();
choose(node.getAttribute("data-node-id"));
}
function syncFromHash() {
try {
var params = new URLSearchParams(location.hash.replace(/^#/, ""));
var route = params.get("route");
if (!route) {
if (mode !== "idle") clear({ updateUrl: false, restoreFocus: false });
return;
}
var parts = route.split("~");
if (parts.length !== 2) return;
var byId = nodesById();
if (!byId[parts[0]] || !byId[parts[1]]) return;
begin({ source: parts[0], focusNode: false });
choose(parts[1], { updateUrl: false });
} catch (_) {}
}
function escapeRoute(options) {
options = options || {};
if (journeyPlaying) {
pauseJourney({ preserveElapsed: true });
if (options.restoreFocus === true) journeyPlayBtn.focus();
return "paused";
}
if (journeyIndex >= 0) {
showJourneyOverview({ reveal: true });
if (options.restoreFocus === true) journeyOverviewBtn.focus();
return "overview";
}
clear({ restoreFocus: options.restoreFocus === true });
return "cleared";
}
function syncJourneyMotion() {
if (journeyPlaying && !journeyMotionAllowed()) pauseJourney({ preserveElapsed: true });
renderJourneyControls();
return journeyMotionAllowed();
}
trigger.addEventListener("click", function () {
toggle({ focusNode: false });
});
findBtn.addEventListener("click", openFinder);
clearBtn.addEventListener("click", function () {
clear({ restoreFocus: true });
});
copyBtn.addEventListener("click", copyLink);
journeyPrevBtn.addEventListener("click", previousJourney);
journeyPlayBtn.addEventListener("click", toggleJourney);
journeyNextBtn.addEventListener("click", nextJourney);
journeyOverviewBtn.addEventListener("click", function () {
showJourneyOverview({ reveal: true });
});
path.addEventListener("click", function (event) {
var button = event.target.closest("[data-route-journey-index]");
if (!button) return;
selectJourneyIndex(Number(button.getAttribute("data-route-journey-index")));
});
path.addEventListener("focusin", function (event) {
if (journeyPlaying && event.target.closest("[data-route-journey-index]"))
pauseJourney({ preserveElapsed: true });
});
path.addEventListener("keydown", function (event) {
var button = event.target.closest("[data-route-journey-index]");
if (!button) return;
var index = Number(button.getAttribute("data-route-journey-index"));
var next = null;
if (event.key === "ArrowRight") next = Math.min(activeNodeIds.length - 1, index + 1);
else if (event.key === "ArrowLeft") next = Math.max(0, index - 1);
else if (event.key === "Home") next = 0;
else if (event.key === "End") next = activeNodeIds.length - 1;
if (next !== null) {
event.preventDefault();
focusJourneyButton(next);
return;
}
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
selectJourneyIndex(index);
}
});
svg.addEventListener("click", interceptSelection, true);
svg.addEventListener("keydown", interceptSelection, true);
container.addEventListener("scroll", updateDocking, { passive: true });
window.addEventListener("resize", requestDocking);
window.addEventListener("beforeprint", function () {
if (journeyPlaying) pauseJourney({ preserveElapsed: true, reason: "print" });
else removeJourneyPulse();
});
window.addEventListener("hashchange", function () {
requestAnimationFrame(syncFromHash);
});
syncFromHash();
return {
begin: begin,
choose: choose,
clear: clear,
toggle: toggle,
escape: escapeRoute,
copyLink: copyLink,
playJourney: playJourney,
pauseJourney: pauseJourney,
showOverview: showJourneyOverview,
selectJourneyIndex: selectJourneyIndex,
syncMotion: syncJourneyMotion,
isJourneyPlaying: function () {
return journeyPlaying;
},
finderContext: finderContext,
finderOpening: finderOpening,
finderClosed: finderClosed,
openFinder: openFinder,
exportSnapshot: exportSnapshot,
active: function () {
return mode === "idle" ? null : mode;
},
result: function () {
return mode === "result"
? {
source: startId,
target: endId,
nodes: activeNodeIds.slice(),
hops: activeEdges.length,
journey: journeyIndex,
playing: journeyPlaying,
}
: null;
},
};
})();
/* ============================================================
Semantic Lens — a counted, viewer-only legend over data-node-kind.
One selected kind reveals every authored relationship touching it.
Two selected kinds compare only direct cross-kind relationships while
preserving the full diagram as a dimmed spatial reference.
============================================================ */
Archify.semanticLens = (function () {
var html = document.documentElement;
var container = document.querySelector(".diagram-container");
var svg = container.querySelector(":scope > svg");
var trigger = document.getElementById("btn-semantic-lens");
var panel = document.getElementById("semantic-lens");
var closeBtn = document.getElementById("semantic-lens-close");
var kindsRoot = document.getElementById("semantic-lens-kinds");
var status = document.getElementById("semantic-lens-status");
var copyBtn = document.getElementById("semantic-lens-copy");
var clearBtn = document.getElementById("semantic-lens-clear");
var legendBridge = svg.querySelector("[data-legend-bridge]");
var legendEntries = [];
var hoveredLegendEntry = null;
var focusedLegendEntry = null;
var activeLegendPreview = null;
var lensOpener = trigger;
var selectedKinds = [];
var namespace = "http://www.w3.org/2000/svg";
var finePointerQuery = window.matchMedia
? window.matchMedia("(hover: hover) and (pointer: fine)")
: null;
var MAX_LENS_FLOW_EDGES = 24;
function nodesById() {
var byId = {};
Array.prototype.forEach.call(
svg.querySelectorAll("[data-node-id][data-node-kind]"),
function (node) {
var id = node.getAttribute("data-node-id");
if (id && !byId[id]) byId[id] = node;
},
);
return byId;
}
function collectKinds() {
var kinds = {};
var byId = nodesById();
Object.keys(byId).forEach(function (id) {
var node = byId[id];
var value = node.getAttribute("data-node-kind") || "neutral";
var kind =
kinds[value] ||
(kinds[value] = { id: value, label: viewerKindLabel(value), nodes: [] });
kind.nodes.push(node);
});
return Object.keys(kinds)
.map(function (key) {
return kinds[key];
})
.sort(function (a, b) {
return b.nodes.length - a.nodes.length || a.label.localeCompare(b.label);
});
}
function edgeGroups() {
var groups = {};
Array.prototype.forEach.call(
svg.querySelectorAll("[data-edge-from][data-edge-to]"),
function (edge) {
var from = edge.getAttribute("data-edge-from");
var to = edge.getAttribute("data-edge-to");
var key =
edge.getAttribute("data-edge-key") ||
[from, to, edge.getAttribute("data-edge-label") || ""].join("\u0000");
if (!groups[key]) groups[key] = { key: key, from: from, to: to, members: [] };
groups[key].members.push(edge);
},
);
return Object.keys(groups).map(function (key) {
return groups[key];
});
}
function edgeShapes(edge) {
if (!edge) return [];
if (/^(path|line|polyline)$/i.test(edge.tagName || "")) return [edge];
return Array.prototype.slice.call(edge.querySelectorAll("path, line, polyline"));
}
function legendSourceBounds(entry) {
var bounds = null;
Array.prototype.forEach.call(entry.children, function (child) {
if (child.hasAttribute("data-legend-bridge-runtime")) return;
var box;
try {
box = child.getBBox();
} catch (_) {
return;
}
if (!box || !Number.isFinite(box.x) || !Number.isFinite(box.y)) return;
var next = {
left: box.x,
top: box.y,
right: box.x + box.width,
bottom: box.y + box.height,
};
if (!bounds) bounds = next;
else {
bounds.left = Math.min(bounds.left, next.left);
bounds.top = Math.min(bounds.top, next.top);
bounds.right = Math.max(bounds.right, next.right);
bounds.bottom = Math.max(bounds.bottom, next.bottom);
}
});
return bounds;
}
function layoutLegendEntry(entry) {
var bounds = legendSourceBounds(entry);
var hit = entry.querySelector("[data-legend-hit]");
var badge = entry.querySelector("[data-legend-count-badge]");
if (!bounds || !hit || !badge) return false;
var count = entry.getAttribute("data-legend-count") || "0";
var centerY = (bounds.top + bounds.bottom) / 2;
var badgeWidth = Math.max(14, count.length * 5 + 8);
var badgeX = bounds.right + 3;
var hitX = bounds.left - 5;
var hitRight = badgeX + badgeWidth + 4;
hit.setAttribute("x", hitX.toFixed(2));
hit.setAttribute("y", (centerY - 12).toFixed(2));
hit.setAttribute("width", Math.max(24, hitRight - hitX).toFixed(2));
hit.setAttribute("height", "24");
hit.setAttribute("rx", "4");
var badgeRect = badge.querySelector("rect");
var badgeText = badge.querySelector("text");
badgeRect.setAttribute("x", badgeX.toFixed(2));
badgeRect.setAttribute("y", (centerY - 7).toFixed(2));
badgeRect.setAttribute("width", badgeWidth.toFixed(2));
badgeRect.setAttribute("height", "14");
badgeRect.setAttribute("rx", "7");
badgeText.setAttribute("x", (badgeX + badgeWidth / 2).toFixed(2));
badgeText.setAttribute("y", (centerY + 2.5).toFixed(2));
return true;
}
function layoutLegendBridge() {
legendEntries.forEach(layoutLegendEntry);
if (!legendBridge) return;
Array.prototype.forEach.call(
legendBridge.querySelectorAll("[data-legend-zero]"),
layoutLegendEntry,
);
}
function decorateLegendBridge() {
legendEntries = [];
if (!legendBridge || html.getAttribute("data-embed") === "true") return false;
var facts = {};
collectKinds().forEach(function (kind) {
facts[kind.id] = kind;
});
Array.prototype.forEach.call(
legendBridge.querySelectorAll("[data-legend-kind]"),
function (entry) {
var kind = entry.getAttribute("data-legend-kind");
var fact = facts[kind];
var count = fact ? fact.nodes.length : 0;
entry.setAttribute("data-legend-count", String(count));
var hit = document.createElementNS(namespace, "rect");
hit.setAttribute("data-legend-bridge-runtime", "");
hit.setAttribute("data-legend-hit", "");
hit.setAttribute("aria-hidden", "true");
entry.insertBefore(hit, entry.firstChild);
var badge = document.createElementNS(namespace, "g");
badge.setAttribute("data-legend-bridge-runtime", "");
badge.setAttribute("data-legend-count-badge", "");
badge.setAttribute("aria-hidden", "true");
badge.appendChild(document.createElementNS(namespace, "rect"));
var countText = document.createElementNS(namespace, "text");
countText.textContent = String(count);
badge.appendChild(countText);
entry.appendChild(badge);
if (!fact || !count) {
entry.setAttribute("data-legend-zero", "");
return;
}
entry.setAttribute("role", "button");
entry.setAttribute("tabindex", legendEntries.length ? "-1" : "0");
var visibleLabel = entry.getAttribute("data-legend-label") || fact.label;
entry.setAttribute(
"aria-label",
viewerCount("viewer.lens.legend.inspect", count, { label: visibleLabel }),
);
entry.setAttribute("aria-pressed", "false");
entry.setAttribute("aria-haspopup", "dialog");
entry.setAttribute("aria-controls", "semantic-lens");
entry.setAttribute("aria-expanded", "false");
legendEntries.push(entry);
},
);
if (!legendEntries.length) return false;
legendBridge.setAttribute("role", legendEntries.length >= 3 ? "toolbar" : "group");
legendBridge.setAttribute("aria-label", viewerText("viewer.lens.legend"));
syncLegendBridge();
layoutLegendBridge();
try {
if (document.fonts && document.fonts.ready)
document.fonts.ready.then(layoutLegendBridge);
} catch (_) {}
return true;
}
function syncLegendBridge() {
legendEntries.forEach(function (entry) {
var selected = selectedKinds.indexOf(entry.getAttribute("data-legend-kind")) >= 0;
entry.setAttribute("aria-pressed", selected ? "true" : "false");
entry.setAttribute(
"aria-expanded",
!panel.hidden && lensOpener === entry ? "true" : "false",
);
if (selected) entry.setAttribute("data-legend-selected", "");
else entry.removeAttribute("data-legend-selected");
});
}
function clearLegendPreview() {
activeLegendPreview = null;
svg.removeAttribute("data-legend-preview-active");
Array.prototype.forEach.call(
svg.querySelectorAll(
"[data-legend-preview-match], [data-legend-preview-selected], [data-legend-preview-peer]",
),
function (element) {
element.removeAttribute("data-legend-preview-match");
element.removeAttribute("data-legend-preview-selected");
element.removeAttribute("data-legend-preview-peer");
},
);
}
function strongerLegendOwnerActive() {
return (
selectedKinds.length > 0 ||
!panel.hidden ||
html.getAttribute("data-present") === "true" ||
svg.hasAttribute("data-focus-active") ||
svg.hasAttribute("data-intent-trace-active") ||
svg.hasAttribute("data-route-picking") ||
svg.hasAttribute("data-route-active") ||
svg.hasAttribute("data-story-active") ||
svg.hasAttribute("data-relationship-preview-active")
);
}
function previewLegendKind(entry) {
clearLegendPreview();
if (!entry || strongerLegendOwnerActive()) return false;
var kind = entry.getAttribute("data-legend-kind");
var byId = nodesById();
var matches = Object.keys(byId).filter(function (id) {
return (byId[id].getAttribute("data-node-kind") || "neutral") === kind;
});
if (!matches.length) return false;
var chosen = {};
matches.forEach(function (id) {
chosen[id] = true;
byId[id].setAttribute("data-legend-preview-match", "");
byId[id].setAttribute("data-legend-preview-selected", "");
});
edgeGroups().forEach(function (edge) {
if (!chosen[edge.from] && !chosen[edge.to]) return;
edge.members.forEach(function (member) {
member.setAttribute("data-legend-preview-match", "");
});
[edge.from, edge.to].forEach(function (id) {
if (!byId[id] || chosen[id]) return;
byId[id].setAttribute("data-legend-preview-match", "");
byId[id].setAttribute("data-legend-preview-peer", "");
});
});
svg.setAttribute("data-legend-preview-active", kind);
activeLegendPreview = entry;
return true;
}
function syncLegendPreview() {
var next = focusedLegendEntry || hoveredLegendEntry;
if (next === activeLegendPreview) return;
clearLegendPreview();
if (next) previewLegendKind(next);
}
function activateLegendEntry(entry) {
if (!entry || entry.getAttribute("role") !== "button") return false;
clearLegendPreview();
select(entry.getAttribute("data-legend-kind"));
return open({ opener: entry });
}
function removeFlowOverlay() {
Array.prototype.forEach.call(
svg.querySelectorAll("[data-semantic-lens-overlay]"),
function (element) {
element.remove();
},
);
svg.removeAttribute("data-lens-flow-count");
svg.removeAttribute("data-lens-flow-density");
}
function flowGeometry(shape, direction, step) {
var clone = shape.cloneNode(false);
clone.removeAttribute("id");
clone.removeAttribute("class");
clone.removeAttribute("style");
clone.removeAttribute("marker-start");
clone.removeAttribute("marker-mid");
clone.removeAttribute("marker-end");
clone.removeAttribute("role");
clone.removeAttribute("aria-label");
clone.removeAttribute("aria-hidden");
clone.removeAttribute("data-animate");
clone.removeAttribute("data-edge-from");
clone.removeAttribute("data-edge-to");
clone.removeAttribute("data-edge-key");
clone.removeAttribute("data-edge-id");
clone.removeAttribute("data-edge-label");
clone.removeAttribute("data-lens-match");
clone.setAttribute("class", "semantic-lens-flow");
clone.setAttribute("data-direction", direction);
clone.setAttribute("pathLength", "1");
clone.style.setProperty("--lens-flow-delay", (step * 0.08).toFixed(2) + "s");
return clone;
}
function renderFlowOverlay(entries) {
removeFlowOverlay();
svg.setAttribute("data-lens-flow-count", String(entries.length));
if (!entries.length || html.getAttribute("data-embed") === "true") return false;
if (entries.length > MAX_LENS_FLOW_EDGES) {
svg.setAttribute("data-lens-flow-density", "quiet");
return false;
}
var overlay = document.createElementNS(namespace, "g");
overlay.setAttribute("class", "semantic-lens-overlay");
overlay.setAttribute("data-semantic-lens-overlay", "");
overlay.setAttribute("aria-hidden", "true");
entries.forEach(function (entry, step) {
var wrapper = document.createElementNS(namespace, "g");
if (entry.edge.members[0].hasAttribute("transform")) {
wrapper.setAttribute("transform", entry.edge.members[0].getAttribute("transform"));
}
edgeShapes(entry.edge.members[0]).forEach(function (shape) {
wrapper.appendChild(flowGeometry(shape, entry.direction, step));
});
if (wrapper.childNodes.length) overlay.appendChild(wrapper);
});
if (!overlay.childNodes.length) return false;
var firstNode = svg.querySelector("[data-node-id]");
if (firstNode) svg.insertBefore(overlay, firstNode);
else svg.appendChild(overlay);
return true;
}
function cleanSvgState() {
removeFlowOverlay();
svg.removeAttribute("data-lens-active");
Array.prototype.forEach.call(
svg.querySelectorAll("[data-lens-match], [data-lens-selected], [data-lens-peer]"),
function (element) {
element.removeAttribute("data-lens-match");
element.removeAttribute("data-lens-selected");
element.removeAttribute("data-lens-peer");
},
);
}
function renderKinds() {
kindsRoot.textContent = "";
collectKinds().forEach(function (kind) {
var button = document.createElement("button");
button.type = "button";
button.className = "semantic-lens-kind";
button.setAttribute("data-kind", kind.id);
button.setAttribute(
"aria-pressed",
selectedKinds.indexOf(kind.id) >= 0 ? "true" : "false",
);
button.setAttribute(
"aria-label",
viewerCount("viewer.lens.kind.count", kind.nodes.length, { label: kind.label }),
);
button.disabled = selectedKinds.length >= 2 && selectedKinds.indexOf(kind.id) === -1;
var swatch = document.createElement("span");
swatch.className = "semantic-lens-swatch";
swatch.setAttribute("aria-hidden", "true");
var label = document.createElement("strong");
label.textContent = kind.label;
var count = document.createElement("em");
count.textContent = String(kind.nodes.length);
button.appendChild(swatch);
button.appendChild(label);
button.appendChild(count);
kindsRoot.appendChild(button);
});
}
function updateHash() {
try {
var hash = selectedKinds.length
? "#lens=" + selectedKinds.map(encodeURIComponent).join("~")
: "";
history.replaceState(null, "", location.pathname + location.search + hash);
} catch (_) {}
}
function updateTrigger() {
var active = selectedKinds.length > 0;
trigger.setAttribute("aria-pressed", active ? "true" : "false");
trigger.setAttribute(
"aria-label",
viewerText(active ? "viewer.lens.openActive" : "viewer.lens.open"),
);
copyBtn.hidden = !active;
syncLegendBridge();
}
function overlapArea(a, b) {
var width = Math.max(0, Math.min(a.right, b.right) - Math.max(a.left, b.left));
var height = Math.max(0, Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top));
return width * height;
}
function dockPanel(byId) {
panel.removeAttribute("data-dock-side");
if (panel.hidden || window.innerWidth <= 720) return "right";
var panelRect = panel.getBoundingClientRect();
var containerRect = container.getBoundingClientRect();
if (!panelRect.width || !panelRect.height) return "right";
var selectedRects = Object.keys(byId)
.filter(function (id) {
return (
selectedKinds.indexOf(byId[id].getAttribute("data-node-kind") || "neutral") >= 0
);
})
.map(function (id) {
return byId[id].getBoundingClientRect();
});
var top = panelRect.top;
var width = panelRect.width;
var leftCandidate = {
left: containerRect.left + 16,
right: containerRect.left + 16 + width,
top: top,
bottom: panelRect.bottom,
};
var rightCandidate = {
left: containerRect.right - 16 - width,
right: containerRect.right - 16,
top: top,
bottom: panelRect.bottom,
};
var leftScore = selectedRects.reduce(function (score, rect) {
return score + overlapArea(leftCandidate, rect);
}, 0);
var rightScore = selectedRects.reduce(function (score, rect) {
return score + overlapArea(rightCandidate, rect);
}, 0);
var legend = svg.querySelector("[data-legend]");
var nav = container.querySelector(".diagram-nav");
var protectedRects = [legend, nav]
.filter(function (element) {
return (
element && !element.hidden && window.getComputedStyle(element).display !== "none"
);
})
.map(function (element) {
return element.getBoundingClientRect();
});
leftScore += protectedRects.reduce(function (score, rect) {
return score + overlapArea(leftCandidate, rect) * 1000;
}, 0);
rightScore += protectedRects.reduce(function (score, rect) {
return score + overlapArea(rightCandidate, rect) * 1000;
}, 0);
var side = leftScore < rightScore ? "left" : "right";
panel.setAttribute("data-dock-side", side);
return side;
}
function applySelection(options) {
options = options || {};
cleanSvgState();
var byId = nodesById();
var nodeKind = {};
Object.keys(byId).forEach(function (id) {
nodeKind[id] = byId[id].getAttribute("data-node-kind") || "neutral";
});
if (!selectedKinds.length) {
status.textContent = viewerText("viewer.lens.choose");
updateTrigger();
renderKinds();
dockPanel(byId);
if (options.updateUrl !== false) updateHash();
return false;
}
var chosen = {};
selectedKinds.forEach(function (kind) {
chosen[kind] = true;
});
Object.keys(byId).forEach(function (id) {
if (!chosen[nodeKind[id]]) return;
byId[id].setAttribute("data-lens-match", "");
byId[id].setAttribute("data-lens-selected", "");
});
var touching = 0;
var forward = 0;
var reverse = 0;
var crossKind = selectedKinds.length === 2;
var matchedFlow = [];
edgeGroups().forEach(function (edge) {
var fromKind = nodeKind[edge.from];
var toKind = nodeKind[edge.to];
var match = false;
if (crossKind) {
if (fromKind === selectedKinds[0] && toKind === selectedKinds[1]) {
match = true;
forward += 1;
}
if (fromKind === selectedKinds[1] && toKind === selectedKinds[0]) {
match = true;
reverse += 1;
}
} else if (fromKind === selectedKinds[0] || toKind === selectedKinds[0]) {
match = true;
touching += 1;
}
if (!match) return;
var direction = "within";
if (crossKind) {
direction = fromKind === selectedKinds[0] ? "forward" : "reverse";
} else {
direction =
fromKind === selectedKinds[0] && toKind !== selectedKinds[0]
? "out"
: toKind === selectedKinds[0] && fromKind !== selectedKinds[0]
? "in"
: "within";
}
matchedFlow.push({ edge: edge, direction: direction });
edge.members.forEach(function (member) {
member.setAttribute("data-lens-match", "");
});
if (!crossKind) {
[edge.from, edge.to].forEach(function (id) {
if (!byId[id]) return;
byId[id].setAttribute("data-lens-match", "");
if (!chosen[nodeKind[id]]) byId[id].setAttribute("data-lens-peer", "");
});
}
});
svg.setAttribute("data-lens-active", selectedKinds.join(" "));
renderFlowOverlay(matchedFlow);
if (crossKind) {
var total = forward + reverse;
status.textContent = viewerCount("viewer.lens.compare", total, {
first: viewerKindLabel(selectedKinds[0]),
second: viewerKindLabel(selectedKinds[1]),
forward: forward,
reverse: reverse,
});
} else {
var nodeCount = collectKinds().filter(function (kind) {
return kind.id === selectedKinds[0];
})[0].nodes.length;
status.textContent = viewerText("viewer.lens.single", {
nodes: viewerCount("viewer.lens.node", nodeCount, {
label: viewerKindLabel(selectedKinds[0]),
}),
relationships: viewerCount("viewer.lens.relationship", touching),
});
}
updateTrigger();
renderKinds();
dockPanel(byId);
if (options.updateUrl !== false) updateHash();
return true;
}
function prepareForLens() {
clearLegendPreview();
if (Archify.focus && typeof Archify.focus.clear === "function") {
Archify.focus.clear({ updateUrl: false, preserveView: true });
}
if (Archify.routeProbe && typeof Archify.routeProbe.clear === "function") {
Archify.routeProbe.clear({ updateUrl: false, restoreFocus: false });
}
if (Archify.guidedViews && typeof Archify.guidedViews.showAll === "function") {
Archify.guidedViews.showAll({ clearFocus: false, updateUrl: false });
}
if (Archify.intentTrace && typeof Archify.intentTrace.clear === "function") {
Archify.intentTrace.clear({ announce: false });
}
}
function select(kind, options) {
options = options || {};
clearLegendPreview();
var exists = collectKinds().some(function (entry) {
return entry.id === kind;
});
if (!exists) return false;
var index = selectedKinds.indexOf(kind);
if (index >= 0) selectedKinds.splice(index, 1);
else {
if (selectedKinds.length >= 2) return false;
if (!selectedKinds.length) prepareForLens();
selectedKinds.push(kind);
}
return applySelection(options);
}
function close(options) {
options = options || {};
panel.hidden = true;
trigger.setAttribute("aria-expanded", "false");
updateTrigger();
if (
options.restoreFocus !== false &&
lensOpener &&
typeof lensOpener.focus === "function"
)
lensOpener.focus();
return false;
}
function open(options) {
options = options || {};
if (html.getAttribute("data-embed") === "true") return false;
lensOpener = options.opener || trigger;
if (Archify.exportMenu && Archify.exportMenu.isOpen()) Archify.exportMenu.close(false);
if (Archify.finder && Archify.finder.isOpen())
Archify.finder.close({ restoreFocus: false });
if (Archify.radar && Archify.radar.isOpen()) Archify.radar.close({ restoreFocus: false });
if (Archify.guide && Archify.guide.isOpen()) Archify.guide.close({ restoreFocus: false });
renderKinds();
panel.hidden = false;
trigger.setAttribute("aria-expanded", "true");
updateTrigger();
requestAnimationFrame(function () {
dockPanel(nodesById());
var activeButton = kindsRoot.querySelector('[aria-pressed="true"]');
var firstButton = activeButton || kindsRoot.querySelector("button:not(:disabled)");
if (firstButton) firstButton.focus();
});
return true;
}
function toggle() {
return panel.hidden ? open({ opener: trigger }) : close();
}
function clear(options) {
options = options || {};
selectedKinds = [];
cleanSvgState();
panel.removeAttribute("data-dock-side");
updateTrigger();
renderKinds();
status.textContent = viewerText("viewer.lens.choose");
if (options.updateUrl !== false) updateHash();
if (
options.preserveView !== true &&
Archify.view &&
typeof Archify.view.reset === "function"
) {
Archify.view.reset({ automatic: true });
}
if (options.closePanel === true) close({ restoreFocus: false });
return false;
}
function fallbackCopy(value) {
var field = document.createElement("textarea");
field.value = value;
field.setAttribute("readonly", "");
field.style.position = "fixed";
field.style.opacity = "0";
document.body.appendChild(field);
field.select();
var copied = false;
try {
copied = document.execCommand("copy");
} catch (_) {}
field.remove();
return copied;
}
function copyLink() {
if (!selectedKinds.length) return Promise.resolve(false);
var value =
location.href.replace(/#.*$/, "") +
"#lens=" +
selectedKinds.map(encodeURIComponent).join("~");
var copy =
navigator.clipboard && typeof navigator.clipboard.writeText === "function"
? navigator.clipboard
.writeText(value)
.then(function () {
return true;
})
.catch(function () {
return fallbackCopy(value);
})
: Promise.resolve(fallbackCopy(value));
return copy.then(function (copied) {
copyBtn.textContent = viewerText(
copied ? "viewer.common.copied" : "viewer.common.copyFailed",
);
window.setTimeout(function () {
copyBtn.textContent = viewerText("viewer.common.copyLink");
}, 1600);
return copied;
});
}
function syncFromHash() {
try {
var params = new URLSearchParams(location.hash.replace(/^#/, ""));
var value = params.get("lens");
if (!value) {
if (selectedKinds.length) clear({ updateUrl: false, preserveView: true });
return;
}
var available = collectKinds().map(function (kind) {
return kind.id;
});
var requested = value
.split("~")
.filter(function (kind, index, list) {
return available.indexOf(kind) >= 0 && list.indexOf(kind) === index;
})
.slice(0, 2);
if (!requested.length) return;
prepareForLens();
selectedKinds = requested;
applySelection({ updateUrl: false });
} catch (_) {}
}
trigger.addEventListener("click", toggle);
if (decorateLegendBridge()) {
legendBridge.addEventListener("click", function (event) {
var entry = event.target.closest('[data-legend-kind][role="button"]');
if (entry) activateLegendEntry(entry);
});
legendBridge.addEventListener("pointerover", function (event) {
if (event.pointerType === "touch" || (finePointerQuery && !finePointerQuery.matches))
return;
var entry = event.target.closest('[data-legend-kind][role="button"]');
if (!entry || (event.relatedTarget && entry.contains(event.relatedTarget))) return;
hoveredLegendEntry = entry;
syncLegendPreview();
});
legendBridge.addEventListener("pointerout", function (event) {
var entry = event.target.closest('[data-legend-kind][role="button"]');
if (!entry || (event.relatedTarget && entry.contains(event.relatedTarget))) return;
if (hoveredLegendEntry === entry) hoveredLegendEntry = null;
syncLegendPreview();
});
legendBridge.addEventListener("focusin", function (event) {
var entry = event.target.closest('[data-legend-kind][role="button"]');
if (!entry) return;
focusedLegendEntry = entry;
legendEntries.forEach(function (candidate) {
candidate.setAttribute("tabindex", candidate === entry ? "0" : "-1");
});
syncLegendPreview();
});
legendBridge.addEventListener("focusout", function (event) {
var entry = event.target.closest('[data-legend-kind][role="button"]');
if (!entry || (event.relatedTarget && entry.contains(event.relatedTarget))) return;
if (focusedLegendEntry === entry) focusedLegendEntry = null;
syncLegendPreview();
});
legendBridge.addEventListener("keydown", function (event) {
var entry = event.target.closest('[data-legend-kind][role="button"]');
if (!entry) return;
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
activateLegendEntry(entry);
return;
}
var index = legendEntries.indexOf(entry);
var next = null;
if (event.key === "ArrowRight") next = (index + 1) % legendEntries.length;
else if (event.key === "ArrowLeft")
next = (index - 1 + legendEntries.length) % legendEntries.length;
else if (event.key === "Home") next = 0;
else if (event.key === "End") next = legendEntries.length - 1;
if (next === null) return;
event.preventDefault();
legendEntries.forEach(function (candidate, candidateIndex) {
candidate.setAttribute("tabindex", candidateIndex === next ? "0" : "-1");
});
legendEntries[next].focus();
});
}
closeBtn.addEventListener("click", function () {
close();
});
clearBtn.addEventListener("click", function () {
clear({ preserveView: true });
});
copyBtn.addEventListener("click", copyLink);
kindsRoot.addEventListener("click", function (event) {
var button = event.target.closest("[data-kind]");
if (!button || button.disabled) return;
select(button.getAttribute("data-kind"));
});
kindsRoot.addEventListener("keydown", function (event) {
var buttons = Array.prototype.slice.call(
kindsRoot.querySelectorAll("button:not(:disabled)"),
);
var index = buttons.indexOf(document.activeElement);
var next = null;
if (index >= 0 && (event.key === "ArrowRight" || event.key === "ArrowDown"))
next = (index + 1) % buttons.length;
else if (index >= 0 && (event.key === "ArrowLeft" || event.key === "ArrowUp"))
next = (index - 1 + buttons.length) % buttons.length;
else if (event.key === "Home" && buttons.length) next = 0;
else if (event.key === "End" && buttons.length) next = buttons.length - 1;
if (next !== null) {
event.preventDefault();
buttons[next].focus();
}
});
panel.addEventListener("keydown", function (event) {
if (event.key !== "Escape") return;
event.preventDefault();
event.stopPropagation();
close();
});
document.addEventListener("click", function (event) {
var eventPath = typeof event.composedPath === "function" ? event.composedPath() : [];
var clickedInside = eventPath.indexOf(panel) >= 0 || panel.contains(event.target);
var clickedLauncher =
event.target === trigger ||
legendEntries.some(function (entry) {
return event.target === entry || entry.contains(event.target);
});
if (!panel.hidden && !clickedInside && !clickedLauncher) close({ restoreFocus: false });
});
window.addEventListener("hashchange", syncFromHash);
window.addEventListener("resize", function () {
if (!panel.hidden && selectedKinds.length) dockPanel(nodesById());
layoutLegendBridge();
});
renderKinds();
syncFromHash();
return {
open: open,
close: close,
toggle: toggle,
clear: clear,
clearPreview: clearLegendPreview,
select: select,
copyLink: copyLink,
isOpen: function () {
return !panel.hidden;
},
active: function () {
return selectedKinds.length ? selectedKinds.slice() : null;
},
kinds: function () {
return collectKinds().map(function (kind) {
return { id: kind.id, count: kind.nodes.length };
});
},
};
})();
/* ============================================================
Diagram Guide — a factual command deck over existing interactions.
Counts come from compiled semantics; actions delegate to Finder, Route
Probe, Semantic Radar, Semantic Lens, Story Trail, Presentation, theme, export, and
camera without adding a second command implementation or SVG state.
============================================================ */
Archify.guide = (function () {
var html = document.documentElement;
var container = document.querySelector(".diagram-container");
var svg = container.querySelector(":scope > svg");
var trigger = document.getElementById("btn-diagram-guide");
var panel = document.getElementById("diagram-guide");
var closeBtn = document.getElementById("diagram-guide-close");
var stats = document.getElementById("diagram-guide-stats");
var actions = document.getElementById("diagram-guide-actions");
var feedback = document.getElementById("diagram-guide-feedback");
var storyBtn = actions.querySelector('[data-guide-action="story"]');
var storyCopy = document.getElementById("diagram-guide-story-copy");
var routePanel = document.getElementById("route-probe");
function viewCount() {
return (Archify.guidedViews && Number(Archify.guidedViews.count)) || 0;
}
function relationshipCount() {
var seen = {};
Array.prototype.forEach.call(
svg.querySelectorAll("[data-edge-from][data-edge-to]"),
function (edge) {
var key =
edge.getAttribute("data-edge-key") ||
[
edge.getAttribute("data-edge-from"),
edge.getAttribute("data-edge-to"),
edge.getAttribute("data-edge-label") || "",
].join("\u0000");
seen[key] = true;
},
);
return Object.keys(seen).length;
}
function renderFacts() {
var nodes = svg.querySelectorAll("[data-node-id]").length;
var relationships = relationshipCount();
var views = viewCount();
stats.textContent = viewerText("viewer.guide.facts", {
nodes: viewerCount("viewer.guide.fact.node", nodes),
relationships: viewerCount("viewer.guide.fact.relationship", relationships),
views: viewerCount("viewer.guide.fact.view", views),
});
storyBtn.disabled = views === 0;
storyBtn.setAttribute("aria-disabled", views === 0 ? "true" : "false");
storyCopy.textContent = views
? viewerCount("viewer.guide.story.available", views)
: viewerText("viewer.guide.story.unavailable");
}
function actionButtons() {
return Array.prototype.slice.call(
actions.querySelectorAll(".diagram-guide-action:not(:disabled)"),
);
}
function close(options) {
options = options || {};
panel.hidden = true;
html.removeAttribute("data-guide-open");
routePanel.removeAttribute("data-guide-open");
trigger.setAttribute("aria-expanded", "false");
trigger.setAttribute("aria-label", viewerText("viewer.guide.open"));
feedback.textContent = "";
if (options.restoreFocus !== false) trigger.focus();
return false;
}
function open() {
if (html.getAttribute("data-embed") === "true") return false;
if (Archify.semanticLens && typeof Archify.semanticLens.clearPreview === "function")
Archify.semanticLens.clearPreview();
if (Archify.exportMenu && Archify.exportMenu.isOpen()) Archify.exportMenu.close(false);
if (Archify.finder && Archify.finder.isOpen())
Archify.finder.close({ restoreFocus: false });
if (Archify.radar && Archify.radar.isOpen()) Archify.radar.close({ restoreFocus: false });
if (Archify.semanticLens && Archify.semanticLens.isOpen())
Archify.semanticLens.close({ restoreFocus: false });
if (
Archify.guidedViews &&
Archify.guidedViews.isPlaying &&
Archify.guidedViews.isPlaying()
) {
Archify.guidedViews.pause();
}
if (
Archify.routeProbe &&
Archify.routeProbe.isJourneyPlaying &&
Archify.routeProbe.isJourneyPlaying()
) {
Archify.routeProbe.pauseJourney({ preserveElapsed: true, reason: "guide" });
}
renderFacts();
panel.hidden = false;
html.setAttribute("data-guide-open", "true");
routePanel.setAttribute("data-guide-open", "true");
trigger.setAttribute("aria-expanded", "true");
trigger.setAttribute("aria-label", viewerText("viewer.guide.close"));
feedback.textContent = "";
requestAnimationFrame(function () {
var first = actionButtons()[0];
if (first) first.focus();
});
return true;
}
function toggle() {
return panel.hidden ? open() : close();
}
function execute(action) {
feedback.textContent = "";
if (action === "story" && !viewCount()) {
feedback.textContent = viewerText("viewer.guide.noStory");
return false;
}
close({ restoreFocus: false });
if (action === "find") return Archify.finder.open();
if (action === "route") return Archify.routeProbe.begin({ focusNode: true });
if (action === "map") return Archify.radar.open();
if (action === "lens") return Archify.semanticLens.open();
if (action === "story") return Archify.guidedViews.play();
if (action === "present") return Archify.presentation.enter();
if (action === "export") return Archify.exportMenu.open();
if (action === "theme") return Archify.theme.toggle();
if (action === "preset") return Archify.preset.cycle();
if (action === "reset") return Archify.view.reset();
if (action === "zoom-in") return Archify.view.zoomIn();
if (action === "zoom-out") return Archify.view.zoomOut();
return false;
}
function actionForKey(key) {
return (
{
"/": "find",
r: "route",
m: "map",
l: "lens",
p: "story",
f: "present",
e: "export",
t: "theme",
s: "preset",
0: "reset",
"+": "zoom-in",
"=": "zoom-in",
"-": "zoom-out",
}[key] || null
);
}
trigger.addEventListener("click", toggle);
closeBtn.addEventListener("click", function () {
close();
});
actions.addEventListener("click", function (event) {
var button = event.target.closest("[data-guide-action]");
if (!button || button.disabled) return;
event.stopPropagation();
execute(button.getAttribute("data-guide-action"));
});
panel.addEventListener("keydown", function (event) {
if (event.key === "Escape" || event.key === "?") {
event.preventDefault();
event.stopPropagation();
close();
return;
}
var buttons = actionButtons();
var index = buttons.indexOf(document.activeElement);
var next = null;
if (index >= 0 && event.key === "ArrowRight") next = (index + 1) % buttons.length;
else if (index >= 0 && event.key === "ArrowDown") next = (index + 1) % buttons.length;
else if (index >= 0 && event.key === "ArrowLeft")
next = (index - 1 + buttons.length) % buttons.length;
else if (index >= 0 && event.key === "ArrowUp")
next = (index - 1 + buttons.length) % buttons.length;
else if (event.key === "Home" && buttons.length) next = 0;
else if (event.key === "End" && buttons.length) next = buttons.length - 1;
if (next !== null) {
event.preventDefault();
event.stopPropagation();
buttons[next].focus();
return;
}
var action = actionForKey(event.key.length === 1 ? event.key.toLowerCase() : event.key);
if (!action) return;
event.preventDefault();
event.stopPropagation();
execute(action);
});
document.addEventListener("click", function (event) {
if (!panel.hidden && !panel.contains(event.target) && event.target !== trigger)
close({ restoreFocus: false });
});
renderFacts();
return {
open: open,
close: close,
toggle: toggle,
execute: execute,
isOpen: function () {
return !panel.hidden;
},
facts: function () {
return {
nodes: svg.querySelectorAll("[data-node-id]").length,
relationships: relationshipCount(),
views: viewCount(),
};
},
};
})();
/* ============================================================
Global keyboard shortcuts
? -> open Diagram Guide
T -> toggle theme
S -> cycle visual style
E -> open export menu
F -> toggle presentation stage
M -> toggle Semantic Radar
L -> toggle Semantic Lens
R -> start/clear Route Probe
/ -> open node finder or the active route endpoint picker
+ / - -> zoom, 0 -> reset view
Escape -> clear temporary trace/focus/view first, then exit presentation
Ignored when focus is inside an input/textarea/contenteditable.
============================================================ */
document.addEventListener("keydown", function (e) {
if (e.metaKey || e.ctrlKey || e.altKey) return;
if (e.defaultPrevented) return;
var t = e.target;
if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
if (
Archify.preset &&
Archify.preset.isOpen() &&
e.key !== "Escape" &&
e.key !== "s" &&
e.key !== "S"
) {
Archify.preset.close(false);
}
if (e.key === "?") {
e.preventDefault();
Archify.guide.toggle();
} else if (e.key === "/") {
e.preventDefault();
Archify.finder.open();
} else if (e.key === "t" || e.key === "T") {
e.preventDefault();
Archify.theme.toggle();
} else if (e.key === "s" || e.key === "S") {
e.preventDefault();
Archify.preset.cycle();
} else if (e.key === "e" || e.key === "E") {
e.preventDefault();
if (!Archify.exportMenu.isOpen()) Archify.exportMenu.open();
} else if (e.key === "f" || e.key === "F") {
e.preventDefault();
Archify.presentation.toggle();
} else if (e.key === "m" || e.key === "M") {
e.preventDefault();
Archify.radar.toggle();
} else if (e.key === "l" || e.key === "L") {
e.preventDefault();
Archify.semanticLens.toggle();
} else if (e.key === "r" || e.key === "R") {
e.preventDefault();
Archify.routeProbe.toggle({ focusNode: true });
} else if (e.key === "+" || e.key === "=") {
e.preventDefault();
Archify.view.zoomIn();
} else if (e.key === "-") {
e.preventDefault();
Archify.view.zoomOut();
} else if (e.key === "0") {
e.preventDefault();
Archify.view.reset();
} else if (e.key === "Escape" && Archify.preset.isOpen()) {
e.preventDefault();
Archify.preset.close(true);
} else if (e.key === "Escape" && Archify.semanticLens.isOpen()) {
e.preventDefault();
Archify.semanticLens.close({ restoreFocus: true });
} else if (e.key === "Escape" && Archify.semanticLens.active()) {
e.preventDefault();
Archify.semanticLens.clear({ preserveView: true });
} else if (e.key === "Escape" && Archify.guide.isOpen()) {
e.preventDefault();
Archify.guide.close({ restoreFocus: true });
} else if (e.key === "Escape" && Archify.radar.isOpen()) {
e.preventDefault();
Archify.radar.close({ restoreFocus: true });
} else if (e.key === "Escape" && Archify.routeProbe.active()) {
e.preventDefault();
Archify.routeProbe.escape({ restoreFocus: true });
} else if (e.key === "Escape" && Archify.intentTrace.active()) {
e.preventDefault();
Archify.intentTrace.clear();
} else if (e.key === "Escape" && Archify.focus.active()) {
e.preventDefault();
Archify.focus.clear({ restoreFocus: true });
} else if (e.key === "Escape" && Archify.presentation.active()) {
e.preventDefault();
Archify.presentation.exit();
}
});
</script>
</body>
</html>