Files
documentation/vendor/archify/examples/sequence-cache-miss-request.html
T
jarvis-at-skic 9ad62a2ed6
Build and publish Eldorado documentation / build (pull_request) Successful in 29s
feat: add section navigation and embedded Archify diagrams
2026-10-02 10:33:55 +00:00

13836 lines
744 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en" data-theme="dark" data-preset="classic">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="generator" content="archify 3.0.1">
<meta name="archify-reader-contract" content="declared-wide-v1">
<title>Cache Miss Request Sequence 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 = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
}
document.documentElement.setAttribute('data-theme', theme);
} catch (_) {}
})();
</script>
<style id="archify-fonts">
/*
JetBrains Mono variable WOFF2 subsets from Google Fonts service revision v24.
These fixed bytes are shared by the viewer and its SVG/raster exports.
No local() source: an installed copy must not override the bundled font.
Uncovered characters (including CJK) still use the system fallback stack.
Upstream: https://github.com/JetBrains/JetBrainsMono
cyrillic-ext 1664 bytes sha256 9343de2ca5d9549f792e7962375af8efb0f320c7643bfd36c884b5a30e5c396f
cyrillic 8892 bytes sha256 4995a9a43ac659ec32fcd8b463755cd6a07b31a6e6b3894a6a153b661cf490e2
greek 6800 bytes sha256 49c3da6c9a2b279b0f1f860f5cfb1f5dc38d88a5c7be9c9b1837bbc4e3db6111
vietnamese 5872 bytes sha256 d44eb1936043a56038eb02dd70b243f379bef65783f94ec12f277550720411f1
latin-ext 11596 bytes sha256 9c38cb2d0d2d93c1ee6e21fa78db76f13ea7e15e15cc64214c7ca89b6aaa35c4
latin 31340 bytes sha256 2c32b9b3ee358c119e210f6f5195f9bd34894d78a785ff2e95d60e718e400af4
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
*/
/* cyrillic-ext */
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 400 800; font-display: swap;
src: url(data:font/woff2;base64,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) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; }
/* cyrillic */
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 400 800; font-display: swap;
src: url(data:font/woff2;base64,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) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; }
/* greek */
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 400 800; font-display: swap;
src: url(data:font/woff2;base64,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) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; }
/* vietnamese */
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 400 800; font-display: swap;
src: url(data:font/woff2;base64,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) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; }
/* latin-ext */
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 400 800; font-display: swap;
src: url(data:font/woff2;base64,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) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
/* latin */
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 400 800; font-display: swap;
src: url(data:font/woff2;base64,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) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
</style>
<style>
/* ==========================================================
THEME VARIABLES — switch by toggling [data-theme] on <html>
========================================================== */
:root,
[data-theme="dark"] {
--bg: #020617;
--grid: #1e293b;
--canvas-dot: rgba(148, 163, 184, 0.16);
--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: #f4f5f7;
--grid: #e9edf2;
--canvas-dot: #d9dee5;
--text: #111827;
--text-muted: #5b6474;
--text-dim: #9aa3b2;
--text-faint: #6b7280;
--panel: #ffffff;
--panel-border: #e5e7eb;
--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 {
--archify-page-top: 2rem;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Noto Sans CJK SC', 'Noto Sans', Roboto, 'Helvetica Neue', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
background: var(--bg);
min-height: 100vh;
padding: var(--archify-page-top) 2rem 2rem;
color: var(--text);
/* Theme surfaces change together; animating the page background leaves it
visibly out of sync with the SVG and panel for the first 200ms. */
}
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"] .node-outline { 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; }
/* 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 {
--archify-page-top: 1rem;
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"]) .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 and measured tall workflows 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;
}
html[data-reader-narrow="true"]:not([data-present="true"]) .diagram-container > svg {
min-width: 0;
max-width: var(--archify-diagram-max-width);
margin-inline: 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: 0.75rem;
min-height: 2.75rem;
margin-bottom: 0.25rem;
}
.pulse-dot {
flex: none;
width: 10px;
height: 10px;
background: var(--frontend-stroke);
border-radius: 50%;
box-shadow: 0 0 0 4px color-mix(in srgb, var(--frontend-stroke) 16%, transparent);
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: 650;
line-height: 1.25;
letter-spacing: -0.015em;
}
.subtitle {
color: var(--text-muted);
font-size: 0.875rem;
line-height: 1.55;
margin-left: 1.375rem;
}
html[data-preset="signal-flow"] .header-row::after {
content: attr(data-preset-badge-signal-flow);
flex: none;
margin-left: 0.25rem;
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);
flex: none;
margin-left: 0.25rem;
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);
flex: none;
margin-left: 0.25rem;
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);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 16px 40px -20px rgba(15, 23, 42, 0.18);
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:not([data-motion-capable="true"]) svg[data-animation="trace"] [data-graph-role="automatic-crossover"]:has(> [data-animate="edge"]) > [data-graph-role="automatic-crossover-underlay"] {
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],
html[data-motion="still"] svg[data-animation="trace"] [data-graph-role="automatic-crossover"]:has(> [data-animate="edge"]) > [data-graph-role="automatic-crossover-underlay"],
html[data-motion-owner] svg[data-animation="trace"] [data-graph-role="automatic-crossover"]:has(> [data-animate="edge"]) > [data-graph-role="automatic-crossover-underlay"],
html[data-embed="true"] svg[data-animation="trace"] [data-graph-role="automatic-crossover"]:has(> [data-animate="edge"]) > [data-graph-role="automatic-crossover-underlay"],
html[data-share-playback="true"] svg[data-animation="trace"] [data-graph-role="automatic-crossover"]:has(> [data-animate="edge"]) > [data-graph-role="automatic-crossover-underlay"],
html[data-document-hidden="true"] svg[data-animation="trace"] [data-graph-role="automatic-crossover"]:has(> [data-animate="edge"]) > [data-graph-role="automatic-crossover-underlay"] {
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"] .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"] .overview-map-live,
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"] .overview-map-live {
animation: none !important;
}
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 { display: none !important; }
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 {
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;
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;
translate: 0 calc(-1 * var(--archify-dock-lift, 0px));
display: inline-flex;
align-items: center;
max-width: calc(100% - 2rem);
padding: 0.25rem;
gap: 0.0625rem;
overflow-x: auto;
border: 1px solid color-mix(in srgb, var(--panel-border) 80%, transparent);
border-radius: 0.75rem;
background: color-mix(in srgb, var(--panel) 84%, transparent);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05), 0 10px 28px -14px rgba(15, 23, 42, 0.28);
backdrop-filter: blur(14px) saturate(1.2);
scrollbar-width: none;
}
.diagram-nav::-webkit-scrollbar { display: none; }
.diagram-nav button,
.focus-chip button {
border: 0;
background: transparent;
color: var(--toolbar-text);
font: inherit;
cursor: pointer;
}
.diagram-nav button {
min-width: 2rem;
height: 2rem;
padding: 0 0.4rem;
border-radius: 0.5rem;
color: var(--text-muted);
font-size: 0.75rem;
font-weight: 500;
letter-spacing: 0;
transition: background 0.15s, color 0.15s;
}
.diagram-nav button:hover,
.diagram-nav button:focus-visible {
color: var(--text);
background: color-mix(in srgb, var(--text) 6%, transparent);
}
.diagram-nav button:hover,
.diagram-nav button:focus-visible,
.focus-chip button:hover,
.focus-chip button:focus-visible { background: var(--toolbar-hover); }
.diagram-nav button:focus-visible,
.focus-chip button:focus-visible {
outline: 2px solid var(--frontend-stroke);
outline-offset: 1px;
}
.diagram-nav button:disabled { opacity: 0.38; cursor: default; }
/* The Guide stays on the ? shortcut; Map only earns dock space once the
reader has zoomed past the full diagram (or opened it from the keyboard). */
.diagram-nav button[hidden] { display: none; }
.diagram-container:not(.is-pannable) #btn-overview-map:not([data-radar-requested]):not(:focus) { display: none; }
.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.6rem;
color: var(--text-muted);
font-weight: 500;
}
.diagram-nav #btn-node-finder,
.diagram-nav [data-view="out"] {
position: relative;
margin-left: 0.375rem;
}
.diagram-nav #btn-node-finder::before,
.diagram-nav [data-view="out"]::before {
position: absolute;
top: 0.55rem;
bottom: 0.55rem;
left: -0.19rem;
width: 1px;
background: color-mix(in srgb, var(--panel-border) 90%, transparent);
content: "";
pointer-events: none;
}
.diagram-nav-icon {
display: block;
width: 0.875rem;
height: 0.875rem;
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: var(--text-muted);
font-size: 0.75rem;
font-weight: 500;
font-variant-numeric: tabular-nums;
line-height: 1;
}
/* Mode buttons carry a small glyph so the dock reads as one icon language. */
.diagram-nav #btn-route-probe,
.diagram-nav #btn-overview-map,
.diagram-nav #btn-semantic-lens {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding-inline: 0.55rem 0.65rem;
}
.diagram-nav #btn-route-probe::before,
.diagram-nav #btn-overview-map::before,
.diagram-nav #btn-semantic-lens::before {
flex: none;
width: 0.875rem;
height: 0.875rem;
background: currentColor;
content: "";
opacity: 0.85;
-webkit-mask: var(--nav-glyph) center / contain no-repeat;
mask: var(--nav-glyph) center / contain no-repeat;
}
.diagram-nav #btn-route-probe { --nav-glyph: 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='4.8' cy='14.8' r='2.2'/%3E%3Ccircle cx='15.2' cy='5.2' r='2.2'/%3E%3Cpath d='M6.9 13.9c3.8-1.4 2.6-6.3 6.2-7.8'/%3E%3C/g%3E%3C/svg%3E"); }
.diagram-nav #btn-overview-map { --nav-glyph: 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-linejoin='round'%3E%3Cpath d='M2.8 5.4 7.4 3.5l5.2 2 4.6-1.9v11l-4.6 1.9-5.2-2-4.6 1.9z'/%3E%3Cpath d='M7.4 3.5v11M12.6 5.5v11'/%3E%3C/g%3E%3C/svg%3E"); }
.diagram-nav #btn-semantic-lens { --nav-glyph: 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'%3E%3Ccircle cx='10' cy='10' r='6.8'/%3E%3Ccircle cx='10' cy='10' r='2.6'/%3E%3C/g%3E%3C/svg%3E"); }
/* Zoom out / level / zoom in join into one stepper well. */
.diagram-nav [data-view="out"],
.diagram-nav [data-view="reset"],
.diagram-nav [data-view="in"] {
border-radius: 0;
background: color-mix(in srgb, var(--text) 4.5%, transparent);
}
.diagram-nav [data-view="out"] { margin-left: 0.375rem; border-radius: 0.5rem 0 0 0.5rem; }
.diagram-nav [data-view="out"]::before { display: none; }
.diagram-nav [data-view="reset"],
.diagram-nav [data-view="in"] { margin-left: -0.0625rem; }
.diagram-nav [data-view="in"] { border-radius: 0 0.5rem 0.5rem 0; }
.diagram-nav [data-view="out"]:hover,
.diagram-nav [data-view="reset"]:hover,
.diagram-nav [data-view="in"]:hover { background: color-mix(in srgb, var(--text) 9%, transparent); }
.diagram-nav [data-view="out"]:disabled,
.diagram-nav [data-view="in"]:disabled { opacity: 1; color: color-mix(in srgb, var(--text-muted) 40%, transparent); }
.diagram-nav [data-view="out"]:disabled:hover,
.diagram-nav [data-view="in"]:disabled:hover { background: color-mix(in srgb, var(--text) 4.5%, transparent); }
.diagram-nav [data-view="reset"] { min-width: 3.25rem; }
.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: color-mix(in srgb, var(--frontend-stroke) 11%, transparent);
}
.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: color-mix(in srgb, var(--backend-stroke) 11%, transparent);
}
.diagram-nav #btn-semantic-lens[aria-expanded="true"],
.diagram-nav #btn-semantic-lens[aria-pressed="true"] {
color: var(--database-stroke);
background: color-mix(in srgb, var(--database-stroke) 11%, transparent);
}
/* 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.75rem;
}
.route-probe[hidden] { display: none; }
/* Picking needs one line of guidance, not a wide empty card; the panel
widens only when there is a traced route to show. */
.route-probe:not([data-state="result"]) { width: min(22rem, calc(100% - 2rem)); min-width: 0; }
.route-probe-path:has(> .route-probe-placeholder:only-child) { 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.625rem 0.75rem 0.5rem;
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.6875rem;
font-weight: 600;
letter-spacing: 0.02em;
}
.route-probe-title {
display: block;
margin-top: 0.14rem;
overflow: hidden;
color: var(--toolbar-text);
font-size: 0.875rem;
font-weight: 600;
line-height: 1.3;
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.75rem;
}
.route-probe-status {
display: block;
min-height: 1.45rem;
padding: 0.5rem 0.75rem 0.625rem;
color: var(--text-muted, var(--text-faint));
font-size: 0.75rem;
line-height: 1.45;
}
.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 {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: start;
gap: 0.75rem;
padding: 0.9rem 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-drag-handle {
position: absolute;
top: 0.12rem;
left: 50%;
width: 2.5rem;
height: 0.78rem;
padding: 0 !important;
transform: translateX(-50%);
color: var(--text-faint) !important;
cursor: grab;
touch-action: none;
user-select: none;
}
.relationship-lens-drag-handle::before {
content: "";
display: block;
width: 1.25rem;
margin: auto;
border-top: 2px dotted currentColor;
opacity: 0.72;
}
.focus-chip[data-panel-dragging="true"] .relationship-lens-drag-handle { cursor: grabbing; }
@media (hover: none), (pointer: coarse) {
.relationship-lens-drag-handle { display: none; }
}
.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;
}
a.semantic-passport-source:hover,
a.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="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;
}
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
gap: 1rem;
margin-top: 2rem;
}
.card {
background: var(--panel);
border-radius: 0.75rem;
border: 1px solid var(--panel-border);
padding: 1.125rem 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; }
body { --archify-page-top: 1.25rem; }
.header { margin-bottom: 0.875rem; }
.cards { margin-top: 0.75rem; }
html[data-nav-stage-rail="true"] body { padding-block: 0.375rem; }
/* Align the fixed toolbar with the compact title row. Reserve its hit area
above the guide without taking extra height from a fitted diagram. */
html[data-nav-stage-rail="true"] .toolbar { top: 0.25rem; }
html[data-nav-stage-rail="true"] .header { min-height: 2.625rem; margin-bottom: 0.5rem; }
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); }
/* Summary rail: the reader measures the viewport and enables it only when the
diagram keeps its readable width beside the notes. */
.reader-rail { display: contents; }
html[data-reader-rail="true"] .container {
display: grid;
grid-template-columns: minmax(0, 1fr) var(--archify-rail-width);
grid-auto-flow: row dense;
column-gap: var(--archify-rail-gap);
align-items: start;
}
html[data-reader-rail="true"] .container > .header { grid-column: 1 / -1; }
html[data-reader-rail="true"] .container > .diagram-container { grid-column: 1; }
/* The rail is at least the diagram's height and may run on to the viewport
floor (--archify-rail-max, set by the reader); past that, notes scroll and
the index keeps a usable share instead of collapsing to its header. */
html[data-reader-rail="true"] .reader-rail {
display: flex;
flex-direction: column;
grid-column: 2;
align-self: stretch;
min-height: 0;
max-height: var(--archify-rail-max, none);
}
html:is([data-reader-rail="true"], [data-reader-rail="overlay"]) .cards:empty { display: none; }
html:is([data-reader-rail="true"], [data-reader-rail="overlay"]) .cards {
display: flex;
flex: 0 1 auto;
flex-direction: column;
gap: 0;
min-height: 0;
margin-top: 0;
padding: 0.125rem 0 0;
overflow-y: auto;
scrollbar-width: none;
-webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 1.25rem), transparent);
mask-image: linear-gradient(to bottom, #000 calc(100% - 1.25rem), transparent);
}
html:is([data-reader-rail="true"], [data-reader-rail="overlay"]) .cards::-webkit-scrollbar { display: none; }
html:is([data-reader-rail="true"], [data-reader-rail="overlay"]) .cards .card { flex: none; }
html:is([data-reader-rail="true"], [data-reader-rail="overlay"]) .cards .card {
padding: 0 0.25rem 1.125rem;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
html:is([data-reader-rail="true"], [data-reader-rail="overlay"]) .cards .card + .card {
padding-top: 1.125rem;
border-top: 1px solid var(--panel-border);
}
html:is([data-reader-rail="true"], [data-reader-rail="overlay"]) .cards .card::before { display: none; }
html:is([data-reader-rail="true"], [data-reader-rail="overlay"]) .card-header { gap: 0.55rem; margin-bottom: 0.45rem; }
html:is([data-reader-rail="true"], [data-reader-rail="overlay"]) .cards .card h3 { font-size: 0.875rem; letter-spacing: -0.005em; }
html:is([data-reader-rail="true"], [data-reader-rail="overlay"]) .cards .card ul { font-size: 0.8125rem; line-height: 1.6; }
.node-outline { display: none; }
html:is([data-reader-rail="true"], [data-reader-rail="overlay"]) .node-outline:not([hidden]) {
display: flex;
flex: 1 0 min(13rem, 55%);
flex-direction: column;
min-height: 0;
margin-top: 0.25rem;
overflow: hidden;
border-top: 1px solid var(--panel-border);
}
html:is([data-reader-rail="true"], [data-reader-rail="overlay"]) .cards:empty + .node-outline:not([hidden]) { margin-top: 0; border-top: 0; }
/* Collapsed: only the reveal control remains, so the diagram takes the width. */
html[data-reader-rail="collapsed"] .reader-rail { display: none; }
/* Overlay: a drawer for diagrams whose readable floor leaves no room to dock. */
html[data-reader-rail="overlay"] .reader-rail {
position: fixed;
z-index: 150;
top: 1rem;
right: 1rem;
bottom: 1rem;
display: flex;
flex-direction: column;
width: var(--archify-rail-width);
padding: 0.75rem 1rem 0.5rem;
border: 1px solid color-mix(in srgb, var(--panel-border) 90%, transparent);
border-radius: 1rem;
background: var(--toolbar-menu-bg);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06), 0 28px 64px -18px rgba(15, 23, 42, 0.38);
animation: rail-drawer-in 0.2s cubic-bezier(0.22, 1, 0.36, 1);
}
@keyframes rail-drawer-in {
from { opacity: 0; transform: translateX(0.75rem); }
to { opacity: 1; transform: translateX(0); }
}
/* Bottom (default): the notes keep their full-width card row and the index
follows as one panel whose groups flow into columns, so nothing scrolls
inside it. The placement toggle sits in the index header. */
html[data-reader-rail="bottom"] .reader-rail {
display: grid;
grid-template-areas: "cards" "index";
grid-template-columns: minmax(0, 1fr);
row-gap: 1rem;
margin-top: 1rem;
}
html[data-reader-rail="bottom"] .cards { grid-area: cards; margin-top: 0; }
html[data-reader-rail="bottom"] .cards:empty { display: none; }
html[data-reader-rail="bottom"] .node-outline:not([hidden]) {
display: block;
grid-area: index;
padding: 0 1.25rem 0.75rem;
border: 1px solid var(--panel-border);
border-radius: 0.75rem;
background: var(--panel);
}
html[data-reader-rail="bottom"] .reader-rail > .rail-controls {
z-index: 1;
grid-area: index;
align-self: start;
justify-self: end;
margin: 0.625rem 0.625rem 0 0;
}
html[data-reader-rail="bottom"] .node-outline-head { padding: 1rem 2.5rem 0.375rem 0.25rem; }
html[data-reader-rail="bottom"] .node-outline-list {
columns: 15rem;
column-gap: 1.75rem;
padding: 0;
overflow: visible;
-webkit-mask-image: none;
mask-image: none;
}
/* Named groups stay whole; a single unnamed group may split across columns,
where a continuous spine would read as a broken line. */
html[data-reader-rail="bottom"] .node-outline-group + .node-outline-group { margin-top: 0; }
html[data-reader-rail="bottom"] .node-outline-group:has(> .node-outline-heading) { break-inside: avoid; padding-bottom: 0.25rem; }
html[data-reader-rail="bottom"] .node-outline-group:not(:has(> .node-outline-heading)) > ul::before { display: none; }
html[data-reader-rail="bottom"] .node-outline-group li { break-inside: avoid; }
.rail-controls { display: none; }
html:is([data-reader-rail="true"], [data-reader-rail="overlay"], [data-reader-rail="bottom"]) .rail-controls {
display: flex;
flex: none;
justify-content: flex-end;
gap: 0.125rem;
margin: -0.25rem -0.25rem 0.25rem 0;
}
.rail-control {
display: inline-grid;
place-items: center;
width: 1.875rem;
height: 1.875rem;
border: 0;
border-radius: 0.5rem;
background: transparent;
color: var(--text-faint);
cursor: pointer;
transition: background 0.14s ease, color 0.14s ease;
}
.rail-control[hidden] { display: none; }
.rail-control:hover,
.rail-control:focus-visible { color: var(--text); background: color-mix(in srgb, var(--text) 6%, transparent); }
.rail-control:focus-visible { outline: 1.5px solid color-mix(in srgb, var(--frontend-stroke) 60%, transparent); outline-offset: -1.5px; }
.rail-control-icon,
.rail-reveal-icon {
width: 1rem;
height: 1rem;
background: currentColor;
-webkit-mask: var(--rail-glyph) center / contain no-repeat;
mask: var(--rail-glyph) center / contain no-repeat;
}
/* Panel glyphs: a frame with the panel drawn on the side it moves to. */
.rail-control-icon.placement,
#rail-placement[data-placement="right"] .rail-control-icon { --rail-glyph: 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.6'%3E%3Crect x='2.8' y='3.8' width='14.4' height='12.4' rx='2'/%3E%3Cpath d='M2.8 12h14.4'/%3E%3C/g%3E%3C/svg%3E"); }
#rail-placement[data-placement="bottom"] .rail-control-icon { --rail-glyph: 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.6'%3E%3Crect x='2.8' y='3.8' width='14.4' height='12.4' rx='2'/%3E%3Cpath d='M12.6 3.8v12.4'/%3E%3C/g%3E%3C/svg%3E"); }
.rail-control-icon.collapse { --rail-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='m8 5.5 4.5 4.5L8 14.5' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
.rail-reveal-icon { --rail-glyph: 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.6'%3E%3Crect x='2.8' y='3.8' width='14.4' height='12.4' rx='2'/%3E%3Cpath d='M12.6 3.8v12.4M14.4 7.2h1M14.4 9.6h1M14.4 12h1'/%3E%3C/g%3E%3C/svg%3E"); }
.rail-reveal[hidden] { display: none; }
/* The collapsed rail leaves one quiet pill in the canvas corner. */
.rail-reveal {
position: absolute;
z-index: 13;
top: 0.75rem;
right: 0.75rem;
display: inline-flex;
align-items: center;
gap: 0.45rem;
min-height: 2rem;
padding: 0 0.75rem 0 0.625rem;
border: 1px solid color-mix(in srgb, var(--panel-border) 85%, transparent);
border-radius: 999px;
background: color-mix(in srgb, var(--panel) 86%, transparent);
color: var(--text-muted);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05), 0 8px 20px -12px rgba(15, 23, 42, 0.28);
backdrop-filter: blur(12px);
font: inherit;
font-size: 0.75rem;
font-weight: 500;
cursor: pointer;
transition: color 0.14s ease, background 0.14s ease;
}
.rail-reveal:hover,
.rail-reveal:focus-visible { color: var(--text); background: var(--panel); }
.rail-reveal:focus-visible { outline: 1.5px solid color-mix(in srgb, var(--frontend-stroke) 60%, transparent); outline-offset: 2px; }
html[data-motion="still"] .reader-rail { animation: none !important; }
.node-outline-head {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 1.125rem 0.25rem 0.375rem;
}
.node-outline-head strong {
color: var(--text);
font-size: 0.875rem;
font-weight: 600;
letter-spacing: -0.005em;
}
.node-outline-count {
min-width: 1.35rem;
padding: 0.1rem 0.4rem;
border-radius: 999px;
background: color-mix(in srgb, var(--text) 6%, transparent);
color: var(--text-muted);
font-size: 0.6875rem;
font-weight: 600;
line-height: 1.3;
text-align: center;
font-variant-numeric: tabular-nums;
}
.node-outline-list {
flex: 1 1 auto;
min-height: 0;
margin: 0 -0.375rem;
padding: 0 0 1.25rem;
overflow-y: auto;
list-style: none;
scrollbar-width: none;
-webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 2rem), transparent);
mask-image: linear-gradient(to bottom, #000 calc(100% - 2rem), transparent);
}
.node-outline-list::-webkit-scrollbar { display: none; }
.node-outline-group + .node-outline-group { margin-top: 0.25rem; }
.node-outline-group > ul {
position: relative;
list-style: none;
}
/* A quiet spine ties each group's swatches together like a lane. */
.node-outline-group > ul::before {
position: absolute;
top: 0.95rem;
bottom: 0.95rem;
left: calc(0.625rem + 0.3125rem - 0.5px);
width: 1px;
background: var(--panel-border);
content: "";
}
.node-outline-heading {
display: flex;
align-items: baseline;
justify-content: space-between;
padding: 0.7rem 0.625rem 0.25rem;
color: var(--text-faint);
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.02em;
}
.node-outline-heading small {
color: var(--text-dim);
font-size: 0.6875rem;
font-weight: 500;
font-variant-numeric: tabular-nums;
}
/* The rail frees the vertical budget, so compact viewports keep the normal
title spacing instead of the stacked layout's tight stage rail. */
html[data-reader-rail]:not([data-reader-rail="bottom"]) body { --archify-page-top: 1.5rem; padding-block: 1.5rem; }
html[data-reader-rail]:not([data-reader-rail="bottom"]) .toolbar { top: calc(var(--archify-page-top) - 0.1875rem); }
html[data-reader-rail]:not([data-reader-rail="bottom"]) .header { min-height: 0; margin-bottom: 1.125rem; }
.node-outline-item {
position: relative;
display: grid;
grid-template-columns: 0.625rem minmax(0, auto) minmax(0, 1fr) 0.75rem;
align-items: center;
column-gap: 0.625rem;
width: 100%;
min-height: 1.95rem;
padding: 0.28rem 0.5rem 0.28rem 0.625rem;
border: 0;
border-radius: 0.5rem;
background: transparent;
color: var(--text);
font: inherit;
text-align: left;
cursor: pointer;
transition: background 0.16s ease;
}
/* Trailing chevron hints that a click focuses the node. */
.node-outline-item::after {
grid-column: 4;
width: 0.75rem;
height: 0.75rem;
background: currentColor;
color: var(--text-muted);
content: "";
opacity: 0;
transform: translateX(-3px);
transition: opacity 0.16s ease, transform 0.16s ease;
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='m4.5 3 3 3-3 3' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='m4.5 3 3 3-3 3' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
}
.node-outline-item:hover,
.node-outline-item:focus-visible,
.node-outline-item[data-previewing] { background: color-mix(in srgb, var(--text) 4.5%, transparent); }
.node-outline-item:hover::after,
.node-outline-item:focus-visible::after { opacity: 0.7; transform: translateX(0); }
.node-outline-item:focus-visible { outline: 1.5px solid color-mix(in srgb, var(--outline-tone) 60%, transparent); outline-offset: -1.5px; }
.node-outline-item[aria-current="true"] { background: color-mix(in srgb, var(--outline-tone) 10%, transparent); }
.node-outline-item[aria-current="true"]::after { opacity: 0; }
/* Swatches echo the diagram's node cards: tinted fill, hairline stroke. */
.node-outline-dot {
width: 0.625rem;
height: 0.5rem;
border: 1.25px solid var(--outline-tone);
border-radius: 0.15rem;
background: color-mix(in srgb, var(--outline-tone) 16%, var(--bg));
transition: background 0.16s ease, transform 0.16s ease, box-shadow 0.16s ease;
}
.node-outline-item:hover .node-outline-dot,
.node-outline-item:focus-visible .node-outline-dot,
.node-outline-item[data-previewing] .node-outline-dot,
.node-outline-item[aria-current="true"] .node-outline-dot {
background: var(--outline-tone);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--outline-tone) 18%, transparent);
transform: scale(1.08);
}
.node-outline-item strong {
overflow: hidden;
font-size: 0.8125rem;
font-weight: 500;
text-overflow: ellipsis;
white-space: nowrap;
}
.node-outline-item[aria-current="true"] strong { font-weight: 600; }
.node-outline-item small {
overflow: hidden;
color: var(--text-faint);
font-size: 0.6875rem;
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
transition: color 0.16s ease;
}
.node-outline-item:hover small,
.node-outline-item[data-previewing] small,
.node-outline-item[aria-current="true"] small { color: var(--text-muted); }
.card-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.625rem;
}
.card-dot {
flex: none;
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.9375rem; font-weight: 600; line-height: 1.4; color: var(--text); }
.card ul {
list-style: none;
color: var(--text-muted);
font-size: 0.8125rem;
line-height: 1.6;
}
.card li {
position: relative;
margin-bottom: 0.3rem;
padding-left: 0.9rem;
}
.card li:last-child { margin-bottom: 0; }
/* Hanging markers take the card's tone so wrapped lines align under text. */
.card li::before {
position: absolute;
top: calc(0.8em - 2px);
left: 0.2rem;
width: 4px;
height: 4px;
border-radius: 50%;
background: color-mix(in srgb, var(--card-tone, var(--text-muted)) 70%, transparent);
content: "";
}
.card:has(.card-dot.cyan) { --card-tone: var(--frontend-stroke); }
.card:has(.card-dot.emerald) { --card-tone: var(--backend-stroke); }
.card:has(.card-dot.violet) { --card-tone: var(--database-stroke); }
.card:has(.card-dot.amber) { --card-tone: var(--cloud-stroke); }
.card:has(.card-dot.rose) { --card-tone: var(--security-stroke); }
.card:has(.card-dot.orange) { --card-tone: var(--messagebus-stroke); }
.card:has(.card-dot.slate) { --card-tone: var(--external-stroke); }
.card-dot { box-shadow: 0 0 0 3px color-mix(in srgb, var(--card-tone, transparent) 16%, transparent); }
/* ==========================================================
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, .archify-toast, .no-print { display: 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; }
.diagram-container svg[data-focus-active] [data-node-id],
.diagram-container svg[data-focus-active] [data-edge-from],
.diagram-container svg[data-focus-active] [data-graph-role="automatic-crossover-underlay"],
.diagram-container svg[data-reach-active] [data-node-id],
.diagram-container svg[data-reach-active] [data-edge-from],
.diagram-container svg[data-reach-active] [data-graph-role="automatic-crossover-underlay"],
.diagram-container svg[data-route-active] [data-node-id],
.diagram-container svg[data-route-active] [data-edge-from],
.diagram-container svg[data-route-active] [data-graph-role="automatic-crossover-underlay"] {
opacity: 1 !important;
filter: none !important;
animation: none !important;
}
h1, .subtitle { color: #0f172a; }
}
/* ==========================================================
TOOLBAR (theme toggle + export menu)
========================================================== */
/* The toolbar tracks the centered reader column and the title row, so
an adaptive narrow reader does not leave its controls stranded at the
viewport edge. */
.toolbar {
position: absolute;
top: var(--archify-page-top);
right: max(1rem, calc((100vw - min(100vw - 4rem, var(--archify-reader-width, 1440px))) / 2));
display: flex;
align-items: center;
gap: 0.125rem;
padding: 0;
border: 0;
background: transparent;
box-shadow: none;
z-index: 100;
}
/* Ghost controls keep a 44px hit area while the visible pill stays compact;
Export is the one primary action. */
.toolbar button {
position: relative;
isolation: isolate;
background: transparent;
color: var(--text-muted);
border: 0;
padding: 0.5rem 0.75rem;
border-radius: 0.5625rem;
font-family: inherit;
font-size: 0.8125rem;
font-weight: 500;
cursor: pointer;
white-space: nowrap;
box-shadow: none;
transition: color 0.15s;
display: inline-flex;
align-items: center;
gap: 0.4rem;
min-height: 2.75rem;
}
.toolbar button::before {
position: absolute;
z-index: -1;
inset: 0.375rem 0;
border-radius: 0.5rem;
background: transparent;
content: "";
transition: background 0.15s, box-shadow 0.15s;
}
.toolbar button:hover { color: var(--text); }
.toolbar button:hover::before { background: color-mix(in srgb, var(--text) 6%, transparent); }
.toolbar button[aria-expanded="true"],
.toolbar button[aria-pressed="true"]:not(#btn-theme) { color: var(--text); }
.toolbar button[aria-expanded="true"]::before,
.toolbar button[aria-pressed="true"]:not(#btn-theme)::before {
background: color-mix(in srgb, var(--text) 8%, transparent);
}
.toolbar #btn-export {
margin-left: 0.375rem;
padding-right: 0.95rem;
padding-left: 1rem;
color: var(--bg);
font-weight: 550;
}
.toolbar #btn-export::before {
background: var(--text);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.12), 0 6px 16px -8px rgba(15, 23, 42, 0.4);
}
.toolbar #btn-export:hover,
.toolbar #btn-export[aria-expanded="true"] { color: var(--bg); }
.toolbar #btn-export:hover::before,
.toolbar #btn-export[aria-expanded="true"]::before { background: color-mix(in srgb, var(--text) 86%, var(--bg)); }
.toolbar button:focus-visible {
outline: 2px solid var(--frontend-stroke);
outline-offset: -4px;
}
.toolbar { gap: 0.5rem; }
.toolbar #btn-export { margin-left: 0; }
.toolbar #btn-export::before { inset: 0.25rem 0; border-radius: 0.6875rem; }
/* Tools share one quiet capsule; the capsule is drawn inside the 44px row
so the controls keep their hit area without a tall visible box. */
.toolbar-group {
position: relative;
isolation: isolate;
display: flex;
align-items: center;
gap: 0.0625rem;
padding: 0 0.25rem;
}
.toolbar-group::before {
position: absolute;
z-index: -1;
inset: 0.25rem 0;
border: 1px solid color-mix(in srgb, var(--panel-border) 85%, transparent);
border-radius: 0.6875rem;
background: color-mix(in srgb, var(--panel) 78%, transparent);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 20px -14px rgba(15, 23, 42, 0.22);
backdrop-filter: blur(12px);
content: "";
}
.toolbar-group button { padding-inline: 0.625rem; }
.toolbar-group button::before { inset: 0.5rem 0; border-radius: 0.5rem; }
/* Theme becomes a sliding switch; its label stays for assistive tech. */
.toolbar #theme-label,
.toolbar #present-label {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
.toolbar #btn-theme {
width: 3.5rem;
padding: 0;
justify-content: center;
}
.toolbar #btn-theme::before {
inset: 0.75rem 0.4375rem;
border-radius: 999px;
background: color-mix(in srgb, var(--text) 9%, transparent);
box-shadow: inset 0 1px 2px rgba(15, 23, 42, 0.08);
}
.toolbar #btn-theme:hover::before { background: color-mix(in srgb, var(--text) 13%, transparent); }
.toolbar #btn-theme::after {
position: absolute;
z-index: 0;
top: 50%;
left: 0.5625rem;
width: 1.125rem;
height: 1.125rem;
border-radius: 50%;
background: var(--panel);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.18), 0 2px 6px rgba(15, 23, 42, 0.12);
content: "";
transform: translateY(-50%);
transition: left 0.22s cubic-bezier(0.22, 1, 0.36, 1);
}
.toolbar #theme-icon {
position: absolute;
z-index: 1;
top: 50%;
left: calc(0.5625rem + 0.1875rem);
width: 0.75rem;
height: 0.75rem;
color: var(--cloud-stroke);
transform: translateY(-50%);
transition: left 0.22s cubic-bezier(0.22, 1, 0.36, 1);
}
[data-theme="dark"] .toolbar #btn-theme::before { background: rgba(148, 163, 184, 0.16); }
[data-theme="dark"] .toolbar #btn-theme::after { left: calc(100% - 0.5625rem - 1.125rem); background: #3b4a63; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); }
[data-theme="dark"] .toolbar #theme-icon { left: calc(100% - 0.5625rem - 1.125rem + 0.1875rem); color: #c7d2fe; }
.toolbar #btn-present { width: 2.5rem; padding: 0; justify-content: center; }
.toolbar #btn-present .toolbar-icon { width: 1rem; height: 1rem; }
html[data-motion="still"] .toolbar #btn-theme::after,
html[data-motion="still"] .toolbar #theme-icon { transition: none; }
html[data-present="true"]:not([data-embed="true"]) .toolbar { right: 1rem; }
html[data-preset="editorial"] .toolbar { border-radius: 0.45rem; }
.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);
}
/* Menus anchor to the tools capsule so they open flush under it. */
.toolbar-group .preset-wrap { position: static; }
.toolbar .preset-menu {
position: absolute;
top: calc(100% + 0.125rem);
right: 0;
display: none;
width: 17.5rem;
max-width: calc(100vw - 2rem);
padding: 0.375rem;
border: 1px solid color-mix(in srgb, var(--panel-border) 90%, transparent);
border-radius: 0.875rem;
background: var(--toolbar-menu-bg);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06), 0 18px 44px -14px rgba(15, 23, 42, 0.3);
}
[data-theme="dark"] .toolbar .preset-menu,
[data-theme="dark"] .toolbar .export-menu {
border-color: rgba(148, 163, 184, 0.16);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 18px 44px -12px rgba(0, 0, 0, 0.6);
}
/* Menu rows are not toolbar pills: drop the ghost-button pseudo surface. */
.toolbar .preset-menu button,
.toolbar .export-menu button { isolation: auto; min-height: 0; }
.toolbar .preset-menu button::before { display: none; }
.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: center;
justify-content: space-between;
gap: 1rem;
padding: 0.45rem 0.55rem 0.4rem;
color: var(--text-faint);
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.01em;
}
.preset-menu-shortcut {
display: inline-flex;
align-items: center;
gap: 0.35rem;
font-weight: 500;
}
.preset-menu-shortcut kbd {
min-width: 1.125rem;
padding: 0.05rem 0.3rem;
border: 1px solid var(--panel-border);
border-bottom-width: 2px;
border-radius: 0.3rem;
background: color-mix(in srgb, var(--text) 3%, var(--toolbar-menu-bg));
color: var(--text-muted);
font-family: inherit;
font-size: 0.625rem;
font-weight: 600;
line-height: 1.3;
text-align: center;
}
.toolbar .preset-option {
display: grid;
grid-template-columns: 2.375rem minmax(0, 1fr) 1rem;
align-items: center;
gap: 0.75rem;
width: 100%;
min-height: 3.125rem;
margin-top: 0.125rem;
padding: 0.45rem 0.6rem 0.45rem 0.5rem;
border: 0;
border-radius: 0.625rem;
background: transparent;
color: var(--text);
box-shadow: none;
backdrop-filter: none;
text-align: left;
transition: background 0.14s ease;
}
.toolbar .preset-option:hover,
.toolbar .preset-option:focus-visible { background: color-mix(in srgb, var(--text) 5%, transparent); }
.toolbar .preset-option[aria-checked="true"] { background: color-mix(in srgb, var(--frontend-stroke) 8%, transparent); }
.toolbar .preset-option:focus-visible {
outline: 1.5px solid color-mix(in srgb, var(--frontend-stroke) 55%, transparent);
outline-offset: -1.5px;
}
.preset-option-swatch {
position: relative;
width: 2.375rem;
height: 1.75rem;
overflow: hidden;
border: 0;
border-radius: 0.4375rem;
background: #0f172a;
box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.28), 0 1px 2px rgba(15, 23, 42, 0.12);
transition: box-shadow 0.14s ease, transform 0.14s ease;
}
.toolbar .preset-option:hover .preset-option-swatch { transform: translateY(-1px); }
.toolbar .preset-option[aria-checked="true"] .preset-option-swatch {
box-shadow: 0 0 0 2px var(--toolbar-menu-bg), 0 0 0 3.5px var(--frontend-stroke);
}
.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);
}
/* Classic previews the canvas it will actually produce in this theme. */
[data-theme="light"] .preset-option-swatch.classic {
background:
radial-gradient(circle at 1px 1px, #d9dee5 0.6px, transparent 0.9px) 0 0 / 5px 5px,
#ffffff;
}
[data-theme="light"] .preset-option-swatch.classic::before {
border-color: #1b87a3;
background: rgba(14, 116, 144, 0.08);
box-shadow: 0 2px 4px rgba(17, 24, 39, 0.12);
}
.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(--text);
font-size: 0.8125rem;
font-weight: 550;
letter-spacing: -0.005em;
}
.toolbar .preset-option[aria-checked="true"] .preset-option-copy strong { font-weight: 650; }
.preset-option-copy small {
margin-top: 0.125rem;
overflow: hidden;
color: var(--text-faint);
font-size: 0.6875rem;
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 {
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.125rem);
right: 0;
background: var(--toolbar-menu-bg);
border: 1px solid color-mix(in srgb, var(--panel-border) 90%, transparent);
width: 19rem;
max-width: calc(100vw - 2rem);
max-height: calc(100vh - 5.5rem);
overflow-y: auto;
border-radius: 0.875rem;
padding: 0.375rem;
display: none;
flex-direction: column;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06), 0 18px 44px -14px rgba(15, 23, 42, 0.3);
scrollbar-width: thin;
}
.toolbar .export-menu.open { display: flex; }
.export-menu-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
padding: 0.55rem 0.6rem 0.6rem;
}
.export-menu-header strong,
.export-menu-header span { display: block; }
.export-menu-header strong {
color: var(--text);
font-size: 0.875rem;
font-weight: 600;
letter-spacing: -0.005em;
}
.export-menu-header span {
margin-top: 0.15rem;
color: var(--text-faint);
font-size: 0.75rem;
}
.export-menu-header kbd {
min-width: 1.125rem;
padding: 0.05rem 0.3rem;
border: 1px solid var(--panel-border);
border-bottom-width: 2px;
border-radius: 0.3rem;
background: color-mix(in srgb, var(--text) 3%, var(--toolbar-menu-bg));
color: var(--text-muted);
font-family: inherit;
font-size: 0.625rem;
font-weight: 600;
line-height: 1.3;
text-align: center;
}
.export-menu-section {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 0.0625rem;
}
.export-menu-section + .export-menu-section {
margin-top: 0.3rem;
padding-top: 0.3rem;
border-top: 1px solid color-mix(in srgb, var(--panel-border) 80%, transparent);
}
.export-menu-heading {
grid-column: 1 / -1;
display: block;
padding: 0.4rem 0.6rem 0.25rem;
color: var(--text-faint);
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.01em;
}
.toolbar .export-menu button {
display: grid;
grid-template-columns: 1rem minmax(0, 1fr);
align-items: center;
column-gap: 0.7rem;
background: transparent;
border: 0;
box-shadow: none;
backdrop-filter: none;
width: 100%;
min-height: 2.5rem;
text-align: left;
padding: 0.4rem 0.6rem;
border-radius: 0.5625rem;
color: var(--text);
white-space: nowrap;
transition: background 0.14s ease;
}
.toolbar .export-menu button:hover,
.toolbar .export-menu button:focus-visible { background: color-mix(in srgb, var(--text) 5%, transparent); color: var(--text); }
.toolbar .export-menu button:focus-visible {
outline: 1.5px solid color-mix(in srgb, var(--frontend-stroke) 55%, transparent);
outline-offset: -1.5px;
}
/* Name and format hint share one line, so each row stays compact. */
.export-item-copy {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.75rem;
min-width: 0;
}
.export-item-copy strong {
overflow: hidden;
color: inherit;
font-size: 0.8125rem;
font-weight: 500;
text-overflow: ellipsis;
}
.export-item-copy small {
flex: none;
color: var(--text-faint);
font-size: 0.6875rem;
font-weight: 450;
}
.toolbar .export-menu button::before {
position: static;
z-index: auto;
inset: auto;
border-radius: 0;
display: block;
width: 1rem;
height: 1rem;
background: currentColor;
color: var(--text-muted);
content: "";
opacity: 0.8;
-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-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-action="route-share-card"],
.toolbar .export-menu button[data-action="reach-share-card"] {
color: var(--text);
background: color-mix(in srgb, var(--frontend-stroke) 7%, transparent);
}
.toolbar .export-menu button[data-action="route-share-card"]::before,
.toolbar .export-menu button[data-action="reach-share-card"]::before { color: var(--frontend-stroke); opacity: 1; }
.toolbar .export-menu button[hidden] { display: none; }
.toolbar .export-menu button[data-action="route-share-card"]:hover,
.toolbar .export-menu button[data-action="reach-share-card"]:hover {
background: color-mix(in srgb, var(--frontend-stroke) 12%, transparent);
}
.toolbar .export-menu button:disabled {
color: var(--text-faint);
background: transparent;
opacity: 0.55;
cursor: not-allowed;
}
.toolbar .export-menu .hint {
color: var(--text-faint);
font-size: 0.6875rem;
}
/* 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;
top: auto;
right: auto;
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-head { padding-top: 0.7rem; }
.relationship-lens-drag-handle { display: 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; }
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;
}
.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"]) .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-action="route-share-card"],
.toolbar .export-menu button[data-action="reach-share-card"] { grid-column: 1; }
}
@media (prefers-reduced-motion: reduce) {
.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; }
}
/* ==========================================================
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-lane-rule { stroke: var(--lane-stroke); fill: none; opacity: 0.7; }
.c-lane-wash { fill: var(--lane-fill); 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); }
/* Relationship labels share the exact color token used by their path. */
.t-edge-default { fill: var(--arrow); }
.t-edge-emphasis { fill: var(--arrow-emphasis); }
.t-edge-security { fill: var(--security-stroke); }
.t-edge-dashed { fill: var(--database-stroke); }
/* Classic canvas: a quiet dot field replaces drafting grid lines, and nodes
read as lifted cards with hairline strokes. Paint only; authored geometry,
hit areas and exports keep their canonical shapes. */
html[data-preset="classic"] .diagram-container {
background:
radial-gradient(circle at 1px 1px, var(--canvas-dot) 1px, transparent 1.4px) 0 0 / 20px 20px,
var(--panel);
}
html[data-preset="classic"] .diagram-container > svg .c-grid { stroke: transparent; }
html[data-preset="classic"][data-theme="light"] .diagram-container > svg [data-node-id] > rect:not(.c-mask) {
filter: drop-shadow(0 1px 1px rgba(17, 24, 39, 0.05)) drop-shadow(0 6px 12px rgba(17, 24, 39, 0.06));
}
html[data-preset="classic"][data-theme="dark"] .diagram-container > svg [data-node-id] > rect:not(.c-mask) {
filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.35));
}
html[data-preset="classic"] .diagram-container > svg [data-edge-from] {
stroke-linecap: round;
stroke-linejoin: round;
}
html[data-preset="classic"] .diagram-container > svg .c-lane-wash { fill: color-mix(in srgb, var(--text) 2.6%, transparent); }
html[data-preset="classic"] .diagram-container > svg .c-lane-rule { stroke: var(--panel-border); opacity: 1; }
html[data-preset="classic"][data-theme="dark"] .diagram-container > svg .c-lane-rule { stroke: rgba(148, 163, 184, 0.16); }
html[data-preset="classic"] .diagram-container > svg [data-node-id] {
transition: transform 0.18s cubic-bezier(0.22, 1, 0.36, 1);
}
html[data-preset="classic"]:not([data-motion="still"]) .diagram-container > svg [data-node-id]:hover {
transform: translateY(-1.5px);
}
/* 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-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-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, fill-opacity 0.18s ease, stroke-opacity 0.18s ease, filter 0.18s ease;
}
svg [data-edge-from],
svg [data-graph-role="automatic-crossover-underlay"] { 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: 1;
transition: 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: 7px;
font-weight: 700;
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; }
/* Automatic crossover masks are visual companions of their one semantic
edge. Keep the mask free of data-edge-* identity so graph traversal and
export still observe exactly one edge, then mirror reader emphasis from
the paired foreground path in this presentation-only wrapper. */
svg[data-legend-preview-active] [data-graph-role="automatic-crossover"] > [data-graph-role="automatic-crossover-underlay"],
svg[data-lens-active] [data-graph-role="automatic-crossover"] > [data-graph-role="automatic-crossover-underlay"] { opacity: 0.11; }
svg[data-legend-preview-active] [data-graph-role="automatic-crossover"]:has(> [data-legend-preview-match]) > [data-graph-role="automatic-crossover-underlay"],
svg[data-lens-active] [data-graph-role="automatic-crossover"]:has(> [data-lens-match]) > [data-graph-role="automatic-crossover-underlay"] { opacity: 1; }
svg[data-legend-preview-active] [data-graph-role="automatic-crossover"]:has(> [data-legend-preview-peer]) > [data-graph-role="automatic-crossover-underlay"],
svg[data-lens-active] [data-graph-role="automatic-crossover"]:has(> [data-lens-peer]) > [data-graph-role="automatic-crossover-underlay"] { opacity: 0.62; }
svg[data-focus-active] [data-node-id],
svg[data-focus-active] [data-edge-from] { opacity: 0.13; }
svg[data-focus-active] [data-graph-role="automatic-crossover"] > [data-graph-role="automatic-crossover-underlay"] { opacity: 0.13; }
svg[data-focus-active] [data-lifecycle-rail] { opacity: 0; }
svg[data-focus-active] [data-focus-match] { opacity: 1; }
svg[data-focus-active] [data-graph-role="automatic-crossover"]:has(> [data-focus-match]) > [data-graph-role="automatic-crossover-underlay"] { 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-graph-role="automatic-crossover"] > [data-graph-role="automatic-crossover-underlay"] { opacity: 0.09; }
svg[data-reach-active] [data-reach-match] { opacity: 1; }
svg[data-reach-active] [data-graph-role="automatic-crossover"]:has(> [data-reach-match]) > [data-graph-role="automatic-crossover-underlay"] { 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-graph-role="automatic-crossover"] > [data-graph-role="automatic-crossover-underlay"] { opacity: 0.16; }
svg[data-relationship-direct-active] [data-relationship-preview],
svg[data-relationship-direct-active] [data-relationship-preview-node] { opacity: 1; }
svg[data-relationship-direct-active] [data-graph-role="automatic-crossover"]:has(> [data-relationship-preview]) > [data-graph-role="automatic-crossover-underlay"] { opacity: 1; }
svg[data-focus-active]:not([data-relationship-pin-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 { 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-graph-role="automatic-crossover"]:has(> [data-focus-match]) > [data-graph-role="automatic-crossover-underlay"] { opacity: 0.18; }
svg[data-relationship-preview-active] [data-lifecycle-rail] { opacity: 0; }
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-graph-role="automatic-crossover"]:has(> [data-relationship-preview]) > [data-graph-role="automatic-crossover-underlay"] { 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;
}
.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. */
/* Keep the node's opaque backing mask intact while its visible ink dims.
Fading the whole group also fades that mask and briefly exposes arrows
routed underneath the node when the pointer leaves. */
svg[data-intent-trace-active] [data-node-id] { fill-opacity: 0.2; stroke-opacity: 0.2; }
svg [data-node-id] > .c-mask { fill-opacity: 1; }
svg[data-intent-trace-active] [data-node-id][data-intent-trace-match] { fill-opacity: 1; stroke-opacity: 1; }
svg[data-intent-trace-active] [data-edge-from] { opacity: 0.2; }
svg[data-intent-trace-active] [data-graph-role="automatic-crossover"] > [data-graph-role="automatic-crossover-underlay"] { opacity: 0.2; }
svg[data-intent-trace-active] [data-lifecycle-rail] { opacity: 0; }
svg[data-intent-trace-active] [data-intent-trace-match] { opacity: 1; }
svg[data-intent-trace-active] [data-graph-role="automatic-crossover"]:has(> [data-intent-trace-match]) > [data-graph-role="automatic-crossover-underlay"] { 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-graph-role="automatic-crossover"] > [data-graph-role="automatic-crossover-underlay"] { opacity: 0.11; }
svg[data-route-active] [data-route-match] { opacity: 1; }
svg[data-route-active] [data-graph-role="automatic-crossover"]:has(> [data-route-match]) > [data-graph-role="automatic-crossover-underlay"] { 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-graph-role="automatic-crossover"]:has(> [data-route-match][data-route-journey-state="past"]) > [data-graph-role="automatic-crossover-underlay"] { opacity: 0.62; }
svg[data-route-journey] [data-graph-role="automatic-crossover"]:has(> [data-route-match][data-route-journey-state="future"]) > [data-graph-role="automatic-crossover-underlay"] { opacity: 0.34; }
svg[data-route-journey] [data-graph-role="automatic-crossover"]:has(> [data-route-match][data-route-journey-state="current"]) > [data-graph-role="automatic-crossover-underlay"] { 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));
}
/* 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-graph-role="automatic-crossover"]:has(> [data-animate="edge"]) > [data-graph-role="automatic-crossover-underlay"] {
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-graph-role="automatic-crossover"]:has(> [data-animate="edge"]) > [data-graph-role="automatic-crossover-underlay"] { 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-graph-role="automatic-crossover"]:has(> [data-animate="edge"]) > [data-graph-role="automatic-crossover-underlay"] { 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-graph-role="automatic-crossover"]:has(> [data-animate="edge"]) > [data-graph-role="automatic-crossover-underlay"] { 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 stay static so proof grids are calm 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;
}
html[data-embed="true"] svg[data-animation="trace"] [data-graph-role="automatic-crossover"]:has(> [data-animate="edge"]) > [data-graph-role="automatic-crossover-underlay"],
html[data-share-playback="true"] svg[data-animation="trace"] [data-graph-role="automatic-crossover"]:has(> [data-animate="edge"]) > [data-graph-role="automatic-crossover-underlay"] {
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-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-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;
}
svg[data-animation="trace"] [data-graph-role="automatic-crossover"]:has(> [data-animate="edge"]) > [data-graph-role="automatic-crossover-underlay"] {
animation: none !important;
stroke-dashoffset: 0;
}
.intent-trace-flow,
.route-probe-flow,
.semantic-lens,
.diagram-guide,
.overview-map-live { animation: none !important; }
.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 { 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; }
.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">
<div class="toolbar-group">
<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 class="preset-menu-shortcut"><kbd>S</kbd>to cycle</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>
<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-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&times;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&times;630 PNG</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 &amp; motion</span>
<button data-format="svg" type="button" role="menuitem" tabindex="-1"><span class="export-item-copy"><strong>SVG · Auto</strong><small class="hint">Matches host theme</small></span></button>
<button data-format="svg-light" type="button" role="menuitem" tabindex="-1"><span class="export-item-copy"><strong>SVG · Light</strong><small class="hint">Always light</small></span></button>
<button data-format="svg-dark" type="button" role="menuitem" tabindex="-1"><span class="export-item-copy"><strong>SVG · Dark</strong><small class="hint">Always dark</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>Cache Miss Request Sequence</h1>
</div>
</div>
<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":"to cycle","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.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.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.svg.auto":"SVG · Auto","viewer.export.svg.auto.hint":"Matches host theme","viewer.export.svg.light":"SVG · Light","viewer.export.svg.light.hint":"Always light","viewer.export.svg.dark":"SVG · Dark","viewer.export.svg.dark.hint":"Always dark","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.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.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":"Path: {source} → {target} · {count} step","viewer.export.card.routeSummary.other":"Path: {source} → {target} · {count} steps","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.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.guide.title":"Explore this system","viewer.focus.selectedNodes":"{count} selected nodes","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.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}","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.open":"Open diagram guide","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.verificationScope":"Verified against local Git at the pinned revision. Remote access has not been checked.","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.move":"Move semantic passport. Drag, use arrow keys, or press Home to reset.","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":"Path","viewer.route.start":"Click where the path starts","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":"Path","viewer.route.pickTwo":"Click two nodes on the diagram","viewer.route.pickOne":"Click a 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":"Click a start, then an end. Paths follow the arrows.","viewer.route.destination":"Where does the path from {label} end?","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":"Pick two different nodes.","viewer.route.unreachable":"Cannot reach {label} from here","viewer.route.unreachable.detail":"{target} cannot be reached from {source} along the arrows. Pick a highlighted node.","viewer.route.start.instructions":"Next, pick the end. Only nodes you can reach will light up.","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 path","viewer.route.overview.node.one":"{count} node","viewer.route.overview.node.other":"{count} nodes","viewer.route.overview.hop.one":"{count} step","viewer.route.overview.hop.other":"{count} steps","viewer.route.phase.playing":"Playing","viewer.route.phase.complete":"Complete","viewer.route.phase.inspecting":"Inspecting","viewer.route.destination.count.one":"{count} reachable node is highlighted. Click it.","viewer.route.destination.count.other":"{count} reachable nodes are highlighted. Click one.","viewer.route.noOutgoing":"No arrows lead out of this node. Clear it and pick another start.","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.outline.title":"Node index","viewer.rail.show":"Notes \u0026 index","viewer.rail.controls":"Side panel","viewer.rail.collapse":"Collapse side panel","viewer.rail.bottom":"Move panel below the diagram","viewer.rail.right":"Move panel beside the diagram","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>
<!-- Main Diagram -->
<div class="diagram-container" data-detail-level="read"
data-preset-badge-editorial-plate="ARCHIFY / PLATE 04">
<svg viewBox="0 0 1080 580" data-sequence-column-fit="spread" role="img" lang="en" aria-labelledby="archify-diagram-title archify-diagram-description" data-animation="trace" data-preset="classic" data-quality-profile="showcase">
<title id="archify-diagram-title">Cache Miss Request Sequence</title>
<desc id="archify-diagram-description">A sequence 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)" />
<!-- Time Segments -->
<rect data-graph-role="structural-frame" data-composition-frame-kind="segment" data-composition-frame-id="0" x="48" y="150" width="984" height="100" rx="10" class="c-lane" stroke-width="1"/>
<rect data-graph-role="structural-frame" data-composition-frame-kind="segment" data-composition-frame-id="1" x="48" y="260" width="984" height="110" rx="10" class="c-lane" stroke-width="1"/>
<rect data-graph-role="structural-frame" data-composition-frame-kind="segment" data-composition-frame-id="2" x="48" y="380" width="984" height="100" rx="10" class="c-lane" stroke-width="1"/>
<!-- Lifelines -->
<path d="M 118.5 142 L 118.5 484" class="a-default" stroke-width="0.8" stroke-dasharray="3,7"/>
<path d="M 262.66666666666663 142 L 262.66666666666663 484" class="a-default" stroke-width="0.8" stroke-dasharray="3,7"/>
<path d="M 406.8333333333333 142 L 406.8333333333333 484" class="a-default" stroke-width="0.8" stroke-dasharray="3,7"/>
<path d="M 551 142 L 551 484" class="a-default" stroke-width="0.8" stroke-dasharray="3,7"/>
<path d="M 695.1666666666666 142 L 695.1666666666666 484" class="a-default" stroke-width="0.8" stroke-dasharray="3,7"/>
<path d="M 839.3333333333333 142 L 839.3333333333333 484" class="a-default" stroke-width="0.8" stroke-dasharray="3,7"/>
<path d="M 983.5 142 L 983.5 484" class="a-default" stroke-width="0.8" stroke-dasharray="3,7"/>
<!-- Activations -->
<rect x="257.66666666666663" y="180" width="10" height="294" rx="3" class="c-mask"/>
<rect x="257.66666666666663" y="180" width="10" height="294" rx="3" class="c-frontend" stroke-width="1"/>
<rect x="401.8333333333333" y="185" width="10" height="265" rx="3" class="c-mask"/>
<rect x="401.8333333333333" y="185" width="10" height="265" rx="3" class="c-backend" stroke-width="1"/>
<rect x="546" y="205" width="10" height="39" rx="3" class="c-mask"/>
<rect x="546" y="205" width="10" height="39" rx="3" class="c-security" stroke-width="1"/>
<rect x="690.1666666666666" y="265" width="10" height="39" rx="3" class="c-mask"/>
<rect x="690.1666666666666" y="265" width="10" height="39" rx="3" class="c-database" stroke-width="1"/>
<rect x="834.3333333333333" y="325" width="10" height="39" rx="3" class="c-mask"/>
<rect x="834.3333333333333" y="325" width="10" height="39" rx="3" class="c-database" stroke-width="1"/>
<rect x="978.5" y="413" width="10" height="36" rx="3" class="c-mask"/>
<rect x="978.5" y="413" width="10" height="36" rx="3" class="c-messagebus" stroke-width="1"/>
<!-- Messages -->
<g data-edge-from="user" data-edge-to="web" data-edge-label="open page" data-edge-key="0" data-edge-id="open-page">
<path data-composition-edge-from="user" data-composition-edge-to="web" data-composition-edge-id="open-page" data-composition-points="125.5,160;255.66666666666663,160" d="M 125.5 160 L 255.66666666666663 160" class="a-default" data-animate="edge" style="--step:0" stroke-width="1.4" marker-end="url(#arrowhead)"/>
<g data-detail="context">
<rect x="154.88333333333333" y="140" width="71.4" height="18" rx="3" class="c-mask"/>
<text x="190.58333333333331" y="150" class="t-muted" font-size="11" text-anchor="middle">open page</text>
</g>
</g>
<g data-edge-from="web" data-edge-to="api" data-edge-label="GET /dashboard" data-edge-key="1" data-edge-id="dashboard-request">
<path data-composition-edge-from="web" data-composition-edge-to="api" data-composition-edge-id="dashboard-request" data-composition-points="269.66666666666663,185;399.8333333333333,185" d="M 269.66666666666663 185 L 399.8333333333333 185" class="a-emphasis" data-animate="edge" style="--step:1" stroke-width="1.8" marker-end="url(#arrowhead-emphasis)"/>
<g data-detail="context">
<rect x="282.55" y="165" width="104.39999999999999" height="18" rx="3" class="c-mask"/>
<text x="334.75" y="175" class="t-edge-emphasis" font-size="11" text-anchor="middle">GET /dashboard</text>
</g>
</g>
<g data-edge-from="api" data-edge-to="auth" data-edge-label="verify JWT" data-edge-key="2" data-edge-id="verify-jwt">
<path data-composition-edge-from="api" data-composition-edge-to="auth" data-composition-edge-id="verify-jwt" data-composition-points="413.8333333333333,210;544,210" d="M 413.8333333333333 210 L 544 210" class="a-security" data-animate="edge" style="--step:2" stroke-width="1.4" marker-end="url(#arrowhead-security)"/>
<g data-detail="context">
<rect x="439.91666666666663" y="190" width="78" height="18" rx="3" class="c-mask"/>
<text x="478.91666666666663" y="200" class="t-edge-security" font-size="11" text-anchor="middle">verify JWT</text>
</g>
</g>
<g data-edge-from="auth" data-edge-to="api" data-edge-label="claims ok" data-edge-key="3" data-edge-id="auth-claims">
<path data-composition-edge-from="auth" data-composition-edge-to="api" data-composition-edge-id="auth-claims" data-composition-points="544,238;413.8333333333333,238" d="M 544 238 L 413.8333333333333 238" class="a-default" data-animate="edge" style="--step:3" stroke-width="1.4" stroke-dasharray="3,5" marker-end="url(#arrowhead)"/>
<g data-detail="context">
<rect x="443.21666666666664" y="218" width="71.4" height="18" rx="3" class="c-mask"/>
<text x="478.91666666666663" y="228" class="t-muted" font-size="11" text-anchor="middle">claims ok</text>
</g>
</g>
<g data-edge-from="api" data-edge-to="redis" data-edge-label="read cache" data-edge-key="4" data-edge-id="cache-read">
<path data-composition-edge-from="api" data-composition-edge-to="redis" data-composition-edge-id="cache-read" data-composition-points="413.8333333333333,270;688.1666666666666,270" d="M 413.8333333333333 270 L 688.1666666666666 270" class="a-default" data-animate="edge" style="--step:4" stroke-width="1.4" marker-end="url(#arrowhead)"/>
<g data-detail="context">
<rect x="512" y="250" width="78" height="18" rx="3" class="c-mask"/>
<text x="551" y="260" class="t-muted" font-size="11" text-anchor="middle">read cache</text>
</g>
</g>
<g data-edge-from="redis" data-edge-to="api" data-edge-label="miss" data-edge-key="5" data-edge-id="cache-miss">
<path data-composition-edge-from="redis" data-composition-edge-to="api" data-composition-edge-id="cache-miss" data-composition-points="688.1666666666666,298;413.8333333333333,298" d="M 688.1666666666666 298 L 413.8333333333333 298" class="a-default" data-animate="edge" style="--step:5" stroke-width="1.4" stroke-dasharray="3,5" marker-end="url(#arrowhead)"/>
<g data-detail="context">
<rect x="531.8" y="278" width="38.4" height="18" rx="3" class="c-mask"/>
<text x="551" y="288" class="t-muted" font-size="11" text-anchor="middle">miss</text>
</g>
</g>
<g data-edge-from="api" data-edge-to="db" data-edge-label="query profile + metrics" data-edge-key="6" data-edge-id="profile-query">
<path data-composition-edge-from="api" data-composition-edge-to="db" data-composition-edge-id="profile-query" data-composition-points="413.8333333333333,330;832.3333333333333,330" d="M 413.8333333333333 330 L 832.3333333333333 330" class="a-emphasis" data-animate="edge" style="--step:6" stroke-width="1.8" marker-end="url(#arrowhead-emphasis)"/>
<g data-detail="context">
<rect x="541.1833333333333" y="310" width="163.79999999999998" height="18" rx="3" class="c-mask"/>
<text x="623.0833333333333" y="320" class="t-edge-emphasis" font-size="11" text-anchor="middle">query profile + metrics</text>
</g>
</g>
<g data-edge-from="db" data-edge-to="api" data-edge-label="rows" data-edge-key="7" data-edge-id="profile-rows">
<path data-composition-edge-from="db" data-composition-edge-to="api" data-composition-edge-id="profile-rows" data-composition-points="832.3333333333333,358;413.8333333333333,358" d="M 832.3333333333333 358 L 413.8333333333333 358" class="a-default" data-animate="edge" style="--step:7" stroke-width="1.4" stroke-dasharray="3,5" marker-end="url(#arrowhead)"/>
<g data-detail="context">
<rect x="603.8833333333332" y="338" width="38.4" height="18" rx="3" class="c-mask"/>
<text x="623.0833333333333" y="348" class="t-muted" font-size="11" text-anchor="middle">rows</text>
</g>
</g>
<g data-edge-from="api" data-edge-to="redis" data-edge-label="set cache" data-edge-key="8" data-edge-id="cache-write">
<path data-composition-edge-from="api" data-composition-edge-to="redis" data-composition-edge-id="cache-write" data-composition-points="413.8333333333333,390;688.1666666666666,390" d="M 413.8333333333333 390 L 688.1666666666666 390" class="a-dashed" data-animate="edge" style="--step:8" stroke-width="1.4" marker-end="url(#arrowhead-dashed)"/>
<g data-detail="context">
<rect x="515.3" y="370" width="71.4" height="18" rx="3" class="c-mask"/>
<text x="551" y="380" class="t-edge-dashed" font-size="11" text-anchor="middle">set cache</text>
</g>
</g>
<g data-edge-from="api" data-edge-to="trace" data-edge-label="emit trace" data-edge-key="9" data-edge-id="trace-emit">
<path data-composition-edge-from="api" data-composition-edge-to="trace" data-composition-edge-id="trace-emit" data-composition-points="413.8333333333333,418;976.5,418" d="M 413.8333333333333 418 L 976.5 418" class="a-dashed" data-animate="edge" style="--step:9" stroke-width="1.4" marker-end="url(#arrowhead-dashed)"/>
<g data-detail="context">
<rect x="656.1666666666666" y="398" width="78" height="18" rx="3" class="c-mask"/>
<text x="695.1666666666666" y="408" class="t-edge-dashed" font-size="11" text-anchor="middle">emit trace</text>
</g>
</g>
<g data-edge-from="api" data-edge-to="web" data-edge-label="200 JSON" data-edge-key="10" data-edge-id="dashboard-response">
<path data-composition-edge-from="api" data-composition-edge-to="web" data-composition-edge-id="dashboard-response" data-composition-points="399.8333333333333,443;269.66666666666663,443" d="M 399.8333333333333 443 L 269.66666666666663 443" class="a-default" data-animate="edge" style="--step:10" stroke-width="1.4" stroke-dasharray="3,5" marker-end="url(#arrowhead)"/>
<g data-detail="context">
<rect x="302.35" y="423" width="64.8" height="18" rx="3" class="c-mask"/>
<text x="334.75" y="433" class="t-muted" font-size="11" text-anchor="middle">200 JSON</text>
</g>
</g>
<g data-edge-from="web" data-edge-to="user" data-edge-label="render" data-edge-key="11" data-edge-id="page-render">
<path data-composition-edge-from="web" data-composition-edge-to="user" data-composition-edge-id="page-render" data-composition-points="255.66666666666663,468;125.5,468" d="M 255.66666666666663 468 L 125.5 468" class="a-default" data-animate="edge" style="--step:11" stroke-width="1.4" stroke-dasharray="3,5" marker-end="url(#arrowhead)"/>
<g data-detail="context">
<rect x="164.7833333333333" y="448" width="51.599999999999994" height="18" rx="3" class="c-mask"/>
<text x="190.58333333333331" y="458" class="t-muted" font-size="11" text-anchor="middle">render</text>
</g>
</g>
<!-- Segment Labels -->
<g data-graph-role="segment-label" data-segment-id="0">
<rect x="56" y="128" width="50.4" height="18" rx="3" class="c-mask"/>
<text x="62" y="141" class="t-dim" font-size="9" font-weight="600">Request</text>
</g>
<g data-graph-role="segment-label" data-segment-id="1">
<rect x="56" y="238" width="55.6" height="18" rx="3" class="c-mask"/>
<text x="62" y="251" class="t-dim" font-size="9" font-weight="600">Fallback</text>
</g>
<g data-graph-role="segment-label" data-segment-id="2">
<rect x="56" y="358" width="97.2" height="18" rx="3" class="c-mask"/>
<text x="62" y="371" class="t-dim" font-size="9" font-weight="600">Response + trace</text>
</g>
<!-- Participants -->
<g id="node-user" data-node-id="user" data-node-label="User" tabindex="0" role="button" aria-label="Focus User, browser session, Sequence participant" aria-pressed="false" data-node-kind="external" data-node-sublabel="browser session" data-node-context="Sequence participant">
<title>User · browser session · Sequence participant</title>
<rect x="62" y="72" width="113" height="60" rx="6" class="c-mask"/>
<rect x="62" y="72" width="113" height="60" 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(68 78) 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="118.5" y="108" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">User</text>
<text data-detail="context" x="118.5" y="122" class="t-muted" font-size="7" text-anchor="middle">browser session</text>
</g>
<g id="node-web" data-node-id="web" data-node-label="Web App" tabindex="0" role="button" aria-label="Focus Web App, React UI, Sequence participant" aria-pressed="false" data-node-kind="frontend" data-node-sublabel="React UI" data-node-context="Sequence participant">
<title>Web App · React UI · Sequence participant</title>
<rect x="206.16666666666663" y="72" width="113" height="60" rx="6" class="c-mask"/>
<rect x="206.16666666666663" y="72" width="113" height="60" 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(212.16666666666663 78) 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="262.66666666666663" y="108" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">Web App</text>
<text data-detail="context" x="262.66666666666663" y="122" class="t-muted" font-size="7" text-anchor="middle">React UI</text>
</g>
<g id="node-api" data-node-id="api" data-node-label="API" tabindex="0" role="button" aria-label="Focus API, request handler, Sequence participant" aria-pressed="false" data-node-kind="backend" data-node-sublabel="request handler" data-node-context="Sequence participant">
<title>API · request handler · Sequence participant</title>
<rect x="350.3333333333333" y="72" width="113" height="60" rx="6" class="c-mask"/>
<rect x="350.3333333333333" y="72" width="113" height="60" 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(356.3333333333333 78) scale(0.6875)">
<path d="M6 3 3 8l3 5M10 3l3 5-3 5"/>
</g>
<text data-node-label="" data-detail-anchor="" x="406.8333333333333" y="108" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">API</text>
<text data-detail="context" x="406.8333333333333" y="122" class="t-muted" font-size="7" text-anchor="middle">request handler</text>
</g>
<g id="node-auth" data-node-id="auth" data-node-label="Auth" tabindex="0" role="button" aria-label="Focus Auth, JWT verify, Sequence participant" aria-pressed="false" data-node-kind="security" data-node-sublabel="JWT verify" data-node-context="Sequence participant">
<title>Auth · JWT verify · Sequence participant</title>
<rect x="494.5" y="72" width="113" height="60" rx="6" class="c-mask"/>
<rect x="494.5" y="72" width="113" height="60" rx="6" class="c-security" data-animate="node" style="--step:3" stroke-width="1.5"/>
<g aria-hidden="true" data-semantic-sigil="security" class="semantic-sigil s-security" transform="translate(500.5 78) 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="551" y="108" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">Auth</text>
<text data-detail="context" x="551" y="122" class="t-muted" font-size="7" text-anchor="middle">JWT verify</text>
</g>
<g id="node-redis" data-node-id="redis" data-node-label="Redis" tabindex="0" role="button" aria-label="Focus Redis, cache, Sequence participant" aria-pressed="false" data-node-kind="database" data-node-sublabel="cache" data-node-context="Sequence participant">
<title>Redis · cache · Sequence participant</title>
<rect x="638.6666666666666" y="72" width="113" height="60" rx="6" class="c-mask"/>
<rect x="638.6666666666666" y="72" width="113" height="60" rx="6" class="c-database" data-animate="node" style="--step:4" stroke-width="1.5"/>
<g aria-hidden="true" data-semantic-sigil="database" class="semantic-sigil s-database" transform="translate(644.6666666666666 78) 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="695.1666666666666" y="108" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">Redis</text>
<text data-detail="context" x="695.1666666666666" y="122" class="t-muted" font-size="7" text-anchor="middle">cache</text>
</g>
<g id="node-db" data-node-id="db" data-node-label="Postgres" tabindex="0" role="button" aria-label="Focus Postgres, source of truth, Sequence participant" aria-pressed="false" data-node-kind="database" data-node-sublabel="source of truth" data-node-context="Sequence participant">
<title>Postgres · source of truth · Sequence participant</title>
<rect x="782.8333333333333" y="72" width="113" height="60" rx="6" class="c-mask"/>
<rect x="782.8333333333333" y="72" width="113" height="60" rx="6" class="c-database" data-animate="node" style="--step:5" stroke-width="1.5"/>
<g aria-hidden="true" data-semantic-sigil="database" class="semantic-sigil s-database" transform="translate(788.8333333333333 78) 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="839.3333333333333" y="108" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">Postgres</text>
<text data-detail="context" x="839.3333333333333" y="122" class="t-muted" font-size="7" text-anchor="middle">source of truth</text>
</g>
<g id="node-trace" data-node-id="trace" data-node-label="Trace" tabindex="0" role="button" aria-label="Focus Trace, async event, Sequence participant" aria-pressed="false" data-node-kind="messagebus" data-node-sublabel="async event" data-node-context="Sequence participant">
<title>Trace · async event · Sequence participant</title>
<rect x="927" y="72" width="113" height="60" rx="6" class="c-mask"/>
<rect x="927" y="72" width="113" height="60" rx="6" class="c-messagebus" data-animate="node" style="--step:6" stroke-width="1.5"/>
<g aria-hidden="true" data-semantic-sigil="messagebus" class="semantic-sigil s-messagebus" transform="translate(933 78) 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="983.5" y="108" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">Trace</text>
<text data-detail="context" x="983.5" y="122" class="t-muted" font-size="7" text-anchor="middle">async event</text>
</g>
<!-- Legend -->
<g data-legend="">
<text x="40" y="506" class="t-primary" font-size="12" font-weight="650">Legend</text>
<g data-legend-semantic-kind="emphasis" data-legend-x="40" data-legend-baseline="526" data-legend-width="103">
<path d="M 40 523 L 74 523" class="a-emphasis" stroke-width="1.8" marker-end="url(#arrowhead-emphasis)"/>
<text x="83" y="526" class="t-muted" font-size="10" font-weight="500">main request</text>
</g>
<g data-legend-semantic-kind="return" data-legend-x="165" data-legend-baseline="526" data-legend-width="73">
<path d="M 165 523 L 199 523" class="a-default" stroke-width="1.4" stroke-dasharray="3,5" marker-end="url(#arrowhead)"/>
<text x="208" y="526" class="t-muted" font-size="10" font-weight="500">return</text>
</g>
<g data-legend-semantic-kind="security" data-legend-x="260" data-legend-baseline="526" data-legend-width="83">
<path d="M 260 523 L 294 523" class="a-security" stroke-width="1.4" marker-end="url(#arrowhead-security)"/>
<text x="303" y="526" class="t-muted" font-size="10" font-weight="500">security</text>
</g>
<g data-legend-semantic-kind="dashed" data-legend-x="365" data-legend-baseline="526" data-legend-width="98">
<path d="M 365 523 L 399 523" class="a-dashed" stroke-width="1.4" marker-end="url(#arrowhead-dashed)"/>
<text x="408" y="526" class="t-muted" font-size="10" font-weight="500">async trace</text>
</g>
<g data-legend-semantic-kind="default" data-legend-x="485" data-legend-baseline="526" data-legend-width="78">
<path d="M 485 523 L 519 523" class="a-default" stroke-width="1.4" marker-end="url(#arrowhead)"/>
<text x="528" y="526" class="t-muted" font-size="10" font-weight="500">message</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="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">&#215;</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="present" aria-label="Enter presentation stage">
<span class="diagram-guide-index" aria-hidden="true">05</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">&#215;</button>
</div>
<div class="node-finder-search">
<span aria-hidden="true">&#8981;</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">
<button class="relationship-lens-drag-handle" id="btn-focus-move" type="button" data-focus-drag-handle aria-label="Move semantic passport. Drag, use arrow keys, or press Home to reset." title="Move semantic passport. Drag, use arrow keys, or press Home to reset."></button>
<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">&#215;</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">Path</span>
<strong class="route-probe-title" id="route-probe-title">Click where the path starts</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="Path">
<span class="route-probe-placeholder">Click two 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">&#8592;</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">&#9654;</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">&#8594;</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">Click a start, then an end. Paths follow the arrows.</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">&#215;</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">&#215;</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>
<button class="rail-reveal no-print" id="rail-reveal" type="button" hidden aria-controls="reader-rail" aria-expanded="false"><span class="rail-reveal-icon" aria-hidden="true"></span><span>Notes &amp; index</span></button>
<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" hidden 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>
<aside class="reader-rail" id="reader-rail">
<div class="rail-controls no-print" role="toolbar" aria-label="Side panel">
<button class="rail-control" id="rail-placement" type="button" hidden aria-label="Move panel below the diagram" title="Move panel below the diagram" data-placement="right"><span class="rail-control-icon placement" aria-hidden="true"></span></button>
<button class="rail-control" id="rail-collapse" type="button" hidden aria-controls="reader-rail" aria-expanded="true" aria-label="Collapse side panel" title="Collapse side panel"><span class="rail-control-icon collapse" aria-hidden="true"></span></button>
</div>
<!-- Info Cards -->
<div class="cards">
<div class="card">
<div class="card-header">
<div class="card-dot emerald"></div>
<h3>Happy Path</h3>
</div>
<ul>
<li>The main request is Web App -&gt; API -&gt; data source -&gt; response</li>
<li>Return messages are quieter than forward calls</li>
<li>Activation bars make ownership duration visible</li>
</ul>
</div>
<div class="card">
<div class="card-header">
<div class="card-dot rose"></div>
<h3>Policy + Fallback</h3>
</div>
<ul>
<li>JWT verification is colored as a security interaction</li>
<li>Cache miss is visible without overpowering the main path</li>
<li>Database access only appears after cache fallback</li>
</ul>
</div>
<div class="card">
<div class="card-header">
<div class="card-dot orange"></div>
<h3>Async Trace</h3>
</div>
<ul>
<li>Trace emission is dashed and secondary</li>
<li>It does not block the response path</li>
<li>The diagram separates user-facing latency from observability</li>
</ul>
</div>
</div>
<nav class="node-outline no-print" id="node-outline" hidden aria-labelledby="node-outline-title">
<div class="node-outline-head">
<strong id="node-outline-title">Node index</strong>
<span class="node-outline-count" id="node-outline-count"></span>
</div>
<ol class="node-outline-list" id="node-outline-list"></ol>
</nav>
</aside>
</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, respects system pref
============================================================ */
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 window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : '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);
// Follow live OS theme changes while the user has no explicit preference.
try {
var media = window.matchMedia('(prefers-color-scheme: light)');
var onChange = function (e) {
var saved = readStored();
if (urlOverride() || saved === 'light' || saved === 'dark') return;
apply(e.matches ? 'light' : 'dark');
};
if (media.addEventListener) media.addEventListener('change', onChange);
else if (media.addListener) media.addListener(onChange);
} catch (_) {}
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 = 40;
var SHARE_CARD_HEADER = 124;
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';
}
function cleanExportClone(clone) {
// 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');
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-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');
});
return !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.style.getPropertyValue('transform') &&
!clone.style.getPropertyValue('clip-path') &&
clone.querySelectorAll('[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;
}
/**
* 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.
* - theme=auto — 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 the default "Download SVG".
* - theme=light|dark — locks the standalone SVG to the requested
* theme, independently of its source Viewer and host OS themes.
*/
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: multiplier (integer unless the figure is oversized) for intrinsic SVG pixel dimensions used by
// the raster path. Defaults to 1 (natural size) for SVG download.
scale = scale || 1;
opts = opts || {};
var requestedTheme = opts.theme || (opts.autoTheme === true ? 'auto' : null);
if (requestedTheme && requestedTheme !== 'auto' && requestedTheme !== 'light' && requestedTheme !== 'dark') {
throw new Error('Unsupported SVG theme: ' + requestedTheme);
}
var autoTheme = requestedTheme === 'auto';
var svg = document.querySelector('.diagram-container svg');
var clone = svg.cloneNode(true);
var canonicalStateClean = cleanExportClone(clone);
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);
// Keep copied Viewer layout rules from overriding the export's size.
clone.style.width = vb.width * scale + 'px';
clone.style.height = vb.height * scale + 'px';
clone.style.minWidth = '0';
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);
} else if (opts.figure && sel.indexOf('.diagram-container > svg') !== -1) {
// Figures look like the Viewer canvas: keep its preset/theme paint
// (quiet grid, lifted nodes) by rescoping matching rules to svg.
var scoped = sel.split(',').map(function (part) {
var at = part.indexOf('.diagram-container > svg');
if (at === -1) return null;
var prefix = part.slice(0, at).trim();
try { if (prefix && !document.documentElement.matches(prefix)) return null; } catch (_) { return null; }
return 'svg' + part.slice(at + '.diagram-container > svg'.length);
}).filter(Boolean);
if (scoped.length) out.push(scoped.join(', ') + ' { ' + rule.style.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 {
background: c.getPropertyValue('--bg').trim(),
vars: varNames.map(function (n) {
return n + ': ' + c.getPropertyValue(n).trim() + ';';
}).join(' ')
};
} finally {
document.body.removeChild(probe);
}
}
// Keep the same font bytes, character coverage, and attribution in
// standalone SVGs and the SVG images used by every raster export.
var fontCss = document.getElementById('archify-fonts').textContent;
var style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
var bgRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
// Cover the real viewBox: auto-cropped canvases start below y=0, and a
// 100% rect anchored at the origin left an unpainted band at the bottom.
bgRect.setAttribute('x', vb.x);
bgRect.setAttribute('y', vb.y);
bgRect.setAttribute('width', vb.width);
bgRect.setAttribute('height', vb.height);
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 darkTheme = resolveVars('dark');
var lightTheme = resolveVars('light');
style.textContent =
fontCss + "\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 { " + darkTheme.vars + " }\n" +
"@media (prefers-color-scheme: light) { :root, svg { " + lightTheme.vars + " } }\n" +
"svg[data-theme=\"light\"] { " + lightTheme.vars + " }\n" +
"svg[data-theme=\"dark\"] { " + darkTheme.vars + " }\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 exports keep the viewer's current theme. Explicit SVG
// exports instead select their own stable light/dark source.
var lockedTheme = requestedTheme || document.documentElement.getAttribute('data-theme') || 'dark';
var resolvedTheme = requestedTheme && resolveVars(lockedTheme);
var computed = getComputedStyle(document.documentElement);
var vars = resolvedTheme ? resolvedTheme.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 =
fontCss + "\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";
if (requestedTheme) clone.setAttribute('data-theme', lockedTheme);
bgRect.setAttribute('fill', (resolvedTheme ? resolvedTheme.background : computed.getPropertyValue('--bg').trim()) || '#ffffff');
}
if (opts.routeSnapshot) {
style.textContent += "\nsvg[data-share-route] [data-node-id], svg[data-share-route] [data-edge-from], svg[data-share-route] [data-graph-role=\"automatic-crossover-underlay\"] { opacity: 0.18; }\n" +
"svg[data-share-route] [data-share-route-match], svg[data-share-route] [data-graph-role=\"automatic-crossover\"]:has(> [data-share-route-match]) > [data-graph-role=\"automatic-crossover-underlay\"] { 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], svg[data-share-reach] [data-graph-role=\"automatic-crossover-underlay\"] { opacity: 0.14; }\n" +
"svg[data-share-reach] [data-share-reach-match], svg[data-share-reach] [data-graph-role=\"automatic-crossover\"]:has(> [data-share-reach-match]) > [data-graph-role=\"automatic-crossover-underlay\"] { 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";
}
// Figures draw the diagram on a painted canvas card, like the Viewer.
if (opts.figure) bgRect.setAttribute('fill', 'none');
clone.insertBefore(style, clone.firstChild);
clone.insertBefore(bgRect, style.nextSibling);
// The XML declaration pins UTF-8: without it, consumers that guess an
// encoding instead of defaulting to UTF-8 mangle non-ASCII text.
return {
svgString: '<?xml version="1.0" encoding="UTF-8"?>\n' +
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; a figure too large even at 1x is
// downscaled to fit rather than allocating an oversized canvas.
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 Math.sqrt(MAX_CANVAS_PIXELS / (vbW * vbH)) * 0.999;
}
// Raster exports reproduce the Viewer page without its controls: the
// title row with its accent dot, then the diagram on the same rounded
// canvas card (fill, hairline, shadow, dot field). CSS pixels.
var FIGURE_MARGIN = 28;
var FIGURE_CARD_PADDING = 24;
var FIGURE_TITLE_SIZE = 24;
var FIGURE_SUBTITLE_SIZE = 14;
function figureLayout(vb) {
var titleNode = document.querySelector('.header h1');
var subtitleNode = document.querySelector('.header .subtitle');
var container = document.querySelector('.diagram-container');
var root = getComputedStyle(document.documentElement);
var card = container ? getComputedStyle(container) : null;
var title = titleNode ? titleNode.textContent.trim() : '';
var subtitle = subtitleNode ? subtitleNode.textContent.trim() : '';
var header = title ? 32 + (subtitle ? 24 : 0) + 18 : 0;
var cardWidth = vb.width + FIGURE_CARD_PADDING * 2;
var cardHeight = vb.height + FIGURE_CARD_PADDING * 2;
var cardFill = card && card.backgroundColor && !/rgba\(0, 0, 0, 0\)|transparent/.test(card.backgroundColor)
? card.backgroundColor : (root.getPropertyValue('--panel').trim() || 'transparent');
return {
title: title,
subtitle: subtitle,
family: titleNode ? getComputedStyle(titleNode).fontFamily : 'sans-serif',
text: root.getPropertyValue('--text').trim() || '#111827',
muted: root.getPropertyValue('--text-muted').trim() || '#64748b',
accent: root.getPropertyValue('--frontend-stroke').trim() || '#0891b2',
bg: root.getPropertyValue('--bg').trim() || currentBg(),
cardFill: cardFill,
cardBorder: root.getPropertyValue('--panel-border').trim() || 'transparent',
dot: card && /radial-gradient/.test(card.backgroundImage) ? (root.getPropertyValue('--canvas-dot').trim() || '') : '',
light: (document.documentElement.getAttribute('data-theme') || 'dark') === 'light',
header: header,
cardWidth: cardWidth,
cardHeight: cardHeight,
width: cardWidth + FIGURE_MARGIN * 2,
height: cardHeight + FIGURE_MARGIN * 2 + header
};
}
function paintFigure(ctx, layout) {
var m = FIGURE_MARGIN;
ctx.fillStyle = layout.bg;
ctx.fillRect(0, 0, layout.width, layout.height);
if (layout.title) {
var titleMid = m + 16;
ctx.fillStyle = layout.accent;
ctx.globalAlpha = 0.16;
ctx.beginPath(); ctx.arc(m + 5, titleMid, 9, 0, Math.PI * 2); ctx.fill();
ctx.globalAlpha = 1;
ctx.beginPath(); ctx.arc(m + 5, titleMid, 5, 0, Math.PI * 2); ctx.fill();
ctx.textBaseline = 'middle';
ctx.fillStyle = layout.text;
ctx.fillText(fitCanvasText(ctx, layout.title, layout.width - m * 2 - 22, FIGURE_TITLE_SIZE, 14, '650', layout.family), m + 22, titleMid + 1);
if (layout.subtitle) {
ctx.fillStyle = layout.muted;
ctx.fillText(fitCanvasText(ctx, layout.subtitle, layout.width - m * 2, FIGURE_SUBTITLE_SIZE, 11, '400', layout.family), m, titleMid + 30);
}
ctx.textBaseline = 'alphabetic';
}
var x = m, y = m + layout.header, w = layout.cardWidth, h = layout.cardHeight, r = 16;
function cardPath() {
ctx.beginPath();
if (typeof ctx.roundRect === 'function') ctx.roundRect(x, y, w, h, r);
else ctx.rect(x, y, w, h);
}
ctx.save();
if (layout.light) {
ctx.shadowColor = 'rgba(15, 23, 42, 0.08)';
ctx.shadowBlur = 28;
ctx.shadowOffsetY = 10;
}
cardPath();
ctx.fillStyle = layout.bg;
ctx.fill();
ctx.restore();
cardPath();
ctx.fillStyle = layout.cardFill;
ctx.fill();
if (layout.dot) {
ctx.save();
cardPath();
ctx.clip();
ctx.fillStyle = layout.dot;
for (var dy = y + 1; dy < y + h; dy += 20) {
for (var dx = x + 1; dx < x + w; dx += 20) {
ctx.beginPath(); ctx.arc(dx, dy, 1, 0, Math.PI * 2); ctx.fill();
}
}
ctx.restore();
}
cardPath();
ctx.strokeStyle = layout.cardBorder;
ctx.lineWidth = 1;
ctx.stroke();
return { x: x + FIGURE_CARD_PADDING, y: y + FIGURE_CARD_PADDING };
}
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 layout = figureLayout(vb);
var scale = pickSafeScale(layout.width, layout.height);
var data = serializeSvg(scale, { figure: true });
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 = Math.round(layout.width * scale);
canvas.height = Math.round(layout.height * scale);
var ctx = canvas2dOrThrow(canvas, format);
// Paint in CSS pixels; the SVG image is already rendered at scale,
// so drawing it at CSS size under this transform stays 1:1.
ctx.setTransform(scale, 0, 0, scale, 0, 0);
var origin = paintFigure(ctx, layout);
ctx.drawImage(img, origin.x, origin.y, vb.width, vb.height);
ctx.setTransform(1, 0, 0, 1, 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, fontFamily) {
var value = String(text || '').trim();
var size = startSize;
var family = fontFamily || "'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) {
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, figure: true });
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 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
})
: 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)
});
var cardLabel = routeSnapshot
? viewerText('viewer.export.card.routeBadge', {
hops: viewerCount('viewer.export.card.hop', routeSnapshot.hops).toUpperCase()
})
: viewerText('viewer.export.card.reachBadge', { direction: directionLabel.toUpperCase() });
var family = titleNode ? getComputedStyle(titleNode).fontFamily : 'sans-serif';
var panelFill = bg;
ctx.fillStyle = bg;
ctx.fillRect(0, 0, SHARE_CARD_WIDTH, SHARE_CARD_HEIGHT);
// Header: a short accent rule and badge over the title and summary.
ctx.fillStyle = accent;
ctx.fillRect(SHARE_CARD_PADDING, 34, 28, 3);
ctx.textBaseline = 'alphabetic';
ctx.font = '600 12px ' + family;
ctx.textAlign = 'right';
ctx.fillStyle = muted;
ctx.fillText(cardLabel, SHARE_CARD_WIDTH - SHARE_CARD_PADDING, 40);
ctx.textAlign = 'left';
ctx.fillStyle = text;
var fittedTitle = fitCanvasText(ctx, title, SHARE_CARD_WIDTH - SHARE_CARD_PADDING * 2 - 260, 30, 18, '650', family);
ctx.fillText(fittedTitle, SHARE_CARD_PADDING, 74);
ctx.fillStyle = muted;
var fittedSubtitle = fitCanvasText(ctx, subtitle, SHARE_CARD_WIDTH - SHARE_CARD_PADDING * 2, 15, 12, '400', family);
ctx.fillText(fittedSubtitle, SHARE_CARD_PADDING, 99);
// The diagram sits on one soft rounded panel with an even inset.
var panelX = SHARE_CARD_PADDING;
var panelY = SHARE_CARD_HEADER;
var panelWidth = SHARE_CARD_WIDTH - SHARE_CARD_PADDING * 2;
var panelHeight = SHARE_CARD_HEIGHT - SHARE_CARD_HEADER - SHARE_CARD_PADDING;
var inset = 18;
ctx.beginPath();
if (typeof ctx.roundRect === 'function') ctx.roundRect(panelX, panelY, panelWidth, panelHeight, 14);
else ctx.rect(panelX, panelY, panelWidth, panelHeight);
ctx.fillStyle = panelFill;
ctx.fill();
ctx.strokeStyle = border;
ctx.lineWidth = 1;
ctx.stroke();
var availableWidth = panelWidth - inset * 2;
var availableHeight = panelHeight - inset * 2;
var fit = Math.min(availableWidth / data.width, availableHeight / data.height);
var drawWidth = data.width * fit;
var drawHeight = data.height * fit;
var drawX = panelX + inset + (availableWidth - drawWidth) / 2;
var drawY = panelY + inset + (availableHeight - drawHeight) / 2;
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 !== '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 === 'svg' || format === 'svg-light' || format === 'svg-dark' || 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' && !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) {
if (['svg', 'svg-light', 'svg-dark', 'png', 'jpeg', 'webp', 'webm'].indexOf(format) === -1) {
return Promise.reject(exportError('viewer.export.unsupported'));
}
var base = diagramFilename();
var svgTheme = format === 'svg' ? 'auto' :
format === 'svg-light' ? 'light' :
format === 'svg-dark' ? 'dark' : null;
close(true);
clearExportReceipt();
if (format === 'webm') toast(viewerText('viewer.export.recording'));
return (svgTheme
? Promise.resolve(serializeSvg(1, { theme: svgTheme })).then(function (d) {
var blob = new Blob([d.svgString], { type: 'image/svg+xml;charset=utf-8' });
recordExportReceipt('svg', blob, d.canonicalStateClean);
download(blob, base + (svgTheme === 'auto' ? '' : '-' + svgTheme) + '.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 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 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
};
// 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 === '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.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-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-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') ? 22 : 0;
var title = document.createElementNS(svgNamespace, 'title');
title.textContent = label;
var plate = document.createElementNS(svgNamespace, 'rect');
plate.setAttribute('height', '13');
plate.setAttribute('rx', '6.5');
var text = document.createElementNS(svgNamespace, 'text');
text.setAttribute('y', '9.4');
text.textContent = viewerText('viewer.passport.sourceMarker') + ' ' + count;
beacon.appendChild(title);
beacon.appendChild(plate);
beacon.appendChild(text);
node.appendChild(beacon);
// Measure the real glyph advance, then clamp the plate between 24
// and 36px; a still-wider label is squeezed with textLength so the
// badge never overlaps the neighbouring brand mark or sigil.
var textWidth = 16;
try {
var measured = text.getComputedTextLength();
if (Number.isFinite(measured) && measured > 0) textWidth = measured;
} catch (_) {}
var plateWidth = Math.min(36, Math.max(24, textWidth + 8));
if (textWidth > plateWidth - 8) {
text.setAttribute('textLength', String(plateWidth - 8));
text.setAttribute('lengthAdjust', 'spacingAndGlyphs');
}
plate.setAttribute('width', String(plateWidth));
text.setAttribute('x', String(plateWidth / 2));
beacon.setAttribute('transform', 'translate(' + (box.x + box.width - 6 - brandOffset - plateWidth) + ' ' + (box.y + 4.5) + ')');
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 moveBtn = document.getElementById('btn-focus-move');
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 lensDrag = null;
var lensDragClickPointer = null;
var manualLensPosition = 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;
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' });
}
placeRelationshipLens();
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.removeAttribute('aria-label');
repositoryLink.textContent = '';
var sources = Archify.sourceEvidence.node(id);
var repository = Archify.sourceEvidence.repository();
if (!repository || !sources.length) {
evidence.hidden = true;
return;
}
repositoryLink.textContent = repository.label + ' @ ' + repository.shortRevision;
if (repository.href) {
repositoryLink.href = repository.href;
repositoryLink.setAttribute('aria-label', viewerText('viewer.passport.repository.open', { revision: repository.revision }));
}
evidence.title = viewerText('viewer.passport.verificationScope');
sources.forEach(function (source) {
var link = document.createElement(source.href ? 'a' : 'div');
link.className = 'semantic-passport-source';
if (source.href) {
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 : '') + (source.href ? ' ↗' : '')
: source.href ? 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 = Object.create(null);
var byId = Object.create(null);
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();
placeRelationshipLens();
}
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);
placeRelationshipLens();
}
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-route-picking') ||
svg.hasAttribute('data-route-active') ||
svg.hasAttribute('data-lens-active');
}
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;
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);
});
}
function lensPlacementBounds() {
var containerRect = container.getBoundingClientRect();
if (containerRect.width <= 0 || containerRect.height <= 0 || chip.offsetWidth <= 0 || chip.offsetHeight <= 0) return null;
var padding = window.innerWidth <= 720 ? 8 : 16;
var visibleLeft = Math.max(padding, -containerRect.left + padding);
var visibleRight = Math.min(
container.clientWidth - padding,
window.innerWidth - containerRect.left - padding
);
var visibleTop = Math.max(padding, -containerRect.top + padding);
var visibleBottom = Math.min(
container.clientHeight - padding,
window.innerHeight - containerRect.top - padding
);
var minLeft = visibleLeft;
var minTop = Math.min(visibleTop, Math.max(padding, visibleBottom - chip.offsetHeight));
return {
containerRect: containerRect,
minLeft: minLeft,
maxLeft: Math.max(minLeft, visibleRight - chip.offsetWidth),
minTop: minTop,
maxTop: Math.max(minTop, visibleBottom - chip.offsetHeight)
};
}
function manualLensPlacementAvailable() {
return window.innerWidth > 720 && (!finePointerQuery || finePointerQuery.matches);
}
function clampLensPosition(position, bounds) {
if (!position || !bounds) return null;
return {
left: Math.max(bounds.minLeft, Math.min(bounds.maxLeft, position.left)),
top: Math.max(bounds.minTop, Math.min(bounds.maxTop, position.top))
};
}
function currentLensPosition() {
var bounds = lensPlacementBounds();
if (!bounds) return null;
var rect = chip.getBoundingClientRect();
return clampLensPosition({
left: rect.left - bounds.containerRect.left,
top: rect.top - bounds.containerRect.top
}, bounds);
}
function applyManualLensPosition(position) {
if (!manualLensPlacementAvailable()) return false;
var bounds = lensPlacementBounds();
var clamped = clampLensPosition(position, bounds);
if (!clamped) return false;
manualLensPosition = clamped;
chip.setAttribute('data-manual-placement', 'true');
chip.style.left = Math.round(clamped.left) + 'px';
chip.style.top = Math.round(clamped.top) + 'px';
if (Archify.radar && typeof Archify.radar.sync === 'function') Archify.radar.sync();
return true;
}
function resetLensPlacement(options) {
options = options || {};
manualLensPosition = null;
chip.removeAttribute('data-manual-placement');
chip.style.removeProperty('left');
chip.style.removeProperty('top');
if (options.reposition !== false && !chip.hidden) requestLensPlacement();
}
function beginLensDrag(event) {
if (!manualLensPlacementAvailable() || event.button !== 0 || lensDrag) return;
var start = currentLensPosition();
if (!start) return;
event.preventDefault();
event.stopPropagation();
lensDrag = {
pointerId: event.pointerId,
originX: event.clientX,
originY: event.clientY,
start: start,
previousManual: manualLensPosition ? { left: manualLensPosition.left, top: manualLensPosition.top } : null,
moved: false
};
chip.setAttribute('data-panel-dragging', 'true');
try { moveBtn.setPointerCapture(event.pointerId); } catch (_) {}
}
function moveLensDrag(event) {
if (!lensDrag || lensDrag.pointerId !== event.pointerId) return;
var dx = event.clientX - lensDrag.originX;
var dy = event.clientY - lensDrag.originY;
if (!lensDrag.moved && Math.hypot(dx, dy) <= 3) return;
lensDrag.moved = true;
event.preventDefault();
event.stopPropagation();
applyManualLensPosition({ left: lensDrag.start.left + dx, top: lensDrag.start.top + dy });
}
function finishLensDrag(event, cancel) {
if (!lensDrag || lensDrag.pointerId !== event.pointerId) return;
event.preventDefault();
event.stopPropagation();
var activeDrag = lensDrag;
lensDrag = null;
if (activeDrag.moved) lensDragClickPointer = activeDrag.pointerId;
chip.removeAttribute('data-panel-dragging');
try { moveBtn.releasePointerCapture(event.pointerId); } catch (_) {}
if (!cancel) return;
manualLensPosition = activeDrag.previousManual
? { left: activeDrag.previousManual.left, top: activeDrag.previousManual.top }
: null;
if (manualLensPosition && manualLensPlacementAvailable()) {
applyManualLensPosition(manualLensPosition);
} else {
chip.removeAttribute('data-manual-placement');
chip.style.removeProperty('left');
chip.style.removeProperty('top');
if (!chip.hidden) requestLensPlacement();
}
}
function moveLensWithKeyboard(event) {
if (event.key === 'Home') {
event.preventDefault();
resetLensPlacement();
return;
}
if (event.metaKey || event.ctrlKey || event.altKey || !manualLensPlacementAvailable()) return;
var directions = {
ArrowLeft: [-1, 0],
ArrowRight: [1, 0],
ArrowUp: [0, -1],
ArrowDown: [0, 1]
};
var direction = directions[event.key];
if (!direction) return;
event.preventDefault();
var current = manualLensPosition || currentLensPosition();
if (!current) return;
var distance = event.shiftKey ? 4 : 16;
applyManualLensPosition({
left: current.left + direction[0] * distance,
top: current.top + direction[1] * distance
});
}
var lensFrame = 0;
function placeRelationshipLens() {
if (lensFrame) {
cancelAnimationFrame(lensFrame);
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 mobile = window.innerWidth <= 720;
if (!manualLensPlacementAvailable()) {
chip.removeAttribute('data-manual-placement');
chip.style.removeProperty('left');
} else if (manualLensPosition) {
applyManualLensPosition(manualLensPosition);
return;
}
var nodeCenter = nodeRect.top - containerRect.top + nodeRect.height / 2;
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 || {};
if (lensDrag) {
var dragPointerId = lensDrag.pointerId;
lensDrag = null;
chip.removeAttribute('data-panel-dragging');
try { moveBtn.releasePointerCapture(dragPointerId); } catch (_) {}
}
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');
if (options.preserveLensPlacement !== true) resetLensPlacement({ reposition: false });
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 = Object.create(null);
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;
}
var preserveLensPlacement = activeIds.length === 1 && normalized.length === 1 && !chip.hidden;
clear({ updateUrl: false, preserveView: true, preserveLensPlacement: preserveLensPlacement });
activeIds = normalized;
var selected = Object.create(null);
var related = Object.create(null);
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.focus.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);
placeRelationshipLens();
}
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' });
}
});
moveBtn.addEventListener('pointerdown', beginLensDrag);
moveBtn.addEventListener('pointermove', moveLensDrag);
moveBtn.addEventListener('pointerup', function (event) { finishLensDrag(event, false); });
moveBtn.addEventListener('pointercancel', function (event) { finishLensDrag(event, true); });
moveBtn.addEventListener('lostpointercapture', function (event) { finishLensDrag(event, true); });
moveBtn.addEventListener('dblclick', function (event) {
event.preventDefault();
event.stopPropagation();
resetLensPlacement();
});
moveBtn.addEventListener('keydown', moveLensWithKeyboard);
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'));
placeRelationshipLens();
});
relationshipList.addEventListener('click', function (event) {
var button = event.target.closest('[data-relationship-target]');
if (!button) return;
var id = button.getAttribute('data-relationship-target');
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('pointerdown', function () {
lensDragClickPointer = null;
}, true);
document.addEventListener('click', function (event) {
// Losing capture (for example when a resize hides the handle) can
// retarget this drag's final click to the page. It is not dismissal.
// A new pointerdown releases the guard; keyboard clicks remain live.
if (lensDragClickPointer != null && event.detail > 0 &&
(event.pointerId == null || event.pointerId === lensDragClickPointer)) {
lensDragClickPointer = null;
event.preventDefault();
event.stopPropagation();
return;
}
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 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: placeRelationshipLens,
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-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 = Object.create(null);
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 = Object.create(null);
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; }
};
})();
/* ============================================================
Adaptive Reader Shell — one diagram across laptop and monitor.
The canonical SVG and viewBox never change. On ordinary desktop pages,
wide diagrams and measured intrinsic tall workflows receive enough outer width to use the available
height without pushing summary cards below the viewport. Mobile,
embed, presentation, print, and unmarked non-wide diagrams retain their own
established layout contracts.
============================================================ */
// Fixed script-private participants; the CLI sees only the joint wait below.
const archifyLayoutOwners = { reader: null, viewerChrome: null };
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 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 measuredHeightFit = svg && svg.getAttribute('data-reader-fit') === 'intrinsic-height';
var frame = 0;
var settleFrame = 0;
var lastWidth = 0;
// Widest reader the overflow settle has accepted for this viewport (0 =
// uncapped). Card copy rewraps as the reader narrows, so recomputing
// from fixed heights alone would widen again and oscillate; only a
// resize lifts the cap.
var settledCap = 0;
var WIDE_RATIO = 1.55;
var MIN_DESKTOP_WIDTH = 1024;
var MIN_READER_WIDTH = 960;
var MAX_READER_WIDTH = 1920;
var MIN_PROJECTED_NODE_TEXT_PX = 6;
var declaredMinimumText = svg ? parseFloat(svg.getAttribute('data-reader-min-text') || '') : null;
var requestedMinimumText = Number.isFinite(declaredMinimumText)
? Math.max(MIN_PROJECTED_NODE_TEXT_PX, declaredMinimumText)
: MIN_PROJECTED_NODE_TEXT_PX;
var declaredPrimaryText = svg ? parseFloat(svg.getAttribute('data-reader-primary-text') || '') : null;
var SAFE_BOTTOM_GAP = 12;
// Wide viewports move summary cards beside the diagram, so the height
// budget belongs to the diagram and spare width holds the notes.
var RAIL_MIN_VIEWPORT = 1280;
var RAIL_WIDTH = 288;
var RAIL_GAP = 20;
// A default rail must leave primary node labels comfortably readable;
// otherwise it starts collapsed and the reader can still open it.
var RAIL_COMFORT_PRIMARY_PX = 12;
var railPanel = shell && shell.querySelector('.reader-rail');
var railReveal = document.getElementById('rail-reveal');
var railCollapse = document.getElementById('rail-collapse');
var railPlacement = document.getElementById('rail-placement');
var RAIL_COLLAPSED_KEY = 'archify-rail-collapsed';
var RAIL_PLACEMENT_KEY = 'archify-rail-placement';
// Memory is the page's source of truth; storage only carries the choice
// to later artifacts, so a blocked or full localStorage cannot freeze it.
var preferences = Object.create(null);
function readPreference(key) {
if (key in preferences) return preferences[key];
try { return localStorage.getItem(key); } catch (_) { return null; }
}
function writePreference(key, value) {
preferences[key] = value;
try { localStorage.setItem(key, value); } catch (_) {}
}
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);
}
// Smallest scale that keeps node text at the requested floor and
// context relationship labels readable. A declared wide reader holds
// relationship labels to the requested floor too; every other reader
// still keeps them at the 6px hard floor the browser gate enforces, so
// a first-screen fit can never push them below it.
function minimumReadableScale() {
var nodeMinimum = null;
var edgeMinimum = null;
var selectors = 'text[data-node-label], text[data-boundary-label], text[data-detail="context"], g[data-detail="context"] text';
Array.from(svg.querySelectorAll(selectors)).forEach(function (text) {
if (text.getAttribute('data-detail') === 'fine' || text.closest('[data-detail="fine"]')) return;
var sourceFontPx = parseFloat(text.getAttribute('font-size') || '');
if (!Number.isFinite(sourceFontPx)) return;
var primary = text.hasAttribute('data-node-label') || text.hasAttribute('data-boundary-label');
var context = text.getAttribute('data-detail') === 'context' || Boolean(text.closest('g[data-detail="context"]'));
if (!primary && context && text.closest('[data-edge-from][data-edge-to]')) {
edgeMinimum = edgeMinimum == null ? sourceFontPx : Math.min(edgeMinimum, sourceFontPx);
} else if (primary || text.closest('[data-node-id]')) {
nodeMinimum = nodeMinimum == null ? sourceFontPx : Math.min(nodeMinimum, sourceFontPx);
}
});
var edgeTarget = measuredHeightFit && ratio >= WIDE_RATIO && Number.isFinite(declaredMinimumText)
? requestedMinimumText
: MIN_PROJECTED_NODE_TEXT_PX;
var scale = 0;
if (nodeMinimum != null) scale = Math.max(scale, requestedMinimumText / nodeMinimum);
if (edgeMinimum != null) scale = Math.max(scale, edgeTarget / edgeMinimum);
return scale > 0 ? Math.min(1, scale) : 1;
}
var sourcePrimary = null;
if (svg) Array.from(svg.querySelectorAll('text[data-node-label]')).forEach(function (text) {
var size = parseFloat(text.getAttribute('font-size') || '');
if (Number.isFinite(size) && size > 0) sourcePrimary = sourcePrimary == null ? size : Math.max(sourcePrimary, size);
});
// SVG width at which the largest node label renders at targetPx. Every
// renderer writes label font sizes, so the rail comfort check works for
// all diagram types; only the renderer-declared floor needs metadata.
function labelWidth(targetPx) {
return sourcePrimary == null || !viewBox ? 0 : viewBox.width * targetPx / sourcePrimary;
}
function primaryReadingWidth() {
if (!measuredHeightFit || !Number.isFinite(declaredPrimaryText) || declaredPrimaryText <= 0) return 0;
// Primary labels should remain comfortable to read when cards or
// auxiliary rows make a one-screen fit too small. Ordinary page
// scroll preserves that reading size; viewport width still caps it.
// A long title may already use a smaller fitted font. Preserve that
// hierarchy rather than enlarging every other node to compensate.
return labelWidth(declaredPrimaryText);
}
function eligible() {
return Boolean(
shell && diagram && svg && (ratio >= WIDE_RATIO || measuredHeightFit) &&
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.style.removeProperty('--archify-diagram-max-width');
html.removeAttribute('data-reader-narrow');
html.removeAttribute('data-reader-layout');
html.removeAttribute('data-reader-overflow');
setRail(false);
lastWidth = 0;
settledCap = 0;
}
// Rail modes: "true" docks beside the diagram, "collapsed" leaves only the
// reveal control, "overlay" opens a drawer when docking would break the
// readable floor, and "bottom" stacks notes and index below the diagram.
function setRail(mode) {
if (mode) {
html.setAttribute('data-reader-rail', mode);
html.style.setProperty('--archify-rail-width', RAIL_WIDTH + 'px');
html.style.setProperty('--archify-rail-gap', RAIL_GAP + 'px');
} else {
html.removeAttribute('data-reader-rail');
html.style.removeProperty('--archify-rail-width');
html.style.removeProperty('--archify-rail-gap');
}
if (railReveal) railReveal.hidden = mode !== 'collapsed';
if (railCollapse) {
railCollapse.hidden = mode !== 'true' && mode !== 'overlay';
railCollapse.setAttribute('aria-expanded', String(mode === 'true' || mode === 'overlay'));
}
if (railPlacement) {
railPlacement.hidden = !mode || mode === 'collapsed' || (mode === 'bottom' && window.innerWidth < RAIL_MIN_VIEWPORT);
railPlacement.setAttribute('data-placement', mode === 'bottom' ? 'bottom' : 'right');
railPlacement.setAttribute('aria-label', viewerText(mode === 'bottom' ? 'viewer.rail.right' : 'viewer.rail.bottom'));
railPlacement.title = railPlacement.getAttribute('aria-label');
}
}
var outline = document.getElementById('node-outline');
function hasCards() {
return Boolean((cards && cards.children.length && !cards.hidden) || (outline && !outline.hidden));
}
// Bottom notes and index are reading material below the fold: the first
// screen belongs to the interactive diagram and its controls.
function belowFold() {
return html.getAttribute('data-reader-rail') === 'bottom' ? outerHeight(railPanel) : 0;
}
// A docked rail may run past a short diagram down to the viewport floor,
// so the index uses that space instead of scrolling inside the diagram's
// height; it never pushes the page into overflow.
function fitDockedRail() {
if (html.getAttribute('data-reader-rail') !== 'true' || !railPanel) return;
var top = railPanel.getBoundingClientRect().top + window.scrollY;
var floor = window.innerHeight - number(window.getComputedStyle(body).paddingBottom) - top;
html.style.setProperty('--archify-rail-max', Math.max(diagram.getBoundingClientRect().height, floor) + 'px');
}
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)
};
}
// `width` is the diagram's reading width. The page shell never narrows
// below the desktop reader floor, so a narrow, tall diagram keeps a
// usable header, toolbar, and controls; its SVG is centred instead.
function applyWidth(width, minWidth) {
var rounded = Math.max(Math.ceil(minWidth || 0), Math.round(width));
if (Math.abs(rounded - lastWidth) < 1) return false;
lastWidth = rounded;
var shellFloor = Math.min(MIN_READER_WIDTH, Math.max(0, window.innerWidth - chromeMetrics().bodyX));
html.style.setProperty('--archify-reader-width', Math.max(rounded, shellFloor) + 'px');
if (rounded < shellFloor) {
// `rounded` already includes a docked rail and its gap; the SVG cap
// is only the diagram's share, not the space beside it.
var railShare = html.getAttribute('data-reader-rail') === 'true' ? RAIL_WIDTH + RAIL_GAP : 0;
html.style.setProperty('--archify-diagram-max-width', Math.max(1, rounded - railShare - chromeMetrics().diagramX) + 'px');
html.setAttribute('data-reader-narrow', 'true');
} else {
html.style.removeProperty('--archify-diagram-max-width');
html.removeAttribute('data-reader-narrow');
}
html.setAttribute('data-reader-layout', 'adaptive');
return true;
}
function settleOverflow(minWidth) {
if (settleFrame) cancelAnimationFrame(settleFrame);
settleFrame = requestAnimationFrame(function () {
settleFrame = 0;
if (!eligible() || !lastWidth) return;
fitDockedRail();
var overflow = Math.max(
document.documentElement.scrollHeight,
document.body.scrollHeight
) - window.innerHeight - belowFold();
if (overflow > 1 && lastWidth > Math.ceil(minWidth)) {
applyWidth(Math.max(minWidth, lastWidth - overflow * ratio - 4), minWidth);
settledCap = lastWidth;
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 readableWidth = viewBox && viewBox.width > 0
? viewBox.width * minimumReadableScale() + chrome.diagramX
: MIN_READER_WIDTH;
var maxWidth = Math.min(MAX_READER_WIDTH, viewportCap);
var readableMinimumWidth = measuredHeightFit && ratio < WIDE_RATIO
? readableWidth
: measuredHeightFit && ratio >= WIDE_RATIO && Number.isFinite(declaredMinimumText)
? Math.max(MIN_READER_WIDTH, readableWidth)
: MIN_READER_WIDTH;
var minWidth;
if (measuredHeightFit && ratio < WIDE_RATIO) {
minWidth = Math.min(readableMinimumWidth, viewportCap);
} else if (measuredHeightFit && ratio >= WIDE_RATIO && Number.isFinite(declaredMinimumText)) {
minWidth = Math.min(readableMinimumWidth, maxWidth);
} else {
minWidth = Math.min(readableMinimumWidth, viewportCap);
}
var primaryWidth = primaryReadingWidth();
var railExtra = RAIL_WIDTH + RAIL_GAP;
var mode = null;
// Notes default below the diagram; the right rail is the reader's
// opt-in and needs a wide viewport.
if (hasCards() && (readPreference(RAIL_PLACEMENT_KEY) !== 'right' || window.innerWidth < RAIL_MIN_VIEWPORT)) {
mode = 'bottom';
} else if (hasCards()) {
var fitsReadable = readableMinimumWidth + railExtra <= maxWidth;
var comfortWidth = labelWidth(RAIL_COMFORT_PRIMARY_PX);
var comfortable = fitsReadable && (!comfortWidth || comfortWidth + chrome.diagramX + railExtra <= maxWidth);
var collapsedPreference = readPreference(RAIL_COLLAPSED_KEY);
if (collapsedPreference === '1' || (collapsedPreference !== '0' && !comfortable)) mode = 'collapsed';
else mode = fitsReadable ? 'true' : 'overlay';
}
// With notes below the fold, the first screen belongs to the whole
// diagram and its controls: it may shrink past the comfortable primary
// size down to the renderer's readable text floor, and zoom restores
// detail. Only a graph taller than that floor allows still scrolls.
if (mode === 'bottom') primaryWidth = 0;
if (primaryWidth > 0) minWidth = Math.max(minWidth, Math.min(maxWidth, primaryWidth + chrome.diagramX));
var docked = mode === 'true';
setRail(mode);
chrome = chromeMetrics();
if (docked) minWidth = Math.min(maxWidth, minWidth + railExtra);
var stackedBelow = mode === 'bottom' ? 0 : docked ? 0 : outerHeight(cards);
var fixedHeight = chrome.bodyY + chrome.diagramY + SAFE_BOTTOM_GAP +
outerHeight(header) + stackedBelow;
var availableSvgHeight = Math.max(1, window.innerHeight - fixedHeight);
var desiredWidth = availableSvgHeight * ratio + chrome.diagramX + (docked ? railExtra : 0);
var width = Math.max(minWidth, Math.min(maxWidth, desiredWidth, settledCap || desiredWidth));
applyWidth(width, minWidth);
settleOverflow(minWidth);
return {
ratio: ratio,
width: lastWidth,
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 layoutPending() { return Boolean(frame || settleFrame); }
function whenStable() {
return Archify.waitForStableLayout({
schedule: schedule,
pending: layoutPending,
snapshot: stableSnapshot,
timeoutMessage: 'Adaptive reader layout did not reach stable dimensions.'
});
}
archifyLayoutOwners.reader = { schedule: schedule, pending: layoutPending, snapshot: stableSnapshot };
window.addEventListener('resize', function () {
settledCap = 0;
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, cards].forEach(function (element) { if (element) resizeObserver.observe(element); });
}
if (typeof MutationObserver === 'function') {
var contentObserver = new MutationObserver(schedule);
if (cards) contentObserver.observe(cards, { attributes: true, childList: true, subtree: true });
contentObserver.observe(html, { attributes: true, attributeFilter: ['data-embed', 'data-present'] });
}
// Reader choices persist across artifacts; a resize-style remeasure
// applies them without reloading.
function chooseRail(key, value) {
writePreference(key, value);
settledCap = 0;
schedule();
}
function collapseRail() {
chooseRail(RAIL_COLLAPSED_KEY, '1');
if (railReveal) requestAnimationFrame(function () { try { railReveal.focus({ preventScroll: true }); } catch (_) {} });
}
if (railReveal) railReveal.addEventListener('click', function () { chooseRail(RAIL_COLLAPSED_KEY, '0'); });
if (railCollapse) railCollapse.addEventListener('click', collapseRail);
if (railPlacement) railPlacement.addEventListener('click', function () {
var toBottom = html.getAttribute('data-reader-rail') !== 'bottom';
if (!toBottom) writePreference(RAIL_COLLAPSED_KEY, '0');
chooseRail(RAIL_PLACEMENT_KEY, toBottom ? 'bottom' : 'right');
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape' && html.getAttribute('data-reader-rail') === 'overlay') collapseRail();
});
document.addEventListener('pointerdown', function (event) {
if (html.getAttribute('data-reader-rail') !== 'overlay' || !railPanel) return;
if (!railPanel.contains(event.target) && !(railReveal && railReveal.contains(event.target))) chooseRail(RAIL_COLLAPSED_KEY, '1');
});
if (typeof ResizeObserver === 'function' && railPanel) new ResizeObserver(schedule).observe(railPanel);
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;
}
/* When the stage runs below the viewport, the dock lifts to the viewport
floor so its controls stay reachable while reading. Stage-rail
decisions and receipts use its resting position, so the lift never
feeds back into layout. */
var lift = 0;
var liftFrame = 0;
function restingNavRect() {
var rect = nav.getBoundingClientRect();
if (!lift) return rect;
return { x: rect.x, y: rect.y + lift, left: rect.left, right: rect.right, top: rect.top + lift, bottom: rect.bottom + lift, width: rect.width, height: rect.height };
}
function updateLift() {
liftFrame = 0;
var next = 0;
if (eligible()) {
var rest = restingNavRect();
var box = container.getBoundingClientRect();
next = Math.round(Math.max(0, Math.min(rest.bottom - (window.innerHeight - 16), rest.top - box.top - 16)));
}
if (next === lift) return;
lift = next;
if (lift) container.style.setProperty('--archify-dock-lift', lift + 'px');
else container.style.removeProperty('--archify-dock-lift');
}
function scheduleLift() {
if (!liftFrame) liftFrame = requestAnimationFrame(updateLift);
}
function measure() {
frame = 0;
scheduleLift();
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 = restingNavRect();
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 = restingNavRect();
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 = restingNavRect();
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 ? restingNavRect() : { 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 layoutPending() { return Boolean(frame || settleFrame || probingBaseline); }
function whenStable() {
return Archify.waitForStableLayout({
schedule: schedule,
pending: layoutPending,
snapshot: stableSnapshot,
timeoutMessage: 'Viewer chrome layout did not reach stable dimensions.'
});
}
archifyLayoutOwners.viewerChrome = { schedule: schedule, pending: layoutPending, snapshot: stableSnapshot };
window.addEventListener('resize', reprobe, { passive: true });
window.addEventListener('scroll', scheduleLift, { 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) {
// A theme switch changes paint, not the stage geometry. Reprobing
// drops the bottom rail for several frames and makes the diagram jump.
var viewerModeChanged = records.some(function (record) {
return record.target === html && record.attributeName !== 'data-theme';
});
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,
dockRect: function () { return nav ? restingNavRect() : null; },
active: function () { return reserve > 0; },
receipt: function () { return lastReceipt || measure(); }
};
})();
Archify.layoutStability = {
whenStable: function () {
var reader = archifyLayoutOwners.reader;
var chrome = archifyLayoutOwners.viewerChrome;
if (!reader || !chrome) {
return Promise.reject(new Error('Joint layout owners are unavailable.'));
}
var observedFontsReady = document.fonts && document.fonts.ready;
var fontCyclePending = false;
var fontGeneration = 0;
function scheduleOwners() { reader.schedule(); chrome.schedule(); }
function fontsPending() {
var fonts = document.fonts;
var ready = fonts && fonts.ready;
if (ready && (ready !== observedFontsReady || (fonts.status === 'loading' && !fontCyclePending))) {
observedFontsReady = ready;
fontCyclePending = true;
var generation = ++fontGeneration;
ready.catch(function () {}).then(function () {
if (generation !== fontGeneration) return;
fontCyclePending = false;
scheduleOwners();
});
}
return fontCyclePending || Boolean(fonts && fonts.status === 'loading');
}
return Archify.waitForStableLayout({
maximumFrames: 960,
schedule: scheduleOwners,
pending: function () {
// Recapture readiness for font cycles that begin during sampling.
return fontsPending() || reader.pending() || chrome.pending();
},
snapshot: function () {
return JSON.stringify([
['reader', reader.snapshot()],
['viewerChrome', chrome.snapshot()]
]);
},
timeoutMessage: 'Joint reader and viewer chrome layout did not reach stable dimensions.'
});
}
};
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) {
var rendered = sampleRenderedState();
stopCameraMotion(reason || 'manual', false);
state = rendered;
state.mode = 'manual';
apply();
renderControls();
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 = Object.create(null);
var wanted = Object.create(null);
(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 - viewBox.x) * contentScale,
y: contentOffsetY + (minY - viewBox.y) * 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.x) / 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 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');
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;
trigger.setAttribute('data-radar-requested', 'true');
build();
attemptRequestedOpen(options);
} else {
clearSpaceRetry();
spaceRetryCount = 0;
viewportDrag = null;
panelDrag = null;
container.classList.remove('is-panning');
panel.hidden = true;
resetDockingStyles();
restorePassport();
feedback.hidden = true;
// Focus before collapsing: the dock hides this trigger at 100% unless it holds focus.
if (options.restoreFocus === true) trigger.focus();
trigger.setAttribute('aria-expanded', 'false');
trigger.removeAttribute('data-radar-requested');
trigger.removeAttribute('data-radar-space-limited');
trigger.setAttribute('aria-label', viewerText('viewer.nav.radar'));
trigger.title = viewerText('viewer.nav.radar.title');
}
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.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
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 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.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
};
})();
/* ============================================================
Node Outline — a rail index of the semantic nodes, grouped by their
authored context. Hover previews neighbors through Intent Trace;
selection delegates to the Finder's focus-and-reveal path. It reads
the canonical SVG and never changes geometry or export state.
============================================================ */
Archify.outline = (function () {
var panel = document.getElementById('node-outline');
var list = document.getElementById('node-outline-list');
var count = document.getElementById('node-outline-count');
var svg = document.querySelector('.diagram-container > svg');
if (!panel || !list || !svg) return { count: 0 };
var TONES = ['frontend', 'backend', 'database', 'cloud', 'security', 'messagebus', 'external'];
var nodes = Array.from(svg.querySelectorAll('[data-node-id]')).filter(function (node) {
return !node.closest('[data-legend-hit], .legend') && node.getAttribute('data-node-label');
});
var buttons = Object.create(null);
function tone(node) {
var shape = node.querySelector('[class*="c-"]:not(.c-mask)');
var match = shape && /(?:^|\s)c-([a-z]+)/.exec(shape.getAttribute('class') || '');
return match && TONES.indexOf(match[1]) !== -1 ? match[1] : 'external';
}
function preview(id) {
if (Archify.intentTrace && typeof Archify.intentTrace.show === 'function') Archify.intentTrace.show(id);
}
function clearPreview() {
if (Archify.intentTrace && typeof Archify.intentTrace.clear === 'function') Archify.intentTrace.clear({ announce: false });
}
function select(id) {
clearPreview();
var finder = Archify.finder;
if (finder && typeof finder.select === 'function') {
if (finder.select(id)) return;
var routeMode = Archify.routeProbe && typeof Archify.routeProbe.active === 'function' ? Archify.routeProbe.active() : 'idle';
// Only an active endpoint picker owns a refused selection.
if (routeMode === 'source' || routeMode === 'target') return;
}
if (Archify.focus && typeof Archify.focus.set === 'function') Archify.focus.set(id, { toggle: false });
}
var groups = [];
var byContext = Object.create(null);
nodes.forEach(function (node) {
// Group by the outermost context (lane or stage); deeper segments
// such as workflow groups and columns would split the index into
// one-node sections.
var context = (node.getAttribute('data-node-context') || '').split(' \u203a ')[0];
if (!byContext[context]) {
byContext[context] = { label: context, nodes: [] };
groups.push(byContext[context]);
}
byContext[context].nodes.push(node);
});
groups.forEach(function (group) {
var section = document.createElement('li');
section.className = 'node-outline-group';
if (groups.length > 1 && group.label) {
var heading = document.createElement('span');
heading.className = 'node-outline-heading';
var headingLabel = document.createElement('span');
headingLabel.textContent = group.label;
var headingCount = document.createElement('small');
headingCount.textContent = String(group.nodes.length);
heading.appendChild(headingLabel);
heading.appendChild(headingCount);
section.appendChild(heading);
}
var items = document.createElement('ul');
group.nodes.forEach(function (node) {
var id = node.getAttribute('data-node-id');
var item = document.createElement('li');
var button = document.createElement('button');
button.type = 'button';
button.className = 'node-outline-item';
button.setAttribute('data-outline-node', id);
button.style.setProperty('--outline-tone', 'var(--' + tone(node) + '-stroke)');
var dot = document.createElement('span');
dot.className = 'node-outline-dot';
dot.setAttribute('aria-hidden', 'true');
var label = document.createElement('strong');
label.textContent = node.getAttribute('data-node-label');
button.appendChild(dot);
button.appendChild(label);
var sublabel = node.getAttribute('data-node-sublabel');
if (sublabel) {
var detail = document.createElement('small');
detail.textContent = sublabel;
button.appendChild(detail);
}
button.addEventListener('pointerenter', function () { preview(id); });
button.addEventListener('focus', function () { preview(id); });
button.addEventListener('pointerleave', clearPreview);
button.addEventListener('blur', clearPreview);
button.addEventListener('click', function () { select(id); });
buttons[id] = button;
item.appendChild(button);
items.appendChild(item);
});
section.appendChild(items);
list.appendChild(section);
});
var previewing = null;
function syncCurrent() {
nodes.forEach(function (node) {
var button = buttons[node.getAttribute('data-node-id')];
if (!button) return;
if (node.getAttribute('aria-pressed') === 'true') button.setAttribute('aria-current', 'true');
else button.removeAttribute('aria-current');
});
// Mirror a diagram-side hover so the index answers "where is this?".
var active = buttons[svg.getAttribute('data-intent-trace-active') || ''] || null;
if (active === previewing) return;
if (previewing) previewing.removeAttribute('data-previewing');
previewing = active;
if (!active) return;
active.setAttribute('data-previewing', '');
if (!active.matches(':hover') && panel.offsetParent) {
var box = list.getBoundingClientRect();
var item = active.getBoundingClientRect();
if (item.top < box.top || item.bottom > box.bottom - 24) {
list.scrollTop += item.top - box.top - box.height / 2 + item.height / 2;
}
}
}
if (typeof MutationObserver === 'function') {
new MutationObserver(syncCurrent).observe(svg, { attributes: true, subtree: true, attributeFilter: ['aria-pressed', 'data-intent-trace-active'] });
}
if (count) count.textContent = String(nodes.length);
panel.hidden = nodes.length < 2;
syncCurrent();
if (Archify.readerLayout && typeof Archify.readerLayout.schedule === 'function') Archify.readerLayout.schedule();
return { count: nodes.length, select: select };
})();
/* ============================================================
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 = Object.create(null);
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 = Object.create(null);
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 = Object.create(null);
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 = Object.create(null);
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.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 = Object.create(null);
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 = Object.create(null);
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 = Object.create(null);
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-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 = Object.create(null);
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.create(null);
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.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, 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 routePanel = document.getElementById('route-probe');
var returnFocus = null;
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();
stats.textContent = viewerText('viewer.guide.facts', {
nodes: viewerCount('viewer.guide.fact.node', nodes),
relationships: viewerCount('viewer.guide.fact.relationship', relationships)
});
}
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) {
var target = trigger.hidden ? returnFocus : trigger;
if (target && typeof target.focus === 'function') target.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.routeProbe && Archify.routeProbe.isJourneyPlaying && Archify.routeProbe.isJourneyPlaying()) {
Archify.routeProbe.pauseJourney({ preserveElapsed: true, reason: 'guide' });
}
returnFocus = document.activeElement;
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 = '';
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 === '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',
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()
};
}
};
})();
/* ============================================================
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>