<!DOCTYPE html>
<html lang="nb">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Når Silicon Valley kommer fram til Forsvarets datasentriske arkitektur (FDSA)</title>
<style>
:root{
--bg:#f7f5f0;--surface:#ffffff;--ink:#1c1c1c;--muted:#5a5a5a;--line:#d9d4c7;
--accent:#8a6d1f;--accent-soft:#f1e8cf;--ok:#2f6b3a;--mid:#8a6d1f;--weak:#8c3b2b;
--box:#ecf1f5;
}
@media (prefers-color-scheme: dark){
:root{--bg:#161616;--surface:#1f1f1f;--ink:#ececec;--muted:#b4b4b4;--line:#3a3a3a;
--accent:#d4b35a;--accent-soft:#2e2a1c;--ok:#7fc38a;--mid:#d4b35a;--weak:#e08a7a;--box:#22282d;}
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);font:17px/1.6 Georgia,"Times New Roman",serif}
.wrap{max-width:820px;margin:0 auto;padding:28px 22px 80px}
[hidden]{display:none!important}
.langbar{display:flex;justify-content:flex-end;gap:6px;margin-bottom:18px;font:600 13px/1 system-ui,sans-serif}
.lb{border:1px solid var(--line);background:var(--surface);color:var(--muted);padding:7px 12px;border-radius:3px;cursor:pointer}
.lb.on{border-color:var(--accent);color:var(--accent);background:var(--accent-soft)}
header{border-bottom:3px solid var(--accent);padding-bottom:22px;margin-bottom:34px}
.kicker{font:600 13px/1.3 system-ui,sans-serif;letter-spacing:.14em;text-transform:uppercase;color:var(--accent)}
h1{font-size:2rem;line-height:1.15;margin:.3em 0 .4em}
.lede{font-size:1.15rem;color:var(--muted);margin:0}
.byline{font:15px/1.5 system-ui,sans-serif;color:var(--ink);margin-top:16px}
.byline span{color:var(--muted)}
.meta{font:14px/1.5 system-ui,sans-serif;color:var(--muted);margin-top:10px}
h2{font:700 1.35rem/1.25 system-ui,sans-serif;margin:2.2em 0 .6em;border-left:4px solid var(--accent);padding-left:12px}
h3{font:700 1.05rem/1.3 system-ui,sans-serif;margin:1.6em 0 .4em}
p{margin:0 0 1em}
.summary{background:var(--accent-soft);border:1px solid var(--line);padding:18px 22px;margin:0 0 1.5em}
.summary h2{margin:0 0 .5em;border:none;padding:0;font-size:1.05rem}
.summary ol{margin:0;padding-left:1.2em}
.summary li{margin-bottom:.5em}
.aud{display:inline-block;font:600 12px/1 system-ui,sans-serif;letter-spacing:.08em;text-transform:uppercase;padding:5px 9px;border:1px solid var(--line);border-radius:3px;color:var(--muted);margin-bottom:.6em}
figure{margin:1.4em 0;background:var(--surface);border:1px solid var(--line);padding:14px}
figcaption{font:14px/1.5 system-ui,sans-serif;color:var(--muted);margin-top:8px}
table{width:100%;border-collapse:collapse;font:15px/1.45 system-ui,sans-serif;margin:1em 0}
th,td{border-bottom:1px solid var(--line);padding:9px 8px;vertical-align:top;text-align:left}
th{font-weight:700;background:var(--surface)}
th.a{background:var(--accent-soft);border-bottom:2px solid var(--accent)}
th.f{background:var(--box);border-bottom:2px solid var(--line)}
th small{display:block;font-weight:500;font-size:11px;color:var(--muted);letter-spacing:.04em;text-transform:uppercase;margin-bottom:2px}
.scroll{overflow-x:auto}
.tag{display:inline-block;font-weight:700;font-size:12px;letter-spacing:.04em;padding:2px 7px;border-radius:3px;white-space:nowrap}
.t-ok{background:var(--ok);color:#fff}.t-mid{background:var(--mid);color:#fff}.t-weak{background:var(--weak);color:#fff}
.t-none{background:var(--line);color:var(--ink)}
blockquote{margin:1.4em 0;padding:12px 18px;border-left:3px solid var(--accent);background:var(--surface);font-style:italic}
.box{background:var(--box);border:1px solid var(--line);padding:16px 20px;margin:1.2em 0}
.box h3{margin-top:0}
.box.note{background:var(--surface);border-left:3px solid var(--weak);font-size:15px}
.src{font:14px/1.6 system-ui,sans-serif;color:var(--muted)}
a{color:var(--accent)}
.svgwrap svg{width:100%;height:auto;display:block}
.svgwrap iframe.arch{display:block;width:100%;border:0}
.arch.fig2{aspect-ratio:301/195}.arch.fig3{aspect-ratio:301/166}
.arch-box{position:relative;padding-bottom:52px}
.arch-bar{display:none}
.arch-open{display:flex;align-items:flex-end;justify-content:center;position:absolute;inset:0;width:100%;padding:0 0 4px;border:0;background:none;cursor:pointer}
.arch-open span{font:600 14px/1 system-ui,sans-serif;color:#fff;background:var(--accent);padding:10px 16px;border-radius:20px;box-shadow:0 2px 8px rgba(0,0,0,.25)}
.arch-box.full{position:fixed;inset:0;padding:0;z-index:1000;background:var(--bg);display:flex;flex-direction:column}
.arch-box.full .arch-bar{display:flex;justify-content:flex-end;padding:6px 8px;border-bottom:1px solid var(--line);background:var(--surface)}
.arch-close{font:600 15px/1 system-ui,sans-serif;color:var(--ink);background:none;border:1px solid var(--line);border-radius:3px;padding:10px 14px;cursor:pointer}
.arch-box.full .arch{flex:1;height:auto;aspect-ratio:auto}
@media (max-width:600px){
.scroll thead{display:none}
.scroll table,.scroll tbody,.scroll tr,.scroll td{display:block}
.scroll tr{border:1px solid var(--line);background:var(--surface);padding:12px 14px;margin:0 0 12px}
.scroll td{border:0;padding:0}
.scroll td:nth-child(-n+2){display:inline;font-weight:700;font-size:16px}
.scroll td:nth-child(1){color:var(--accent)}.scroll td:nth-child(1)::after{content:". "}
.scroll td:nth-child(n+3){margin-top:10px}
.scroll td:nth-child(n+3)::before{display:block;font:600 11px/1.4 system-ui,sans-serif;letter-spacing:.04em;text-transform:uppercase;color:var(--muted);margin-bottom:2px}
[data-l="nb"] .scroll td:nth-child(3)::before{content:"Hva artikkelen beskriver"}
[data-l="nb"] .scroll td:nth-child(4)::before{content:"Artikkelens dekning av området"}
[data-l="nb"] .scroll td:nth-child(5)::before{content:"Hva det betyr for FDSA"}
[data-l="en"] .scroll td:nth-child(3)::before{content:"What the essay describes"}
[data-l="en"] .scroll td:nth-child(4)::before{content:"The essay\'s coverage of the area"}
[data-l="en"] .scroll td:nth-child(5)::before{content:"What it means for FDSA"}
}
.arch-box.full .arch-open{display:none}
body.arch-lock{overflow:hidden}
.fignum{font:600 12px/1 system-ui,sans-serif;letter-spacing:.1em;text-transform:uppercase;color:var(--accent);margin-bottom:8px}
.f1{display:grid;grid-template-columns:1fr 1fr;gap:10px 14px;font-family:system-ui,sans-serif;background:var(--bg);padding:12px;border-radius:3px}
.f1 .h{font:600 12px/1.3 system-ui,sans-serif;letter-spacing:.1em;text-transform:uppercase;color:var(--accent);padding:0 2px 2px}
.f1 .c{border:1px solid var(--line);background:var(--surface);padding:12px 14px;border-radius:3px}
.f1 .c.g{background:var(--accent-soft);border-color:var(--accent)}
.f1 .c.m{background:none;border:1.5px dashed var(--weak)}
.f1 .q{font-weight:700;font-size:15px;color:var(--ink);margin:0 0 4px}
.f1 .d{font-size:13px;color:var(--muted);line-height:1.4}
.f1 .f{font-weight:700;font-size:13px;color:var(--ink);margin:0 0 4px}
.f1 .r{font:600 11px/1.4 system-ui,sans-serif;letter-spacing:.03em;color:var(--accent);margin-top:6px}
.f1 .note{grid-column:1/-1;font-size:12px;color:var(--muted);padding:2px}
@media (max-width:600px){.f1{grid-template-columns:1fr}.f1 .h:nth-child(2){display:none}.f1 .c.g,.f1 .c.m{margin-bottom:8px}}
</style>
</head>
<body>
<div class="wrap">
<div class="langbar"><button class="lb on" data-set="nb" onclick="setLang('nb')">Norsk</button><button class="lb" data-set="en" onclick="setLang('en')">English</button></div>
<!-- ===================== NORSK ===================== -->
<div data-l="nb">
<header>
<div class="kicker">Fagnotat · oktober 2026</div>
<h1>Når Silicon Valley kommer fram til Forsvarets datasentriske arkitektur (FDSA)</h1>
<p class="lede">27. oktober publiserte Andreessen Horowitz en omfattende og lang artikkel om hvordan KI-agenter endrer hva en virksomhet er. Den er verd tiden: noe av det klareste jeg har lest om hvorfor data, mening og rettigheter må være på plass før KI gir verdi. Argumentet er, nesten punkt for punkt, det samme som ligger til grunn for <a href="https://www.forsvaret.no/om-forsvaret/digital-transformasjon-i-forsvaret" target="_blank" rel="noopener">Forsvarets datasentriske arkitektur (FDSA)</a>. FDSA er ett av målbildene som realiserer Forsvarets digitale reguleringsplan (DRP), og konkretiserer DRPs prinsipper for data og arkitektur. Her er hva artikkelen sier, hvor den treffer FDSA, og hva vi kan lære av den.</p>
<div class="byline">Av Inge André Sandvik <span>· seniorrådgiver i Forsvarsstaben Teknologi, Cyber og IKT (FST TCI), og deltaker i arbeidet med FDSA. Vurderingene er mine egne.</span></div>
<div class="meta">Kilde: Ben Horowitz og Michael McGuiness, «Databricks & the Future of the Firm», a16z.com, 27. oktober 2026. Lesetid for dette notatet: 8 minutter. Lesetid for originalen: 29 minutter.</div>
</header>
<div class="summary">
<h2>For den som har to minutter</h2>
<ol>
<li><strong>KI har ikke et intelligensproblem, men et kontekstproblem.</strong> Databricks' administrerende direktør Ali Ghodsi sier at selv den smarteste modellen er ubrukelig uten tilgang til virksomhetens data, mening, rettigheter og tilbakemelding. Det er nøyaktig FDSAs begrunnelse.</li>
<li><strong>Et «system of record for agenter» må svare på fire spørsmål:</strong> hva er sant, hva betyr det, hva er tillatt, og virket det. De tre første er FDSAs datalag, semantiske lag og sikkerhetslag. Det fjerde er neste skritt.</li>
<li><strong>Det suverene firmaet eier det det lærer.</strong> To banker som leier samme KI-modell skiller lag etter ett år: den som eier definisjonene, rettighetene og de vurderte sakene har bygget en bedre bank. Den andre starter på nytt hver gang modellen byttes. Dette er datasuverenitet, forklart av en amerikansk investor.</li>
</ol>
</div>
<div class="box note">
<h3>Et forbehold om Databricks</h3>
<p>Dette notatet handler om argumentet i artikkelen, ikke om produktet. Jeg har ingen tilknytning til eller preferanse for Databricks som leverandør, og Forsvaret har ikke tatt stilling til dem. FDSA er teknologinøytralt, og ethvert produkt måles mot de samme kravene: drift i Forsvarets egne datasentre, autonom drift ved brutt samband på tvers av core, fog og edge, åpne formater og nasjonal kryptokontroll. Databricks leveres i dag som en tjeneste hos de store skyleverandørene, og jeg kjenner ikke til en utgave som kan driftes frakoblet på den måten. Det er derfor tankegangen, ikke teknologien, som er interessant her.</p>
</div>
<h2>Hva artikkelen sier</h2>
<p>Artikkelen handler om Databricks, et selskap a16z har investert i siden 2013, og som nå omsetter for over sju milliarder dollar i året. Men forfatterne bruker selskapet som inngang til et større spørsmål: hva gjør folk på jobb hele dagen når KI-agenter kan utføre det meste av det rutinepregede arbeidet?</p>
<p>Svaret deres bygger på en observasjon som er lett å overse: <em>hva en ansatt kan gjøre, er begrenset av virksomhetens system for sannhet og beslutning, og det igjen er begrenset av databaseteknologien som ligger under.</em> Databricks' historie er fire teknologibølger som hver har løftet det taket: Spark gjorde maskinlæring raskt, lakehouse slo sammen datavarehus og datasjø, lakebase slo sammen operasjonelle og analytiske data på samme lagring, og nå kommer agentene. Tre år siden var hver interaksjon på plattformen menneskelig. I dag kommer flere spørringer fra agenter enn fra mennesker, og agenter starter over 17 millioner databaser daglig.</p>
<p>Den økonomiske rammen er Ronald Coases spørsmål fra 1937 om hvorfor firmaer finnes i det hele tatt, og Herbert Simons observasjon om at et delt register gjør desentralisert handling trygg. Billettselgeren i Boston trengte ikke ringe alle de andre billettselgerne før hun solgte siste sete til Chicago. Hun så i registeret, og når hun solgte, endret registeret seg for alle. Hierarki var det 20. århundrets informasjonsrutingssystem. Agenter tar over rutingen.</p>
<h2>De fire spørsmålene, og hvor FDSA svarer</h2>
<p>Kjernen i artikkelen er Ghodsis begrep «System of Record for Agents», et felles register som hele tiden må svare på fire spørsmål. Figur 1 viser hvordan de fire spørsmålene faller på FDSAs kapabilitetsområder og fundamenter, og hvilket initiativ i Forsvaret som realiserer svaret.</p>
<figure>
<div class="fignum">Figur 1 · De fire spørsmålene mot FDSA</div>
<div class="f1">
<div class="h">Databricks: system of record for agenter</div>
<div class="h">FDSA: område, fundament og initiativ</div>
<div class="c"><div class="q">1. Hva er sant?</div><div class="d">Lakehouse og Lakebase holder tilstanden</div></div>
<div class="c g"><div class="f">Dataprodukter · Plattform og infrastruktur</div><div class="d">Fundament 5 Data som produkt · 9.1 Data skilt fra applikasjon</div><div class="r">Realiseres av: Forsvarets distribuerte dataplattform (FDDP) · Forsvarets skyløsning (MilSky)</div></div>
<div class="c"><div class="q">2. Hva betyr det?</div><div class="d">Genie Ontology og OntoRank: en graf over hva firmaet vet</div></div>
<div class="c g"><div class="f">Dataforvaltning · Datakatalog</div><div class="d">Fundament 7.1 Begrepsapparat og ontologi · 7.3 Klargjøring for KI</div><div class="r">Realiseres av: FDDP · Sentral autoritet for informasjonsarkitektur</div></div>
<div class="c"><div class="q">3. Hva er tillatt?</div><div class="d">Unity Gateway: identitet, budsjett og sporing per agent</div></div>
<div class="c g"><div class="f">Datasentrisk sikkerhet · Føderert datastyring</div><div class="d">Fundament 2.2 Felles identitet · 2.4 ABAC · 3.1 Policy og PEP · 3.5 Sporbarhet</div><div class="r">Realiseres av: Identitets- og tilgangsstyring (ICAM / ForsvarsID) · FDDP (policy-as-code)</div></div>
<div class="c"><div class="q">4. Virket det?</div><div class="d">Egne prøver per agent, optimalisering mot firmaets «godt nok»</div></div>
<div class="c m"><div class="f">Ikke beskrevet i målbildet ennå</div><div class="d">Nærmest: Datatjenester (Analyse og KI) · 5.3 Brukerorientert utvikling</div><div class="r">Realiseres av: ikke plassert</div></div>
<div class="note">Tre av fire spørsmål har et direkte svar i FDSA. Det fjerde, evalueringssløyfen, er neste skritt.</div>
</div>
<figcaption>Mappingen i høyre kolonne er min tolkning, ikke noe artikkelen selv sier om FDSA. Initiativene er hentet fra FDSAs avgrensning (kapittel 4) der de finnes; identitets- og tilgangsstyring er ikke nevnt der, men er det initiativet som i praksis realiserer identitetsdelen av datasentrisk sikkerhet.</figcaption>
</figure>
<p>Det mest brukbare tallet i artikkelen hører til spørsmål 2. Databricks' egne tester viser at en agent som får svare med grunnlag i virksomhetens ontologi, går fra omtrent 50 prosent til 84,5 prosent riktige svar. Det er, så vidt jeg kjenner til, det tydeligste kommersielle argumentet for begrepsapparat og ontologi som noen har publisert. Den som lurer på hvorfor FDSA bruker så mye plass på semantikk, kan starte der.</p>
<p>Spørsmål 3 er det som binder sammen flest ting. «Hva er tillatt» er umulig å svare på uten en autoritativ identitet å knytte svaret til. Databricks løser det ved å gi hver agent en egen identitet i Unity Gateway. I Forsvaret er dette identitets- og tilgangsstyring (ICAM), og det er forutsetningen for at attributtbasert tilgang (ABAC) og maskinelt håndhevet regelverk (policy-as-code) i det hele tatt kan virke. Uten en felles identitet har datasentrisk sikkerhet ingen å spørre om hvem som ber om dataene.</p>
<p>Figur 2 setter de fire spørsmålene sammen til én dataflyt, slik den ville sett ut i Forsvaret. Les den fra venstre: ingen får lese før identitetsløsningen og policyhåndhevelsen har svart på hva som er tillatt, data får mening gjennom ontologien før de blir et svar, og den stiplede sløyfen tilbake fra Evaluering er det som gjenstår.</p>
<div class="fignum">Figur 2 · System of record for agenter, Forsvarets utgave</div>
</div>
<!-- ===================== ENGLISH ===================== -->
<div data-l="en" hidden>
<header>
<div class="kicker">Professional note · October 2026</div>
<h1>When Silicon Valley arrives at the Norwegian Armed Forces' Data-Centric Architecture (FDSA)</h1>
<p class="lede">On 27 October, Andreessen Horowitz published a comprehensive, long-form essay on how AI agents change what a firm is. It is worth the time: one of the clearest accounts I have read of why data, meaning and permissions must be in place before AI creates value. The argument is, almost point for point, the same one that underpins the <a href="https://www.forsvaret.no/om-forsvaret/digital-transformasjon-i-forsvaret" target="_blank" rel="noopener">Norwegian Armed Forces' Data-Centric Architecture (FDSA)</a>. FDSA is one of the target architectures that realizes the Norwegian Armed Forces' Digital Regulatory Plan (DRP), making the DRP's principles for data and architecture concrete. Here is what the essay says, where it meets FDSA, and what we can learn from it.</p>
<div class="byline">By Inge André Sandvik <span>· Senior Advisor, Norwegian Defence Staff, Technology, Cyber and ICT (FST TCI), and a contributor to the FDSA work. The views are my own.</span></div>
<div class="meta">Source: Ben Horowitz and Michael McGuiness, "Databricks & the Future of the Firm", a16z.com, 27 October 2026. Reading time for this note: 8 minutes. For the original: 29 minutes.</div>
</header>
<div class="summary">
<h2>If you have two minutes</h2>
<ol>
<li><strong>AI does not have an intelligence problem, it has a context problem.</strong> Databricks CEO Ali Ghodsi argues that even the smartest model is useless without access to the organisation's data, meaning, permissions and feedback. That is exactly FDSA's rationale.</li>
<li><strong>A "system of record for agents" must answer four questions:</strong> what is true, what does it mean, what is allowed, and did it work. The first three are FDSA's data layer, semantic layer and security layer. The fourth is the next step.</li>
<li><strong>The sovereign firm owns what it learns.</strong> Two banks renting the same AI model part ways after a year: the one that owns the definitions, the permissions and the evaluated cases has built a better bank. The other starts over every time the model is swapped. This is data sovereignty, explained by an American investor.</li>
</ol>
</div>
<div class="box note">
<h3>A caveat about Databricks</h3>
<p>This note is about the argument in the essay, not about the product. I have no affiliation with or preference for Databricks as a vendor, and the Norwegian Armed Forces have taken no position on them. FDSA is technology-neutral, and every product is measured against the same requirements: operation in the Armed Forces' own data centres, autonomous operation under broken connectivity across core, fog and edge, open formats, and national control of cryptography. Databricks is delivered today as a service on the major cloud providers, and I am not aware of an edition that can run disconnected in that way. It is the thinking, not the technology, that is of interest here.</p>
</div>
<h2>What the essay says</h2>
<p>The essay is about Databricks, a company a16z has backed since 2013, now past seven billion dollars in annual revenue. But the authors use the company as a way into a bigger question: what do people do at work all day once AI agents can handle most routine work?</p>
<p>Their answer rests on an observation that is easy to miss: <em>what an employee can do is constrained by the organisation's system of record and decision-making, and that in turn is constrained by the database technology underneath.</em> Databricks' story is four technology waves that each raised that ceiling: Spark made machine learning fast, the lakehouse merged the data warehouse and the data lake, the lakebase put operational and analytical data on the same storage, and now come the agents. Three years ago every interaction on the platform was human. Today more queries come from agents than from people, and agents launch over 17 million databases a day.</p>
<p>The economic frame is Ronald Coase's 1937 question of why firms exist at all, and Herbert Simon's observation that a shared record makes decentralised action safe. The ticket agent in Boston did not have to call every other ticket agent before selling the last seat to Chicago. She consulted the record, and when she sold, the record changed for everyone. Hierarchy was the 20th century's information-routing system. Agents are taking over the routing.</p>
<h2>The four questions, and where FDSA answers</h2>
<p>The core of the essay is Ghodsi's "System of Record for Agents", a shared record that must continuously answer four questions. Figure 1 shows how the four questions map onto FDSA's capability areas and foundations, and which Norwegian initiative realises each answer.</p>
<figure>
<div class="fignum">Figure 1 · The four questions against FDSA</div>
<div class="f1">
<div class="h">Databricks: system of record for agents</div>
<div class="h">FDSA: area, foundation and initiative</div>
<div class="c"><div class="q">1. What is true?</div><div class="d">Lakehouse and Lakebase hold the state</div></div>
<div class="c g"><div class="f">Data products · Platform and infrastructure</div><div class="d">Foundation 5 Data as a product · 9.1 Data separated from application</div><div class="r">Realised by: Norwegian Armed Forces Distributed Data Platform (FDDP) · Norwegian Armed Forces Cloud (MilSky)</div></div>
<div class="c"><div class="q">2. What does it mean?</div><div class="d">Genie Ontology and OntoRank: a graph of what the firm knows</div></div>
<div class="c g"><div class="f">Data management · Data catalogue</div><div class="d">Foundation 7.1 Vocabulary and ontology · 7.3 AI readiness</div><div class="r">Realised by: FDDP · Central authority for information architecture</div></div>
<div class="c"><div class="q">3. What is allowed?</div><div class="d">Unity Gateway: identity, budget and tracing per agent</div></div>
<div class="c g"><div class="f">Data-centric security · Federated data governance</div><div class="d">Foundation 2.2 Common identity · 2.4 ABAC · 3.1 Policy and PEP · 3.5 Traceability</div><div class="r">Realised by: Identity, credential and access management (ICAM / ForsvarsID) · FDDP (policy-as-code)</div></div>
<div class="c"><div class="q">4. Did it work?</div><div class="d">Per-agent tests, optimisation against the firm's "good enough"</div></div>
<div class="c m"><div class="f">Not yet described in the target architecture</div><div class="d">Closest: Data services (Analytics and AI) · 5.3 User-driven development</div><div class="r">Realised by: not assigned</div></div>
<div class="note">Three of the four questions have a direct answer in FDSA. The fourth, the evaluation loop, is the next step.</div>
</div>
<figcaption>The right-hand column is my interpretation, not something the essay itself says about FDSA. The initiatives come from FDSA's scoping section (chapter 4) where they exist; identity and access management is not listed there, but is in practice the initiative that realises the identity part of data-centric security.</figcaption>
</figure>
<p>The most useful number in the essay belongs to question 2. Databricks' own tests show that an agent grounded in the organisation's ontology goes from roughly 50 percent to 84.5 percent correct answers. As far as I know, it is the clearest commercial argument for vocabularies and ontologies anyone has published. Anyone wondering why FDSA spends so much space on semantics can start there.</p>
<p>Question 3 is the one that ties the most together. "What is allowed" cannot be answered without an authoritative identity to attach the answer to. Databricks solves it by giving every agent its own identity in Unity Gateway. In the Norwegian Armed Forces this is identity, credential and access management (ICAM), and it is the precondition for attribute-based access control (ABAC) and machine-enforced policy (policy-as-code) to work at all. Without a common identity, data-centric security has no one to ask who is requesting the data.</p>
<p>Figure 2 assembles the four questions into one data flow, as it would look in the Norwegian Armed Forces. Read it from the left: nobody gets to read before the identity service and the policy enforcement point have answered what is allowed, data gains meaning through the ontology before it becomes an answer, and the dashed loop back from Evaluation is what remains.</p>
<div class="fignum">Figure 2 · System of record for agents, Norwegian Armed Forces edition</div>
</div>
<!-- ===================== FIGUR 2 (felles, tekst byttes med JS) ===================== -->
<figure class="svgwrap" style="margin-top:0">
<div class="arch-box">
<div class="arch-bar"><button type="button" class="arch-close"><span data-l="nb">✕ Lukk</span><span data-l="en" hidden>✕ Close</span></button></div>
<iframe data-l="nb" class="arch fig2" title="Figur 2 · System of record for agenter, Forsvarets utgave" srcdoc="<!DOCTYPE html>
<html lang="nb" 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.2.0">
<meta name="archify-reader-contract" content="declared-wide-v1">
<title>System of record for agenter, Forsvarets utgave · 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: #94a3b8;
--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: #e2e7ec;
--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: #64748b;
--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. Compact automatic sequences fit at
their authored text size or larger. Long sequences and waterfall rows
favor reading width and page scroll; other diagrams keep height fitting.
Small automatic canvases retain a modest scale inside a usable shell.
Every strategy preserves the authored SVG/viewBox and semantic geometry. */
.container {
width: 100%;
max-width: var(--archify-reader-width, 1440px);
margin: 0 auto;
}
html[data-reader-area="true"] .diagram-container {
min-height: var(--archify-diagram-min-height);
display: flex;
align-items: center;
}
html[data-reader-area="true"] .diagram-container > svg { flex-shrink: 0; }
/* The original SVG legend can use the outer corner while the capped or fitted
diagram stays centered. The container still clips all outer paint. */
.diagram-container:has([data-reader-legend-corner]) > svg { overflow: visible; }
.diagram-container [data-reader-legend-corner] {
transform-origin: 0 0;
transform-box: view-box;
transform: var(--archify-reader-legend-transform);
}
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-entry="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.95rem;
height: 0.95rem;
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.95rem;
height: 0.95rem;
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"] {
position: relative;
border-radius: 0;
background: color-mix(in srgb, var(--panel-border) 38%, 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="reset"]::after,
.diagram-nav [data-view="in"]::after {
position: absolute;
top: 0.55rem;
bottom: 0.55rem;
left: 0;
width: 1px;
background: color-mix(in srgb, var(--panel-border) 75%, transparent);
content: "";
pointer-events: none;
}
.diagram-nav [data-view="out"]:hover,
.diagram-nav [data-view="reset"]:hover,
.diagram-nav [data-view="in"]:hover { background: color-mix(in srgb, var(--panel-border) 60%, 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(--panel-border) 38%, 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; }
.diagram-guide-credit {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.6rem;
padding: 0.46rem 0.9rem 0.56rem;
border-top: 1px solid color-mix(in srgb, var(--toolbar-border) 74%, transparent);
color: var(--text-faint);
font-size: 0.5rem;
letter-spacing: 0.02em;
text-decoration: none;
}
.diagram-guide-credit-link {
display: inline-flex;
align-items: center;
gap: 0.28rem;
flex: none;
opacity: 0.72;
transition: opacity 120ms ease, color 120ms ease;
}
.diagram-guide-star {
width: 0.62rem;
height: 0.62rem;
background: currentColor;
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 1.6l1.9 3.9 4.3.6-3.1 3 .7 4.3L8 11.4l-3.8 2 .7-4.3-3.1-3 4.3-.6z' fill='none' stroke='black' stroke-width='1.3' 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 16 16'%3E%3Cpath d='M8 1.6l1.9 3.9 4.3.6-3.1 3 .7 4.3L8 11.4l-3.8 2 .7-4.3-3.1-3 4.3-.6z' fill='none' stroke='black' stroke-width='1.3' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
}
.diagram-guide-credit:hover .diagram-guide-star,
.diagram-guide-credit:focus-visible .diagram-guide-star {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 1.6l1.9 3.9 4.3.6-3.1 3 .7 4.3L8 11.4l-3.8 2 .7-4.3-3.1-3 4.3-.6z' stroke='black' stroke-width='1.3' 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 16 16'%3E%3Cpath d='M8 1.6l1.9 3.9 4.3.6-3.1 3 .7 4.3L8 11.4l-3.8 2 .7-4.3-3.1-3 4.3-.6z' stroke='black' stroke-width='1.3' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.diagram-guide-credit:hover .diagram-guide-credit-link,
.diagram-guide-credit:focus-visible .diagram-guide-credit-link { opacity: 1; color: var(--cloud-stroke); }
.diagram-guide-credit:focus-visible { outline: 2px solid var(--cloud-stroke); outline-offset: -2px; }
@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.35rem;
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) 14%, 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;
scrollbar-color: color-mix(in srgb, var(--panel-border) 90%, transparent) transparent;
}
/* Slim overlay-style thumb instead of the chunky platform default. */
.node-finder-results::-webkit-scrollbar { width: 10px; }
.node-finder-results::-webkit-scrollbar-track { background: transparent; }
.node-finder-results::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--panel-border) 85%, transparent);
border: 3px solid transparent;
border-radius: 999px;
background-clip: padding-box;
}
.node-finder-results::-webkit-scrollbar-thumb:hover {
background: color-mix(in srgb, var(--text-muted) 65%, transparent);
border: 3px solid transparent;
background-clip: padding-box;
}
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: 3rem;
padding: 0.52rem 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;
}
/* A zero count is absence, not information — let it recede. */
.node-finder-result em[data-zero] { color: var(--text-faint); }
.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;
/* Card notes are authored prose and may carry an unbreakable run such as a
slash-joined pipeline or a long identifier. Without this, that run sets the
card's min-content width, spills past its track in every rail placement,
and pushes the whole page into horizontal overflow at desktop widths.
`anywhere` rather than `break-word` because only `anywhere` shrinks the
intrinsic minimum the layout is sized from. */
overflow-wrap: anywhere;
}
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 {
display: grid;
gap: 0.5rem;
padding: 0;
overflow: visible;
-webkit-mask-image: none;
mask-image: none;
}
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) { padding-bottom: 0.25rem; }
html[data-reader-rail="bottom"] .node-outline-group > ul {
display: flex;
flex-wrap: wrap;
gap: 0.25rem 0.5rem;
}
html[data-reader-rail="bottom"] .node-outline-group > ul::before { display: none; }
html[data-reader-rail="bottom"] .node-outline-group li {
flex: 0 1 auto;
min-width: min(100%, 12rem);
max-width: min(100%, 32rem);
}
html[data-reader-rail="bottom"] .node-outline-heading {
justify-content: flex-start;
gap: 0.5rem;
}
html[data-reader-rail="bottom"] .node-outline-heading > span { overflow-wrap: anywhere; }
.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); }
/* The desktop bottom rail is an index: compact rows keep related nodes
together while allowing full names and descriptions to wrap naturally. */
html[data-reader-rail="bottom"] .node-outline-item {
grid-template-columns: 0.625rem minmax(0, 1fr) 0.75rem;
align-items: first baseline;
column-gap: 0.375rem;
min-height: 0;
padding-block: 0.375rem;
}
html[data-reader-rail="bottom"] .node-outline-item strong,
html[data-reader-rail="bottom"] .node-outline-item small {
overflow: visible;
text-overflow: clip;
white-space: normal;
overflow-wrap: anywhere;
}
html[data-reader-rail="bottom"] .node-outline-item strong {
grid-column: 2;
grid-row: 1;
}
html[data-reader-rail="bottom"] .node-outline-item small {
grid-column: 2;
grid-row: 2;
text-align: left;
}
html[data-reader-rail="bottom"] .node-outline-item::after {
grid-column: 3;
grid-row: 1;
align-self: center;
}
.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;
}
/* The motion button breathes on the relay cycle, so the chrome keeps time
with the wave it controls. Motion Governor mirrors the planned cycle from
the diagram SVG onto the document. */
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden]) #btn-motion .motion-control-dot {
animation: archify-motion-dot-breathe var(--flow-cycle-chrome, 6.4s) ease-in-out infinite;
}
@keyframes archify-motion-dot-breathe {
0%, 100% { opacity: 1; transform: scale(1); }
46% { opacity: 0.55; transform: scale(0.72); }
}
.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) {
--ambient-node-base-filter: drop-shadow(0 1px 1px rgba(17, 24, 39, 0.05)) drop-shadow(0 6px 12px rgba(17, 24, 39, 0.06));
filter: var(--ambient-node-base-filter);
}
html[data-preset="classic"][data-theme="dark"] .diagram-container > svg [data-node-id] > rect:not(.c-mask) {
--ambient-node-base-filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.35));
filter: var(--ambient-node-base-filter);
}
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;
}
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; }
/* Live plays one relay per cycle: a comet (halo, tail, head) leaves each
source when the signal reaches it, travels at one shared speed, leaves a
soft wake, and lands as a ripple while its target glows. Downstream edges
depart only after their source is reached, so the reader sees the path the
diagram describes rather than every edge shimmering at once. Sequence
messages relay in authored chronological order. Motion Governor plans the
timeline and its per-edge keyframes; this stylesheet owns appearance.
Original line language and markers remain static. Node entry is bounded to
one pass per document. */
svg[data-animation="trace"] {
--flow-cycle: 6.4s;
--flow-halo-peak: 0.17;
--flow-glow-peak: 0.5;
--flow-tail-peak: 0.42;
--flow-head-peak: 0.95;
--flow-wake-peak: 0.3;
--flow-ripple-peak: 0.65;
}
.ambient-edge-flow {
fill: none;
stroke: var(--arrow-emphasis);
stroke-linecap: round;
pointer-events: none;
display: none;
}
.ambient-flow-wake {
--flow-peak: var(--flow-wake-peak, 0.3);
stroke-width: 3.2;
stroke-dasharray: var(--flow-dasharray, none);
}
.ambient-flow-halo {
--flow-peak: var(--flow-halo-peak, 0.17);
stroke-width: 6;
stroke-dasharray: var(--flow-dash, 12px) calc(var(--flow-len, 0px) - var(--flow-dash, 12px));
}
/* A wide blurred streak centered on the head: this is the bloom that makes
the comet read as a traveling orb of light rather than a bright dash. */
.ambient-flow-glow {
--flow-peak: var(--flow-glow-peak, 0.5);
stroke-width: 9;
stroke-dasharray: var(--flow-dash, 12px) calc(var(--flow-len, 0px) - var(--flow-dash, 12px));
filter: blur(2.5px);
}
.ambient-flow-tail {
--flow-peak: var(--flow-tail-peak, 0.42);
stroke-width: 2.4;
stroke-dasharray: var(--flow-dasharray, var(--flow-dash, 20px) calc(var(--flow-len, 0px) - var(--flow-dash, 20px)));
}
.ambient-flow-head {
--flow-peak: var(--flow-head-peak, 0.95);
stroke: color-mix(in srgb, var(--arrow-emphasis) 40%, white);
stroke-width: 2.6;
stroke-dasharray: var(--flow-dash, 6px) calc(var(--flow-len, 0px) - var(--flow-dash, 6px));
}
.ambient-flow-ripple {
--flow-peak: var(--flow-ripple-peak, 0.65);
stroke: color-mix(in srgb, var(--arrow-emphasis) 55%, white);
stroke-width: 2;
}
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden]) .ambient-edge-flow {
display: inline;
animation-name: var(--flow-name, none);
animation-duration: var(--flow-cycle, 6.4s);
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-delay: var(--flow-delay, 0s);
/* Without backwards fill a delayed layer would sit at its natural opacity
(fully lit) until its departure arrives. */
animation-fill-mode: backwards;
}
/* Stream packets trail the comet head as smaller, dimmer beads. */
.ambient-flow-head.packet {
--flow-peak: calc(var(--flow-head-peak, 0.95) * 0.62);
stroke-width: 3.4;
}
/* Timeline and waterfall playhead: a crisp rule over a soft bloom. */
.ambient-flow-playhead {
--flow-peak: 0.9;
stroke: color-mix(in srgb, var(--arrow-emphasis) 55%, white);
stroke-width: 1.6;
}
.ambient-flow-playhead-glow {
--flow-peak: 0.32;
stroke-width: 10;
filter: blur(4px);
}
/* Waterfall spans fill in proportion to their measured time, then settle. */
.ambient-flow-fill {
--flow-peak: 0.5;
fill: color-mix(in srgb, var(--flow-fill, var(--arrow-emphasis)) 55%, white);
stroke: none;
transform-box: fill-box;
transform-origin: left center;
}
.ambient-flow-fill-failed { fill: var(--security-stroke); --flow-peak: 0.5; }
html[data-theme="light"] .ambient-flow-playhead { stroke: color-mix(in srgb, var(--arrow-emphasis) 70%, black); }
html[data-theme="light"] .ambient-flow-fill { --flow-peak: 0.42; fill: color-mix(in srgb, var(--flow-fill, var(--arrow-emphasis)) 70%, black); }
svg[data-preset="editorial"] .ambient-flow-fill { --flow-peak: 0.2; }
svg[data-preset="editorial"] .ambient-flow-playhead-glow { --flow-peak: 0.14; }
/* Each decorative flow follows its owning edge through tree visibility. */
.ambient-edge-flow[data-tree-hidden] { display: none !important; animation: none !important; }
svg[data-preset="blueprint"] .ambient-edge-flow { stroke-linecap: square; }
svg[data-preset="editorial"] { --flow-halo-peak: 0.07; --flow-glow-peak: 0.28; --flow-tail-peak: 0.2; --flow-head-peak: 0.7; --flow-wake-peak: 0.14; --flow-ripple-peak: 0.3; }
/* On light canvases the relay reads by saturation instead of bloom: the head
and ripple mix toward ink instead of white, and every layer's peak rises so
the wave survives the bright background. */
html[data-theme="light"] svg[data-animation="trace"] {
--flow-halo-peak: 0.22;
--flow-glow-peak: 0.4;
--flow-tail-peak: 0.5;
--flow-wake-peak: 0.34;
}
html[data-theme="light"] .ambient-flow-head {
stroke: color-mix(in srgb, var(--arrow-emphasis) 55%, black);
stroke-width: 2.8;
}
html[data-theme="light"] .ambient-flow-ripple {
stroke: color-mix(in srgb, var(--arrow-emphasis) 65%, black);
}
html[data-ambient-entry="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 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
html[data-ambient-entry="running"] svg[data-preset="signal-flow"][data-animation="trace"] [data-animate="node"] {
animation: archify-node-pulse 3.1s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
html[data-ambient-entry="running"] svg[data-preset="blueprint"][data-animation="trace"] [data-animate="node"] {
animation: archify-blueprint-node-pulse 3.8s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
html[data-ambient-entry="running"] svg[data-preset="editorial"][data-animation="trace"] [data-animate="node"] {
animation: archify-editorial-node-pulse 3.5s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
/* Each node glows in its planned windows: when the relay reaches it, while a
walk token rests on it, as a message lands, or as the playhead passes.
During the bounded entrance all three animations run; the entrance pulse is
listed after the repeating glow and wins on filter until it completes. */
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden]) svg[data-animation="trace"] [data-animate="node"] {
animation: var(--flow-node-name, none) var(--flow-cycle, 6.4s) linear infinite;
}
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden])[data-ambient-entry="running"] svg[data-animation="trace"] [data-animate="node"] {
animation: var(--flow-node-name, none) var(--flow-cycle, 6.4s) linear infinite,
archify-node-pulse 3.6s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden])[data-ambient-entry="running"] svg[data-preset="signal-flow"][data-animation="trace"] [data-animate="node"] {
animation: var(--flow-node-name, none) var(--flow-cycle, 6.4s) linear infinite,
archify-node-pulse 3.1s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden])[data-ambient-entry="running"] svg[data-preset="blueprint"][data-animation="trace"] [data-animate="node"] {
animation: var(--flow-node-name, none) var(--flow-cycle, 6.4s) linear infinite,
archify-blueprint-node-pulse 3.8s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden])[data-ambient-entry="running"] svg[data-preset="editorial"][data-animation="trace"] [data-animate="node"] {
animation: var(--flow-node-name, none) var(--flow-cycle, 6.4s) linear infinite,
archify-editorial-node-pulse 3.5s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
@keyframes archify-node-enter {
from { opacity: 0; }
to { opacity: 1; }
}
@media print {
.ambient-edge-flow { display: none !important; animation: none !important; }
svg[data-animation="trace"] [data-animate="node"] { animation: none !important; }
}
@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) {
.ambient-edge-flow { display: none !important; animation: none !important; }
html[data-preset="signal-flow"] .diagram-container::before {
animation: none !important;
}
.pulse-dot { animation: none !important; }
.motion-control-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; }
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="Diagramhandlinger">
<div class="toolbar-group">
<button id="btn-theme" type="button"
title="Bytt tema (T)"
aria-label="Bytt fargetema"
aria-pressed="false">
<span id="theme-icon" class="toolbar-icon" aria-hidden="true"></span>
<span id="theme-label">Mørk</span>
</button>
<div class="preset-wrap">
<button id="btn-preset" type="button"
title="Velg visuell stil (S bytter)"
aria-label="Velg visuell stil"
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">Stil</span>
<span class="toolbar-icon toolbar-chevron" aria-hidden="true"></span>
</button>
<div class="preset-menu" id="preset-menu" role="menu" aria-label="Visuell stil">
<div class="preset-menu-heading" role="presentation"><span>Visuell identitet</span><span class="preset-menu-shortcut"><kbd>S</kbd>for å bytte</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>Klassisk</strong><small>Stabil teknisk standard</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>Signalflyt</strong><small>Presentasjon med vekt på bevegelse</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>Blåkopi</strong><small>Teknisk gjennomgang</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>Redaksjonell</strong><small>Publikasjoner og lanseringsnotater</small></span>
<span class="preset-option-check" aria-hidden="true"></span>
</button>
</div>
</div>
<button id="btn-motion" type="button" hidden
title="Sett bevegelse på pause"
aria-label="Sett bevegelse på pause"
aria-pressed="true">
<span class="motion-control-dot" aria-hidden="true"></span>
<span id="motion-label">Levende</span>
</button>
<button id="btn-present" type="button"
title="Presentasjonsmodus (F)"
aria-label="Gå til presentasjonsmodus"
aria-pressed="false">
<span id="present-icon" class="toolbar-icon" aria-hidden="true"></span>
<span id="present-label">Presenter</span>
</button>
</div>
<div class="export-wrap">
<button id="btn-export" type="button"
title="Eksporter diagram (E)"
aria-label="Eksporter diagram"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="export-menu">
Eksporter <span class="toolbar-icon toolbar-chevron" aria-hidden="true"></span>
</button>
<div class="export-menu" id="export-menu" role="menu" aria-label="Eksporter">
<div class="export-menu-header" role="presentation">
<div><strong>Eksporter diagram</strong><span>Portable, rene filer</span></div>
<kbd>E</kbd>
</div>
<div class="export-menu-section" role="group" aria-label="Del">
<span class="export-menu-heading" aria-hidden="true">Del</span>
<button data-action="route-share-card" type="button" role="menuitem" tabindex="-1" hidden disabled><span class="export-item-copy"><strong>Delingskort for rute</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>Delingskort for rekkevidde</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>Kopier diagram</strong><small class="hint">PNG til utklippstavle</small></span></button>
</div>
<div class="export-menu-section" role="group" aria-label="Rasterbilder">
<span class="export-menu-heading" aria-hidden="true">Bilde</span>
<button data-format="png" type="button" role="menuitem" tabindex="-1"><span class="export-item-copy"><strong>PNG</strong><small class="hint">Tapsfritt bilde</small></span></button>
<button data-format="jpeg" type="button" role="menuitem" tabindex="-1"><span class="export-item-copy"><strong>JPEG</strong><small class="hint">Kompakt bilde</small></span></button>
<button data-format="webp" type="button" role="menuitem" tabindex="-1"><span class="export-item-copy"><strong>WebP</strong><small class="hint">Moderne bilde</small></span></button>
</div>
<div class="export-menu-section" role="group" aria-label="Vektor og bevegelse">
<span class="export-menu-heading" aria-hidden="true">Vektor og bevegelse</span>
<button data-format="svg" type="button" role="menuitem" tabindex="-1"><span class="export-item-copy"><strong>SVG · Auto</strong><small class="hint">Følger vertssidens tema</small></span></button>
<button data-format="svg-light" type="button" role="menuitem" tabindex="-1"><span class="export-item-copy"><strong>SVG · Lys</strong><small class="hint">Alltid lys</small></span></button>
<button data-format="svg-dark" type="button" role="menuitem" tabindex="-1"><span class="export-item-copy"><strong>SVG · Mørk</strong><small class="hint">Alltid mørk</small></span></button>
<button data-format="webm" type="button" role="menuitem" tabindex="-1"><span class="export-item-copy"><strong>WebM</strong><small class="hint">6 s bevegelse</small></span></button>
</div>
</div>
</div>
</div>
<div class="container">
<!-- Header -->
<div class="header">
<div class="header-row"
data-preset-badge-signal-flow="SIGNALFLYT"
data-preset-badge-blueprint="BLÅKOPI / REV 01"
data-preset-badge-editorial="REDAKSJONELL / FELTNOTAT">
<div class="pulse-dot"></div>
<h1>System of record for agenter, Forsvarets utgave</h1>
</div>
</div>
<script id="archify-i18n-data" type="application/json">{"locale":"nb","messages":{"viewer.kind.frontend":"Frontend","viewer.kind.backend":"Backend","viewer.kind.database":"Database","viewer.kind.cloud":"Sky","viewer.kind.security":"Sikkerhet","viewer.kind.messagebus":"Meldingsbuss","viewer.kind.external":"Ekstern","viewer.kind.neutral":"Nøytral","viewer.kind.node":"Node","viewer.kind.start":"Start","viewer.kind.active":"Aktiv","viewer.kind.waiting":"Venter","viewer.kind.decision":"Beslutning","viewer.kind.success":"Suksess","viewer.kind.failure":"Feil","viewer.toolbar.actions":"Diagramhandlinger","viewer.theme.toggle.title":"Bytt tema (T)","viewer.theme.toggle":"Bytt fargetema","viewer.theme.dark":"Mørk","viewer.theme.light":"Lys","viewer.preset.choose.title":"Velg visuell stil (S bytter)","viewer.preset.choose":"Velg visuell stil","viewer.preset.style":"Stil","viewer.preset.menu":"Visuell stil","viewer.preset.identity":"Visuell identitet","viewer.preset.cycles":"for å bytte","viewer.preset.classic":"Klassisk","viewer.preset.classic.short":"Klassisk","viewer.preset.classic.hint":"Stabil teknisk standard","viewer.preset.flow":"Signalflyt","viewer.preset.flow.short":"Flyt","viewer.preset.flow.hint":"Presentasjon med vekt på bevegelse","viewer.preset.blueprint":"Blåkopi","viewer.preset.blueprint.hint":"Teknisk gjennomgang","viewer.preset.editorial":"Redaksjonell","viewer.preset.editorial.hint":"Publikasjoner og lanseringsnotater","viewer.preset.badge.signalFlow":"SIGNALFLYT","viewer.preset.badge.blueprint":"BLÅKOPI / REV 01","viewer.preset.badge.editorial":"REDAKSJONELL / FELTNOTAT","viewer.preset.badge.editorialPlate":"ARCHIFY / PLANSJE 04","viewer.preset.current":"Visuell stil: {style}. Velg visuell stil","viewer.motion.live":"Levende","viewer.motion.still":"Stille","viewer.motion.pause":"Sett bevegelse på pause","viewer.motion.resume":"Gjenoppta bevegelse","viewer.motion.reduced":"Bevegelse satt på pause av innstillingen for redusert bevegelse","viewer.motion.hidden":"Bevegelse satt på pause mens siden er skjult","viewer.motion.yielding":"Sett bevegelse på pause; viker nå for {owner}","viewer.motion.yielding.title":"Levende forhåndsvisning aktivert · viker for {owner}","viewer.owner.route":"Rutesonde","viewer.owner.lens":"Semantisk linse","viewer.owner.relationship":"Forhåndsvisning av relasjon","viewer.owner.intent":"Intensjonsspor","viewer.owner.focus":"semantisk fokus","viewer.owner.legend":"forhåndsvisning av tegnforklaring","viewer.owner.reader":"leserinteraksjon","viewer.present.enter":"Gå til presentasjonsmodus","viewer.present.enter.title":"Presentasjonsmodus (F)","viewer.present.exit":"Avslutt presentasjonsmodus","viewer.present.exit.title":"Avslutt presentasjonsmodus (F eller Escape)","viewer.present.present":"Presenter","viewer.present.exit.label":"Avslutt","viewer.export.button":"Eksporter","viewer.export.button.title":"Eksporter diagram (E)","viewer.export.diagram":"Eksporter diagram","viewer.export.menu":"Eksporter","viewer.export.subtitle":"Portable, rene filer","viewer.export.share":"Del","viewer.export.shareCard":"Delingskort","viewer.export.routeShareCard":"Delingskort for rute","viewer.export.reachShareCard":"Delingskort for rekkevidde","viewer.export.copyDiagram":"Kopier diagram","viewer.export.clipboardPng":"PNG til utklippstavle","viewer.export.raster":"Rasterbilder","viewer.export.image":"Bilde","viewer.export.lossless":"Tapsfritt bilde","viewer.export.compact":"Kompakt bilde","viewer.export.modern":"Moderne bilde","viewer.export.vectorMotion":"Vektor og bevegelse","viewer.export.vectorMotion.heading":"Vektor og bevegelse","viewer.export.editable":"Redigerbar vektor","viewer.export.svg.auto":"SVG · Auto","viewer.export.svg.auto.hint":"Følger vertssidens tema","viewer.export.svg.light":"SVG · Lys","viewer.export.svg.light.hint":"Alltid lys","viewer.export.svg.dark":"SVG · Mørk","viewer.export.svg.dark.hint":"Alltid mørk","viewer.export.motion6s":"6 s bevegelse","viewer.export.unsupported":"Støttes ikke av denne nettleseren","viewer.export.clipboardUnsupported":"Denne nettleseren støtter ikke skriving av bilder til utklippstavlen","viewer.export.clipboardUnsupported.short":"Skriving av bilder til utklippstavlen støttes ikke i denne nettleseren.","viewer.export.motionUnavailable":"Bevegelsesopptak er ikke tilgjengelig i denne nettleseren","viewer.export.webmUnavailable":"WebM er ikke tilgjengelig i denne nettleseren","viewer.export.failed":"Eksport mislyktes: {message}","viewer.export.unknownVariant":"Ukjent variant av delingskort: {variant}","viewer.export.routeRequired":"Spor en rute før du eksporterer delingskort for rute","viewer.export.reachRequired":"Spor definert rekkevidde før du eksporterer delingskort for rekkevidde","viewer.export.unknown":"ukjent","viewer.export.routeFailed":"Eksport av delingskort for rute mislyktes: {message}","viewer.export.reachFailed":"Eksport av delingskort for rekkevidde mislyktes: {message}","viewer.export.copyFailed":"Kopiering mislyktes: {message}","viewer.export.copiedPng":"PNG kopiert til utklippstavlen","viewer.export.downloadedRoute":"Delingskort for rute lastet ned","viewer.export.downloadedReach":"Delingskort for rekkevidde lastet ned","viewer.export.downloadedWebm":"WebM lastet ned","viewer.export.recording":"Tar opp 6 sekunder med bevegelse…","viewer.export.card.routeSummary.one":"Rute: {source} → {target} · {count} steg","viewer.export.card.routeSummary.other":"Rute: {source} → {target} · {count} steg","viewer.export.card.reachSummary":"Definert: {direction} fra {origin} · {nodes} · {links} · maks {hops}","viewer.export.card.node.one":"{count} node","viewer.export.card.node.other":"{count} noder","viewer.export.card.link.one":"{count} kobling","viewer.export.card.link.other":"{count} koblinger","viewer.export.card.hop.one":"{count} hopp","viewer.export.card.hop.other":"{count} hopp","viewer.export.card.routeBadge":"ARCHIFY · RUTE · {hops}","viewer.export.card.reachBadge":"ARCHIFY · {direction} REKKEVIDDE","viewer.export.direction.upstream":"Oppstrøms","viewer.export.direction.downstream":"Nedstrøms","viewer.export.error.canvasUnavailable":"Canvas er ikke tilgjengelig for {label}","viewer.export.error.contextUnavailable":"2D-canvaskontekst er ikke tilgjengelig for {label}","viewer.export.error.toBlobUnavailable":"canvas.toBlob er ikke tilgjengelig for {label}","viewer.export.error.toBlobNull":"canvas.toBlob returnerte ingen data for {label}","viewer.export.error.variantsCombined":"Varianter av delingskort kan ikke kombineres","viewer.export.error.viewerState":"Eksport av delingskort kunne ikke fjerne midlertidig visningstilstand","viewer.export.error.routeState":"Eksport av delingskort for rute kunne ikke bevare den beregnede ruten på en sikker måte","viewer.export.error.reachState":"Eksport av delingskort for rekkevidde kunne ikke bevare den definerte rekkevidden på en sikker måte","viewer.export.error.webmRequirements":"Eksport av WebM-bevegelse krever en sporanimasjon og nettleserstøtte for MediaRecorder","viewer.export.error.mediaRecorder":"MediaRecorder mislyktes","viewer.export.error.emptyWebm":"MediaRecorder ga en tom WebM-fil","viewer.export.error.webmBackground":"SVG-bakgrunnen kunne ikke lastes inn for WebM-eksport","viewer.guide.title":"Utforsk dette systemet","viewer.focus.selectedNodes":"{count} valgte noder","viewer.guide.eyebrow":"Diagramguide","viewer.guide.close":"Lukk diagramguiden","viewer.guide.inspecting":"Inspiserer kompilert semantikk","viewer.guide.actions":"Handlinger for utforsking av diagrammet","viewer.guide.find":"Finn hvilken som helst node","viewer.guide.find.hint":"Søk i etiketter, ansvar, typer og stabile ID-er.","viewer.guide.route":"Spor en rute","viewer.guide.route.aria":"Spor en rettet rute","viewer.guide.route.hint":"Finn ut hvordan to semantiske noder henger sammen i definert retning.","viewer.guide.map":"Se hele systemet","viewer.guide.map.hint":"Åpne semantisk radar med levende visningsområde og stabile noder.","viewer.guide.lens":"Sammenlign semantiske typer","viewer.guide.lens.hint":"Tell roller, vis trafikken deres og sammenlign direkte definerte koblinger.","viewer.guide.present":"Gå til presentasjonsmodus","viewer.guide.present.hint":"Gi det levende diagrammet hele visningsområdet uten å endre eksporten.","viewer.guide.shortcuts":"Flere hurtigtaster","viewer.guide.shortcut.export":"Eksporter","viewer.guide.shortcut.theme":"Tema","viewer.guide.shortcut.style":"Stil","viewer.guide.shortcut.reset":"Tilbakestill","viewer.guide.shortcut.zoomIn":"Zoom inn","viewer.guide.shortcut.zoomOut":"Zoom ut","viewer.guide.shortcut.close":"Lukk","viewer.guide.credit":"Synes du at Archify er nyttig? En stjerne på GitHub hjelper","viewer.guide.star":"Gi stjerne","viewer.guide.facts":"{nodes} · {relationships}","viewer.guide.fact.node.one":"{count} semantisk node","viewer.guide.fact.node.other":"{count} semantiske noder","viewer.guide.fact.relationship.one":"{count} relasjon","viewer.guide.fact.relationship.other":"{count} relasjoner","viewer.guide.open":"Åpne diagramguiden","viewer.finder.title":"Finn en node","viewer.finder.close":"Lukk nodesøket","viewer.finder.placeholder":"Søk etter etiketter eller ID-er","viewer.finder.search":"Søk i diagramnoder","viewer.finder.results":"Diagramnoder","viewer.finder.empty":"Ingen samsvarende noder","viewer.finder.result.focus":"Fokuser på {label}","viewer.finder.result.routeStart":"Velg {label} som rutestart","viewer.finder.result.routeTarget":"Velg {label} som rutemål, {links}","viewer.finder.status.empty":"Ingen samsvarende noder","viewer.finder.status.count.one":"{count} samsvarende node","viewer.finder.status.count.other":"{count} samsvarende noder","viewer.finder.noun.nodes":"noder","viewer.finder.link.one":"{count} kobling","viewer.finder.link.other":"{count} koblinger","viewer.finder.result.focus.one":"Fokuser på {label}, {count} relatert kobling","viewer.finder.result.focus.other":"Fokuser på {label}, {count} relaterte koblinger","viewer.finder.status.filtered":"{visible} av {available} {noun}","viewer.finder.status.all":"{available} {noun}","viewer.passport.eyebrow":"Semantisk pass","viewer.passport.metadata":"Nodemetadata","viewer.passport.evidence":"Verifisert kildebevis","viewer.passport.verified":"Verifisert kilde","viewer.passport.verificationScope":"Verifisert mot lokal Git ved den låste revisjonen. Tilgang til eksternt repositorium er ikke kontrollert.","viewer.passport.reach":"Definert rekkevidde","viewer.passport.reach.trace":"Spor definert rekkevidde","viewer.passport.upstream":"Oppstrøms","viewer.passport.downstream":"Nedstrøms","viewer.passport.upstream.trace":"Spor definert rekkevidde oppstrøms","viewer.passport.downstream.trace":"Spor definert rekkevidde nedstrøms","viewer.passport.close":"Lukk semantisk pass","viewer.passport.move":"Flytt semantisk pass. Dra, bruk piltastene eller trykk Home for å tilbakestille.","viewer.passport.copy":"Kopier lenke","viewer.passport.copy.focus":"Kopier lenke til noden i fokus","viewer.passport.relations":"Relasjoner","viewer.passport.relations.show":"Vis tilknyttede relasjoner","viewer.passport.relations.hide":"Skjul tilknyttede relasjoner","viewer.passport.relations.list":"Tilknyttede relasjoner","viewer.passport.copyRelation":"Kopier relasjon","viewer.passport.copyNode":"Kopier node","viewer.passport.copyPinned":"Kopier lenke til festet relasjon","viewer.passport.copySource":"Kopier lenke til kildenoden","viewer.passport.copy.focused.success":"Lenke til noden i fokus kopiert","viewer.passport.copy.pinned.success":"Lenke til festet relasjon kopiert","viewer.passport.copy.focused.failed":"Kunne ikke kopiere lenken til noden i fokus","viewer.passport.copy.pinned.failed":"Kunne ikke kopiere lenken til festet relasjon","viewer.passport.relationship.none":"Ingen tilknyttede relasjoner","viewer.passport.relationship.count.one":"{count} relasjon","viewer.passport.relationship.count.other":"{count} relasjoner","viewer.passport.relationship.show.one":"Vis {count} tilknyttet relasjon","viewer.passport.relationship.show.other":"Vis {count} tilknyttede relasjoner","viewer.passport.relationship.summary":"{out} utgående · {in} inngående{loops}","viewer.passport.relationship.loops":" · selvløkker: {count}","viewer.passport.relationship.explorer":"Utforsker for direkte relasjoner","viewer.passport.relationship.help":"Bruk piltastene for å utforske relasjoner. Trykk Enter eller mellomrom for å feste detaljer; Escape fjerner dem.","viewer.passport.relationship.loopsBack":"kobler til seg selv","viewer.passport.relationship.connectsTo":"kobler til","viewer.passport.relationship.connectsFrom":"kommer fra","viewer.passport.relationship.pinned":"Festet relasjon · {from} → {to} · {label}","viewer.passport.relationship.inspect":"Inspiser relasjon {index} av {total}: {from} til {to}, {label}. Trykk Enter for detaljer.","viewer.passport.relationship.group.out":"Utgående","viewer.passport.relationship.group.in":"Inngående","viewer.passport.relationship.group.loop":"Selvløkker","viewer.passport.relationship.row":"{group}: {relationship}, {neighbor}","viewer.passport.relationship.direction.out":"UT →","viewer.passport.relationship.direction.in":"← INN","viewer.passport.relationship.direction.loop":"LØKKE","viewer.passport.sourceCount.one":"{count} verifisert kildereferanse","viewer.passport.sourceCount.other":"{count} verifiserte kildereferanser","viewer.passport.repository.open":"Åpne verifisert revisjon {revision} av repositoriet","viewer.passport.source.open":"Åpne verifisert kilde {path} ved revisjon {revision}","viewer.passport.source.openLink":"Åpne ↗","viewer.passport.reach.upstream.one":"Spor {count} definert node oppstrøms","viewer.passport.reach.upstream.other":"Spor {count} definerte noder oppstrøms","viewer.passport.reach.downstream.one":"Spor {count} definert node nedstrøms","viewer.passport.reach.downstream.other":"Spor {count} definerte noder nedstrøms","viewer.passport.reach.noUpstream":"Ingen definerte noder oppstrøms","viewer.passport.reach.noDownstream":"Ingen definerte noder nedstrøms","viewer.passport.reach.status":"{direction} · {nodes} noder · {links} koblinger · maks {hops} hopp","viewer.route.eyebrow":"Rute","viewer.route.start":"Klikk der ruten starter","viewer.route.start.find":"Finn start","viewer.route.start.find.aria":"Finn en rutestart","viewer.route.copy":"Kopier lenke","viewer.route.copy.aria":"Kopier lenke til sporet rute","viewer.route.clear":"Fjern","viewer.route.clear.aria":"Fjern rutesonde","viewer.route.traced":"Rute","viewer.route.pickTwo":"Klikk på to noder i diagrammet","viewer.route.pickOne":"Klikk på en node i diagrammet","viewer.route.controls":"Kontroller for rutereisen","viewer.route.previous":"Forrige posisjon på ruten","viewer.route.play":"Spill av rutereisen","viewer.route.pause":"Sett rutereisen på pause","viewer.route.replay":"Spill av rutereisen på nytt","viewer.route.next":"Neste posisjon på ruten","viewer.route.journey":"Reise","viewer.route.pause.label":"Pause","viewer.route.replay.label":"Spill på nytt","viewer.route.overview":"Oversikt","viewer.route.overview.aria":"Vis full oversikt over ruten","viewer.route.instructions":"Klikk på et startpunkt og deretter et sluttpunkt. Ruter følger pilene.","viewer.route.destination":"Hvor ender ruten fra {label}?","viewer.route.destination.find":"Finn mål","viewer.route.destination.find.aria":"Finn et rutemål som kan nås","viewer.route.differentDestination":"Velg et annet mål","viewer.route.distinct":"Velg to forskjellige noder.","viewer.route.unreachable":"Kan ikke nå {label} herfra","viewer.route.unreachable.detail":"{target} kan ikke nås fra {source} langs pilene. Velg en uthevet node.","viewer.route.start.instructions":"Velg deretter sluttpunktet. Bare noder du kan nå, lyser opp.","viewer.route.copy.success":"Lenke til sporet rute kopiert","viewer.route.copy.failed":"Kunne ikke kopiere lenken til sporet rute","viewer.route.position":"Posisjon {index} av {total} på ruten: {label}","viewer.route.step":"Steg {index} av {total} · {phase} · {label}","viewer.route.motionRequired":"Automatisk reise krever bevegelsesmodusen Levende","viewer.route.trigger.clear":"Fjern sporet rute","viewer.route.overview.status":"{nodes} · {hops} · korteste rute","viewer.route.overview.node.one":"{count} node","viewer.route.overview.node.other":"{count} noder","viewer.route.overview.hop.one":"{count} steg","viewer.route.overview.hop.other":"{count} steg","viewer.route.phase.playing":"Spiller av","viewer.route.phase.complete":"Fullført","viewer.route.phase.inspecting":"Inspiserer","viewer.route.destination.count.one":"{count} node er uthevet og kan nås. Klikk på den.","viewer.route.destination.count.other":"{count} noder er uthevet og kan nås. Klikk på en av dem.","viewer.route.noOutgoing":"Ingen piler går ut fra denne noden. Fjern den og velg et annet startpunkt.","viewer.route.result.title":"{source} til {target}","viewer.route.finder.source.title":"Velg rutestart","viewer.route.finder.source.placeholder":"Søk etter rutekilder","viewer.route.finder.source.empty":"Ingen samsvarende rutekilder","viewer.route.finder.source.results":"Noder som kan starte en rute","viewer.route.finder.source.noun":"rutekilder","viewer.route.finder.source.badge":"start","viewer.route.finder.target.title":"Mål fra {label}","viewer.route.finder.target.placeholder":"Søk etter mål som kan nås","viewer.route.finder.target.empty":"Ingen samsvarende mål som kan nås","viewer.route.finder.target.results":"Rutemål som kan nås","viewer.route.finder.target.noun":"mål som kan nås","viewer.route.hop.one":"{count} hopp","viewer.route.hop.other":"{count} hopp","viewer.lens.eyebrow":"Semantisk linse","viewer.lens.title":"Sammenlign systemroller","viewer.lens.close":"Lukk semantisk linse","viewer.lens.instruction":"Velg opptil to semantiske typer. Én viser den faktiske trafikken; to sammenligner bare direkte definerte relasjoner.","viewer.lens.kinds":"Semantiske typer","viewer.lens.choose":"Velg en type for å inspisere nodene og de tilstøtende relasjonene.","viewer.lens.copy":"Kopier lenke til semantisk linse","viewer.lens.clear":"Fjern semantisk linse","viewer.lens.open":"Åpne semantisk linse","viewer.lens.openActive":"Åpne aktiv semantisk linse","viewer.lens.legend":"Semantisk tegnforklaring","viewer.lens.legend.inspect.one":"Inspiser {label}, {count} node","viewer.lens.legend.inspect.other":"Inspiser {label}, {count} noder","viewer.lens.kind.count.one":"{label}, {count} node","viewer.lens.kind.count.other":"{label}, {count} noder","viewer.lens.compare.one":"{first} → {second}: {forward} · {second} → {first}: {reverse} · {count} direkte relasjon","viewer.lens.compare.other":"{first} → {second}: {forward} · {second} → {first}: {reverse} · {count} direkte relasjoner","viewer.lens.single":"{nodes} · {relationships} · tilknyttede naboer forblir synlige","viewer.lens.node.one":"{count} {label}-node","viewer.lens.node.other":"{count} {label}-noder","viewer.lens.relationship.one":"{count} tilstøtende relasjon","viewer.lens.relationship.other":"{count} tilstøtende relasjoner","viewer.radar.title":"Semantisk radar","viewer.radar.building":"Bygger oversikt","viewer.radar.openFull":"Åpne full semantisk radar","viewer.radar.open":"Åpne radar","viewer.radar.close":"Lukk semantisk radar","viewer.radar.surface":"Diagramoversikt. Klikk på en node for å sette fokus på den, eller bruk piltastene for å panorere.","viewer.radar.click":"Klikk på node","viewer.radar.drag":"Dra for å panorere","viewer.radar.space":"Semantisk radar trenger mer kartplass.","viewer.radar.nodes":"Noder i semantisk diagramradar","viewer.radar.focus":"Fokuser på {label} fra semantisk radar","viewer.radar.status":"{count} noder · {viewport}","viewer.radar.fullMap":"{count} noder · fullt kart","viewer.radar.compacted":"Radaren er komprimert for å unngå å dekke det semantiske passet eller kartkontrollene.","viewer.radar.cancelWaiting":"Avbryt venting på mer kartplass for semantisk radar","viewer.radar.needsSpace":"Semantisk radar trenger mer synlig kartplass","viewer.radar.viewport.full":"fullt kart","viewer.radar.viewport.width":"{percent} % bredde","viewer.radar.viewport.scale":"{percent} % av visningsområdet","viewer.nav.controls":"Kontroller for diagramvisning","viewer.nav.route":"Spor en rettet rute","viewer.nav.route.title":"Spor rute (R)","viewer.nav.route.short":"RUTE","viewer.nav.radar":"Åpne semantisk radar","viewer.nav.radar.title":"Semantisk radar (M)","viewer.nav.radar.short":"KART","viewer.nav.lens":"Åpne semantisk linse","viewer.nav.lens.title":"Semantisk linse (L)","viewer.nav.lens.short":"LINSE","viewer.nav.find":"Finn en node","viewer.nav.find.title":"Finn en node (/)","viewer.outline.title":"Nodeindeks","viewer.rail.show":"Notater og indeks","viewer.rail.controls":"Sidepanel","viewer.rail.collapse":"Fold sammen sidepanelet","viewer.rail.bottom":"Flytt panelet under diagrammet","viewer.rail.right":"Flytt panelet ved siden av diagrammet","viewer.nav.guide":"Åpne diagramguiden","viewer.nav.guide.title":"Diagramguide (?)","viewer.nav.zoomOut":"Zoom ut","viewer.nav.zoomOut.title":"Zoom ut (-)","viewer.nav.reset":"Tilbakestill diagramvisningen","viewer.nav.reset.title":"Tilbakestill visning (0)","viewer.nav.read":"LES","viewer.nav.zoomIn":"Zoom inn","viewer.nav.zoomIn.title":"Zoom inn (+)","viewer.nav.camera":"{hint}. Tilbakestill diagramvisningen","viewer.nav.camera.title":"{semantic}{hint} · tilbakestill visning (0)","viewer.nav.camera.semantic":"Aktivt semantisk kamera · ","viewer.nav.level.map":"KART","viewer.nav.level.read":"LES","viewer.nav.level.full":"FULL","viewer.nav.level.auto":"AUTO","viewer.nav.detail.map":"Zoom inn for å vise relasjonsetiketter og nodekontekst","viewer.nav.detail.read":"Zoom inn igjen for å vise tagger og merknader","viewer.nav.detail.full":"Fullt detaljnivå i diagrammet","viewer.intent.summary":"{label}. {out} utgående, {in} inngående{loops}. {total} koblinger. Trykk Enter for detaljer.","viewer.intent.loops":", selvløkker: {count}","viewer.common.copied":"Kopiert","viewer.common.copyFailed":"Kopiering mislyktes","viewer.common.copyLink":"Kopier lenke","viewer.common.clear":"Fjern","viewer.common.close":"Lukk"}}</script>
<!-- Main Diagram -->
<div class="diagram-container" data-detail-level="read"
data-preset-badge-editorial-plate="ARCHIFY / PLANSJE 04">
<svg viewBox="0 0 1080 640" role="img" lang="nb" aria-labelledby="archify-diagram-title archify-diagram-description" data-animation="trace" data-preset="classic" data-quality-profile="showcase" data-motion-choreography="stream">
<title id="archify-diagram-title">System of record for agenter, Forsvarets utgave</title>
<desc id="archify-diagram-description">Et dataflytdiagram generert av 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)" />
<!-- Data Stages -->
<rect data-graph-role="structural-frame" data-composition-frame-kind="stage" data-composition-frame-id="0" x="16" y="46" width="168" height="520" rx="10" class="c-lane" stroke-width="1"/>
<text x="100" y="68" class="t-dim" font-size="9" font-weight="600" text-anchor="middle">01 / Aktører</text>
<rect data-graph-role="structural-frame" data-composition-frame-kind="stage" data-composition-frame-id="1" x="231" y="46" width="168" height="520" rx="10" class="c-lane" stroke-width="1"/>
<text x="315" y="68" class="t-dim" font-size="9" font-weight="600" text-anchor="middle">02 / Hva er tillatt</text>
<rect data-graph-role="structural-frame" data-composition-frame-kind="stage" data-composition-frame-id="2" x="446" y="46" width="168" height="520" rx="10" class="c-lane" stroke-width="1"/>
<text x="530" y="68" class="t-dim" font-size="9" font-weight="600" text-anchor="middle">03 / Hva er sant</text>
<rect data-graph-role="structural-frame" data-composition-frame-kind="stage" data-composition-frame-id="3" x="661" y="46" width="168" height="520" rx="10" class="c-lane" stroke-width="1"/>
<text x="745" y="68" class="t-dim" font-size="9" font-weight="600" text-anchor="middle">04 / Hva betyr det</text>
<rect data-graph-role="structural-frame" data-composition-frame-kind="stage" data-composition-frame-id="4" x="876" y="46" width="168" height="520" rx="10" class="c-lane" stroke-width="1"/>
<text x="960" y="68" class="t-dim" font-size="9" font-weight="600" text-anchor="middle">05 / Virket det</text>
<!-- Flow paths -->
<path data-edge-from="operator" data-edge-to="agent" data-edge-label="oppgave og mandat" data-edge-key="0" data-edge-id="task" data-composition-points="100,300;100,470" d="M 100 300 L 100 470" class="a-default" data-animate="edge" style="--step:0" stroke-width="1.4" marker-end="url(#arrowhead)"/>
<path data-edge-from="agent" data-edge-to="icam" data-edge-label="identitet" data-edge-key="1" data-edge-id="identity" data-composition-points="156,499;207.5,499;207.5,271;259,271" d="M 156 499 L 207.5 499 L 207.5 271 L 259 271" class="a-security" data-animate="edge" style="--step:1" stroke-width="1.4" marker-end="url(#arrowhead-security)"/>
<path data-edge-from="icam" data-edge-to="pep" data-edge-label="attributter" data-edge-key="2" data-edge-id="attributes" data-composition-points="315,300;315,470" d="M 315 300 L 315 470" class="a-security" data-animate="edge" style="--step:2" stroke-width="1.4" marker-end="url(#arrowhead-security)"/>
<path data-edge-from="pep" data-edge-to="fddp" data-edge-label="tillatt spørring" data-edge-key="3" data-edge-id="permitted" data-composition-points="371,499;422.5,499;422.5,385;474,385" d="M 371 499 L 422.5 499 L 422.5 385 L 474 385" class="a-security" data-animate="edge" style="--step:3" stroke-width="1.4" marker-end="url(#arrowhead-security)"/>
<path data-edge-from="fddp" data-edge-to="ontology" data-edge-label="data med begrep" data-edge-key="4" data-edge-id="meaning" data-composition-points="586,385;637.5,385;637.5,499;689,499" d="M 586 385 L 637.5 385 L 637.5 499 L 689 499" class="a-emphasis" data-animate="edge" style="--step:4" stroke-width="1.8" marker-end="url(#arrowhead-emphasis)"/>
<path data-edge-from="ontology" data-edge-to="answer" data-edge-label="grunnet svar" data-edge-key="5" data-edge-id="grounded" data-composition-points="801,499;904,499" d="M 801 499 L 904 499" class="a-emphasis" data-animate="edge" style="--step:5" stroke-width="1.8" marker-end="url(#arrowhead-emphasis)"/>
<path data-edge-from="fddp" data-edge-to="trace" data-edge-label="opphav" data-edge-key="6" data-edge-id="origin" data-composition-points="530,356;530,186" d="M 530 356 L 530 186" class="a-default" data-animate="edge" style="--step:6" stroke-width="1.4" marker-end="url(#arrowhead)"/>
<path data-edge-from="trace" data-edge-to="evaluation" data-edge-label="hva ga svaret" data-edge-key="7" data-edge-id="evidence" data-composition-points="586,157;960,157;960,242" d="M 586 157 L 960 157 L 960 242" class="a-default" data-animate="edge" style="--step:7" stroke-width="1.4" marker-end="url(#arrowhead)"/>
<path data-edge-from="answer" data-edge-to="evaluation" data-edge-label="korreksjon" data-edge-key="8" data-edge-id="correction" data-composition-points="960,470;960,300" d="M 960 470 L 960 300" class="a-dashed" data-animate="edge" style="--step:8" stroke-width="1.4" marker-end="url(#arrowhead-dashed)"/>
<path data-edge-from="evaluation" data-edge-to="ontology" data-edge-label="oppdatert mening" data-edge-key="9" data-edge-id="updated" data-composition-points="904,271;745,271;745,470" d="M 904 271 L 745 271 L 745 470" class="a-dashed" data-animate="edge" style="--step:9" stroke-width="1.4" marker-end="url(#arrowhead-dashed)"/>
<!-- Nodes -->
<g id="node-operator" data-node-id="operator" data-node-label="Operatør" tabindex="0" role="button" aria-label="Fokuser på Operatør, spør, gir mandat, 01 / Aktører" aria-pressed="false" data-node-kind="frontend" data-node-sublabel="spør, gir mandat" data-node-context="01 / Aktører">
<title>Operatør · spør, gir mandat · 01 / Aktører</title>
<rect x="44" y="242" width="112" height="58" rx="6" class="c-mask"/>
<rect x="44" y="242" width="112" height="58" rx="6" class="c-frontend" data-animate="node" style="--step:0" stroke-width="1.5"/>
<g aria-hidden="true" data-semantic-sigil="frontend" class="semantic-sigil s-frontend" transform="translate(50 248) 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="100" y="263" class="t-primary" font-size="10" font-weight="600" text-anchor="middle">Operatør</text>
<text data-detail="context" x="100" y="279" class="t-muted" font-size="7" text-anchor="middle">spør, gir mandat</text>
</g>
<g id="node-agent" data-node-id="agent" data-node-label="KI-agent" tabindex="0" role="button" aria-label="Fokuser på KI-agent, egen identitet, 01 / Aktører" aria-pressed="false" data-node-kind="backend" data-node-sublabel="egen identitet" data-node-tag="ny aktør" data-node-context="01 / Aktører">
<title>KI-agent · egen identitet · 01 / Aktører · ny aktør</title>
<rect x="44" y="470" width="112" height="58" rx="6" class="c-mask"/>
<rect x="44" y="470" width="112" height="58" rx="6" class="c-backend" data-animate="node" style="--step:1" stroke-width="1.5"/>
<g aria-hidden="true" data-semantic-sigil="backend" class="semantic-sigil s-backend" transform="translate(50 476) scale(0.6875)">
<path d="M6 3 3 8l3 5M10 3l3 5-3 5"/>
</g>
<text data-node-label="" data-detail-anchor="" x="100" y="491" class="t-primary" font-size="10" font-weight="600" text-anchor="middle">KI-agent</text>
<text data-detail="context" x="100" y="507" class="t-muted" font-size="7" text-anchor="middle">egen identitet</text>
<text data-detail="fine" x="100" y="517" class="t-backend" font-size="7" text-anchor="middle">ny aktør</text>
</g>
<g id="node-icam" data-node-id="icam" data-node-label="ICAM / ForsvarsID" tabindex="0" role="button" aria-label="Fokuser på ICAM / ForsvarsID, hvem spør, mandat, 02 / Hva er tillatt" aria-pressed="false" data-node-kind="security" data-node-sublabel="hvem spør, mandat" data-node-tag="2.2" data-node-context="02 / Hva er tillatt">
<title>ICAM / ForsvarsID · hvem spør, mandat · 02 / Hva er tillatt · 2.2</title>
<rect x="259" y="242" width="112" height="58" rx="6" class="c-mask"/>
<rect x="259" y="242" width="112" height="58" rx="6" class="c-security" data-animate="node" style="--step:2" stroke-width="1.5"/>
<g aria-hidden="true" data-semantic-sigil="security" class="semantic-sigil s-security" transform="translate(265 243) scale(0.375)">
<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="315" y="263" class="t-primary" font-size="10" font-weight="600" text-anchor="middle">ICAM / ForsvarsID</text>
<text data-detail="context" x="315" y="279" class="t-muted" font-size="7" text-anchor="middle">hvem spør, mandat</text>
<text data-detail="fine" x="315" y="289" class="t-security" font-size="7" text-anchor="middle">2.2</text>
</g>
<g id="node-pep" data-node-id="pep" data-node-label="PEP (policy)" tabindex="0" role="button" aria-label="Fokuser på PEP (policy), ABAC, policy-as-code, 02 / Hva er tillatt" aria-pressed="false" data-node-kind="security" data-node-sublabel="ABAC, policy-as-code" data-node-tag="2.4 og 3.1" data-node-context="02 / Hva er tillatt">
<title>PEP (policy) · ABAC, policy-as-code · 02 / Hva er tillatt · 2.4 og 3.1</title>
<rect x="259" y="470" width="112" height="58" rx="6" class="c-mask"/>
<rect x="259" y="470" width="112" height="58" 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(265 476) 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="315" y="491" class="t-primary" font-size="10" font-weight="600" text-anchor="middle">PEP (policy)</text>
<text data-detail="context" x="315" y="507" class="t-muted" font-size="7" text-anchor="middle">ABAC, policy-as-code</text>
<text data-detail="fine" x="315" y="517" class="t-security" font-size="7" text-anchor="middle">2.4 og 3.1</text>
</g>
<g id="node-fddp" data-node-id="fddp" data-node-label="FDDP" tabindex="0" role="button" aria-label="Fokuser på FDDP, dataprodukter, 03 / Hva er sant" aria-pressed="false" data-node-kind="database" data-node-sublabel="dataprodukter" data-node-tag="FDDP-node" data-node-context="03 / Hva er sant">
<title>FDDP · dataprodukter · 03 / Hva er sant · FDDP-node</title>
<rect x="474" y="356" width="112" height="58" rx="6" class="c-mask"/>
<rect x="474" y="356" width="112" height="58" 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(480 362) 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="530" y="377" class="t-primary" font-size="10" font-weight="600" text-anchor="middle">FDDP</text>
<text data-detail="context" x="530" y="393" class="t-muted" font-size="7" text-anchor="middle">dataprodukter</text>
<text data-detail="fine" x="530" y="403" class="t-database" font-size="7" text-anchor="middle">FDDP-node</text>
</g>
<g id="node-trace" data-node-id="trace" data-node-label="Sporbarhet" tabindex="0" role="button" aria-label="Fokuser på Sporbarhet, hva ga svaret, 03 / Hva er sant" aria-pressed="false" data-node-kind="database" data-node-sublabel="hva ga svaret" data-node-tag="3.5" data-node-context="03 / Hva er sant">
<title>Sporbarhet · hva ga svaret · 03 / Hva er sant · 3.5</title>
<rect x="474" y="128" width="112" height="58" rx="6" class="c-mask"/>
<rect x="474" y="128" width="112" height="58" rx="6" class="c-database" data-animate="node" style="--step:7" stroke-width="1.5"/>
<g aria-hidden="true" data-semantic-sigil="database" class="semantic-sigil s-database" transform="translate(480 134) 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="530" y="149" class="t-primary" font-size="10" font-weight="600" text-anchor="middle">Sporbarhet</text>
<text data-detail="context" x="530" y="165" class="t-muted" font-size="7" text-anchor="middle">hva ga svaret</text>
<text data-detail="fine" x="530" y="175" class="t-database" font-size="7" text-anchor="middle">3.5</text>
</g>
<g id="node-ontology" data-node-id="ontology" data-node-label="Ontologi" tabindex="0" role="button" aria-label="Fokuser på Ontologi, gir data mening, 04 / Hva betyr det" aria-pressed="false" data-node-kind="backend" data-node-sublabel="gir data mening" data-node-tag="7.1 og 7.3" data-node-context="04 / Hva betyr det">
<title>Ontologi · gir data mening · 04 / Hva betyr det · 7.1 og 7.3</title>
<rect x="689" y="470" width="112" height="58" rx="6" class="c-mask"/>
<rect x="689" y="470" width="112" height="58" rx="6" class="c-backend" data-animate="node" style="--step:5" stroke-width="1.5"/>
<g aria-hidden="true" data-semantic-sigil="backend" class="semantic-sigil s-backend" transform="translate(695 476) scale(0.6875)">
<path d="M6 3 3 8l3 5M10 3l3 5-3 5"/>
</g>
<text data-node-label="" data-detail-anchor="" x="745" y="491" class="t-primary" font-size="10" font-weight="600" text-anchor="middle">Ontologi</text>
<text data-detail="context" x="745" y="507" class="t-muted" font-size="7" text-anchor="middle">gir data mening</text>
<text data-detail="fine" x="745" y="517" class="t-backend" font-size="7" text-anchor="middle">7.1 og 7.3</text>
</g>
<g id="node-answer" data-node-id="answer" data-node-label="Svar og handling" tabindex="0" role="button" aria-label="Fokuser på Svar og handling, beslutning, bilde, 05 / Virket det" aria-pressed="false" data-node-kind="frontend" data-node-sublabel="beslutning, bilde" data-node-context="05 / Virket det">
<title>Svar og handling · beslutning, bilde · 05 / Virket det</title>
<rect x="904" y="470" width="112" height="58" rx="6" class="c-mask"/>
<rect x="904" y="470" width="112" height="58" rx="6" class="c-frontend" data-animate="node" style="--step:6" stroke-width="1.5"/>
<g aria-hidden="true" data-semantic-sigil="frontend" class="semantic-sigil s-frontend" transform="translate(910 476) 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="960" y="501" class="t-primary" font-size="10" font-weight="600" text-anchor="middle">Svar og handling</text>
<text data-detail="context" x="960" y="514.4" class="t-muted" font-size="7" text-anchor="middle">beslutning, bilde</text>
</g>
<g id="node-evaluation" data-node-id="evaluation" data-node-label="Evaluering" tabindex="0" role="button" aria-label="Fokuser på Evaluering, korreksjon tilbake, 05 / Virket det" aria-pressed="false" data-node-kind="backend" data-node-sublabel="korreksjon tilbake" data-node-tag="neste skritt" data-node-context="05 / Virket det">
<title>Evaluering · korreksjon tilbake · 05 / Virket det · neste skritt</title>
<rect x="904" y="242" width="112" height="58" rx="6" class="c-mask"/>
<rect x="904" y="242" width="112" height="58" rx="6" class="c-backend" data-animate="node" style="--step:8" stroke-width="1.5"/>
<g aria-hidden="true" data-semantic-sigil="backend" class="semantic-sigil s-backend" transform="translate(910 248) scale(0.6875)">
<path d="M6 3 3 8l3 5M10 3l3 5-3 5"/>
</g>
<text data-node-label="" data-detail-anchor="" x="960" y="263" class="t-primary" font-size="10" font-weight="600" text-anchor="middle">Evaluering</text>
<text data-detail="context" x="960" y="279" class="t-muted" font-size="7" text-anchor="middle">korreksjon tilbake</text>
<text data-detail="fine" x="960" y="289" class="t-backend" font-size="7" text-anchor="middle">neste skritt</text>
</g>
<!-- Flow labels -->
<g data-detail="context" data-edge-from="operator" data-edge-to="agent" data-edge-label="oppgave og mandat" data-edge-key="0" data-edge-id="task">
<rect x="52.35" y="304" width="95.3" height="16" rx="4" class="c-mask"/>
<text x="100" y="315" class="t-edge-default" font-size="8" text-anchor="middle">oppgave og mandat</text>
</g>
<g data-detail="context" data-edge-from="agent" data-edge-to="icam" data-edge-label="identitet" data-edge-key="1" data-edge-id="identity">
<rect x="179.45" y="364" width="56.1" height="16" rx="4" class="c-mask"/>
<text x="207.5" y="375" class="t-edge-security" font-size="8" text-anchor="middle">identitet</text>
</g>
<g data-detail="context" data-edge-from="icam" data-edge-to="pep" data-edge-label="attributter" data-edge-key="2" data-edge-id="attributes">
<rect x="282.05" y="304" width="65.9" height="16" rx="4" class="c-mask"/>
<text x="315" y="315" class="t-edge-security" font-size="8" text-anchor="middle">attributter</text>
</g>
<g data-detail="context" data-edge-from="pep" data-edge-to="fddp" data-edge-label="tillatt spørring" data-edge-key="3" data-edge-id="permitted">
<rect x="377.3" y="421" width="90.4" height="16" rx="4" class="c-mask"/>
<text x="422.5" y="432" class="t-edge-security" font-size="8" text-anchor="middle">tillatt spørring</text>
</g>
<g data-detail="context" data-edge-from="fddp" data-edge-to="ontology" data-edge-label="data med begrep" data-edge-key="4" data-edge-id="meaning">
<rect x="594.75" y="421" width="85.5" height="16" rx="4" class="c-mask"/>
<text x="637.5" y="432" class="t-edge-emphasis" font-size="8" text-anchor="middle">data med begrep</text>
</g>
<g data-detail="context" data-edge-from="ontology" data-edge-to="answer" data-edge-label="grunnet svar" data-edge-key="5" data-edge-id="grounded">
<rect x="817.1" y="475" width="70.8" height="16" rx="4" class="c-mask"/>
<text x="852.5" y="486" class="t-edge-emphasis" font-size="8" text-anchor="middle">grunnet svar</text>
</g>
<g data-detail="context" data-edge-from="fddp" data-edge-to="trace" data-edge-label="opphav" data-edge-key="6" data-edge-id="origin">
<rect x="509.3" y="335" width="41.4" height="16" rx="4" class="c-mask"/>
<text x="530" y="346" class="t-edge-default" font-size="8" text-anchor="middle">opphav</text>
</g>
<g data-detail="context" data-edge-from="trace" data-edge-to="evaluation" data-edge-label="hva ga svaret" data-edge-key="7" data-edge-id="evidence">
<rect x="922.15" y="178.5" width="75.7" height="16" rx="4" class="c-mask"/>
<text x="960" y="189.5" class="t-edge-default" font-size="8" text-anchor="middle">hva ga svaret</text>
</g>
<g data-detail="context" data-edge-from="answer" data-edge-to="evaluation" data-edge-label="korreksjon" data-edge-key="8" data-edge-id="correction">
<rect x="929.5" y="449" width="61" height="16" rx="4" class="c-mask"/>
<text x="960" y="460" class="t-edge-dashed" font-size="8" text-anchor="middle">korreksjon</text>
</g>
<g data-detail="context" data-edge-from="evaluation" data-edge-to="ontology" data-edge-label="oppdatert mening" data-edge-key="9" data-edge-id="updated">
<rect x="699.8" y="349.5" width="90.4" height="16" rx="4" class="c-mask"/>
<text x="745" y="360.5" class="t-edge-dashed" font-size="8" text-anchor="middle">oppdatert mening</text>
</g>
<!-- Legend -->
<g data-legend="" data-legend-bridge="">
<text x="40" y="584" class="t-primary" font-size="12" font-weight="650">Tegnforklaring</text>
<g data-legend-semantic-kind="emphasis" data-legend-x="40" data-legend-baseline="604" data-legend-width="99">
<path d="M 40 601 L 74 601" class="a-emphasis" stroke-width="1.8" marker-end="url(#arrowhead-emphasis)"/>
<text x="83" y="604" class="t-muted" font-size="10" font-weight="500">Hovedflyt</text>
</g>
<g data-legend-semantic-kind="security" data-legend-x="161" data-legend-baseline="604" data-legend-width="161">
<path d="M 161 601 L 195 601" class="a-security" stroke-width="1.4" marker-end="url(#arrowhead-security)"/>
<text x="204" y="604" class="t-muted" font-size="10" font-weight="500">Identitet og policy</text>
</g>
<g data-legend-semantic-kind="dashed" data-legend-x="344" data-legend-baseline="604" data-legend-width="124">
<path d="M 344 601 L 378 601" class="a-dashed" stroke-width="1.4" marker-end="url(#arrowhead-dashed)"/>
<text x="387" y="604" class="t-muted" font-size="10" font-weight="500">Læringssløyfe</text>
</g>
<g data-legend-semantic-kind="database" data-legend-kind="database" data-legend-label="Lager i FDDP" data-legend-x="490" data-legend-baseline="604" data-legend-width="118">
<rect x="490" y="596" width="14" height="9" rx="2" class="c-database" stroke-width="1"/>
<text x="512" y="604" class="t-muted" font-size="10" font-weight="500">Lager i FDDP</text>
</g>
<g data-legend-semantic-kind="default" data-legend-x="630" data-legend-baseline="604" data-legend-width="118">
<path d="M 630 601 L 664 601" class="a-default" stroke-width="1.4" marker-end="url(#arrowhead)"/>
<text x="673" y="604" class="t-muted" font-size="10" font-weight="500">Ordinær flyt</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">Diagramguide</span>
<strong class="diagram-guide-title" id="diagram-guide-title">Utforsk dette systemet</strong>
</span>
<button class="diagram-guide-close" id="diagram-guide-close" type="button" aria-label="Lukk diagramguiden">&#215;</button>
</div>
<p class="diagram-guide-stats" id="diagram-guide-stats">Inspiserer kompilert semantikk</p>
<div class="diagram-guide-actions" id="diagram-guide-actions" role="group" aria-label="Handlinger for utforsking av diagrammet">
<button class="diagram-guide-action" type="button" data-guide-action="find" aria-label="Finn hvilken som helst node">
<span class="diagram-guide-index" aria-hidden="true">01</span>
<span class="diagram-guide-action-copy"><strong>Finn hvilken som helst node</strong><small>Søk i etiketter, ansvar, typer og stabile ID-er.</small></span>
<kbd aria-hidden="true">/</kbd>
</button>
<button class="diagram-guide-action" type="button" data-guide-action="route" aria-label="Spor en rettet rute">
<span class="diagram-guide-index" aria-hidden="true">02</span>
<span class="diagram-guide-action-copy"><strong>Spor en rute</strong><small>Finn ut hvordan to semantiske noder henger sammen i definert retning.</small></span>
<kbd aria-hidden="true">R</kbd>
</button>
<button class="diagram-guide-action" type="button" data-guide-action="map" aria-label="Se hele systemet">
<span class="diagram-guide-index" aria-hidden="true">03</span>
<span class="diagram-guide-action-copy"><strong>Se hele systemet</strong><small>Åpne semantisk radar med levende visningsområde og stabile noder.</small></span>
<kbd aria-hidden="true">M</kbd>
</button>
<button class="diagram-guide-action" type="button" data-guide-action="lens" aria-label="Sammenlign semantiske typer">
<span class="diagram-guide-index" aria-hidden="true">04</span>
<span class="diagram-guide-action-copy"><strong>Sammenlign semantiske typer</strong><small>Tell roller, vis trafikken deres og sammenlign direkte definerte koblinger.</small></span>
<kbd aria-hidden="true">L</kbd>
</button>
<button class="diagram-guide-action" type="button" data-guide-action="present" aria-label="Gå til presentasjonsmodus">
<span class="diagram-guide-index" aria-hidden="true">05</span>
<span class="diagram-guide-action-copy"><strong>Gå til presentasjonsmodus</strong><small>Gi det levende diagrammet hele visningsområdet uten å endre eksporten.</small></span>
<kbd aria-hidden="true">F</kbd>
</button>
</div>
<div class="diagram-guide-shortcuts" aria-label="Flere hurtigtaster">
<span><kbd>E</kbd> Eksporter</span><span><kbd>T</kbd> Tema</span><span><kbd>S</kbd> Stil</span><span><kbd>0</kbd> Tilbakestill</span><span><kbd>+</kbd> Zoom inn</span><span><kbd>-</kbd> Zoom ut</span><span><kbd>Esc</kbd> Lukk</span>
</div>
<p class="diagram-guide-feedback" id="diagram-guide-feedback" aria-live="polite"></p>
<a class="diagram-guide-credit" href="https://github.com/tt-a1i/archify" target="_blank" rel="noopener noreferrer"><span>Synes du at Archify er nyttig? En stjerne på GitHub hjelper</span><span class="diagram-guide-credit-link"><span class="diagram-guide-star" aria-hidden="true"></span>Gi stjerne <span aria-hidden="true">&#8599;</span></span></a>
</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">Finn en node</strong>
<button class="node-finder-close" id="node-finder-close" type="button" aria-label="Lukk nodesøket">&#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="Søk etter etiketter eller ID-er" aria-label="Søk i diagramnoder" aria-describedby="node-finder-status">
<kbd aria-hidden="true">/</kbd>
</div>
<div class="node-finder-results" id="node-finder-results" role="group" aria-label="Diagramnoder"></div>
<p class="node-finder-empty" id="node-finder-empty" hidden>Ingen samsvarende noder</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="Flytt semantisk pass. Dra, bruk piltastene eller trykk Home for å tilbakestille." title="Flytt semantisk pass. Dra, bruk piltastene eller trykk Home for å tilbakestille."></button>
<div class="relationship-lens-copy">
<span class="relationship-lens-eyebrow">Semantisk pass</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="Nodemetadata">
<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="Verifisert kildebevis">
<div class="semantic-passport-evidence-head">
<span class="semantic-passport-evidence-status">Verifisert kilde</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">Definert rekkevidde</span>
<div class="semantic-passport-reach-actions" role="group" aria-label="Spor definert rekkevidde">
<button id="btn-reach-upstream" type="button" aria-label="Spor definert rekkevidde oppstrøms" aria-pressed="false">
<span>Oppstrøms</span><strong id="focus-reach-upstream-count">0</strong>
</button>
<button id="btn-reach-downstream" type="button" aria-label="Spor definert rekkevidde nedstrøms" aria-pressed="false">
<span>Nedstrøms</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="Lukk semantisk pass" title="Lukk">&#215;</button>
<button id="btn-focus-copy" type="button" aria-label="Kopier lenke til noden i fokus">Kopier lenke</button>
<button id="btn-focus-relations" type="button" aria-label="Vis tilknyttede relasjoner" aria-expanded="false" aria-controls="relationship-lens-list">Relasjoner</button>
</div>
</div>
<div class="relationship-lens-list" id="relationship-lens-list" aria-label="Tilknyttede relasjoner"></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">Rute</span>
<strong class="route-probe-title" id="route-probe-title">Klikk der ruten starter</strong>
</span>
<span class="route-probe-actions">
<button class="route-probe-find" id="route-probe-find" type="button" aria-label="Finn en rutestart" data-node-finder-trigger>Finn start</button>
<button class="route-probe-copy-link" id="route-probe-copy" type="button" aria-label="Kopier lenke til sporet rute" hidden>Kopier lenke</button>
<button id="route-probe-clear" type="button" aria-label="Fjern rutesonde">Fjern</button>
</span>
</div>
<div class="route-probe-path" id="route-probe-path" aria-label="Rute">
<span class="route-probe-placeholder">Klikk på to noder i diagrammet</span>
</div>
<div class="route-journey-controls" id="route-journey-controls" hidden role="group" aria-label="Kontroller for rutereisen" data-playing="false">
<button id="route-journey-prev" type="button" aria-label="Forrige posisjon på ruten" title="Forrige posisjon på ruten">&#8592;</button>
<button class="route-journey-play" id="route-journey-play" type="button" aria-label="Spill av rutereisen" aria-pressed="false" title="Spill av rutereisen">
<span id="route-journey-play-icon" aria-hidden="true">&#9654;</span>
<span id="route-journey-play-label">Reise</span>
</button>
<button id="route-journey-next" type="button" aria-label="Neste posisjon på ruten" title="Neste posisjon på ruten">&#8594;</button>
<button class="route-journey-overview" id="route-journey-overview" type="button" aria-label="Vis full oversikt over ruten" title="Vis full oversikt over ruten">Oversikt</button>
</div>
<small class="route-probe-status" id="route-probe-status" aria-live="polite">Klikk på et startpunkt og deretter et sluttpunkt. Ruter følger pilene.</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">Semantisk linse</span>
<strong class="semantic-lens-title" id="semantic-lens-title">Sammenlign systemroller</strong>
</span>
<button class="semantic-lens-close" id="semantic-lens-close" type="button" aria-label="Lukk semantisk linse">&#215;</button>
</div>
<p class="semantic-lens-instruction">Velg opptil to semantiske typer. Én viser den faktiske trafikken; to sammenligner bare direkte definerte relasjoner.</p>
<div class="semantic-lens-kinds" id="semantic-lens-kinds" role="group" aria-label="Semantiske typer"></div>
<p class="semantic-lens-status" id="semantic-lens-status" aria-live="polite">Velg en type for å inspisere nodene og de tilstøtende relasjonene.</p>
<div class="semantic-lens-actions">
<button id="semantic-lens-copy" type="button" aria-label="Kopier lenke til semantisk linse" hidden>Kopier lenke</button>
<button id="semantic-lens-clear" type="button" aria-label="Fjern semantisk linse">Fjern</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">Semantisk radar</strong>
<small id="overview-map-status" aria-live="polite">Bygger oversikt</small>
</span>
<button class="overview-map-expand" id="overview-map-expand" type="button" aria-label="Åpne full semantisk radar">Åpne radar</button>
<button class="overview-map-close" id="overview-map-close" type="button" aria-label="Lukk semantisk radar">&#215;</button>
</div>
<div class="overview-map-surface" id="overview-map-surface" tabindex="0" role="group" aria-label="Diagramoversikt. Klikk på en node for å sette fokus på den, eller bruk piltastene for å panorere."></div>
<div class="overview-map-hint" aria-hidden="true"><span>Klikk på node</span><span>Dra for å panorere</span></div>
</div>
<p class="overview-map-feedback no-print" id="overview-map-feedback" role="status" aria-live="polite" hidden>Semantisk radar trenger mer kartplass.</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>Notater og indeks</span></button>
<div class="diagram-nav no-print" role="toolbar" aria-label="Kontroller for diagramvisning">
<button id="btn-route-probe" type="button" aria-label="Spor en rettet rute" aria-pressed="false" aria-controls="route-probe" title="Spor rute (R)">RUTE</button>
<button id="btn-overview-map" type="button" aria-label="Åpne semantisk radar" aria-expanded="false" aria-controls="overview-map" title="Semantisk radar (M)">KART</button>
<button id="btn-semantic-lens" type="button" aria-label="Åpne semantisk linse" aria-haspopup="dialog" aria-expanded="false" aria-pressed="false" aria-controls="semantic-lens" title="Semantisk linse (L)">LINSE</button>
<button id="btn-node-finder" type="button" aria-label="Finn en node" aria-haspopup="dialog" aria-expanded="false" aria-controls="node-finder" title="Finn en node (/)" data-node-finder-trigger><span class="diagram-nav-icon find" aria-hidden="true"></span></button>
<button id="btn-diagram-guide" type="button" aria-label="Åpne diagramguiden" aria-haspopup="dialog" aria-expanded="false" aria-controls="diagram-guide" title="Diagramguide (?)"><span class="diagram-nav-icon guide" aria-hidden="true"></span></button>
<button data-view="out" type="button" aria-label="Zoom ut" title="Zoom ut (-)"><span class="diagram-nav-icon minus" aria-hidden="true"></span></button>
<button data-view="reset" type="button" aria-label="Tilbakestill diagramvisningen" title="Tilbakestill visning (0)"><span class="diagram-nav-detail" data-view-detail hidden>LES</span><span class="diagram-nav-percent" data-view-percent>100%</span></button>
<button data-view="in" type="button" aria-label="Zoom inn" title="Zoom inn (+)"><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="Sidepanel">
<button class="rail-control" id="rail-placement" type="button" hidden aria-label="Flytt panelet under diagrammet" title="Flytt panelet under diagrammet" 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="Fold sammen sidepanelet" title="Fold sammen sidepanelet"><span class="rail-control-icon collapse" aria-hidden="true"></span></button>
</div>
<!-- Info Cards -->
<div class="cards">
</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">Nodeindeks</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 readerLink(state) {
var template = document.documentElement.getAttribute('data-reading-link-template');
if (template === null) return location.href.replace(/#.*$/, '') + '#' + state;
if (!template || template.length > 4096 || template.split('{state}').length !== 2) return null;
try {
var url = new URL(template.replace('{state}', encodeURIComponent(state)));
if (['http:', 'https:', 'file:'].indexOf(url.protocol) === -1 || url.username || url.password) return null;
return url.href;
} catch (_) { return null; }
}
function copyReaderText(value) {
function fallbackCopy() {
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;
}
var policy = document.permissionsPolicy || document.featurePolicy;
var allowed = !policy || typeof policy.allowsFeature !== 'function' || policy.allowsFeature('clipboard-write');
if (allowed && navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
try { return Promise.resolve(navigator.clipboard.writeText(value)).then(function () { return true; }).catch(fallbackCopy); }
catch (_) {}
}
return Promise.resolve(fallbackCopy());
}
function hasDrawableGeometry(element) {
if (!element || element.hasAttribute('data-ambient-flow-overlay')) 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) {
if (geometry.hasAttribute('data-ambient-flow-overlay')) return false;
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.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return;
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; }
if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) 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) {
Array.prototype.forEach.call(clone.querySelectorAll('[data-ambient-flow-overlay]'), function (el) {
el.remove();
});
// Route interaction rewrites CSS declarations through CSSOM. Normalize
// authored motion delays on the clone so exported bytes do not depend
// on whether the reader traced a route before downloading.
Array.prototype.forEach.call(clone.querySelectorAll('[data-animate]'), function (el) {
var step = el.style.getPropertyValue('--step');
if (step) el.setAttribute('style', el.style.cssText);
});
// 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-reader-legend-corner]'), function (el) {
el.removeAttribute('data-reader-legend-corner');
el.style.removeProperty('--archify-reader-legend-transform');
});
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) 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');
});
// A downloaded tree is always the complete hierarchy: collapsing is a
// reading aid, never a silent change to what the artifact contains.
clone.removeAttribute('data-tree-any-collapsed');
Array.prototype.forEach.call(clone.querySelectorAll('[data-tree-hidden], [data-tree-collapsed]'), function (el) {
el.removeAttribute('data-tree-hidden');
el.removeAttribute('data-tree-collapsed');
});
Array.prototype.forEach.call(clone.querySelectorAll('[data-tree-toggle]'), function (el) {
el.setAttribute('aria-expanded', 'true');
el.setAttribute('aria-label', el.getAttribute('data-tree-label-collapse') || '');
});
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.hasAttribute('data-tree-any-collapsed') &&
!clone.style.getPropertyValue('transform') &&
!clone.style.getPropertyValue('clip-path') &&
clone.querySelectorAll('[data-ambient-flow-overlay], [data-reader-legend-corner], [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], [data-tree-hidden], [data-tree-collapsed]').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;
// Keep the export-owned activation inside its existing control boundary.
// The synthetic download click must not dismiss the reader selection.
menu.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);
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 geometry = element;
if (element.getAttribute('data-motion-path')) {
geometry = element.cloneNode(false);
geometry.setAttribute('d', element.getAttribute('data-motion-path'));
}
var length;
try { length = geometry.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, geometry.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 sourceUrl = null;
var backgroundImage = null;
var stream = null;
var recorder = null;
var raf = 0;
var durationTimer = 0;
var flushTimer = 0;
var settled = false;
function finish(error, blob) {
if (settled) return;
// 先锁定终态:停止录制和释放流可能产生晚到的编码事件。
settled = true;
cancelAnimationFrame(raf);
clearTimeout(durationTimer);
clearTimeout(flushTimer);
if (backgroundImage) {
backgroundImage.onload = null;
backgroundImage.onerror = null;
}
if (recorder) {
recorder.ondataavailable = null;
recorder.onerror = null;
recorder.onstop = null;
try { if (recorder.state !== 'inactive') recorder.stop(); } catch (_) {}
}
if (stream) stream.getTracks().forEach(function (track) { track.stop(); });
if (sourceUrl) URL.revokeObjectURL(sourceUrl);
if (blob) resolve(blob);
else reject(error);
}
try {
sourceUrl = URL.createObjectURL(new Blob([data.svgString], { type: 'image/svg+xml;charset=utf-8' }));
backgroundImage = new Image();
backgroundImage.onload = function () {
if (settled) return;
try {
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');
if (!ctx) throw exportError('viewer.export.error.contextUnavailable', { label: 'WebM' });
stream = canvas.captureStream(fps);
var mime = motionMimeType();
recorder = new MediaRecorder(stream, { mimeType: mime, videoBitsPerSecond: 6000000 });
var chunks = [];
var startedAt = performance.now();
function draw(now) {
if (settled) return;
try {
var elapsed = Math.max(0, ((Number(now) || performance.now()) - startedAt) / 1000);
drawMotionFrame(ctx, backgroundImage, motionScene, elapsed);
raf = requestAnimationFrame(draw);
} catch (error) { finish(error); }
}
recorder.ondataavailable = function (event) {
if (!settled && event.data && event.data.size) chunks.push(event.data);
};
recorder.onerror = function (event) {
finish(event.error || exportError('viewer.export.error.mediaRecorder'));
};
recorder.onstop = function () {
if (settled) return;
try {
var blob = new Blob(chunks, { type: recorder.mimeType || mime });
if (!blob.size) throw exportError('viewer.export.error.emptyWebm');
finish(null, blob);
} catch (error) { finish(error); }
};
drawMotionFrame(ctx, backgroundImage, motionScene, 0);
recorder.start(250);
if (settled) return;
startedAt = performance.now();
raf = requestAnimationFrame(draw);
durationTimer = setTimeout(function () {
if (settled || recorder.state === 'inactive') return;
// 保留最佳努力的编码器 flush;失败后仍通过 stop 收集最后的片段。
try { recorder.requestData(); } catch (_) {}
if (settled) return;
flushTimer = setTimeout(function () {
if (settled) return;
try { if (recorder.state !== 'inactive') recorder.stop(); }
catch (error) { finish(error); }
}, 120);
}, duration);
} catch (error) { finish(error); }
};
backgroundImage.onerror = function () {
finish(exportError('viewer.export.error.webmBackground'));
};
backgroundImage.src = sourceUrl;
} catch (error) { finish(error); }
});
}
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.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return;
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; }
if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) 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 entryStarted = false;
var entryPending = new Set();
var flowCount = 0;
// Runtime decoration carries geometry and tree visibility only. Keep each
// flow beside its authored edge so transforms, clipping and crossover
// paint order apply.
// Each renderer may declare a choreography that matches what its diagram
// means. Relay (the default) sends a comet from each source when the
// signal reaches it; stream adds a packet train for continuous data; walk
// moves one token along the main state path; sequence plays messages in
// authored order; sweep runs a playhead along a timeline; replay fills
// waterfall spans in proportion to their measured time. Motion Governor
// plans the timeline and per-element keyframes; viewer.css owns looks.
var SVG_NS = 'http://www.w3.org/2000/svg';
var choreography = svg ? (svg.getAttribute('data-motion-choreography') || 'relay') : 'relay';
var RELAY = {
speed: 280,
minTravel: 0.42,
maxTravel: 1.5,
handoff: 0.1,
dwell: 0.55,
maxSpan: 9,
land: 0.9,
rest: 1.4,
minCycle: 3.6
};
function stepOf(element, fallback) {
var step = parseFloat(element && element.style ? element.style.getPropertyValue('--step') : '');
return Number.isFinite(step) ? step : fallback;
}
function byStep(a, b) { return a.step - b.step || a.index - b.index; }
function addWindow(windows, id, start, end, strong) {
if (!id) return;
(windows[id] = windows[id] || []).push({ start: start, end: end, strong: !!strong });
}
function planRelay(records, windows) {
var arrive = Object.create(null);
var seeded = Object.create(null);
var targets = Object.create(null);
records.forEach(function (record) { if (record.from !== record.to) targets[record.to] = true; });
function seed(id) {
if (arrive[id] !== undefined) return;
arrive[id] = 0;
seeded[id] = true;
}
function relax() {
// Earliest arrival over a few dozen edges; a scan per settle keeps
// cycles and fan-in deterministic without a priority queue.
var settled = Object.create(null);
for (;;) {
var next = null;
Object.keys(arrive).forEach(function (id) {
if (!settled[id] && (next === null || arrive[id] < arrive[next])) next = id;
});
if (next === null) return;
settled[next] = true;
var depart = arrive[next] + (seeded[next] ? 0 : RELAY.handoff);
records.forEach(function (record) {
if (record.from !== next) return;
var at = depart + record.travel;
if (arrive[record.to] === undefined || at < arrive[record.to]) arrive[record.to] = at;
});
}
}
records.forEach(function (record) { if (!targets[record.from]) seed(record.from); });
relax();
for (;;) {
// A closed cycle has no entry; start it at its earliest authored edge.
var orphan = records.filter(function (record) { return arrive[record.from] === undefined; }).sort(byStep)[0];
if (!orphan) break;
seed(orphan.from);
relax();
}
records.forEach(function (record) {
record.depart = arrive[record.from] + (seeded[record.from] ? 0 : RELAY.handoff);
record.active = true;
});
Object.keys(arrive).forEach(function (id) { addWindow(windows, id, arrive[id], arrive[id]); });
}
function planWalk(records, windows) {
// A state machine is in one state at a time: follow the first authored
// transition to an unvisited state and let the current state stay lit.
var targets = Object.create(null);
records.forEach(function (record) { if (record.from !== record.to) targets[record.to] = true; });
var ordered = records.slice().sort(byStep);
var start = (ordered.find(function (record) { return !targets[record.from]; }) || ordered[0]).from;
var visited = Object.create(null);
var current = start;
var clock = 0;
var enteredAt = 0;
visited[start] = true;
for (var hops = 0; hops < 16; hops += 1) {
var next = ordered.find(function (record) { return record.from === current && !visited[record.to]; });
if (!next) break;
next.active = true;
next.depart = clock + RELAY.dwell;
addWindow(windows, current, enteredAt, next.depart, true);
clock = next.depart + next.travel;
enteredAt = clock;
current = next.to;
visited[current] = true;
}
addWindow(windows, current, enteredAt, enteredAt + 1.2, true);
}
function planSequence(records, windows) {
var clock = 0;
records.slice().sort(byStep).forEach(function (record) {
record.active = true;
record.depart = clock;
clock += record.travel + RELAY.handoff;
addWindow(windows, record.from, record.depart, record.depart);
addWindow(windows, record.to, record.depart + record.travel, record.depart + record.travel);
});
}
function scalePlan(records, windows, span) {
var scale = span > RELAY.maxSpan ? RELAY.maxSpan / span : 1;
if (scale === 1) return span;
records.forEach(function (record) {
if (!record.active) return;
record.depart *= scale;
record.travel *= scale;
});
Object.keys(windows).forEach(function (id) {
windows[id].forEach(function (window) { window.start *= scale; window.end *= scale; });
});
return span * scale;
}
function nodeShapes() {
var shapes = Object.create(null);
Array.prototype.forEach.call(svg.querySelectorAll('[data-animate="node"]'), function (shape) {
var wrapper = shape.closest('[data-node-id]');
var id = wrapper ? wrapper.getAttribute('data-node-id') : '';
if (id) (shapes[id] = shapes[id] || []).push(shape);
});
return shapes;
}
function sceneBounds(elements) {
var box = null;
elements.forEach(function (element) {
var b = null;
try { b = element.getBBox(); } catch (_) {}
if (!b || !(b.width || b.height)) return;
if (!box) box = { x: b.x, y: b.y, right: b.x + b.width, bottom: b.y + b.height };
else {
box.x = Math.min(box.x, b.x); box.y = Math.min(box.y, b.y);
box.right = Math.max(box.right, b.x + b.width); box.bottom = Math.max(box.bottom, b.y + b.height);
}
});
return box;
}
function overlay(tag, cls, attrs, parent, before) {
var element = document.createElementNS(SVG_NS, tag);
Object.keys(attrs).forEach(function (name) { element.setAttribute(name, attrs[name]); });
element.setAttribute('class', 'ambient-edge-flow ' + cls);
element.setAttribute('data-ambient-flow-overlay', 'true');
element.setAttribute('aria-hidden', 'true');
parent.insertBefore(element, before || null);
return element;
}
// Timeline and waterfall share a playhead: a lit vertical rule and its
// bloom travel the time axis linearly, because time is the subject.
function addPlayhead(box, x0, x1, lead, sweep, extras) {
var anchor = svg.querySelector('[data-node-id]');
var parent = anchor ? anchor.parentNode : svg;
var attrs = { x1: x0.toFixed(1), x2: x0.toFixed(1), y1: (box.y - 10).toFixed(1), y2: (box.bottom + 10).toFixed(1) };
['playhead-glow', 'playhead'].forEach(function (kind) {
var line = overlay('line', 'ambient-flow-' + kind, attrs, parent, anchor);
line.style.setProperty('--flow-name', 'archify-motion-playhead');
extras.push(line);
});
extras.playhead = { distance: x1 - x0, lead: lead, sweep: sweep };
}
function planSweep(windows, extras) {
var events = Array.prototype.filter.call(svg.querySelectorAll('[data-node-id][data-timeline-x]'), function (node) {
return Number.isFinite(parseFloat(node.getAttribute('data-timeline-x')));
});
if (!events.length) return 0;
var xs = events.map(function (node) { return parseFloat(node.getAttribute('data-timeline-x')); });
var x0 = Math.min.apply(null, xs), x1 = Math.max.apply(null, xs);
var lead = 0.35;
var sweep = Math.min(7, Math.max(3.2, events.length * 0.6));
events.forEach(function (node, index) {
var at = lead + (x1 > x0 ? (xs[index] - x0) / (x1 - x0) : 0) * sweep;
addWindow(windows, node.getAttribute('data-node-id'), at, at + 0.35, true);
});
var box = sceneBounds(Array.prototype.slice.call(svg.querySelectorAll('[data-timeline-lane]')));
if (box && x1 > x0) addPlayhead(box, x0, x1, lead, sweep, extras);
return lead + sweep + 0.6;
}
function planReplay(windows, extras) {
var spans = [];
Array.prototype.forEach.call(svg.querySelectorAll('[data-node-id][data-waterfall-start]'), function (node) {
var start = parseFloat(node.getAttribute('data-waterfall-start'));
var end = parseFloat(node.getAttribute('data-waterfall-end'));
var bar = node.querySelector('[data-animate="node"]');
if (!bar || !Number.isFinite(start) || !(end >= start)) return;
var width = parseFloat(bar.getAttribute('width'));
var x = parseFloat(bar.getAttribute('x'));
if (!(width > 0) || !Number.isFinite(x)) return;
spans.push({ node: node, bar: bar, start: start, end: end, x: x, width: width });
});
if (!spans.length) return 0;
var t0 = Math.min.apply(null, spans.map(function (span) { return span.start; }));
var t1 = Math.max.apply(null, spans.map(function (span) { return span.end; }));
if (!(t1 > t0)) return 0;
var ruler = spans.reduce(function (best, span) { return span.end - span.start > best.end - best.start ? span : best; });
var pxPerUnit = ruler.width / Math.max(ruler.end - ruler.start, 1e-9);
var x0 = ruler.x - (ruler.start - t0) * pxPerUnit;
var lead = 0.35;
var replay = Math.min(6, Math.max(3.4, spans.length * 0.45));
var at = function (t) { return lead + (t - t0) / (t1 - t0) * replay; };
var settle = lead + replay + 0.9;
spans.forEach(function (span) {
var fill = overlay('rect', 'ambient-flow-fill', {
x: span.bar.getAttribute('x'), y: span.bar.getAttribute('y'),
width: span.bar.getAttribute('width'), height: span.bar.getAttribute('height'),
rx: span.bar.getAttribute('rx') || '0'
}, span.bar.parentNode, span.bar.nextSibling);
var tone = /(?:^|\s)c-([a-z]+)(?:\s|$)/.exec(span.bar.getAttribute('class') || '');
if (tone) fill.style.setProperty('--flow-fill', 'var(--' + tone[1] + '-stroke)');
if (span.node.getAttribute('data-waterfall-status') && span.node.getAttribute('data-waterfall-status') !== 'ok') {
fill.classList.add('ambient-flow-fill-failed');
}
fill.timing = { start: at(span.start), end: Math.max(at(span.end), at(span.start) + 0.08), settle: settle };
extras.push(fill);
addWindow(windows, span.node.getAttribute('data-node-id'), fill.timing.end, fill.timing.end);
});
var box = sceneBounds(spans.map(function (span) { return span.bar; }));
if (box) addPlayhead(box, x0, x0 + (t1 - t0) * pxPerUnit, lead, replay, extras);
return settle;
}
function writeKeyframes(records, windows, extras, cycle) {
function at(seconds) {
return (Math.min(100, Math.max(0, seconds / cycle * 100))).toFixed(3) + '%';
}
var ease = 'animation-timing-function: cubic-bezier(0.45, 0.05, 0.35, 1);';
var rules = [
'@keyframes archify-relay-ripple {',
' 0% { r: 2px; opacity: var(--flow-peak, 0.6); }',
' ' + at(0.75) + ', 100% { r: 20px; opacity: 0; }',
'}'
];
records.forEach(function (record) {
if (!record.active) return;
var travel = record.travel;
rules.push(
'@keyframes archify-relay-comet-' + record.index + ' {',
' 0% { stroke-dashoffset: calc(var(--flow-len, 0px) + var(--flow-shift, 0px)); opacity: 0; ' + ease + ' }',
' ' + at(Math.min(0.12, travel * 0.2)) + ' { opacity: var(--flow-peak, 0.9); }',
' ' + at(travel) + ' { stroke-dashoffset: var(--flow-shift, 0px); opacity: var(--flow-peak, 0.9); }',
' ' + at(travel + 0.16) + ', 100% { stroke-dashoffset: var(--flow-shift, 0px); opacity: 0; }',
'}',
// A solid wake draws the travelled path behind the head on the same
// easing; a dashed wake only brightens its authored segments.
'@keyframes archify-relay-wake-' + record.index + ' {',
record.dashed
? ' 0%, ' + at(travel * 0.3) + ' { opacity: 0; }'
: ' 0% { stroke-dashoffset: var(--flow-len, 0px); opacity: var(--flow-peak, 0.3); ' + ease + ' }',
record.dashed
? ' ' + at(travel) + ' { opacity: var(--flow-peak, 0.3); }'
: ' ' + at(travel) + ' { stroke-dashoffset: 0px; opacity: var(--flow-peak, 0.3); }',
' ' + at(travel + 0.9) + ', 100% { stroke-dashoffset: 0px; opacity: 0; }',
'}'
);
});
var glow = function (strong) {
return 'filter: var(--ambient-node-base-filter,) drop-shadow(0 0 ' + (strong ? 14 : 10) + 'px var(--arrow-emphasis));';
};
var base = 'filter: var(--ambient-node-base-filter, none);';
Object.keys(windows).forEach(function (id, index) {
var merged = [];
windows[id].slice().sort(function (a, b) { return a.start - b.start; }).forEach(function (window) {
var peakEnd = Math.max(window.end, window.start + 0.42);
var last = merged[merged.length - 1];
if (last && window.start <= last.peakEnd + 0.78) {
last.peakEnd = Math.max(last.peakEnd, peakEnd);
last.strong = last.strong || window.strong;
} else merged.push({ start: window.start, peakEnd: peakEnd, strong: window.strong });
});
var frames = [' 0%, 100% { ' + base + ' }'];
merged.forEach(function (window) {
frames.push(
' ' + at(window.start) + ' { ' + base + ' }',
' ' + at(window.start + 0.12) + ', ' + at(window.peakEnd) + ' { ' + glow(window.strong) + ' }',
' ' + at(window.peakEnd + 0.78) + ' { ' + base + ' }'
);
});
rules.push('@keyframes archify-relay-node-' + index + ' {', frames.join('\n'), '}');
windows[id].keyframes = 'archify-relay-node-' + index;
});
if (extras.playhead) {
var p = extras.playhead;
rules.push(
'@keyframes archify-motion-playhead {',
' 0% { transform: translateX(0px); opacity: 0; }',
' ' + at(Math.max(0.05, p.lead - 0.2)) + ' { transform: translateX(0px); opacity: 0; }',
' ' + at(p.lead) + ' { transform: translateX(0px); opacity: var(--flow-peak, 0.9); }',
' ' + at(p.lead + p.sweep) + ' { transform: translateX(' + p.distance.toFixed(1) + 'px); opacity: var(--flow-peak, 0.9); }',
' ' + at(p.lead + p.sweep + 0.45) + ', 100% { transform: translateX(' + p.distance.toFixed(1) + 'px); opacity: 0; }',
'}'
);
}
extras.forEach(function (element, index) {
if (!element.timing) return;
var t = element.timing;
rules.push(
'@keyframes archify-replay-fill-' + index + ' {',
' 0%, ' + at(t.start) + ' { transform: scaleX(0); opacity: var(--flow-peak, 0.4); }',
' ' + at(t.end) + ', ' + at(t.settle) + ' { transform: scaleX(1); opacity: var(--flow-peak, 0.4); }',
' ' + at(t.settle + 0.7) + ', 100% { transform: scaleX(1); opacity: 0; }',
'}'
);
element.style.setProperty('--flow-name', 'archify-replay-fill-' + index);
});
var style = document.createElement('style');
style.setAttribute('data-ambient-flow-keyframes', 'true');
style.textContent = rules.join('\n');
document.head.appendChild(style);
}
function createFlows() {
var records = [];
Array.prototype.forEach.call(svg.querySelectorAll('path[data-animate="edge"]'), function (shape, index) {
var d = shape.getAttribute('data-motion-path') || shape.getAttribute('d');
if (!d) return;
var anchor = shape;
// Tree Branches initializes after these overlays and includes their
// inherited ancestors in its existing collapse/expand lifecycle.
function tagOverlay(flow) {
flow.setAttribute('data-ambient-flow-overlay', 'true');
flow.setAttribute('aria-hidden', 'true');
if (shape.hasAttribute('data-tree-ancestors')) {
flow.setAttribute('data-tree-ancestors', shape.getAttribute('data-tree-ancestors'));
}
}
function addPath(cls) {
var flow = document.createElementNS(SVG_NS, 'path');
flow.setAttribute('d', d);
['transform', 'mask', 'clip-path', 'vector-effect'].forEach(function (name) {
if (shape.hasAttribute(name)) flow.setAttribute(name, shape.getAttribute(name));
});
flow.setAttribute('class', 'ambient-edge-flow ambient-flow-' + cls);
tagOverlay(flow);
shape.parentNode.insertBefore(flow, anchor.nextSibling);
anchor = flow;
return flow;
}
var wake = addPath('wake');
var len = 0;
try { len = wake.getTotalLength(); } catch (_) {}
if (!(len > 0)) {
wake.remove();
return;
}
var record = {
index: records.length,
len: len,
from: shape.getAttribute('data-edge-from') || shape.getAttribute('data-composition-edge-from') || '',
to: shape.getAttribute('data-edge-to') || shape.getAttribute('data-composition-edge-to') || '',
step: stepOf(shape, index),
travel: Math.min(RELAY.maxTravel, Math.max(RELAY.minTravel, len / RELAY.speed)),
depart: 0,
active: false,
layers: []
};
records.push(record);
// Dashed authored lines keep their dash language through the relay:
// the wake brightens only the authored segments and never fills the
// gaps back into a solid line.
var authoredDash = '';
try { authoredDash = window.getComputedStyle(shape).strokeDasharray; } catch (_) {}
record.dashed = !!(authoredDash && authoredDash !== 'none');
wake.style.setProperty('--flow-dasharray', record.dashed ? authoredDash : len.toFixed(1) + ' ' + len.toFixed(1));
// Absolute caps keep the comet a slim streak on long edges instead
// of growing into a bright slab; the tail disintegrates into a
// sparkle of fragments behind the head, and --flow-shift trails
// each layer behind the head tip by its own length.
var headLen = Math.min(14, Math.max(7, len * 0.03));
var haloLen = Math.min(22, Math.max(12, len * 0.07));
var trail = len >= 120
? { pattern: '2.5 2 4 3 5.5 4.5 8 ' + (len - 30).toFixed(1), length: 30 }
: (len >= 60
? { pattern: '3 3 7 ' + (len - 13).toFixed(1), length: 13 }
: { pattern: '5 ' + Math.max(4, len - 5).toFixed(1), length: 5 });
record.layers.push(
{ flow: wake, kind: 'wake', dash: len, shift: 0 },
{ flow: addPath('glow'), kind: 'comet', dash: haloLen, shift: (haloLen - headLen) / 2 },
{ flow: addPath('halo'), kind: 'comet', dash: haloLen, shift: (haloLen - headLen) / 2 },
{ flow: addPath('tail'), kind: 'comet', dash: trail.length, shift: trail.length - headLen, dasharray: trail.pattern },
{ flow: addPath('head'), kind: 'comet', dash: headLen, shift: 0 }
);
if (choreography === 'stream') {
// A packet train behind the comet reads as a continuous stream.
[1, 2, 3].forEach(function (order) {
record.layers.push({ flow: addPath('head packet'), kind: 'comet', dash: Math.max(4, headLen * 0.55), shift: 0, packet: order });
});
}
// The ripple lands where the comet does; keep it out of the edge's
// mask so it can bloom over the node boundary.
var end = null;
try { end = wake.getPointAtLength(len); } catch (_) {}
if (end) {
var ripple = document.createElementNS(SVG_NS, 'circle');
ripple.setAttribute('cx', end.x.toFixed(1));
ripple.setAttribute('cy', end.y.toFixed(1));
ripple.setAttribute('r', '2');
if (shape.hasAttribute('transform')) ripple.setAttribute('transform', shape.getAttribute('transform'));
ripple.setAttribute('class', 'ambient-edge-flow ambient-flow-ripple');
tagOverlay(ripple);
shape.parentNode.insertBefore(ripple, anchor.nextSibling);
record.ripple = ripple;
}
});
var windows = Object.create(null);
var extras = [];
var span = 0;
if (choreography === 'sweep') span = planSweep(windows, extras);
else if (choreography === 'replay') span = planReplay(windows, extras);
else if (records.length) {
var linked = records.filter(function (record) { return record.from && record.to; });
if (choreography === 'sequence' || linked.length < records.length) planSequence(records, windows);
else if (choreography === 'walk') planWalk(records, windows);
else planRelay(records, windows);
span = records.reduce(function (max, record) {
return record.active ? Math.max(max, record.depart + record.travel) : max;
}, 0);
span = scalePlan(records, windows, span);
}
records.forEach(function (record) {
if (record.active) return;
record.layers.forEach(function (layer) { layer.flow.remove(); });
if (record.ripple) record.ripple.remove();
});
var active = records.filter(function (record) { return record.active; });
if (!active.length && !extras.length) return;
var cycle = Math.max(RELAY.minCycle, span + RELAY.land + RELAY.rest);
writeKeyframes(active, windows, extras, cycle);
svg.style.setProperty('--flow-cycle', cycle.toFixed(3) + 's');
html.style.setProperty('--flow-cycle-chrome', cycle.toFixed(3) + 's');
active.forEach(function (record) {
var gap = Math.min(0.22, record.travel * 0.28);
record.layers.forEach(function (layer) {
var flow = layer.flow;
flow.style.setProperty('--flow-len', record.len.toFixed(1) + 'px');
flow.style.setProperty('--flow-delay', (record.depart + (layer.packet || 0) * gap).toFixed(3) + 's');
flow.style.setProperty('--flow-dash', layer.dash.toFixed(1) + 'px');
flow.style.setProperty('--flow-shift', layer.shift.toFixed(1) + 'px');
flow.style.setProperty('--flow-name', 'archify-relay-' + layer.kind + '-' + record.index);
if (layer.dasharray) flow.style.setProperty('--flow-dasharray', layer.dasharray);
});
if (record.ripple) {
record.ripple.style.setProperty('--flow-delay', (record.depart + record.travel).toFixed(3) + 's');
record.ripple.style.setProperty('--flow-name', 'archify-relay-ripple');
}
});
var shapes = nodeShapes();
Object.keys(windows).forEach(function (id) {
(shapes[id] || []).forEach(function (shape) {
shape.style.setProperty('--flow-node-name', windows[id].keyframes);
});
});
flowCount = active.length + extras.length;
}
function settleEntry() {
entryPending.clear();
svg.removeEventListener('animationend', onEntryBoundary, true);
svg.removeEventListener('animationcancel', onEntryBoundary, true);
html.setAttribute('data-ambient-entry', 'settled');
}
function onEntryBoundary(event) {
if (!entryPending.has(event.target)) return;
// The entrance settles only when each node's pulse finishes; the
// shorter fade-rise enter animation may end first without ending it.
if (event.animationName && !/node-pulse/.test(event.animationName)) return;
entryPending.delete(event.target);
if (!entryPending.size) settleEntry();
}
function renderAmbient(suppressed) {
html.setAttribute('data-ambient-motion', flowCount ? (suppressed ? 'paused' : 'running') : 'empty');
if (suppressed) {
entryStarted = true;
settleEntry();
} else if (!entryStarted) {
entryStarted = true;
entryPending = new Set(Array.prototype.slice.call(svg.querySelectorAll('[data-animate="node"]')));
if (!entryPending.size) settleEntry();
else {
svg.addEventListener('animationend', onEntryBoundary, true);
svg.addEventListener('animationcancel', onEntryBoundary, true);
html.setAttribute('data-ambient-entry', 'running');
}
}
}
function readStored() {
try { return localStorage.getItem(STORAGE_KEY); } catch (_) { return null; }
}
function writeStored() {
try {
if (readerPaused) localStorage.removeItem(STORAGE_KEY);
else localStorage.setItem(STORAGE_KEY, 'live');
} 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');
renderAmbient(paused || owner || html.hasAttribute('data-embed') || html.hasAttribute('data-share-playback') || html.hasAttribute('data-document-hidden'));
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-entry');
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 ''; }
};
}
createFlows();
html.setAttribute('data-motion-capable', 'true');
btn.hidden = false;
readerPaused = readStored() !== 'live';
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 (typeof MutationObserver !== 'undefined') {
var contextObserver = new MutationObserver(render);
contextObserver.observe(html, { attributes: true, attributeFilter: ['data-embed', 'data-share-playback'] });
}
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;
if (element) {
try {
var parsed = JSON.parse(element.textContent || 'null');
if (parsed && parsed.verified === true && parsed.repository && parsed.nodes) payload = parsed;
} catch (_) {}
}
// Keep verified evidence discoverable to screen readers without adding
// visual marks. Restore first so repeated initialization never appends
// another count or retains one after evidence becomes unavailable.
var svg = document.querySelector('.diagram-container svg');
if (svg) Array.prototype.forEach.call(svg.querySelectorAll('[data-node-id]'), function (node) {
if (node.hasAttribute('data-source-evidence-count')) {
var savedLabel = node.getAttribute('data-source-evidence-original-label');
if (savedLabel == null) node.removeAttribute('aria-label');
else node.setAttribute('aria-label', savedLabel);
node.removeAttribute('data-source-evidence-count');
node.removeAttribute('data-source-evidence-original-label');
}
var sources = payload && payload.nodes[node.getAttribute('data-node-id')];
if (!Array.isArray(sources) || !sources.length) return;
var originalLabel = node.getAttribute('aria-label');
node.setAttribute('data-source-evidence-count', String(sources.length));
if (originalLabel != null) node.setAttribute('data-source-evidence-original-label', originalLabel);
node.setAttribute('aria-label', (originalLabel ? originalLabel + ', ' : '') +
viewerCount('viewer.passport.sourceCount', sources.length));
});
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() : [];
}
};
})();
/* ============================================================
Tree Branches — expand/collapse for hierarchy diagrams. The renderer
lays out the complete tree once and tags every node, link, and toggle
with its ancestors; collapsing only hides descendants, so visible nodes
never move and the reader keeps their place. Selecting a hidden node
through Finder, Outline, or a shared link expands its ancestors. Export
always writes the complete tree (see Export Cleanup).
============================================================ */
Archify.treeBranches = (function () {
var svg = document.querySelector('.diagram-container > svg[data-tree-ui]');
if (!svg) return { active: false };
var collapsed = Object.create(null);
var toggles = Object.create(null);
Array.prototype.forEach.call(svg.querySelectorAll('[data-tree-toggle]'), function (toggle) {
toggles[toggle.getAttribute('data-tree-toggle')] = toggle;
});
var tagged = svg.querySelectorAll('[data-tree-ancestors]');
var expandAll = svg.querySelector('[data-tree-expand-all]');
function ancestors(el) {
return (el.getAttribute('data-tree-ancestors') || '').split(' ').filter(Boolean);
}
function apply() {
Array.prototype.forEach.call(tagged, function (el) {
var hidden = ancestors(el).some(function (id) { return collapsed[id]; });
if (hidden) el.setAttribute('data-tree-hidden', '');
else el.removeAttribute('data-tree-hidden');
});
var any = false;
Object.keys(toggles).forEach(function (id) {
var toggle = toggles[id];
var isCollapsed = Boolean(collapsed[id]);
any = any || isCollapsed;
toggle.setAttribute('aria-expanded', isCollapsed ? 'false' : 'true');
toggle.setAttribute('aria-label', toggle.getAttribute(isCollapsed ? 'data-tree-label-expand' : 'data-tree-label-collapse') || '');
var node = svg.querySelector('[data-node-id="' + id + '"]');
if (node) {
if (isCollapsed) node.setAttribute('data-tree-collapsed', '');
else node.removeAttribute('data-tree-collapsed');
}
});
if (any) svg.setAttribute('data-tree-any-collapsed', '');
else svg.removeAttribute('data-tree-any-collapsed');
}
function setCollapsed(id, value) {
if (!toggles[id]) return false;
if (value) collapsed[id] = true;
else delete collapsed[id];
apply();
return true;
}
function toggle(id) { return setCollapsed(id, !collapsed[id]); }
// Reveal a node by expanding every collapsed ancestor; nothing else moves.
function reveal(id) {
var node = svg.querySelector('[data-node-id="' + id + '"]');
if (!node) return false;
var changed = false;
ancestors(node).forEach(function (ancestor) {
if (collapsed[ancestor]) { delete collapsed[ancestor]; changed = true; }
});
if (changed) apply();
return changed;
}
function restoreAll() {
collapsed = Object.create(null);
apply();
}
function activate(event) {
var target = event.target.closest('[data-tree-toggle], [data-tree-expand-all]');
if (!target || !svg.contains(target)) return false;
event.preventDefault();
event.stopPropagation();
if (target.hasAttribute('data-tree-expand-all')) {
restoreAll();
var root = svg.querySelector('[data-tree-toggle]:not([data-tree-ancestors])');
if (root) root.focus();
} else {
toggle(target.getAttribute('data-tree-toggle'));
target.focus();
}
return true;
}
// Capture phase so the canvas focus handler never treats a toggle press
// as a background click that clears the current focus.
svg.addEventListener('click', activate, true);
svg.addEventListener('keydown', function (event) {
if (event.key === 'Enter' || event.key === ' ') { activate(event); return; }
var node = event.target.closest('[data-node-id]');
if (!node || !svg.contains(node)) return;
var id = node.getAttribute('data-node-id');
// Arrow keys follow the tree convention: left collapses, right expands.
if (event.key === 'ArrowLeft' && toggles[id] && !collapsed[id]) { event.preventDefault(); setCollapsed(id, true); }
else if (event.key === 'ArrowRight' && collapsed[id]) { event.preventDefault(); setCollapsed(id, false); }
}, true);
// Focus reveals ancestors synchronously before positioning its lens.
// The observer also covers paths that change pressed state directly.
new MutationObserver(function (records) {
records.forEach(function (record) {
var el = record.target;
if (el.hasAttribute && el.hasAttribute('data-tree-hidden') && el.getAttribute('aria-pressed') === 'true') {
reveal(el.getAttribute('data-node-id'));
}
});
}).observe(svg, { subtree: true, attributes: true, attributeFilter: ['aria-pressed'] });
Object.keys(toggles).forEach(function (id) {
if (toggles[id].hasAttribute('data-tree-initially-collapsed')) collapsed[id] = true;
});
apply();
return {
active: true,
toggle: toggle,
collapse: function (id) { return setCollapsed(id, true); },
expand: function (id) { return setCollapsed(id, false); },
reveal: reveal,
restoreAll: restoreAll,
collapsedIds: function () { return Object.keys(collapsed); },
expandAllControl: expandAll
};
})();
/* ============================================================
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:not([data-ambient-flow-overlay]), 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);
if (shape.getAttribute('data-motion-path')) clone.setAttribute('d', relationshipTokenPath(shape));
clone.removeAttribute('data-motion-path');
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('data-motion-path') || 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;
// Hash restoration and internal selections use this path too. Reveal
// tree ancestors before measuring the selected node for its lens.
if (Archify.treeBranches && Archify.treeBranches.active) {
normalized.forEach(function (id) { Archify.treeBranches.reveal(id); });
}
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 copyFocusLink() {
if (activeIds.length !== 1) return Promise.resolve(false);
var record = pinnedRelationshipRecord();
var relationId = record && record.id;
var value = readerLink(relationId
? 'relation=' + encodeURIComponent(relationId)
: 'focus=' + encodeURIComponent(activeIds[0]) + (reachabilityMode ? '&reach=' + reachabilityMode : ''));
return (value === null ? Promise.resolve(false) : copyReaderText(value)).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.ctrlKey || event.altKey || event.metaKey || event.shiftKey) return;
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;
// Export consumes the active semantic snapshot; its controls are not
// an outside selection. Genuine outside activation still clears it.
if (target.closest('[data-node-id], [data-relationship-hit-key], .overview-map, #btn-export, #export-menu')) return;
clear();
}, true);
window.addEventListener('scroll', requestLensPlacement, { passive: true });
window.addEventListener('resize', requestLensPlacement);
container.addEventListener('scroll', requestLensPlacement, { passive: true });
// Reader and Chrome may finish resizing the stage after window resize.
// The absolute Passport does not resize its container when reclamped.
if (typeof ResizeObserver === 'function') {
new ResizeObserver(function () {
if (!chip.hidden) requestLensPlacement();
}).observe(container);
}
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:not([data-ambient-flow-overlay]), 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);
if (shape.getAttribute('data-motion-path')) clone.setAttribute('d', shape.getAttribute('data-motion-path'));
clone.removeAttribute('data-motion-path');
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 legend = svg && svg.querySelector('[data-legend]');
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 readerFit = svg && svg.getAttribute('data-reader-fit');
var widthFirstReading = readerFit === 'width-first';
var automaticSequence = widthFirstReading && svg.hasAttribute('data-sequence-column-fit');
var readingScroll = widthFirstReading;
var measuredHeightFit = readerFit === 'intrinsic-height' || widthFirstReading;
var frame = 0;
var settleFrame = 0;
var lastWidth = 0;
var automaticAreaHeight = 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 MAX_AUTOMATIC_SCALE = 1.5;
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.style.removeProperty('--archify-diagram-min-height');
html.removeAttribute('data-reader-area');
html.removeAttribute('data-reader-narrow');
html.removeAttribute('data-reader-layout');
html.removeAttribute('data-reader-overflow');
setRail(false);
lastWidth = 0;
automaticAreaHeight = 0;
settledCap = 0;
clearLegend();
}
function clearLegend() {
if (!legend || !legend.hasAttribute('data-reader-legend-corner')) return;
legend.removeAttribute('data-reader-legend-corner');
legend.style.removeProperty('--archify-reader-legend-transform');
}
// Keep the original legend in its SVG: renderer hover/focus selectors and
// canonical export still own that same group. Ordinary capped reading
// and fitted automatic sequences borrow the outer corner at 25–100%.
function syncLegend() {
var cameraScale = Number(svg && svg.getAttribute('data-view-scale')) || 1;
var cornerReading = html.getAttribute('data-reader-area') === 'true' || automaticSequence && !readingScroll;
if (!legend || !eligible() || !cornerReading || cameraScale > 1.001) {
clearLegend();
return;
}
var box = legend.getBBox();
var parentMatrix = legend.parentNode.getScreenCTM();
if (!parentMatrix || !box.width || !box.height) {
clearLegend();
return;
}
var rect = diagram.getBoundingClientRect();
var style = window.getComputedStyle(diagram);
var scale = Math.min(svg.clientWidth / viewBox.width, svg.clientHeight / viewBox.height);
if (!Number.isFinite(scale) || scale <= 0) {
clearLegend();
return;
}
var left = rect.left + number(style.borderLeftWidth) + number(style.paddingLeft);
var right = rect.right - number(style.borderRightWidth) - number(style.paddingRight);
var bottom = rect.bottom - number(style.borderBottomWidth) - number(style.paddingBottom);
var top = bottom - box.height * scale;
var nav = diagram.querySelector('.diagram-nav');
var navRect = visible(nav) ? nav.getBoundingClientRect() : null;
// Long or multi-row legends retain their canonical position if the
// spare corner cannot hold them clear of the dock and content area.
if (left + box.width * scale > right || top < rect.top + number(style.paddingTop) + number(style.borderTopWidth) ||
(navRect && left < navRect.right + 10 && left + box.width * scale > navRect.left - 10 &&
top < navRect.bottom + 10 && bottom > navRect.top - 10)) {
clearLegend();
return;
}
var matrix = parentMatrix.inverse().translate(left, top).scale(scale).translate(-box.x, -box.y);
var value = 'matrix(' + [matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f].join(',') + ')';
if (legend.style.getPropertyValue('--archify-reader-legend-transform') !== value) {
legend.style.setProperty('--archify-reader-legend-transform', value);
}
if (!legend.hasAttribute('data-reader-legend-corner')) legend.setAttribute('data-reader-legend-corner', '');
}
// 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));
var changed = Math.abs(rounded - lastWidth) >= 1;
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');
var railShare = html.getAttribute('data-reader-rail') === 'true' ? RAIL_WIDTH + RAIL_GAP : 0;
var diagramWidth = Math.max(1, rounded - railShare - chromeMetrics().diagramX);
// Content fitting and automatic enlargement never reduce the outer
// reader region. The SVG alone takes the narrower reading width.
var automaticWidthCap = measuredHeightFit && viewBox && viewBox.width > 0
? viewBox.width * MAX_AUTOMATIC_SCALE : 0;
if (automaticWidthCap && diagramWidth > automaticWidthCap) {
diagramWidth = automaticWidthCap;
html.style.setProperty('--archify-diagram-min-height', automaticAreaHeight + 'px');
html.setAttribute('data-reader-area', 'true');
} else {
// Ordinary automatic diagrams retain their existing document flow.
// Settling can also stop binding the cap without a new measure.
html.style.removeProperty('--archify-diagram-min-height');
html.removeAttribute('data-reader-area');
}
if (diagramWidth < Math.max(rounded, shellFloor) - railShare - chromeMetrics().diagramX) {
html.style.setProperty('--archify-diagram-max-width', diagramWidth + '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');
syncLegend();
return changed;
}
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();
// visual-check fails any page whose rounded-up height exceeds the
// viewport, so even a 1px remainder must be reduced or declared.
if (overflow > 0 && !readingScroll && lastWidth > Math.ceil(minWidth)) {
applyWidth(Math.max(minWidth, lastWidth - overflow * ratio - 4), minWidth);
settledCap = lastWidth;
html.setAttribute('data-reader-overflow', 'reduced');
} else if (overflow > 0) {
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);
// Sequence first-screen fitting preserves source text sizes instead
// of shrinking to the shared hard readability floor.
var readableWidth = viewBox && viewBox.width > 0
? viewBox.width * (automaticSequence ? 1 : minimumReadableScale()) + chrome.diagramX
: MIN_READER_WIDTH;
var maxWidth = Math.min(MAX_READER_WIDTH, viewportCap);
var readableMinimumWidth = automaticSequence || measuredHeightFit && ratio < WIDE_RATIO
? readableWidth
: measuredHeightFit && ratio >= WIDE_RATIO && Number.isFinite(declaredMinimumText)
? Math.max(MIN_READER_WIDTH, readableWidth)
: MIN_READER_WIDTH;
var minWidth;
if (automaticSequence) {
minWidth = Math.min(readableMinimumWidth, maxWidth);
} else 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';
}
// Height-fit families with notes below the fold may shrink to their
// readable floor. Width-first families choose their reading width
// below and retain page scrolling instead.
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);
// Compact automatic sequences fit the first screen without shrinking
// their authored text. Longer sequences retain reading width and page
// scroll; other width-first families keep their existing strategy.
var sequenceFits = automaticSequence && Math.min(desiredWidth, maxWidth) >=
readableWidth + (docked ? railExtra : 0);
readingScroll = widthFirstReading && !sequenceFits;
if (readingScroll) desiredWidth = maxWidth;
// Only a binding enlargement cap needs a separate normal reader area.
// Uncapped diagrams keep their existing natural height and Chrome fit.
automaticAreaHeight = availableSvgHeight + chrome.diagramY;
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,
legend && legend.hasAttribute('data-reader-legend-corner') ? legend.style.getPropertyValue('--archify-reader-legend-transform') : ''
].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 });
window.addEventListener('beforeprint', clearLegend);
window.addEventListener('afterprint', schedule);
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,
syncLegend: syncLegend,
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,
lift,
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 || liftFrame || 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); });
// Camera zoom changes outer page flow without changing intrinsic SVG
// size. Follow that box for dock placement, independently of scrolling
// and the baseline stage-rail measurement.
new ResizeObserver(scheduleLift).observe(container);
}
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 minimumZoom = 0.25;
var maximumZoom = 3;
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 originalFlowHeight = container.style.getPropertyValue('height');
var originalFlowHeightPriority = container.style.getPropertyPriority('height');
var compactPageFlow = false;
var viewBox = svg.viewBox && svg.viewBox.baseVal;
function clamp() {
var width = svg.clientWidth || 1;
var height = svg.clientHeight || 1;
if (state.scale < 1) {
var visibleWidth = Math.min(width, container.clientWidth);
var availableX = visibleWidth - width * state.scale;
state.x = availableX >= 0 ? container.scrollLeft + availableX / 2
: Math.max(container.scrollLeft + availableX, Math.min(container.scrollLeft, state.x));
// Long sequences keep their first rows in view instead of centering
// inside a layout box that can be several screens tall.
state.y = height > window.innerHeight ? 0 : height * (1 - state.scale) / 2;
return;
}
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 camera = sampleRenderedState();
var x;
var y;
var width;
var height;
if (window.innerWidth <= 720 && container.hasAttribute('data-wide-diagram') && camera.scale >= 1) {
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 + (((container.scrollLeft - camera.x) / camera.scale) - metrics.offsetX) / metrics.scale;
y = viewBox.y + ((-camera.y / camera.scale) - metrics.offsetY) / metrics.scale;
width = Math.min(viewBox.width, Math.min(metrics.width, container.clientWidth) / camera.scale / metrics.scale);
height = Math.min(viewBox.height, metrics.height / camera.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));
// The camera's layout box can span several browser screens. Intersect
// its logical bounds with the actual window, including page scrolling.
var matrix = svg.getScreenCTM();
if (matrix && matrix.d > 0) {
var end = y + height;
y = Math.max(y, Math.min(end - 1, -matrix.f / matrix.d));
height = Math.max(1, Math.min(end, (window.innerHeight - matrix.f) / matrix.d) - y);
}
return { x: x, y: y, width: width, height: height, scale: camera.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
});
var readout = (showDetailLevel ? resolvedLevel + ' ' : '') + percent;
resetBtn.setAttribute('aria-label', readout + ' · ' + 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;
}
// CSS transforms keep the SVG's intrinsic layout size. Shrink only its
// outer page-flow box for long ordinary diagrams, never the SVG itself:
// changing its height would feed the smaller size back into camera math.
function ordinaryPage() {
var html = document.documentElement;
return html.getAttribute('data-embed') !== 'true' &&
html.getAttribute('data-present') !== 'true' &&
(!window.matchMedia || !window.matchMedia('print').matches);
}
function syncPageFlow(camera) {
camera = camera || state;
var height = svg.clientHeight;
var ordinary = ordinaryPage();
// Restore ordinary flow as soon as the target reaches 100%. Chrome
// uses that target to resume baseline rail measurement; retaining a
// partially shrunk box during the CSS transition would inflate reserve.
if (ordinary && state.scale < 1 && camera.scale < 1 && height > window.innerHeight) {
var style = getComputedStyle(container);
var chrome = ['paddingTop', 'paddingBottom', 'borderTopWidth', 'borderBottomWidth']
.reduce(function (total, key) { return total + (parseFloat(style[key]) || 0); }, 0);
var flowHeight = Math.ceil(height * camera.scale + Math.max(0, camera.y) + chrome) + 'px';
if (container.style.getPropertyValue('height') !== flowHeight) container.style.setProperty('height', flowHeight);
compactPageFlow = true;
} else if (compactPageFlow) {
if (originalFlowHeight) container.style.setProperty('height', originalFlowHeight, originalFlowHeightPriority);
else container.style.removeProperty('height');
compactPageFlow = false;
}
}
function syncViewportClip() {
if (clipFrame) cancelAnimationFrame(clipFrame);
clipFrame = 0;
function sample() {
clipFrame = 0;
var rendered = sampleRenderedState();
syncPageFlow(rendered);
clipToViewport(rendered);
if (Archify.radar && Archify.radar.isOpen()) Archify.radar.sync();
if (Archify.readerLayout && typeof Archify.readerLayout.syncLegend === 'function') Archify.readerLayout.syncLegend();
if (!cameraSettled(rendered)) clipFrame = requestAnimationFrame(sample);
}
sample();
}
function apply() {
clamp();
svg.style.transform = 'translate(' + state.x + 'px,' + state.y + 'px) scale(' + state.scale + ')';
svg.setAttribute('data-view-scale', String(state.scale));
syncViewportClip();
renderControls();
outBtn.disabled = state.scale <= minimumZoom;
inBtn.disabled = state.scale >= maximumZoom;
container.classList.toggle('is-pannable', state.scale > 1);
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(minimumZoom, Math.min(maximumZoom, Math.round(next * 4) / 4));
if (next === previous) return;
var longDiagram = svg.clientHeight > window.innerHeight && Math.min(previous, next) < 1;
var before = longDiagram ? svg.getBoundingClientRect() : null;
var anchorY = before ? Math.max(0, Math.min(window.innerHeight, before.bottom) + Math.max(0, before.top)) / 2 : 0;
if (before && (before.bottom <= 0 || before.top >= window.innerHeight)) anchorY = window.innerHeight / 2;
var logicalY = before ? Math.max(0, Math.min(svg.clientHeight, (anchorY - before.top) / previous)) : 0;
var documentTop = before ? window.scrollY + before.top - state.y : 0;
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();
if (longDiagram) {
var scaledHeight = svg.clientHeight * next;
var pageY = scaledHeight <= window.innerHeight ? documentTop + state.y
: documentTop + state.y + logicalY * next - anchorY;
window.scrollTo({ left: window.scrollX, top: Math.max(0, pageY), behavior: 'instant' });
}
}
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 scrollLongDiagramTo(contentY, metrics, options) {
if (!ordinaryPage() || metrics.height <= window.innerHeight) return;
// apply() may still be animating its CSS transform. Recover the layout
// origin from the painted transform, then use the final camera target.
var painted = sampleRenderedState();
var rect = svg.getBoundingClientRect();
var pixelScale = rect.height / metrics.height / painted.scale;
var documentTop = window.scrollY + rect.top - painted.y * pixelScale;
var target = documentTop + (state.y + contentY * state.scale) * pixelScale - window.innerHeight / 2;
window.scrollTo({ left: window.scrollX, top: Math.max(0, target), behavior: options.instant ? 'instant' : 'smooth' });
}
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; }
scrollLongDiagramTo(metrics.offsetY + (logicalY - viewBox.y) * metrics.scale, metrics, options);
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();
scrollLongDiagramTo(contentY, metrics, options);
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();
});
});
// Reader width and Chrome reserve can change without a window resize.
// Observe their resulting sizes; the SVG remains intrinsically sized, so
// the outer flow height cannot feed back into its camera base.
if (typeof ResizeObserver === 'function') {
var flowObserver = new ResizeObserver(function () { syncPageFlow(sampleRenderedState()); });
flowObserver.observe(svg);
flowObserver.observe(container);
}
if (typeof MutationObserver === 'function') {
new MutationObserver(function () { syncPageFlow(sampleRenderedState()); })
.observe(document.documentElement, { attributes: true, attributeFilter: ['data-embed', 'data-present'] });
}
var printMedia = window.matchMedia && window.matchMedia('print');
if (printMedia && printMedia.addEventListener) printMedia.addEventListener('change', function () { syncPageFlow(sampleRenderedState()); });
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;
if (item.links === 0) links.setAttribute('data-zero', 'true');
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) {
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) return;
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) {
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) return;
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:not([data-ambient-flow-overlay]), 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);
if (shape.getAttribute('data-motion-path')) clone.setAttribute('d', shape.getAttribute('data-motion-path'));
clone.removeAttribute('data-motion-path');
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);
if (shape.getAttribute('data-motion-path')) clone.setAttribute('d', shape.getAttribute('data-motion-path'));
clone.removeAttribute('data-motion-path');
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 copyLink() {
if (mode !== 'result') return Promise.resolve(false);
var value = readerLink('route=' + encodeURIComponent(startId) + '~' + encodeURIComponent(endId));
return (value === null ? Promise.resolve(false) : copyReaderText(value)).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) {
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) return;
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:not([data-ambient-flow-overlay]), 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);
if (shape.getAttribute('data-motion-path')) clone.setAttribute('d', shape.getAttribute('data-motion-path'));
clone.removeAttribute('data-motion-path');
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 copyLink() {
if (!selectedKinds.length) return Promise.resolve(false);
var value = readerLink('lens=' + selectedKinds.map(encodeURIComponent).join('~'));
return (value === null ? Promise.resolve(false) : copyReaderText(value)).then(function (copied) {
copyBtn.textContent = viewerText(copied ? 'viewer.common.copied' : 'viewer.common.copyFailed');
copyBtn.setAttribute('aria-label', viewerText(copied ? 'viewer.common.copied' : 'viewer.common.copyFailed'));
window.setTimeout(function () {
copyBtn.textContent = viewerText('viewer.common.copyLink');
copyBtn.setAttribute('aria-label', viewerText('viewer.lens.copy'));
}, 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) {
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) return;
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) {
// Return focus where the reader was when the guide opened; the
// trigger itself is only the fallback when that element is gone.
var target = returnFocus && returnFocus.isConnected ? 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) {
// Clicks anywhere on the trigger (including its icon span) are the
// toggle itself, never an outside dismissal.
if (!panel.hidden && !panel.contains(event.target) && !trigger.contains(event.target)) 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.shiftKey && (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Home' || e.key === 'End')) 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>
<script>(function(){var h=document.documentElement;function set(m){
if(m==='embed')h.setAttribute('data-embed','true');else h.removeAttribute('data-embed');
var p=window.Archify&&Archify.presentation;if(p){if(m==='present')p.enter();else if(p.active())p.exit();}
dispatchEvent(new Event('resize'));if(window.Archify&&Archify.view&&Archify.view.reset)Archify.view.reset({automatic:true});}
addEventListener('message',function(e){if(e.source===parent&&typeof e.data==='string'&&e.data.indexOf('arch:')===0)set(e.data.slice(5));});
// The viewer's own Esc/Exit leaves presentation inside the iframe; tell the article so it closes the overlay too.
new MutationObserver(function(){if(!h.hasAttribute('data-present'))parent.postMessage('arch:exit','*');}).observe(h,{attributes:true,attributeFilter:['data-present']});
parent.postMessage('arch:ready','*');})();</script>
<style>/* Viewer centres the 720px canvas in full screen; on a narrow phone that hides its left edge past scroll 0. Auto margins centre without overflowing left. */
html[data-present="true"]:not([data-embed="true"]) .diagram-container{justify-content:flex-start}
html[data-present="true"]:not([data-embed="true"]) .diagram-container>svg{margin-inline:auto}</style></body>
</html>
"></iframe>
<iframe data-l="en" hidden class="arch fig2" title="Figure 2 · System of record for agents, Norwegian Armed Forces edition" srcdoc="<!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.2.0">
<meta name="archify-reader-contract" content="declared-wide-v1">
<title>System of record for agents, Norwegian Armed Forces edition 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,d09GMgABAAAAABqQABIAAAAANggAABopAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGnwbHhyBWgZgP1NUQVSBICceAIFiL0wRCAqrfKMTC4FmADCgXAE2AiQDg0QEIAWHRgeHQRt7L1UHctg4AONHDw/J/98StCEx8OvQuaoS6nSKOb1TAFiw0Fg4OrHmUFx9lE0RHuCAwNZTYLzu0qIYrA2BIRRRhOaLtZ08a/sp9zYCdQA8KAuMM8JgShJLBIph0a9CVKNU5Kz3C6kz9f9u+04Z3maEJLM+PW3a+3+XBRaLKxEnQkSEEoEu1rSppBKruKauuYp7ciJWOcmUo7nhJS8iHiFtjmq7+WH3ih3OVZkJbJviPSALIQK9R2ufHSCVMDwf3t6dTUbxJMCBZZzEHGDq/dW5+BuhCCwAyNGp7Qbfqz4neKAyTZqkGLqcOXD2yAf6S9CXBKjiYHe7/6RBBJEETaIBSYQfAEpOU/IgUnwldDH6e5eQmMaEF7GNOjhvxlN4rPb8O1d9ZTAkFFEhK06K6oYYy9vXX9FcKceGqruiyWE2JCclRgHFyIg66EQRUWcsdUzg/vg/XWk7f2d14NAqqA3ePkfv6RIuurMrd3UqjTRrwUg5rbL7vDodg9Y+ExzrQgidzOugHCAuSqIOqEyXok1RVKmsstSQWe1WWYkPCB2v22Tun9a18LCMh7GGEmM6XPw5a4EAkANqUzghYDwPnpH3EcgHC+HV1Hy4cD5SFB8jho+XzGulG7k3nqCAJyjlCap5At3xCRr0RWDiCSxAMgAPn2dCCl+VdZCABQC7HQBgbrxidHmvu3FUC5SwCxOQ6poAHfAe0QaloTJaGWDAQSf/xZSacW2eDz4yws0e48wXgakYTRmcfIB1YrQBABIopChcNDhxSYyuFQkBLWkYEOfEiH46rVhq7LKIALoAY4AKU0Ks0VTuMAWkyBeci7es/AJwGQrmlb3Lc4DphegAkC3/VwBV054AAUQglOdSAC6bC0HHfoaMIgB+IKiaqDY3RJ1c0XzIqYqUOACyBkixZFk1PQ80AyJmAtBvcq01mxzEpcY2TadE5E01uYqNGl6eWHUBxFlRdDkvwCoOnHc8Qp8o0oKnMPAZENNn8ZCLEXD3WCjk78+zFVcIZUY7xQUucpnb3GeXd3zkq7MDCKNg1uaWuNwd7vcj72Y7IyjK5bt5eUemqvc7oHdv9ub2zrGJ+RcLgrzKRZMfGCxcf1St2sUbIIFGomQp6mmlMjGzqGVUx6CvBoI0BIADCsKO8eFBy0im8g6CgLBNApERkSpqo5xwIekkCiIGu0Q56bBHNWIdBnyzeRuIlgVic0C60JQ8zYDaSR0me6BaVZsihtnzAjUTzCIy0mZ0ndVjc+2+CFPkLJwWCwGKnhBJc01RbKponrUcQ3xFlmEkUmepVC5RCl8nJ6VCKvFQ8XXRScE1AZ5XKFh2x1GxLKp8Iqci3JRuLs5eDvEBbkZWBaVXjMtxAz2UKlepR6xbn9xJzrIlIYHBxrcMDc4lHRT5NKzzVrbmJ/zI5Ml2clsFATKRG6KH58X8xmxb/ETWNKnEi1mJc0GHjAI59LSCo5OiguksFii5rWQpm/XVwm8ie9xZem6I/Q1gp2pK/I1abEITR5Ss19NLEoO76Wx09wvmZNKVNVXSw/OGpmMb8QOAYU9pD5sV4kxzv6ml4HFn6ro29zZQiJisz9xWJR51jr3UvNNN+d6mPmq+y1f8W0GXFeSQJAKZ8Uj0r8nP1NIjgGwjm28qPTfU/iavBDGTrMspK+0JYjONRSmPSRxEF0rKO7LGjw2tUT3knQ3TlKH6Y5XFdvJaBwFGNrdlSWMmBj1xE5tpM6nOcx/j9qJIhkuDeV1spps0ot+0oPB8rgMJIsqOQKV3N0zzQ3V2d+6HwBAdFrHnTO9PaPgz84cgK35rIKzTrwqvJk6i3nopC/XZBflAofAzhJVgLJnfCQ1jbvzldHZqrr3BZ/4KBvv21tV9cmGF8b4UIGg19Piga95n0h9qnnje+IdOFjoM/Pp8IfclGMivV/xKw0GoVWZMeujlo497y7bCxwlNQ0SsMvqH511Qr11f3UjTLZ5RoKxnfn5SvqQppumAUNAVWIdYQtc/2tsG3/nF5shtVeBjHOe4ejr8wC668QvNTa7106oNf0Tq+S2dl4wXjNIYguaICF1TQVF8gjrTrrYNme6SvKgeBo/sbqDWybhnp0d9QUN4beOFQ7jmIRlavgwsle5NI0W0+0WPUaWQ55EIF914AeoD+zzGEdRfgp0FgjTcdkKXBuulJjfQyKjucsrxdrXWqUfpOtAHWz1Mj6CIeIe3cT9spO5Tq6xQS73Js8KyNdNJjl/68DBgmK+eqolt7QYb+rqJfVUuwHxE2LCpSgETT029ZWyhZ+dbloQAVP+PSg8AzYXu8ee7TXGPfXqJyFOc1n5hj/UzZ8oH/h/WPY9Wt61qx5alc0tKY5bVxYTqEMdtP6Idr5ukppispDLqwcJumSGOo0jeA5p6dlYqPgrvs3RAhf4dvULqV6nCfXO6r3Ro1p+fslo/J3Ng1j8Xab2rU/vfab04YfB1fZoroLJnTMXXR2bHk/+Wlfw0pscojR7UKh3FD9Xu6vm7e+awQeIOUXvCMAg94z5r8xhYSnUhlxi26U/b3AeUC70EK6T51mjMoxkWcxuT1UhcmqUxbwVagpjhDC4aoKxqGdHA3POzPiDB+4zynfp118+Onnx2RnEKYTMJz8b/83/0P+lvzwdLn7TfHDxk6+vTFguZxUd1vBpws/HJ2MEHP9fiVmQXHTX5B9gdP77tLos6/dTd/aPTp0dZwsrjUC+kgnrlyqpctQKixPm2aMKj5+QnuWeT1Uhcem5ekrcCLUJKyTm5RQOUmZaxEyBSamvf2vUsD6u6IBId3+N0d2wFv59aZN637SqfeDLzrDHt9rkcWnr2VWpOgtez8S1h3g82Vj8PE9nxmKN+lZfg2Qls1kwcZBukX+VoHCuuvVvVs9+Hll6M+inirm384uLzET9F3c9Vg0a19bF1y1eDT4vO+Tdf+cZWx8QfvgnfXRsnGNz49hPjfw/UnaDD/Rr9w3prW2gNL+LI2LL9ABNUZRfj4qiZqz+tP3UQt7NUf7ACtHo0A02rfKqLS/Z3/N7k8UscTCkBup/TJjq1zrLnbaGf1H1DnChUx7seMd3Zog79Qp2byjh/ZULTu4k6JpvKLdTYMdwK3bOW8GhKmoXNFIS5iGdJ9pe8dDMYdqzGN7g7lGwkW57tBO6IxtdDJq219vlX22y9il5MqRPHb7AWWxvGT8TF9KK8iAx8QXYhFSVFIxfkfs6A6djd7ETHixHiL09UFITqW14psqYQ5/oJevxxbmKmYcqyTlGgsFzEBif/r9VSKoMP9fGroeY9vT3XTgkESGi5d/Conq3+3x+H8TpH020J0tRTxmm+/qoxKjv9gxfVODK/yVF5zVpJ09ZJ0hrbCMzXXg2PeDW4IBBYoL0aEf5qUAEo4uLsTfU0JtQYyK78kBAOaNWFMFbqFS+39T02QjP76D6eC/+O3V3YF88vXdQXDYcfGWK7orUMFJwIcqbBdN7ZtadcYJUrhb2HnAqKy9jUWeG14ZPLi6RBRbKZgmxmTfIppgE+MKutDRMW0rxpVLtrWoutmRATXiQG8e7EeUiSoqLckykXxColWf7D8ulR1QpsiS9ZtMQIS8/qg04Naph7oh6alz5lD/bjuuuRq6/52c/v66tHr4b5WI/QBBUqiIjC1dZqb0OfE/ojsTiQTGgaw+8GfVhS6NLxZ2U34Mb+leuTq6O8rVpiWkcorYhCOovalXYyRxQlR5ItXp9ua/fBzcCdrRT8YuDml8IrXc24nM8vyy6YWgOWYx45MduNEGpHoha5OifSLkRFrZTGEyf2m7R19PtoYNLoH5CdbQDDg2Usye1EGfGDXA1anCaKkDF8Vk6+Cv3O1/BX79gEDLfWyLW5z3Qjx2sTjtXYZl4oGWNimm2pTFm8igteNzC9pDpraHfqn+ivFeQohyqAHjveb1jPdaoOooby/nCdU888aFE33bmT/S+DGejBSHtkCJBrDGoGfL/084zSQAoZfjXdO3GvNNOoFwbR3WpVS7c/578oHibz85OF3SstRUQlr7F1V5pr7xZuwcyUnOIsLquYzjvmodnc78LGloWouhE3Kz/2aIj2jSsN/vTFyMrLEpw+fYmoob4XGvZArX4QFnoPO7fUpMyYnoJ905XD0gI7AwP2B7hkIvJIsjNq/TzfQF+/IF/fIL8m/H/1WsHzTXi7UVHw7FtZpy+01FCTVxJmD1+mCivJk0Y8m6V/g75I026x5myBwyZVP51hRp+xiF+EqkEAcBixdFULucIEs9gFi7AD5rAYS7AU81iGXbEcK7AKu7lKOAWPATHaEtGQFOX+pMZh1N0YzYnxzclwxFRIXBy7FQT4HIZPK6N8Du4GdsaO4CSvZpSAvRLno1ZN1x2K27HTAq1rj2E0WF1HS68SRzI7MZO55kkB2SRESTn0RxQaD+HQzy8mHn77+HymOc3Y8kuqDoAGqJo5aaZpTyzgBC3gDk0ggXqLgVST/1FzFfm9OmaIh0qQgwAfjmvMO0C/ibVA/oOCGrQ1gfTjBFfT5LVT8j1BDEU4lsWr6ltsScw9yP/wu9WFnb+ZGLImADSMUsr70YjU0NgYTNUuCaLr8w8MjYWQd/2gyVKdSgppUjOOI05qzYKSHtWc1JqFRQRpcwRKRpd/cffNqbRZvpuwZGr3q+Pvt/frdOKx65HHZi0m9/x+rBQEAJny/0HsA/TuKrjxEaSCoNk4C3AAV/A9AZGTSJJDqju6i7u5+3qi3/Qd9CKlEupLg2kEjaN9sTGQcUxhFivpZAu7OM1FrnOXx/xJnRkvJpAJZ+KYFCaLKbRcnSNc5TVfj5jNYBtn4uyY03Nxnszv85ZHiOJEpdux6/b6dm+Cd4xwHBc0uJ4zc/25IVwLN4qbwE3j5nJd3CnuAvc5y7l/xSqx2xCfIUH7jz1mSOKQCnEfcbO4XTxDvEC8QtyV63pagAPEowB+bT1GrBUjvWzOdIhD6raZTFEAgrz97+crP9+QRDhA5/BidhXd9DwiFgoUELSDuIgMcGveIt9Ls4McxbIheGS7Wbyk7p4vc10j6QqubI7cQbU/OEh43Q/H7dIyKgq3czCgauO/XWp/9xWb9efGAeBr+RLbpDZjabJPgEK2xW+KRjAEfC4DMpMZoovw5BdXiO3PBhPIg26zedEQlhUICqAoz2FwshwtSQGHB8tuXf/jeYPS/39rNeR3HyTy2tPmbBisuZHb6M/7c4vzc2bzaGFh5vWfKNBbGpBlom2Wx5fYw2ouWXTqltnT1zttLTPZ/347tHM3HC71pf36qspa1R3iYXQGc5DG+3SThNGYXqtFsC8bczSbRrOWTk6mphjNUT9jSDUlMxUkNOQqPB4AVEPyMsKvrXkT5q54vrW21jJ+s6JIAMO2Nt5ZIb/LQoKtST8aCQvd6t5qQ6mPA5GqpI/YPEYOuv4xymOLe8O8EA4dFxVWgjiePHriaPK4QEAQH2YynzKZ96yA36FPlquj7R2CsC+fYCx7DJXS+6WBaOx66cxlFi4ECLbY/NxLr/CiC9Pg08nMuiJOrqbRqCEXJJAJOojxJm2QJB4bsXuXQ2r3us6MVpw7rWeny+Qks+uoGQypHY+ZlAcN9CJgyQtAvOCDQkJklF/UxB5rgexNmtEcosEnsjUi4qEnq8ZhNOxg9y3Ni4Y7cxGdzHnlJQh5XcYq3+gcHkvDNJjWI6KOfKazUaENPS3FiK7XykVWVfQMigZD+PHDQRRttVzoXMvjQlLVnncLPNaIID8Y9pZ1frg3BE5qNh5bWcnqu0GIHzQOH+o7enY3JKZ2lKpGo5533DlOcjUEVcFgnR6gS/98Fqon8s+E1tVmvzDIXAeTyc+f8Z3ESW9+Onv6liSQRAIRbt9HcYZRFJDit4nKNC1H+uTdAWv5yEOXW1yUb70S6d2vZ3npz9+V1mOfHG6O7O8/aTZ9EvgqfIH5OwS8E0W3yr3Ie6XZ0+ipw/8F8f//T4e0k6dOWU1XFU4GKF2vlopUU6SxehMudFw2H8FQVNSMlctAs8SQS3W3pKZuyXF9gk63O6OAXmAJAAKn9fNHBlSvstkScESiUUG0VLW1iwbTmNOn2wc6OGibGjBOaX9iXy3BduM0vomzG7gGYNmhbFJqXgMva0iQT3p4QYkgbKoKtmZ5NV8re+Tju6gk47i7KPBlzP3sSQ5X2uKaJkMvlHJHnz/u0HTW5zdtWvf/F0zXXewX22c1Y5d1x8Bu40oac5JKMa5G+WCWl03JpURyBxtDBwNao+chTgQyGGGFsH32prAplLSw6/BN83zqHXgjq6jeHI3cNdt6mKYZQNc6n4QQcc1bCBH6WnahRdbFO+952BVlk7WSrrOcZTn4FOM40fLHz5w9dRwDp86ePvZKoSdN3iQYZkmgBwdSoPRjBNghy9k1SVLyjeSaI8VHcdcvOdIp58oud450CSYEI9AayNDhQLCHQM7jiSFr02iQKTzr5aydLrNt37m6LFcyp44hG7KbemfiARxS1yhEHRfR6uisjZmdxQJ2a6QQ57E/tie0ZLsaTVPXUJOrbQOK9PRhGO7rirKsP7HhUnlOVQt56e0Pb974zijZzMTwRhCRfR+BExvr64v4gSqK0TAeOJSmpwXFrFo2RYjtS5i1BnyaDJasggMjgVChwDBH760cS5GCJKE+R3wZMSONtgHT0KK6/s4UciaFU3Nt2E0Yexsk2Qu5wm1LJRKcI38SJTmGEjheKtGAWK+vZyjnJGXkuyPxoEtazbp3D75/XxYHJ45gMk+tl+XVFJkjpPbo3AI+ICowUfz307nLv565nkGqlVAUDoIXyhxiUqhtFS0AeBBuFg0X4pMmwIcEwFw7RThMcfEuXHcIz4KjhUJlq5JHZMkYAgxbXi63KWakUGu3t07m6TtbtHdifKLZKH63/xbEaGAckVFymFX9+CBsGpRE+fSOpCadPk9Pfeaew2BZouFWhYdYiJHUxRDK408vVjY20TSJYYJ0bsqAz+DDcDK5gFtWPDCeCLFcgSYInKJGmYdnKWT4DfAHaGWr6HZ7Mnz/tEkHNQDHOjHBpVSQ6P1bwcBlJ6py8UrHea40ffezaOB5Pi4NGhsdM+r/f3brb0TG0bRHszaZef7ukzjK9sSGMYe4Mob7Vh/FNbYTSPLXHcinjzLIXTgaReIoKG6Qv6tGwx3e/O/b229+2b/rCO/kJoF9qyRT8bf8n+SXq9gQa00rmjJqjHwTxupItaKoGSz+/r0gCqcinkToRMEp8ChQxtWIQcC1qpFv53Hh/1YkSTG/FrmvGUxgfFSAAtzewij8VZpDZJPv96k5wkZB/+up/6Cqh85RoiEWrim8fWKikoNO2A6A8bItfNE21jZzZiiKhgjBR4c6StO2ufktUy9LRGLLlQBcDnurg2XwqzWTR3PZSjcOMC0+96v0g79YXoZpVXHzhto8T2RDxnx2zGyVInMz3DjNA8PGdXgnsfjQMeuVi1NyxOFUgLHiHu9/oj01/KCqolIk8F/QtH5Vo6yPDnFbZ4mjIzhi2af3n5bWgwaUl+Y7R7wO8T+wgtNqiXsT3csmGnPDwLE8pGkEIV75qM90NhXBfzlres3syYois6wsV4yuHOoYv2R5BPB1eI7Ovp9S73WSo11OZIEGryR3A6qnFdARClLBWr52wA/cLcjvhypjWu1HGSDyEkNtwPkQORejENEqthGLIPrzKm4WsU6NsI17JMDBB2JFmw6p6hiKUq0OCLGwLt4k+ZaPEGRuDqFIkmLz1AGwHu254R/70ldlbjsg+h0AANZmv1lvPv3VTMPZVdkrKfRkAA4UAECAbx5yqHGi6kPoxYQg72trY4RcVwMWu8mEZjDblVDb2ARaCIdgu35kC23QAlZICTfi2i1VaLLrm8NGN2GNK1vXHra65LSQYoeboEH8mW9xAV3WTFVIsx2YbUFBpl3sxi5sto1qDGXQQj7Ugye2anDKWaNDq2OKukK4vhl0thkTumGXvX/EdYcxSuySAJjcxt0AnhuLxHuOL7WvjgDoEI9BEjbMz4yCAnD2ExgQVgrgPA2oigQ+zYoUSvJOZJBZZ0UW0bVEFMGrRokcIqv8cCVSyuVoBJATCWNxqRJFkKNX5CDBG1EMJ1+JMiQ5LMqhMUlUIFa1qEQqN0MFjv93OoAHcDM9PeE1UfDAaJooSwC+5IJeZNPbcb+T6tSzaPl0iKfTaeEtkuChVqfBCrZGFm7hAYKoZo5Bh5OBi36uCjlImRz1SO1FYsJjast3pfEQgzpGNnMrV96c3vikjKNiCIuxzxh2szEIE+DUG7P5GBtaGuPILGNaSIxtmLxjw87D7CcEUimzvvaMvQrJFKh7a1EbtzkWYfimFpr8hyr45Oui+xmCPB3eWBTeU+o5OrU93h2NEvITau+GBRDsuKMALQvvoAkNj4q2dvy3Ke6yaDMys/Qwoi8pK6qmG6ZlO67H6xOACsKEhEXgCCQKjcHiRMXw4nr16Tdg0JBhI0aNGTdh0pSEpJQjjjqWDVKdcNIpp51x1jnnmSw2h8vjC0xMzczjqILfNVWWYchvbWNrlyXqHBydnBV3qZnh3f0d16+pTEpINJPFu2uS/MS8Qb8i0iS/+nIJ6sO2VW86iWlDK7vsmdZjJ9d7Dru33D7feGRCPjiD86zYfOUR6rZCC42WGp2Rr74P/2ntaI14mx90h8NW4T18/HC+ErzOJ2xnfb1Hu/LzJJ6C/g40HY9xqtYYAmrJlEsKybqgNeHDCbrq5rthkzAXbafuP620/ZELm2jHHQUmhQtPhImM0Z9ZKPBkmvhDglUC8R0/KNYW8jX00uiLnKg5WNisEz8mklcAFKeBQUTuLiXKiySyC2PPZ8UaekxY87W9XgiaVew0K9qvrECVdCYFgs+TcAnl0F4oSOHb75vWbuJ7krbIHBWfF6H29siCNkklk/eLT5Ur/3XvXTqIvrf733vVumw7TrFoj1RDrZ1tGQAAAAA=) 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: #94a3b8;
--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: #e2e7ec;
--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: #64748b;
--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. Compact automatic sequences fit at
their authored text size or larger. Long sequences and waterfall rows
favor reading width and page scroll; other diagrams keep height fitting.
Small automatic canvases retain a modest scale inside a usable shell.
Every strategy preserves the authored SVG/viewBox and semantic geometry. */
.container {
width: 100%;
max-width: var(--archify-reader-width, 1440px);
margin: 0 auto;
}
html[data-reader-area="true"] .diagram-container {
min-height: var(--archify-diagram-min-height);
display: flex;
align-items: center;
}
html[data-reader-area="true"] .diagram-container > svg { flex-shrink: 0; }
/* The original SVG legend can use the outer corner while the capped or fitted
diagram stays centered. The container still clips all outer paint. */
.diagram-container:has([data-reader-legend-corner]) > svg { overflow: visible; }
.diagram-container [data-reader-legend-corner] {
transform-origin: 0 0;
transform-box: view-box;
transform: var(--archify-reader-legend-transform);
}
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-entry="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.95rem;
height: 0.95rem;
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.95rem;
height: 0.95rem;
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"] {
position: relative;
border-radius: 0;
background: color-mix(in srgb, var(--panel-border) 38%, 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="reset"]::after,
.diagram-nav [data-view="in"]::after {
position: absolute;
top: 0.55rem;
bottom: 0.55rem;
left: 0;
width: 1px;
background: color-mix(in srgb, var(--panel-border) 75%, transparent);
content: "";
pointer-events: none;
}
.diagram-nav [data-view="out"]:hover,
.diagram-nav [data-view="reset"]:hover,
.diagram-nav [data-view="in"]:hover { background: color-mix(in srgb, var(--panel-border) 60%, 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(--panel-border) 38%, 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; }
.diagram-guide-credit {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.6rem;
padding: 0.46rem 0.9rem 0.56rem;
border-top: 1px solid color-mix(in srgb, var(--toolbar-border) 74%, transparent);
color: var(--text-faint);
font-size: 0.5rem;
letter-spacing: 0.02em;
text-decoration: none;
}
.diagram-guide-credit-link {
display: inline-flex;
align-items: center;
gap: 0.28rem;
flex: none;
opacity: 0.72;
transition: opacity 120ms ease, color 120ms ease;
}
.diagram-guide-star {
width: 0.62rem;
height: 0.62rem;
background: currentColor;
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 1.6l1.9 3.9 4.3.6-3.1 3 .7 4.3L8 11.4l-3.8 2 .7-4.3-3.1-3 4.3-.6z' fill='none' stroke='black' stroke-width='1.3' 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 16 16'%3E%3Cpath d='M8 1.6l1.9 3.9 4.3.6-3.1 3 .7 4.3L8 11.4l-3.8 2 .7-4.3-3.1-3 4.3-.6z' fill='none' stroke='black' stroke-width='1.3' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
}
.diagram-guide-credit:hover .diagram-guide-star,
.diagram-guide-credit:focus-visible .diagram-guide-star {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 1.6l1.9 3.9 4.3.6-3.1 3 .7 4.3L8 11.4l-3.8 2 .7-4.3-3.1-3 4.3-.6z' stroke='black' stroke-width='1.3' 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 16 16'%3E%3Cpath d='M8 1.6l1.9 3.9 4.3.6-3.1 3 .7 4.3L8 11.4l-3.8 2 .7-4.3-3.1-3 4.3-.6z' stroke='black' stroke-width='1.3' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.diagram-guide-credit:hover .diagram-guide-credit-link,
.diagram-guide-credit:focus-visible .diagram-guide-credit-link { opacity: 1; color: var(--cloud-stroke); }
.diagram-guide-credit:focus-visible { outline: 2px solid var(--cloud-stroke); outline-offset: -2px; }
@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.35rem;
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) 14%, 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;
scrollbar-color: color-mix(in srgb, var(--panel-border) 90%, transparent) transparent;
}
/* Slim overlay-style thumb instead of the chunky platform default. */
.node-finder-results::-webkit-scrollbar { width: 10px; }
.node-finder-results::-webkit-scrollbar-track { background: transparent; }
.node-finder-results::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--panel-border) 85%, transparent);
border: 3px solid transparent;
border-radius: 999px;
background-clip: padding-box;
}
.node-finder-results::-webkit-scrollbar-thumb:hover {
background: color-mix(in srgb, var(--text-muted) 65%, transparent);
border: 3px solid transparent;
background-clip: padding-box;
}
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: 3rem;
padding: 0.52rem 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;
}
/* A zero count is absence, not information — let it recede. */
.node-finder-result em[data-zero] { color: var(--text-faint); }
.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;
/* Card notes are authored prose and may carry an unbreakable run such as a
slash-joined pipeline or a long identifier. Without this, that run sets the
card's min-content width, spills past its track in every rail placement,
and pushes the whole page into horizontal overflow at desktop widths.
`anywhere` rather than `break-word` because only `anywhere` shrinks the
intrinsic minimum the layout is sized from. */
overflow-wrap: anywhere;
}
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 {
display: grid;
gap: 0.5rem;
padding: 0;
overflow: visible;
-webkit-mask-image: none;
mask-image: none;
}
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) { padding-bottom: 0.25rem; }
html[data-reader-rail="bottom"] .node-outline-group > ul {
display: flex;
flex-wrap: wrap;
gap: 0.25rem 0.5rem;
}
html[data-reader-rail="bottom"] .node-outline-group > ul::before { display: none; }
html[data-reader-rail="bottom"] .node-outline-group li {
flex: 0 1 auto;
min-width: min(100%, 12rem);
max-width: min(100%, 32rem);
}
html[data-reader-rail="bottom"] .node-outline-heading {
justify-content: flex-start;
gap: 0.5rem;
}
html[data-reader-rail="bottom"] .node-outline-heading > span { overflow-wrap: anywhere; }
.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); }
/* The desktop bottom rail is an index: compact rows keep related nodes
together while allowing full names and descriptions to wrap naturally. */
html[data-reader-rail="bottom"] .node-outline-item {
grid-template-columns: 0.625rem minmax(0, 1fr) 0.75rem;
align-items: first baseline;
column-gap: 0.375rem;
min-height: 0;
padding-block: 0.375rem;
}
html[data-reader-rail="bottom"] .node-outline-item strong,
html[data-reader-rail="bottom"] .node-outline-item small {
overflow: visible;
text-overflow: clip;
white-space: normal;
overflow-wrap: anywhere;
}
html[data-reader-rail="bottom"] .node-outline-item strong {
grid-column: 2;
grid-row: 1;
}
html[data-reader-rail="bottom"] .node-outline-item small {
grid-column: 2;
grid-row: 2;
text-align: left;
}
html[data-reader-rail="bottom"] .node-outline-item::after {
grid-column: 3;
grid-row: 1;
align-self: center;
}
.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;
}
/* The motion button breathes on the relay cycle, so the chrome keeps time
with the wave it controls. Motion Governor mirrors the planned cycle from
the diagram SVG onto the document. */
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden]) #btn-motion .motion-control-dot {
animation: archify-motion-dot-breathe var(--flow-cycle-chrome, 6.4s) ease-in-out infinite;
}
@keyframes archify-motion-dot-breathe {
0%, 100% { opacity: 1; transform: scale(1); }
46% { opacity: 0.55; transform: scale(0.72); }
}
.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) {
--ambient-node-base-filter: drop-shadow(0 1px 1px rgba(17, 24, 39, 0.05)) drop-shadow(0 6px 12px rgba(17, 24, 39, 0.06));
filter: var(--ambient-node-base-filter);
}
html[data-preset="classic"][data-theme="dark"] .diagram-container > svg [data-node-id] > rect:not(.c-mask) {
--ambient-node-base-filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.35));
filter: var(--ambient-node-base-filter);
}
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;
}
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; }
/* Live plays one relay per cycle: a comet (halo, tail, head) leaves each
source when the signal reaches it, travels at one shared speed, leaves a
soft wake, and lands as a ripple while its target glows. Downstream edges
depart only after their source is reached, so the reader sees the path the
diagram describes rather than every edge shimmering at once. Sequence
messages relay in authored chronological order. Motion Governor plans the
timeline and its per-edge keyframes; this stylesheet owns appearance.
Original line language and markers remain static. Node entry is bounded to
one pass per document. */
svg[data-animation="trace"] {
--flow-cycle: 6.4s;
--flow-halo-peak: 0.17;
--flow-glow-peak: 0.5;
--flow-tail-peak: 0.42;
--flow-head-peak: 0.95;
--flow-wake-peak: 0.3;
--flow-ripple-peak: 0.65;
}
.ambient-edge-flow {
fill: none;
stroke: var(--arrow-emphasis);
stroke-linecap: round;
pointer-events: none;
display: none;
}
.ambient-flow-wake {
--flow-peak: var(--flow-wake-peak, 0.3);
stroke-width: 3.2;
stroke-dasharray: var(--flow-dasharray, none);
}
.ambient-flow-halo {
--flow-peak: var(--flow-halo-peak, 0.17);
stroke-width: 6;
stroke-dasharray: var(--flow-dash, 12px) calc(var(--flow-len, 0px) - var(--flow-dash, 12px));
}
/* A wide blurred streak centered on the head: this is the bloom that makes
the comet read as a traveling orb of light rather than a bright dash. */
.ambient-flow-glow {
--flow-peak: var(--flow-glow-peak, 0.5);
stroke-width: 9;
stroke-dasharray: var(--flow-dash, 12px) calc(var(--flow-len, 0px) - var(--flow-dash, 12px));
filter: blur(2.5px);
}
.ambient-flow-tail {
--flow-peak: var(--flow-tail-peak, 0.42);
stroke-width: 2.4;
stroke-dasharray: var(--flow-dasharray, var(--flow-dash, 20px) calc(var(--flow-len, 0px) - var(--flow-dash, 20px)));
}
.ambient-flow-head {
--flow-peak: var(--flow-head-peak, 0.95);
stroke: color-mix(in srgb, var(--arrow-emphasis) 40%, white);
stroke-width: 2.6;
stroke-dasharray: var(--flow-dash, 6px) calc(var(--flow-len, 0px) - var(--flow-dash, 6px));
}
.ambient-flow-ripple {
--flow-peak: var(--flow-ripple-peak, 0.65);
stroke: color-mix(in srgb, var(--arrow-emphasis) 55%, white);
stroke-width: 2;
}
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden]) .ambient-edge-flow {
display: inline;
animation-name: var(--flow-name, none);
animation-duration: var(--flow-cycle, 6.4s);
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-delay: var(--flow-delay, 0s);
/* Without backwards fill a delayed layer would sit at its natural opacity
(fully lit) until its departure arrives. */
animation-fill-mode: backwards;
}
/* Stream packets trail the comet head as smaller, dimmer beads. */
.ambient-flow-head.packet {
--flow-peak: calc(var(--flow-head-peak, 0.95) * 0.62);
stroke-width: 3.4;
}
/* Timeline and waterfall playhead: a crisp rule over a soft bloom. */
.ambient-flow-playhead {
--flow-peak: 0.9;
stroke: color-mix(in srgb, var(--arrow-emphasis) 55%, white);
stroke-width: 1.6;
}
.ambient-flow-playhead-glow {
--flow-peak: 0.32;
stroke-width: 10;
filter: blur(4px);
}
/* Waterfall spans fill in proportion to their measured time, then settle. */
.ambient-flow-fill {
--flow-peak: 0.5;
fill: color-mix(in srgb, var(--flow-fill, var(--arrow-emphasis)) 55%, white);
stroke: none;
transform-box: fill-box;
transform-origin: left center;
}
.ambient-flow-fill-failed { fill: var(--security-stroke); --flow-peak: 0.5; }
html[data-theme="light"] .ambient-flow-playhead { stroke: color-mix(in srgb, var(--arrow-emphasis) 70%, black); }
html[data-theme="light"] .ambient-flow-fill { --flow-peak: 0.42; fill: color-mix(in srgb, var(--flow-fill, var(--arrow-emphasis)) 70%, black); }
svg[data-preset="editorial"] .ambient-flow-fill { --flow-peak: 0.2; }
svg[data-preset="editorial"] .ambient-flow-playhead-glow { --flow-peak: 0.14; }
/* Each decorative flow follows its owning edge through tree visibility. */
.ambient-edge-flow[data-tree-hidden] { display: none !important; animation: none !important; }
svg[data-preset="blueprint"] .ambient-edge-flow { stroke-linecap: square; }
svg[data-preset="editorial"] { --flow-halo-peak: 0.07; --flow-glow-peak: 0.28; --flow-tail-peak: 0.2; --flow-head-peak: 0.7; --flow-wake-peak: 0.14; --flow-ripple-peak: 0.3; }
/* On light canvases the relay reads by saturation instead of bloom: the head
and ripple mix toward ink instead of white, and every layer's peak rises so
the wave survives the bright background. */
html[data-theme="light"] svg[data-animation="trace"] {
--flow-halo-peak: 0.22;
--flow-glow-peak: 0.4;
--flow-tail-peak: 0.5;
--flow-wake-peak: 0.34;
}
html[data-theme="light"] .ambient-flow-head {
stroke: color-mix(in srgb, var(--arrow-emphasis) 55%, black);
stroke-width: 2.8;
}
html[data-theme="light"] .ambient-flow-ripple {
stroke: color-mix(in srgb, var(--arrow-emphasis) 65%, black);
}
html[data-ambient-entry="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 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
html[data-ambient-entry="running"] svg[data-preset="signal-flow"][data-animation="trace"] [data-animate="node"] {
animation: archify-node-pulse 3.1s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
html[data-ambient-entry="running"] svg[data-preset="blueprint"][data-animation="trace"] [data-animate="node"] {
animation: archify-blueprint-node-pulse 3.8s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
html[data-ambient-entry="running"] svg[data-preset="editorial"][data-animation="trace"] [data-animate="node"] {
animation: archify-editorial-node-pulse 3.5s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
/* Each node glows in its planned windows: when the relay reaches it, while a
walk token rests on it, as a message lands, or as the playhead passes.
During the bounded entrance all three animations run; the entrance pulse is
listed after the repeating glow and wins on filter until it completes. */
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden]) svg[data-animation="trace"] [data-animate="node"] {
animation: var(--flow-node-name, none) var(--flow-cycle, 6.4s) linear infinite;
}
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden])[data-ambient-entry="running"] svg[data-animation="trace"] [data-animate="node"] {
animation: var(--flow-node-name, none) var(--flow-cycle, 6.4s) linear infinite,
archify-node-pulse 3.6s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden])[data-ambient-entry="running"] svg[data-preset="signal-flow"][data-animation="trace"] [data-animate="node"] {
animation: var(--flow-node-name, none) var(--flow-cycle, 6.4s) linear infinite,
archify-node-pulse 3.1s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden])[data-ambient-entry="running"] svg[data-preset="blueprint"][data-animation="trace"] [data-animate="node"] {
animation: var(--flow-node-name, none) var(--flow-cycle, 6.4s) linear infinite,
archify-blueprint-node-pulse 3.8s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden])[data-ambient-entry="running"] svg[data-preset="editorial"][data-animation="trace"] [data-animate="node"] {
animation: var(--flow-node-name, none) var(--flow-cycle, 6.4s) linear infinite,
archify-editorial-node-pulse 3.5s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
@keyframes archify-node-enter {
from { opacity: 0; }
to { opacity: 1; }
}
@media print {
.ambient-edge-flow { display: none !important; animation: none !important; }
svg[data-animation="trace"] [data-animate="node"] { animation: none !important; }
}
@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) {
.ambient-edge-flow { display: none !important; animation: none !important; }
html[data-preset="signal-flow"] .diagram-container::before {
animation: none !important;
}
.pulse-dot { animation: none !important; }
.motion-control-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; }
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>System of record for agents, Norwegian Armed Forces edition</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.credit":"Find Archify useful? A star on GitHub helps","viewer.guide.star":"Star","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.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":"ROUTE","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 640" role="img" lang="en" aria-labelledby="archify-diagram-title archify-diagram-description" data-animation="trace" data-preset="classic" data-quality-profile="showcase" data-motion-choreography="stream">
<title id="archify-diagram-title">System of record for agents, Norwegian Armed Forces edition</title>
<desc id="archify-diagram-description">A data-flow 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)" />
<!-- Data Stages -->
<rect data-graph-role="structural-frame" data-composition-frame-kind="stage" data-composition-frame-id="0" x="16" y="46" width="168" height="520" rx="10" class="c-lane" stroke-width="1"/>
<text x="100" y="68" class="t-dim" font-size="9" font-weight="600" text-anchor="middle">01 / Actors</text>
<rect data-graph-role="structural-frame" data-composition-frame-kind="stage" data-composition-frame-id="1" x="231" y="46" width="168" height="520" rx="10" class="c-lane" stroke-width="1"/>
<text x="315" y="68" class="t-dim" font-size="9" font-weight="600" text-anchor="middle">02 / What is allowed</text>
<rect data-graph-role="structural-frame" data-composition-frame-kind="stage" data-composition-frame-id="2" x="446" y="46" width="168" height="520" rx="10" class="c-lane" stroke-width="1"/>
<text x="530" y="68" class="t-dim" font-size="9" font-weight="600" text-anchor="middle">03 / What is true</text>
<rect data-graph-role="structural-frame" data-composition-frame-kind="stage" data-composition-frame-id="3" x="661" y="46" width="168" height="520" rx="10" class="c-lane" stroke-width="1"/>
<text x="745" y="68" class="t-dim" font-size="9" font-weight="600" text-anchor="middle">04 / What does it mean</text>
<rect data-graph-role="structural-frame" data-composition-frame-kind="stage" data-composition-frame-id="4" x="876" y="46" width="168" height="520" rx="10" class="c-lane" stroke-width="1"/>
<text x="960" y="68" class="t-dim" font-size="9" font-weight="600" text-anchor="middle">05 / Did it work</text>
<!-- Flow paths -->
<path data-edge-from="operator" data-edge-to="agent" data-edge-label="task and mandate" data-edge-key="0" data-edge-id="task" data-composition-points="100,300;100,470" d="M 100 300 L 100 470" class="a-default" data-animate="edge" style="--step:0" stroke-width="1.4" marker-end="url(#arrowhead)"/>
<path data-edge-from="agent" data-edge-to="icam" data-edge-label="identity" data-edge-key="1" data-edge-id="identity" data-composition-points="156,499;207.5,499;207.5,271;259,271" d="M 156 499 L 207.5 499 L 207.5 271 L 259 271" class="a-security" data-animate="edge" style="--step:1" stroke-width="1.4" marker-end="url(#arrowhead-security)"/>
<path data-edge-from="icam" data-edge-to="pep" data-edge-label="attributes" data-edge-key="2" data-edge-id="attributes" data-composition-points="315,300;315,470" d="M 315 300 L 315 470" class="a-security" data-animate="edge" style="--step:2" stroke-width="1.4" marker-end="url(#arrowhead-security)"/>
<path data-edge-from="pep" data-edge-to="fddp" data-edge-label="permitted query" data-edge-key="3" data-edge-id="permitted" data-composition-points="371,499;422.5,499;422.5,385;474,385" d="M 371 499 L 422.5 499 L 422.5 385 L 474 385" class="a-security" data-animate="edge" style="--step:3" stroke-width="1.4" marker-end="url(#arrowhead-security)"/>
<path data-edge-from="fddp" data-edge-to="ontology" data-edge-label="data with meaning" data-edge-key="4" data-edge-id="meaning" data-composition-points="586,385;637.5,385;637.5,499;689,499" d="M 586 385 L 637.5 385 L 637.5 499 L 689 499" class="a-emphasis" data-animate="edge" style="--step:4" stroke-width="1.8" marker-end="url(#arrowhead-emphasis)"/>
<path data-edge-from="ontology" data-edge-to="answer" data-edge-label="grounded reply" data-edge-key="5" data-edge-id="grounded" data-composition-points="801,499;904,499" d="M 801 499 L 904 499" class="a-emphasis" data-animate="edge" style="--step:5" stroke-width="1.8" marker-end="url(#arrowhead-emphasis)"/>
<path data-edge-from="fddp" data-edge-to="trace" data-edge-label="origin" data-edge-key="6" data-edge-id="origin" data-composition-points="530,356;530,186" d="M 530 356 L 530 186" class="a-default" data-animate="edge" style="--step:6" stroke-width="1.4" marker-end="url(#arrowhead)"/>
<path data-edge-from="trace" data-edge-to="evaluation" data-edge-label="evidence used" data-edge-key="7" data-edge-id="evidence" data-composition-points="586,157;960,157;960,242" d="M 586 157 L 960 157 L 960 242" class="a-default" data-animate="edge" style="--step:7" stroke-width="1.4" marker-end="url(#arrowhead)"/>
<path data-edge-from="answer" data-edge-to="evaluation" data-edge-label="correction" data-edge-key="8" data-edge-id="correction" data-composition-points="960,470;960,300" d="M 960 470 L 960 300" class="a-dashed" data-animate="edge" style="--step:8" stroke-width="1.4" marker-end="url(#arrowhead-dashed)"/>
<path data-edge-from="evaluation" data-edge-to="ontology" data-edge-label="updated meaning" data-edge-key="9" data-edge-id="updated" data-composition-points="904,271;745,271;745,470" d="M 904 271 L 745 271 L 745 470" class="a-dashed" data-animate="edge" style="--step:9" stroke-width="1.4" marker-end="url(#arrowhead-dashed)"/>
<!-- Nodes -->
<g id="node-operator" data-node-id="operator" data-node-label="Operator" tabindex="0" role="button" aria-label="Focus Operator, asks, mandates, 01 / Actors" aria-pressed="false" data-node-kind="frontend" data-node-sublabel="asks, mandates" data-node-context="01 / Actors">
<title>Operator · asks, mandates · 01 / Actors</title>
<rect x="44" y="242" width="112" height="58" rx="6" class="c-mask"/>
<rect x="44" y="242" width="112" height="58" rx="6" class="c-frontend" data-animate="node" style="--step:0" stroke-width="1.5"/>
<g aria-hidden="true" data-semantic-sigil="frontend" class="semantic-sigil s-frontend" transform="translate(50 248) 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="100" y="263" class="t-primary" font-size="10" font-weight="600" text-anchor="middle">Operator</text>
<text data-detail="context" x="100" y="279" class="t-muted" font-size="7" text-anchor="middle">asks, mandates</text>
</g>
<g id="node-agent" data-node-id="agent" data-node-label="AI agent" tabindex="0" role="button" aria-label="Focus AI agent, own identity, 01 / Actors" aria-pressed="false" data-node-kind="backend" data-node-sublabel="own identity" data-node-tag="new actor" data-node-context="01 / Actors">
<title>AI agent · own identity · 01 / Actors · new actor</title>
<rect x="44" y="470" width="112" height="58" rx="6" class="c-mask"/>
<rect x="44" y="470" width="112" height="58" rx="6" class="c-backend" data-animate="node" style="--step:1" stroke-width="1.5"/>
<g aria-hidden="true" data-semantic-sigil="backend" class="semantic-sigil s-backend" transform="translate(50 476) scale(0.6875)">
<path d="M6 3 3 8l3 5M10 3l3 5-3 5"/>
</g>
<text data-node-label="" data-detail-anchor="" x="100" y="491" class="t-primary" font-size="10" font-weight="600" text-anchor="middle">AI agent</text>
<text data-detail="context" x="100" y="507" class="t-muted" font-size="7" text-anchor="middle">own identity</text>
<text data-detail="fine" x="100" y="517" class="t-backend" font-size="7" text-anchor="middle">new actor</text>
</g>
<g id="node-icam" data-node-id="icam" data-node-label="ICAM / ForsvarsID" tabindex="0" role="button" aria-label="Focus ICAM / ForsvarsID, who asks, mandate, 02 / What is allowed" aria-pressed="false" data-node-kind="security" data-node-sublabel="who asks, mandate" data-node-tag="2.2" data-node-context="02 / What is allowed">
<title>ICAM / ForsvarsID · who asks, mandate · 02 / What is allowed · 2.2</title>
<rect x="259" y="242" width="112" height="58" rx="6" class="c-mask"/>
<rect x="259" y="242" width="112" height="58" rx="6" class="c-security" data-animate="node" style="--step:2" stroke-width="1.5"/>
<g aria-hidden="true" data-semantic-sigil="security" class="semantic-sigil s-security" transform="translate(265 243) scale(0.375)">
<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="315" y="263" class="t-primary" font-size="10" font-weight="600" text-anchor="middle">ICAM / ForsvarsID</text>
<text data-detail="context" x="315" y="279" class="t-muted" font-size="7" text-anchor="middle">who asks, mandate</text>
<text data-detail="fine" x="315" y="289" class="t-security" font-size="7" text-anchor="middle">2.2</text>
</g>
<g id="node-pep" data-node-id="pep" data-node-label="PEP (policy)" tabindex="0" role="button" aria-label="Focus PEP (policy), ABAC, policy-as-code, 02 / What is allowed" aria-pressed="false" data-node-kind="security" data-node-sublabel="ABAC, policy-as-code" data-node-tag="2.4 and 3.1" data-node-context="02 / What is allowed">
<title>PEP (policy) · ABAC, policy-as-code · 02 / What is allowed · 2.4 and 3.1</title>
<rect x="259" y="470" width="112" height="58" rx="6" class="c-mask"/>
<rect x="259" y="470" width="112" height="58" 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(265 476) 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="315" y="491" class="t-primary" font-size="10" font-weight="600" text-anchor="middle">PEP (policy)</text>
<text data-detail="context" x="315" y="507" class="t-muted" font-size="7" text-anchor="middle">ABAC, policy-as-code</text>
<text data-detail="fine" x="315" y="517" class="t-security" font-size="7" text-anchor="middle">2.4 and 3.1</text>
</g>
<g id="node-fddp" data-node-id="fddp" data-node-label="FDDP" tabindex="0" role="button" aria-label="Focus FDDP, data products, 03 / What is true" aria-pressed="false" data-node-kind="database" data-node-sublabel="data products" data-node-tag="FDDP node" data-node-context="03 / What is true">
<title>FDDP · data products · 03 / What is true · FDDP node</title>
<rect x="474" y="356" width="112" height="58" rx="6" class="c-mask"/>
<rect x="474" y="356" width="112" height="58" 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(480 362) 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="530" y="377" class="t-primary" font-size="10" font-weight="600" text-anchor="middle">FDDP</text>
<text data-detail="context" x="530" y="393" class="t-muted" font-size="7" text-anchor="middle">data products</text>
<text data-detail="fine" x="530" y="403" class="t-database" font-size="7" text-anchor="middle">FDDP node</text>
</g>
<g id="node-trace" data-node-id="trace" data-node-label="Traceability" tabindex="0" role="button" aria-label="Focus Traceability, evidence used, 03 / What is true" aria-pressed="false" data-node-kind="database" data-node-sublabel="evidence used" data-node-tag="3.5" data-node-context="03 / What is true">
<title>Traceability · evidence used · 03 / What is true · 3.5</title>
<rect x="474" y="128" width="112" height="58" rx="6" class="c-mask"/>
<rect x="474" y="128" width="112" height="58" rx="6" class="c-database" data-animate="node" style="--step:7" stroke-width="1.5"/>
<g aria-hidden="true" data-semantic-sigil="database" class="semantic-sigil s-database" transform="translate(480 134) 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="530" y="149" class="t-primary" font-size="10" font-weight="600" text-anchor="middle">Traceability</text>
<text data-detail="context" x="530" y="165" class="t-muted" font-size="7" text-anchor="middle">evidence used</text>
<text data-detail="fine" x="530" y="175" class="t-database" font-size="7" text-anchor="middle">3.5</text>
</g>
<g id="node-ontology" data-node-id="ontology" data-node-label="Ontology" tabindex="0" role="button" aria-label="Focus Ontology, gives data meaning, 04 / What does it mean" aria-pressed="false" data-node-kind="backend" data-node-sublabel="gives data meaning" data-node-tag="7.1 and 7.3" data-node-context="04 / What does it mean">
<title>Ontology · gives data meaning · 04 / What does it mean · 7.1 and 7.3</title>
<rect x="689" y="470" width="112" height="58" rx="6" class="c-mask"/>
<rect x="689" y="470" width="112" height="58" rx="6" class="c-backend" data-animate="node" style="--step:5" stroke-width="1.5"/>
<g aria-hidden="true" data-semantic-sigil="backend" class="semantic-sigil s-backend" transform="translate(695 476) scale(0.6875)">
<path d="M6 3 3 8l3 5M10 3l3 5-3 5"/>
</g>
<text data-node-label="" data-detail-anchor="" x="745" y="491" class="t-primary" font-size="10" font-weight="600" text-anchor="middle">Ontology</text>
<text data-detail="context" x="745" y="507" class="t-muted" font-size="7" text-anchor="middle">gives data meaning</text>
<text data-detail="fine" x="745" y="517" class="t-backend" font-size="7" text-anchor="middle">7.1 and 7.3</text>
</g>
<g id="node-answer" data-node-id="answer" data-node-label="Answer and action" tabindex="0" role="button" aria-label="Focus Answer and action, decision, picture, 05 / Did it work" aria-pressed="false" data-node-kind="frontend" data-node-sublabel="decision, picture" data-node-context="05 / Did it work">
<title>Answer and action · decision, picture · 05 / Did it work</title>
<rect x="904" y="470" width="112" height="58" rx="6" class="c-mask"/>
<rect x="904" y="470" width="112" height="58" rx="6" class="c-frontend" data-animate="node" style="--step:6" stroke-width="1.5"/>
<g aria-hidden="true" data-semantic-sigil="frontend" class="semantic-sigil s-frontend" transform="translate(910 476) 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="960" y="501" class="t-primary" font-size="10" font-weight="600" text-anchor="middle">Answer and action</text>
<text data-detail="context" x="960" y="514.4" class="t-muted" font-size="7" text-anchor="middle">decision, picture</text>
</g>
<g id="node-evaluation" data-node-id="evaluation" data-node-label="Evaluation" tabindex="0" role="button" aria-label="Focus Evaluation, correction fed back, 05 / Did it work" aria-pressed="false" data-node-kind="backend" data-node-sublabel="correction fed back" data-node-tag="next step" data-node-context="05 / Did it work">
<title>Evaluation · correction fed back · 05 / Did it work · next step</title>
<rect x="904" y="242" width="112" height="58" rx="6" class="c-mask"/>
<rect x="904" y="242" width="112" height="58" rx="6" class="c-backend" data-animate="node" style="--step:8" stroke-width="1.5"/>
<g aria-hidden="true" data-semantic-sigil="backend" class="semantic-sigil s-backend" transform="translate(910 248) scale(0.6875)">
<path d="M6 3 3 8l3 5M10 3l3 5-3 5"/>
</g>
<text data-node-label="" data-detail-anchor="" x="960" y="263" class="t-primary" font-size="10" font-weight="600" text-anchor="middle">Evaluation</text>
<text data-detail="context" x="960" y="279" class="t-muted" font-size="7" text-anchor="middle">correction fed back</text>
<text data-detail="fine" x="960" y="289" class="t-backend" font-size="7" text-anchor="middle">next step</text>
</g>
<!-- Flow labels -->
<g data-detail="context" data-edge-from="operator" data-edge-to="agent" data-edge-label="task and mandate" data-edge-key="0" data-edge-id="task">
<rect x="54.8" y="304" width="90.4" height="16" rx="4" class="c-mask"/>
<text x="100" y="315" class="t-edge-default" font-size="8" text-anchor="middle">task and mandate</text>
</g>
<g data-detail="context" data-edge-from="agent" data-edge-to="icam" data-edge-label="identity" data-edge-key="1" data-edge-id="identity">
<rect x="181.9" y="364" width="51.2" height="16" rx="4" class="c-mask"/>
<text x="207.5" y="375" class="t-edge-security" font-size="8" text-anchor="middle">identity</text>
</g>
<g data-detail="context" data-edge-from="icam" data-edge-to="pep" data-edge-label="attributes" data-edge-key="2" data-edge-id="attributes">
<rect x="284.5" y="304" width="61" height="16" rx="4" class="c-mask"/>
<text x="315" y="315" class="t-edge-security" font-size="8" text-anchor="middle">attributes</text>
</g>
<g data-detail="context" data-edge-from="pep" data-edge-to="fddp" data-edge-label="permitted query" data-edge-key="3" data-edge-id="permitted">
<rect x="379.75" y="421" width="85.5" height="16" rx="4" class="c-mask"/>
<text x="422.5" y="432" class="t-edge-security" font-size="8" text-anchor="middle">permitted query</text>
</g>
<g data-detail="context" data-edge-from="fddp" data-edge-to="ontology" data-edge-label="data with meaning" data-edge-key="4" data-edge-id="meaning">
<rect x="589.85" y="421" width="95.3" height="16" rx="4" class="c-mask"/>
<text x="637.5" y="432" class="t-edge-emphasis" font-size="8" text-anchor="middle">data with meaning</text>
</g>
<g data-detail="context" data-edge-from="ontology" data-edge-to="answer" data-edge-label="grounded reply" data-edge-key="5" data-edge-id="grounded">
<rect x="812.2" y="475" width="80.6" height="16" rx="4" class="c-mask"/>
<text x="852.5" y="486" class="t-edge-emphasis" font-size="8" text-anchor="middle">grounded reply</text>
</g>
<g data-detail="context" data-edge-from="fddp" data-edge-to="trace" data-edge-label="origin" data-edge-key="6" data-edge-id="origin">
<rect x="509.3" y="335" width="41.4" height="16" rx="4" class="c-mask"/>
<text x="530" y="346" class="t-edge-default" font-size="8" text-anchor="middle">origin</text>
</g>
<g data-detail="context" data-edge-from="trace" data-edge-to="evaluation" data-edge-label="evidence used" data-edge-key="7" data-edge-id="evidence">
<rect x="922.15" y="178.5" width="75.7" height="16" rx="4" class="c-mask"/>
<text x="960" y="189.5" class="t-edge-default" font-size="8" text-anchor="middle">evidence used</text>
</g>
<g data-detail="context" data-edge-from="answer" data-edge-to="evaluation" data-edge-label="correction" data-edge-key="8" data-edge-id="correction">
<rect x="929.5" y="449" width="61" height="16" rx="4" class="c-mask"/>
<text x="960" y="460" class="t-edge-dashed" font-size="8" text-anchor="middle">correction</text>
</g>
<g data-detail="context" data-edge-from="evaluation" data-edge-to="ontology" data-edge-label="updated meaning" data-edge-key="9" data-edge-id="updated">
<rect x="702.25" y="349.5" width="85.5" height="16" rx="4" class="c-mask"/>
<text x="745" y="360.5" class="t-edge-dashed" font-size="8" text-anchor="middle">updated meaning</text>
</g>
<!-- Legend -->
<g data-legend="" data-legend-bridge="">
<text x="40" y="584" class="t-primary" font-size="12" font-weight="650">Legend</text>
<g data-legend-semantic-kind="emphasis" data-legend-x="40" data-legend-baseline="604" data-legend-width="99">
<path d="M 40 601 L 74 601" class="a-emphasis" stroke-width="1.8" marker-end="url(#arrowhead-emphasis)"/>
<text x="83" y="604" class="t-muted" font-size="10" font-weight="500">Main flow</text>
</g>
<g data-legend-semantic-kind="security" data-legend-x="161" data-legend-baseline="604" data-legend-width="161">
<path d="M 161 601 L 195 601" class="a-security" stroke-width="1.4" marker-end="url(#arrowhead-security)"/>
<text x="204" y="604" class="t-muted" font-size="10" font-weight="500">Identity and policy</text>
</g>
<g data-legend-semantic-kind="dashed" data-legend-x="344" data-legend-baseline="604" data-legend-width="124">
<path d="M 344 601 L 378 601" class="a-dashed" stroke-width="1.4" marker-end="url(#arrowhead-dashed)"/>
<text x="387" y="604" class="t-muted" font-size="10" font-weight="500">Learning loop</text>
</g>
<g data-legend-semantic-kind="database" data-legend-kind="database" data-legend-label="Storage in FDDP" data-legend-x="490" data-legend-baseline="604" data-legend-width="136">
<rect x="490" y="596" width="14" height="9" rx="2" class="c-database" stroke-width="1"/>
<text x="512" y="604" class="t-muted" font-size="10" font-weight="500">Storage in FDDP</text>
</g>
<g data-legend-semantic-kind="default" data-legend-x="648" data-legend-baseline="604" data-legend-width="124">
<path d="M 648 601 L 682 601" class="a-default" stroke-width="1.4" marker-end="url(#arrowhead)"/>
<text x="691" y="604" class="t-muted" font-size="10" font-weight="500">Ordinary flow</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>
<a class="diagram-guide-credit" href="https://github.com/tt-a1i/archify" target="_blank" rel="noopener noreferrer"><span>Find Archify useful? A star on GitHub helps</span><span class="diagram-guide-credit-link"><span class="diagram-guide-star" aria-hidden="true"></span>Star <span aria-hidden="true">&#8599;</span></span></a>
</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)">ROUTE</button>
<button id="btn-overview-map" type="button" aria-label="Open semantic radar" aria-expanded="false" aria-controls="overview-map" title="Semantic radar (M)">MAP</button>
<button id="btn-semantic-lens" type="button" aria-label="Open semantic lens" aria-haspopup="dialog" aria-expanded="false" aria-pressed="false" aria-controls="semantic-lens" title="Semantic lens (L)">LENS</button>
<button id="btn-node-finder" type="button" aria-label="Find a node" aria-haspopup="dialog" aria-expanded="false" aria-controls="node-finder" title="Find a node (/)" data-node-finder-trigger><span class="diagram-nav-icon find" aria-hidden="true"></span></button>
<button id="btn-diagram-guide" type="button" aria-label="Open diagram guide" aria-haspopup="dialog" aria-expanded="false" aria-controls="diagram-guide" title="Diagram guide (?)"><span class="diagram-nav-icon guide" aria-hidden="true"></span></button>
<button data-view="out" type="button" aria-label="Zoom out" title="Zoom out (-)"><span class="diagram-nav-icon minus" aria-hidden="true"></span></button>
<button data-view="reset" type="button" aria-label="Reset diagram view" title="Reset view (0)"><span class="diagram-nav-detail" data-view-detail hidden>READ</span><span class="diagram-nav-percent" data-view-percent>100%</span></button>
<button data-view="in" type="button" aria-label="Zoom in" title="Zoom in (+)"><span class="diagram-nav-icon plus" aria-hidden="true"></span></button>
</div>
</div>
<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>
<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 readerLink(state) {
var template = document.documentElement.getAttribute('data-reading-link-template');
if (template === null) return location.href.replace(/#.*$/, '') + '#' + state;
if (!template || template.length > 4096 || template.split('{state}').length !== 2) return null;
try {
var url = new URL(template.replace('{state}', encodeURIComponent(state)));
if (['http:', 'https:', 'file:'].indexOf(url.protocol) === -1 || url.username || url.password) return null;
return url.href;
} catch (_) { return null; }
}
function copyReaderText(value) {
function fallbackCopy() {
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;
}
var policy = document.permissionsPolicy || document.featurePolicy;
var allowed = !policy || typeof policy.allowsFeature !== 'function' || policy.allowsFeature('clipboard-write');
if (allowed && navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
try { return Promise.resolve(navigator.clipboard.writeText(value)).then(function () { return true; }).catch(fallbackCopy); }
catch (_) {}
}
return Promise.resolve(fallbackCopy());
}
function hasDrawableGeometry(element) {
if (!element || element.hasAttribute('data-ambient-flow-overlay')) 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) {
if (geometry.hasAttribute('data-ambient-flow-overlay')) return false;
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.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return;
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; }
if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) 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) {
Array.prototype.forEach.call(clone.querySelectorAll('[data-ambient-flow-overlay]'), function (el) {
el.remove();
});
// Route interaction rewrites CSS declarations through CSSOM. Normalize
// authored motion delays on the clone so exported bytes do not depend
// on whether the reader traced a route before downloading.
Array.prototype.forEach.call(clone.querySelectorAll('[data-animate]'), function (el) {
var step = el.style.getPropertyValue('--step');
if (step) el.setAttribute('style', el.style.cssText);
});
// 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-reader-legend-corner]'), function (el) {
el.removeAttribute('data-reader-legend-corner');
el.style.removeProperty('--archify-reader-legend-transform');
});
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) 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');
});
// A downloaded tree is always the complete hierarchy: collapsing is a
// reading aid, never a silent change to what the artifact contains.
clone.removeAttribute('data-tree-any-collapsed');
Array.prototype.forEach.call(clone.querySelectorAll('[data-tree-hidden], [data-tree-collapsed]'), function (el) {
el.removeAttribute('data-tree-hidden');
el.removeAttribute('data-tree-collapsed');
});
Array.prototype.forEach.call(clone.querySelectorAll('[data-tree-toggle]'), function (el) {
el.setAttribute('aria-expanded', 'true');
el.setAttribute('aria-label', el.getAttribute('data-tree-label-collapse') || '');
});
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.hasAttribute('data-tree-any-collapsed') &&
!clone.style.getPropertyValue('transform') &&
!clone.style.getPropertyValue('clip-path') &&
clone.querySelectorAll('[data-ambient-flow-overlay], [data-reader-legend-corner], [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], [data-tree-hidden], [data-tree-collapsed]').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;
// Keep the export-owned activation inside its existing control boundary.
// The synthetic download click must not dismiss the reader selection.
menu.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);
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 geometry = element;
if (element.getAttribute('data-motion-path')) {
geometry = element.cloneNode(false);
geometry.setAttribute('d', element.getAttribute('data-motion-path'));
}
var length;
try { length = geometry.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, geometry.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 sourceUrl = null;
var backgroundImage = null;
var stream = null;
var recorder = null;
var raf = 0;
var durationTimer = 0;
var flushTimer = 0;
var settled = false;
function finish(error, blob) {
if (settled) return;
// 先锁定终态:停止录制和释放流可能产生晚到的编码事件。
settled = true;
cancelAnimationFrame(raf);
clearTimeout(durationTimer);
clearTimeout(flushTimer);
if (backgroundImage) {
backgroundImage.onload = null;
backgroundImage.onerror = null;
}
if (recorder) {
recorder.ondataavailable = null;
recorder.onerror = null;
recorder.onstop = null;
try { if (recorder.state !== 'inactive') recorder.stop(); } catch (_) {}
}
if (stream) stream.getTracks().forEach(function (track) { track.stop(); });
if (sourceUrl) URL.revokeObjectURL(sourceUrl);
if (blob) resolve(blob);
else reject(error);
}
try {
sourceUrl = URL.createObjectURL(new Blob([data.svgString], { type: 'image/svg+xml;charset=utf-8' }));
backgroundImage = new Image();
backgroundImage.onload = function () {
if (settled) return;
try {
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');
if (!ctx) throw exportError('viewer.export.error.contextUnavailable', { label: 'WebM' });
stream = canvas.captureStream(fps);
var mime = motionMimeType();
recorder = new MediaRecorder(stream, { mimeType: mime, videoBitsPerSecond: 6000000 });
var chunks = [];
var startedAt = performance.now();
function draw(now) {
if (settled) return;
try {
var elapsed = Math.max(0, ((Number(now) || performance.now()) - startedAt) / 1000);
drawMotionFrame(ctx, backgroundImage, motionScene, elapsed);
raf = requestAnimationFrame(draw);
} catch (error) { finish(error); }
}
recorder.ondataavailable = function (event) {
if (!settled && event.data && event.data.size) chunks.push(event.data);
};
recorder.onerror = function (event) {
finish(event.error || exportError('viewer.export.error.mediaRecorder'));
};
recorder.onstop = function () {
if (settled) return;
try {
var blob = new Blob(chunks, { type: recorder.mimeType || mime });
if (!blob.size) throw exportError('viewer.export.error.emptyWebm');
finish(null, blob);
} catch (error) { finish(error); }
};
drawMotionFrame(ctx, backgroundImage, motionScene, 0);
recorder.start(250);
if (settled) return;
startedAt = performance.now();
raf = requestAnimationFrame(draw);
durationTimer = setTimeout(function () {
if (settled || recorder.state === 'inactive') return;
// 保留最佳努力的编码器 flush;失败后仍通过 stop 收集最后的片段。
try { recorder.requestData(); } catch (_) {}
if (settled) return;
flushTimer = setTimeout(function () {
if (settled) return;
try { if (recorder.state !== 'inactive') recorder.stop(); }
catch (error) { finish(error); }
}, 120);
}, duration);
} catch (error) { finish(error); }
};
backgroundImage.onerror = function () {
finish(exportError('viewer.export.error.webmBackground'));
};
backgroundImage.src = sourceUrl;
} catch (error) { finish(error); }
});
}
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.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return;
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; }
if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) 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 entryStarted = false;
var entryPending = new Set();
var flowCount = 0;
// Runtime decoration carries geometry and tree visibility only. Keep each
// flow beside its authored edge so transforms, clipping and crossover
// paint order apply.
// Each renderer may declare a choreography that matches what its diagram
// means. Relay (the default) sends a comet from each source when the
// signal reaches it; stream adds a packet train for continuous data; walk
// moves one token along the main state path; sequence plays messages in
// authored order; sweep runs a playhead along a timeline; replay fills
// waterfall spans in proportion to their measured time. Motion Governor
// plans the timeline and per-element keyframes; viewer.css owns looks.
var SVG_NS = 'http://www.w3.org/2000/svg';
var choreography = svg ? (svg.getAttribute('data-motion-choreography') || 'relay') : 'relay';
var RELAY = {
speed: 280,
minTravel: 0.42,
maxTravel: 1.5,
handoff: 0.1,
dwell: 0.55,
maxSpan: 9,
land: 0.9,
rest: 1.4,
minCycle: 3.6
};
function stepOf(element, fallback) {
var step = parseFloat(element && element.style ? element.style.getPropertyValue('--step') : '');
return Number.isFinite(step) ? step : fallback;
}
function byStep(a, b) { return a.step - b.step || a.index - b.index; }
function addWindow(windows, id, start, end, strong) {
if (!id) return;
(windows[id] = windows[id] || []).push({ start: start, end: end, strong: !!strong });
}
function planRelay(records, windows) {
var arrive = Object.create(null);
var seeded = Object.create(null);
var targets = Object.create(null);
records.forEach(function (record) { if (record.from !== record.to) targets[record.to] = true; });
function seed(id) {
if (arrive[id] !== undefined) return;
arrive[id] = 0;
seeded[id] = true;
}
function relax() {
// Earliest arrival over a few dozen edges; a scan per settle keeps
// cycles and fan-in deterministic without a priority queue.
var settled = Object.create(null);
for (;;) {
var next = null;
Object.keys(arrive).forEach(function (id) {
if (!settled[id] && (next === null || arrive[id] < arrive[next])) next = id;
});
if (next === null) return;
settled[next] = true;
var depart = arrive[next] + (seeded[next] ? 0 : RELAY.handoff);
records.forEach(function (record) {
if (record.from !== next) return;
var at = depart + record.travel;
if (arrive[record.to] === undefined || at < arrive[record.to]) arrive[record.to] = at;
});
}
}
records.forEach(function (record) { if (!targets[record.from]) seed(record.from); });
relax();
for (;;) {
// A closed cycle has no entry; start it at its earliest authored edge.
var orphan = records.filter(function (record) { return arrive[record.from] === undefined; }).sort(byStep)[0];
if (!orphan) break;
seed(orphan.from);
relax();
}
records.forEach(function (record) {
record.depart = arrive[record.from] + (seeded[record.from] ? 0 : RELAY.handoff);
record.active = true;
});
Object.keys(arrive).forEach(function (id) { addWindow(windows, id, arrive[id], arrive[id]); });
}
function planWalk(records, windows) {
// A state machine is in one state at a time: follow the first authored
// transition to an unvisited state and let the current state stay lit.
var targets = Object.create(null);
records.forEach(function (record) { if (record.from !== record.to) targets[record.to] = true; });
var ordered = records.slice().sort(byStep);
var start = (ordered.find(function (record) { return !targets[record.from]; }) || ordered[0]).from;
var visited = Object.create(null);
var current = start;
var clock = 0;
var enteredAt = 0;
visited[start] = true;
for (var hops = 0; hops < 16; hops += 1) {
var next = ordered.find(function (record) { return record.from === current && !visited[record.to]; });
if (!next) break;
next.active = true;
next.depart = clock + RELAY.dwell;
addWindow(windows, current, enteredAt, next.depart, true);
clock = next.depart + next.travel;
enteredAt = clock;
current = next.to;
visited[current] = true;
}
addWindow(windows, current, enteredAt, enteredAt + 1.2, true);
}
function planSequence(records, windows) {
var clock = 0;
records.slice().sort(byStep).forEach(function (record) {
record.active = true;
record.depart = clock;
clock += record.travel + RELAY.handoff;
addWindow(windows, record.from, record.depart, record.depart);
addWindow(windows, record.to, record.depart + record.travel, record.depart + record.travel);
});
}
function scalePlan(records, windows, span) {
var scale = span > RELAY.maxSpan ? RELAY.maxSpan / span : 1;
if (scale === 1) return span;
records.forEach(function (record) {
if (!record.active) return;
record.depart *= scale;
record.travel *= scale;
});
Object.keys(windows).forEach(function (id) {
windows[id].forEach(function (window) { window.start *= scale; window.end *= scale; });
});
return span * scale;
}
function nodeShapes() {
var shapes = Object.create(null);
Array.prototype.forEach.call(svg.querySelectorAll('[data-animate="node"]'), function (shape) {
var wrapper = shape.closest('[data-node-id]');
var id = wrapper ? wrapper.getAttribute('data-node-id') : '';
if (id) (shapes[id] = shapes[id] || []).push(shape);
});
return shapes;
}
function sceneBounds(elements) {
var box = null;
elements.forEach(function (element) {
var b = null;
try { b = element.getBBox(); } catch (_) {}
if (!b || !(b.width || b.height)) return;
if (!box) box = { x: b.x, y: b.y, right: b.x + b.width, bottom: b.y + b.height };
else {
box.x = Math.min(box.x, b.x); box.y = Math.min(box.y, b.y);
box.right = Math.max(box.right, b.x + b.width); box.bottom = Math.max(box.bottom, b.y + b.height);
}
});
return box;
}
function overlay(tag, cls, attrs, parent, before) {
var element = document.createElementNS(SVG_NS, tag);
Object.keys(attrs).forEach(function (name) { element.setAttribute(name, attrs[name]); });
element.setAttribute('class', 'ambient-edge-flow ' + cls);
element.setAttribute('data-ambient-flow-overlay', 'true');
element.setAttribute('aria-hidden', 'true');
parent.insertBefore(element, before || null);
return element;
}
// Timeline and waterfall share a playhead: a lit vertical rule and its
// bloom travel the time axis linearly, because time is the subject.
function addPlayhead(box, x0, x1, lead, sweep, extras) {
var anchor = svg.querySelector('[data-node-id]');
var parent = anchor ? anchor.parentNode : svg;
var attrs = { x1: x0.toFixed(1), x2: x0.toFixed(1), y1: (box.y - 10).toFixed(1), y2: (box.bottom + 10).toFixed(1) };
['playhead-glow', 'playhead'].forEach(function (kind) {
var line = overlay('line', 'ambient-flow-' + kind, attrs, parent, anchor);
line.style.setProperty('--flow-name', 'archify-motion-playhead');
extras.push(line);
});
extras.playhead = { distance: x1 - x0, lead: lead, sweep: sweep };
}
function planSweep(windows, extras) {
var events = Array.prototype.filter.call(svg.querySelectorAll('[data-node-id][data-timeline-x]'), function (node) {
return Number.isFinite(parseFloat(node.getAttribute('data-timeline-x')));
});
if (!events.length) return 0;
var xs = events.map(function (node) { return parseFloat(node.getAttribute('data-timeline-x')); });
var x0 = Math.min.apply(null, xs), x1 = Math.max.apply(null, xs);
var lead = 0.35;
var sweep = Math.min(7, Math.max(3.2, events.length * 0.6));
events.forEach(function (node, index) {
var at = lead + (x1 > x0 ? (xs[index] - x0) / (x1 - x0) : 0) * sweep;
addWindow(windows, node.getAttribute('data-node-id'), at, at + 0.35, true);
});
var box = sceneBounds(Array.prototype.slice.call(svg.querySelectorAll('[data-timeline-lane]')));
if (box && x1 > x0) addPlayhead(box, x0, x1, lead, sweep, extras);
return lead + sweep + 0.6;
}
function planReplay(windows, extras) {
var spans = [];
Array.prototype.forEach.call(svg.querySelectorAll('[data-node-id][data-waterfall-start]'), function (node) {
var start = parseFloat(node.getAttribute('data-waterfall-start'));
var end = parseFloat(node.getAttribute('data-waterfall-end'));
var bar = node.querySelector('[data-animate="node"]');
if (!bar || !Number.isFinite(start) || !(end >= start)) return;
var width = parseFloat(bar.getAttribute('width'));
var x = parseFloat(bar.getAttribute('x'));
if (!(width > 0) || !Number.isFinite(x)) return;
spans.push({ node: node, bar: bar, start: start, end: end, x: x, width: width });
});
if (!spans.length) return 0;
var t0 = Math.min.apply(null, spans.map(function (span) { return span.start; }));
var t1 = Math.max.apply(null, spans.map(function (span) { return span.end; }));
if (!(t1 > t0)) return 0;
var ruler = spans.reduce(function (best, span) { return span.end - span.start > best.end - best.start ? span : best; });
var pxPerUnit = ruler.width / Math.max(ruler.end - ruler.start, 1e-9);
var x0 = ruler.x - (ruler.start - t0) * pxPerUnit;
var lead = 0.35;
var replay = Math.min(6, Math.max(3.4, spans.length * 0.45));
var at = function (t) { return lead + (t - t0) / (t1 - t0) * replay; };
var settle = lead + replay + 0.9;
spans.forEach(function (span) {
var fill = overlay('rect', 'ambient-flow-fill', {
x: span.bar.getAttribute('x'), y: span.bar.getAttribute('y'),
width: span.bar.getAttribute('width'), height: span.bar.getAttribute('height'),
rx: span.bar.getAttribute('rx') || '0'
}, span.bar.parentNode, span.bar.nextSibling);
var tone = /(?:^|\s)c-([a-z]+)(?:\s|$)/.exec(span.bar.getAttribute('class') || '');
if (tone) fill.style.setProperty('--flow-fill', 'var(--' + tone[1] + '-stroke)');
if (span.node.getAttribute('data-waterfall-status') && span.node.getAttribute('data-waterfall-status') !== 'ok') {
fill.classList.add('ambient-flow-fill-failed');
}
fill.timing = { start: at(span.start), end: Math.max(at(span.end), at(span.start) + 0.08), settle: settle };
extras.push(fill);
addWindow(windows, span.node.getAttribute('data-node-id'), fill.timing.end, fill.timing.end);
});
var box = sceneBounds(spans.map(function (span) { return span.bar; }));
if (box) addPlayhead(box, x0, x0 + (t1 - t0) * pxPerUnit, lead, replay, extras);
return settle;
}
function writeKeyframes(records, windows, extras, cycle) {
function at(seconds) {
return (Math.min(100, Math.max(0, seconds / cycle * 100))).toFixed(3) + '%';
}
var ease = 'animation-timing-function: cubic-bezier(0.45, 0.05, 0.35, 1);';
var rules = [
'@keyframes archify-relay-ripple {',
' 0% { r: 2px; opacity: var(--flow-peak, 0.6); }',
' ' + at(0.75) + ', 100% { r: 20px; opacity: 0; }',
'}'
];
records.forEach(function (record) {
if (!record.active) return;
var travel = record.travel;
rules.push(
'@keyframes archify-relay-comet-' + record.index + ' {',
' 0% { stroke-dashoffset: calc(var(--flow-len, 0px) + var(--flow-shift, 0px)); opacity: 0; ' + ease + ' }',
' ' + at(Math.min(0.12, travel * 0.2)) + ' { opacity: var(--flow-peak, 0.9); }',
' ' + at(travel) + ' { stroke-dashoffset: var(--flow-shift, 0px); opacity: var(--flow-peak, 0.9); }',
' ' + at(travel + 0.16) + ', 100% { stroke-dashoffset: var(--flow-shift, 0px); opacity: 0; }',
'}',
// A solid wake draws the travelled path behind the head on the same
// easing; a dashed wake only brightens its authored segments.
'@keyframes archify-relay-wake-' + record.index + ' {',
record.dashed
? ' 0%, ' + at(travel * 0.3) + ' { opacity: 0; }'
: ' 0% { stroke-dashoffset: var(--flow-len, 0px); opacity: var(--flow-peak, 0.3); ' + ease + ' }',
record.dashed
? ' ' + at(travel) + ' { opacity: var(--flow-peak, 0.3); }'
: ' ' + at(travel) + ' { stroke-dashoffset: 0px; opacity: var(--flow-peak, 0.3); }',
' ' + at(travel + 0.9) + ', 100% { stroke-dashoffset: 0px; opacity: 0; }',
'}'
);
});
var glow = function (strong) {
return 'filter: var(--ambient-node-base-filter,) drop-shadow(0 0 ' + (strong ? 14 : 10) + 'px var(--arrow-emphasis));';
};
var base = 'filter: var(--ambient-node-base-filter, none);';
Object.keys(windows).forEach(function (id, index) {
var merged = [];
windows[id].slice().sort(function (a, b) { return a.start - b.start; }).forEach(function (window) {
var peakEnd = Math.max(window.end, window.start + 0.42);
var last = merged[merged.length - 1];
if (last && window.start <= last.peakEnd + 0.78) {
last.peakEnd = Math.max(last.peakEnd, peakEnd);
last.strong = last.strong || window.strong;
} else merged.push({ start: window.start, peakEnd: peakEnd, strong: window.strong });
});
var frames = [' 0%, 100% { ' + base + ' }'];
merged.forEach(function (window) {
frames.push(
' ' + at(window.start) + ' { ' + base + ' }',
' ' + at(window.start + 0.12) + ', ' + at(window.peakEnd) + ' { ' + glow(window.strong) + ' }',
' ' + at(window.peakEnd + 0.78) + ' { ' + base + ' }'
);
});
rules.push('@keyframes archify-relay-node-' + index + ' {', frames.join('\n'), '}');
windows[id].keyframes = 'archify-relay-node-' + index;
});
if (extras.playhead) {
var p = extras.playhead;
rules.push(
'@keyframes archify-motion-playhead {',
' 0% { transform: translateX(0px); opacity: 0; }',
' ' + at(Math.max(0.05, p.lead - 0.2)) + ' { transform: translateX(0px); opacity: 0; }',
' ' + at(p.lead) + ' { transform: translateX(0px); opacity: var(--flow-peak, 0.9); }',
' ' + at(p.lead + p.sweep) + ' { transform: translateX(' + p.distance.toFixed(1) + 'px); opacity: var(--flow-peak, 0.9); }',
' ' + at(p.lead + p.sweep + 0.45) + ', 100% { transform: translateX(' + p.distance.toFixed(1) + 'px); opacity: 0; }',
'}'
);
}
extras.forEach(function (element, index) {
if (!element.timing) return;
var t = element.timing;
rules.push(
'@keyframes archify-replay-fill-' + index + ' {',
' 0%, ' + at(t.start) + ' { transform: scaleX(0); opacity: var(--flow-peak, 0.4); }',
' ' + at(t.end) + ', ' + at(t.settle) + ' { transform: scaleX(1); opacity: var(--flow-peak, 0.4); }',
' ' + at(t.settle + 0.7) + ', 100% { transform: scaleX(1); opacity: 0; }',
'}'
);
element.style.setProperty('--flow-name', 'archify-replay-fill-' + index);
});
var style = document.createElement('style');
style.setAttribute('data-ambient-flow-keyframes', 'true');
style.textContent = rules.join('\n');
document.head.appendChild(style);
}
function createFlows() {
var records = [];
Array.prototype.forEach.call(svg.querySelectorAll('path[data-animate="edge"]'), function (shape, index) {
var d = shape.getAttribute('data-motion-path') || shape.getAttribute('d');
if (!d) return;
var anchor = shape;
// Tree Branches initializes after these overlays and includes their
// inherited ancestors in its existing collapse/expand lifecycle.
function tagOverlay(flow) {
flow.setAttribute('data-ambient-flow-overlay', 'true');
flow.setAttribute('aria-hidden', 'true');
if (shape.hasAttribute('data-tree-ancestors')) {
flow.setAttribute('data-tree-ancestors', shape.getAttribute('data-tree-ancestors'));
}
}
function addPath(cls) {
var flow = document.createElementNS(SVG_NS, 'path');
flow.setAttribute('d', d);
['transform', 'mask', 'clip-path', 'vector-effect'].forEach(function (name) {
if (shape.hasAttribute(name)) flow.setAttribute(name, shape.getAttribute(name));
});
flow.setAttribute('class', 'ambient-edge-flow ambient-flow-' + cls);
tagOverlay(flow);
shape.parentNode.insertBefore(flow, anchor.nextSibling);
anchor = flow;
return flow;
}
var wake = addPath('wake');
var len = 0;
try { len = wake.getTotalLength(); } catch (_) {}
if (!(len > 0)) {
wake.remove();
return;
}
var record = {
index: records.length,
len: len,
from: shape.getAttribute('data-edge-from') || shape.getAttribute('data-composition-edge-from') || '',
to: shape.getAttribute('data-edge-to') || shape.getAttribute('data-composition-edge-to') || '',
step: stepOf(shape, index),
travel: Math.min(RELAY.maxTravel, Math.max(RELAY.minTravel, len / RELAY.speed)),
depart: 0,
active: false,
layers: []
};
records.push(record);
// Dashed authored lines keep their dash language through the relay:
// the wake brightens only the authored segments and never fills the
// gaps back into a solid line.
var authoredDash = '';
try { authoredDash = window.getComputedStyle(shape).strokeDasharray; } catch (_) {}
record.dashed = !!(authoredDash && authoredDash !== 'none');
wake.style.setProperty('--flow-dasharray', record.dashed ? authoredDash : len.toFixed(1) + ' ' + len.toFixed(1));
// Absolute caps keep the comet a slim streak on long edges instead
// of growing into a bright slab; the tail disintegrates into a
// sparkle of fragments behind the head, and --flow-shift trails
// each layer behind the head tip by its own length.
var headLen = Math.min(14, Math.max(7, len * 0.03));
var haloLen = Math.min(22, Math.max(12, len * 0.07));
var trail = len >= 120
? { pattern: '2.5 2 4 3 5.5 4.5 8 ' + (len - 30).toFixed(1), length: 30 }
: (len >= 60
? { pattern: '3 3 7 ' + (len - 13).toFixed(1), length: 13 }
: { pattern: '5 ' + Math.max(4, len - 5).toFixed(1), length: 5 });
record.layers.push(
{ flow: wake, kind: 'wake', dash: len, shift: 0 },
{ flow: addPath('glow'), kind: 'comet', dash: haloLen, shift: (haloLen - headLen) / 2 },
{ flow: addPath('halo'), kind: 'comet', dash: haloLen, shift: (haloLen - headLen) / 2 },
{ flow: addPath('tail'), kind: 'comet', dash: trail.length, shift: trail.length - headLen, dasharray: trail.pattern },
{ flow: addPath('head'), kind: 'comet', dash: headLen, shift: 0 }
);
if (choreography === 'stream') {
// A packet train behind the comet reads as a continuous stream.
[1, 2, 3].forEach(function (order) {
record.layers.push({ flow: addPath('head packet'), kind: 'comet', dash: Math.max(4, headLen * 0.55), shift: 0, packet: order });
});
}
// The ripple lands where the comet does; keep it out of the edge's
// mask so it can bloom over the node boundary.
var end = null;
try { end = wake.getPointAtLength(len); } catch (_) {}
if (end) {
var ripple = document.createElementNS(SVG_NS, 'circle');
ripple.setAttribute('cx', end.x.toFixed(1));
ripple.setAttribute('cy', end.y.toFixed(1));
ripple.setAttribute('r', '2');
if (shape.hasAttribute('transform')) ripple.setAttribute('transform', shape.getAttribute('transform'));
ripple.setAttribute('class', 'ambient-edge-flow ambient-flow-ripple');
tagOverlay(ripple);
shape.parentNode.insertBefore(ripple, anchor.nextSibling);
record.ripple = ripple;
}
});
var windows = Object.create(null);
var extras = [];
var span = 0;
if (choreography === 'sweep') span = planSweep(windows, extras);
else if (choreography === 'replay') span = planReplay(windows, extras);
else if (records.length) {
var linked = records.filter(function (record) { return record.from && record.to; });
if (choreography === 'sequence' || linked.length < records.length) planSequence(records, windows);
else if (choreography === 'walk') planWalk(records, windows);
else planRelay(records, windows);
span = records.reduce(function (max, record) {
return record.active ? Math.max(max, record.depart + record.travel) : max;
}, 0);
span = scalePlan(records, windows, span);
}
records.forEach(function (record) {
if (record.active) return;
record.layers.forEach(function (layer) { layer.flow.remove(); });
if (record.ripple) record.ripple.remove();
});
var active = records.filter(function (record) { return record.active; });
if (!active.length && !extras.length) return;
var cycle = Math.max(RELAY.minCycle, span + RELAY.land + RELAY.rest);
writeKeyframes(active, windows, extras, cycle);
svg.style.setProperty('--flow-cycle', cycle.toFixed(3) + 's');
html.style.setProperty('--flow-cycle-chrome', cycle.toFixed(3) + 's');
active.forEach(function (record) {
var gap = Math.min(0.22, record.travel * 0.28);
record.layers.forEach(function (layer) {
var flow = layer.flow;
flow.style.setProperty('--flow-len', record.len.toFixed(1) + 'px');
flow.style.setProperty('--flow-delay', (record.depart + (layer.packet || 0) * gap).toFixed(3) + 's');
flow.style.setProperty('--flow-dash', layer.dash.toFixed(1) + 'px');
flow.style.setProperty('--flow-shift', layer.shift.toFixed(1) + 'px');
flow.style.setProperty('--flow-name', 'archify-relay-' + layer.kind + '-' + record.index);
if (layer.dasharray) flow.style.setProperty('--flow-dasharray', layer.dasharray);
});
if (record.ripple) {
record.ripple.style.setProperty('--flow-delay', (record.depart + record.travel).toFixed(3) + 's');
record.ripple.style.setProperty('--flow-name', 'archify-relay-ripple');
}
});
var shapes = nodeShapes();
Object.keys(windows).forEach(function (id) {
(shapes[id] || []).forEach(function (shape) {
shape.style.setProperty('--flow-node-name', windows[id].keyframes);
});
});
flowCount = active.length + extras.length;
}
function settleEntry() {
entryPending.clear();
svg.removeEventListener('animationend', onEntryBoundary, true);
svg.removeEventListener('animationcancel', onEntryBoundary, true);
html.setAttribute('data-ambient-entry', 'settled');
}
function onEntryBoundary(event) {
if (!entryPending.has(event.target)) return;
// The entrance settles only when each node's pulse finishes; the
// shorter fade-rise enter animation may end first without ending it.
if (event.animationName && !/node-pulse/.test(event.animationName)) return;
entryPending.delete(event.target);
if (!entryPending.size) settleEntry();
}
function renderAmbient(suppressed) {
html.setAttribute('data-ambient-motion', flowCount ? (suppressed ? 'paused' : 'running') : 'empty');
if (suppressed) {
entryStarted = true;
settleEntry();
} else if (!entryStarted) {
entryStarted = true;
entryPending = new Set(Array.prototype.slice.call(svg.querySelectorAll('[data-animate="node"]')));
if (!entryPending.size) settleEntry();
else {
svg.addEventListener('animationend', onEntryBoundary, true);
svg.addEventListener('animationcancel', onEntryBoundary, true);
html.setAttribute('data-ambient-entry', 'running');
}
}
}
function readStored() {
try { return localStorage.getItem(STORAGE_KEY); } catch (_) { return null; }
}
function writeStored() {
try {
if (readerPaused) localStorage.removeItem(STORAGE_KEY);
else localStorage.setItem(STORAGE_KEY, 'live');
} 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');
renderAmbient(paused || owner || html.hasAttribute('data-embed') || html.hasAttribute('data-share-playback') || html.hasAttribute('data-document-hidden'));
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-entry');
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 ''; }
};
}
createFlows();
html.setAttribute('data-motion-capable', 'true');
btn.hidden = false;
readerPaused = readStored() !== 'live';
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 (typeof MutationObserver !== 'undefined') {
var contextObserver = new MutationObserver(render);
contextObserver.observe(html, { attributes: true, attributeFilter: ['data-embed', 'data-share-playback'] });
}
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;
if (element) {
try {
var parsed = JSON.parse(element.textContent || 'null');
if (parsed && parsed.verified === true && parsed.repository && parsed.nodes) payload = parsed;
} catch (_) {}
}
// Keep verified evidence discoverable to screen readers without adding
// visual marks. Restore first so repeated initialization never appends
// another count or retains one after evidence becomes unavailable.
var svg = document.querySelector('.diagram-container svg');
if (svg) Array.prototype.forEach.call(svg.querySelectorAll('[data-node-id]'), function (node) {
if (node.hasAttribute('data-source-evidence-count')) {
var savedLabel = node.getAttribute('data-source-evidence-original-label');
if (savedLabel == null) node.removeAttribute('aria-label');
else node.setAttribute('aria-label', savedLabel);
node.removeAttribute('data-source-evidence-count');
node.removeAttribute('data-source-evidence-original-label');
}
var sources = payload && payload.nodes[node.getAttribute('data-node-id')];
if (!Array.isArray(sources) || !sources.length) return;
var originalLabel = node.getAttribute('aria-label');
node.setAttribute('data-source-evidence-count', String(sources.length));
if (originalLabel != null) node.setAttribute('data-source-evidence-original-label', originalLabel);
node.setAttribute('aria-label', (originalLabel ? originalLabel + ', ' : '') +
viewerCount('viewer.passport.sourceCount', sources.length));
});
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() : [];
}
};
})();
/* ============================================================
Tree Branches — expand/collapse for hierarchy diagrams. The renderer
lays out the complete tree once and tags every node, link, and toggle
with its ancestors; collapsing only hides descendants, so visible nodes
never move and the reader keeps their place. Selecting a hidden node
through Finder, Outline, or a shared link expands its ancestors. Export
always writes the complete tree (see Export Cleanup).
============================================================ */
Archify.treeBranches = (function () {
var svg = document.querySelector('.diagram-container > svg[data-tree-ui]');
if (!svg) return { active: false };
var collapsed = Object.create(null);
var toggles = Object.create(null);
Array.prototype.forEach.call(svg.querySelectorAll('[data-tree-toggle]'), function (toggle) {
toggles[toggle.getAttribute('data-tree-toggle')] = toggle;
});
var tagged = svg.querySelectorAll('[data-tree-ancestors]');
var expandAll = svg.querySelector('[data-tree-expand-all]');
function ancestors(el) {
return (el.getAttribute('data-tree-ancestors') || '').split(' ').filter(Boolean);
}
function apply() {
Array.prototype.forEach.call(tagged, function (el) {
var hidden = ancestors(el).some(function (id) { return collapsed[id]; });
if (hidden) el.setAttribute('data-tree-hidden', '');
else el.removeAttribute('data-tree-hidden');
});
var any = false;
Object.keys(toggles).forEach(function (id) {
var toggle = toggles[id];
var isCollapsed = Boolean(collapsed[id]);
any = any || isCollapsed;
toggle.setAttribute('aria-expanded', isCollapsed ? 'false' : 'true');
toggle.setAttribute('aria-label', toggle.getAttribute(isCollapsed ? 'data-tree-label-expand' : 'data-tree-label-collapse') || '');
var node = svg.querySelector('[data-node-id="' + id + '"]');
if (node) {
if (isCollapsed) node.setAttribute('data-tree-collapsed', '');
else node.removeAttribute('data-tree-collapsed');
}
});
if (any) svg.setAttribute('data-tree-any-collapsed', '');
else svg.removeAttribute('data-tree-any-collapsed');
}
function setCollapsed(id, value) {
if (!toggles[id]) return false;
if (value) collapsed[id] = true;
else delete collapsed[id];
apply();
return true;
}
function toggle(id) { return setCollapsed(id, !collapsed[id]); }
// Reveal a node by expanding every collapsed ancestor; nothing else moves.
function reveal(id) {
var node = svg.querySelector('[data-node-id="' + id + '"]');
if (!node) return false;
var changed = false;
ancestors(node).forEach(function (ancestor) {
if (collapsed[ancestor]) { delete collapsed[ancestor]; changed = true; }
});
if (changed) apply();
return changed;
}
function restoreAll() {
collapsed = Object.create(null);
apply();
}
function activate(event) {
var target = event.target.closest('[data-tree-toggle], [data-tree-expand-all]');
if (!target || !svg.contains(target)) return false;
event.preventDefault();
event.stopPropagation();
if (target.hasAttribute('data-tree-expand-all')) {
restoreAll();
var root = svg.querySelector('[data-tree-toggle]:not([data-tree-ancestors])');
if (root) root.focus();
} else {
toggle(target.getAttribute('data-tree-toggle'));
target.focus();
}
return true;
}
// Capture phase so the canvas focus handler never treats a toggle press
// as a background click that clears the current focus.
svg.addEventListener('click', activate, true);
svg.addEventListener('keydown', function (event) {
if (event.key === 'Enter' || event.key === ' ') { activate(event); return; }
var node = event.target.closest('[data-node-id]');
if (!node || !svg.contains(node)) return;
var id = node.getAttribute('data-node-id');
// Arrow keys follow the tree convention: left collapses, right expands.
if (event.key === 'ArrowLeft' && toggles[id] && !collapsed[id]) { event.preventDefault(); setCollapsed(id, true); }
else if (event.key === 'ArrowRight' && collapsed[id]) { event.preventDefault(); setCollapsed(id, false); }
}, true);
// Focus reveals ancestors synchronously before positioning its lens.
// The observer also covers paths that change pressed state directly.
new MutationObserver(function (records) {
records.forEach(function (record) {
var el = record.target;
if (el.hasAttribute && el.hasAttribute('data-tree-hidden') && el.getAttribute('aria-pressed') === 'true') {
reveal(el.getAttribute('data-node-id'));
}
});
}).observe(svg, { subtree: true, attributes: true, attributeFilter: ['aria-pressed'] });
Object.keys(toggles).forEach(function (id) {
if (toggles[id].hasAttribute('data-tree-initially-collapsed')) collapsed[id] = true;
});
apply();
return {
active: true,
toggle: toggle,
collapse: function (id) { return setCollapsed(id, true); },
expand: function (id) { return setCollapsed(id, false); },
reveal: reveal,
restoreAll: restoreAll,
collapsedIds: function () { return Object.keys(collapsed); },
expandAllControl: expandAll
};
})();
/* ============================================================
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:not([data-ambient-flow-overlay]), 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);
if (shape.getAttribute('data-motion-path')) clone.setAttribute('d', relationshipTokenPath(shape));
clone.removeAttribute('data-motion-path');
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('data-motion-path') || 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;
// Hash restoration and internal selections use this path too. Reveal
// tree ancestors before measuring the selected node for its lens.
if (Archify.treeBranches && Archify.treeBranches.active) {
normalized.forEach(function (id) { Archify.treeBranches.reveal(id); });
}
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 copyFocusLink() {
if (activeIds.length !== 1) return Promise.resolve(false);
var record = pinnedRelationshipRecord();
var relationId = record && record.id;
var value = readerLink(relationId
? 'relation=' + encodeURIComponent(relationId)
: 'focus=' + encodeURIComponent(activeIds[0]) + (reachabilityMode ? '&reach=' + reachabilityMode : ''));
return (value === null ? Promise.resolve(false) : copyReaderText(value)).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.ctrlKey || event.altKey || event.metaKey || event.shiftKey) return;
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;
// Export consumes the active semantic snapshot; its controls are not
// an outside selection. Genuine outside activation still clears it.
if (target.closest('[data-node-id], [data-relationship-hit-key], .overview-map, #btn-export, #export-menu')) return;
clear();
}, true);
window.addEventListener('scroll', requestLensPlacement, { passive: true });
window.addEventListener('resize', requestLensPlacement);
container.addEventListener('scroll', requestLensPlacement, { passive: true });
// Reader and Chrome may finish resizing the stage after window resize.
// The absolute Passport does not resize its container when reclamped.
if (typeof ResizeObserver === 'function') {
new ResizeObserver(function () {
if (!chip.hidden) requestLensPlacement();
}).observe(container);
}
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:not([data-ambient-flow-overlay]), 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);
if (shape.getAttribute('data-motion-path')) clone.setAttribute('d', shape.getAttribute('data-motion-path'));
clone.removeAttribute('data-motion-path');
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 legend = svg && svg.querySelector('[data-legend]');
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 readerFit = svg && svg.getAttribute('data-reader-fit');
var widthFirstReading = readerFit === 'width-first';
var automaticSequence = widthFirstReading && svg.hasAttribute('data-sequence-column-fit');
var readingScroll = widthFirstReading;
var measuredHeightFit = readerFit === 'intrinsic-height' || widthFirstReading;
var frame = 0;
var settleFrame = 0;
var lastWidth = 0;
var automaticAreaHeight = 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 MAX_AUTOMATIC_SCALE = 1.5;
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.style.removeProperty('--archify-diagram-min-height');
html.removeAttribute('data-reader-area');
html.removeAttribute('data-reader-narrow');
html.removeAttribute('data-reader-layout');
html.removeAttribute('data-reader-overflow');
setRail(false);
lastWidth = 0;
automaticAreaHeight = 0;
settledCap = 0;
clearLegend();
}
function clearLegend() {
if (!legend || !legend.hasAttribute('data-reader-legend-corner')) return;
legend.removeAttribute('data-reader-legend-corner');
legend.style.removeProperty('--archify-reader-legend-transform');
}
// Keep the original legend in its SVG: renderer hover/focus selectors and
// canonical export still own that same group. Ordinary capped reading
// and fitted automatic sequences borrow the outer corner at 25–100%.
function syncLegend() {
var cameraScale = Number(svg && svg.getAttribute('data-view-scale')) || 1;
var cornerReading = html.getAttribute('data-reader-area') === 'true' || automaticSequence && !readingScroll;
if (!legend || !eligible() || !cornerReading || cameraScale > 1.001) {
clearLegend();
return;
}
var box = legend.getBBox();
var parentMatrix = legend.parentNode.getScreenCTM();
if (!parentMatrix || !box.width || !box.height) {
clearLegend();
return;
}
var rect = diagram.getBoundingClientRect();
var style = window.getComputedStyle(diagram);
var scale = Math.min(svg.clientWidth / viewBox.width, svg.clientHeight / viewBox.height);
if (!Number.isFinite(scale) || scale <= 0) {
clearLegend();
return;
}
var left = rect.left + number(style.borderLeftWidth) + number(style.paddingLeft);
var right = rect.right - number(style.borderRightWidth) - number(style.paddingRight);
var bottom = rect.bottom - number(style.borderBottomWidth) - number(style.paddingBottom);
var top = bottom - box.height * scale;
var nav = diagram.querySelector('.diagram-nav');
var navRect = visible(nav) ? nav.getBoundingClientRect() : null;
// Long or multi-row legends retain their canonical position if the
// spare corner cannot hold them clear of the dock and content area.
if (left + box.width * scale > right || top < rect.top + number(style.paddingTop) + number(style.borderTopWidth) ||
(navRect && left < navRect.right + 10 && left + box.width * scale > navRect.left - 10 &&
top < navRect.bottom + 10 && bottom > navRect.top - 10)) {
clearLegend();
return;
}
var matrix = parentMatrix.inverse().translate(left, top).scale(scale).translate(-box.x, -box.y);
var value = 'matrix(' + [matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f].join(',') + ')';
if (legend.style.getPropertyValue('--archify-reader-legend-transform') !== value) {
legend.style.setProperty('--archify-reader-legend-transform', value);
}
if (!legend.hasAttribute('data-reader-legend-corner')) legend.setAttribute('data-reader-legend-corner', '');
}
// 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));
var changed = Math.abs(rounded - lastWidth) >= 1;
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');
var railShare = html.getAttribute('data-reader-rail') === 'true' ? RAIL_WIDTH + RAIL_GAP : 0;
var diagramWidth = Math.max(1, rounded - railShare - chromeMetrics().diagramX);
// Content fitting and automatic enlargement never reduce the outer
// reader region. The SVG alone takes the narrower reading width.
var automaticWidthCap = measuredHeightFit && viewBox && viewBox.width > 0
? viewBox.width * MAX_AUTOMATIC_SCALE : 0;
if (automaticWidthCap && diagramWidth > automaticWidthCap) {
diagramWidth = automaticWidthCap;
html.style.setProperty('--archify-diagram-min-height', automaticAreaHeight + 'px');
html.setAttribute('data-reader-area', 'true');
} else {
// Ordinary automatic diagrams retain their existing document flow.
// Settling can also stop binding the cap without a new measure.
html.style.removeProperty('--archify-diagram-min-height');
html.removeAttribute('data-reader-area');
}
if (diagramWidth < Math.max(rounded, shellFloor) - railShare - chromeMetrics().diagramX) {
html.style.setProperty('--archify-diagram-max-width', diagramWidth + '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');
syncLegend();
return changed;
}
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();
// visual-check fails any page whose rounded-up height exceeds the
// viewport, so even a 1px remainder must be reduced or declared.
if (overflow > 0 && !readingScroll && lastWidth > Math.ceil(minWidth)) {
applyWidth(Math.max(minWidth, lastWidth - overflow * ratio - 4), minWidth);
settledCap = lastWidth;
html.setAttribute('data-reader-overflow', 'reduced');
} else if (overflow > 0) {
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);
// Sequence first-screen fitting preserves source text sizes instead
// of shrinking to the shared hard readability floor.
var readableWidth = viewBox && viewBox.width > 0
? viewBox.width * (automaticSequence ? 1 : minimumReadableScale()) + chrome.diagramX
: MIN_READER_WIDTH;
var maxWidth = Math.min(MAX_READER_WIDTH, viewportCap);
var readableMinimumWidth = automaticSequence || measuredHeightFit && ratio < WIDE_RATIO
? readableWidth
: measuredHeightFit && ratio >= WIDE_RATIO && Number.isFinite(declaredMinimumText)
? Math.max(MIN_READER_WIDTH, readableWidth)
: MIN_READER_WIDTH;
var minWidth;
if (automaticSequence) {
minWidth = Math.min(readableMinimumWidth, maxWidth);
} else 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';
}
// Height-fit families with notes below the fold may shrink to their
// readable floor. Width-first families choose their reading width
// below and retain page scrolling instead.
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);
// Compact automatic sequences fit the first screen without shrinking
// their authored text. Longer sequences retain reading width and page
// scroll; other width-first families keep their existing strategy.
var sequenceFits = automaticSequence && Math.min(desiredWidth, maxWidth) >=
readableWidth + (docked ? railExtra : 0);
readingScroll = widthFirstReading && !sequenceFits;
if (readingScroll) desiredWidth = maxWidth;
// Only a binding enlargement cap needs a separate normal reader area.
// Uncapped diagrams keep their existing natural height and Chrome fit.
automaticAreaHeight = availableSvgHeight + chrome.diagramY;
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,
legend && legend.hasAttribute('data-reader-legend-corner') ? legend.style.getPropertyValue('--archify-reader-legend-transform') : ''
].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 });
window.addEventListener('beforeprint', clearLegend);
window.addEventListener('afterprint', schedule);
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,
syncLegend: syncLegend,
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,
lift,
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 || liftFrame || 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); });
// Camera zoom changes outer page flow without changing intrinsic SVG
// size. Follow that box for dock placement, independently of scrolling
// and the baseline stage-rail measurement.
new ResizeObserver(scheduleLift).observe(container);
}
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 minimumZoom = 0.25;
var maximumZoom = 3;
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 originalFlowHeight = container.style.getPropertyValue('height');
var originalFlowHeightPriority = container.style.getPropertyPriority('height');
var compactPageFlow = false;
var viewBox = svg.viewBox && svg.viewBox.baseVal;
function clamp() {
var width = svg.clientWidth || 1;
var height = svg.clientHeight || 1;
if (state.scale < 1) {
var visibleWidth = Math.min(width, container.clientWidth);
var availableX = visibleWidth - width * state.scale;
state.x = availableX >= 0 ? container.scrollLeft + availableX / 2
: Math.max(container.scrollLeft + availableX, Math.min(container.scrollLeft, state.x));
// Long sequences keep their first rows in view instead of centering
// inside a layout box that can be several screens tall.
state.y = height > window.innerHeight ? 0 : height * (1 - state.scale) / 2;
return;
}
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 camera = sampleRenderedState();
var x;
var y;
var width;
var height;
if (window.innerWidth <= 720 && container.hasAttribute('data-wide-diagram') && camera.scale >= 1) {
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 + (((container.scrollLeft - camera.x) / camera.scale) - metrics.offsetX) / metrics.scale;
y = viewBox.y + ((-camera.y / camera.scale) - metrics.offsetY) / metrics.scale;
width = Math.min(viewBox.width, Math.min(metrics.width, container.clientWidth) / camera.scale / metrics.scale);
height = Math.min(viewBox.height, metrics.height / camera.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));
// The camera's layout box can span several browser screens. Intersect
// its logical bounds with the actual window, including page scrolling.
var matrix = svg.getScreenCTM();
if (matrix && matrix.d > 0) {
var end = y + height;
y = Math.max(y, Math.min(end - 1, -matrix.f / matrix.d));
height = Math.max(1, Math.min(end, (window.innerHeight - matrix.f) / matrix.d) - y);
}
return { x: x, y: y, width: width, height: height, scale: camera.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
});
var readout = (showDetailLevel ? resolvedLevel + ' ' : '') + percent;
resetBtn.setAttribute('aria-label', readout + ' · ' + 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;
}
// CSS transforms keep the SVG's intrinsic layout size. Shrink only its
// outer page-flow box for long ordinary diagrams, never the SVG itself:
// changing its height would feed the smaller size back into camera math.
function ordinaryPage() {
var html = document.documentElement;
return html.getAttribute('data-embed') !== 'true' &&
html.getAttribute('data-present') !== 'true' &&
(!window.matchMedia || !window.matchMedia('print').matches);
}
function syncPageFlow(camera) {
camera = camera || state;
var height = svg.clientHeight;
var ordinary = ordinaryPage();
// Restore ordinary flow as soon as the target reaches 100%. Chrome
// uses that target to resume baseline rail measurement; retaining a
// partially shrunk box during the CSS transition would inflate reserve.
if (ordinary && state.scale < 1 && camera.scale < 1 && height > window.innerHeight) {
var style = getComputedStyle(container);
var chrome = ['paddingTop', 'paddingBottom', 'borderTopWidth', 'borderBottomWidth']
.reduce(function (total, key) { return total + (parseFloat(style[key]) || 0); }, 0);
var flowHeight = Math.ceil(height * camera.scale + Math.max(0, camera.y) + chrome) + 'px';
if (container.style.getPropertyValue('height') !== flowHeight) container.style.setProperty('height', flowHeight);
compactPageFlow = true;
} else if (compactPageFlow) {
if (originalFlowHeight) container.style.setProperty('height', originalFlowHeight, originalFlowHeightPriority);
else container.style.removeProperty('height');
compactPageFlow = false;
}
}
function syncViewportClip() {
if (clipFrame) cancelAnimationFrame(clipFrame);
clipFrame = 0;
function sample() {
clipFrame = 0;
var rendered = sampleRenderedState();
syncPageFlow(rendered);
clipToViewport(rendered);
if (Archify.radar && Archify.radar.isOpen()) Archify.radar.sync();
if (Archify.readerLayout && typeof Archify.readerLayout.syncLegend === 'function') Archify.readerLayout.syncLegend();
if (!cameraSettled(rendered)) clipFrame = requestAnimationFrame(sample);
}
sample();
}
function apply() {
clamp();
svg.style.transform = 'translate(' + state.x + 'px,' + state.y + 'px) scale(' + state.scale + ')';
svg.setAttribute('data-view-scale', String(state.scale));
syncViewportClip();
renderControls();
outBtn.disabled = state.scale <= minimumZoom;
inBtn.disabled = state.scale >= maximumZoom;
container.classList.toggle('is-pannable', state.scale > 1);
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(minimumZoom, Math.min(maximumZoom, Math.round(next * 4) / 4));
if (next === previous) return;
var longDiagram = svg.clientHeight > window.innerHeight && Math.min(previous, next) < 1;
var before = longDiagram ? svg.getBoundingClientRect() : null;
var anchorY = before ? Math.max(0, Math.min(window.innerHeight, before.bottom) + Math.max(0, before.top)) / 2 : 0;
if (before && (before.bottom <= 0 || before.top >= window.innerHeight)) anchorY = window.innerHeight / 2;
var logicalY = before ? Math.max(0, Math.min(svg.clientHeight, (anchorY - before.top) / previous)) : 0;
var documentTop = before ? window.scrollY + before.top - state.y : 0;
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();
if (longDiagram) {
var scaledHeight = svg.clientHeight * next;
var pageY = scaledHeight <= window.innerHeight ? documentTop + state.y
: documentTop + state.y + logicalY * next - anchorY;
window.scrollTo({ left: window.scrollX, top: Math.max(0, pageY), behavior: 'instant' });
}
}
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 scrollLongDiagramTo(contentY, metrics, options) {
if (!ordinaryPage() || metrics.height <= window.innerHeight) return;
// apply() may still be animating its CSS transform. Recover the layout
// origin from the painted transform, then use the final camera target.
var painted = sampleRenderedState();
var rect = svg.getBoundingClientRect();
var pixelScale = rect.height / metrics.height / painted.scale;
var documentTop = window.scrollY + rect.top - painted.y * pixelScale;
var target = documentTop + (state.y + contentY * state.scale) * pixelScale - window.innerHeight / 2;
window.scrollTo({ left: window.scrollX, top: Math.max(0, target), behavior: options.instant ? 'instant' : 'smooth' });
}
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; }
scrollLongDiagramTo(metrics.offsetY + (logicalY - viewBox.y) * metrics.scale, metrics, options);
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();
scrollLongDiagramTo(contentY, metrics, options);
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();
});
});
// Reader width and Chrome reserve can change without a window resize.
// Observe their resulting sizes; the SVG remains intrinsically sized, so
// the outer flow height cannot feed back into its camera base.
if (typeof ResizeObserver === 'function') {
var flowObserver = new ResizeObserver(function () { syncPageFlow(sampleRenderedState()); });
flowObserver.observe(svg);
flowObserver.observe(container);
}
if (typeof MutationObserver === 'function') {
new MutationObserver(function () { syncPageFlow(sampleRenderedState()); })
.observe(document.documentElement, { attributes: true, attributeFilter: ['data-embed', 'data-present'] });
}
var printMedia = window.matchMedia && window.matchMedia('print');
if (printMedia && printMedia.addEventListener) printMedia.addEventListener('change', function () { syncPageFlow(sampleRenderedState()); });
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;
if (item.links === 0) links.setAttribute('data-zero', 'true');
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) {
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) return;
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) {
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) return;
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:not([data-ambient-flow-overlay]), 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);
if (shape.getAttribute('data-motion-path')) clone.setAttribute('d', shape.getAttribute('data-motion-path'));
clone.removeAttribute('data-motion-path');
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);
if (shape.getAttribute('data-motion-path')) clone.setAttribute('d', shape.getAttribute('data-motion-path'));
clone.removeAttribute('data-motion-path');
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 copyLink() {
if (mode !== 'result') return Promise.resolve(false);
var value = readerLink('route=' + encodeURIComponent(startId) + '~' + encodeURIComponent(endId));
return (value === null ? Promise.resolve(false) : copyReaderText(value)).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) {
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) return;
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:not([data-ambient-flow-overlay]), 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);
if (shape.getAttribute('data-motion-path')) clone.setAttribute('d', shape.getAttribute('data-motion-path'));
clone.removeAttribute('data-motion-path');
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 copyLink() {
if (!selectedKinds.length) return Promise.resolve(false);
var value = readerLink('lens=' + selectedKinds.map(encodeURIComponent).join('~'));
return (value === null ? Promise.resolve(false) : copyReaderText(value)).then(function (copied) {
copyBtn.textContent = viewerText(copied ? 'viewer.common.copied' : 'viewer.common.copyFailed');
copyBtn.setAttribute('aria-label', viewerText(copied ? 'viewer.common.copied' : 'viewer.common.copyFailed'));
window.setTimeout(function () {
copyBtn.textContent = viewerText('viewer.common.copyLink');
copyBtn.setAttribute('aria-label', viewerText('viewer.lens.copy'));
}, 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) {
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) return;
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) {
// Return focus where the reader was when the guide opened; the
// trigger itself is only the fallback when that element is gone.
var target = returnFocus && returnFocus.isConnected ? 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) {
// Clicks anywhere on the trigger (including its icon span) are the
// toggle itself, never an outside dismissal.
if (!panel.hidden && !panel.contains(event.target) && !trigger.contains(event.target)) 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.shiftKey && (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Home' || e.key === 'End')) 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>
<script>(function(){var h=document.documentElement;function set(m){
if(m==='embed')h.setAttribute('data-embed','true');else h.removeAttribute('data-embed');
var p=window.Archify&&Archify.presentation;if(p){if(m==='present')p.enter();else if(p.active())p.exit();}
dispatchEvent(new Event('resize'));if(window.Archify&&Archify.view&&Archify.view.reset)Archify.view.reset({automatic:true});}
addEventListener('message',function(e){if(e.source===parent&&typeof e.data==='string'&&e.data.indexOf('arch:')===0)set(e.data.slice(5));});
// The viewer's own Esc/Exit leaves presentation inside the iframe; tell the article so it closes the overlay too.
new MutationObserver(function(){if(!h.hasAttribute('data-present'))parent.postMessage('arch:exit','*');}).observe(h,{attributes:true,attributeFilter:['data-present']});
parent.postMessage('arch:ready','*');})();</script>
<style>/* Viewer centres the 720px canvas in full screen; on a narrow phone that hides its left edge past scroll 0. Auto margins centre without overflowing left. */
html[data-present="true"]:not([data-embed="true"]) .diagram-container{justify-content:flex-start}
html[data-present="true"]:not([data-embed="true"]) .diagram-container>svg{margin-inline:auto}</style></body>
</html>
"></iframe>
<button type="button" class="arch-open"><span data-l="nb">Utforsk diagrammet</span><span data-l="en" hidden>Explore the diagram</span></button>
</div>
<figcaption data-l="nb">Tallene i boksene viser hvilket FDSA-fundament som dekker noden. Agenten får ikke lese før identitetsløsningen har svart på hvem den er og policyhåndhevelsen (PEP) har sjekket hva mandatet tillater. Sløyfen fra Evaluering tilbake til Ontologi er stiplet fordi FDSA ikke beskriver den ennå.</figcaption>
<figcaption data-l="en" hidden>The numbers in the boxes show which FDSA foundation covers the node. The agent gets no data before the identity service has answered who it is and the policy enforcement point (PEP) has checked what its mandate allows. The loop from Evaluation back to Ontology is dashed because FDSA does not yet describe it.</figcaption>
</figure>
<!-- ===================== NORSK, del 2 ===================== -->
<div data-l="nb">
<h2>Mapping mot de ti kapabilitetsområdene</h2>
<span class="aud">For arkitekter og IT-ledere</span>
<p>FDSA deler arkitekturen inn i ti kapabilitetsområder. Tabellen leses radvis fra venstre: for hvert område står først hva artikkelen beskriver, deretter hvor godt artikkelen dekker området, og til slutt hva det betyr for FDSA. Dekningsgraden sier noe om artikkelen, ikke om hvor langt Forsvaret har kommet.</p>
<div class="scroll">
<table>
<thead><tr><th>#</th><th>Kapabilitetsområde i FDSA</th><th class="a"><small>Artikkelen</small>Hva artikkelen beskriver</th><th class="a"><small>Artikkelen</small>Dekning av området</th><th class="f"><small>FDSA</small>Hva det betyr for FDSA</th></tr></thead>
<tbody>
<tr><td>1</td><td>Føderert datastyring</td><td>Unity Catalog styrer data på tvers av Jack Dorseys Block-selskaper Square, Cash App og TIDAL. OntoRank avgjør algoritmisk hvilken definisjon som er autoritativ, ut fra hvem som laget den, hvor mye den brukes og hvor nylig den ble endret.</td><td><span class="tag t-ok">Sterk</span></td><td>OntoRank er en algoritmisk variant av fundament 4.2: en sentral autoritet avgjør ved tvil om begrepsbruk. I FDSA er det en myndighet, hos Databricks en rangering. Forskjellen er verdt en diskusjon.</td></tr>
<tr><td>2</td><td>Datasentrisk sikkerhet</td><td>Unity Gateway gir hver modell, agent og hvert KI-verktøy en identitet, et budsjett og guardrails, og logger alle handlinger.</td><td><span class="tag t-ok">Sterk</span></td><td>«Hva er tillatt» er deres ord for ABAC og Zero Trust (2.2 og 2.4). Realiseres i Forsvaret av identitets- og tilgangsstyring for identitet og av FDDP for policy-as-code. Nytt: identitet for agenter, ikke bare for personell, materiell og systemer. Bør inn i kravene til identitetsløsningen.</td></tr>
<tr><td>3</td><td>Dataprodukter</td><td>Ikke eksplisitt. Artikkelen snakker om tabeller og databaser.</td><td><span class="tag t-weak">Svak</span></td><td>Produkt med eier og kontrakt, og FDSAs FAKTISK-kriterier (finnbare, anvendelige, klare, troverdige, interoperable, sikre, koblede), har ingen motpart. Her er FDSA tydeligere enn Databricks.</td></tr>
<tr><td>4</td><td>Datakatalog</td><td>Genie Ontology, beskrevet som en kontinuerlig bygget graf over hva organisasjonen vet.</td><td><span class="tag t-mid">Middels</span></td><td>Deres katalog er en kunnskapsgraf for agenter, ikke en bestillingsdisk for mennesker. Det er dit en datakatalog bør utvikles (7.3).</td></tr>
<tr><td>5</td><td>Datatjenester</td><td>Spark, Genie (naturlig språk til spørring og apper), Agent Bricks (utvikleragenter), og evalueringssløyfen «virket det».</td><td><span class="tag t-ok">Sterk</span></td><td>Eksemplene fra Novo Nordisk og Rivian er fundament 6 i praksis: forskere og analytikere som får svar uten dataingeniør i løypa. Evalueringssløyfen er ikke beskrevet i FDSA.</td></tr>
<tr><td>6</td><td>Dataforvaltning</td><td>Genie Ontology og spørsmålet «hva betyr det», med 50 til 84,5 prosent treffsikkerhet.</td><td><span class="tag t-ok">Sterk</span></td><td>Artikkelens beste argument for 7.1. Egnet for enhver ledergruppe.</td></tr>
<tr><td>7</td><td>Datautveksling</td><td>Åpne tabellformater (Delta Lake, Iceberg, Hudi) som gjorde lakehouse mulig.</td><td><span class="tag t-mid">Middels</span></td><td>Treffer 1.2 og FDSAs tredje suverenitetsforutsetning om åpne formater. Ingen motpart for utveksling mot allierte eller mellom graderingsnivåer.</td></tr>
<tr><td>8</td><td>Datautnyttelse</td><td>Hele delen om firmaets form: agenter som informasjonsrutere, «alle blir ledere», færre lag. Eksempelet er Jack Dorseys Block, som vil erstatte mellomledelse med en felles modell av hele virksomheten.</td><td><span class="tag t-ok">Sterk, men sivil</span></td><td>Deres anvendelsesområde er virksomhetsstyring i et betalingsselskap. Overføres til kommandokjeden med forsiktighet. Ghodsi skiller selv mellom organisasjonskart og informasjonsflyt; det skillet er viktigere i Forsvaret enn hos Block.</td></tr>
<tr><td>9</td><td>Plattform og infrastruktur</td><td>Lakehouse (analytisk) og Lakebase (operasjonell) på samme lagring, kalt LTAP. Databaser som kan grenes og kastes som kode.</td><td><span class="tag t-ok">Sterk</span></td><td>LTAP er nøyaktig FDSAs fjerde scenario, fusjon av operasjonelle og analytiske data uten rørlegging mellom dem. Ett lagringslag under begge er et konkret krav å ta med i det tekniske målbildet som følger FDSA. Prinsippet gjelder uansett hvilket produkt som realiserer det.</td></tr>
<tr><td>10</td><td>Kommunikasjonsinfrastruktur</td><td>Ingen.</td><td><span class="tag t-none">Ingen</span></td><td>Artikkelen forutsetter alltid-på sky. Degraderte miljøer (DDIL), fog og edge finnes ikke i deres verden, og det er også grunnen til at produktet i seg selv ikke er et svar for Forsvaret.</td></tr>
</tbody>
</table>
</div>
<h2>Tre ting FDSA kan lære av artikkelen</h2>
<p>Artikkelen bekrefter mye, men den har også tre ting FDSA ikke har ennå. Det er de som gjør notatet verdt å lese, og her går jeg fra å referere til å mene. Dette er mine vurderinger.</p>
<div class="box">
<h3>1. Agenter er aktører med identitet, og det er en identitetssak</h3>
<p>FDSA snakker om felles autoritativ identitet for personell, materiell og informasjonssystemer (2.2). Databricks legger til agenter som en fjerde kategori, med egen identitet, eget budsjett og egne grenser. Når en saksbehandler kan starte hundre agenter om dagen, holder ikke én pålogging per ansatt, og det holder heller ikke at agenten «låner» saksbehandlerens identitet. Da vet ingen i ettertid om det var mennesket eller agenten som hentet dataene.</p>
<p>Slik jeg ser det betyr det tre krav til identitets- og tilgangsstyring som bør være på plass før det første agentbaserte verktøyet er i drift: en agent skal ha egen identitet knyttet til den som startet den; en agent skal ha et eksplisitt mandat (hvilke data, hvilke handlinger, hvilket budsjett) som kan uttrykkes som attributter i ABAC; og hver handling skal spores til agentidentiteten, ikke bare til eieren. Det er samme prinsipp som når et materiellsystem har egen identitet i stedet for operatørens.</p>
</div>
<div class="box">
<h3>2. Evalueringssløyfen er en kapabilitet</h3>
<p>«Virket det» er ikke et rapporteringskrav, men en del av registeret. Hver gang et menneske korrigerer en agent, skal korreksjonen forbedre neste kjøring, og det lærte skal ligge igjen i virksomhetens eget register, ikke hos modelleverandøren. FDSA har tilbakemelding fra bruker til dataforvalter (5.3), men ikke sløyfen fra utfall tilbake til policy og modell. Jeg mener det er et naturlig neste steg når FDSA konkretiseres i et teknisk målbilde.</p>
</div>
<div class="box">
<h3>3. Databaser som kan grenes</h3>
<p>Lakebase kopierer bare endringer, så en agent kan lage en isolert gren av en database på sekunder, eksperimentere, og kaste den. Det er det som gjør det trygt å la agenter jobbe mot ekte data. Jeg ville stilt det som krav til plattformlaget: at dataprodukter kan grenes uten å kopiere hele datamengden.</p>
</div>
<h2>Det suverene firmaet, og Stortingets vedtak</h2>
<p>Den delen av artikkelen som bør leses høyt for enhver som lurer på hvorfor Forsvaret insisterer på datasuverenitet, er avsnittet om to banker. Begge leier den samme KI-modellen for kredittbeslutninger. Den første sender inn et spørsmål, får et svar og lagrer beslutningen. Den andre lagrer hvilke fakta som ga svaret, hver menneskelige overstyring, hvilke beslutninger som viste seg riktige, og justerer reglene sine løpende. Et år senere, når en bedre modell kommer, har bare den ene bygget en bedre bank. Hvis den nye modellen arver definisjonene, rettighetene, de vurderte sakene og unntakene, er det et varig fortrinn. Hvis den starter fra samme prompt som alle andre, er det ikke.</p>
<div class="fignum">Figur 3 · Det suverene firmaet: to banker, samme KI-modell</div>
</div>
<!-- ===================== ENGLISH, part 2 ===================== -->
<div data-l="en" hidden>
<h2>Mapping against the ten capability areas</h2>
<span class="aud">For architects and IT leaders</span>
<p>FDSA divides the architecture into ten capability areas. Read the table row by row from the left: for each area, first what the essay describes, then how well the essay covers the area, and finally what it means for FDSA. The coverage grade says something about the essay, not about how far the Armed Forces have come.</p>
<div class="scroll">
<table>
<thead><tr><th>#</th><th>Capability area in FDSA</th><th class="a"><small>The essay</small>What the essay describes</th><th class="a"><small>The essay</small>Coverage of the area</th><th class="f"><small>FDSA</small>What it means for FDSA</th></tr></thead>
<tbody>
<tr><td>1</td><td>Federated data governance</td><td>Unity Catalog governs data across Jack Dorsey's Block companies Square, Cash App and TIDAL. OntoRank algorithmically decides which definition is authoritative, based on who created it, how widely it is used and how recently it changed.</td><td><span class="tag t-ok">Strong</span></td><td>OntoRank is an algorithmic variant of foundation 4.2: a central authority decides when terms conflict. In FDSA it is an authority, at Databricks a ranking. The difference is worth a discussion.</td></tr>
<tr><td>2</td><td>Data-centric security</td><td>Unity Gateway gives every model, agent and AI tool an identity, a budget and guardrails, and logs every action.</td><td><span class="tag t-ok">Strong</span></td><td>"What is allowed" is their phrase for ABAC and Zero Trust (2.2 and 2.4). Realised in the Armed Forces by identity and access management for identity and by FDDP for policy-as-code. New: identity for agents, not just for personnel, materiel and systems. Should go into the requirements for the identity service.</td></tr>
<tr><td>3</td><td>Data products</td><td>Not explicit. The essay talks about tables and databases.</td><td><span class="tag t-weak">Weak</span></td><td>A product with an owner and a contract, and FDSA's FAKTISK criteria (findable, usable, clear, trustworthy, interoperable, secure, linked), have no counterpart. Here FDSA is clearer than Databricks.</td></tr>
<tr><td>4</td><td>Data catalogue</td><td>Genie Ontology, described as a continuously built graph of what the organisation knows.</td><td><span class="tag t-mid">Medium</span></td><td>Their catalogue is a knowledge graph for agents, not an ordering desk for people. That is where a data catalogue should be heading (7.3).</td></tr>
<tr><td>5</td><td>Data services</td><td>Spark, Genie (natural language to queries and apps), Agent Bricks (developer agents), and the "did it work" evaluation loop.</td><td><span class="tag t-ok">Strong</span></td><td>The Novo Nordisk and Rivian examples are foundation 6 in practice: scientists and analysts getting answers without a data engineer in the loop. The evaluation loop is not described in FDSA.</td></tr>
<tr><td>6</td><td>Data management</td><td>Genie Ontology and the question "what does it mean", with 50 to 84.5 percent accuracy.</td><td><span class="tag t-ok">Strong</span></td><td>The essay's best argument for 7.1. Suitable for any leadership team.</td></tr>
<tr><td>7</td><td>Data exchange</td><td>Open table formats (Delta Lake, Iceberg, Hudi) that made the lakehouse possible.</td><td><span class="tag t-mid">Medium</span></td><td>Hits 1.2 and FDSA's third sovereignty precondition on open formats. No counterpart for exchange with allies or across classification levels.</td></tr>
<tr><td>8</td><td>Data exploitation</td><td>The whole section on the shape of the firm: agents as information routers, "everyone becomes a manager", fewer layers. The example is Jack Dorsey's Block, which wants to replace middle management with a shared model of the whole business.</td><td><span class="tag t-ok">Strong, but civilian</span></td><td>Their application area is enterprise management in a payments company. Transfer to the chain of command with care. Ghodsi himself separates the org chart from information flow; that distinction matters more in the Armed Forces than at Block.</td></tr>
<tr><td>9</td><td>Platform and infrastructure</td><td>Lakehouse (analytical) and Lakebase (operational) on the same storage, called LTAP. Databases that can be branched and discarded like code.</td><td><span class="tag t-ok">Strong</span></td><td>LTAP is exactly FDSA's fourth scenario, fusing operational and analytical data without plumbing between them. One storage layer under both is a concrete requirement for the technical target architecture that follows FDSA. The principle holds regardless of which product realises it.</td></tr>
<tr><td>10</td><td>Communications infrastructure</td><td>None.</td><td><span class="tag t-none">None</span></td><td>The essay assumes always-on cloud. Degraded environments (DDIL), fog and edge do not exist in their world, which is also why the product itself is not an answer for the Armed Forces.</td></tr>
</tbody>
</table>
</div>
<h2>Three things FDSA can learn from the essay</h2>
<p>The essay confirms a great deal, but it also has three things FDSA does not have yet. They are what make the note worth reading, and this is where I move from reporting to opinion. These are my own assessments.</p>
<div class="box">
<h3>1. Agents are actors with identity, and that is an identity matter</h3>
<p>FDSA speaks of a common authoritative identity for personnel, materiel and information systems (2.2). Databricks adds agents as a fourth category, with their own identity, budget and limits. When a case officer can launch a hundred agents a day, one login per employee is not enough, and neither is letting the agent "borrow" the officer's identity. Afterwards, nobody knows whether the human or the agent fetched the data.</p>
<p>As I see it, this means three requirements for identity and access management that should be in place before the first agent-based tool goes live: an agent gets its own identity tied to whoever launched it; an agent gets an explicit mandate (which data, which actions, which budget) expressible as ABAC attributes; and every action is traced to the agent identity, not just to its owner. It is the same principle as a materiel system having its own identity rather than the operator's.</p>
</div>
<div class="box">
<h3>2. The evaluation loop is a capability</h3>
<p>"Did it work" is not a reporting requirement but part of the record. Every time a human corrects an agent, the correction should improve the next run, and what was learned should stay in the organisation's own record, not with the model vendor. FDSA has feedback from user to data steward (5.3), but not the loop from outcome back to policy and model. I think it is a natural next step as FDSA is made concrete in a technical target architecture.</p>
</div>
<div class="box">
<h3>3. Databases that can be branched</h3>
<p>Lakebase copies only changes, so an agent can create an isolated branch of a database in seconds, experiment, and throw it away. That is what makes it safe to let agents work against real data. I would make it a requirement on the platform layer: data products can be branched without copying the whole dataset.</p>
</div>
<h2>The sovereign firm, and Parliament's decisions</h2>
<p>The part of the essay that should be read aloud to anyone wondering why the Norwegian Armed Forces insist on data sovereignty is the passage about two banks. Both rent the same AI model for credit decisions. The first sends in a question, gets an answer and stores the decision. The second stores which facts produced the answer, every human override, which decisions turned out right, and adjusts its rules continuously. A year later, when a better model arrives, only one of them has built a better bank. If the new model inherits the definitions, the permissions, the evaluated cases and the exceptions, that is a lasting advantage. If it starts from the same prompt as everyone else, it is not.</p>
<div class="fignum">Figure 3 · The sovereign firm: two banks, same AI model</div>
</div>
<!-- ===================== FIGUR 3 (felles) ===================== -->
<figure class="svgwrap" style="margin-top:0">
<div class="arch-box">
<div class="arch-bar"><button type="button" class="arch-close"><span data-l="nb">✕ Lukk</span><span data-l="en" hidden>✕ Close</span></button></div>
<iframe data-l="nb" class="arch fig3" title="Figur 3 · Det suverene firmaet: to banker, samme KI-modell" srcdoc="<!DOCTYPE html>
<html lang="nb" 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.2.0">
<meta name="archify-reader-contract" content="declared-wide-v1">
<title>Det suverene firmaet: to banker, samme KI-modell · 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: #94a3b8;
--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: #e2e7ec;
--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: #64748b;
--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. Compact automatic sequences fit at
their authored text size or larger. Long sequences and waterfall rows
favor reading width and page scroll; other diagrams keep height fitting.
Small automatic canvases retain a modest scale inside a usable shell.
Every strategy preserves the authored SVG/viewBox and semantic geometry. */
.container {
width: 100%;
max-width: var(--archify-reader-width, 1440px);
margin: 0 auto;
}
html[data-reader-area="true"] .diagram-container {
min-height: var(--archify-diagram-min-height);
display: flex;
align-items: center;
}
html[data-reader-area="true"] .diagram-container > svg { flex-shrink: 0; }
/* The original SVG legend can use the outer corner while the capped or fitted
diagram stays centered. The container still clips all outer paint. */
.diagram-container:has([data-reader-legend-corner]) > svg { overflow: visible; }
.diagram-container [data-reader-legend-corner] {
transform-origin: 0 0;
transform-box: view-box;
transform: var(--archify-reader-legend-transform);
}
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-entry="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.95rem;
height: 0.95rem;
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.95rem;
height: 0.95rem;
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"] {
position: relative;
border-radius: 0;
background: color-mix(in srgb, var(--panel-border) 38%, 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="reset"]::after,
.diagram-nav [data-view="in"]::after {
position: absolute;
top: 0.55rem;
bottom: 0.55rem;
left: 0;
width: 1px;
background: color-mix(in srgb, var(--panel-border) 75%, transparent);
content: "";
pointer-events: none;
}
.diagram-nav [data-view="out"]:hover,
.diagram-nav [data-view="reset"]:hover,
.diagram-nav [data-view="in"]:hover { background: color-mix(in srgb, var(--panel-border) 60%, 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(--panel-border) 38%, 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; }
.diagram-guide-credit {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.6rem;
padding: 0.46rem 0.9rem 0.56rem;
border-top: 1px solid color-mix(in srgb, var(--toolbar-border) 74%, transparent);
color: var(--text-faint);
font-size: 0.5rem;
letter-spacing: 0.02em;
text-decoration: none;
}
.diagram-guide-credit-link {
display: inline-flex;
align-items: center;
gap: 0.28rem;
flex: none;
opacity: 0.72;
transition: opacity 120ms ease, color 120ms ease;
}
.diagram-guide-star {
width: 0.62rem;
height: 0.62rem;
background: currentColor;
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 1.6l1.9 3.9 4.3.6-3.1 3 .7 4.3L8 11.4l-3.8 2 .7-4.3-3.1-3 4.3-.6z' fill='none' stroke='black' stroke-width='1.3' 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 16 16'%3E%3Cpath d='M8 1.6l1.9 3.9 4.3.6-3.1 3 .7 4.3L8 11.4l-3.8 2 .7-4.3-3.1-3 4.3-.6z' fill='none' stroke='black' stroke-width='1.3' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
}
.diagram-guide-credit:hover .diagram-guide-star,
.diagram-guide-credit:focus-visible .diagram-guide-star {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 1.6l1.9 3.9 4.3.6-3.1 3 .7 4.3L8 11.4l-3.8 2 .7-4.3-3.1-3 4.3-.6z' stroke='black' stroke-width='1.3' 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 16 16'%3E%3Cpath d='M8 1.6l1.9 3.9 4.3.6-3.1 3 .7 4.3L8 11.4l-3.8 2 .7-4.3-3.1-3 4.3-.6z' stroke='black' stroke-width='1.3' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.diagram-guide-credit:hover .diagram-guide-credit-link,
.diagram-guide-credit:focus-visible .diagram-guide-credit-link { opacity: 1; color: var(--cloud-stroke); }
.diagram-guide-credit:focus-visible { outline: 2px solid var(--cloud-stroke); outline-offset: -2px; }
@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.35rem;
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) 14%, 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;
scrollbar-color: color-mix(in srgb, var(--panel-border) 90%, transparent) transparent;
}
/* Slim overlay-style thumb instead of the chunky platform default. */
.node-finder-results::-webkit-scrollbar { width: 10px; }
.node-finder-results::-webkit-scrollbar-track { background: transparent; }
.node-finder-results::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--panel-border) 85%, transparent);
border: 3px solid transparent;
border-radius: 999px;
background-clip: padding-box;
}
.node-finder-results::-webkit-scrollbar-thumb:hover {
background: color-mix(in srgb, var(--text-muted) 65%, transparent);
border: 3px solid transparent;
background-clip: padding-box;
}
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: 3rem;
padding: 0.52rem 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;
}
/* A zero count is absence, not information — let it recede. */
.node-finder-result em[data-zero] { color: var(--text-faint); }
.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;
/* Card notes are authored prose and may carry an unbreakable run such as a
slash-joined pipeline or a long identifier. Without this, that run sets the
card's min-content width, spills past its track in every rail placement,
and pushes the whole page into horizontal overflow at desktop widths.
`anywhere` rather than `break-word` because only `anywhere` shrinks the
intrinsic minimum the layout is sized from. */
overflow-wrap: anywhere;
}
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 {
display: grid;
gap: 0.5rem;
padding: 0;
overflow: visible;
-webkit-mask-image: none;
mask-image: none;
}
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) { padding-bottom: 0.25rem; }
html[data-reader-rail="bottom"] .node-outline-group > ul {
display: flex;
flex-wrap: wrap;
gap: 0.25rem 0.5rem;
}
html[data-reader-rail="bottom"] .node-outline-group > ul::before { display: none; }
html[data-reader-rail="bottom"] .node-outline-group li {
flex: 0 1 auto;
min-width: min(100%, 12rem);
max-width: min(100%, 32rem);
}
html[data-reader-rail="bottom"] .node-outline-heading {
justify-content: flex-start;
gap: 0.5rem;
}
html[data-reader-rail="bottom"] .node-outline-heading > span { overflow-wrap: anywhere; }
.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); }
/* The desktop bottom rail is an index: compact rows keep related nodes
together while allowing full names and descriptions to wrap naturally. */
html[data-reader-rail="bottom"] .node-outline-item {
grid-template-columns: 0.625rem minmax(0, 1fr) 0.75rem;
align-items: first baseline;
column-gap: 0.375rem;
min-height: 0;
padding-block: 0.375rem;
}
html[data-reader-rail="bottom"] .node-outline-item strong,
html[data-reader-rail="bottom"] .node-outline-item small {
overflow: visible;
text-overflow: clip;
white-space: normal;
overflow-wrap: anywhere;
}
html[data-reader-rail="bottom"] .node-outline-item strong {
grid-column: 2;
grid-row: 1;
}
html[data-reader-rail="bottom"] .node-outline-item small {
grid-column: 2;
grid-row: 2;
text-align: left;
}
html[data-reader-rail="bottom"] .node-outline-item::after {
grid-column: 3;
grid-row: 1;
align-self: center;
}
.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;
}
/* The motion button breathes on the relay cycle, so the chrome keeps time
with the wave it controls. Motion Governor mirrors the planned cycle from
the diagram SVG onto the document. */
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden]) #btn-motion .motion-control-dot {
animation: archify-motion-dot-breathe var(--flow-cycle-chrome, 6.4s) ease-in-out infinite;
}
@keyframes archify-motion-dot-breathe {
0%, 100% { opacity: 1; transform: scale(1); }
46% { opacity: 0.55; transform: scale(0.72); }
}
.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) {
--ambient-node-base-filter: drop-shadow(0 1px 1px rgba(17, 24, 39, 0.05)) drop-shadow(0 6px 12px rgba(17, 24, 39, 0.06));
filter: var(--ambient-node-base-filter);
}
html[data-preset="classic"][data-theme="dark"] .diagram-container > svg [data-node-id] > rect:not(.c-mask) {
--ambient-node-base-filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.35));
filter: var(--ambient-node-base-filter);
}
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;
}
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; }
/* Live plays one relay per cycle: a comet (halo, tail, head) leaves each
source when the signal reaches it, travels at one shared speed, leaves a
soft wake, and lands as a ripple while its target glows. Downstream edges
depart only after their source is reached, so the reader sees the path the
diagram describes rather than every edge shimmering at once. Sequence
messages relay in authored chronological order. Motion Governor plans the
timeline and its per-edge keyframes; this stylesheet owns appearance.
Original line language and markers remain static. Node entry is bounded to
one pass per document. */
svg[data-animation="trace"] {
--flow-cycle: 6.4s;
--flow-halo-peak: 0.17;
--flow-glow-peak: 0.5;
--flow-tail-peak: 0.42;
--flow-head-peak: 0.95;
--flow-wake-peak: 0.3;
--flow-ripple-peak: 0.65;
}
.ambient-edge-flow {
fill: none;
stroke: var(--arrow-emphasis);
stroke-linecap: round;
pointer-events: none;
display: none;
}
.ambient-flow-wake {
--flow-peak: var(--flow-wake-peak, 0.3);
stroke-width: 3.2;
stroke-dasharray: var(--flow-dasharray, none);
}
.ambient-flow-halo {
--flow-peak: var(--flow-halo-peak, 0.17);
stroke-width: 6;
stroke-dasharray: var(--flow-dash, 12px) calc(var(--flow-len, 0px) - var(--flow-dash, 12px));
}
/* A wide blurred streak centered on the head: this is the bloom that makes
the comet read as a traveling orb of light rather than a bright dash. */
.ambient-flow-glow {
--flow-peak: var(--flow-glow-peak, 0.5);
stroke-width: 9;
stroke-dasharray: var(--flow-dash, 12px) calc(var(--flow-len, 0px) - var(--flow-dash, 12px));
filter: blur(2.5px);
}
.ambient-flow-tail {
--flow-peak: var(--flow-tail-peak, 0.42);
stroke-width: 2.4;
stroke-dasharray: var(--flow-dasharray, var(--flow-dash, 20px) calc(var(--flow-len, 0px) - var(--flow-dash, 20px)));
}
.ambient-flow-head {
--flow-peak: var(--flow-head-peak, 0.95);
stroke: color-mix(in srgb, var(--arrow-emphasis) 40%, white);
stroke-width: 2.6;
stroke-dasharray: var(--flow-dash, 6px) calc(var(--flow-len, 0px) - var(--flow-dash, 6px));
}
.ambient-flow-ripple {
--flow-peak: var(--flow-ripple-peak, 0.65);
stroke: color-mix(in srgb, var(--arrow-emphasis) 55%, white);
stroke-width: 2;
}
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden]) .ambient-edge-flow {
display: inline;
animation-name: var(--flow-name, none);
animation-duration: var(--flow-cycle, 6.4s);
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-delay: var(--flow-delay, 0s);
/* Without backwards fill a delayed layer would sit at its natural opacity
(fully lit) until its departure arrives. */
animation-fill-mode: backwards;
}
/* Stream packets trail the comet head as smaller, dimmer beads. */
.ambient-flow-head.packet {
--flow-peak: calc(var(--flow-head-peak, 0.95) * 0.62);
stroke-width: 3.4;
}
/* Timeline and waterfall playhead: a crisp rule over a soft bloom. */
.ambient-flow-playhead {
--flow-peak: 0.9;
stroke: color-mix(in srgb, var(--arrow-emphasis) 55%, white);
stroke-width: 1.6;
}
.ambient-flow-playhead-glow {
--flow-peak: 0.32;
stroke-width: 10;
filter: blur(4px);
}
/* Waterfall spans fill in proportion to their measured time, then settle. */
.ambient-flow-fill {
--flow-peak: 0.5;
fill: color-mix(in srgb, var(--flow-fill, var(--arrow-emphasis)) 55%, white);
stroke: none;
transform-box: fill-box;
transform-origin: left center;
}
.ambient-flow-fill-failed { fill: var(--security-stroke); --flow-peak: 0.5; }
html[data-theme="light"] .ambient-flow-playhead { stroke: color-mix(in srgb, var(--arrow-emphasis) 70%, black); }
html[data-theme="light"] .ambient-flow-fill { --flow-peak: 0.42; fill: color-mix(in srgb, var(--flow-fill, var(--arrow-emphasis)) 70%, black); }
svg[data-preset="editorial"] .ambient-flow-fill { --flow-peak: 0.2; }
svg[data-preset="editorial"] .ambient-flow-playhead-glow { --flow-peak: 0.14; }
/* Each decorative flow follows its owning edge through tree visibility. */
.ambient-edge-flow[data-tree-hidden] { display: none !important; animation: none !important; }
svg[data-preset="blueprint"] .ambient-edge-flow { stroke-linecap: square; }
svg[data-preset="editorial"] { --flow-halo-peak: 0.07; --flow-glow-peak: 0.28; --flow-tail-peak: 0.2; --flow-head-peak: 0.7; --flow-wake-peak: 0.14; --flow-ripple-peak: 0.3; }
/* On light canvases the relay reads by saturation instead of bloom: the head
and ripple mix toward ink instead of white, and every layer's peak rises so
the wave survives the bright background. */
html[data-theme="light"] svg[data-animation="trace"] {
--flow-halo-peak: 0.22;
--flow-glow-peak: 0.4;
--flow-tail-peak: 0.5;
--flow-wake-peak: 0.34;
}
html[data-theme="light"] .ambient-flow-head {
stroke: color-mix(in srgb, var(--arrow-emphasis) 55%, black);
stroke-width: 2.8;
}
html[data-theme="light"] .ambient-flow-ripple {
stroke: color-mix(in srgb, var(--arrow-emphasis) 65%, black);
}
html[data-ambient-entry="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 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
html[data-ambient-entry="running"] svg[data-preset="signal-flow"][data-animation="trace"] [data-animate="node"] {
animation: archify-node-pulse 3.1s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
html[data-ambient-entry="running"] svg[data-preset="blueprint"][data-animation="trace"] [data-animate="node"] {
animation: archify-blueprint-node-pulse 3.8s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
html[data-ambient-entry="running"] svg[data-preset="editorial"][data-animation="trace"] [data-animate="node"] {
animation: archify-editorial-node-pulse 3.5s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
/* Each node glows in its planned windows: when the relay reaches it, while a
walk token rests on it, as a message lands, or as the playhead passes.
During the bounded entrance all three animations run; the entrance pulse is
listed after the repeating glow and wins on filter until it completes. */
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden]) svg[data-animation="trace"] [data-animate="node"] {
animation: var(--flow-node-name, none) var(--flow-cycle, 6.4s) linear infinite;
}
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden])[data-ambient-entry="running"] svg[data-animation="trace"] [data-animate="node"] {
animation: var(--flow-node-name, none) var(--flow-cycle, 6.4s) linear infinite,
archify-node-pulse 3.6s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden])[data-ambient-entry="running"] svg[data-preset="signal-flow"][data-animation="trace"] [data-animate="node"] {
animation: var(--flow-node-name, none) var(--flow-cycle, 6.4s) linear infinite,
archify-node-pulse 3.1s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden])[data-ambient-entry="running"] svg[data-preset="blueprint"][data-animation="trace"] [data-animate="node"] {
animation: var(--flow-node-name, none) var(--flow-cycle, 6.4s) linear infinite,
archify-blueprint-node-pulse 3.8s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden])[data-ambient-entry="running"] svg[data-preset="editorial"][data-animation="trace"] [data-animate="node"] {
animation: var(--flow-node-name, none) var(--flow-cycle, 6.4s) linear infinite,
archify-editorial-node-pulse 3.5s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
@keyframes archify-node-enter {
from { opacity: 0; }
to { opacity: 1; }
}
@media print {
.ambient-edge-flow { display: none !important; animation: none !important; }
svg[data-animation="trace"] [data-animate="node"] { animation: none !important; }
}
@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) {
.ambient-edge-flow { display: none !important; animation: none !important; }
html[data-preset="signal-flow"] .diagram-container::before {
animation: none !important;
}
.pulse-dot { animation: none !important; }
.motion-control-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; }
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="Diagramhandlinger">
<div class="toolbar-group">
<button id="btn-theme" type="button"
title="Bytt tema (T)"
aria-label="Bytt fargetema"
aria-pressed="false">
<span id="theme-icon" class="toolbar-icon" aria-hidden="true"></span>
<span id="theme-label">Mørk</span>
</button>
<div class="preset-wrap">
<button id="btn-preset" type="button"
title="Velg visuell stil (S bytter)"
aria-label="Velg visuell stil"
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">Stil</span>
<span class="toolbar-icon toolbar-chevron" aria-hidden="true"></span>
</button>
<div class="preset-menu" id="preset-menu" role="menu" aria-label="Visuell stil">
<div class="preset-menu-heading" role="presentation"><span>Visuell identitet</span><span class="preset-menu-shortcut"><kbd>S</kbd>for å bytte</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>Klassisk</strong><small>Stabil teknisk standard</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>Signalflyt</strong><small>Presentasjon med vekt på bevegelse</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>Blåkopi</strong><small>Teknisk gjennomgang</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>Redaksjonell</strong><small>Publikasjoner og lanseringsnotater</small></span>
<span class="preset-option-check" aria-hidden="true"></span>
</button>
</div>
</div>
<button id="btn-motion" type="button" hidden
title="Sett bevegelse på pause"
aria-label="Sett bevegelse på pause"
aria-pressed="true">
<span class="motion-control-dot" aria-hidden="true"></span>
<span id="motion-label">Levende</span>
</button>
<button id="btn-present" type="button"
title="Presentasjonsmodus (F)"
aria-label="Gå til presentasjonsmodus"
aria-pressed="false">
<span id="present-icon" class="toolbar-icon" aria-hidden="true"></span>
<span id="present-label">Presenter</span>
</button>
</div>
<div class="export-wrap">
<button id="btn-export" type="button"
title="Eksporter diagram (E)"
aria-label="Eksporter diagram"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="export-menu">
Eksporter <span class="toolbar-icon toolbar-chevron" aria-hidden="true"></span>
</button>
<div class="export-menu" id="export-menu" role="menu" aria-label="Eksporter">
<div class="export-menu-header" role="presentation">
<div><strong>Eksporter diagram</strong><span>Portable, rene filer</span></div>
<kbd>E</kbd>
</div>
<div class="export-menu-section" role="group" aria-label="Del">
<span class="export-menu-heading" aria-hidden="true">Del</span>
<button data-action="route-share-card" type="button" role="menuitem" tabindex="-1" hidden disabled><span class="export-item-copy"><strong>Delingskort for rute</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>Delingskort for rekkevidde</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>Kopier diagram</strong><small class="hint">PNG til utklippstavle</small></span></button>
</div>
<div class="export-menu-section" role="group" aria-label="Rasterbilder">
<span class="export-menu-heading" aria-hidden="true">Bilde</span>
<button data-format="png" type="button" role="menuitem" tabindex="-1"><span class="export-item-copy"><strong>PNG</strong><small class="hint">Tapsfritt bilde</small></span></button>
<button data-format="jpeg" type="button" role="menuitem" tabindex="-1"><span class="export-item-copy"><strong>JPEG</strong><small class="hint">Kompakt bilde</small></span></button>
<button data-format="webp" type="button" role="menuitem" tabindex="-1"><span class="export-item-copy"><strong>WebP</strong><small class="hint">Moderne bilde</small></span></button>
</div>
<div class="export-menu-section" role="group" aria-label="Vektor og bevegelse">
<span class="export-menu-heading" aria-hidden="true">Vektor og bevegelse</span>
<button data-format="svg" type="button" role="menuitem" tabindex="-1"><span class="export-item-copy"><strong>SVG · Auto</strong><small class="hint">Følger vertssidens tema</small></span></button>
<button data-format="svg-light" type="button" role="menuitem" tabindex="-1"><span class="export-item-copy"><strong>SVG · Lys</strong><small class="hint">Alltid lys</small></span></button>
<button data-format="svg-dark" type="button" role="menuitem" tabindex="-1"><span class="export-item-copy"><strong>SVG · Mørk</strong><small class="hint">Alltid mørk</small></span></button>
<button data-format="webm" type="button" role="menuitem" tabindex="-1"><span class="export-item-copy"><strong>WebM</strong><small class="hint">6 s bevegelse</small></span></button>
</div>
</div>
</div>
</div>
<div class="container">
<!-- Header -->
<div class="header">
<div class="header-row"
data-preset-badge-signal-flow="SIGNALFLYT"
data-preset-badge-blueprint="BLÅKOPI / REV 01"
data-preset-badge-editorial="REDAKSJONELL / FELTNOTAT">
<div class="pulse-dot"></div>
<h1>Det suverene firmaet: to banker, samme KI-modell</h1>
</div>
</div>
<script id="archify-i18n-data" type="application/json">{"locale":"nb","messages":{"viewer.kind.frontend":"Frontend","viewer.kind.backend":"Backend","viewer.kind.database":"Database","viewer.kind.cloud":"Sky","viewer.kind.security":"Sikkerhet","viewer.kind.messagebus":"Meldingsbuss","viewer.kind.external":"Ekstern","viewer.kind.neutral":"Nøytral","viewer.kind.node":"Node","viewer.kind.start":"Start","viewer.kind.active":"Aktiv","viewer.kind.waiting":"Venter","viewer.kind.decision":"Beslutning","viewer.kind.success":"Suksess","viewer.kind.failure":"Feil","viewer.toolbar.actions":"Diagramhandlinger","viewer.theme.toggle.title":"Bytt tema (T)","viewer.theme.toggle":"Bytt fargetema","viewer.theme.dark":"Mørk","viewer.theme.light":"Lys","viewer.preset.choose.title":"Velg visuell stil (S bytter)","viewer.preset.choose":"Velg visuell stil","viewer.preset.style":"Stil","viewer.preset.menu":"Visuell stil","viewer.preset.identity":"Visuell identitet","viewer.preset.cycles":"for å bytte","viewer.preset.classic":"Klassisk","viewer.preset.classic.short":"Klassisk","viewer.preset.classic.hint":"Stabil teknisk standard","viewer.preset.flow":"Signalflyt","viewer.preset.flow.short":"Flyt","viewer.preset.flow.hint":"Presentasjon med vekt på bevegelse","viewer.preset.blueprint":"Blåkopi","viewer.preset.blueprint.hint":"Teknisk gjennomgang","viewer.preset.editorial":"Redaksjonell","viewer.preset.editorial.hint":"Publikasjoner og lanseringsnotater","viewer.preset.badge.signalFlow":"SIGNALFLYT","viewer.preset.badge.blueprint":"BLÅKOPI / REV 01","viewer.preset.badge.editorial":"REDAKSJONELL / FELTNOTAT","viewer.preset.badge.editorialPlate":"ARCHIFY / PLANSJE 04","viewer.preset.current":"Visuell stil: {style}. Velg visuell stil","viewer.motion.live":"Levende","viewer.motion.still":"Stille","viewer.motion.pause":"Sett bevegelse på pause","viewer.motion.resume":"Gjenoppta bevegelse","viewer.motion.reduced":"Bevegelse satt på pause av innstillingen for redusert bevegelse","viewer.motion.hidden":"Bevegelse satt på pause mens siden er skjult","viewer.motion.yielding":"Sett bevegelse på pause; viker nå for {owner}","viewer.motion.yielding.title":"Levende forhåndsvisning aktivert · viker for {owner}","viewer.owner.route":"Rutesonde","viewer.owner.lens":"Semantisk linse","viewer.owner.relationship":"Forhåndsvisning av relasjon","viewer.owner.intent":"Intensjonsspor","viewer.owner.focus":"semantisk fokus","viewer.owner.legend":"forhåndsvisning av tegnforklaring","viewer.owner.reader":"leserinteraksjon","viewer.present.enter":"Gå til presentasjonsmodus","viewer.present.enter.title":"Presentasjonsmodus (F)","viewer.present.exit":"Avslutt presentasjonsmodus","viewer.present.exit.title":"Avslutt presentasjonsmodus (F eller Escape)","viewer.present.present":"Presenter","viewer.present.exit.label":"Avslutt","viewer.export.button":"Eksporter","viewer.export.button.title":"Eksporter diagram (E)","viewer.export.diagram":"Eksporter diagram","viewer.export.menu":"Eksporter","viewer.export.subtitle":"Portable, rene filer","viewer.export.share":"Del","viewer.export.shareCard":"Delingskort","viewer.export.routeShareCard":"Delingskort for rute","viewer.export.reachShareCard":"Delingskort for rekkevidde","viewer.export.copyDiagram":"Kopier diagram","viewer.export.clipboardPng":"PNG til utklippstavle","viewer.export.raster":"Rasterbilder","viewer.export.image":"Bilde","viewer.export.lossless":"Tapsfritt bilde","viewer.export.compact":"Kompakt bilde","viewer.export.modern":"Moderne bilde","viewer.export.vectorMotion":"Vektor og bevegelse","viewer.export.vectorMotion.heading":"Vektor og bevegelse","viewer.export.editable":"Redigerbar vektor","viewer.export.svg.auto":"SVG · Auto","viewer.export.svg.auto.hint":"Følger vertssidens tema","viewer.export.svg.light":"SVG · Lys","viewer.export.svg.light.hint":"Alltid lys","viewer.export.svg.dark":"SVG · Mørk","viewer.export.svg.dark.hint":"Alltid mørk","viewer.export.motion6s":"6 s bevegelse","viewer.export.unsupported":"Støttes ikke av denne nettleseren","viewer.export.clipboardUnsupported":"Denne nettleseren støtter ikke skriving av bilder til utklippstavlen","viewer.export.clipboardUnsupported.short":"Skriving av bilder til utklippstavlen støttes ikke i denne nettleseren.","viewer.export.motionUnavailable":"Bevegelsesopptak er ikke tilgjengelig i denne nettleseren","viewer.export.webmUnavailable":"WebM er ikke tilgjengelig i denne nettleseren","viewer.export.failed":"Eksport mislyktes: {message}","viewer.export.unknownVariant":"Ukjent variant av delingskort: {variant}","viewer.export.routeRequired":"Spor en rute før du eksporterer delingskort for rute","viewer.export.reachRequired":"Spor definert rekkevidde før du eksporterer delingskort for rekkevidde","viewer.export.unknown":"ukjent","viewer.export.routeFailed":"Eksport av delingskort for rute mislyktes: {message}","viewer.export.reachFailed":"Eksport av delingskort for rekkevidde mislyktes: {message}","viewer.export.copyFailed":"Kopiering mislyktes: {message}","viewer.export.copiedPng":"PNG kopiert til utklippstavlen","viewer.export.downloadedRoute":"Delingskort for rute lastet ned","viewer.export.downloadedReach":"Delingskort for rekkevidde lastet ned","viewer.export.downloadedWebm":"WebM lastet ned","viewer.export.recording":"Tar opp 6 sekunder med bevegelse…","viewer.export.card.routeSummary.one":"Rute: {source} → {target} · {count} steg","viewer.export.card.routeSummary.other":"Rute: {source} → {target} · {count} steg","viewer.export.card.reachSummary":"Definert: {direction} fra {origin} · {nodes} · {links} · maks {hops}","viewer.export.card.node.one":"{count} node","viewer.export.card.node.other":"{count} noder","viewer.export.card.link.one":"{count} kobling","viewer.export.card.link.other":"{count} koblinger","viewer.export.card.hop.one":"{count} hopp","viewer.export.card.hop.other":"{count} hopp","viewer.export.card.routeBadge":"ARCHIFY · RUTE · {hops}","viewer.export.card.reachBadge":"ARCHIFY · {direction} REKKEVIDDE","viewer.export.direction.upstream":"Oppstrøms","viewer.export.direction.downstream":"Nedstrøms","viewer.export.error.canvasUnavailable":"Canvas er ikke tilgjengelig for {label}","viewer.export.error.contextUnavailable":"2D-canvaskontekst er ikke tilgjengelig for {label}","viewer.export.error.toBlobUnavailable":"canvas.toBlob er ikke tilgjengelig for {label}","viewer.export.error.toBlobNull":"canvas.toBlob returnerte ingen data for {label}","viewer.export.error.variantsCombined":"Varianter av delingskort kan ikke kombineres","viewer.export.error.viewerState":"Eksport av delingskort kunne ikke fjerne midlertidig visningstilstand","viewer.export.error.routeState":"Eksport av delingskort for rute kunne ikke bevare den beregnede ruten på en sikker måte","viewer.export.error.reachState":"Eksport av delingskort for rekkevidde kunne ikke bevare den definerte rekkevidden på en sikker måte","viewer.export.error.webmRequirements":"Eksport av WebM-bevegelse krever en sporanimasjon og nettleserstøtte for MediaRecorder","viewer.export.error.mediaRecorder":"MediaRecorder mislyktes","viewer.export.error.emptyWebm":"MediaRecorder ga en tom WebM-fil","viewer.export.error.webmBackground":"SVG-bakgrunnen kunne ikke lastes inn for WebM-eksport","viewer.guide.title":"Utforsk dette systemet","viewer.focus.selectedNodes":"{count} valgte noder","viewer.guide.eyebrow":"Diagramguide","viewer.guide.close":"Lukk diagramguiden","viewer.guide.inspecting":"Inspiserer kompilert semantikk","viewer.guide.actions":"Handlinger for utforsking av diagrammet","viewer.guide.find":"Finn hvilken som helst node","viewer.guide.find.hint":"Søk i etiketter, ansvar, typer og stabile ID-er.","viewer.guide.route":"Spor en rute","viewer.guide.route.aria":"Spor en rettet rute","viewer.guide.route.hint":"Finn ut hvordan to semantiske noder henger sammen i definert retning.","viewer.guide.map":"Se hele systemet","viewer.guide.map.hint":"Åpne semantisk radar med levende visningsområde og stabile noder.","viewer.guide.lens":"Sammenlign semantiske typer","viewer.guide.lens.hint":"Tell roller, vis trafikken deres og sammenlign direkte definerte koblinger.","viewer.guide.present":"Gå til presentasjonsmodus","viewer.guide.present.hint":"Gi det levende diagrammet hele visningsområdet uten å endre eksporten.","viewer.guide.shortcuts":"Flere hurtigtaster","viewer.guide.shortcut.export":"Eksporter","viewer.guide.shortcut.theme":"Tema","viewer.guide.shortcut.style":"Stil","viewer.guide.shortcut.reset":"Tilbakestill","viewer.guide.shortcut.zoomIn":"Zoom inn","viewer.guide.shortcut.zoomOut":"Zoom ut","viewer.guide.shortcut.close":"Lukk","viewer.guide.credit":"Synes du at Archify er nyttig? En stjerne på GitHub hjelper","viewer.guide.star":"Gi stjerne","viewer.guide.facts":"{nodes} · {relationships}","viewer.guide.fact.node.one":"{count} semantisk node","viewer.guide.fact.node.other":"{count} semantiske noder","viewer.guide.fact.relationship.one":"{count} relasjon","viewer.guide.fact.relationship.other":"{count} relasjoner","viewer.guide.open":"Åpne diagramguiden","viewer.finder.title":"Finn en node","viewer.finder.close":"Lukk nodesøket","viewer.finder.placeholder":"Søk etter etiketter eller ID-er","viewer.finder.search":"Søk i diagramnoder","viewer.finder.results":"Diagramnoder","viewer.finder.empty":"Ingen samsvarende noder","viewer.finder.result.focus":"Fokuser på {label}","viewer.finder.result.routeStart":"Velg {label} som rutestart","viewer.finder.result.routeTarget":"Velg {label} som rutemål, {links}","viewer.finder.status.empty":"Ingen samsvarende noder","viewer.finder.status.count.one":"{count} samsvarende node","viewer.finder.status.count.other":"{count} samsvarende noder","viewer.finder.noun.nodes":"noder","viewer.finder.link.one":"{count} kobling","viewer.finder.link.other":"{count} koblinger","viewer.finder.result.focus.one":"Fokuser på {label}, {count} relatert kobling","viewer.finder.result.focus.other":"Fokuser på {label}, {count} relaterte koblinger","viewer.finder.status.filtered":"{visible} av {available} {noun}","viewer.finder.status.all":"{available} {noun}","viewer.passport.eyebrow":"Semantisk pass","viewer.passport.metadata":"Nodemetadata","viewer.passport.evidence":"Verifisert kildebevis","viewer.passport.verified":"Verifisert kilde","viewer.passport.verificationScope":"Verifisert mot lokal Git ved den låste revisjonen. Tilgang til eksternt repositorium er ikke kontrollert.","viewer.passport.reach":"Definert rekkevidde","viewer.passport.reach.trace":"Spor definert rekkevidde","viewer.passport.upstream":"Oppstrøms","viewer.passport.downstream":"Nedstrøms","viewer.passport.upstream.trace":"Spor definert rekkevidde oppstrøms","viewer.passport.downstream.trace":"Spor definert rekkevidde nedstrøms","viewer.passport.close":"Lukk semantisk pass","viewer.passport.move":"Flytt semantisk pass. Dra, bruk piltastene eller trykk Home for å tilbakestille.","viewer.passport.copy":"Kopier lenke","viewer.passport.copy.focus":"Kopier lenke til noden i fokus","viewer.passport.relations":"Relasjoner","viewer.passport.relations.show":"Vis tilknyttede relasjoner","viewer.passport.relations.hide":"Skjul tilknyttede relasjoner","viewer.passport.relations.list":"Tilknyttede relasjoner","viewer.passport.copyRelation":"Kopier relasjon","viewer.passport.copyNode":"Kopier node","viewer.passport.copyPinned":"Kopier lenke til festet relasjon","viewer.passport.copySource":"Kopier lenke til kildenoden","viewer.passport.copy.focused.success":"Lenke til noden i fokus kopiert","viewer.passport.copy.pinned.success":"Lenke til festet relasjon kopiert","viewer.passport.copy.focused.failed":"Kunne ikke kopiere lenken til noden i fokus","viewer.passport.copy.pinned.failed":"Kunne ikke kopiere lenken til festet relasjon","viewer.passport.relationship.none":"Ingen tilknyttede relasjoner","viewer.passport.relationship.count.one":"{count} relasjon","viewer.passport.relationship.count.other":"{count} relasjoner","viewer.passport.relationship.show.one":"Vis {count} tilknyttet relasjon","viewer.passport.relationship.show.other":"Vis {count} tilknyttede relasjoner","viewer.passport.relationship.summary":"{out} utgående · {in} inngående{loops}","viewer.passport.relationship.loops":" · selvløkker: {count}","viewer.passport.relationship.explorer":"Utforsker for direkte relasjoner","viewer.passport.relationship.help":"Bruk piltastene for å utforske relasjoner. Trykk Enter eller mellomrom for å feste detaljer; Escape fjerner dem.","viewer.passport.relationship.loopsBack":"kobler til seg selv","viewer.passport.relationship.connectsTo":"kobler til","viewer.passport.relationship.connectsFrom":"kommer fra","viewer.passport.relationship.pinned":"Festet relasjon · {from} → {to} · {label}","viewer.passport.relationship.inspect":"Inspiser relasjon {index} av {total}: {from} til {to}, {label}. Trykk Enter for detaljer.","viewer.passport.relationship.group.out":"Utgående","viewer.passport.relationship.group.in":"Inngående","viewer.passport.relationship.group.loop":"Selvløkker","viewer.passport.relationship.row":"{group}: {relationship}, {neighbor}","viewer.passport.relationship.direction.out":"UT →","viewer.passport.relationship.direction.in":"← INN","viewer.passport.relationship.direction.loop":"LØKKE","viewer.passport.sourceCount.one":"{count} verifisert kildereferanse","viewer.passport.sourceCount.other":"{count} verifiserte kildereferanser","viewer.passport.repository.open":"Åpne verifisert revisjon {revision} av repositoriet","viewer.passport.source.open":"Åpne verifisert kilde {path} ved revisjon {revision}","viewer.passport.source.openLink":"Åpne ↗","viewer.passport.reach.upstream.one":"Spor {count} definert node oppstrøms","viewer.passport.reach.upstream.other":"Spor {count} definerte noder oppstrøms","viewer.passport.reach.downstream.one":"Spor {count} definert node nedstrøms","viewer.passport.reach.downstream.other":"Spor {count} definerte noder nedstrøms","viewer.passport.reach.noUpstream":"Ingen definerte noder oppstrøms","viewer.passport.reach.noDownstream":"Ingen definerte noder nedstrøms","viewer.passport.reach.status":"{direction} · {nodes} noder · {links} koblinger · maks {hops} hopp","viewer.route.eyebrow":"Rute","viewer.route.start":"Klikk der ruten starter","viewer.route.start.find":"Finn start","viewer.route.start.find.aria":"Finn en rutestart","viewer.route.copy":"Kopier lenke","viewer.route.copy.aria":"Kopier lenke til sporet rute","viewer.route.clear":"Fjern","viewer.route.clear.aria":"Fjern rutesonde","viewer.route.traced":"Rute","viewer.route.pickTwo":"Klikk på to noder i diagrammet","viewer.route.pickOne":"Klikk på en node i diagrammet","viewer.route.controls":"Kontroller for rutereisen","viewer.route.previous":"Forrige posisjon på ruten","viewer.route.play":"Spill av rutereisen","viewer.route.pause":"Sett rutereisen på pause","viewer.route.replay":"Spill av rutereisen på nytt","viewer.route.next":"Neste posisjon på ruten","viewer.route.journey":"Reise","viewer.route.pause.label":"Pause","viewer.route.replay.label":"Spill på nytt","viewer.route.overview":"Oversikt","viewer.route.overview.aria":"Vis full oversikt over ruten","viewer.route.instructions":"Klikk på et startpunkt og deretter et sluttpunkt. Ruter følger pilene.","viewer.route.destination":"Hvor ender ruten fra {label}?","viewer.route.destination.find":"Finn mål","viewer.route.destination.find.aria":"Finn et rutemål som kan nås","viewer.route.differentDestination":"Velg et annet mål","viewer.route.distinct":"Velg to forskjellige noder.","viewer.route.unreachable":"Kan ikke nå {label} herfra","viewer.route.unreachable.detail":"{target} kan ikke nås fra {source} langs pilene. Velg en uthevet node.","viewer.route.start.instructions":"Velg deretter sluttpunktet. Bare noder du kan nå, lyser opp.","viewer.route.copy.success":"Lenke til sporet rute kopiert","viewer.route.copy.failed":"Kunne ikke kopiere lenken til sporet rute","viewer.route.position":"Posisjon {index} av {total} på ruten: {label}","viewer.route.step":"Steg {index} av {total} · {phase} · {label}","viewer.route.motionRequired":"Automatisk reise krever bevegelsesmodusen Levende","viewer.route.trigger.clear":"Fjern sporet rute","viewer.route.overview.status":"{nodes} · {hops} · korteste rute","viewer.route.overview.node.one":"{count} node","viewer.route.overview.node.other":"{count} noder","viewer.route.overview.hop.one":"{count} steg","viewer.route.overview.hop.other":"{count} steg","viewer.route.phase.playing":"Spiller av","viewer.route.phase.complete":"Fullført","viewer.route.phase.inspecting":"Inspiserer","viewer.route.destination.count.one":"{count} node er uthevet og kan nås. Klikk på den.","viewer.route.destination.count.other":"{count} noder er uthevet og kan nås. Klikk på en av dem.","viewer.route.noOutgoing":"Ingen piler går ut fra denne noden. Fjern den og velg et annet startpunkt.","viewer.route.result.title":"{source} til {target}","viewer.route.finder.source.title":"Velg rutestart","viewer.route.finder.source.placeholder":"Søk etter rutekilder","viewer.route.finder.source.empty":"Ingen samsvarende rutekilder","viewer.route.finder.source.results":"Noder som kan starte en rute","viewer.route.finder.source.noun":"rutekilder","viewer.route.finder.source.badge":"start","viewer.route.finder.target.title":"Mål fra {label}","viewer.route.finder.target.placeholder":"Søk etter mål som kan nås","viewer.route.finder.target.empty":"Ingen samsvarende mål som kan nås","viewer.route.finder.target.results":"Rutemål som kan nås","viewer.route.finder.target.noun":"mål som kan nås","viewer.route.hop.one":"{count} hopp","viewer.route.hop.other":"{count} hopp","viewer.lens.eyebrow":"Semantisk linse","viewer.lens.title":"Sammenlign systemroller","viewer.lens.close":"Lukk semantisk linse","viewer.lens.instruction":"Velg opptil to semantiske typer. Én viser den faktiske trafikken; to sammenligner bare direkte definerte relasjoner.","viewer.lens.kinds":"Semantiske typer","viewer.lens.choose":"Velg en type for å inspisere nodene og de tilstøtende relasjonene.","viewer.lens.copy":"Kopier lenke til semantisk linse","viewer.lens.clear":"Fjern semantisk linse","viewer.lens.open":"Åpne semantisk linse","viewer.lens.openActive":"Åpne aktiv semantisk linse","viewer.lens.legend":"Semantisk tegnforklaring","viewer.lens.legend.inspect.one":"Inspiser {label}, {count} node","viewer.lens.legend.inspect.other":"Inspiser {label}, {count} noder","viewer.lens.kind.count.one":"{label}, {count} node","viewer.lens.kind.count.other":"{label}, {count} noder","viewer.lens.compare.one":"{first} → {second}: {forward} · {second} → {first}: {reverse} · {count} direkte relasjon","viewer.lens.compare.other":"{first} → {second}: {forward} · {second} → {first}: {reverse} · {count} direkte relasjoner","viewer.lens.single":"{nodes} · {relationships} · tilknyttede naboer forblir synlige","viewer.lens.node.one":"{count} {label}-node","viewer.lens.node.other":"{count} {label}-noder","viewer.lens.relationship.one":"{count} tilstøtende relasjon","viewer.lens.relationship.other":"{count} tilstøtende relasjoner","viewer.radar.title":"Semantisk radar","viewer.radar.building":"Bygger oversikt","viewer.radar.openFull":"Åpne full semantisk radar","viewer.radar.open":"Åpne radar","viewer.radar.close":"Lukk semantisk radar","viewer.radar.surface":"Diagramoversikt. Klikk på en node for å sette fokus på den, eller bruk piltastene for å panorere.","viewer.radar.click":"Klikk på node","viewer.radar.drag":"Dra for å panorere","viewer.radar.space":"Semantisk radar trenger mer kartplass.","viewer.radar.nodes":"Noder i semantisk diagramradar","viewer.radar.focus":"Fokuser på {label} fra semantisk radar","viewer.radar.status":"{count} noder · {viewport}","viewer.radar.fullMap":"{count} noder · fullt kart","viewer.radar.compacted":"Radaren er komprimert for å unngå å dekke det semantiske passet eller kartkontrollene.","viewer.radar.cancelWaiting":"Avbryt venting på mer kartplass for semantisk radar","viewer.radar.needsSpace":"Semantisk radar trenger mer synlig kartplass","viewer.radar.viewport.full":"fullt kart","viewer.radar.viewport.width":"{percent} % bredde","viewer.radar.viewport.scale":"{percent} % av visningsområdet","viewer.nav.controls":"Kontroller for diagramvisning","viewer.nav.route":"Spor en rettet rute","viewer.nav.route.title":"Spor rute (R)","viewer.nav.route.short":"RUTE","viewer.nav.radar":"Åpne semantisk radar","viewer.nav.radar.title":"Semantisk radar (M)","viewer.nav.radar.short":"KART","viewer.nav.lens":"Åpne semantisk linse","viewer.nav.lens.title":"Semantisk linse (L)","viewer.nav.lens.short":"LINSE","viewer.nav.find":"Finn en node","viewer.nav.find.title":"Finn en node (/)","viewer.outline.title":"Nodeindeks","viewer.rail.show":"Notater og indeks","viewer.rail.controls":"Sidepanel","viewer.rail.collapse":"Fold sammen sidepanelet","viewer.rail.bottom":"Flytt panelet under diagrammet","viewer.rail.right":"Flytt panelet ved siden av diagrammet","viewer.nav.guide":"Åpne diagramguiden","viewer.nav.guide.title":"Diagramguide (?)","viewer.nav.zoomOut":"Zoom ut","viewer.nav.zoomOut.title":"Zoom ut (-)","viewer.nav.reset":"Tilbakestill diagramvisningen","viewer.nav.reset.title":"Tilbakestill visning (0)","viewer.nav.read":"LES","viewer.nav.zoomIn":"Zoom inn","viewer.nav.zoomIn.title":"Zoom inn (+)","viewer.nav.camera":"{hint}. Tilbakestill diagramvisningen","viewer.nav.camera.title":"{semantic}{hint} · tilbakestill visning (0)","viewer.nav.camera.semantic":"Aktivt semantisk kamera · ","viewer.nav.level.map":"KART","viewer.nav.level.read":"LES","viewer.nav.level.full":"FULL","viewer.nav.level.auto":"AUTO","viewer.nav.detail.map":"Zoom inn for å vise relasjonsetiketter og nodekontekst","viewer.nav.detail.read":"Zoom inn igjen for å vise tagger og merknader","viewer.nav.detail.full":"Fullt detaljnivå i diagrammet","viewer.intent.summary":"{label}. {out} utgående, {in} inngående{loops}. {total} koblinger. Trykk Enter for detaljer.","viewer.intent.loops":", selvløkker: {count}","viewer.common.copied":"Kopiert","viewer.common.copyFailed":"Kopiering mislyktes","viewer.common.copyLink":"Kopier lenke","viewer.common.clear":"Fjern","viewer.common.close":"Lukk"}}</script>
<!-- Main Diagram -->
<div class="diagram-container" data-detail-level="read"
data-preset-badge-editorial-plate="ARCHIFY / PLANSJE 04">
<svg viewBox="0 0 1306 638" data-reader-fit="intrinsic-height" data-lifecycle-layout="main-path-v3" role="img" lang="nb" aria-labelledby="archify-diagram-title archify-diagram-description" data-animation="trace" data-preset="classic" data-quality-profile="showcase" data-motion-choreography="walk">
<title id="archify-diagram-title">Det suverene firmaet: to banker, samme KI-modell</title>
<desc id="archify-diagram-description">Et livssyklusdiagram generert av 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)" />
<!-- Composite phases -->
<!-- Transition paths -->
<g data-graph-role="automatic-crossover" style="--step:0">
<path data-graph-role="automatic-crossover-underlay" d="M 225 60 L 297 60" fill="none" stroke="var(--mask)" stroke-width="5.8" stroke-linecap="round" stroke-linejoin="round" pointer-events="none"/>
<path data-edge-from="rented" data-edge-to="stored" data-edge-label="svar" data-edge-key="0" data-edge-id="t1" data-composition-points="225,60;297,60" data-composition-crossover="halo" data-composition-independent="true" d="M 225 60 L 297 60" class="a-emphasis" data-animate="edge" style="--step:0" stroke-width="1.8" marker-end="url(#arrowhead-emphasis)"/>
</g>
<g data-graph-role="automatic-crossover" style="--step:1">
<path data-graph-role="automatic-crossover-underlay" d="M 493 60 L 615 60" fill="none" stroke="var(--mask)" stroke-width="5.8" stroke-linecap="round" stroke-linejoin="round" pointer-events="none"/>
<path data-edge-from="stored" data-edge-to="facts" data-edge-label="det bak svaret" data-edge-key="1" data-edge-id="t2" data-composition-points="493,60;615,60" data-composition-crossover="halo" data-composition-independent="true" d="M 493 60 L 615 60" class="a-emphasis" data-animate="edge" style="--step:1" stroke-width="1.8" marker-end="url(#arrowhead-emphasis)"/>
</g>
<g data-graph-role="automatic-crossover" style="--step:2">
<path data-graph-role="automatic-crossover-underlay" d="M 767 60 L 841 60" fill="none" stroke="var(--mask)" stroke-width="5.8" stroke-linecap="round" stroke-linejoin="round" pointer-events="none"/>
<path data-edge-from="facts" data-edge-to="newmodel" data-edge-label="ett år" data-edge-key="2" data-edge-id="t3" data-composition-points="767,60;841,60" data-composition-crossover="halo" data-composition-independent="true" d="M 767 60 L 841 60" class="a-emphasis" data-animate="edge" style="--step:2" stroke-width="1.8" marker-end="url(#arrowhead-emphasis)"/>
</g>
<g data-graph-role="automatic-crossover" style="--step:3">
<path data-graph-role="automatic-crossover-underlay" d="M 1000 60 L 1098 60" fill="none" stroke="var(--mask)" stroke-width="5.8" stroke-linecap="round" stroke-linejoin="round" pointer-events="none"/>
<path data-edge-from="newmodel" data-edge-to="inherits" data-edge-label="bytt fritt" data-edge-key="3" data-edge-id="t4" data-composition-points="1000,60;1098,60" data-composition-crossover="halo" data-composition-independent="true" d="M 1000 60 L 1098 60" class="a-emphasis" data-animate="edge" style="--step:3" stroke-width="1.8" marker-end="url(#arrowhead-emphasis)"/>
</g>
<g data-graph-role="automatic-crossover" style="--step:4">
<path data-graph-role="automatic-crossover-underlay" d="M 319 92 L 319 184" fill="none" stroke="var(--mask)" stroke-width="5.2" stroke-linecap="round" stroke-linejoin="round" pointer-events="none"/>
<path data-edge-from="stored" data-edge-to="answeronly" data-edge-label="bare svaret" data-edge-key="4" data-edge-id="b1" data-composition-points="319,92;319,184" data-composition-crossover="halo" data-composition-independent="true" d="M 319 92 L 319 184" class="a-default" data-animate="edge" style="--step:4" stroke-width="1.2" marker-end="url(#arrowhead)"/>
</g>
<g data-graph-role="automatic-crossover" style="--step:5">
<path data-graph-role="automatic-crossover-underlay" d="M 299.8 248 L 299.8 340" fill="none" stroke="var(--mask)" stroke-width="5.2" stroke-linecap="round" stroke-linejoin="round" pointer-events="none"/>
<path data-edge-from="answeronly" data-edge-to="newmodel2" data-edge-label="ett år" data-edge-key="5" data-edge-id="b2" data-composition-points="299.8,248;299.8,340" data-composition-crossover="halo" data-composition-independent="true" d="M 299.8 248 L 299.8 340" class="a-default" data-animate="edge" style="--step:5" stroke-width="1.2" marker-end="url(#arrowhead)"/>
</g>
<g data-graph-role="automatic-crossover" style="--step:6">
<path data-graph-role="automatic-crossover-underlay" d="M 299.8 404 L 299.8 496" fill="none" stroke="var(--mask)" stroke-width="5.2" stroke-linecap="round" stroke-linejoin="round" pointer-events="none"/>
<path data-edge-from="newmodel2" data-edge-to="zero" data-edge-label="ingen arv" data-edge-key="6" data-edge-id="b3" data-composition-points="299.8,404;299.8,496" data-composition-crossover="halo" data-composition-independent="true" d="M 299.8 404 L 299.8 496" class="a-security" data-animate="edge" style="--step:6" stroke-width="1.2" marker-end="url(#arrowhead-security)"/>
</g>
<g data-graph-role="automatic-crossover" style="--step:7">
<path data-graph-role="automatic-crossover-underlay" d="M 471 92 L 471 184" fill="none" stroke="var(--mask)" stroke-width="5.2" stroke-linecap="round" stroke-linejoin="round" pointer-events="none"/>
<path data-edge-from="stored" data-edge-to="vendor" data-edge-label="hos leverandør" data-edge-key="7" data-edge-id="c1" data-composition-points="471,92;471,184" data-composition-crossover="halo" data-composition-independent="true" d="M 471 92 L 471 184" class="a-default" data-animate="edge" style="--step:7" stroke-width="1.2" marker-end="url(#arrowhead)"/>
</g>
<g data-graph-role="automatic-crossover" style="--step:8">
<path data-graph-role="automatic-crossover-underlay" d="M 512.3 248 L 512.3 340" fill="none" stroke="var(--mask)" stroke-width="5.2" stroke-linecap="round" stroke-linejoin="round" pointer-events="none"/>
<path data-edge-from="vendor" data-edge-to="loss" data-edge-label="vil bytte" data-edge-key="8" data-edge-id="c2" data-composition-points="512.3,248;512.3,340" data-composition-crossover="halo" data-composition-independent="true" d="M 512.3 248 L 512.3 340" class="a-security" data-animate="edge" style="--step:8" stroke-width="1.2" marker-end="url(#arrowhead-security)"/>
</g>
<!-- States -->
<g id="node-rented" data-node-id="rented" data-node-label="Samme modell leid" tabindex="0" role="button" aria-label="Fokuser på Samme modell leid, to banker, samme prompt, Livssyklustilstand" aria-pressed="false" data-node-kind="start" data-node-sublabel="to banker, samme prompt" data-node-context="Livssyklustilstand">
<title>Samme modell leid · to banker, samme prompt · Livssyklustilstand</title>
<rect x="56" y="28" width="169" height="64" rx="8" class="c-mask"/>
<rect x="56" y="28" width="169" height="64" rx="8" class="c-frontend" data-animate="node" style="--step:0" stroke-width="1.5"/>
<g aria-hidden="true" data-lifecycle-initial-marker=""><circle cx="30" cy="60" r="4.5" style="fill: var(--arrow-emphasis)"/><path d="M 34.5 60 L 49 60" class="a-emphasis" stroke-width="1.4"/><path d="M 48 56.5 L 55 60 L 48 63.5 Z" style="fill: var(--arrow-emphasis)"/></g>
<text data-detail="fine" x="79" y="42" class="t-frontend" font-size="8" font-weight="700">01</text>
<text data-node-label="" data-detail-anchor="" x="152.8" y="55" class="t-primary" font-size="12" font-weight="600" text-anchor="middle">Samme modell leid</text>
<text data-detail="context" x="140.5" y="73" class="t-muted" font-size="9" text-anchor="middle">to banker, samme prompt</text>
</g>
<g id="node-stored" data-node-id="stored" data-node-label="Hva lagres?" tabindex="0" role="button" aria-label="Fokuser på Hva lagres?, svaret, eller det bak svaret, Livssyklustilstand" aria-pressed="false" data-node-kind="decision" data-node-sublabel="svaret, eller det bak svaret" data-node-context="Livssyklustilstand">
<title>Hva lagres? · svaret, eller det bak svaret · Livssyklustilstand</title>
<rect x="297" y="28" width="196" height="64" rx="8" class="c-mask"/>
<rect x="297" y="28" width="196" height="64" rx="8" class="c-database" data-animate="node" style="--step:1" stroke-width="1.5"/>
<g aria-hidden="true" data-semantic-sigil="decision" class="semantic-sigil s-database" transform="translate(303 34) scale(0.6875)">
<path d="M8 2.2 13.8 8 8 13.8 2.2 8Z"/>
</g>
<text data-detail="fine" x="320" y="42" class="t-database" font-size="8" font-weight="700">02</text>
<text data-node-label="" data-detail-anchor="" x="395" y="55" class="t-primary" font-size="12" font-weight="600" text-anchor="middle">Hva lagres?</text>
<text data-detail="context" x="395" y="73" class="t-muted" font-size="9" text-anchor="middle">svaret, eller det bak svaret</text>
</g>
<g id="node-facts" data-node-id="facts" data-node-label="Fakta og utfall" tabindex="0" role="button" aria-label="Fokuser på Fakta og utfall, lagret og justert, Livssyklustilstand" aria-pressed="false" data-node-kind="active" data-node-sublabel="lagret og justert" data-node-tag="suveren" data-node-context="Livssyklustilstand">
<title>Fakta og utfall · lagret og justert · Livssyklustilstand · suveren</title>
<rect x="615" y="28" width="152" height="64" rx="8" class="c-mask"/>
<rect x="615" y="28" width="152" height="64" rx="8" class="c-frontend" data-animate="node" style="--step:2" stroke-width="1.5"/>
<text data-detail="fine" x="638" y="42" class="t-frontend" font-size="8" font-weight="700">03</text>
<text data-node-label="" data-detail-anchor="" x="704.6" y="51" class="t-primary" font-size="12" font-weight="600" text-anchor="middle">Fakta og utfall</text>
<text data-detail="context" x="691" y="68" class="t-muted" font-size="9" text-anchor="middle">lagret og justert</text>
<text data-detail="fine" x="691" y="83" class="t-frontend" font-size="8" text-anchor="middle">suveren</text>
</g>
<g id="node-newmodel" data-node-id="newmodel" data-node-label="Ny modell kommer" tabindex="0" role="button" aria-label="Fokuser på Ny modell kommer, etter ett år, Livssyklustilstand" aria-pressed="false" data-node-kind="waiting" data-node-sublabel="etter ett år" data-node-context="Livssyklustilstand">
<title>Ny modell kommer · etter ett år · Livssyklustilstand</title>
<rect x="841" y="28" width="159" height="64" rx="8" class="c-mask"/>
<rect x="841" y="28" width="159" height="64" rx="8" class="c-cloud" data-animate="node" style="--step:3" stroke-width="1.5"/>
<g aria-hidden="true" data-semantic-sigil="waiting" class="semantic-sigil s-cloud" transform="translate(847 34) scale(0.6875)">
<path d="M4 2.5h8M4 13.5h8M5 3c0 2.8 2 3.2 3 5-1 1.8-3 2.2-3 5M11 3c0 2.8-2 3.2-3 5 1 1.8 3 2.2 3 5"/>
</g>
<text data-detail="fine" x="864" y="42" class="t-cloud" font-size="8" font-weight="700">04</text>
<text data-node-label="" data-detail-anchor="" x="934.2" y="55" class="t-primary" font-size="12" font-weight="600" text-anchor="middle">Ny modell kommer</text>
<text data-detail="context" x="920.5" y="73" class="t-muted" font-size="9" text-anchor="middle">etter ett år</text>
</g>
<g id="node-inherits" data-node-id="inherits" data-node-label="Arver det lærte" tabindex="0" role="button" aria-label="Fokuser på Arver det lærte, regler og saker, Livssyklustilstand" aria-pressed="false" data-node-kind="success" data-node-sublabel="regler og saker" data-node-tag="varig fortrinn" data-node-context="Livssyklustilstand">
<title>Arver det lærte · regler og saker · Livssyklustilstand · varig fortrinn</title>
<rect x="1098" y="28" width="152" height="64" rx="8" class="c-mask"/>
<rect x="1098" y="28" width="152" height="64" rx="8" class="c-backend" data-animate="node" style="--step:4" stroke-width="1.5"/>
<rect x="1101" y="31" width="146" height="58" rx="5" class="c-backend" style="fill: none" stroke-width="1"/>
<g aria-hidden="true" data-semantic-sigil="success" class="semantic-sigil s-backend" transform="translate(1104 34) scale(0.6875)">
<circle cx="8" cy="8" r="5.3"/>
<path d="m5.2 8 1.8 1.8 3.8-4"/>
</g>
<text data-detail="fine" x="1121" y="42" class="t-backend" font-size="8" font-weight="700">05</text>
<text data-node-label="" data-detail-anchor="" x="1187.6" y="51" class="t-primary" font-size="12" font-weight="600" text-anchor="middle">Arver det lærte</text>
<text data-detail="context" x="1174" y="68" class="t-muted" font-size="9" text-anchor="middle">regler og saker</text>
<text data-detail="fine" x="1174" y="83" class="t-backend" font-size="8" text-anchor="middle">varig fortrinn</text>
</g>
<g id="node-answeronly" data-node-id="answeronly" data-node-label="Bare svaret lagret" tabindex="0" role="button" aria-label="Fokuser på Bare svaret lagret, spørsmål inn, svar ut, Livssyklustilstand" aria-pressed="false" data-node-kind="active" data-node-sublabel="spørsmål inn, svar ut" data-node-tag="leietaker" data-node-context="Livssyklustilstand">
<title>Bare svaret lagret · spørsmål inn, svar ut · Livssyklustilstand · leietaker</title>
<rect x="220.75" y="184" width="158" height="64" rx="8" class="c-mask"/>
<rect x="220.75" y="184" width="158" height="64" rx="8" class="c-frontend" data-animate="node" style="--step:5" stroke-width="1.5"/>
<text data-node-label="" data-detail-anchor="" x="304.55" y="207" class="t-primary" font-size="12" font-weight="600" text-anchor="middle">Bare svaret lagret</text>
<text data-detail="context" x="299.75" y="224" class="t-muted" font-size="9" text-anchor="middle">spørsmål inn, svar ut</text>
<text data-detail="fine" x="299.75" y="239" class="t-frontend" font-size="8" text-anchor="middle">leietaker</text>
</g>
<g id="node-newmodel2" data-node-id="newmodel2" data-node-label="Ny modell kommer" tabindex="0" role="button" aria-label="Fokuser på Ny modell kommer, etter ett år, Livssyklustilstand" aria-pressed="false" data-node-kind="waiting" data-node-sublabel="etter ett år" data-node-context="Livssyklustilstand">
<title>Ny modell kommer · etter ett år · Livssyklustilstand</title>
<rect x="228.25" y="340" width="143" height="64" rx="8" class="c-mask"/>
<rect x="228.25" y="340" width="143" height="64" rx="8" class="c-cloud" data-animate="node" style="--step:6" stroke-width="1.5"/>
<g aria-hidden="true" data-semantic-sigil="waiting" class="semantic-sigil s-cloud" transform="translate(234.25 346) scale(0.6875)">
<path d="M4 2.5h8M4 13.5h8M5 3c0 2.8 2 3.2 3 5-1 1.8-3 2.2-3 5M11 3c0 2.8-2 3.2-3 5 1 1.8 3 2.2 3 5"/>
</g>
<text data-node-label="" data-detail-anchor="" x="304.85" y="367" class="t-primary" font-size="12" font-weight="600" text-anchor="middle">Ny modell kommer</text>
<text data-detail="context" x="299.75" y="385" class="t-muted" font-size="9" text-anchor="middle">etter ett år</text>
</g>
<g id="node-zero" data-node-id="zero" data-node-label="Starter fra null" tabindex="0" role="button" aria-label="Fokuser på Starter fra null, samme prompt som alle, Livssyklustilstand" aria-pressed="false" data-node-kind="failure" data-node-sublabel="samme prompt som alle" data-node-tag="tapt" data-node-context="Livssyklustilstand">
<title>Starter fra null · samme prompt som alle · Livssyklustilstand · tapt</title>
<rect x="220.75" y="496" width="158" height="64" rx="8" class="c-mask"/>
<rect x="220.75" y="496" width="158" height="64" rx="8" class="c-security" data-animate="node" style="--step:7" stroke-width="1.5"/>
<rect x="223.75" y="499" width="152" height="58" rx="5" class="c-security" style="fill: none" stroke-width="1"/>
<g aria-hidden="true" data-semantic-sigil="failure" class="semantic-sigil s-security" transform="translate(226.75 502) scale(0.6875)">
<circle cx="8" cy="8" r="5.3"/>
<path d="m5.7 5.7 4.6 4.6m0-4.6-4.6 4.6"/>
</g>
<text data-node-label="" data-detail-anchor="" x="299.75" y="519" class="t-primary" font-size="12" font-weight="600" text-anchor="middle">Starter fra null</text>
<text data-detail="context" x="299.75" y="536" class="t-muted" font-size="9" text-anchor="middle">samme prompt som alle</text>
<text data-detail="fine" x="299.75" y="551" class="t-security" font-size="8" text-anchor="middle">tapt</text>
</g>
<g id="node-vendor" data-node-id="vendor" data-node-label="Lagret hos leverandør" tabindex="0" role="button" aria-label="Fokuser på Lagret hos leverandør, proprietært format, Livssyklustilstand" aria-pressed="false" data-node-kind="active" data-node-sublabel="proprietært format" data-node-tag="innlåst" data-node-context="Livssyklustilstand">
<title>Lagret hos leverandør · proprietært format · Livssyklustilstand · innlåst</title>
<rect x="422.75" y="184" width="179" height="64" rx="8" class="c-mask"/>
<rect x="422.75" y="184" width="179" height="64" rx="8" class="c-frontend" data-animate="node" style="--step:8" stroke-width="1.5"/>
<text data-node-label="" data-detail-anchor="" x="517.35" y="207" class="t-primary" font-size="12" font-weight="600" text-anchor="middle">Lagret hos leverandør</text>
<text data-detail="context" x="512.25" y="224" class="t-muted" font-size="9" text-anchor="middle">proprietært format</text>
<text data-detail="fine" x="512.25" y="239" class="t-frontend" font-size="8" text-anchor="middle">innlåst</text>
</g>
<g id="node-loss" data-node-id="loss" data-node-label="Bytte betyr tap" tabindex="0" role="button" aria-label="Fokuser på Bytte betyr tap, hukommelsen blir igjen, Livssyklustilstand" aria-pressed="false" data-node-kind="failure" data-node-sublabel="hukommelsen blir igjen" data-node-tag="tapt" data-node-context="Livssyklustilstand">
<title>Bytte betyr tap · hukommelsen blir igjen · Livssyklustilstand · tapt</title>
<rect x="430.75" y="340" width="163" height="64" rx="8" class="c-mask"/>
<rect x="430.75" y="340" width="163" height="64" rx="8" class="c-security" data-animate="node" style="--step:9" stroke-width="1.5"/>
<rect x="433.75" y="343" width="157" height="58" rx="5" class="c-security" style="fill: none" stroke-width="1"/>
<g aria-hidden="true" data-semantic-sigil="failure" class="semantic-sigil s-security" transform="translate(436.75 346) scale(0.6875)">
<circle cx="8" cy="8" r="5.3"/>
<path d="m5.7 5.7 4.6 4.6m0-4.6-4.6 4.6"/>
</g>
<text data-node-label="" data-detail-anchor="" x="512.25" y="363" class="t-primary" font-size="12" font-weight="600" text-anchor="middle">Bytte betyr tap</text>
<text data-detail="context" x="512.25" y="380" class="t-muted" font-size="9" text-anchor="middle">hukommelsen blir igjen</text>
<text data-detail="fine" x="512.25" y="395" class="t-security" font-size="8" text-anchor="middle">tapt</text>
</g>
<!-- Transition labels -->
<g data-detail="context" data-edge-from="rented" data-edge-to="stored" data-edge-label="svar" data-edge-key="0" data-edge-id="t1">
<rect x="243" y="38" width="36" height="16" rx="4" class="c-mask"/>
<text x="261" y="49.5" class="t-edge-emphasis" font-size="10" text-anchor="middle">svar</text>
</g>
<g data-detail="context" data-edge-from="stored" data-edge-to="facts" data-edge-label="det bak svaret" data-edge-key="1" data-edge-id="t2">
<rect x="506" y="38" width="96" height="16" rx="4" class="c-mask"/>
<text x="554" y="49.5" class="t-edge-emphasis" font-size="10" text-anchor="middle">det bak svaret</text>
</g>
<g data-detail="context" data-edge-from="facts" data-edge-to="newmodel" data-edge-label="ett år" data-edge-key="2" data-edge-id="t3">
<rect x="780" y="38" width="48" height="16" rx="4" class="c-mask"/>
<text x="804" y="49.5" class="t-edge-emphasis" font-size="10" text-anchor="middle">ett år</text>
</g>
<g data-detail="context" data-edge-from="newmodel" data-edge-to="inherits" data-edge-label="bytt fritt" data-edge-key="3" data-edge-id="t4">
<rect x="1013" y="38" width="72" height="16" rx="4" class="c-mask"/>
<text x="1049" y="49.5" class="t-edge-emphasis" font-size="10" text-anchor="middle">bytt fritt</text>
</g>
<g data-detail="context" data-edge-from="stored" data-edge-to="answeronly" data-edge-label="bare svaret" data-edge-key="4" data-edge-id="b1">
<rect x="325" y="130" width="78" height="16" rx="4" class="c-mask"/>
<text x="364" y="141.5" class="t-muted" font-size="10" text-anchor="middle">bare svaret</text>
</g>
<g data-detail="context" data-edge-from="answeronly" data-edge-to="newmodel2" data-edge-label="ett år" data-edge-key="5" data-edge-id="b2">
<rect x="305.8" y="286" width="48" height="16" rx="4" class="c-mask"/>
<text x="329.8" y="297.5" class="t-muted" font-size="10" text-anchor="middle">ett år</text>
</g>
<g data-detail="context" data-edge-from="newmodel2" data-edge-to="zero" data-edge-label="ingen arv" data-edge-key="6" data-edge-id="b3">
<rect x="305.8" y="442" width="66" height="16" rx="4" class="c-mask"/>
<text x="338.8" y="453.5" class="t-edge-security" font-size="10" text-anchor="middle">ingen arv</text>
</g>
<g data-detail="context" data-edge-from="stored" data-edge-to="vendor" data-edge-label="hos leverandør" data-edge-key="7" data-edge-id="c1">
<rect x="477" y="130" width="96" height="16" rx="4" class="c-mask"/>
<text x="525" y="141.5" class="t-muted" font-size="10" text-anchor="middle">hos leverandør</text>
</g>
<g data-detail="context" data-edge-from="vendor" data-edge-to="loss" data-edge-label="vil bytte" data-edge-key="8" data-edge-id="c2">
<rect x="518.3" y="286" width="66" height="16" rx="4" class="c-mask"/>
<text x="551.3" y="297.5" class="t-edge-security" font-size="10" text-anchor="middle">vil bytte</text>
</g>
<!-- Legend -->
<g data-legend="" data-legend-bridge="">
<text x="40" y="586" class="t-primary" font-size="12" font-weight="650">Tegnforklaring</text>
<g data-legend-semantic-kind="start" data-legend-kind="start" data-legend-label="Utgangspunkt" data-legend-x="40" data-legend-baseline="606" data-legend-width="130">
<circle cx="44.5" cy="602.5" r="4.5" style="fill: var(--arrow-emphasis)"/><path d="M 49 602.5 L 58 602.5" class="a-emphasis" stroke-width="1.4"/><path d="M 57 599 L 64 602.5 L 57 606 Z" style="fill: var(--arrow-emphasis)"/>
<text x="74" y="606" class="t-muted" font-size="10" font-weight="500">Utgangspunkt</text>
</g>
<g data-legend-semantic-kind="active" data-legend-kind="active" data-legend-label="Pågående praksis" data-legend-x="192" data-legend-baseline="606" data-legend-width="143">
<rect x="192" y="598" width="14" height="9" rx="2" class="c-frontend" stroke-width="1"/>
<text x="214" y="606" class="t-muted" font-size="10" font-weight="500">Pågående praksis</text>
</g>
<g data-legend-semantic-kind="waiting" data-legend-kind="waiting" data-legend-label="Ytre hendelse" data-legend-x="357" data-legend-baseline="606" data-legend-width="124">
<rect x="357" y="598" width="14" height="9" rx="2" class="c-cloud" stroke-width="1"/>
<text x="379" y="606" class="t-muted" font-size="10" font-weight="500">Ytre hendelse</text>
</g>
<g data-legend-semantic-kind="decision" data-legend-kind="decision" data-legend-label="Veivalg" data-legend-x="503" data-legend-baseline="606" data-legend-width="87">
<rect x="503" y="598" width="14" height="9" rx="2" class="c-database" stroke-width="1"/>
<text x="525" y="606" class="t-muted" font-size="10" font-weight="500">Veivalg</text>
</g>
<g data-legend-semantic-kind="success" data-legend-kind="success" data-legend-label="Varig fortrinn" data-legend-x="612" data-legend-baseline="606" data-legend-width="130">
<rect x="612" y="598" width="14" height="9" rx="2" class="c-backend" stroke-width="1"/>
<text x="634" y="606" class="t-muted" font-size="10" font-weight="500">Varig fortrinn</text>
</g>
<g data-legend-semantic-kind="failure" data-legend-kind="failure" data-legend-label="Tapt læring" data-legend-x="764" data-legend-baseline="606" data-legend-width="112">
<rect x="764" y="598" width="14" height="9" rx="2" class="c-security" stroke-width="1"/>
<text x="786" y="606" class="t-muted" font-size="10" font-weight="500">Tapt læring</text>
</g>
<g data-legend-semantic-kind="final" data-legend-x="898" data-legend-baseline="606" data-legend-width="99">
<rect x="898" y="598" width="14" height="9" rx="2" class="c-external" stroke-width="1"/><rect x="900.5" y="600.5" width="9" height="4" rx="1" class="c-external" style="fill: none" stroke-width="0.8"/>
<text x="922" y="606" class="t-muted" font-size="10" font-weight="500">Sluttilstand</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">Diagramguide</span>
<strong class="diagram-guide-title" id="diagram-guide-title">Utforsk dette systemet</strong>
</span>
<button class="diagram-guide-close" id="diagram-guide-close" type="button" aria-label="Lukk diagramguiden">&#215;</button>
</div>
<p class="diagram-guide-stats" id="diagram-guide-stats">Inspiserer kompilert semantikk</p>
<div class="diagram-guide-actions" id="diagram-guide-actions" role="group" aria-label="Handlinger for utforsking av diagrammet">
<button class="diagram-guide-action" type="button" data-guide-action="find" aria-label="Finn hvilken som helst node">
<span class="diagram-guide-index" aria-hidden="true">01</span>
<span class="diagram-guide-action-copy"><strong>Finn hvilken som helst node</strong><small>Søk i etiketter, ansvar, typer og stabile ID-er.</small></span>
<kbd aria-hidden="true">/</kbd>
</button>
<button class="diagram-guide-action" type="button" data-guide-action="route" aria-label="Spor en rettet rute">
<span class="diagram-guide-index" aria-hidden="true">02</span>
<span class="diagram-guide-action-copy"><strong>Spor en rute</strong><small>Finn ut hvordan to semantiske noder henger sammen i definert retning.</small></span>
<kbd aria-hidden="true">R</kbd>
</button>
<button class="diagram-guide-action" type="button" data-guide-action="map" aria-label="Se hele systemet">
<span class="diagram-guide-index" aria-hidden="true">03</span>
<span class="diagram-guide-action-copy"><strong>Se hele systemet</strong><small>Åpne semantisk radar med levende visningsområde og stabile noder.</small></span>
<kbd aria-hidden="true">M</kbd>
</button>
<button class="diagram-guide-action" type="button" data-guide-action="lens" aria-label="Sammenlign semantiske typer">
<span class="diagram-guide-index" aria-hidden="true">04</span>
<span class="diagram-guide-action-copy"><strong>Sammenlign semantiske typer</strong><small>Tell roller, vis trafikken deres og sammenlign direkte definerte koblinger.</small></span>
<kbd aria-hidden="true">L</kbd>
</button>
<button class="diagram-guide-action" type="button" data-guide-action="present" aria-label="Gå til presentasjonsmodus">
<span class="diagram-guide-index" aria-hidden="true">05</span>
<span class="diagram-guide-action-copy"><strong>Gå til presentasjonsmodus</strong><small>Gi det levende diagrammet hele visningsområdet uten å endre eksporten.</small></span>
<kbd aria-hidden="true">F</kbd>
</button>
</div>
<div class="diagram-guide-shortcuts" aria-label="Flere hurtigtaster">
<span><kbd>E</kbd> Eksporter</span><span><kbd>T</kbd> Tema</span><span><kbd>S</kbd> Stil</span><span><kbd>0</kbd> Tilbakestill</span><span><kbd>+</kbd> Zoom inn</span><span><kbd>-</kbd> Zoom ut</span><span><kbd>Esc</kbd> Lukk</span>
</div>
<p class="diagram-guide-feedback" id="diagram-guide-feedback" aria-live="polite"></p>
<a class="diagram-guide-credit" href="https://github.com/tt-a1i/archify" target="_blank" rel="noopener noreferrer"><span>Synes du at Archify er nyttig? En stjerne på GitHub hjelper</span><span class="diagram-guide-credit-link"><span class="diagram-guide-star" aria-hidden="true"></span>Gi stjerne <span aria-hidden="true">&#8599;</span></span></a>
</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">Finn en node</strong>
<button class="node-finder-close" id="node-finder-close" type="button" aria-label="Lukk nodesøket">&#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="Søk etter etiketter eller ID-er" aria-label="Søk i diagramnoder" aria-describedby="node-finder-status">
<kbd aria-hidden="true">/</kbd>
</div>
<div class="node-finder-results" id="node-finder-results" role="group" aria-label="Diagramnoder"></div>
<p class="node-finder-empty" id="node-finder-empty" hidden>Ingen samsvarende noder</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="Flytt semantisk pass. Dra, bruk piltastene eller trykk Home for å tilbakestille." title="Flytt semantisk pass. Dra, bruk piltastene eller trykk Home for å tilbakestille."></button>
<div class="relationship-lens-copy">
<span class="relationship-lens-eyebrow">Semantisk pass</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="Nodemetadata">
<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="Verifisert kildebevis">
<div class="semantic-passport-evidence-head">
<span class="semantic-passport-evidence-status">Verifisert kilde</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">Definert rekkevidde</span>
<div class="semantic-passport-reach-actions" role="group" aria-label="Spor definert rekkevidde">
<button id="btn-reach-upstream" type="button" aria-label="Spor definert rekkevidde oppstrøms" aria-pressed="false">
<span>Oppstrøms</span><strong id="focus-reach-upstream-count">0</strong>
</button>
<button id="btn-reach-downstream" type="button" aria-label="Spor definert rekkevidde nedstrøms" aria-pressed="false">
<span>Nedstrøms</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="Lukk semantisk pass" title="Lukk">&#215;</button>
<button id="btn-focus-copy" type="button" aria-label="Kopier lenke til noden i fokus">Kopier lenke</button>
<button id="btn-focus-relations" type="button" aria-label="Vis tilknyttede relasjoner" aria-expanded="false" aria-controls="relationship-lens-list">Relasjoner</button>
</div>
</div>
<div class="relationship-lens-list" id="relationship-lens-list" aria-label="Tilknyttede relasjoner"></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">Rute</span>
<strong class="route-probe-title" id="route-probe-title">Klikk der ruten starter</strong>
</span>
<span class="route-probe-actions">
<button class="route-probe-find" id="route-probe-find" type="button" aria-label="Finn en rutestart" data-node-finder-trigger>Finn start</button>
<button class="route-probe-copy-link" id="route-probe-copy" type="button" aria-label="Kopier lenke til sporet rute" hidden>Kopier lenke</button>
<button id="route-probe-clear" type="button" aria-label="Fjern rutesonde">Fjern</button>
</span>
</div>
<div class="route-probe-path" id="route-probe-path" aria-label="Rute">
<span class="route-probe-placeholder">Klikk på to noder i diagrammet</span>
</div>
<div class="route-journey-controls" id="route-journey-controls" hidden role="group" aria-label="Kontroller for rutereisen" data-playing="false">
<button id="route-journey-prev" type="button" aria-label="Forrige posisjon på ruten" title="Forrige posisjon på ruten">&#8592;</button>
<button class="route-journey-play" id="route-journey-play" type="button" aria-label="Spill av rutereisen" aria-pressed="false" title="Spill av rutereisen">
<span id="route-journey-play-icon" aria-hidden="true">&#9654;</span>
<span id="route-journey-play-label">Reise</span>
</button>
<button id="route-journey-next" type="button" aria-label="Neste posisjon på ruten" title="Neste posisjon på ruten">&#8594;</button>
<button class="route-journey-overview" id="route-journey-overview" type="button" aria-label="Vis full oversikt over ruten" title="Vis full oversikt over ruten">Oversikt</button>
</div>
<small class="route-probe-status" id="route-probe-status" aria-live="polite">Klikk på et startpunkt og deretter et sluttpunkt. Ruter følger pilene.</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">Semantisk linse</span>
<strong class="semantic-lens-title" id="semantic-lens-title">Sammenlign systemroller</strong>
</span>
<button class="semantic-lens-close" id="semantic-lens-close" type="button" aria-label="Lukk semantisk linse">&#215;</button>
</div>
<p class="semantic-lens-instruction">Velg opptil to semantiske typer. Én viser den faktiske trafikken; to sammenligner bare direkte definerte relasjoner.</p>
<div class="semantic-lens-kinds" id="semantic-lens-kinds" role="group" aria-label="Semantiske typer"></div>
<p class="semantic-lens-status" id="semantic-lens-status" aria-live="polite">Velg en type for å inspisere nodene og de tilstøtende relasjonene.</p>
<div class="semantic-lens-actions">
<button id="semantic-lens-copy" type="button" aria-label="Kopier lenke til semantisk linse" hidden>Kopier lenke</button>
<button id="semantic-lens-clear" type="button" aria-label="Fjern semantisk linse">Fjern</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">Semantisk radar</strong>
<small id="overview-map-status" aria-live="polite">Bygger oversikt</small>
</span>
<button class="overview-map-expand" id="overview-map-expand" type="button" aria-label="Åpne full semantisk radar">Åpne radar</button>
<button class="overview-map-close" id="overview-map-close" type="button" aria-label="Lukk semantisk radar">&#215;</button>
</div>
<div class="overview-map-surface" id="overview-map-surface" tabindex="0" role="group" aria-label="Diagramoversikt. Klikk på en node for å sette fokus på den, eller bruk piltastene for å panorere."></div>
<div class="overview-map-hint" aria-hidden="true"><span>Klikk på node</span><span>Dra for å panorere</span></div>
</div>
<p class="overview-map-feedback no-print" id="overview-map-feedback" role="status" aria-live="polite" hidden>Semantisk radar trenger mer kartplass.</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>Notater og indeks</span></button>
<div class="diagram-nav no-print" role="toolbar" aria-label="Kontroller for diagramvisning">
<button id="btn-route-probe" type="button" aria-label="Spor en rettet rute" aria-pressed="false" aria-controls="route-probe" title="Spor rute (R)">RUTE</button>
<button id="btn-overview-map" type="button" aria-label="Åpne semantisk radar" aria-expanded="false" aria-controls="overview-map" title="Semantisk radar (M)">KART</button>
<button id="btn-semantic-lens" type="button" aria-label="Åpne semantisk linse" aria-haspopup="dialog" aria-expanded="false" aria-pressed="false" aria-controls="semantic-lens" title="Semantisk linse (L)">LINSE</button>
<button id="btn-node-finder" type="button" aria-label="Finn en node" aria-haspopup="dialog" aria-expanded="false" aria-controls="node-finder" title="Finn en node (/)" data-node-finder-trigger><span class="diagram-nav-icon find" aria-hidden="true"></span></button>
<button id="btn-diagram-guide" type="button" aria-label="Åpne diagramguiden" aria-haspopup="dialog" aria-expanded="false" aria-controls="diagram-guide" title="Diagramguide (?)"><span class="diagram-nav-icon guide" aria-hidden="true"></span></button>
<button data-view="out" type="button" aria-label="Zoom ut" title="Zoom ut (-)"><span class="diagram-nav-icon minus" aria-hidden="true"></span></button>
<button data-view="reset" type="button" aria-label="Tilbakestill diagramvisningen" title="Tilbakestill visning (0)"><span class="diagram-nav-detail" data-view-detail hidden>LES</span><span class="diagram-nav-percent" data-view-percent>100%</span></button>
<button data-view="in" type="button" aria-label="Zoom inn" title="Zoom inn (+)"><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="Sidepanel">
<button class="rail-control" id="rail-placement" type="button" hidden aria-label="Flytt panelet under diagrammet" title="Flytt panelet under diagrammet" 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="Fold sammen sidepanelet" title="Fold sammen sidepanelet"><span class="rail-control-icon collapse" aria-hidden="true"></span></button>
</div>
<!-- Info Cards -->
<div class="cards">
</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">Nodeindeks</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 readerLink(state) {
var template = document.documentElement.getAttribute('data-reading-link-template');
if (template === null) return location.href.replace(/#.*$/, '') + '#' + state;
if (!template || template.length > 4096 || template.split('{state}').length !== 2) return null;
try {
var url = new URL(template.replace('{state}', encodeURIComponent(state)));
if (['http:', 'https:', 'file:'].indexOf(url.protocol) === -1 || url.username || url.password) return null;
return url.href;
} catch (_) { return null; }
}
function copyReaderText(value) {
function fallbackCopy() {
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;
}
var policy = document.permissionsPolicy || document.featurePolicy;
var allowed = !policy || typeof policy.allowsFeature !== 'function' || policy.allowsFeature('clipboard-write');
if (allowed && navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
try { return Promise.resolve(navigator.clipboard.writeText(value)).then(function () { return true; }).catch(fallbackCopy); }
catch (_) {}
}
return Promise.resolve(fallbackCopy());
}
function hasDrawableGeometry(element) {
if (!element || element.hasAttribute('data-ambient-flow-overlay')) 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) {
if (geometry.hasAttribute('data-ambient-flow-overlay')) return false;
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.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return;
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; }
if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) 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) {
Array.prototype.forEach.call(clone.querySelectorAll('[data-ambient-flow-overlay]'), function (el) {
el.remove();
});
// Route interaction rewrites CSS declarations through CSSOM. Normalize
// authored motion delays on the clone so exported bytes do not depend
// on whether the reader traced a route before downloading.
Array.prototype.forEach.call(clone.querySelectorAll('[data-animate]'), function (el) {
var step = el.style.getPropertyValue('--step');
if (step) el.setAttribute('style', el.style.cssText);
});
// 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-reader-legend-corner]'), function (el) {
el.removeAttribute('data-reader-legend-corner');
el.style.removeProperty('--archify-reader-legend-transform');
});
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) 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');
});
// A downloaded tree is always the complete hierarchy: collapsing is a
// reading aid, never a silent change to what the artifact contains.
clone.removeAttribute('data-tree-any-collapsed');
Array.prototype.forEach.call(clone.querySelectorAll('[data-tree-hidden], [data-tree-collapsed]'), function (el) {
el.removeAttribute('data-tree-hidden');
el.removeAttribute('data-tree-collapsed');
});
Array.prototype.forEach.call(clone.querySelectorAll('[data-tree-toggle]'), function (el) {
el.setAttribute('aria-expanded', 'true');
el.setAttribute('aria-label', el.getAttribute('data-tree-label-collapse') || '');
});
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.hasAttribute('data-tree-any-collapsed') &&
!clone.style.getPropertyValue('transform') &&
!clone.style.getPropertyValue('clip-path') &&
clone.querySelectorAll('[data-ambient-flow-overlay], [data-reader-legend-corner], [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], [data-tree-hidden], [data-tree-collapsed]').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;
// Keep the export-owned activation inside its existing control boundary.
// The synthetic download click must not dismiss the reader selection.
menu.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);
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 geometry = element;
if (element.getAttribute('data-motion-path')) {
geometry = element.cloneNode(false);
geometry.setAttribute('d', element.getAttribute('data-motion-path'));
}
var length;
try { length = geometry.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, geometry.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 sourceUrl = null;
var backgroundImage = null;
var stream = null;
var recorder = null;
var raf = 0;
var durationTimer = 0;
var flushTimer = 0;
var settled = false;
function finish(error, blob) {
if (settled) return;
// 先锁定终态:停止录制和释放流可能产生晚到的编码事件。
settled = true;
cancelAnimationFrame(raf);
clearTimeout(durationTimer);
clearTimeout(flushTimer);
if (backgroundImage) {
backgroundImage.onload = null;
backgroundImage.onerror = null;
}
if (recorder) {
recorder.ondataavailable = null;
recorder.onerror = null;
recorder.onstop = null;
try { if (recorder.state !== 'inactive') recorder.stop(); } catch (_) {}
}
if (stream) stream.getTracks().forEach(function (track) { track.stop(); });
if (sourceUrl) URL.revokeObjectURL(sourceUrl);
if (blob) resolve(blob);
else reject(error);
}
try {
sourceUrl = URL.createObjectURL(new Blob([data.svgString], { type: 'image/svg+xml;charset=utf-8' }));
backgroundImage = new Image();
backgroundImage.onload = function () {
if (settled) return;
try {
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');
if (!ctx) throw exportError('viewer.export.error.contextUnavailable', { label: 'WebM' });
stream = canvas.captureStream(fps);
var mime = motionMimeType();
recorder = new MediaRecorder(stream, { mimeType: mime, videoBitsPerSecond: 6000000 });
var chunks = [];
var startedAt = performance.now();
function draw(now) {
if (settled) return;
try {
var elapsed = Math.max(0, ((Number(now) || performance.now()) - startedAt) / 1000);
drawMotionFrame(ctx, backgroundImage, motionScene, elapsed);
raf = requestAnimationFrame(draw);
} catch (error) { finish(error); }
}
recorder.ondataavailable = function (event) {
if (!settled && event.data && event.data.size) chunks.push(event.data);
};
recorder.onerror = function (event) {
finish(event.error || exportError('viewer.export.error.mediaRecorder'));
};
recorder.onstop = function () {
if (settled) return;
try {
var blob = new Blob(chunks, { type: recorder.mimeType || mime });
if (!blob.size) throw exportError('viewer.export.error.emptyWebm');
finish(null, blob);
} catch (error) { finish(error); }
};
drawMotionFrame(ctx, backgroundImage, motionScene, 0);
recorder.start(250);
if (settled) return;
startedAt = performance.now();
raf = requestAnimationFrame(draw);
durationTimer = setTimeout(function () {
if (settled || recorder.state === 'inactive') return;
// 保留最佳努力的编码器 flush;失败后仍通过 stop 收集最后的片段。
try { recorder.requestData(); } catch (_) {}
if (settled) return;
flushTimer = setTimeout(function () {
if (settled) return;
try { if (recorder.state !== 'inactive') recorder.stop(); }
catch (error) { finish(error); }
}, 120);
}, duration);
} catch (error) { finish(error); }
};
backgroundImage.onerror = function () {
finish(exportError('viewer.export.error.webmBackground'));
};
backgroundImage.src = sourceUrl;
} catch (error) { finish(error); }
});
}
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.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return;
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; }
if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) 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 entryStarted = false;
var entryPending = new Set();
var flowCount = 0;
// Runtime decoration carries geometry and tree visibility only. Keep each
// flow beside its authored edge so transforms, clipping and crossover
// paint order apply.
// Each renderer may declare a choreography that matches what its diagram
// means. Relay (the default) sends a comet from each source when the
// signal reaches it; stream adds a packet train for continuous data; walk
// moves one token along the main state path; sequence plays messages in
// authored order; sweep runs a playhead along a timeline; replay fills
// waterfall spans in proportion to their measured time. Motion Governor
// plans the timeline and per-element keyframes; viewer.css owns looks.
var SVG_NS = 'http://www.w3.org/2000/svg';
var choreography = svg ? (svg.getAttribute('data-motion-choreography') || 'relay') : 'relay';
var RELAY = {
speed: 280,
minTravel: 0.42,
maxTravel: 1.5,
handoff: 0.1,
dwell: 0.55,
maxSpan: 9,
land: 0.9,
rest: 1.4,
minCycle: 3.6
};
function stepOf(element, fallback) {
var step = parseFloat(element && element.style ? element.style.getPropertyValue('--step') : '');
return Number.isFinite(step) ? step : fallback;
}
function byStep(a, b) { return a.step - b.step || a.index - b.index; }
function addWindow(windows, id, start, end, strong) {
if (!id) return;
(windows[id] = windows[id] || []).push({ start: start, end: end, strong: !!strong });
}
function planRelay(records, windows) {
var arrive = Object.create(null);
var seeded = Object.create(null);
var targets = Object.create(null);
records.forEach(function (record) { if (record.from !== record.to) targets[record.to] = true; });
function seed(id) {
if (arrive[id] !== undefined) return;
arrive[id] = 0;
seeded[id] = true;
}
function relax() {
// Earliest arrival over a few dozen edges; a scan per settle keeps
// cycles and fan-in deterministic without a priority queue.
var settled = Object.create(null);
for (;;) {
var next = null;
Object.keys(arrive).forEach(function (id) {
if (!settled[id] && (next === null || arrive[id] < arrive[next])) next = id;
});
if (next === null) return;
settled[next] = true;
var depart = arrive[next] + (seeded[next] ? 0 : RELAY.handoff);
records.forEach(function (record) {
if (record.from !== next) return;
var at = depart + record.travel;
if (arrive[record.to] === undefined || at < arrive[record.to]) arrive[record.to] = at;
});
}
}
records.forEach(function (record) { if (!targets[record.from]) seed(record.from); });
relax();
for (;;) {
// A closed cycle has no entry; start it at its earliest authored edge.
var orphan = records.filter(function (record) { return arrive[record.from] === undefined; }).sort(byStep)[0];
if (!orphan) break;
seed(orphan.from);
relax();
}
records.forEach(function (record) {
record.depart = arrive[record.from] + (seeded[record.from] ? 0 : RELAY.handoff);
record.active = true;
});
Object.keys(arrive).forEach(function (id) { addWindow(windows, id, arrive[id], arrive[id]); });
}
function planWalk(records, windows) {
// A state machine is in one state at a time: follow the first authored
// transition to an unvisited state and let the current state stay lit.
var targets = Object.create(null);
records.forEach(function (record) { if (record.from !== record.to) targets[record.to] = true; });
var ordered = records.slice().sort(byStep);
var start = (ordered.find(function (record) { return !targets[record.from]; }) || ordered[0]).from;
var visited = Object.create(null);
var current = start;
var clock = 0;
var enteredAt = 0;
visited[start] = true;
for (var hops = 0; hops < 16; hops += 1) {
var next = ordered.find(function (record) { return record.from === current && !visited[record.to]; });
if (!next) break;
next.active = true;
next.depart = clock + RELAY.dwell;
addWindow(windows, current, enteredAt, next.depart, true);
clock = next.depart + next.travel;
enteredAt = clock;
current = next.to;
visited[current] = true;
}
addWindow(windows, current, enteredAt, enteredAt + 1.2, true);
}
function planSequence(records, windows) {
var clock = 0;
records.slice().sort(byStep).forEach(function (record) {
record.active = true;
record.depart = clock;
clock += record.travel + RELAY.handoff;
addWindow(windows, record.from, record.depart, record.depart);
addWindow(windows, record.to, record.depart + record.travel, record.depart + record.travel);
});
}
function scalePlan(records, windows, span) {
var scale = span > RELAY.maxSpan ? RELAY.maxSpan / span : 1;
if (scale === 1) return span;
records.forEach(function (record) {
if (!record.active) return;
record.depart *= scale;
record.travel *= scale;
});
Object.keys(windows).forEach(function (id) {
windows[id].forEach(function (window) { window.start *= scale; window.end *= scale; });
});
return span * scale;
}
function nodeShapes() {
var shapes = Object.create(null);
Array.prototype.forEach.call(svg.querySelectorAll('[data-animate="node"]'), function (shape) {
var wrapper = shape.closest('[data-node-id]');
var id = wrapper ? wrapper.getAttribute('data-node-id') : '';
if (id) (shapes[id] = shapes[id] || []).push(shape);
});
return shapes;
}
function sceneBounds(elements) {
var box = null;
elements.forEach(function (element) {
var b = null;
try { b = element.getBBox(); } catch (_) {}
if (!b || !(b.width || b.height)) return;
if (!box) box = { x: b.x, y: b.y, right: b.x + b.width, bottom: b.y + b.height };
else {
box.x = Math.min(box.x, b.x); box.y = Math.min(box.y, b.y);
box.right = Math.max(box.right, b.x + b.width); box.bottom = Math.max(box.bottom, b.y + b.height);
}
});
return box;
}
function overlay(tag, cls, attrs, parent, before) {
var element = document.createElementNS(SVG_NS, tag);
Object.keys(attrs).forEach(function (name) { element.setAttribute(name, attrs[name]); });
element.setAttribute('class', 'ambient-edge-flow ' + cls);
element.setAttribute('data-ambient-flow-overlay', 'true');
element.setAttribute('aria-hidden', 'true');
parent.insertBefore(element, before || null);
return element;
}
// Timeline and waterfall share a playhead: a lit vertical rule and its
// bloom travel the time axis linearly, because time is the subject.
function addPlayhead(box, x0, x1, lead, sweep, extras) {
var anchor = svg.querySelector('[data-node-id]');
var parent = anchor ? anchor.parentNode : svg;
var attrs = { x1: x0.toFixed(1), x2: x0.toFixed(1), y1: (box.y - 10).toFixed(1), y2: (box.bottom + 10).toFixed(1) };
['playhead-glow', 'playhead'].forEach(function (kind) {
var line = overlay('line', 'ambient-flow-' + kind, attrs, parent, anchor);
line.style.setProperty('--flow-name', 'archify-motion-playhead');
extras.push(line);
});
extras.playhead = { distance: x1 - x0, lead: lead, sweep: sweep };
}
function planSweep(windows, extras) {
var events = Array.prototype.filter.call(svg.querySelectorAll('[data-node-id][data-timeline-x]'), function (node) {
return Number.isFinite(parseFloat(node.getAttribute('data-timeline-x')));
});
if (!events.length) return 0;
var xs = events.map(function (node) { return parseFloat(node.getAttribute('data-timeline-x')); });
var x0 = Math.min.apply(null, xs), x1 = Math.max.apply(null, xs);
var lead = 0.35;
var sweep = Math.min(7, Math.max(3.2, events.length * 0.6));
events.forEach(function (node, index) {
var at = lead + (x1 > x0 ? (xs[index] - x0) / (x1 - x0) : 0) * sweep;
addWindow(windows, node.getAttribute('data-node-id'), at, at + 0.35, true);
});
var box = sceneBounds(Array.prototype.slice.call(svg.querySelectorAll('[data-timeline-lane]')));
if (box && x1 > x0) addPlayhead(box, x0, x1, lead, sweep, extras);
return lead + sweep + 0.6;
}
function planReplay(windows, extras) {
var spans = [];
Array.prototype.forEach.call(svg.querySelectorAll('[data-node-id][data-waterfall-start]'), function (node) {
var start = parseFloat(node.getAttribute('data-waterfall-start'));
var end = parseFloat(node.getAttribute('data-waterfall-end'));
var bar = node.querySelector('[data-animate="node"]');
if (!bar || !Number.isFinite(start) || !(end >= start)) return;
var width = parseFloat(bar.getAttribute('width'));
var x = parseFloat(bar.getAttribute('x'));
if (!(width > 0) || !Number.isFinite(x)) return;
spans.push({ node: node, bar: bar, start: start, end: end, x: x, width: width });
});
if (!spans.length) return 0;
var t0 = Math.min.apply(null, spans.map(function (span) { return span.start; }));
var t1 = Math.max.apply(null, spans.map(function (span) { return span.end; }));
if (!(t1 > t0)) return 0;
var ruler = spans.reduce(function (best, span) { return span.end - span.start > best.end - best.start ? span : best; });
var pxPerUnit = ruler.width / Math.max(ruler.end - ruler.start, 1e-9);
var x0 = ruler.x - (ruler.start - t0) * pxPerUnit;
var lead = 0.35;
var replay = Math.min(6, Math.max(3.4, spans.length * 0.45));
var at = function (t) { return lead + (t - t0) / (t1 - t0) * replay; };
var settle = lead + replay + 0.9;
spans.forEach(function (span) {
var fill = overlay('rect', 'ambient-flow-fill', {
x: span.bar.getAttribute('x'), y: span.bar.getAttribute('y'),
width: span.bar.getAttribute('width'), height: span.bar.getAttribute('height'),
rx: span.bar.getAttribute('rx') || '0'
}, span.bar.parentNode, span.bar.nextSibling);
var tone = /(?:^|\s)c-([a-z]+)(?:\s|$)/.exec(span.bar.getAttribute('class') || '');
if (tone) fill.style.setProperty('--flow-fill', 'var(--' + tone[1] + '-stroke)');
if (span.node.getAttribute('data-waterfall-status') && span.node.getAttribute('data-waterfall-status') !== 'ok') {
fill.classList.add('ambient-flow-fill-failed');
}
fill.timing = { start: at(span.start), end: Math.max(at(span.end), at(span.start) + 0.08), settle: settle };
extras.push(fill);
addWindow(windows, span.node.getAttribute('data-node-id'), fill.timing.end, fill.timing.end);
});
var box = sceneBounds(spans.map(function (span) { return span.bar; }));
if (box) addPlayhead(box, x0, x0 + (t1 - t0) * pxPerUnit, lead, replay, extras);
return settle;
}
function writeKeyframes(records, windows, extras, cycle) {
function at(seconds) {
return (Math.min(100, Math.max(0, seconds / cycle * 100))).toFixed(3) + '%';
}
var ease = 'animation-timing-function: cubic-bezier(0.45, 0.05, 0.35, 1);';
var rules = [
'@keyframes archify-relay-ripple {',
' 0% { r: 2px; opacity: var(--flow-peak, 0.6); }',
' ' + at(0.75) + ', 100% { r: 20px; opacity: 0; }',
'}'
];
records.forEach(function (record) {
if (!record.active) return;
var travel = record.travel;
rules.push(
'@keyframes archify-relay-comet-' + record.index + ' {',
' 0% { stroke-dashoffset: calc(var(--flow-len, 0px) + var(--flow-shift, 0px)); opacity: 0; ' + ease + ' }',
' ' + at(Math.min(0.12, travel * 0.2)) + ' { opacity: var(--flow-peak, 0.9); }',
' ' + at(travel) + ' { stroke-dashoffset: var(--flow-shift, 0px); opacity: var(--flow-peak, 0.9); }',
' ' + at(travel + 0.16) + ', 100% { stroke-dashoffset: var(--flow-shift, 0px); opacity: 0; }',
'}',
// A solid wake draws the travelled path behind the head on the same
// easing; a dashed wake only brightens its authored segments.
'@keyframes archify-relay-wake-' + record.index + ' {',
record.dashed
? ' 0%, ' + at(travel * 0.3) + ' { opacity: 0; }'
: ' 0% { stroke-dashoffset: var(--flow-len, 0px); opacity: var(--flow-peak, 0.3); ' + ease + ' }',
record.dashed
? ' ' + at(travel) + ' { opacity: var(--flow-peak, 0.3); }'
: ' ' + at(travel) + ' { stroke-dashoffset: 0px; opacity: var(--flow-peak, 0.3); }',
' ' + at(travel + 0.9) + ', 100% { stroke-dashoffset: 0px; opacity: 0; }',
'}'
);
});
var glow = function (strong) {
return 'filter: var(--ambient-node-base-filter,) drop-shadow(0 0 ' + (strong ? 14 : 10) + 'px var(--arrow-emphasis));';
};
var base = 'filter: var(--ambient-node-base-filter, none);';
Object.keys(windows).forEach(function (id, index) {
var merged = [];
windows[id].slice().sort(function (a, b) { return a.start - b.start; }).forEach(function (window) {
var peakEnd = Math.max(window.end, window.start + 0.42);
var last = merged[merged.length - 1];
if (last && window.start <= last.peakEnd + 0.78) {
last.peakEnd = Math.max(last.peakEnd, peakEnd);
last.strong = last.strong || window.strong;
} else merged.push({ start: window.start, peakEnd: peakEnd, strong: window.strong });
});
var frames = [' 0%, 100% { ' + base + ' }'];
merged.forEach(function (window) {
frames.push(
' ' + at(window.start) + ' { ' + base + ' }',
' ' + at(window.start + 0.12) + ', ' + at(window.peakEnd) + ' { ' + glow(window.strong) + ' }',
' ' + at(window.peakEnd + 0.78) + ' { ' + base + ' }'
);
});
rules.push('@keyframes archify-relay-node-' + index + ' {', frames.join('\n'), '}');
windows[id].keyframes = 'archify-relay-node-' + index;
});
if (extras.playhead) {
var p = extras.playhead;
rules.push(
'@keyframes archify-motion-playhead {',
' 0% { transform: translateX(0px); opacity: 0; }',
' ' + at(Math.max(0.05, p.lead - 0.2)) + ' { transform: translateX(0px); opacity: 0; }',
' ' + at(p.lead) + ' { transform: translateX(0px); opacity: var(--flow-peak, 0.9); }',
' ' + at(p.lead + p.sweep) + ' { transform: translateX(' + p.distance.toFixed(1) + 'px); opacity: var(--flow-peak, 0.9); }',
' ' + at(p.lead + p.sweep + 0.45) + ', 100% { transform: translateX(' + p.distance.toFixed(1) + 'px); opacity: 0; }',
'}'
);
}
extras.forEach(function (element, index) {
if (!element.timing) return;
var t = element.timing;
rules.push(
'@keyframes archify-replay-fill-' + index + ' {',
' 0%, ' + at(t.start) + ' { transform: scaleX(0); opacity: var(--flow-peak, 0.4); }',
' ' + at(t.end) + ', ' + at(t.settle) + ' { transform: scaleX(1); opacity: var(--flow-peak, 0.4); }',
' ' + at(t.settle + 0.7) + ', 100% { transform: scaleX(1); opacity: 0; }',
'}'
);
element.style.setProperty('--flow-name', 'archify-replay-fill-' + index);
});
var style = document.createElement('style');
style.setAttribute('data-ambient-flow-keyframes', 'true');
style.textContent = rules.join('\n');
document.head.appendChild(style);
}
function createFlows() {
var records = [];
Array.prototype.forEach.call(svg.querySelectorAll('path[data-animate="edge"]'), function (shape, index) {
var d = shape.getAttribute('data-motion-path') || shape.getAttribute('d');
if (!d) return;
var anchor = shape;
// Tree Branches initializes after these overlays and includes their
// inherited ancestors in its existing collapse/expand lifecycle.
function tagOverlay(flow) {
flow.setAttribute('data-ambient-flow-overlay', 'true');
flow.setAttribute('aria-hidden', 'true');
if (shape.hasAttribute('data-tree-ancestors')) {
flow.setAttribute('data-tree-ancestors', shape.getAttribute('data-tree-ancestors'));
}
}
function addPath(cls) {
var flow = document.createElementNS(SVG_NS, 'path');
flow.setAttribute('d', d);
['transform', 'mask', 'clip-path', 'vector-effect'].forEach(function (name) {
if (shape.hasAttribute(name)) flow.setAttribute(name, shape.getAttribute(name));
});
flow.setAttribute('class', 'ambient-edge-flow ambient-flow-' + cls);
tagOverlay(flow);
shape.parentNode.insertBefore(flow, anchor.nextSibling);
anchor = flow;
return flow;
}
var wake = addPath('wake');
var len = 0;
try { len = wake.getTotalLength(); } catch (_) {}
if (!(len > 0)) {
wake.remove();
return;
}
var record = {
index: records.length,
len: len,
from: shape.getAttribute('data-edge-from') || shape.getAttribute('data-composition-edge-from') || '',
to: shape.getAttribute('data-edge-to') || shape.getAttribute('data-composition-edge-to') || '',
step: stepOf(shape, index),
travel: Math.min(RELAY.maxTravel, Math.max(RELAY.minTravel, len / RELAY.speed)),
depart: 0,
active: false,
layers: []
};
records.push(record);
// Dashed authored lines keep their dash language through the relay:
// the wake brightens only the authored segments and never fills the
// gaps back into a solid line.
var authoredDash = '';
try { authoredDash = window.getComputedStyle(shape).strokeDasharray; } catch (_) {}
record.dashed = !!(authoredDash && authoredDash !== 'none');
wake.style.setProperty('--flow-dasharray', record.dashed ? authoredDash : len.toFixed(1) + ' ' + len.toFixed(1));
// Absolute caps keep the comet a slim streak on long edges instead
// of growing into a bright slab; the tail disintegrates into a
// sparkle of fragments behind the head, and --flow-shift trails
// each layer behind the head tip by its own length.
var headLen = Math.min(14, Math.max(7, len * 0.03));
var haloLen = Math.min(22, Math.max(12, len * 0.07));
var trail = len >= 120
? { pattern: '2.5 2 4 3 5.5 4.5 8 ' + (len - 30).toFixed(1), length: 30 }
: (len >= 60
? { pattern: '3 3 7 ' + (len - 13).toFixed(1), length: 13 }
: { pattern: '5 ' + Math.max(4, len - 5).toFixed(1), length: 5 });
record.layers.push(
{ flow: wake, kind: 'wake', dash: len, shift: 0 },
{ flow: addPath('glow'), kind: 'comet', dash: haloLen, shift: (haloLen - headLen) / 2 },
{ flow: addPath('halo'), kind: 'comet', dash: haloLen, shift: (haloLen - headLen) / 2 },
{ flow: addPath('tail'), kind: 'comet', dash: trail.length, shift: trail.length - headLen, dasharray: trail.pattern },
{ flow: addPath('head'), kind: 'comet', dash: headLen, shift: 0 }
);
if (choreography === 'stream') {
// A packet train behind the comet reads as a continuous stream.
[1, 2, 3].forEach(function (order) {
record.layers.push({ flow: addPath('head packet'), kind: 'comet', dash: Math.max(4, headLen * 0.55), shift: 0, packet: order });
});
}
// The ripple lands where the comet does; keep it out of the edge's
// mask so it can bloom over the node boundary.
var end = null;
try { end = wake.getPointAtLength(len); } catch (_) {}
if (end) {
var ripple = document.createElementNS(SVG_NS, 'circle');
ripple.setAttribute('cx', end.x.toFixed(1));
ripple.setAttribute('cy', end.y.toFixed(1));
ripple.setAttribute('r', '2');
if (shape.hasAttribute('transform')) ripple.setAttribute('transform', shape.getAttribute('transform'));
ripple.setAttribute('class', 'ambient-edge-flow ambient-flow-ripple');
tagOverlay(ripple);
shape.parentNode.insertBefore(ripple, anchor.nextSibling);
record.ripple = ripple;
}
});
var windows = Object.create(null);
var extras = [];
var span = 0;
if (choreography === 'sweep') span = planSweep(windows, extras);
else if (choreography === 'replay') span = planReplay(windows, extras);
else if (records.length) {
var linked = records.filter(function (record) { return record.from && record.to; });
if (choreography === 'sequence' || linked.length < records.length) planSequence(records, windows);
else if (choreography === 'walk') planWalk(records, windows);
else planRelay(records, windows);
span = records.reduce(function (max, record) {
return record.active ? Math.max(max, record.depart + record.travel) : max;
}, 0);
span = scalePlan(records, windows, span);
}
records.forEach(function (record) {
if (record.active) return;
record.layers.forEach(function (layer) { layer.flow.remove(); });
if (record.ripple) record.ripple.remove();
});
var active = records.filter(function (record) { return record.active; });
if (!active.length && !extras.length) return;
var cycle = Math.max(RELAY.minCycle, span + RELAY.land + RELAY.rest);
writeKeyframes(active, windows, extras, cycle);
svg.style.setProperty('--flow-cycle', cycle.toFixed(3) + 's');
html.style.setProperty('--flow-cycle-chrome', cycle.toFixed(3) + 's');
active.forEach(function (record) {
var gap = Math.min(0.22, record.travel * 0.28);
record.layers.forEach(function (layer) {
var flow = layer.flow;
flow.style.setProperty('--flow-len', record.len.toFixed(1) + 'px');
flow.style.setProperty('--flow-delay', (record.depart + (layer.packet || 0) * gap).toFixed(3) + 's');
flow.style.setProperty('--flow-dash', layer.dash.toFixed(1) + 'px');
flow.style.setProperty('--flow-shift', layer.shift.toFixed(1) + 'px');
flow.style.setProperty('--flow-name', 'archify-relay-' + layer.kind + '-' + record.index);
if (layer.dasharray) flow.style.setProperty('--flow-dasharray', layer.dasharray);
});
if (record.ripple) {
record.ripple.style.setProperty('--flow-delay', (record.depart + record.travel).toFixed(3) + 's');
record.ripple.style.setProperty('--flow-name', 'archify-relay-ripple');
}
});
var shapes = nodeShapes();
Object.keys(windows).forEach(function (id) {
(shapes[id] || []).forEach(function (shape) {
shape.style.setProperty('--flow-node-name', windows[id].keyframes);
});
});
flowCount = active.length + extras.length;
}
function settleEntry() {
entryPending.clear();
svg.removeEventListener('animationend', onEntryBoundary, true);
svg.removeEventListener('animationcancel', onEntryBoundary, true);
html.setAttribute('data-ambient-entry', 'settled');
}
function onEntryBoundary(event) {
if (!entryPending.has(event.target)) return;
// The entrance settles only when each node's pulse finishes; the
// shorter fade-rise enter animation may end first without ending it.
if (event.animationName && !/node-pulse/.test(event.animationName)) return;
entryPending.delete(event.target);
if (!entryPending.size) settleEntry();
}
function renderAmbient(suppressed) {
html.setAttribute('data-ambient-motion', flowCount ? (suppressed ? 'paused' : 'running') : 'empty');
if (suppressed) {
entryStarted = true;
settleEntry();
} else if (!entryStarted) {
entryStarted = true;
entryPending = new Set(Array.prototype.slice.call(svg.querySelectorAll('[data-animate="node"]')));
if (!entryPending.size) settleEntry();
else {
svg.addEventListener('animationend', onEntryBoundary, true);
svg.addEventListener('animationcancel', onEntryBoundary, true);
html.setAttribute('data-ambient-entry', 'running');
}
}
}
function readStored() {
try { return localStorage.getItem(STORAGE_KEY); } catch (_) { return null; }
}
function writeStored() {
try {
if (readerPaused) localStorage.removeItem(STORAGE_KEY);
else localStorage.setItem(STORAGE_KEY, 'live');
} 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');
renderAmbient(paused || owner || html.hasAttribute('data-embed') || html.hasAttribute('data-share-playback') || html.hasAttribute('data-document-hidden'));
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-entry');
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 ''; }
};
}
createFlows();
html.setAttribute('data-motion-capable', 'true');
btn.hidden = false;
readerPaused = readStored() !== 'live';
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 (typeof MutationObserver !== 'undefined') {
var contextObserver = new MutationObserver(render);
contextObserver.observe(html, { attributes: true, attributeFilter: ['data-embed', 'data-share-playback'] });
}
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;
if (element) {
try {
var parsed = JSON.parse(element.textContent || 'null');
if (parsed && parsed.verified === true && parsed.repository && parsed.nodes) payload = parsed;
} catch (_) {}
}
// Keep verified evidence discoverable to screen readers without adding
// visual marks. Restore first so repeated initialization never appends
// another count or retains one after evidence becomes unavailable.
var svg = document.querySelector('.diagram-container svg');
if (svg) Array.prototype.forEach.call(svg.querySelectorAll('[data-node-id]'), function (node) {
if (node.hasAttribute('data-source-evidence-count')) {
var savedLabel = node.getAttribute('data-source-evidence-original-label');
if (savedLabel == null) node.removeAttribute('aria-label');
else node.setAttribute('aria-label', savedLabel);
node.removeAttribute('data-source-evidence-count');
node.removeAttribute('data-source-evidence-original-label');
}
var sources = payload && payload.nodes[node.getAttribute('data-node-id')];
if (!Array.isArray(sources) || !sources.length) return;
var originalLabel = node.getAttribute('aria-label');
node.setAttribute('data-source-evidence-count', String(sources.length));
if (originalLabel != null) node.setAttribute('data-source-evidence-original-label', originalLabel);
node.setAttribute('aria-label', (originalLabel ? originalLabel + ', ' : '') +
viewerCount('viewer.passport.sourceCount', sources.length));
});
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() : [];
}
};
})();
/* ============================================================
Tree Branches — expand/collapse for hierarchy diagrams. The renderer
lays out the complete tree once and tags every node, link, and toggle
with its ancestors; collapsing only hides descendants, so visible nodes
never move and the reader keeps their place. Selecting a hidden node
through Finder, Outline, or a shared link expands its ancestors. Export
always writes the complete tree (see Export Cleanup).
============================================================ */
Archify.treeBranches = (function () {
var svg = document.querySelector('.diagram-container > svg[data-tree-ui]');
if (!svg) return { active: false };
var collapsed = Object.create(null);
var toggles = Object.create(null);
Array.prototype.forEach.call(svg.querySelectorAll('[data-tree-toggle]'), function (toggle) {
toggles[toggle.getAttribute('data-tree-toggle')] = toggle;
});
var tagged = svg.querySelectorAll('[data-tree-ancestors]');
var expandAll = svg.querySelector('[data-tree-expand-all]');
function ancestors(el) {
return (el.getAttribute('data-tree-ancestors') || '').split(' ').filter(Boolean);
}
function apply() {
Array.prototype.forEach.call(tagged, function (el) {
var hidden = ancestors(el).some(function (id) { return collapsed[id]; });
if (hidden) el.setAttribute('data-tree-hidden', '');
else el.removeAttribute('data-tree-hidden');
});
var any = false;
Object.keys(toggles).forEach(function (id) {
var toggle = toggles[id];
var isCollapsed = Boolean(collapsed[id]);
any = any || isCollapsed;
toggle.setAttribute('aria-expanded', isCollapsed ? 'false' : 'true');
toggle.setAttribute('aria-label', toggle.getAttribute(isCollapsed ? 'data-tree-label-expand' : 'data-tree-label-collapse') || '');
var node = svg.querySelector('[data-node-id="' + id + '"]');
if (node) {
if (isCollapsed) node.setAttribute('data-tree-collapsed', '');
else node.removeAttribute('data-tree-collapsed');
}
});
if (any) svg.setAttribute('data-tree-any-collapsed', '');
else svg.removeAttribute('data-tree-any-collapsed');
}
function setCollapsed(id, value) {
if (!toggles[id]) return false;
if (value) collapsed[id] = true;
else delete collapsed[id];
apply();
return true;
}
function toggle(id) { return setCollapsed(id, !collapsed[id]); }
// Reveal a node by expanding every collapsed ancestor; nothing else moves.
function reveal(id) {
var node = svg.querySelector('[data-node-id="' + id + '"]');
if (!node) return false;
var changed = false;
ancestors(node).forEach(function (ancestor) {
if (collapsed[ancestor]) { delete collapsed[ancestor]; changed = true; }
});
if (changed) apply();
return changed;
}
function restoreAll() {
collapsed = Object.create(null);
apply();
}
function activate(event) {
var target = event.target.closest('[data-tree-toggle], [data-tree-expand-all]');
if (!target || !svg.contains(target)) return false;
event.preventDefault();
event.stopPropagation();
if (target.hasAttribute('data-tree-expand-all')) {
restoreAll();
var root = svg.querySelector('[data-tree-toggle]:not([data-tree-ancestors])');
if (root) root.focus();
} else {
toggle(target.getAttribute('data-tree-toggle'));
target.focus();
}
return true;
}
// Capture phase so the canvas focus handler never treats a toggle press
// as a background click that clears the current focus.
svg.addEventListener('click', activate, true);
svg.addEventListener('keydown', function (event) {
if (event.key === 'Enter' || event.key === ' ') { activate(event); return; }
var node = event.target.closest('[data-node-id]');
if (!node || !svg.contains(node)) return;
var id = node.getAttribute('data-node-id');
// Arrow keys follow the tree convention: left collapses, right expands.
if (event.key === 'ArrowLeft' && toggles[id] && !collapsed[id]) { event.preventDefault(); setCollapsed(id, true); }
else if (event.key === 'ArrowRight' && collapsed[id]) { event.preventDefault(); setCollapsed(id, false); }
}, true);
// Focus reveals ancestors synchronously before positioning its lens.
// The observer also covers paths that change pressed state directly.
new MutationObserver(function (records) {
records.forEach(function (record) {
var el = record.target;
if (el.hasAttribute && el.hasAttribute('data-tree-hidden') && el.getAttribute('aria-pressed') === 'true') {
reveal(el.getAttribute('data-node-id'));
}
});
}).observe(svg, { subtree: true, attributes: true, attributeFilter: ['aria-pressed'] });
Object.keys(toggles).forEach(function (id) {
if (toggles[id].hasAttribute('data-tree-initially-collapsed')) collapsed[id] = true;
});
apply();
return {
active: true,
toggle: toggle,
collapse: function (id) { return setCollapsed(id, true); },
expand: function (id) { return setCollapsed(id, false); },
reveal: reveal,
restoreAll: restoreAll,
collapsedIds: function () { return Object.keys(collapsed); },
expandAllControl: expandAll
};
})();
/* ============================================================
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:not([data-ambient-flow-overlay]), 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);
if (shape.getAttribute('data-motion-path')) clone.setAttribute('d', relationshipTokenPath(shape));
clone.removeAttribute('data-motion-path');
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('data-motion-path') || 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;
// Hash restoration and internal selections use this path too. Reveal
// tree ancestors before measuring the selected node for its lens.
if (Archify.treeBranches && Archify.treeBranches.active) {
normalized.forEach(function (id) { Archify.treeBranches.reveal(id); });
}
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 copyFocusLink() {
if (activeIds.length !== 1) return Promise.resolve(false);
var record = pinnedRelationshipRecord();
var relationId = record && record.id;
var value = readerLink(relationId
? 'relation=' + encodeURIComponent(relationId)
: 'focus=' + encodeURIComponent(activeIds[0]) + (reachabilityMode ? '&reach=' + reachabilityMode : ''));
return (value === null ? Promise.resolve(false) : copyReaderText(value)).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.ctrlKey || event.altKey || event.metaKey || event.shiftKey) return;
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;
// Export consumes the active semantic snapshot; its controls are not
// an outside selection. Genuine outside activation still clears it.
if (target.closest('[data-node-id], [data-relationship-hit-key], .overview-map, #btn-export, #export-menu')) return;
clear();
}, true);
window.addEventListener('scroll', requestLensPlacement, { passive: true });
window.addEventListener('resize', requestLensPlacement);
container.addEventListener('scroll', requestLensPlacement, { passive: true });
// Reader and Chrome may finish resizing the stage after window resize.
// The absolute Passport does not resize its container when reclamped.
if (typeof ResizeObserver === 'function') {
new ResizeObserver(function () {
if (!chip.hidden) requestLensPlacement();
}).observe(container);
}
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:not([data-ambient-flow-overlay]), 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);
if (shape.getAttribute('data-motion-path')) clone.setAttribute('d', shape.getAttribute('data-motion-path'));
clone.removeAttribute('data-motion-path');
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 legend = svg && svg.querySelector('[data-legend]');
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 readerFit = svg && svg.getAttribute('data-reader-fit');
var widthFirstReading = readerFit === 'width-first';
var automaticSequence = widthFirstReading && svg.hasAttribute('data-sequence-column-fit');
var readingScroll = widthFirstReading;
var measuredHeightFit = readerFit === 'intrinsic-height' || widthFirstReading;
var frame = 0;
var settleFrame = 0;
var lastWidth = 0;
var automaticAreaHeight = 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 MAX_AUTOMATIC_SCALE = 1.5;
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.style.removeProperty('--archify-diagram-min-height');
html.removeAttribute('data-reader-area');
html.removeAttribute('data-reader-narrow');
html.removeAttribute('data-reader-layout');
html.removeAttribute('data-reader-overflow');
setRail(false);
lastWidth = 0;
automaticAreaHeight = 0;
settledCap = 0;
clearLegend();
}
function clearLegend() {
if (!legend || !legend.hasAttribute('data-reader-legend-corner')) return;
legend.removeAttribute('data-reader-legend-corner');
legend.style.removeProperty('--archify-reader-legend-transform');
}
// Keep the original legend in its SVG: renderer hover/focus selectors and
// canonical export still own that same group. Ordinary capped reading
// and fitted automatic sequences borrow the outer corner at 25–100%.
function syncLegend() {
var cameraScale = Number(svg && svg.getAttribute('data-view-scale')) || 1;
var cornerReading = html.getAttribute('data-reader-area') === 'true' || automaticSequence && !readingScroll;
if (!legend || !eligible() || !cornerReading || cameraScale > 1.001) {
clearLegend();
return;
}
var box = legend.getBBox();
var parentMatrix = legend.parentNode.getScreenCTM();
if (!parentMatrix || !box.width || !box.height) {
clearLegend();
return;
}
var rect = diagram.getBoundingClientRect();
var style = window.getComputedStyle(diagram);
var scale = Math.min(svg.clientWidth / viewBox.width, svg.clientHeight / viewBox.height);
if (!Number.isFinite(scale) || scale <= 0) {
clearLegend();
return;
}
var left = rect.left + number(style.borderLeftWidth) + number(style.paddingLeft);
var right = rect.right - number(style.borderRightWidth) - number(style.paddingRight);
var bottom = rect.bottom - number(style.borderBottomWidth) - number(style.paddingBottom);
var top = bottom - box.height * scale;
var nav = diagram.querySelector('.diagram-nav');
var navRect = visible(nav) ? nav.getBoundingClientRect() : null;
// Long or multi-row legends retain their canonical position if the
// spare corner cannot hold them clear of the dock and content area.
if (left + box.width * scale > right || top < rect.top + number(style.paddingTop) + number(style.borderTopWidth) ||
(navRect && left < navRect.right + 10 && left + box.width * scale > navRect.left - 10 &&
top < navRect.bottom + 10 && bottom > navRect.top - 10)) {
clearLegend();
return;
}
var matrix = parentMatrix.inverse().translate(left, top).scale(scale).translate(-box.x, -box.y);
var value = 'matrix(' + [matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f].join(',') + ')';
if (legend.style.getPropertyValue('--archify-reader-legend-transform') !== value) {
legend.style.setProperty('--archify-reader-legend-transform', value);
}
if (!legend.hasAttribute('data-reader-legend-corner')) legend.setAttribute('data-reader-legend-corner', '');
}
// 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));
var changed = Math.abs(rounded - lastWidth) >= 1;
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');
var railShare = html.getAttribute('data-reader-rail') === 'true' ? RAIL_WIDTH + RAIL_GAP : 0;
var diagramWidth = Math.max(1, rounded - railShare - chromeMetrics().diagramX);
// Content fitting and automatic enlargement never reduce the outer
// reader region. The SVG alone takes the narrower reading width.
var automaticWidthCap = measuredHeightFit && viewBox && viewBox.width > 0
? viewBox.width * MAX_AUTOMATIC_SCALE : 0;
if (automaticWidthCap && diagramWidth > automaticWidthCap) {
diagramWidth = automaticWidthCap;
html.style.setProperty('--archify-diagram-min-height', automaticAreaHeight + 'px');
html.setAttribute('data-reader-area', 'true');
} else {
// Ordinary automatic diagrams retain their existing document flow.
// Settling can also stop binding the cap without a new measure.
html.style.removeProperty('--archify-diagram-min-height');
html.removeAttribute('data-reader-area');
}
if (diagramWidth < Math.max(rounded, shellFloor) - railShare - chromeMetrics().diagramX) {
html.style.setProperty('--archify-diagram-max-width', diagramWidth + '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');
syncLegend();
return changed;
}
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();
// visual-check fails any page whose rounded-up height exceeds the
// viewport, so even a 1px remainder must be reduced or declared.
if (overflow > 0 && !readingScroll && lastWidth > Math.ceil(minWidth)) {
applyWidth(Math.max(minWidth, lastWidth - overflow * ratio - 4), minWidth);
settledCap = lastWidth;
html.setAttribute('data-reader-overflow', 'reduced');
} else if (overflow > 0) {
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);
// Sequence first-screen fitting preserves source text sizes instead
// of shrinking to the shared hard readability floor.
var readableWidth = viewBox && viewBox.width > 0
? viewBox.width * (automaticSequence ? 1 : minimumReadableScale()) + chrome.diagramX
: MIN_READER_WIDTH;
var maxWidth = Math.min(MAX_READER_WIDTH, viewportCap);
var readableMinimumWidth = automaticSequence || measuredHeightFit && ratio < WIDE_RATIO
? readableWidth
: measuredHeightFit && ratio >= WIDE_RATIO && Number.isFinite(declaredMinimumText)
? Math.max(MIN_READER_WIDTH, readableWidth)
: MIN_READER_WIDTH;
var minWidth;
if (automaticSequence) {
minWidth = Math.min(readableMinimumWidth, maxWidth);
} else 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';
}
// Height-fit families with notes below the fold may shrink to their
// readable floor. Width-first families choose their reading width
// below and retain page scrolling instead.
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);
// Compact automatic sequences fit the first screen without shrinking
// their authored text. Longer sequences retain reading width and page
// scroll; other width-first families keep their existing strategy.
var sequenceFits = automaticSequence && Math.min(desiredWidth, maxWidth) >=
readableWidth + (docked ? railExtra : 0);
readingScroll = widthFirstReading && !sequenceFits;
if (readingScroll) desiredWidth = maxWidth;
// Only a binding enlargement cap needs a separate normal reader area.
// Uncapped diagrams keep their existing natural height and Chrome fit.
automaticAreaHeight = availableSvgHeight + chrome.diagramY;
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,
legend && legend.hasAttribute('data-reader-legend-corner') ? legend.style.getPropertyValue('--archify-reader-legend-transform') : ''
].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 });
window.addEventListener('beforeprint', clearLegend);
window.addEventListener('afterprint', schedule);
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,
syncLegend: syncLegend,
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,
lift,
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 || liftFrame || 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); });
// Camera zoom changes outer page flow without changing intrinsic SVG
// size. Follow that box for dock placement, independently of scrolling
// and the baseline stage-rail measurement.
new ResizeObserver(scheduleLift).observe(container);
}
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 minimumZoom = 0.25;
var maximumZoom = 3;
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 originalFlowHeight = container.style.getPropertyValue('height');
var originalFlowHeightPriority = container.style.getPropertyPriority('height');
var compactPageFlow = false;
var viewBox = svg.viewBox && svg.viewBox.baseVal;
function clamp() {
var width = svg.clientWidth || 1;
var height = svg.clientHeight || 1;
if (state.scale < 1) {
var visibleWidth = Math.min(width, container.clientWidth);
var availableX = visibleWidth - width * state.scale;
state.x = availableX >= 0 ? container.scrollLeft + availableX / 2
: Math.max(container.scrollLeft + availableX, Math.min(container.scrollLeft, state.x));
// Long sequences keep their first rows in view instead of centering
// inside a layout box that can be several screens tall.
state.y = height > window.innerHeight ? 0 : height * (1 - state.scale) / 2;
return;
}
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 camera = sampleRenderedState();
var x;
var y;
var width;
var height;
if (window.innerWidth <= 720 && container.hasAttribute('data-wide-diagram') && camera.scale >= 1) {
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 + (((container.scrollLeft - camera.x) / camera.scale) - metrics.offsetX) / metrics.scale;
y = viewBox.y + ((-camera.y / camera.scale) - metrics.offsetY) / metrics.scale;
width = Math.min(viewBox.width, Math.min(metrics.width, container.clientWidth) / camera.scale / metrics.scale);
height = Math.min(viewBox.height, metrics.height / camera.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));
// The camera's layout box can span several browser screens. Intersect
// its logical bounds with the actual window, including page scrolling.
var matrix = svg.getScreenCTM();
if (matrix && matrix.d > 0) {
var end = y + height;
y = Math.max(y, Math.min(end - 1, -matrix.f / matrix.d));
height = Math.max(1, Math.min(end, (window.innerHeight - matrix.f) / matrix.d) - y);
}
return { x: x, y: y, width: width, height: height, scale: camera.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
});
var readout = (showDetailLevel ? resolvedLevel + ' ' : '') + percent;
resetBtn.setAttribute('aria-label', readout + ' · ' + 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;
}
// CSS transforms keep the SVG's intrinsic layout size. Shrink only its
// outer page-flow box for long ordinary diagrams, never the SVG itself:
// changing its height would feed the smaller size back into camera math.
function ordinaryPage() {
var html = document.documentElement;
return html.getAttribute('data-embed') !== 'true' &&
html.getAttribute('data-present') !== 'true' &&
(!window.matchMedia || !window.matchMedia('print').matches);
}
function syncPageFlow(camera) {
camera = camera || state;
var height = svg.clientHeight;
var ordinary = ordinaryPage();
// Restore ordinary flow as soon as the target reaches 100%. Chrome
// uses that target to resume baseline rail measurement; retaining a
// partially shrunk box during the CSS transition would inflate reserve.
if (ordinary && state.scale < 1 && camera.scale < 1 && height > window.innerHeight) {
var style = getComputedStyle(container);
var chrome = ['paddingTop', 'paddingBottom', 'borderTopWidth', 'borderBottomWidth']
.reduce(function (total, key) { return total + (parseFloat(style[key]) || 0); }, 0);
var flowHeight = Math.ceil(height * camera.scale + Math.max(0, camera.y) + chrome) + 'px';
if (container.style.getPropertyValue('height') !== flowHeight) container.style.setProperty('height', flowHeight);
compactPageFlow = true;
} else if (compactPageFlow) {
if (originalFlowHeight) container.style.setProperty('height', originalFlowHeight, originalFlowHeightPriority);
else container.style.removeProperty('height');
compactPageFlow = false;
}
}
function syncViewportClip() {
if (clipFrame) cancelAnimationFrame(clipFrame);
clipFrame = 0;
function sample() {
clipFrame = 0;
var rendered = sampleRenderedState();
syncPageFlow(rendered);
clipToViewport(rendered);
if (Archify.radar && Archify.radar.isOpen()) Archify.radar.sync();
if (Archify.readerLayout && typeof Archify.readerLayout.syncLegend === 'function') Archify.readerLayout.syncLegend();
if (!cameraSettled(rendered)) clipFrame = requestAnimationFrame(sample);
}
sample();
}
function apply() {
clamp();
svg.style.transform = 'translate(' + state.x + 'px,' + state.y + 'px) scale(' + state.scale + ')';
svg.setAttribute('data-view-scale', String(state.scale));
syncViewportClip();
renderControls();
outBtn.disabled = state.scale <= minimumZoom;
inBtn.disabled = state.scale >= maximumZoom;
container.classList.toggle('is-pannable', state.scale > 1);
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(minimumZoom, Math.min(maximumZoom, Math.round(next * 4) / 4));
if (next === previous) return;
var longDiagram = svg.clientHeight > window.innerHeight && Math.min(previous, next) < 1;
var before = longDiagram ? svg.getBoundingClientRect() : null;
var anchorY = before ? Math.max(0, Math.min(window.innerHeight, before.bottom) + Math.max(0, before.top)) / 2 : 0;
if (before && (before.bottom <= 0 || before.top >= window.innerHeight)) anchorY = window.innerHeight / 2;
var logicalY = before ? Math.max(0, Math.min(svg.clientHeight, (anchorY - before.top) / previous)) : 0;
var documentTop = before ? window.scrollY + before.top - state.y : 0;
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();
if (longDiagram) {
var scaledHeight = svg.clientHeight * next;
var pageY = scaledHeight <= window.innerHeight ? documentTop + state.y
: documentTop + state.y + logicalY * next - anchorY;
window.scrollTo({ left: window.scrollX, top: Math.max(0, pageY), behavior: 'instant' });
}
}
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 scrollLongDiagramTo(contentY, metrics, options) {
if (!ordinaryPage() || metrics.height <= window.innerHeight) return;
// apply() may still be animating its CSS transform. Recover the layout
// origin from the painted transform, then use the final camera target.
var painted = sampleRenderedState();
var rect = svg.getBoundingClientRect();
var pixelScale = rect.height / metrics.height / painted.scale;
var documentTop = window.scrollY + rect.top - painted.y * pixelScale;
var target = documentTop + (state.y + contentY * state.scale) * pixelScale - window.innerHeight / 2;
window.scrollTo({ left: window.scrollX, top: Math.max(0, target), behavior: options.instant ? 'instant' : 'smooth' });
}
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; }
scrollLongDiagramTo(metrics.offsetY + (logicalY - viewBox.y) * metrics.scale, metrics, options);
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();
scrollLongDiagramTo(contentY, metrics, options);
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();
});
});
// Reader width and Chrome reserve can change without a window resize.
// Observe their resulting sizes; the SVG remains intrinsically sized, so
// the outer flow height cannot feed back into its camera base.
if (typeof ResizeObserver === 'function') {
var flowObserver = new ResizeObserver(function () { syncPageFlow(sampleRenderedState()); });
flowObserver.observe(svg);
flowObserver.observe(container);
}
if (typeof MutationObserver === 'function') {
new MutationObserver(function () { syncPageFlow(sampleRenderedState()); })
.observe(document.documentElement, { attributes: true, attributeFilter: ['data-embed', 'data-present'] });
}
var printMedia = window.matchMedia && window.matchMedia('print');
if (printMedia && printMedia.addEventListener) printMedia.addEventListener('change', function () { syncPageFlow(sampleRenderedState()); });
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;
if (item.links === 0) links.setAttribute('data-zero', 'true');
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) {
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) return;
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) {
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) return;
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:not([data-ambient-flow-overlay]), 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);
if (shape.getAttribute('data-motion-path')) clone.setAttribute('d', shape.getAttribute('data-motion-path'));
clone.removeAttribute('data-motion-path');
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);
if (shape.getAttribute('data-motion-path')) clone.setAttribute('d', shape.getAttribute('data-motion-path'));
clone.removeAttribute('data-motion-path');
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 copyLink() {
if (mode !== 'result') return Promise.resolve(false);
var value = readerLink('route=' + encodeURIComponent(startId) + '~' + encodeURIComponent(endId));
return (value === null ? Promise.resolve(false) : copyReaderText(value)).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) {
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) return;
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:not([data-ambient-flow-overlay]), 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);
if (shape.getAttribute('data-motion-path')) clone.setAttribute('d', shape.getAttribute('data-motion-path'));
clone.removeAttribute('data-motion-path');
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 copyLink() {
if (!selectedKinds.length) return Promise.resolve(false);
var value = readerLink('lens=' + selectedKinds.map(encodeURIComponent).join('~'));
return (value === null ? Promise.resolve(false) : copyReaderText(value)).then(function (copied) {
copyBtn.textContent = viewerText(copied ? 'viewer.common.copied' : 'viewer.common.copyFailed');
copyBtn.setAttribute('aria-label', viewerText(copied ? 'viewer.common.copied' : 'viewer.common.copyFailed'));
window.setTimeout(function () {
copyBtn.textContent = viewerText('viewer.common.copyLink');
copyBtn.setAttribute('aria-label', viewerText('viewer.lens.copy'));
}, 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) {
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) return;
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) {
// Return focus where the reader was when the guide opened; the
// trigger itself is only the fallback when that element is gone.
var target = returnFocus && returnFocus.isConnected ? 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) {
// Clicks anywhere on the trigger (including its icon span) are the
// toggle itself, never an outside dismissal.
if (!panel.hidden && !panel.contains(event.target) && !trigger.contains(event.target)) 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.shiftKey && (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Home' || e.key === 'End')) 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>
<script>(function(){var h=document.documentElement;function set(m){
if(m==='embed')h.setAttribute('data-embed','true');else h.removeAttribute('data-embed');
var p=window.Archify&&Archify.presentation;if(p){if(m==='present')p.enter();else if(p.active())p.exit();}
dispatchEvent(new Event('resize'));if(window.Archify&&Archify.view&&Archify.view.reset)Archify.view.reset({automatic:true});}
addEventListener('message',function(e){if(e.source===parent&&typeof e.data==='string'&&e.data.indexOf('arch:')===0)set(e.data.slice(5));});
// The viewer's own Esc/Exit leaves presentation inside the iframe; tell the article so it closes the overlay too.
new MutationObserver(function(){if(!h.hasAttribute('data-present'))parent.postMessage('arch:exit','*');}).observe(h,{attributes:true,attributeFilter:['data-present']});
parent.postMessage('arch:ready','*');})();</script>
<style>/* Viewer centres the 720px canvas in full screen; on a narrow phone that hides its left edge past scroll 0. Auto margins centre without overflowing left. */
html[data-present="true"]:not([data-embed="true"]) .diagram-container{justify-content:flex-start}
html[data-present="true"]:not([data-embed="true"]) .diagram-container>svg{margin-inline:auto}</style></body>
</html>
"></iframe>
<iframe data-l="en" hidden class="arch fig3" title="Figure 3 · The sovereign firm: two banks, same AI model" srcdoc="<!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.2.0">
<meta name="archify-reader-contract" content="declared-wide-v1">
<title>The sovereign firm: two banks, same AI model 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,d09GMgABAAAAACK8ABIAAAAARmAAACJTAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGoE1G4JgHIFaBmA/U1RBVIEgJx4AgkwvTBEICrporgMLggoAMKw6ATYCJAOEBAQgBYdGB4kHG7Q9RQdy2DiAAS7vZkS1alhU/18T1IqxA6flgFcrUbKj5VbNnlXLwaeDwbp1QnuC56gjzYrVYUfM09q5suLaHUkQV50dVYOIj26jU47IlZS6HX26O7S6VQJNqV6RcRcvsUAQII6oCK8Iu2mJiGj0lbNNfv915sno33zICElmoYK1snoO9x4ZgoQycWQEkjCsiF3igH/+3aw/UUIgZoRc8CIaKNsy7I6Id8U6+0QMmicmH98m2lGjQsWg7fC4rX/GRXlV6hVOVBAYeQzG3oA5QhhREzZSDsy8ai/KX1lexQ/5fdfe77h/t2zmAwnMQNS8XJNTe71y53BmES9PtGhcdjddmYF5Hvnplektv8fZF9WsH3X2PpAUWbLj5IgLgGPnKbJze4ety/rXAlKO0AEFDOJ5270/ol1gXWCBR5AATa2Byydp0/756aEcCuM5BMYhlMWoWVg4td0VrEW873we4D6NuDiaDTigbNtFBA01zRucdKtyYsbau/Ylw818WV/15quCBtElSPDFNq1gu4PrCh3/MpoYAQSwsw76HuYAm+/vVdX1P0E4ga5gKphK+cI7pGsY08qWYSxrnz7wBZKfn7QAUHJA0XQIi76AKhdQ8l3pcEVabVO+qBTbaVJKLWMrU+3rlnFM5vD81z3j5r+Di4FRsw+auZu0mKdlKUaQMGUyVONOtki2UuHREIwdOn6N/8daxcfsuGwuatyWeAwS/gwJkdjWvc6rQxDIip5TBBqvgGEfo3k41TziVqCQURGjeeJomYwOU85jm4boTuboAdboYY7oce7oWUH6BOQ8ntYQfVacvgoY0dfqpO+CXMAEMVsE4p6jwRAtBMjDAsYwo7OnQYAkVH73x/BEilZ+blxNESQAvZluhQoTgXIeUkRVxjGF8DLSwUESHtwWNyzRJAk/INVbLBfQdJo1LwurBFGHVqAOnZhMUbC9KJq2gGSpodWaSgJQwINwQKhGX8ycUBW2CY2GAmmDgTgEtpIW0XO5ikjTqJTMTzAyMQvJNSU5VsZfcqyFdh3akw8QbsMeTDw++czSmkvvssG6a1ZQ27wojJXmBmzB16q1FNE8oLvWBc0x5I0VARvPLYZNbt++pDze6Jicp43puIAcBdwMQ6ETYEq7FKiLBZDFOrMtw7QUklimpFiUmSj/XQrt3NkLBbDjrxUAhqhKs4FmW58BDoVsfRjr5wUbvADuEpLxcA3+InD9OYUneed/t3RYLicB6caBoZ2AA0QPSbb8Ld9LNwoAP4UV0fShXsyLIFJsKsoELhEAFwAyiX08YjfBmpGPiKIvOjM4Ga/QtqncFMgebCGi++JUQ1O7ZkMHMuNQd3ED5gg3+5RT9oPKcBvBXiwAY8VCjTSCLQ0fyvHvB93prQwERgbJkBNueqedDpv3vjf9f4DMXWMkT9pOeot32WXOe0cjWS00d8Nk2P5f9Q8Af1Lv2FPszuvF5KHEEpE6DVxpg0cWeDNmsVgCev2nDWdAqSCntVixEqWWWU6gDKKckIKSmorGSjp6GFINmpWNXS2Gi4eXj19AQlK9lLRVGjTKatOth1svh9ZwGlmTZl2C4jKWcsoDCNcMZsE8rA+gvfojcWEvAnMfKFxGRoU4brKOcW6voIHxcjYmjLu2g5SyC4pZdV+jdgC6Dzb2qBcxCcT2OTHgGISxN6jBMXRTYgMaygZXLxbLIjioWzerTwJKhEt07ypIx4QlY7kMIQvFIaXG8VCWJFVL1bQiHStOMfJTeEOrVbPyAwU5LCXkTlZC/ZmXqMWpSfEZjbLIXB2itDYzSd/hWlknOStXUVmZqfPV4lw+SROUOFSTzCXnBVU1TVVnJmsROwG5pASVE7TKvKToui5rfMFYMeRdhrLnsM5hDpKLHH52bYwy7g2KWGF7CHsHkQn8PNzh1fnZHqM5IkIy605rFEPJHG814p4GFvpZA7CzQyHlN9SwYoAOjsSTY4UFSyyTRIKRFUIDSGbYaY2QCZUqRfnsQK7Gzx4v+fo6CjvnvdmLdKFb0TTHpir0kG95eCxgxrvt7CDiqiSFgpwDGZ+DSEiYWckJdbA6XNqa5ZwZ7trbnW9K1bKe38fOeTX9oTvXXa86rgUt456S5XBailVmNHu7fB5VMhCMkDLPIkmjdnVgb7eeKRPCBsO8WcoIiLqigM6PtAvb/YinG4ORL87FBYMTLXVopGhcgEZEIKcLXGWutmZpar5GrCYnhDLpnbWW/wp4vslst3MpiwB7xbQQI05LgNEFo0sh3QfHwJnSA0gC0nzuEHvn0WovnuvnA44nmILXDqFUHizk6f46kRfHW4V+Q2Yg1MHNCnkikROc3MuSF5gkVVvbTVxPeiFpAAusg9cr8SKNHVnh66YzN1Au77j9BokJ7Tshr2yZcUvLZIQuPGAnWnQKMsxqCVyS1XiBGu9Xtuuv1wf4fpEh3ZvnkdusbjlTYnkn6grL/FgCDu3DCtcQlJxIOaPQWKyljgchl+QISGb2EaiH9jpyGspB/tKwcdRoZnp3QdMtuZRiSFLWg7HNLVp8OF8MBoKQJhJG8JwTCbkFjCA0uk0H5HYHRzJzDbarL1y1d9iC6tCO5jE9TPJ5FNNMXsqyPxxJX1Hx+Fna2DvxTJN6DoX0Y5JAYHjVZgGzlbW5sdgukezbbMIXskZtaLV6v+GjogCReew1E8V8DYkcm3QyMrBb7SIgJIfWnXy+yhWidEbrQl2NLNrsUtq51BmDUNJ9dF1iLcvv0pu5njJVYQiuWGKQSJP+UPBvFfOAWgbfSynLGpGWSTkV/9MkIxnJSJaMQtolPeoAty1C/EUnxj7AJY+EkgODAAQvrI5icbygXFvfi4X0tf0GbIu0p4CRIdjfHrI3z/X73HO6eL27LHWKKAMsBUneUgLe9OJkYmknZQUgwhigEYGI+xPJZCSHkQqM+J2qKEynyVaRrPmF3zs/jGYvUJZBq/p9C6NYzjyJkcDYb/UQzT7bXeBsZ8SW85hElsARD1spNNNsJ3WnCQpwe8JuNsu9DWkX3aIPxuPzdECFDrRtfWDYbO7eXEQmwgz/55si4N4a7Xt1tE7y1ucvHDjtxbqv/pdOFxf7q/4fBPHkeGsihFZY7OXM9L0TxdU+DJfX8VLjZmbQQJNaHW01DDq6taReqYR6bQ9odMioTMFBK6mIzCMhSR6VRyCLEySGkRC34WTA948KZwk5x6NgHO91284LXcICmxsEkdWMadjLUMwYAw46h1SH3FtGfGBoB3u66lkjRROE8IzPP+GeNHFfbxcINHJXNt6liT/Lvd6ffsj1jucbzGG37foTpVLiv4/vHevOBSeJqJZJnZOz6q5GvruRmlQ/oX1cm2wSqHVITobWUkYjnP4aw7TBEnMmyGA4iVvx1IlpDTsMPgjg9V0JhYonoSo6/aea9olW78MoWo9TNdg+UHboeok6NI1VWw1GA6VXi6rhq/FS6zymwYIp/LwURjuywJOoCB2mJdSoWAKPJ5bQ85xNVp06lJSBzasV95h7ZDvIPGp7P72q9eN1CGtF1rV/nHg/dronPXhlHWKzla1bfRX0tTARtNcmAr6H1fL3NTIoJNmJVIcnwDfFvP3BzPTvmsNmFUW4DKjjQbGsqvpie8hxmTI+TCoj8UfBd6TyA6yA0GgwM4nfgJuNmIZYuYLNDyzzQa0Do3IFZxgmFJE5A197ESLmV+5eP5Ne8cv42ERTJ1NOlkHCjOuhGZaMA07nnQ0PuwvPXXMDx+ZZj5tI/XOX3cSEWE9bCtT6SN4TYD/wGjHMA7DEnAk6MQiJ/UTqxLTkDuNZA/4tx2ZmzTG4aZMn3sjfZRDVMTKN0WPB58zJG7yz9/fRbUZimAjqlWpoxofxDyzI8WlOLJ9uY8NQVKnUClZc/Wj/FGaag6asEYZDnLzldjb0XzYtv4QsX3u/ZUpwGmOj7VF/HLTcX/T85KXxoexhdfnhbNNDmeen/BKyXc9kj9Gab/q7nwFff818eJQYX+uJvm4wvM70eGJFozyFw27dZ33UlYAg4meiel19uJnS+yBmvA8cw4fczm1718v7noofRPPEqPie+caZvqLxzzfvokaZoS1D2k/+fvQ3hU+DOaoL6Oe39MW4tLHSZDdV1Ru5nvj67kiLT+y3+8WtPlDsmNISXUDLep16vclRo8WclJF1h/QWm1pD63C9xaRhcXVdEjVlBvgQWulwVIXoSKur/ZhBG21UgzEM3CzIZwroBZ+39mao7DEX0vO/aAVtE2eJEAcInz/dmmO3vTf+A7qOHfSDEN66/1e5ug38dPrVj4rjG5Jv/YPQZ3e1J+PjR8Gy9ujr5GjYMAZlXJ0Cp7wUXqeQcXDMgIZZOU559v1efuxY1VjFEb4PcR6pGKs6RiDga9lvU+PlTzW3Rf8cW92yh8KkH+1vY3Pglj20Ncfu+GQHu8P2GdhBjk4c7hqmhzuHPTkwbQ89kXPt7txNOZ4GSj4BGyffcVfN8PvKd60GoY4n2k/GlSoeEqqozEzuc+3CaFqn88+B7QLvuzLkvizKCNda5Uq+R5JgGL2iirpVqaA8mE6GhY4K5Q4FqtyurBBzOW8O7LJmLTo5nsxEIh0d8FK9VMBLD+1ZW3iMx+WsIeLUCV4R9KKFDgw/Y0QHRH6d1eep6a3pV2kdShxInLfqkJvLepGbEF3odfIyeZRZTR7hAe1icdwYA2v7v3Z/vRYn7vEjF2cJlONQMORA/hmJI2SDUYurdC1Qvx6LNDgQjazyoJGTySOkFcq5+U0Hr3w3Owln7/+2ejxQ4426daxQf7xSc9vrKvBuTQ+lgrc4nZ6cB3NuDQWTDoZNBduDbMpZW3VY1a46XJW/lGDktAHOtmWsBmUkpoBf5cZUnTuUvZu3FN7qTBSmNdYA6fV4SvmZYmHVh8ae9NUQY2igToKDvBNrbU/+9W5v98AQO+TycCwsOhs4DI2BaEucMcr4iBw6tXozZGBKa+dRJvK5BiH4zh+xm1fWmoiJlNeMWRNu9kbHTVILUm6R3uQYrpwgSP6R/yyYqNjplp1TV5XxvYL6zolRMUguAHPHeS8a7WAfvp4h7JPrH3ix4M8RKgZStixsmRLgv1f+cGi29tz3/paJPcVs4s/G2qG1xvifkOwNNFse51edrJbImsqEtfI+R3f130kL7itFhQdC/G1NTieKeQQio0T236dIodCwHbc0+OpACPyiEgm94vMPX7d4H7CzPv8IngX2AnfH+fSL475pmTt+CS/CJcesakENCBzlb0DYc2+Tt3VmeQMtw8Ah6K8580SpVFq0t+t0PxLwifMy+Q0F0cKYho+rivM326fTB0LAXrp6ewxhv786s0/fKo/l3NV/weZW5eiYc+RjIF5fjDY1cAcjzVn+WWZfzkYOco8rjz5b+9fnm9CoP6iH5i0YYeYTIowY+OKXn7fRggvgRFf3MPuw3KTVV95/d1GCdEQPYT/65D9OyFBlp853nafPd573IKMaxJXrzFHAkwGkHtYYgqe7C9gjd0eyYRfTEAnf5bpb0SwWNyvuBh+ODTGXj7yZv0aPq3fmXjAM3iPiUL2mXc4JiWrEyvuk0vuUG+nZrIuAXf6shZ8XELsXjXS91cyV5nqjA/nzYExqgEHHSKicfRt4RraPWEb0XbRBONiW2dtnn7bqBTsFQm/TQ+Nwnm+78dlXNcsJS95h3hHammsr1IZPrcc7V/z5yJIKkVlCiuo/kiTLLxuH3zvNobCP9T1oe5DJ5NXbMIMnlgqqWBGGEyoNYSaWs1rQ0XhPxO4J/nwy7bketNW6w5Q1FK69bA9FLGbkXsllyb1I2cHx+TgIWifPfrfLR7iydZFvEfZ43/pj6eIDLx1A94qq9oLV1yfLbqsDp2EtQqXBq36AnI3zsb77bfczmdkCaKPMHj4VnJ9yh1HwjO981zvdOMMSnqtcOXv2i9Sj0vYeNWMkdMOcMuGcuS/uqBJmJJclGWFFaHw0oS1F9Ly/5N1F05yz/tKPg4UrJctKSpeXlCwvjeG/Zx7IH13miop72TL38F3g/4XMzljzTpDfQwecsKb8/0x2evleiPMFsi/IKpjMjVaqnKr40oe3x+RLAzIAHUIlyYMGhXCbBK6ARk0kGH0o0HZHhXBaBbEWpD+LADlkARB8LEgETqCYQMKuOhOfBZ4xImGn0BH8TOIKh0e9q8UUo+wdICjCabtNlrJn5RoQ0ykzqC03FEI0LYQQaltAmKlFUI/2ToEI7xC0+FC88yE15H4Z0Z1Zz9mTNCgylYrHou+wGCcxCyJhNY2rwh7wE6IQX4fP0j9AAMoJ0QqYywYrR/R74NT5RoB/EOZyAWioOzCDF41CFZTTfsZF3sYyS19JWFle1FcJ0A5WN1jBhubmEEKCjxIk4BJ+eIr0V5XGNoUs+NFTQniGpcBDSaHqYvIypplGgkITyhIY6sPJiuRnmKZGkPpSTgmU+olwSP//BbGKLWfTYo1dlrzxic8yYelz9iTO+qOc9vnTh9vcwUbeyRS7HPPNvmgiSJiqq6ltCmuvOCnyKaKm+ppqraPuWta6RevebZtjbvYa7zbiiMd+l1PbiqqWurGwYkqqSt2bp+vd7Bf3noaaaKXL3ex+n+JII60ciTfZ7AgYPHK8iaSQWjr5ZDDaxMedHcNMetbOLZOfzZzWvM3dvvQ6t3Vv2uCutm/utdz63XltN3w77+Dd9Q4QDIkGdMqIZ+OSdVE5SaLrOBBscCDv25sCENJ/vt28f/MxTUkCcRzveU8JHSiY65gpjISpp+ESCaT123PxfhhlIjZCGlUSmyyeUL53ncszdXOZHrmlZNhFmYzFBbW7qPb1oW2j1OXWqUzEFomju7xqQOEZkdbQje2DKD84PzimJ6nI+KB0ZoCPa76pslrgQvpUWiKBK6/XH++vQ0/8Y/f6E9GXclK6enG11ObA5c7oNs8E+cyxqBf/Bv72bIQyxaCFsySzJ4I9syRXEvL0ZCKfWYGQK3a/WnWYTwAjjcJ8CJ6IpdigeeAK3+fz0F4RoQ7k8KZwLQHNFYX/ofcYjU08mdyV4z1T39+a5s7dNqT4HQ9a0WS7AZJ58tMvWt12C/sfO53Gz4/qsHNSG1nIBb0SlmaVgbn/Yi7EB0Uud9by4TJbKbFy/BKqLzmshzX2QWycqqtR1ubii6+PiWOfRHzq1StqYFyOL4xGXhbN6wIN1JWZyR95dJ4HuSq4luRKQtpcxOZCY2k0coVpqA19cqgFyoLL880SeDKduyeENmanCq4TCPppv8dPvSqEeIn9UfU2GTNOtUQyIgZFXz8SbZIdMjcPUIqIj/OPuaj+ZoBxTVsPaS1gfTb/aT+u/idZ1A1IfsoTWk6N1BqjjdpIB3TqT416tV7/azF0JG9teCx5JBPSZJNJebgxZd+HtSu0BfBCPcTMbYWu9/mNf07ihfbBdRSVSq/Hym4W9UMo5JC+HJBO9FgNOCE0ZZvZDENih6+x9kgTcLhgZ5MubSegwNedoeLfKuOCbxy3EhFSjQwdrEAC5zq8CP3LFhFqyfNwMjnmeKB4yO0N6zblJNEyPqyzjTr0o5ExhkLLEGtaQM604TQFC9qpSHEpgQRPFAbay3pf/yNogABF/MCVyRZ1zWhSPhlPearReRL8Jz9I9ItwZ7u9LIlKAq1Js8JA7+8YgvQRT8j3m9lF0jOHcOq5U+3j5lnFYWawzhlhqJ2KRxN9qVPpyimd0yYbLDxSo5gSicc1Wu2H5VHxeR/n4kys+IbhDKm2S5fAq69GWM//C30CNk6dwNP8wsIatfqHvFik92eI2x4uRzPrwTRJ2VdFazKljLgf+gbVC1TUs81MyZaF6j6IzvHEvbOmftl101hVBQwcLSA2Tjt8hE8aAHWOei6od9ZnIQBqfzt2zItzcP+kHDyeC+RUq1l3681WB3bqL41Wu9WouhaQn7sShfJi0FwDMYOrTETzBcaTVr011r03zo5597tRjQvGMbwM2zZp1pCpdVZuUxOxpS9tr9Fvfuy24G3UYqfziunz4rFToAVxNkEecF2KpWI7vwERvU0IM7P43dDUgcow9Tq7m9r3d9ZWwvzpz/ejyYujcsJ4nrJWqxayuU4m893Lr/Zv9YfusGc6aCSl7YFqTT9e7aPCBvBy/mH8bM52llC1bYkiLgePm4DaIYZtfedyS4Zm7uiFoslORVMnpp9YmObObKSam/Yg7upOUFsio5P1cB1DyN06KWN2OpjDVTLQpUKNuGpGr7Z/ZvFICW7wJKn9HJJlacDviDqx5oxAvZ9k5KebXsVYdnJXy5uffPe9Bx9dN2rJGwsxn1AmrUWb/J3wNfZBviAG856ohVsM36P4Nz+UefUlLQVdtO7x5bXi0iumfG8lA0G+S4Y0qxf+n7NIQJW6tYfftntva5HI4UxN/tYyNcUk3WrelxXNNHXltbrghVjBxoJhExubj8bDYJ5JIrKLpDmUG8nC+jlbfvL/A9Y6Olo8AzOubZgW+7gY/0DJQsCgINYj8UrM37SOzyrshJmcMQvIEL3EOpIhOZoN8T0s/n3LUQtaOTKKPgVHZiZ1quzc7dyf7nuiEHVMGszWbHBWf8grg/yP2AkTbgTCSvq/jQEsXrtEHd7OclR2QDte8qLdyKqmDsyNYk8ZqRV04LUltGhbLdHpimL5PGOEIAzPiJ3J5PIjCTWxcv0+4omTQ1IVasiq7oqwsZCN1dCBbr+eAcR7e9PyqoS5h+iiJOIiFtd69sMc9g0eK5O6QYwfSXRTg3hlJOXTpsEWlspDrRoVT5HoNAKuokHpnuAyvjzZiFxU81Bk2r5miEpvvpG7bfeTeqGbGJqlkeEC7zKKJpDLRcvaYR/EYheEkMVLGC5eNLISeluMy9H+Yxhr93xALqGgfefaql5lWsdn06O3ANpj99AcLOozSAZWBX/r9vrLs+R9KR/a8ZnGspdhDuYGZmGfOh1IAq9GC6G1ijiH5svdutAm9fZ32BPxPLT44ce3CGI/qzLZD7zxy35QLz5GP2rMY7f8ZMZ1KzEDDQ1luroeSWPIdT3xl19Jdt6pNpGeRKpF5o8djTYs4EGCdYzyito1RJvdiKVhTPKQjyHWUhzNuzzAo1HO6yu0IltS1qdwcEBfH62x6ItXu8sHHgOuZ9zQkgkObimOc4PfTSvJObDSGEbIU3bt6Xge+nyFBmLnL6HCmXHHMbBrBmHTEqCL/exL2LWbNFMG8TLv2a3SkQs1IkSeHZrnvh6JQ+vAKbc9NOJl4w5sboKwt24U2aiiMTB3hEw5Qt4jXMj+/udUS6Bf4kbOr2NJwabr2tRC/B96YSxtFzHs8fmOBCtbQ5x163ZdRc6InkT3k9AempF3w5gS3qNB2jjEyQG67jYUlcUSSaXxb6RDo5Lk9lzfaXUdSq42Cy/hqCfGkfd941U9elskIpGomsKlmVAOkuvuFa7719pxvtGQW1HBox1u5CbcRN/ZE65m7X6W2PEe9hPnzX2kpFsoOXL5Jro1CMQ1qyuyqbHu7EmvrMhKBNONRadRqUm7jkI8gCj2TJk5mjudyYqmqOjBxkL7zdjE5EZfCcoy7kcYVUEZ8VJycnSO4T7Ugo1+vwxP46M1eYJxt5E3hT34y99qbwzhTDB6+oP/FrZGrqzPC3hFRekwHwwFpSFRFatUL1z1wJ3uYaHocMltUzGuUKLUd3AMIyuy/6G3lMIm9dPRw/FZ4xSjsU9nllpJJ+gfRcU5iiNWxwJfD/7+6PnsH5ulWXDYp/f6t4JrxTY7LjeSL1CnAXg+fJGwOf8fgl8Ct6OdPG0FY0RiQvRJRINrZ+WhabJ+pCja7IIpiwNlBWmPqEXq2oNYQvmHdF3MctmkFk0DkJ1JGfCSNyuOiYplCR/Dwk8gT3VXwSZV5DPJ7/1T30uSZan7gCxNMEw2m9zNdVWFe8Nhsb1Tdk22BJWHicmQPIITQ6RT7TXISEuZpW7cM78CY6GSTOEZI5k6GApTAdbB7scdNi5NaaMosvpOumwFgo1AIvVP/OmiLKsnbFwST/E8elw2JpHb+JBxHLJBgkqDPpxx1EEGXM0v863Z4+sjemwLU9W5siuuxQV/3IUZNBku8M6aq2JHUSKVVDWtoqyI/RlG9ZDHVftEgcUQtKcbYiogVR8KSk2mpnSAdMqszTw5Kj0tTnY7qQ5Am1bP2eavjUGooWs2HAk8gQTst61VnRwXRWzihuAis2bqjJir+h1s/xa67rWx3T4XBhWzDOjL1waZH5ZFA82ZOvapVI6zxiUn4iA7ZOiKYtdJXI/SKy4OsYcyCBO4hJzqjv/A5bCD30gqmTqqSljWzH+sa7Nbq+AKYwE1M2KhUlA/xbu8niZThTWSuCyrDJ58Uh4sTfeRtvKO/QxAxdM9p3ucmeVw+Dp32rAPzkWiiHQcPjHlDK9xhxfLgODfpQKtgAEOVDscP2eh2HUSLd+C5IorxShrZrPrth1Bdph0WZ52wJ08l3bsnaOHc0HGfTKhwuv4jsePECxUiyaGaM0nxoMVpdj1eGZwWM9pHVDexqFfhFgJIn20Is9UazRrCC++g4Efc4C1c/WrP0PN/oZ8J8RGGNEw4zVs6GxJGR9DmupqcfBmbTj5qpCHGWCSPijL5YJiK1Oimcn1jYUFUSTGsOiIevUzM9QKI0WImQOIAVeyvX8w0Yonv+UoqCeX5AP25owuE7AbsGF8uh5IEug9L4X+UUy7oRyuPPCAAVbr+68HjXCY7jUEcQ/A88ccxB7Qs84j/A7D5tDzALzgACqt3+5v2tQtqSk/FeRRVHdXEKxvFYUIBHPJEmRADGGwi0GgQQ1CgIflI+GwdlGd47SGmwEWikmYCkqRFcMwJ2xHl+NoKKiZEFCBMGeTyBGPjZz0/9sGZIYRkQZvONNYAKADDYAclAIuvOExXAq5WSByuRjAQrDnOJJELnoIGATQbXOE4yhy47hhFsI+AAA46A4NPPJKAXa5xFNOTiT/4hDgoC4FtBR6CDNPuqcoY7jA3H4AbpajlMAIF5nA4bWUIGjlBwmSQj6aoAjl9QmaXA5sxNOIujkMIFhMiNRoYlR1LIMEDQwLCR/IzicCUPd4IgiuwQQHJY4EDyPmUQEiOxIRkwHGa9EkpplF5zRro4wEq1ReJbWLxdpmHWq1MNwi7YmZ9baQvEBqed+RkMyXpWyhVkqvjIwuTfLgpmmljcVhtWM0WFAvBFjmh0ZY5yNWdasOWhJtogmhmZJ2ZtFqHbRRaEuNpjcvpxPFjmzReKg/p+0iZKpLoaqdynQxGtqWTrZh6cIqFItnFgkzHywvuUlq9EKrvg3DNgQtk9LCUt4SUkuBusBon2i2gSWUi1CXjbXfHpE7dKG9i2kZcaFL/MQSjj0siYnDK2lwV4V8iv37vwcFEPzXQUC2wzJACkL4g3mn6k+UnIoaFl8ptI9lxNPD7xVT/D8HUZIVVdMN07IdSVZUTTdMK2SHHRSJxuKJZCo9lMnm8oViqVypurX6cOORkdHmltajj23dhsZgcXgCkUSmUGl0BpPF5nB5fIFQJJZIZXKFUqUO+6fV6Q1Gk9liTfR77YU3XvBd/pdUIMHOyoShBK2SomWLlHhrlSpH2mjjerUbxDA0ugkDygRPQkrJGqd6kVSjjgHNBS6krjewhS3EMJSwVULKmvIESVKVI220cb3arUQwNLoJQ5UJnoSUkjVOWthSjbob20ZyKRdSN7osOfBGp/zbeWfb5Sv2IZTse/ye/d7w+gv/rv08vPufXPP/uL/xRy/vzPXhAAAA) 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: #94a3b8;
--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: #e2e7ec;
--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: #64748b;
--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. Compact automatic sequences fit at
their authored text size or larger. Long sequences and waterfall rows
favor reading width and page scroll; other diagrams keep height fitting.
Small automatic canvases retain a modest scale inside a usable shell.
Every strategy preserves the authored SVG/viewBox and semantic geometry. */
.container {
width: 100%;
max-width: var(--archify-reader-width, 1440px);
margin: 0 auto;
}
html[data-reader-area="true"] .diagram-container {
min-height: var(--archify-diagram-min-height);
display: flex;
align-items: center;
}
html[data-reader-area="true"] .diagram-container > svg { flex-shrink: 0; }
/* The original SVG legend can use the outer corner while the capped or fitted
diagram stays centered. The container still clips all outer paint. */
.diagram-container:has([data-reader-legend-corner]) > svg { overflow: visible; }
.diagram-container [data-reader-legend-corner] {
transform-origin: 0 0;
transform-box: view-box;
transform: var(--archify-reader-legend-transform);
}
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-entry="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.95rem;
height: 0.95rem;
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.95rem;
height: 0.95rem;
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"] {
position: relative;
border-radius: 0;
background: color-mix(in srgb, var(--panel-border) 38%, 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="reset"]::after,
.diagram-nav [data-view="in"]::after {
position: absolute;
top: 0.55rem;
bottom: 0.55rem;
left: 0;
width: 1px;
background: color-mix(in srgb, var(--panel-border) 75%, transparent);
content: "";
pointer-events: none;
}
.diagram-nav [data-view="out"]:hover,
.diagram-nav [data-view="reset"]:hover,
.diagram-nav [data-view="in"]:hover { background: color-mix(in srgb, var(--panel-border) 60%, 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(--panel-border) 38%, 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; }
.diagram-guide-credit {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.6rem;
padding: 0.46rem 0.9rem 0.56rem;
border-top: 1px solid color-mix(in srgb, var(--toolbar-border) 74%, transparent);
color: var(--text-faint);
font-size: 0.5rem;
letter-spacing: 0.02em;
text-decoration: none;
}
.diagram-guide-credit-link {
display: inline-flex;
align-items: center;
gap: 0.28rem;
flex: none;
opacity: 0.72;
transition: opacity 120ms ease, color 120ms ease;
}
.diagram-guide-star {
width: 0.62rem;
height: 0.62rem;
background: currentColor;
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 1.6l1.9 3.9 4.3.6-3.1 3 .7 4.3L8 11.4l-3.8 2 .7-4.3-3.1-3 4.3-.6z' fill='none' stroke='black' stroke-width='1.3' 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 16 16'%3E%3Cpath d='M8 1.6l1.9 3.9 4.3.6-3.1 3 .7 4.3L8 11.4l-3.8 2 .7-4.3-3.1-3 4.3-.6z' fill='none' stroke='black' stroke-width='1.3' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
}
.diagram-guide-credit:hover .diagram-guide-star,
.diagram-guide-credit:focus-visible .diagram-guide-star {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 1.6l1.9 3.9 4.3.6-3.1 3 .7 4.3L8 11.4l-3.8 2 .7-4.3-3.1-3 4.3-.6z' stroke='black' stroke-width='1.3' 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 16 16'%3E%3Cpath d='M8 1.6l1.9 3.9 4.3.6-3.1 3 .7 4.3L8 11.4l-3.8 2 .7-4.3-3.1-3 4.3-.6z' stroke='black' stroke-width='1.3' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.diagram-guide-credit:hover .diagram-guide-credit-link,
.diagram-guide-credit:focus-visible .diagram-guide-credit-link { opacity: 1; color: var(--cloud-stroke); }
.diagram-guide-credit:focus-visible { outline: 2px solid var(--cloud-stroke); outline-offset: -2px; }
@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.35rem;
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) 14%, 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;
scrollbar-color: color-mix(in srgb, var(--panel-border) 90%, transparent) transparent;
}
/* Slim overlay-style thumb instead of the chunky platform default. */
.node-finder-results::-webkit-scrollbar { width: 10px; }
.node-finder-results::-webkit-scrollbar-track { background: transparent; }
.node-finder-results::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--panel-border) 85%, transparent);
border: 3px solid transparent;
border-radius: 999px;
background-clip: padding-box;
}
.node-finder-results::-webkit-scrollbar-thumb:hover {
background: color-mix(in srgb, var(--text-muted) 65%, transparent);
border: 3px solid transparent;
background-clip: padding-box;
}
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: 3rem;
padding: 0.52rem 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;
}
/* A zero count is absence, not information — let it recede. */
.node-finder-result em[data-zero] { color: var(--text-faint); }
.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;
/* Card notes are authored prose and may carry an unbreakable run such as a
slash-joined pipeline or a long identifier. Without this, that run sets the
card's min-content width, spills past its track in every rail placement,
and pushes the whole page into horizontal overflow at desktop widths.
`anywhere` rather than `break-word` because only `anywhere` shrinks the
intrinsic minimum the layout is sized from. */
overflow-wrap: anywhere;
}
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 {
display: grid;
gap: 0.5rem;
padding: 0;
overflow: visible;
-webkit-mask-image: none;
mask-image: none;
}
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) { padding-bottom: 0.25rem; }
html[data-reader-rail="bottom"] .node-outline-group > ul {
display: flex;
flex-wrap: wrap;
gap: 0.25rem 0.5rem;
}
html[data-reader-rail="bottom"] .node-outline-group > ul::before { display: none; }
html[data-reader-rail="bottom"] .node-outline-group li {
flex: 0 1 auto;
min-width: min(100%, 12rem);
max-width: min(100%, 32rem);
}
html[data-reader-rail="bottom"] .node-outline-heading {
justify-content: flex-start;
gap: 0.5rem;
}
html[data-reader-rail="bottom"] .node-outline-heading > span { overflow-wrap: anywhere; }
.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); }
/* The desktop bottom rail is an index: compact rows keep related nodes
together while allowing full names and descriptions to wrap naturally. */
html[data-reader-rail="bottom"] .node-outline-item {
grid-template-columns: 0.625rem minmax(0, 1fr) 0.75rem;
align-items: first baseline;
column-gap: 0.375rem;
min-height: 0;
padding-block: 0.375rem;
}
html[data-reader-rail="bottom"] .node-outline-item strong,
html[data-reader-rail="bottom"] .node-outline-item small {
overflow: visible;
text-overflow: clip;
white-space: normal;
overflow-wrap: anywhere;
}
html[data-reader-rail="bottom"] .node-outline-item strong {
grid-column: 2;
grid-row: 1;
}
html[data-reader-rail="bottom"] .node-outline-item small {
grid-column: 2;
grid-row: 2;
text-align: left;
}
html[data-reader-rail="bottom"] .node-outline-item::after {
grid-column: 3;
grid-row: 1;
align-self: center;
}
.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;
}
/* The motion button breathes on the relay cycle, so the chrome keeps time
with the wave it controls. Motion Governor mirrors the planned cycle from
the diagram SVG onto the document. */
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden]) #btn-motion .motion-control-dot {
animation: archify-motion-dot-breathe var(--flow-cycle-chrome, 6.4s) ease-in-out infinite;
}
@keyframes archify-motion-dot-breathe {
0%, 100% { opacity: 1; transform: scale(1); }
46% { opacity: 0.55; transform: scale(0.72); }
}
.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) {
--ambient-node-base-filter: drop-shadow(0 1px 1px rgba(17, 24, 39, 0.05)) drop-shadow(0 6px 12px rgba(17, 24, 39, 0.06));
filter: var(--ambient-node-base-filter);
}
html[data-preset="classic"][data-theme="dark"] .diagram-container > svg [data-node-id] > rect:not(.c-mask) {
--ambient-node-base-filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.35));
filter: var(--ambient-node-base-filter);
}
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;
}
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; }
/* Live plays one relay per cycle: a comet (halo, tail, head) leaves each
source when the signal reaches it, travels at one shared speed, leaves a
soft wake, and lands as a ripple while its target glows. Downstream edges
depart only after their source is reached, so the reader sees the path the
diagram describes rather than every edge shimmering at once. Sequence
messages relay in authored chronological order. Motion Governor plans the
timeline and its per-edge keyframes; this stylesheet owns appearance.
Original line language and markers remain static. Node entry is bounded to
one pass per document. */
svg[data-animation="trace"] {
--flow-cycle: 6.4s;
--flow-halo-peak: 0.17;
--flow-glow-peak: 0.5;
--flow-tail-peak: 0.42;
--flow-head-peak: 0.95;
--flow-wake-peak: 0.3;
--flow-ripple-peak: 0.65;
}
.ambient-edge-flow {
fill: none;
stroke: var(--arrow-emphasis);
stroke-linecap: round;
pointer-events: none;
display: none;
}
.ambient-flow-wake {
--flow-peak: var(--flow-wake-peak, 0.3);
stroke-width: 3.2;
stroke-dasharray: var(--flow-dasharray, none);
}
.ambient-flow-halo {
--flow-peak: var(--flow-halo-peak, 0.17);
stroke-width: 6;
stroke-dasharray: var(--flow-dash, 12px) calc(var(--flow-len, 0px) - var(--flow-dash, 12px));
}
/* A wide blurred streak centered on the head: this is the bloom that makes
the comet read as a traveling orb of light rather than a bright dash. */
.ambient-flow-glow {
--flow-peak: var(--flow-glow-peak, 0.5);
stroke-width: 9;
stroke-dasharray: var(--flow-dash, 12px) calc(var(--flow-len, 0px) - var(--flow-dash, 12px));
filter: blur(2.5px);
}
.ambient-flow-tail {
--flow-peak: var(--flow-tail-peak, 0.42);
stroke-width: 2.4;
stroke-dasharray: var(--flow-dasharray, var(--flow-dash, 20px) calc(var(--flow-len, 0px) - var(--flow-dash, 20px)));
}
.ambient-flow-head {
--flow-peak: var(--flow-head-peak, 0.95);
stroke: color-mix(in srgb, var(--arrow-emphasis) 40%, white);
stroke-width: 2.6;
stroke-dasharray: var(--flow-dash, 6px) calc(var(--flow-len, 0px) - var(--flow-dash, 6px));
}
.ambient-flow-ripple {
--flow-peak: var(--flow-ripple-peak, 0.65);
stroke: color-mix(in srgb, var(--arrow-emphasis) 55%, white);
stroke-width: 2;
}
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden]) .ambient-edge-flow {
display: inline;
animation-name: var(--flow-name, none);
animation-duration: var(--flow-cycle, 6.4s);
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-delay: var(--flow-delay, 0s);
/* Without backwards fill a delayed layer would sit at its natural opacity
(fully lit) until its departure arrives. */
animation-fill-mode: backwards;
}
/* Stream packets trail the comet head as smaller, dimmer beads. */
.ambient-flow-head.packet {
--flow-peak: calc(var(--flow-head-peak, 0.95) * 0.62);
stroke-width: 3.4;
}
/* Timeline and waterfall playhead: a crisp rule over a soft bloom. */
.ambient-flow-playhead {
--flow-peak: 0.9;
stroke: color-mix(in srgb, var(--arrow-emphasis) 55%, white);
stroke-width: 1.6;
}
.ambient-flow-playhead-glow {
--flow-peak: 0.32;
stroke-width: 10;
filter: blur(4px);
}
/* Waterfall spans fill in proportion to their measured time, then settle. */
.ambient-flow-fill {
--flow-peak: 0.5;
fill: color-mix(in srgb, var(--flow-fill, var(--arrow-emphasis)) 55%, white);
stroke: none;
transform-box: fill-box;
transform-origin: left center;
}
.ambient-flow-fill-failed { fill: var(--security-stroke); --flow-peak: 0.5; }
html[data-theme="light"] .ambient-flow-playhead { stroke: color-mix(in srgb, var(--arrow-emphasis) 70%, black); }
html[data-theme="light"] .ambient-flow-fill { --flow-peak: 0.42; fill: color-mix(in srgb, var(--flow-fill, var(--arrow-emphasis)) 70%, black); }
svg[data-preset="editorial"] .ambient-flow-fill { --flow-peak: 0.2; }
svg[data-preset="editorial"] .ambient-flow-playhead-glow { --flow-peak: 0.14; }
/* Each decorative flow follows its owning edge through tree visibility. */
.ambient-edge-flow[data-tree-hidden] { display: none !important; animation: none !important; }
svg[data-preset="blueprint"] .ambient-edge-flow { stroke-linecap: square; }
svg[data-preset="editorial"] { --flow-halo-peak: 0.07; --flow-glow-peak: 0.28; --flow-tail-peak: 0.2; --flow-head-peak: 0.7; --flow-wake-peak: 0.14; --flow-ripple-peak: 0.3; }
/* On light canvases the relay reads by saturation instead of bloom: the head
and ripple mix toward ink instead of white, and every layer's peak rises so
the wave survives the bright background. */
html[data-theme="light"] svg[data-animation="trace"] {
--flow-halo-peak: 0.22;
--flow-glow-peak: 0.4;
--flow-tail-peak: 0.5;
--flow-wake-peak: 0.34;
}
html[data-theme="light"] .ambient-flow-head {
stroke: color-mix(in srgb, var(--arrow-emphasis) 55%, black);
stroke-width: 2.8;
}
html[data-theme="light"] .ambient-flow-ripple {
stroke: color-mix(in srgb, var(--arrow-emphasis) 65%, black);
}
html[data-ambient-entry="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 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
html[data-ambient-entry="running"] svg[data-preset="signal-flow"][data-animation="trace"] [data-animate="node"] {
animation: archify-node-pulse 3.1s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
html[data-ambient-entry="running"] svg[data-preset="blueprint"][data-animation="trace"] [data-animate="node"] {
animation: archify-blueprint-node-pulse 3.8s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
html[data-ambient-entry="running"] svg[data-preset="editorial"][data-animation="trace"] [data-animate="node"] {
animation: archify-editorial-node-pulse 3.5s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
/* Each node glows in its planned windows: when the relay reaches it, while a
walk token rests on it, as a message lands, or as the playhead passes.
During the bounded entrance all three animations run; the entrance pulse is
listed after the repeating glow and wins on filter until it completes. */
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden]) svg[data-animation="trace"] [data-animate="node"] {
animation: var(--flow-node-name, none) var(--flow-cycle, 6.4s) linear infinite;
}
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden])[data-ambient-entry="running"] svg[data-animation="trace"] [data-animate="node"] {
animation: var(--flow-node-name, none) var(--flow-cycle, 6.4s) linear infinite,
archify-node-pulse 3.6s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden])[data-ambient-entry="running"] svg[data-preset="signal-flow"][data-animation="trace"] [data-animate="node"] {
animation: var(--flow-node-name, none) var(--flow-cycle, 6.4s) linear infinite,
archify-node-pulse 3.1s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden])[data-ambient-entry="running"] svg[data-preset="blueprint"][data-animation="trace"] [data-animate="node"] {
animation: var(--flow-node-name, none) var(--flow-cycle, 6.4s) linear infinite,
archify-blueprint-node-pulse 3.8s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
html[data-ambient-motion="running"][data-motion="live"]:not([data-motion-owner]):not([data-embed]):not([data-share-playback]):not([data-document-hidden])[data-ambient-entry="running"] svg[data-preset="editorial"][data-animation="trace"] [data-animate="node"] {
animation: var(--flow-node-name, none) var(--flow-cycle, 6.4s) linear infinite,
archify-editorial-node-pulse 3.5s ease-in-out 1 calc(var(--step, 0) * 160ms),
archify-node-enter 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards calc(var(--step, 0) * 160ms);
}
@keyframes archify-node-enter {
from { opacity: 0; }
to { opacity: 1; }
}
@media print {
.ambient-edge-flow { display: none !important; animation: none !important; }
svg[data-animation="trace"] [data-animate="node"] { animation: none !important; }
}
@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) {
.ambient-edge-flow { display: none !important; animation: none !important; }
html[data-preset="signal-flow"] .diagram-container::before {
animation: none !important;
}
.pulse-dot { animation: none !important; }
.motion-control-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; }
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>The sovereign firm: two banks, same AI model</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.credit":"Find Archify useful? A star on GitHub helps","viewer.guide.star":"Star","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.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":"ROUTE","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 1355 638" data-reader-fit="intrinsic-height" data-lifecycle-layout="main-path-v3" role="img" lang="en" aria-labelledby="archify-diagram-title archify-diagram-description" data-animation="trace" data-preset="classic" data-quality-profile="showcase" data-motion-choreography="walk">
<title id="archify-diagram-title">The sovereign firm: two banks, same AI model</title>
<desc id="archify-diagram-description">A lifecycle 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)" />
<!-- Composite phases -->
<!-- Transition paths -->
<g data-graph-role="automatic-crossover" style="--step:0">
<path data-graph-role="automatic-crossover-underlay" d="M 222 60 L 296 60" fill="none" stroke="var(--mask)" stroke-width="5.8" stroke-linecap="round" stroke-linejoin="round" pointer-events="none"/>
<path data-edge-from="rented" data-edge-to="stored" data-edge-label="answer" data-edge-key="0" data-edge-id="t1" data-composition-points="222,60;296,60" data-composition-crossover="halo" data-composition-independent="true" d="M 222 60 L 296 60" class="a-emphasis" data-animate="edge" style="--step:0" stroke-width="1.8" marker-end="url(#arrowhead-emphasis)"/>
</g>
<g data-graph-role="automatic-crossover" style="--step:1">
<path data-graph-role="automatic-crossover-underlay" d="M 470 60 L 562 60" fill="none" stroke="var(--mask)" stroke-width="5.8" stroke-linecap="round" stroke-linejoin="round" pointer-events="none"/>
<path data-edge-from="stored" data-edge-to="facts" data-edge-label="its basis" data-edge-key="1" data-edge-id="t2" data-composition-points="470,60;562,60" data-composition-crossover="halo" data-composition-independent="true" d="M 470 60 L 562 60" class="a-emphasis" data-animate="edge" style="--step:1" stroke-width="1.8" marker-end="url(#arrowhead-emphasis)"/>
</g>
<g data-graph-role="automatic-crossover" style="--step:2">
<path data-graph-role="automatic-crossover-underlay" d="M 736 60 L 822 60" fill="none" stroke="var(--mask)" stroke-width="5.8" stroke-linecap="round" stroke-linejoin="round" pointer-events="none"/>
<path data-edge-from="facts" data-edge-to="newmodel" data-edge-label="one year" data-edge-key="2" data-edge-id="t3" data-composition-points="736,60;822,60" data-composition-crossover="halo" data-composition-independent="true" d="M 736 60 L 822 60" class="a-emphasis" data-animate="edge" style="--step:2" stroke-width="1.8" marker-end="url(#arrowhead-emphasis)"/>
</g>
<g data-graph-role="automatic-crossover" style="--step:3">
<path data-graph-role="automatic-crossover-underlay" d="M 988 60 L 1104 60" fill="none" stroke="var(--mask)" stroke-width="5.8" stroke-linecap="round" stroke-linejoin="round" pointer-events="none"/>
<path data-edge-from="newmodel" data-edge-to="inherits" data-edge-label="switch freely" data-edge-key="3" data-edge-id="t4" data-composition-points="988,60;1104,60" data-composition-crossover="halo" data-composition-independent="true" d="M 988 60 L 1104 60" class="a-emphasis" data-animate="edge" style="--step:3" stroke-width="1.8" marker-end="url(#arrowhead-emphasis)"/>
</g>
<g data-graph-role="automatic-crossover" style="--step:4">
<path data-graph-role="automatic-crossover-underlay" d="M 318 92 L 318 184" fill="none" stroke="var(--mask)" stroke-width="5.2" stroke-linecap="round" stroke-linejoin="round" pointer-events="none"/>
<path data-edge-from="stored" data-edge-to="answeronly" data-edge-label="only the answer" data-edge-key="4" data-edge-id="b1" data-composition-points="318,92;318,184" data-composition-crossover="halo" data-composition-independent="true" d="M 318 92 L 318 184" class="a-default" data-animate="edge" style="--step:4" stroke-width="1.2" marker-end="url(#arrowhead)"/>
</g>
<g data-graph-role="automatic-crossover" style="--step:5">
<path data-graph-role="automatic-crossover-underlay" d="M 282.8 248 L 282.8 340" fill="none" stroke="var(--mask)" stroke-width="5.2" stroke-linecap="round" stroke-linejoin="round" pointer-events="none"/>
<path data-edge-from="answeronly" data-edge-to="newmodel2" data-edge-label="one year" data-edge-key="5" data-edge-id="b2" data-composition-points="282.8,248;282.8,340" data-composition-crossover="halo" data-composition-independent="true" d="M 282.8 248 L 282.8 340" class="a-default" data-animate="edge" style="--step:5" stroke-width="1.2" marker-end="url(#arrowhead)"/>
</g>
<g data-graph-role="automatic-crossover" style="--step:6">
<path data-graph-role="automatic-crossover-underlay" d="M 282.8 404 L 282.8 496" fill="none" stroke="var(--mask)" stroke-width="5.2" stroke-linecap="round" stroke-linejoin="round" pointer-events="none"/>
<path data-edge-from="newmodel2" data-edge-to="zero" data-edge-label="no inheritance" data-edge-key="6" data-edge-id="b3" data-composition-points="282.8,404;282.8,496" data-composition-crossover="halo" data-composition-independent="true" d="M 282.8 404 L 282.8 496" class="a-security" data-animate="edge" style="--step:6" stroke-width="1.2" marker-end="url(#arrowhead-security)"/>
</g>
<g data-graph-role="automatic-crossover" style="--step:7">
<path data-graph-role="automatic-crossover-underlay" d="M 448 92 L 448 184" fill="none" stroke="var(--mask)" stroke-width="5.2" stroke-linecap="round" stroke-linejoin="round" pointer-events="none"/>
<path data-edge-from="stored" data-edge-to="vendor" data-edge-label="at the vendor" data-edge-key="7" data-edge-id="c1" data-composition-points="448,92;448,184" data-composition-crossover="halo" data-composition-independent="true" d="M 448 92 L 448 184" class="a-default" data-animate="edge" style="--step:7" stroke-width="1.2" marker-end="url(#arrowhead)"/>
</g>
<g data-graph-role="automatic-crossover" style="--step:8">
<path data-graph-role="automatic-crossover-underlay" d="M 505.3 248 L 505.3 340" fill="none" stroke="var(--mask)" stroke-width="5.2" stroke-linecap="round" stroke-linejoin="round" pointer-events="none"/>
<path data-edge-from="vendor" data-edge-to="loss" data-edge-label="wants to switch" data-edge-key="8" data-edge-id="c2" data-composition-points="505.3,248;505.3,340" data-composition-crossover="halo" data-composition-independent="true" d="M 505.3 248 L 505.3 340" class="a-security" data-animate="edge" style="--step:8" stroke-width="1.2" marker-end="url(#arrowhead-security)"/>
</g>
<!-- States -->
<g id="node-rented" data-node-id="rented" data-node-label="Same model rented" tabindex="0" role="button" aria-label="Focus Same model rented, two banks, same prompt, Lifecycle state" aria-pressed="false" data-node-kind="start" data-node-sublabel="two banks, same prompt" data-node-context="Lifecycle state">
<title>Same model rented · two banks, same prompt · Lifecycle state</title>
<rect x="56" y="28" width="166" height="64" rx="8" class="c-mask"/>
<rect x="56" y="28" width="166" height="64" rx="8" class="c-frontend" data-animate="node" style="--step:0" stroke-width="1.5"/>
<g aria-hidden="true" data-lifecycle-initial-marker=""><circle cx="30" cy="60" r="4.5" style="fill: var(--arrow-emphasis)"/><path d="M 34.5 60 L 49 60" class="a-emphasis" stroke-width="1.4"/><path d="M 48 56.5 L 55 60 L 48 63.5 Z" style="fill: var(--arrow-emphasis)"/></g>
<text data-detail="fine" x="79" y="42" class="t-frontend" font-size="8" font-weight="700">01</text>
<text data-node-label="" data-detail-anchor="" x="152.8" y="55" class="t-primary" font-size="12" font-weight="600" text-anchor="middle">Same model rented</text>
<text data-detail="context" x="139" y="73" class="t-muted" font-size="9" text-anchor="middle">two banks, same prompt</text>
</g>
<g id="node-stored" data-node-id="stored" data-node-label="What is stored?" tabindex="0" role="button" aria-label="Focus What is stored?, the answer, or its basis, Lifecycle state" aria-pressed="false" data-node-kind="decision" data-node-sublabel="the answer, or its basis" data-node-context="Lifecycle state">
<title>What is stored? · the answer, or its basis · Lifecycle state</title>
<rect x="296" y="28" width="174" height="64" rx="8" class="c-mask"/>
<rect x="296" y="28" width="174" height="64" rx="8" class="c-database" data-animate="node" style="--step:1" stroke-width="1.5"/>
<g aria-hidden="true" data-semantic-sigil="decision" class="semantic-sigil s-database" transform="translate(302 34) scale(0.6875)">
<path d="M8 2.2 13.8 8 8 13.8 2.2 8Z"/>
</g>
<text data-detail="fine" x="319" y="42" class="t-database" font-size="8" font-weight="700">02</text>
<text data-node-label="" data-detail-anchor="" x="385.6" y="55" class="t-primary" font-size="12" font-weight="600" text-anchor="middle">What is stored?</text>
<text data-detail="context" x="383" y="73" class="t-muted" font-size="9" text-anchor="middle">the answer, or its basis</text>
</g>
<g id="node-facts" data-node-id="facts" data-node-label="Facts and outcomes" tabindex="0" role="button" aria-label="Focus Facts and outcomes, stored and adjusted, Lifecycle state" aria-pressed="false" data-node-kind="active" data-node-sublabel="stored and adjusted" data-node-tag="sovereign" data-node-context="Lifecycle state">
<title>Facts and outcomes · stored and adjusted · Lifecycle state · sovereign</title>
<rect x="562" y="28" width="174" height="64" rx="8" class="c-mask"/>
<rect x="562" y="28" width="174" height="64" rx="8" class="c-frontend" data-animate="node" style="--step:2" stroke-width="1.5"/>
<text data-detail="fine" x="585" y="42" class="t-frontend" font-size="8" font-weight="700">03</text>
<text data-node-label="" data-detail-anchor="" x="662.4" y="51" class="t-primary" font-size="12" font-weight="600" text-anchor="middle">Facts and outcomes</text>
<text data-detail="context" x="649" y="68" class="t-muted" font-size="9" text-anchor="middle">stored and adjusted</text>
<text data-detail="fine" x="649" y="83" class="t-frontend" font-size="8" text-anchor="middle">sovereign</text>
</g>
<g id="node-newmodel" data-node-id="newmodel" data-node-label="New model arrives" tabindex="0" role="button" aria-label="Focus New model arrives, after one year, Lifecycle state" aria-pressed="false" data-node-kind="waiting" data-node-sublabel="after one year" data-node-context="Lifecycle state">
<title>New model arrives · after one year · Lifecycle state</title>
<rect x="822" y="28" width="166" height="64" rx="8" class="c-mask"/>
<rect x="822" y="28" width="166" height="64" rx="8" class="c-cloud" data-animate="node" style="--step:3" stroke-width="1.5"/>
<g aria-hidden="true" data-semantic-sigil="waiting" class="semantic-sigil s-cloud" transform="translate(828 34) scale(0.6875)">
<path d="M4 2.5h8M4 13.5h8M5 3c0 2.8 2 3.2 3 5-1 1.8-3 2.2-3 5M11 3c0 2.8-2 3.2-3 5 1 1.8 3 2.2 3 5"/>
</g>
<text data-detail="fine" x="845" y="42" class="t-cloud" font-size="8" font-weight="700">04</text>
<text data-node-label="" data-detail-anchor="" x="918.8" y="55" class="t-primary" font-size="12" font-weight="600" text-anchor="middle">New model arrives</text>
<text data-detail="context" x="905" y="73" class="t-muted" font-size="9" text-anchor="middle">after one year</text>
</g>
<g id="node-inherits" data-node-id="inherits" data-node-label="Inherits the learning" tabindex="0" role="button" aria-label="Focus Inherits the learning, rules and cases, Lifecycle state" aria-pressed="false" data-node-kind="success" data-node-sublabel="rules and cases" data-node-tag="lasting advantage" data-node-context="Lifecycle state">
<title>Inherits the learning · rules and cases · Lifecycle state · lasting advantage</title>
<rect x="1104" y="28" width="195" height="64" rx="8" class="c-mask"/>
<rect x="1104" y="28" width="195" height="64" rx="8" class="c-backend" data-animate="node" style="--step:4" stroke-width="1.5"/>
<rect x="1107" y="31" width="189" height="58" rx="5" class="c-backend" style="fill: none" stroke-width="1"/>
<g aria-hidden="true" data-semantic-sigil="success" class="semantic-sigil s-backend" transform="translate(1110 34) scale(0.6875)">
<circle cx="8" cy="8" r="5.3"/>
<path d="m5.2 8 1.8 1.8 3.8-4"/>
</g>
<text data-detail="fine" x="1127" y="42" class="t-backend" font-size="8" font-weight="700">05</text>
<text data-node-label="" data-detail-anchor="" x="1215.2" y="51" class="t-primary" font-size="12" font-weight="600" text-anchor="middle">Inherits the learning</text>
<text data-detail="context" x="1201.5" y="68" class="t-muted" font-size="9" text-anchor="middle">rules and cases</text>
<text data-detail="fine" x="1201.5" y="83" class="t-backend" font-size="8" text-anchor="middle">lasting advantage</text>
</g>
<g id="node-answeronly" data-node-id="answeronly" data-node-label="Only the answer stored" tabindex="0" role="button" aria-label="Focus Only the answer stored, question in, answer out, Lifecycle state" aria-pressed="false" data-node-kind="active" data-node-sublabel="question in, answer out" data-node-tag="tenant" data-node-context="Lifecycle state">
<title>Only the answer stored · question in, answer out · Lifecycle state · tenant</title>
<rect x="189.75" y="184" width="186" height="64" rx="8" class="c-mask"/>
<rect x="189.75" y="184" width="186" height="64" rx="8" class="c-frontend" data-animate="node" style="--step:5" stroke-width="1.5"/>
<text data-node-label="" data-detail-anchor="" x="287.95" y="207" class="t-primary" font-size="12" font-weight="600" text-anchor="middle">Only the answer stored</text>
<text data-detail="context" x="282.75" y="224" class="t-muted" font-size="9" text-anchor="middle">question in, answer out</text>
<text data-detail="fine" x="282.75" y="239" class="t-frontend" font-size="8" text-anchor="middle">tenant</text>
</g>
<g id="node-newmodel2" data-node-id="newmodel2" data-node-label="New model arrives" tabindex="0" role="button" aria-label="Focus New model arrives, after one year, Lifecycle state" aria-pressed="false" data-node-kind="waiting" data-node-sublabel="after one year" data-node-context="Lifecycle state">
<title>New model arrives · after one year · Lifecycle state</title>
<rect x="207.75" y="340" width="150" height="64" rx="8" class="c-mask"/>
<rect x="207.75" y="340" width="150" height="64" rx="8" class="c-cloud" data-animate="node" style="--step:6" stroke-width="1.5"/>
<g aria-hidden="true" data-semantic-sigil="waiting" class="semantic-sigil s-cloud" transform="translate(213.75 346) scale(0.6875)">
<path d="M4 2.5h8M4 13.5h8M5 3c0 2.8 2 3.2 3 5-1 1.8-3 2.2-3 5M11 3c0 2.8-2 3.2-3 5 1 1.8 3 2.2 3 5"/>
</g>
<text data-node-label="" data-detail-anchor="" x="287.95" y="367" class="t-primary" font-size="12" font-weight="600" text-anchor="middle">New model arrives</text>
<text data-detail="context" x="282.75" y="385" class="t-muted" font-size="9" text-anchor="middle">after one year</text>
</g>
<g id="node-zero" data-node-id="zero" data-node-label="Starts from zero" tabindex="0" role="button" aria-label="Focus Starts from zero, same prompt as everyone, Lifecycle state" aria-pressed="false" data-node-kind="failure" data-node-sublabel="same prompt as everyone" data-node-tag="lost" data-node-context="Lifecycle state">
<title>Starts from zero · same prompt as everyone · Lifecycle state · lost</title>
<rect x="198.25" y="496" width="169" height="64" rx="8" class="c-mask"/>
<rect x="198.25" y="496" width="169" height="64" rx="8" class="c-security" data-animate="node" style="--step:7" stroke-width="1.5"/>
<rect x="201.25" y="499" width="163" height="58" rx="5" class="c-security" style="fill: none" stroke-width="1"/>
<g aria-hidden="true" data-semantic-sigil="failure" class="semantic-sigil s-security" transform="translate(204.25 502) scale(0.6875)">
<circle cx="8" cy="8" r="5.3"/>
<path d="m5.7 5.7 4.6 4.6m0-4.6-4.6 4.6"/>
</g>
<text data-node-label="" data-detail-anchor="" x="282.75" y="519" class="t-primary" font-size="12" font-weight="600" text-anchor="middle">Starts from zero</text>
<text data-detail="context" x="282.75" y="536" class="t-muted" font-size="9" text-anchor="middle">same prompt as everyone</text>
<text data-detail="fine" x="282.75" y="551" class="t-security" font-size="8" text-anchor="middle">lost</text>
</g>
<g id="node-vendor" data-node-id="vendor" data-node-label="Stored at the vendor" tabindex="0" role="button" aria-label="Focus Stored at the vendor, proprietary format, Lifecycle state" aria-pressed="false" data-node-kind="active" data-node-sublabel="proprietary format" data-node-tag="locked in" data-node-context="Lifecycle state">
<title>Stored at the vendor · proprietary format · Lifecycle state · locked in</title>
<rect x="419.75" y="184" width="171" height="64" rx="8" class="c-mask"/>
<rect x="419.75" y="184" width="171" height="64" rx="8" class="c-frontend" data-animate="node" style="--step:8" stroke-width="1.5"/>
<text data-node-label="" data-detail-anchor="" x="510.75" y="207" class="t-primary" font-size="12" font-weight="600" text-anchor="middle">Stored at the vendor</text>
<text data-detail="context" x="505.25" y="224" class="t-muted" font-size="9" text-anchor="middle">proprietary format</text>
<text data-detail="fine" x="505.25" y="239" class="t-frontend" font-size="8" text-anchor="middle">locked in</text>
</g>
<g id="node-loss" data-node-id="loss" data-node-label="Switching means loss" tabindex="0" role="button" aria-label="Focus Switching means loss, the memory stays behind, Lifecycle state" aria-pressed="false" data-node-kind="failure" data-node-sublabel="the memory stays behind" data-node-tag="lost" data-node-context="Lifecycle state">
<title>Switching means loss · the memory stays behind · Lifecycle state · lost</title>
<rect x="419.75" y="340" width="171" height="64" rx="8" class="c-mask"/>
<rect x="419.75" y="340" width="171" height="64" rx="8" class="c-security" data-animate="node" style="--step:9" stroke-width="1.5"/>
<rect x="422.75" y="343" width="165" height="58" rx="5" class="c-security" style="fill: none" stroke-width="1"/>
<g aria-hidden="true" data-semantic-sigil="failure" class="semantic-sigil s-security" transform="translate(425.75 346) scale(0.6875)">
<circle cx="8" cy="8" r="5.3"/>
<path d="m5.7 5.7 4.6 4.6m0-4.6-4.6 4.6"/>
</g>
<text data-node-label="" data-detail-anchor="" x="510.75" y="363" class="t-primary" font-size="12" font-weight="600" text-anchor="middle">Switching means loss</text>
<text data-detail="context" x="505.25" y="380" class="t-muted" font-size="9" text-anchor="middle">the memory stays behind</text>
<text data-detail="fine" x="505.25" y="395" class="t-security" font-size="8" text-anchor="middle">lost</text>
</g>
<!-- Transition labels -->
<g data-detail="context" data-edge-from="rented" data-edge-to="stored" data-edge-label="answer" data-edge-key="0" data-edge-id="t1">
<rect x="235" y="38" width="48" height="16" rx="4" class="c-mask"/>
<text x="259" y="49.5" class="t-edge-emphasis" font-size="10" text-anchor="middle">answer</text>
</g>
<g data-detail="context" data-edge-from="stored" data-edge-to="facts" data-edge-label="its basis" data-edge-key="1" data-edge-id="t2">
<rect x="483" y="38" width="66" height="16" rx="4" class="c-mask"/>
<text x="516" y="49.5" class="t-edge-emphasis" font-size="10" text-anchor="middle">its basis</text>
</g>
<g data-detail="context" data-edge-from="facts" data-edge-to="newmodel" data-edge-label="one year" data-edge-key="2" data-edge-id="t3">
<rect x="749" y="38" width="60" height="16" rx="4" class="c-mask"/>
<text x="779" y="49.5" class="t-edge-emphasis" font-size="10" text-anchor="middle">one year</text>
</g>
<g data-detail="context" data-edge-from="newmodel" data-edge-to="inherits" data-edge-label="switch freely" data-edge-key="3" data-edge-id="t4">
<rect x="1001" y="38" width="90" height="16" rx="4" class="c-mask"/>
<text x="1046" y="49.5" class="t-edge-emphasis" font-size="10" text-anchor="middle">switch freely</text>
</g>
<g data-detail="context" data-edge-from="stored" data-edge-to="answeronly" data-edge-label="only the answer" data-edge-key="4" data-edge-id="b1">
<rect x="324" y="130" width="102" height="16" rx="4" class="c-mask"/>
<text x="375" y="141.5" class="t-muted" font-size="10" text-anchor="middle">only the answer</text>
</g>
<g data-detail="context" data-edge-from="answeronly" data-edge-to="newmodel2" data-edge-label="one year" data-edge-key="5" data-edge-id="b2">
<rect x="288.8" y="286" width="60" height="16" rx="4" class="c-mask"/>
<text x="318.8" y="297.5" class="t-muted" font-size="10" text-anchor="middle">one year</text>
</g>
<g data-detail="context" data-edge-from="newmodel2" data-edge-to="zero" data-edge-label="no inheritance" data-edge-key="6" data-edge-id="b3">
<rect x="288.8" y="442" width="96" height="16" rx="4" class="c-mask"/>
<text x="336.8" y="453.5" class="t-edge-security" font-size="10" text-anchor="middle">no inheritance</text>
</g>
<g data-detail="context" data-edge-from="stored" data-edge-to="vendor" data-edge-label="at the vendor" data-edge-key="7" data-edge-id="c1">
<rect x="454" y="130" width="90" height="16" rx="4" class="c-mask"/>
<text x="499" y="141.5" class="t-muted" font-size="10" text-anchor="middle">at the vendor</text>
</g>
<g data-detail="context" data-edge-from="vendor" data-edge-to="loss" data-edge-label="wants to switch" data-edge-key="8" data-edge-id="c2">
<rect x="511.29999999999995" y="286" width="102" height="16" rx="4" class="c-mask"/>
<text x="562.3" y="297.5" class="t-edge-security" font-size="10" text-anchor="middle">wants to switch</text>
</g>
<!-- Legend -->
<g data-legend="" data-legend-bridge="">
<text x="40" y="586" class="t-primary" font-size="12" font-weight="650">Legend</text>
<g data-legend-semantic-kind="start" data-legend-kind="start" data-legend-label="Starting point" data-legend-x="40" data-legend-baseline="606" data-legend-width="142">
<circle cx="44.5" cy="602.5" r="4.5" style="fill: var(--arrow-emphasis)"/><path d="M 49 602.5 L 58 602.5" class="a-emphasis" stroke-width="1.4"/><path d="M 57 599 L 64 602.5 L 57 606 Z" style="fill: var(--arrow-emphasis)"/>
<text x="74" y="606" class="t-muted" font-size="10" font-weight="500">Starting point</text>
</g>
<g data-legend-semantic-kind="active" data-legend-kind="active" data-legend-label="Ongoing practice" data-legend-x="204" data-legend-baseline="606" data-legend-width="143">
<rect x="204" y="598" width="14" height="9" rx="2" class="c-frontend" stroke-width="1"/>
<text x="226" y="606" class="t-muted" font-size="10" font-weight="500">Ongoing practice</text>
</g>
<g data-legend-semantic-kind="waiting" data-legend-kind="waiting" data-legend-label="External event" data-legend-x="369" data-legend-baseline="606" data-legend-width="130">
<rect x="369" y="598" width="14" height="9" rx="2" class="c-cloud" stroke-width="1"/>
<text x="391" y="606" class="t-muted" font-size="10" font-weight="500">External event</text>
</g>
<g data-legend-semantic-kind="decision" data-legend-kind="decision" data-legend-label="Choice" data-legend-x="521" data-legend-baseline="606" data-legend-width="81">
<rect x="521" y="598" width="14" height="9" rx="2" class="c-database" stroke-width="1"/>
<text x="543" y="606" class="t-muted" font-size="10" font-weight="500">Choice</text>
</g>
<g data-legend-semantic-kind="success" data-legend-kind="success" data-legend-label="Lasting advantage" data-legend-x="624" data-legend-baseline="606" data-legend-width="149">
<rect x="624" y="598" width="14" height="9" rx="2" class="c-backend" stroke-width="1"/>
<text x="646" y="606" class="t-muted" font-size="10" font-weight="500">Lasting advantage</text>
</g>
<g data-legend-semantic-kind="failure" data-legend-kind="failure" data-legend-label="Lost learning" data-legend-x="795" data-legend-baseline="606" data-legend-width="124">
<rect x="795" y="598" width="14" height="9" rx="2" class="c-security" stroke-width="1"/>
<text x="817" y="606" class="t-muted" font-size="10" font-weight="500">Lost learning</text>
</g>
<g data-legend-semantic-kind="final" data-legend-x="941" data-legend-baseline="606" data-legend-width="80">
<rect x="941" y="598" width="14" height="9" rx="2" class="c-external" stroke-width="1"/><rect x="943.5" y="600.5" width="9" height="4" rx="1" class="c-external" style="fill: none" stroke-width="0.8"/>
<text x="965" y="606" class="t-muted" font-size="10" font-weight="500">End state</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>
<a class="diagram-guide-credit" href="https://github.com/tt-a1i/archify" target="_blank" rel="noopener noreferrer"><span>Find Archify useful? A star on GitHub helps</span><span class="diagram-guide-credit-link"><span class="diagram-guide-star" aria-hidden="true"></span>Star <span aria-hidden="true">&#8599;</span></span></a>
</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)">ROUTE</button>
<button id="btn-overview-map" type="button" aria-label="Open semantic radar" aria-expanded="false" aria-controls="overview-map" title="Semantic radar (M)">MAP</button>
<button id="btn-semantic-lens" type="button" aria-label="Open semantic lens" aria-haspopup="dialog" aria-expanded="false" aria-pressed="false" aria-controls="semantic-lens" title="Semantic lens (L)">LENS</button>
<button id="btn-node-finder" type="button" aria-label="Find a node" aria-haspopup="dialog" aria-expanded="false" aria-controls="node-finder" title="Find a node (/)" data-node-finder-trigger><span class="diagram-nav-icon find" aria-hidden="true"></span></button>
<button id="btn-diagram-guide" type="button" aria-label="Open diagram guide" aria-haspopup="dialog" aria-expanded="false" aria-controls="diagram-guide" title="Diagram guide (?)"><span class="diagram-nav-icon guide" aria-hidden="true"></span></button>
<button data-view="out" type="button" aria-label="Zoom out" title="Zoom out (-)"><span class="diagram-nav-icon minus" aria-hidden="true"></span></button>
<button data-view="reset" type="button" aria-label="Reset diagram view" title="Reset view (0)"><span class="diagram-nav-detail" data-view-detail hidden>READ</span><span class="diagram-nav-percent" data-view-percent>100%</span></button>
<button data-view="in" type="button" aria-label="Zoom in" title="Zoom in (+)"><span class="diagram-nav-icon plus" aria-hidden="true"></span></button>
</div>
</div>
<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>
<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 readerLink(state) {
var template = document.documentElement.getAttribute('data-reading-link-template');
if (template === null) return location.href.replace(/#.*$/, '') + '#' + state;
if (!template || template.length > 4096 || template.split('{state}').length !== 2) return null;
try {
var url = new URL(template.replace('{state}', encodeURIComponent(state)));
if (['http:', 'https:', 'file:'].indexOf(url.protocol) === -1 || url.username || url.password) return null;
return url.href;
} catch (_) { return null; }
}
function copyReaderText(value) {
function fallbackCopy() {
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;
}
var policy = document.permissionsPolicy || document.featurePolicy;
var allowed = !policy || typeof policy.allowsFeature !== 'function' || policy.allowsFeature('clipboard-write');
if (allowed && navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
try { return Promise.resolve(navigator.clipboard.writeText(value)).then(function () { return true; }).catch(fallbackCopy); }
catch (_) {}
}
return Promise.resolve(fallbackCopy());
}
function hasDrawableGeometry(element) {
if (!element || element.hasAttribute('data-ambient-flow-overlay')) 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) {
if (geometry.hasAttribute('data-ambient-flow-overlay')) return false;
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.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return;
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; }
if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) 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) {
Array.prototype.forEach.call(clone.querySelectorAll('[data-ambient-flow-overlay]'), function (el) {
el.remove();
});
// Route interaction rewrites CSS declarations through CSSOM. Normalize
// authored motion delays on the clone so exported bytes do not depend
// on whether the reader traced a route before downloading.
Array.prototype.forEach.call(clone.querySelectorAll('[data-animate]'), function (el) {
var step = el.style.getPropertyValue('--step');
if (step) el.setAttribute('style', el.style.cssText);
});
// 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-reader-legend-corner]'), function (el) {
el.removeAttribute('data-reader-legend-corner');
el.style.removeProperty('--archify-reader-legend-transform');
});
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) 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');
});
// A downloaded tree is always the complete hierarchy: collapsing is a
// reading aid, never a silent change to what the artifact contains.
clone.removeAttribute('data-tree-any-collapsed');
Array.prototype.forEach.call(clone.querySelectorAll('[data-tree-hidden], [data-tree-collapsed]'), function (el) {
el.removeAttribute('data-tree-hidden');
el.removeAttribute('data-tree-collapsed');
});
Array.prototype.forEach.call(clone.querySelectorAll('[data-tree-toggle]'), function (el) {
el.setAttribute('aria-expanded', 'true');
el.setAttribute('aria-label', el.getAttribute('data-tree-label-collapse') || '');
});
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.hasAttribute('data-tree-any-collapsed') &&
!clone.style.getPropertyValue('transform') &&
!clone.style.getPropertyValue('clip-path') &&
clone.querySelectorAll('[data-ambient-flow-overlay], [data-reader-legend-corner], [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], [data-tree-hidden], [data-tree-collapsed]').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;
// Keep the export-owned activation inside its existing control boundary.
// The synthetic download click must not dismiss the reader selection.
menu.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);
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 geometry = element;
if (element.getAttribute('data-motion-path')) {
geometry = element.cloneNode(false);
geometry.setAttribute('d', element.getAttribute('data-motion-path'));
}
var length;
try { length = geometry.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, geometry.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 sourceUrl = null;
var backgroundImage = null;
var stream = null;
var recorder = null;
var raf = 0;
var durationTimer = 0;
var flushTimer = 0;
var settled = false;
function finish(error, blob) {
if (settled) return;
// 先锁定终态:停止录制和释放流可能产生晚到的编码事件。
settled = true;
cancelAnimationFrame(raf);
clearTimeout(durationTimer);
clearTimeout(flushTimer);
if (backgroundImage) {
backgroundImage.onload = null;
backgroundImage.onerror = null;
}
if (recorder) {
recorder.ondataavailable = null;
recorder.onerror = null;
recorder.onstop = null;
try { if (recorder.state !== 'inactive') recorder.stop(); } catch (_) {}
}
if (stream) stream.getTracks().forEach(function (track) { track.stop(); });
if (sourceUrl) URL.revokeObjectURL(sourceUrl);
if (blob) resolve(blob);
else reject(error);
}
try {
sourceUrl = URL.createObjectURL(new Blob([data.svgString], { type: 'image/svg+xml;charset=utf-8' }));
backgroundImage = new Image();
backgroundImage.onload = function () {
if (settled) return;
try {
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');
if (!ctx) throw exportError('viewer.export.error.contextUnavailable', { label: 'WebM' });
stream = canvas.captureStream(fps);
var mime = motionMimeType();
recorder = new MediaRecorder(stream, { mimeType: mime, videoBitsPerSecond: 6000000 });
var chunks = [];
var startedAt = performance.now();
function draw(now) {
if (settled) return;
try {
var elapsed = Math.max(0, ((Number(now) || performance.now()) - startedAt) / 1000);
drawMotionFrame(ctx, backgroundImage, motionScene, elapsed);
raf = requestAnimationFrame(draw);
} catch (error) { finish(error); }
}
recorder.ondataavailable = function (event) {
if (!settled && event.data && event.data.size) chunks.push(event.data);
};
recorder.onerror = function (event) {
finish(event.error || exportError('viewer.export.error.mediaRecorder'));
};
recorder.onstop = function () {
if (settled) return;
try {
var blob = new Blob(chunks, { type: recorder.mimeType || mime });
if (!blob.size) throw exportError('viewer.export.error.emptyWebm');
finish(null, blob);
} catch (error) { finish(error); }
};
drawMotionFrame(ctx, backgroundImage, motionScene, 0);
recorder.start(250);
if (settled) return;
startedAt = performance.now();
raf = requestAnimationFrame(draw);
durationTimer = setTimeout(function () {
if (settled || recorder.state === 'inactive') return;
// 保留最佳努力的编码器 flush;失败后仍通过 stop 收集最后的片段。
try { recorder.requestData(); } catch (_) {}
if (settled) return;
flushTimer = setTimeout(function () {
if (settled) return;
try { if (recorder.state !== 'inactive') recorder.stop(); }
catch (error) { finish(error); }
}, 120);
}, duration);
} catch (error) { finish(error); }
};
backgroundImage.onerror = function () {
finish(exportError('viewer.export.error.webmBackground'));
};
backgroundImage.src = sourceUrl;
} catch (error) { finish(error); }
});
}
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.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return;
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; }
if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) 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 entryStarted = false;
var entryPending = new Set();
var flowCount = 0;
// Runtime decoration carries geometry and tree visibility only. Keep each
// flow beside its authored edge so transforms, clipping and crossover
// paint order apply.
// Each renderer may declare a choreography that matches what its diagram
// means. Relay (the default) sends a comet from each source when the
// signal reaches it; stream adds a packet train for continuous data; walk
// moves one token along the main state path; sequence plays messages in
// authored order; sweep runs a playhead along a timeline; replay fills
// waterfall spans in proportion to their measured time. Motion Governor
// plans the timeline and per-element keyframes; viewer.css owns looks.
var SVG_NS = 'http://www.w3.org/2000/svg';
var choreography = svg ? (svg.getAttribute('data-motion-choreography') || 'relay') : 'relay';
var RELAY = {
speed: 280,
minTravel: 0.42,
maxTravel: 1.5,
handoff: 0.1,
dwell: 0.55,
maxSpan: 9,
land: 0.9,
rest: 1.4,
minCycle: 3.6
};
function stepOf(element, fallback) {
var step = parseFloat(element && element.style ? element.style.getPropertyValue('--step') : '');
return Number.isFinite(step) ? step : fallback;
}
function byStep(a, b) { return a.step - b.step || a.index - b.index; }
function addWindow(windows, id, start, end, strong) {
if (!id) return;
(windows[id] = windows[id] || []).push({ start: start, end: end, strong: !!strong });
}
function planRelay(records, windows) {
var arrive = Object.create(null);
var seeded = Object.create(null);
var targets = Object.create(null);
records.forEach(function (record) { if (record.from !== record.to) targets[record.to] = true; });
function seed(id) {
if (arrive[id] !== undefined) return;
arrive[id] = 0;
seeded[id] = true;
}
function relax() {
// Earliest arrival over a few dozen edges; a scan per settle keeps
// cycles and fan-in deterministic without a priority queue.
var settled = Object.create(null);
for (;;) {
var next = null;
Object.keys(arrive).forEach(function (id) {
if (!settled[id] && (next === null || arrive[id] < arrive[next])) next = id;
});
if (next === null) return;
settled[next] = true;
var depart = arrive[next] + (seeded[next] ? 0 : RELAY.handoff);
records.forEach(function (record) {
if (record.from !== next) return;
var at = depart + record.travel;
if (arrive[record.to] === undefined || at < arrive[record.to]) arrive[record.to] = at;
});
}
}
records.forEach(function (record) { if (!targets[record.from]) seed(record.from); });
relax();
for (;;) {
// A closed cycle has no entry; start it at its earliest authored edge.
var orphan = records.filter(function (record) { return arrive[record.from] === undefined; }).sort(byStep)[0];
if (!orphan) break;
seed(orphan.from);
relax();
}
records.forEach(function (record) {
record.depart = arrive[record.from] + (seeded[record.from] ? 0 : RELAY.handoff);
record.active = true;
});
Object.keys(arrive).forEach(function (id) { addWindow(windows, id, arrive[id], arrive[id]); });
}
function planWalk(records, windows) {
// A state machine is in one state at a time: follow the first authored
// transition to an unvisited state and let the current state stay lit.
var targets = Object.create(null);
records.forEach(function (record) { if (record.from !== record.to) targets[record.to] = true; });
var ordered = records.slice().sort(byStep);
var start = (ordered.find(function (record) { return !targets[record.from]; }) || ordered[0]).from;
var visited = Object.create(null);
var current = start;
var clock = 0;
var enteredAt = 0;
visited[start] = true;
for (var hops = 0; hops < 16; hops += 1) {
var next = ordered.find(function (record) { return record.from === current && !visited[record.to]; });
if (!next) break;
next.active = true;
next.depart = clock + RELAY.dwell;
addWindow(windows, current, enteredAt, next.depart, true);
clock = next.depart + next.travel;
enteredAt = clock;
current = next.to;
visited[current] = true;
}
addWindow(windows, current, enteredAt, enteredAt + 1.2, true);
}
function planSequence(records, windows) {
var clock = 0;
records.slice().sort(byStep).forEach(function (record) {
record.active = true;
record.depart = clock;
clock += record.travel + RELAY.handoff;
addWindow(windows, record.from, record.depart, record.depart);
addWindow(windows, record.to, record.depart + record.travel, record.depart + record.travel);
});
}
function scalePlan(records, windows, span) {
var scale = span > RELAY.maxSpan ? RELAY.maxSpan / span : 1;
if (scale === 1) return span;
records.forEach(function (record) {
if (!record.active) return;
record.depart *= scale;
record.travel *= scale;
});
Object.keys(windows).forEach(function (id) {
windows[id].forEach(function (window) { window.start *= scale; window.end *= scale; });
});
return span * scale;
}
function nodeShapes() {
var shapes = Object.create(null);
Array.prototype.forEach.call(svg.querySelectorAll('[data-animate="node"]'), function (shape) {
var wrapper = shape.closest('[data-node-id]');
var id = wrapper ? wrapper.getAttribute('data-node-id') : '';
if (id) (shapes[id] = shapes[id] || []).push(shape);
});
return shapes;
}
function sceneBounds(elements) {
var box = null;
elements.forEach(function (element) {
var b = null;
try { b = element.getBBox(); } catch (_) {}
if (!b || !(b.width || b.height)) return;
if (!box) box = { x: b.x, y: b.y, right: b.x + b.width, bottom: b.y + b.height };
else {
box.x = Math.min(box.x, b.x); box.y = Math.min(box.y, b.y);
box.right = Math.max(box.right, b.x + b.width); box.bottom = Math.max(box.bottom, b.y + b.height);
}
});
return box;
}
function overlay(tag, cls, attrs, parent, before) {
var element = document.createElementNS(SVG_NS, tag);
Object.keys(attrs).forEach(function (name) { element.setAttribute(name, attrs[name]); });
element.setAttribute('class', 'ambient-edge-flow ' + cls);
element.setAttribute('data-ambient-flow-overlay', 'true');
element.setAttribute('aria-hidden', 'true');
parent.insertBefore(element, before || null);
return element;
}
// Timeline and waterfall share a playhead: a lit vertical rule and its
// bloom travel the time axis linearly, because time is the subject.
function addPlayhead(box, x0, x1, lead, sweep, extras) {
var anchor = svg.querySelector('[data-node-id]');
var parent = anchor ? anchor.parentNode : svg;
var attrs = { x1: x0.toFixed(1), x2: x0.toFixed(1), y1: (box.y - 10).toFixed(1), y2: (box.bottom + 10).toFixed(1) };
['playhead-glow', 'playhead'].forEach(function (kind) {
var line = overlay('line', 'ambient-flow-' + kind, attrs, parent, anchor);
line.style.setProperty('--flow-name', 'archify-motion-playhead');
extras.push(line);
});
extras.playhead = { distance: x1 - x0, lead: lead, sweep: sweep };
}
function planSweep(windows, extras) {
var events = Array.prototype.filter.call(svg.querySelectorAll('[data-node-id][data-timeline-x]'), function (node) {
return Number.isFinite(parseFloat(node.getAttribute('data-timeline-x')));
});
if (!events.length) return 0;
var xs = events.map(function (node) { return parseFloat(node.getAttribute('data-timeline-x')); });
var x0 = Math.min.apply(null, xs), x1 = Math.max.apply(null, xs);
var lead = 0.35;
var sweep = Math.min(7, Math.max(3.2, events.length * 0.6));
events.forEach(function (node, index) {
var at = lead + (x1 > x0 ? (xs[index] - x0) / (x1 - x0) : 0) * sweep;
addWindow(windows, node.getAttribute('data-node-id'), at, at + 0.35, true);
});
var box = sceneBounds(Array.prototype.slice.call(svg.querySelectorAll('[data-timeline-lane]')));
if (box && x1 > x0) addPlayhead(box, x0, x1, lead, sweep, extras);
return lead + sweep + 0.6;
}
function planReplay(windows, extras) {
var spans = [];
Array.prototype.forEach.call(svg.querySelectorAll('[data-node-id][data-waterfall-start]'), function (node) {
var start = parseFloat(node.getAttribute('data-waterfall-start'));
var end = parseFloat(node.getAttribute('data-waterfall-end'));
var bar = node.querySelector('[data-animate="node"]');
if (!bar || !Number.isFinite(start) || !(end >= start)) return;
var width = parseFloat(bar.getAttribute('width'));
var x = parseFloat(bar.getAttribute('x'));
if (!(width > 0) || !Number.isFinite(x)) return;
spans.push({ node: node, bar: bar, start: start, end: end, x: x, width: width });
});
if (!spans.length) return 0;
var t0 = Math.min.apply(null, spans.map(function (span) { return span.start; }));
var t1 = Math.max.apply(null, spans.map(function (span) { return span.end; }));
if (!(t1 > t0)) return 0;
var ruler = spans.reduce(function (best, span) { return span.end - span.start > best.end - best.start ? span : best; });
var pxPerUnit = ruler.width / Math.max(ruler.end - ruler.start, 1e-9);
var x0 = ruler.x - (ruler.start - t0) * pxPerUnit;
var lead = 0.35;
var replay = Math.min(6, Math.max(3.4, spans.length * 0.45));
var at = function (t) { return lead + (t - t0) / (t1 - t0) * replay; };
var settle = lead + replay + 0.9;
spans.forEach(function (span) {
var fill = overlay('rect', 'ambient-flow-fill', {
x: span.bar.getAttribute('x'), y: span.bar.getAttribute('y'),
width: span.bar.getAttribute('width'), height: span.bar.getAttribute('height'),
rx: span.bar.getAttribute('rx') || '0'
}, span.bar.parentNode, span.bar.nextSibling);
var tone = /(?:^|\s)c-([a-z]+)(?:\s|$)/.exec(span.bar.getAttribute('class') || '');
if (tone) fill.style.setProperty('--flow-fill', 'var(--' + tone[1] + '-stroke)');
if (span.node.getAttribute('data-waterfall-status') && span.node.getAttribute('data-waterfall-status') !== 'ok') {
fill.classList.add('ambient-flow-fill-failed');
}
fill.timing = { start: at(span.start), end: Math.max(at(span.end), at(span.start) + 0.08), settle: settle };
extras.push(fill);
addWindow(windows, span.node.getAttribute('data-node-id'), fill.timing.end, fill.timing.end);
});
var box = sceneBounds(spans.map(function (span) { return span.bar; }));
if (box) addPlayhead(box, x0, x0 + (t1 - t0) * pxPerUnit, lead, replay, extras);
return settle;
}
function writeKeyframes(records, windows, extras, cycle) {
function at(seconds) {
return (Math.min(100, Math.max(0, seconds / cycle * 100))).toFixed(3) + '%';
}
var ease = 'animation-timing-function: cubic-bezier(0.45, 0.05, 0.35, 1);';
var rules = [
'@keyframes archify-relay-ripple {',
' 0% { r: 2px; opacity: var(--flow-peak, 0.6); }',
' ' + at(0.75) + ', 100% { r: 20px; opacity: 0; }',
'}'
];
records.forEach(function (record) {
if (!record.active) return;
var travel = record.travel;
rules.push(
'@keyframes archify-relay-comet-' + record.index + ' {',
' 0% { stroke-dashoffset: calc(var(--flow-len, 0px) + var(--flow-shift, 0px)); opacity: 0; ' + ease + ' }',
' ' + at(Math.min(0.12, travel * 0.2)) + ' { opacity: var(--flow-peak, 0.9); }',
' ' + at(travel) + ' { stroke-dashoffset: var(--flow-shift, 0px); opacity: var(--flow-peak, 0.9); }',
' ' + at(travel + 0.16) + ', 100% { stroke-dashoffset: var(--flow-shift, 0px); opacity: 0; }',
'}',
// A solid wake draws the travelled path behind the head on the same
// easing; a dashed wake only brightens its authored segments.
'@keyframes archify-relay-wake-' + record.index + ' {',
record.dashed
? ' 0%, ' + at(travel * 0.3) + ' { opacity: 0; }'
: ' 0% { stroke-dashoffset: var(--flow-len, 0px); opacity: var(--flow-peak, 0.3); ' + ease + ' }',
record.dashed
? ' ' + at(travel) + ' { opacity: var(--flow-peak, 0.3); }'
: ' ' + at(travel) + ' { stroke-dashoffset: 0px; opacity: var(--flow-peak, 0.3); }',
' ' + at(travel + 0.9) + ', 100% { stroke-dashoffset: 0px; opacity: 0; }',
'}'
);
});
var glow = function (strong) {
return 'filter: var(--ambient-node-base-filter,) drop-shadow(0 0 ' + (strong ? 14 : 10) + 'px var(--arrow-emphasis));';
};
var base = 'filter: var(--ambient-node-base-filter, none);';
Object.keys(windows).forEach(function (id, index) {
var merged = [];
windows[id].slice().sort(function (a, b) { return a.start - b.start; }).forEach(function (window) {
var peakEnd = Math.max(window.end, window.start + 0.42);
var last = merged[merged.length - 1];
if (last && window.start <= last.peakEnd + 0.78) {
last.peakEnd = Math.max(last.peakEnd, peakEnd);
last.strong = last.strong || window.strong;
} else merged.push({ start: window.start, peakEnd: peakEnd, strong: window.strong });
});
var frames = [' 0%, 100% { ' + base + ' }'];
merged.forEach(function (window) {
frames.push(
' ' + at(window.start) + ' { ' + base + ' }',
' ' + at(window.start + 0.12) + ', ' + at(window.peakEnd) + ' { ' + glow(window.strong) + ' }',
' ' + at(window.peakEnd + 0.78) + ' { ' + base + ' }'
);
});
rules.push('@keyframes archify-relay-node-' + index + ' {', frames.join('\n'), '}');
windows[id].keyframes = 'archify-relay-node-' + index;
});
if (extras.playhead) {
var p = extras.playhead;
rules.push(
'@keyframes archify-motion-playhead {',
' 0% { transform: translateX(0px); opacity: 0; }',
' ' + at(Math.max(0.05, p.lead - 0.2)) + ' { transform: translateX(0px); opacity: 0; }',
' ' + at(p.lead) + ' { transform: translateX(0px); opacity: var(--flow-peak, 0.9); }',
' ' + at(p.lead + p.sweep) + ' { transform: translateX(' + p.distance.toFixed(1) + 'px); opacity: var(--flow-peak, 0.9); }',
' ' + at(p.lead + p.sweep + 0.45) + ', 100% { transform: translateX(' + p.distance.toFixed(1) + 'px); opacity: 0; }',
'}'
);
}
extras.forEach(function (element, index) {
if (!element.timing) return;
var t = element.timing;
rules.push(
'@keyframes archify-replay-fill-' + index + ' {',
' 0%, ' + at(t.start) + ' { transform: scaleX(0); opacity: var(--flow-peak, 0.4); }',
' ' + at(t.end) + ', ' + at(t.settle) + ' { transform: scaleX(1); opacity: var(--flow-peak, 0.4); }',
' ' + at(t.settle + 0.7) + ', 100% { transform: scaleX(1); opacity: 0; }',
'}'
);
element.style.setProperty('--flow-name', 'archify-replay-fill-' + index);
});
var style = document.createElement('style');
style.setAttribute('data-ambient-flow-keyframes', 'true');
style.textContent = rules.join('\n');
document.head.appendChild(style);
}
function createFlows() {
var records = [];
Array.prototype.forEach.call(svg.querySelectorAll('path[data-animate="edge"]'), function (shape, index) {
var d = shape.getAttribute('data-motion-path') || shape.getAttribute('d');
if (!d) return;
var anchor = shape;
// Tree Branches initializes after these overlays and includes their
// inherited ancestors in its existing collapse/expand lifecycle.
function tagOverlay(flow) {
flow.setAttribute('data-ambient-flow-overlay', 'true');
flow.setAttribute('aria-hidden', 'true');
if (shape.hasAttribute('data-tree-ancestors')) {
flow.setAttribute('data-tree-ancestors', shape.getAttribute('data-tree-ancestors'));
}
}
function addPath(cls) {
var flow = document.createElementNS(SVG_NS, 'path');
flow.setAttribute('d', d);
['transform', 'mask', 'clip-path', 'vector-effect'].forEach(function (name) {
if (shape.hasAttribute(name)) flow.setAttribute(name, shape.getAttribute(name));
});
flow.setAttribute('class', 'ambient-edge-flow ambient-flow-' + cls);
tagOverlay(flow);
shape.parentNode.insertBefore(flow, anchor.nextSibling);
anchor = flow;
return flow;
}
var wake = addPath('wake');
var len = 0;
try { len = wake.getTotalLength(); } catch (_) {}
if (!(len > 0)) {
wake.remove();
return;
}
var record = {
index: records.length,
len: len,
from: shape.getAttribute('data-edge-from') || shape.getAttribute('data-composition-edge-from') || '',
to: shape.getAttribute('data-edge-to') || shape.getAttribute('data-composition-edge-to') || '',
step: stepOf(shape, index),
travel: Math.min(RELAY.maxTravel, Math.max(RELAY.minTravel, len / RELAY.speed)),
depart: 0,
active: false,
layers: []
};
records.push(record);
// Dashed authored lines keep their dash language through the relay:
// the wake brightens only the authored segments and never fills the
// gaps back into a solid line.
var authoredDash = '';
try { authoredDash = window.getComputedStyle(shape).strokeDasharray; } catch (_) {}
record.dashed = !!(authoredDash && authoredDash !== 'none');
wake.style.setProperty('--flow-dasharray', record.dashed ? authoredDash : len.toFixed(1) + ' ' + len.toFixed(1));
// Absolute caps keep the comet a slim streak on long edges instead
// of growing into a bright slab; the tail disintegrates into a
// sparkle of fragments behind the head, and --flow-shift trails
// each layer behind the head tip by its own length.
var headLen = Math.min(14, Math.max(7, len * 0.03));
var haloLen = Math.min(22, Math.max(12, len * 0.07));
var trail = len >= 120
? { pattern: '2.5 2 4 3 5.5 4.5 8 ' + (len - 30).toFixed(1), length: 30 }
: (len >= 60
? { pattern: '3 3 7 ' + (len - 13).toFixed(1), length: 13 }
: { pattern: '5 ' + Math.max(4, len - 5).toFixed(1), length: 5 });
record.layers.push(
{ flow: wake, kind: 'wake', dash: len, shift: 0 },
{ flow: addPath('glow'), kind: 'comet', dash: haloLen, shift: (haloLen - headLen) / 2 },
{ flow: addPath('halo'), kind: 'comet', dash: haloLen, shift: (haloLen - headLen) / 2 },
{ flow: addPath('tail'), kind: 'comet', dash: trail.length, shift: trail.length - headLen, dasharray: trail.pattern },
{ flow: addPath('head'), kind: 'comet', dash: headLen, shift: 0 }
);
if (choreography === 'stream') {
// A packet train behind the comet reads as a continuous stream.
[1, 2, 3].forEach(function (order) {
record.layers.push({ flow: addPath('head packet'), kind: 'comet', dash: Math.max(4, headLen * 0.55), shift: 0, packet: order });
});
}
// The ripple lands where the comet does; keep it out of the edge's
// mask so it can bloom over the node boundary.
var end = null;
try { end = wake.getPointAtLength(len); } catch (_) {}
if (end) {
var ripple = document.createElementNS(SVG_NS, 'circle');
ripple.setAttribute('cx', end.x.toFixed(1));
ripple.setAttribute('cy', end.y.toFixed(1));
ripple.setAttribute('r', '2');
if (shape.hasAttribute('transform')) ripple.setAttribute('transform', shape.getAttribute('transform'));
ripple.setAttribute('class', 'ambient-edge-flow ambient-flow-ripple');
tagOverlay(ripple);
shape.parentNode.insertBefore(ripple, anchor.nextSibling);
record.ripple = ripple;
}
});
var windows = Object.create(null);
var extras = [];
var span = 0;
if (choreography === 'sweep') span = planSweep(windows, extras);
else if (choreography === 'replay') span = planReplay(windows, extras);
else if (records.length) {
var linked = records.filter(function (record) { return record.from && record.to; });
if (choreography === 'sequence' || linked.length < records.length) planSequence(records, windows);
else if (choreography === 'walk') planWalk(records, windows);
else planRelay(records, windows);
span = records.reduce(function (max, record) {
return record.active ? Math.max(max, record.depart + record.travel) : max;
}, 0);
span = scalePlan(records, windows, span);
}
records.forEach(function (record) {
if (record.active) return;
record.layers.forEach(function (layer) { layer.flow.remove(); });
if (record.ripple) record.ripple.remove();
});
var active = records.filter(function (record) { return record.active; });
if (!active.length && !extras.length) return;
var cycle = Math.max(RELAY.minCycle, span + RELAY.land + RELAY.rest);
writeKeyframes(active, windows, extras, cycle);
svg.style.setProperty('--flow-cycle', cycle.toFixed(3) + 's');
html.style.setProperty('--flow-cycle-chrome', cycle.toFixed(3) + 's');
active.forEach(function (record) {
var gap = Math.min(0.22, record.travel * 0.28);
record.layers.forEach(function (layer) {
var flow = layer.flow;
flow.style.setProperty('--flow-len', record.len.toFixed(1) + 'px');
flow.style.setProperty('--flow-delay', (record.depart + (layer.packet || 0) * gap).toFixed(3) + 's');
flow.style.setProperty('--flow-dash', layer.dash.toFixed(1) + 'px');
flow.style.setProperty('--flow-shift', layer.shift.toFixed(1) + 'px');
flow.style.setProperty('--flow-name', 'archify-relay-' + layer.kind + '-' + record.index);
if (layer.dasharray) flow.style.setProperty('--flow-dasharray', layer.dasharray);
});
if (record.ripple) {
record.ripple.style.setProperty('--flow-delay', (record.depart + record.travel).toFixed(3) + 's');
record.ripple.style.setProperty('--flow-name', 'archify-relay-ripple');
}
});
var shapes = nodeShapes();
Object.keys(windows).forEach(function (id) {
(shapes[id] || []).forEach(function (shape) {
shape.style.setProperty('--flow-node-name', windows[id].keyframes);
});
});
flowCount = active.length + extras.length;
}
function settleEntry() {
entryPending.clear();
svg.removeEventListener('animationend', onEntryBoundary, true);
svg.removeEventListener('animationcancel', onEntryBoundary, true);
html.setAttribute('data-ambient-entry', 'settled');
}
function onEntryBoundary(event) {
if (!entryPending.has(event.target)) return;
// The entrance settles only when each node's pulse finishes; the
// shorter fade-rise enter animation may end first without ending it.
if (event.animationName && !/node-pulse/.test(event.animationName)) return;
entryPending.delete(event.target);
if (!entryPending.size) settleEntry();
}
function renderAmbient(suppressed) {
html.setAttribute('data-ambient-motion', flowCount ? (suppressed ? 'paused' : 'running') : 'empty');
if (suppressed) {
entryStarted = true;
settleEntry();
} else if (!entryStarted) {
entryStarted = true;
entryPending = new Set(Array.prototype.slice.call(svg.querySelectorAll('[data-animate="node"]')));
if (!entryPending.size) settleEntry();
else {
svg.addEventListener('animationend', onEntryBoundary, true);
svg.addEventListener('animationcancel', onEntryBoundary, true);
html.setAttribute('data-ambient-entry', 'running');
}
}
}
function readStored() {
try { return localStorage.getItem(STORAGE_KEY); } catch (_) { return null; }
}
function writeStored() {
try {
if (readerPaused) localStorage.removeItem(STORAGE_KEY);
else localStorage.setItem(STORAGE_KEY, 'live');
} 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');
renderAmbient(paused || owner || html.hasAttribute('data-embed') || html.hasAttribute('data-share-playback') || html.hasAttribute('data-document-hidden'));
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-entry');
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 ''; }
};
}
createFlows();
html.setAttribute('data-motion-capable', 'true');
btn.hidden = false;
readerPaused = readStored() !== 'live';
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 (typeof MutationObserver !== 'undefined') {
var contextObserver = new MutationObserver(render);
contextObserver.observe(html, { attributes: true, attributeFilter: ['data-embed', 'data-share-playback'] });
}
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;
if (element) {
try {
var parsed = JSON.parse(element.textContent || 'null');
if (parsed && parsed.verified === true && parsed.repository && parsed.nodes) payload = parsed;
} catch (_) {}
}
// Keep verified evidence discoverable to screen readers without adding
// visual marks. Restore first so repeated initialization never appends
// another count or retains one after evidence becomes unavailable.
var svg = document.querySelector('.diagram-container svg');
if (svg) Array.prototype.forEach.call(svg.querySelectorAll('[data-node-id]'), function (node) {
if (node.hasAttribute('data-source-evidence-count')) {
var savedLabel = node.getAttribute('data-source-evidence-original-label');
if (savedLabel == null) node.removeAttribute('aria-label');
else node.setAttribute('aria-label', savedLabel);
node.removeAttribute('data-source-evidence-count');
node.removeAttribute('data-source-evidence-original-label');
}
var sources = payload && payload.nodes[node.getAttribute('data-node-id')];
if (!Array.isArray(sources) || !sources.length) return;
var originalLabel = node.getAttribute('aria-label');
node.setAttribute('data-source-evidence-count', String(sources.length));
if (originalLabel != null) node.setAttribute('data-source-evidence-original-label', originalLabel);
node.setAttribute('aria-label', (originalLabel ? originalLabel + ', ' : '') +
viewerCount('viewer.passport.sourceCount', sources.length));
});
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() : [];
}
};
})();
/* ============================================================
Tree Branches — expand/collapse for hierarchy diagrams. The renderer
lays out the complete tree once and tags every node, link, and toggle
with its ancestors; collapsing only hides descendants, so visible nodes
never move and the reader keeps their place. Selecting a hidden node
through Finder, Outline, or a shared link expands its ancestors. Export
always writes the complete tree (see Export Cleanup).
============================================================ */
Archify.treeBranches = (function () {
var svg = document.querySelector('.diagram-container > svg[data-tree-ui]');
if (!svg) return { active: false };
var collapsed = Object.create(null);
var toggles = Object.create(null);
Array.prototype.forEach.call(svg.querySelectorAll('[data-tree-toggle]'), function (toggle) {
toggles[toggle.getAttribute('data-tree-toggle')] = toggle;
});
var tagged = svg.querySelectorAll('[data-tree-ancestors]');
var expandAll = svg.querySelector('[data-tree-expand-all]');
function ancestors(el) {
return (el.getAttribute('data-tree-ancestors') || '').split(' ').filter(Boolean);
}
function apply() {
Array.prototype.forEach.call(tagged, function (el) {
var hidden = ancestors(el).some(function (id) { return collapsed[id]; });
if (hidden) el.setAttribute('data-tree-hidden', '');
else el.removeAttribute('data-tree-hidden');
});
var any = false;
Object.keys(toggles).forEach(function (id) {
var toggle = toggles[id];
var isCollapsed = Boolean(collapsed[id]);
any = any || isCollapsed;
toggle.setAttribute('aria-expanded', isCollapsed ? 'false' : 'true');
toggle.setAttribute('aria-label', toggle.getAttribute(isCollapsed ? 'data-tree-label-expand' : 'data-tree-label-collapse') || '');
var node = svg.querySelector('[data-node-id="' + id + '"]');
if (node) {
if (isCollapsed) node.setAttribute('data-tree-collapsed', '');
else node.removeAttribute('data-tree-collapsed');
}
});
if (any) svg.setAttribute('data-tree-any-collapsed', '');
else svg.removeAttribute('data-tree-any-collapsed');
}
function setCollapsed(id, value) {
if (!toggles[id]) return false;
if (value) collapsed[id] = true;
else delete collapsed[id];
apply();
return true;
}
function toggle(id) { return setCollapsed(id, !collapsed[id]); }
// Reveal a node by expanding every collapsed ancestor; nothing else moves.
function reveal(id) {
var node = svg.querySelector('[data-node-id="' + id + '"]');
if (!node) return false;
var changed = false;
ancestors(node).forEach(function (ancestor) {
if (collapsed[ancestor]) { delete collapsed[ancestor]; changed = true; }
});
if (changed) apply();
return changed;
}
function restoreAll() {
collapsed = Object.create(null);
apply();
}
function activate(event) {
var target = event.target.closest('[data-tree-toggle], [data-tree-expand-all]');
if (!target || !svg.contains(target)) return false;
event.preventDefault();
event.stopPropagation();
if (target.hasAttribute('data-tree-expand-all')) {
restoreAll();
var root = svg.querySelector('[data-tree-toggle]:not([data-tree-ancestors])');
if (root) root.focus();
} else {
toggle(target.getAttribute('data-tree-toggle'));
target.focus();
}
return true;
}
// Capture phase so the canvas focus handler never treats a toggle press
// as a background click that clears the current focus.
svg.addEventListener('click', activate, true);
svg.addEventListener('keydown', function (event) {
if (event.key === 'Enter' || event.key === ' ') { activate(event); return; }
var node = event.target.closest('[data-node-id]');
if (!node || !svg.contains(node)) return;
var id = node.getAttribute('data-node-id');
// Arrow keys follow the tree convention: left collapses, right expands.
if (event.key === 'ArrowLeft' && toggles[id] && !collapsed[id]) { event.preventDefault(); setCollapsed(id, true); }
else if (event.key === 'ArrowRight' && collapsed[id]) { event.preventDefault(); setCollapsed(id, false); }
}, true);
// Focus reveals ancestors synchronously before positioning its lens.
// The observer also covers paths that change pressed state directly.
new MutationObserver(function (records) {
records.forEach(function (record) {
var el = record.target;
if (el.hasAttribute && el.hasAttribute('data-tree-hidden') && el.getAttribute('aria-pressed') === 'true') {
reveal(el.getAttribute('data-node-id'));
}
});
}).observe(svg, { subtree: true, attributes: true, attributeFilter: ['aria-pressed'] });
Object.keys(toggles).forEach(function (id) {
if (toggles[id].hasAttribute('data-tree-initially-collapsed')) collapsed[id] = true;
});
apply();
return {
active: true,
toggle: toggle,
collapse: function (id) { return setCollapsed(id, true); },
expand: function (id) { return setCollapsed(id, false); },
reveal: reveal,
restoreAll: restoreAll,
collapsedIds: function () { return Object.keys(collapsed); },
expandAllControl: expandAll
};
})();
/* ============================================================
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:not([data-ambient-flow-overlay]), 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);
if (shape.getAttribute('data-motion-path')) clone.setAttribute('d', relationshipTokenPath(shape));
clone.removeAttribute('data-motion-path');
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('data-motion-path') || 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;
// Hash restoration and internal selections use this path too. Reveal
// tree ancestors before measuring the selected node for its lens.
if (Archify.treeBranches && Archify.treeBranches.active) {
normalized.forEach(function (id) { Archify.treeBranches.reveal(id); });
}
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 copyFocusLink() {
if (activeIds.length !== 1) return Promise.resolve(false);
var record = pinnedRelationshipRecord();
var relationId = record && record.id;
var value = readerLink(relationId
? 'relation=' + encodeURIComponent(relationId)
: 'focus=' + encodeURIComponent(activeIds[0]) + (reachabilityMode ? '&reach=' + reachabilityMode : ''));
return (value === null ? Promise.resolve(false) : copyReaderText(value)).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.ctrlKey || event.altKey || event.metaKey || event.shiftKey) return;
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;
// Export consumes the active semantic snapshot; its controls are not
// an outside selection. Genuine outside activation still clears it.
if (target.closest('[data-node-id], [data-relationship-hit-key], .overview-map, #btn-export, #export-menu')) return;
clear();
}, true);
window.addEventListener('scroll', requestLensPlacement, { passive: true });
window.addEventListener('resize', requestLensPlacement);
container.addEventListener('scroll', requestLensPlacement, { passive: true });
// Reader and Chrome may finish resizing the stage after window resize.
// The absolute Passport does not resize its container when reclamped.
if (typeof ResizeObserver === 'function') {
new ResizeObserver(function () {
if (!chip.hidden) requestLensPlacement();
}).observe(container);
}
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:not([data-ambient-flow-overlay]), 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);
if (shape.getAttribute('data-motion-path')) clone.setAttribute('d', shape.getAttribute('data-motion-path'));
clone.removeAttribute('data-motion-path');
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 legend = svg && svg.querySelector('[data-legend]');
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 readerFit = svg && svg.getAttribute('data-reader-fit');
var widthFirstReading = readerFit === 'width-first';
var automaticSequence = widthFirstReading && svg.hasAttribute('data-sequence-column-fit');
var readingScroll = widthFirstReading;
var measuredHeightFit = readerFit === 'intrinsic-height' || widthFirstReading;
var frame = 0;
var settleFrame = 0;
var lastWidth = 0;
var automaticAreaHeight = 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 MAX_AUTOMATIC_SCALE = 1.5;
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.style.removeProperty('--archify-diagram-min-height');
html.removeAttribute('data-reader-area');
html.removeAttribute('data-reader-narrow');
html.removeAttribute('data-reader-layout');
html.removeAttribute('data-reader-overflow');
setRail(false);
lastWidth = 0;
automaticAreaHeight = 0;
settledCap = 0;
clearLegend();
}
function clearLegend() {
if (!legend || !legend.hasAttribute('data-reader-legend-corner')) return;
legend.removeAttribute('data-reader-legend-corner');
legend.style.removeProperty('--archify-reader-legend-transform');
}
// Keep the original legend in its SVG: renderer hover/focus selectors and
// canonical export still own that same group. Ordinary capped reading
// and fitted automatic sequences borrow the outer corner at 25–100%.
function syncLegend() {
var cameraScale = Number(svg && svg.getAttribute('data-view-scale')) || 1;
var cornerReading = html.getAttribute('data-reader-area') === 'true' || automaticSequence && !readingScroll;
if (!legend || !eligible() || !cornerReading || cameraScale > 1.001) {
clearLegend();
return;
}
var box = legend.getBBox();
var parentMatrix = legend.parentNode.getScreenCTM();
if (!parentMatrix || !box.width || !box.height) {
clearLegend();
return;
}
var rect = diagram.getBoundingClientRect();
var style = window.getComputedStyle(diagram);
var scale = Math.min(svg.clientWidth / viewBox.width, svg.clientHeight / viewBox.height);
if (!Number.isFinite(scale) || scale <= 0) {
clearLegend();
return;
}
var left = rect.left + number(style.borderLeftWidth) + number(style.paddingLeft);
var right = rect.right - number(style.borderRightWidth) - number(style.paddingRight);
var bottom = rect.bottom - number(style.borderBottomWidth) - number(style.paddingBottom);
var top = bottom - box.height * scale;
var nav = diagram.querySelector('.diagram-nav');
var navRect = visible(nav) ? nav.getBoundingClientRect() : null;
// Long or multi-row legends retain their canonical position if the
// spare corner cannot hold them clear of the dock and content area.
if (left + box.width * scale > right || top < rect.top + number(style.paddingTop) + number(style.borderTopWidth) ||
(navRect && left < navRect.right + 10 && left + box.width * scale > navRect.left - 10 &&
top < navRect.bottom + 10 && bottom > navRect.top - 10)) {
clearLegend();
return;
}
var matrix = parentMatrix.inverse().translate(left, top).scale(scale).translate(-box.x, -box.y);
var value = 'matrix(' + [matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f].join(',') + ')';
if (legend.style.getPropertyValue('--archify-reader-legend-transform') !== value) {
legend.style.setProperty('--archify-reader-legend-transform', value);
}
if (!legend.hasAttribute('data-reader-legend-corner')) legend.setAttribute('data-reader-legend-corner', '');
}
// 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));
var changed = Math.abs(rounded - lastWidth) >= 1;
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');
var railShare = html.getAttribute('data-reader-rail') === 'true' ? RAIL_WIDTH + RAIL_GAP : 0;
var diagramWidth = Math.max(1, rounded - railShare - chromeMetrics().diagramX);
// Content fitting and automatic enlargement never reduce the outer
// reader region. The SVG alone takes the narrower reading width.
var automaticWidthCap = measuredHeightFit && viewBox && viewBox.width > 0
? viewBox.width * MAX_AUTOMATIC_SCALE : 0;
if (automaticWidthCap && diagramWidth > automaticWidthCap) {
diagramWidth = automaticWidthCap;
html.style.setProperty('--archify-diagram-min-height', automaticAreaHeight + 'px');
html.setAttribute('data-reader-area', 'true');
} else {
// Ordinary automatic diagrams retain their existing document flow.
// Settling can also stop binding the cap without a new measure.
html.style.removeProperty('--archify-diagram-min-height');
html.removeAttribute('data-reader-area');
}
if (diagramWidth < Math.max(rounded, shellFloor) - railShare - chromeMetrics().diagramX) {
html.style.setProperty('--archify-diagram-max-width', diagramWidth + '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');
syncLegend();
return changed;
}
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();
// visual-check fails any page whose rounded-up height exceeds the
// viewport, so even a 1px remainder must be reduced or declared.
if (overflow > 0 && !readingScroll && lastWidth > Math.ceil(minWidth)) {
applyWidth(Math.max(minWidth, lastWidth - overflow * ratio - 4), minWidth);
settledCap = lastWidth;
html.setAttribute('data-reader-overflow', 'reduced');
} else if (overflow > 0) {
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);
// Sequence first-screen fitting preserves source text sizes instead
// of shrinking to the shared hard readability floor.
var readableWidth = viewBox && viewBox.width > 0
? viewBox.width * (automaticSequence ? 1 : minimumReadableScale()) + chrome.diagramX
: MIN_READER_WIDTH;
var maxWidth = Math.min(MAX_READER_WIDTH, viewportCap);
var readableMinimumWidth = automaticSequence || measuredHeightFit && ratio < WIDE_RATIO
? readableWidth
: measuredHeightFit && ratio >= WIDE_RATIO && Number.isFinite(declaredMinimumText)
? Math.max(MIN_READER_WIDTH, readableWidth)
: MIN_READER_WIDTH;
var minWidth;
if (automaticSequence) {
minWidth = Math.min(readableMinimumWidth, maxWidth);
} else 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';
}
// Height-fit families with notes below the fold may shrink to their
// readable floor. Width-first families choose their reading width
// below and retain page scrolling instead.
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);
// Compact automatic sequences fit the first screen without shrinking
// their authored text. Longer sequences retain reading width and page
// scroll; other width-first families keep their existing strategy.
var sequenceFits = automaticSequence && Math.min(desiredWidth, maxWidth) >=
readableWidth + (docked ? railExtra : 0);
readingScroll = widthFirstReading && !sequenceFits;
if (readingScroll) desiredWidth = maxWidth;
// Only a binding enlargement cap needs a separate normal reader area.
// Uncapped diagrams keep their existing natural height and Chrome fit.
automaticAreaHeight = availableSvgHeight + chrome.diagramY;
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,
legend && legend.hasAttribute('data-reader-legend-corner') ? legend.style.getPropertyValue('--archify-reader-legend-transform') : ''
].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 });
window.addEventListener('beforeprint', clearLegend);
window.addEventListener('afterprint', schedule);
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,
syncLegend: syncLegend,
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,
lift,
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 || liftFrame || 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); });
// Camera zoom changes outer page flow without changing intrinsic SVG
// size. Follow that box for dock placement, independently of scrolling
// and the baseline stage-rail measurement.
new ResizeObserver(scheduleLift).observe(container);
}
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 minimumZoom = 0.25;
var maximumZoom = 3;
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 originalFlowHeight = container.style.getPropertyValue('height');
var originalFlowHeightPriority = container.style.getPropertyPriority('height');
var compactPageFlow = false;
var viewBox = svg.viewBox && svg.viewBox.baseVal;
function clamp() {
var width = svg.clientWidth || 1;
var height = svg.clientHeight || 1;
if (state.scale < 1) {
var visibleWidth = Math.min(width, container.clientWidth);
var availableX = visibleWidth - width * state.scale;
state.x = availableX >= 0 ? container.scrollLeft + availableX / 2
: Math.max(container.scrollLeft + availableX, Math.min(container.scrollLeft, state.x));
// Long sequences keep their first rows in view instead of centering
// inside a layout box that can be several screens tall.
state.y = height > window.innerHeight ? 0 : height * (1 - state.scale) / 2;
return;
}
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 camera = sampleRenderedState();
var x;
var y;
var width;
var height;
if (window.innerWidth <= 720 && container.hasAttribute('data-wide-diagram') && camera.scale >= 1) {
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 + (((container.scrollLeft - camera.x) / camera.scale) - metrics.offsetX) / metrics.scale;
y = viewBox.y + ((-camera.y / camera.scale) - metrics.offsetY) / metrics.scale;
width = Math.min(viewBox.width, Math.min(metrics.width, container.clientWidth) / camera.scale / metrics.scale);
height = Math.min(viewBox.height, metrics.height / camera.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));
// The camera's layout box can span several browser screens. Intersect
// its logical bounds with the actual window, including page scrolling.
var matrix = svg.getScreenCTM();
if (matrix && matrix.d > 0) {
var end = y + height;
y = Math.max(y, Math.min(end - 1, -matrix.f / matrix.d));
height = Math.max(1, Math.min(end, (window.innerHeight - matrix.f) / matrix.d) - y);
}
return { x: x, y: y, width: width, height: height, scale: camera.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
});
var readout = (showDetailLevel ? resolvedLevel + ' ' : '') + percent;
resetBtn.setAttribute('aria-label', readout + ' · ' + 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;
}
// CSS transforms keep the SVG's intrinsic layout size. Shrink only its
// outer page-flow box for long ordinary diagrams, never the SVG itself:
// changing its height would feed the smaller size back into camera math.
function ordinaryPage() {
var html = document.documentElement;
return html.getAttribute('data-embed') !== 'true' &&
html.getAttribute('data-present') !== 'true' &&
(!window.matchMedia || !window.matchMedia('print').matches);
}
function syncPageFlow(camera) {
camera = camera || state;
var height = svg.clientHeight;
var ordinary = ordinaryPage();
// Restore ordinary flow as soon as the target reaches 100%. Chrome
// uses that target to resume baseline rail measurement; retaining a
// partially shrunk box during the CSS transition would inflate reserve.
if (ordinary && state.scale < 1 && camera.scale < 1 && height > window.innerHeight) {
var style = getComputedStyle(container);
var chrome = ['paddingTop', 'paddingBottom', 'borderTopWidth', 'borderBottomWidth']
.reduce(function (total, key) { return total + (parseFloat(style[key]) || 0); }, 0);
var flowHeight = Math.ceil(height * camera.scale + Math.max(0, camera.y) + chrome) + 'px';
if (container.style.getPropertyValue('height') !== flowHeight) container.style.setProperty('height', flowHeight);
compactPageFlow = true;
} else if (compactPageFlow) {
if (originalFlowHeight) container.style.setProperty('height', originalFlowHeight, originalFlowHeightPriority);
else container.style.removeProperty('height');
compactPageFlow = false;
}
}
function syncViewportClip() {
if (clipFrame) cancelAnimationFrame(clipFrame);
clipFrame = 0;
function sample() {
clipFrame = 0;
var rendered = sampleRenderedState();
syncPageFlow(rendered);
clipToViewport(rendered);
if (Archify.radar && Archify.radar.isOpen()) Archify.radar.sync();
if (Archify.readerLayout && typeof Archify.readerLayout.syncLegend === 'function') Archify.readerLayout.syncLegend();
if (!cameraSettled(rendered)) clipFrame = requestAnimationFrame(sample);
}
sample();
}
function apply() {
clamp();
svg.style.transform = 'translate(' + state.x + 'px,' + state.y + 'px) scale(' + state.scale + ')';
svg.setAttribute('data-view-scale', String(state.scale));
syncViewportClip();
renderControls();
outBtn.disabled = state.scale <= minimumZoom;
inBtn.disabled = state.scale >= maximumZoom;
container.classList.toggle('is-pannable', state.scale > 1);
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(minimumZoom, Math.min(maximumZoom, Math.round(next * 4) / 4));
if (next === previous) return;
var longDiagram = svg.clientHeight > window.innerHeight && Math.min(previous, next) < 1;
var before = longDiagram ? svg.getBoundingClientRect() : null;
var anchorY = before ? Math.max(0, Math.min(window.innerHeight, before.bottom) + Math.max(0, before.top)) / 2 : 0;
if (before && (before.bottom <= 0 || before.top >= window.innerHeight)) anchorY = window.innerHeight / 2;
var logicalY = before ? Math.max(0, Math.min(svg.clientHeight, (anchorY - before.top) / previous)) : 0;
var documentTop = before ? window.scrollY + before.top - state.y : 0;
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();
if (longDiagram) {
var scaledHeight = svg.clientHeight * next;
var pageY = scaledHeight <= window.innerHeight ? documentTop + state.y
: documentTop + state.y + logicalY * next - anchorY;
window.scrollTo({ left: window.scrollX, top: Math.max(0, pageY), behavior: 'instant' });
}
}
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 scrollLongDiagramTo(contentY, metrics, options) {
if (!ordinaryPage() || metrics.height <= window.innerHeight) return;
// apply() may still be animating its CSS transform. Recover the layout
// origin from the painted transform, then use the final camera target.
var painted = sampleRenderedState();
var rect = svg.getBoundingClientRect();
var pixelScale = rect.height / metrics.height / painted.scale;
var documentTop = window.scrollY + rect.top - painted.y * pixelScale;
var target = documentTop + (state.y + contentY * state.scale) * pixelScale - window.innerHeight / 2;
window.scrollTo({ left: window.scrollX, top: Math.max(0, target), behavior: options.instant ? 'instant' : 'smooth' });
}
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; }
scrollLongDiagramTo(metrics.offsetY + (logicalY - viewBox.y) * metrics.scale, metrics, options);
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();
scrollLongDiagramTo(contentY, metrics, options);
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();
});
});
// Reader width and Chrome reserve can change without a window resize.
// Observe their resulting sizes; the SVG remains intrinsically sized, so
// the outer flow height cannot feed back into its camera base.
if (typeof ResizeObserver === 'function') {
var flowObserver = new ResizeObserver(function () { syncPageFlow(sampleRenderedState()); });
flowObserver.observe(svg);
flowObserver.observe(container);
}
if (typeof MutationObserver === 'function') {
new MutationObserver(function () { syncPageFlow(sampleRenderedState()); })
.observe(document.documentElement, { attributes: true, attributeFilter: ['data-embed', 'data-present'] });
}
var printMedia = window.matchMedia && window.matchMedia('print');
if (printMedia && printMedia.addEventListener) printMedia.addEventListener('change', function () { syncPageFlow(sampleRenderedState()); });
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;
if (item.links === 0) links.setAttribute('data-zero', 'true');
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) {
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) return;
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) {
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) return;
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:not([data-ambient-flow-overlay]), 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);
if (shape.getAttribute('data-motion-path')) clone.setAttribute('d', shape.getAttribute('data-motion-path'));
clone.removeAttribute('data-motion-path');
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);
if (shape.getAttribute('data-motion-path')) clone.setAttribute('d', shape.getAttribute('data-motion-path'));
clone.removeAttribute('data-motion-path');
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 copyLink() {
if (mode !== 'result') return Promise.resolve(false);
var value = readerLink('route=' + encodeURIComponent(startId) + '~' + encodeURIComponent(endId));
return (value === null ? Promise.resolve(false) : copyReaderText(value)).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) {
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) return;
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:not([data-ambient-flow-overlay]), 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);
if (shape.getAttribute('data-motion-path')) clone.setAttribute('d', shape.getAttribute('data-motion-path'));
clone.removeAttribute('data-motion-path');
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 copyLink() {
if (!selectedKinds.length) return Promise.resolve(false);
var value = readerLink('lens=' + selectedKinds.map(encodeURIComponent).join('~'));
return (value === null ? Promise.resolve(false) : copyReaderText(value)).then(function (copied) {
copyBtn.textContent = viewerText(copied ? 'viewer.common.copied' : 'viewer.common.copyFailed');
copyBtn.setAttribute('aria-label', viewerText(copied ? 'viewer.common.copied' : 'viewer.common.copyFailed'));
window.setTimeout(function () {
copyBtn.textContent = viewerText('viewer.common.copyLink');
copyBtn.setAttribute('aria-label', viewerText('viewer.lens.copy'));
}, 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) {
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) return;
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) {
// Return focus where the reader was when the guide opened; the
// trigger itself is only the fallback when that element is gone.
var target = returnFocus && returnFocus.isConnected ? 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) {
// Clicks anywhere on the trigger (including its icon span) are the
// toggle itself, never an outside dismissal.
if (!panel.hidden && !panel.contains(event.target) && !trigger.contains(event.target)) 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.shiftKey && (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Home' || e.key === 'End')) 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>
<script>(function(){var h=document.documentElement;function set(m){
if(m==='embed')h.setAttribute('data-embed','true');else h.removeAttribute('data-embed');
var p=window.Archify&&Archify.presentation;if(p){if(m==='present')p.enter();else if(p.active())p.exit();}
dispatchEvent(new Event('resize'));if(window.Archify&&Archify.view&&Archify.view.reset)Archify.view.reset({automatic:true});}
addEventListener('message',function(e){if(e.source===parent&&typeof e.data==='string'&&e.data.indexOf('arch:')===0)set(e.data.slice(5));});
// The viewer's own Esc/Exit leaves presentation inside the iframe; tell the article so it closes the overlay too.
new MutationObserver(function(){if(!h.hasAttribute('data-present'))parent.postMessage('arch:exit','*');}).observe(h,{attributes:true,attributeFilter:['data-present']});
parent.postMessage('arch:ready','*');})();</script>
<style>/* Viewer centres the 720px canvas in full screen; on a narrow phone that hides its left edge past scroll 0. Auto margins centre without overflowing left. */
html[data-present="true"]:not([data-embed="true"]) .diagram-container{justify-content:flex-start}
html[data-present="true"]:not([data-embed="true"]) .diagram-container>svg{margin-inline:auto}</style></body>
</html>
"></iframe>
<button type="button" class="arch-open"><span data-l="nb">Utforsk diagrammet</span><span data-l="en" hidden>Explore the diagram</span></button>
</div>
<figcaption data-l="nb">Hovedsporet øverst er den suverene banken. De to sidesporene ender i rødt: «bare svaret lagret» gir en bank som starter fra null ved hvert modellbytte, og «lagret hos leverandør» er FDSAs tillegg om at eierskap ikke hjelper hvis det lærte ligger i et proprietært format. Illustrasjon av artikkelens tankeeksperiment, ikke en beskrivelse av faktiske banker.</figcaption>
<figcaption data-l="en" hidden>The main track at the top is the sovereign bank. The two side tracks end in red: "only the answer stored" gives a bank that starts from zero at every model swap, and "stored at the vendor" is FDSA's addition that ownership does not help if what was learned sits in a proprietary format. An illustration of the essay's thought experiment, not a description of real banks.</figcaption>
</figure>
<!-- ===================== NORSK, del 3 ===================== -->
<div data-l="nb">
<p>Forfatterne kaller dette «det suverene firmaet»: et firma som kan bytte leverandør uten å miste det det har lært. Microsofts Satya Nadella sier noe lignende om «token capital», KI-kapasiteten et selskap bygger og eier selv, og Nvidias Jensen Huang kaller selskapets egne data for gullgruven.</p>
<p>Dette er, med sivil språkdrakt, kjernen i FDSAs første fundament, datasuverenitet, og i Stortingets vedtak 991 og 992 fra 2026 om nasjonal kontroll over data og infrastruktur. FDSA legger til en tredje forutsetning som artikkelen bekrefter indirekte: åpne formater. Lakehouse ble mulig fordi tre selskaper uavhengig av hverandre landet på åpne tabellformater. Proprietære formater gir innlåsing selv innenfor norsk jurisdiksjon.</p>
<blockquote>Eierskap alene er ingen vollgrav, sier forfatterne, men det hindrer at firmaets hukommelse blir fanget i en leverandørs teknologistabel. Fortrinnet oppstår når dataene og KI-en som resonnerer over dem, gjør firmaet bedre raskere enn konkurrentene.</blockquote>
<p>Bytt ut «konkurrentene» med «motstanderen», så har du Forsvarets versjon.</p>
<h2>Hva dette betyr i praksis</h2>
<span class="aud">For utviklere, teamledere og leverandører</span>
<p>Hvis du bygger noe for Forsvaret i dag, enten innenfra eller som leverandør, er det fire konkrete konsekvenser.</p>
<p><strong>Publiser data som produkt, ikke som skjermbilde.</strong> Agentene i artikkelen leser og skriver direkte mot registeret. Et system som bare leverer informasjon gjennom sitt eget brukergrensesnitt, er usynlig for en agent. FDSAs krav om at applikasjoner avgir informasjon av felles verdi gjennom standardiserte grensesnitt (9.1) er det som gjør systemet ditt brukbart i en agentverden.</p>
<p><strong>Knytt dataene til begrepsapparatet.</strong> Forskjellen mellom 50 og 84,5 prosent riktige svar er ikke modellen, men om modellen vet hva feltene betyr. Hvert dataprodukt som registreres uten kobling til begreper og ontologi, er et dataprodukt agentene vil misforstå.</p>
<p><strong>Gi agenten sin egen identitet.</strong> Ikke la verktøyet ditt kjøre med brukerens pålogging. Knytt det til den felles identitetsløsningen som egen aktør med eget mandat, slik at tilgangen kan styres med attributter og handlingene kan spores. Det er dyrere å bygge om i ettertid enn å gjøre riktig fra start.</p>
<p><strong>Logg hvilke fakta som ga svaret.</strong> Opphav og sporbarhet (3.5) er ikke bare et revisjonskrav. Det er råvaren for evalueringssløyfen. Et svar uten spor tilbake til kildene kan ikke læres av.</p>
<h2>En analogi til slutt</h2>
<p>Artikkelen beskriver et jernbaneselskap som har lagt skinnene, bygget rutetabellen, og nå lar automatiske tog kjøre uten lokfører. I Forsvaret er den digitale reguleringsplanen (DRP) reguleringsplanen for hele området: hva som kan bygges, og hvilke regler som gjelder overalt. FDSA er sporplanen innenfor den: hvilken sporvidde som gjelder, hvor stasjonene ligger, og hvem som eier skinnene og rutetabellen.</p>
<p>Forskjellen er at vi i Forsvaret har planlagt for at skinnene kan bli sprengt, og det har ikke de. De har planlagt for at togene kjører seg selv, og det har ikke vi ennå. Jeg mener begge deler hører hjemme i den samme sporplanen, og at artikkelen er en god grunn til å ta det neste steget nå.</p>
<h2>Kilder og om produksjonen</h2>
<p class="src">
Ben Horowitz og Michael McGuiness: <a href="https://a16z.com/databricks-the-future-of-the-firm/" target="_blank" rel="noopener">Databricks & the Future of the Firm</a>, a16z.com, 27. oktober 2026.<br>
<a href="https://www.forsvaret.no/om-forsvaret/digital-transformasjon-i-forsvaret" target="_blank" rel="noopener">Digital transformasjon i Forsvaret</a>, forsvaret.no, med målbildet for Forsvarets datasentriske arkitektur (FDSA), Forsvarets skystrategi og Forsvarets digitale reguleringsplan (DRP) som nedlastbare dokumenter. FDSA trådte i kraft 5. oktober 2026.<br>
Innst. 440 S (2025–2026), jf. Prop. 68 S (2025–2026), vedtak 991 og 992.<br>
Alle tall om Databricks er artikkelens egne. Forfatterne er investorer i selskapet; tallene er ikke uavhengig verifisert her. Jeg har ingen tilknytning til Databricks eller a16z. Mappingen mot FDSA og figur 1 til 3 er min tolkning, ikke Forsvarets offisielle lesning.<br>
Notatet er skrevet med Claude fra Anthropic som arbeidsverktøy: lesing og sammenfatning av kildeartikkelen, mapping mot FDSA, oversettelse og teksthjelp. Figur 2 og 3 er tegnet med Archify. Vurderingene, redigeringen og ansvaret for innholdet er mine. Det er for øvrig et lite eksempel på artikkelens eget poeng: verktøyet leverte fart, men det er fagkunnskapen om FDSA som avgjorde hva som ble stående. Den engelske versjonen er en oversettelse av den norske; ved avvik gjelder den norske.
</p>
</div>
<!-- ===================== ENGLISH, part 3 ===================== -->
<div data-l="en" hidden>
<p>The authors call this "the sovereign firm": a firm that can switch vendors without losing what it has learned. Microsoft's Satya Nadella says something similar about "token capital", the AI capability a company builds and owns itself, and Nvidia's Jensen Huang calls a company's own data its gold mine.</p>
<p>This is, in civilian language, the core of FDSA's first foundation, data sovereignty, and of the Norwegian Parliament's decisions 991 and 992 of 2026 on national control over data and infrastructure. FDSA adds a third precondition the essay confirms indirectly: open formats. The lakehouse became possible because three companies independently landed on open table formats. Proprietary formats lock you in even within Norwegian jurisdiction.</p>
<blockquote>Ownership alone is not a moat, the authors say, but it keeps the firm's memory from being trapped in a vendor's stack. The advantage emerges when the data and the AI that reasons over it make the firm better faster than its competitors.</blockquote>
<p>Replace "competitors" with "adversary", and you have the Armed Forces' version.</p>
<h2>What this means in practice</h2>
<span class="aud">For developers, team leads and suppliers</span>
<p>If you are building something for the Norwegian Armed Forces today, from inside or as a supplier, there are four concrete consequences.</p>
<p><strong>Publish data as a product, not as a screen.</strong> The agents in the essay read and write directly against the record. A system that only delivers information through its own user interface is invisible to an agent. FDSA's requirement that applications expose information of shared value through standardised interfaces (9.1) is what makes your system usable in an agent world.</p>
<p><strong>Tie the data to the vocabulary.</strong> The difference between 50 and 84.5 percent correct answers is not the model, but whether the model knows what the fields mean. Every data product registered without a link to concepts and ontology is a data product the agents will misunderstand.</p>
<p><strong>Give the agent its own identity.</strong> Do not let your tool run on the user's login. Attach it to the common identity service as its own actor with its own mandate, so access can be governed by attributes and actions can be traced. Retrofitting is more expensive than getting it right from the start.</p>
<p><strong>Log which facts produced the answer.</strong> Provenance and lineage (3.5) are not just an audit requirement. They are the raw material of the evaluation loop. An answer with no trace back to its sources cannot be learned from.</p>
<h2>An analogy to close</h2>
<p>The essay describes a railway company that has laid the tracks, built the timetable, and now lets automatic trains run without a driver. In the Norwegian Armed Forces, the Digital Zoning Plan (DRP) is the zoning plan for the whole area: what may be built, and which rules apply everywhere. FDSA is the track plan within it: which gauge applies, where the stations are, and who owns the rails and the timetable.</p>
<p>The difference is that we in the Armed Forces have planned for the tracks being blown up, and they have not. They have planned for the trains driving themselves, and we have not yet. I believe both belong in the same track plan, and that the essay is a good reason to take the next step now.</p>
<h2>Sources and how this was made</h2>
<p class="src">
Ben Horowitz and Michael McGuiness: <a href="https://a16z.com/databricks-the-future-of-the-firm/" target="_blank" rel="noopener">Databricks & the Future of the Firm</a>, a16z.com, 27 October 2026.<br>
<a href="https://www.forsvaret.no/om-forsvaret/digital-transformasjon-i-forsvaret" target="_blank" rel="noopener">Digital transformation in the Norwegian Armed Forces</a>, forsvaret.no (in Norwegian), with the FDSA target architecture, the Armed Forces' cloud strategy and the Digital Zoning Plan (DRP) as downloadable documents. FDSA entered into force on 5 October 2026.<br>
Recommendation 440 S (2025–2026) to the Storting, cf. Proposition 68 S (2025–2026), decisions 991 and 992.<br>
All figures about Databricks are the essay's own. The authors are investors in the company; the figures have not been independently verified here. I have no affiliation with Databricks or a16z. The mapping to FDSA and figures 1 to 3 are my interpretation, not the Armed Forces' official reading.<br>
This note was written with Claude from Anthropic as a working tool: reading and summarising the source essay, mapping it against FDSA, translation and drafting help. Figures 2 and 3 were drawn with Archify. The assessments, the editing and the responsibility for the content are mine. It is, incidentally, a small example of the essay's own point: the tool delivered speed, but it was the domain knowledge of FDSA that decided what stayed in. This English version is a translation of the Norwegian original; where they differ, the Norwegian text applies.
</p>
</div>
</div>
<script>
var T={"01 / Aktører":"01 / Actors","02 / Hva er tillatt":"02 / What is allowed","03 / Hva er sant":"03 / What is true","04 / Hva betyr det":"04 / What does it mean","05 / Virket det":"05 / Did it work","Operatør":"Operator","spør, gir mandat":"asks, mandates","KI-agent":"AI agent","egen identitet":"own identity","ny aktør":"new actor","hvem spør, mandat":"who asks, mandate","2.4 og 3.1":"2.4 and 3.1","dataprodukter":"data products","FDDP-node":"FDDP node","Ontologi":"Ontology","gir data mening":"gives data meaning","7.1 og 7.3":"7.1 and 7.3","Sporbarhet":"Traceability","hva ga svaret":"evidence used","Svar og handling":"Answer and action","beslutning, bilde":"decision, picture","Evaluering":"Evaluation","korreksjon tilbake":"correction fed back","neste skritt":"next step","oppgave og mandat":"task and mandate","identitet":"identity","attributter":"attributes","tillatt spørring":"permitted query","data med begrep":"data with meaning","grunnet svar":"grounded reply","opphav":"origin","korreksjon":"correction","oppdatert mening":"updated meaning","Tegnforklaring":"Legend","Hovedflyt":"Main flow","Identitet og policy":"Identity and policy","Læringssløyfe":"Learning loop","Lager i FDDP":"Storage in FDDP","Ordinær flyt":"Ordinary flow","Samme modell leid":"Same model rented","to banker, samme prompt":"two banks, same prompt","Hva lagres?":"What is stored?","svaret, eller det bak svaret":"the answer, or its basis","Fakta og utfall":"Facts and outcomes","lagret og justert":"stored and adjusted","suveren":"sovereign","Ny modell kommer":"New model arrives","etter ett år":"after one year","Arver det lærte":"Inherits the learning","regler og saker":"rules and cases","varig fortrinn":"lasting advantage","Bare svaret lagret":"Only the answer stored","spørsmål inn, svar ut":"question in, answer out","leietaker":"tenant","Starter fra null":"Starts from zero","samme prompt som alle":"same prompt as everyone","tapt":"lost","Lagret hos leverandør":"Stored at the vendor","proprietært format":"proprietary format","innlåst":"locked in","Bytte betyr tap":"Switching means loss","hukommelsen blir igjen":"the memory stays behind","svar":"answer","det bak svaret":"its basis","bytt fritt":"switch freely","bare svaret":"only the answer","ett år":"one year","ingen arv":"no inheritance","hos leverandør":"at the vendor","vil bytte":"wants to switch","Utgangspunkt":"Starting point","Pågående praksis":"Ongoing practice","Ytre hendelse":"External event","Veivalg":"Choice","Varig fortrinn":"Lasting advantage","Tapt læring":"Lost learning","Sluttilstand":"End state"};
var TITLES={nb:"Når Silicon Valley kommer fram til Forsvarets datasentriske arkitektur (FDSA)",en:"When Silicon Valley arrives at the Norwegian Armed Forces' Data-Centric Architecture (FDSA)"};
function setLang(l){
l=(l==='en')?'en':'nb';
document.documentElement.lang=l;
document.title=TITLES[l];
var els=document.querySelectorAll('[data-l]');
for(var i=0;i<els.length;i++){els[i].hidden=(els[i].getAttribute('data-l')!==l);}
var bs=document.querySelectorAll('.lb');
for(var j=0;j<bs.length;j++){bs[j].className='lb'+(bs[j].getAttribute('data-set')===l?' on':'');}
var ts=document.querySelectorAll('svg.ar text');
for(var k=0;k<ts.length;k++){
var t=ts[k]; var no=t.getAttribute('data-no');
if(no===null){no=t.textContent;t.setAttribute('data-no',no);}
t.textContent=(l==='en'&&T[no])?T[no]:no;
}
try{if(history.replaceState){history.replaceState(null,'','#'+l);}}catch(e){}
}
(function(){
var h=(location.hash||'').replace('#','');
if(h==='en'||h==='nb'){setLang(h);return;}
var nl=(navigator.language||'').toLowerCase();
setLang(/^(nb|nn|no)/.test(nl)?'nb':'en');
})();
</script>
<script>
(function(){
var boxes=document.querySelectorAll('.arch-box');
function mode(b){return b.classList.contains('full')?'present':'embed';}
function send(b){var fs=b.querySelectorAll('iframe.arch');for(var i=0;i<fs.length;i++){if(fs[i].contentWindow)fs[i].contentWindow.postMessage('arch:'+mode(b),'*');}}
function setFull(b,on){b.classList.toggle('full',on);document.body.classList.toggle('arch-lock',on);send(b);
var t=b.querySelector(on?'.arch-close':'.arch-open');if(t&&t.offsetParent)t.focus();}
addEventListener('message',function(e){if(e.data!=='arch:ready'&&e.data!=='arch:exit')return;
for(var i=0;i<boxes.length;i++){var fs=boxes[i].querySelectorAll('iframe.arch');
for(var j=0;j<fs.length;j++){if(fs[j].contentWindow!==e.source)continue;
if(e.data==='arch:ready')e.source.postMessage('arch:'+mode(boxes[i]),'*');
else if(boxes[i].classList.contains('full'))setFull(boxes[i],false);}}});
for(var i=0;i<boxes.length;i++){(function(b){
b.querySelector('.arch-open').addEventListener('click',function(){setFull(b,true);});
b.querySelector('.arch-close').addEventListener('click',function(){setFull(b,false);});})(boxes[i]);}
addEventListener('keydown',function(e){if(e.key!=='Escape')return;for(var i=0;i<boxes.length;i++){if(boxes[i].classList.contains('full'))setFull(boxes[i],false);}});
})();
</script>
</body>
</html><script>(()=>{if(parent===window)return;let el=null,slim=false;const say=(s)=>{if(s!==slim){slim=s;parent.postMessage(s?'campfile:slim':'campfile:tall','*')}};addEventListener('scroll',(e)=>{const t=e.target===document?document.scrollingElement:e.target;if(!(t instanceof Element))return;if(t.scrollTop>8&&t.scrollHeight-t.clientHeight>64){el=t;say(true)}else if(t===el&&t.scrollTop<=0){el=null;say(false)}},{capture:true,passive:true});const touch=(e)=>{if(e.isTrusted&&!el)say(true)};addEventListener('click',touch,{capture:true,passive:true});addEventListener('keydown',touch,true)})()</script>