@@ -361,19 +361,26 @@ const formatPercent = (frac: number) => `${(frac * 100).toFixed(0)}%`;
361361
362362 <!-- Mermaid diagram, generated from the compiled LangGraph state
363363 machine. Source lives at site/src/content/orchestration/
364- v6_pipeline.mmd; the script tag below renders it on mount. -->
364+ v6_pipeline.mmd; the script tag below renders it on mount.
365+ The source is delivered via a `data-mermaid-source` attribute
366+ (not as text content) so Astro's CSS scoping doesn't trip
367+ over the rendered SVG, and the host div is replaced outright
368+ when the render succeeds. -->
365369 <div class =" mermaid-frame mb-10" >
366370 <p class =" mermaid-caption text-xs font-mono uppercase tracking-[0.18em] text-text-muted" >
367371 Compiled LangGraph state machine · v2.0.0
368372 </p >
369- <div class =" mermaid-host" role =" img" aria-label =" Pipeline state-machine diagram" >
370- <pre class =" mermaid" data-pipeline-source >{ mermaidSource } </pre >
371- <noscript >
372- <p class =" text-sm font-mono text-text-secondary" >
373- (Pipeline diagram requires JavaScript. The Mermaid source is
374- in the page; see docs/orchestration/v6_pipeline.mmd in the repo.)
375- </p >
376- </noscript >
373+ <div
374+ class =" mermaid-host"
375+ role =" img"
376+ aria-label =" Pipeline state-machine diagram"
377+ data-mermaid-source ={ mermaidSource }
378+ >
379+ <p class =" mermaid-fallback text-sm font-mono text-text-muted" >
380+ Loading diagram… (if this persists, see
381+ <code class =" font-mono text-text-primary" >docs/orchestration/v6_pipeline.mmd</code >
382+ in the repo for the source.)
383+ </p >
377384 </div >
378385 </div >
379386
@@ -848,7 +855,7 @@ const formatPercent = (frac: number) => `${(frac * 100).toFixed(0)}%`;
848855 line-height: 1.5;
849856 }
850857
851- /* ─────────────── Mermaid pipeline diagram ─────────────── */
858+ /* ─────────────── Mermaid pipeline diagram (frame) ─────────────── */
852859 .mermaid-frame {
853860 border: 1px solid var(--color-border-subtle);
854861 background: var(--color-bg-elevated);
@@ -859,27 +866,37 @@ const formatPercent = (frac: number) => `${(frac * 100).toFixed(0)}%`;
859866 }
860867 .mermaid-host {
861868 overflow-x: auto;
862- }
863- /* Hide the raw .mmd source until Mermaid replaces it with rendered
864- SVG; prevents a flash of un-rendered source on slow loads. */
865- .mermaid-host pre.mermaid:not([data-rendered]) {
866- visibility: hidden;
867869 min-height: 14rem;
870+ display: flex;
871+ align-items: center;
872+ justify-content: center;
868873 }
869- .mermaid-host pre.mermaid {
870- background: transparent;
871- border: 0;
872- padding: 0;
873- margin: 0;
874- font-family: var(--font-mono);
874+ .mermaid-host[data-rendered="true"] {
875+ display: block;
876+ min-height: 0;
875877 }
876- /* Mermaid emits inline fills on its rendered SVG; override aggressively
877- so the diagram matches the site's monochrome dark aesthetic without
878- the default purple/teal palette bleeding through. */
878+ .mermaid-fallback {
879+ text-align: center;
880+ padding: 1rem;
881+ }
882+ .mermaid-host[data-rendered] .mermaid-fallback {
883+ display: none;
884+ }
885+ </style >
886+
887+ <!-- Global styles for the runtime-injected Mermaid SVG. Must live
888+ outside the scoped <style> block above, because Astro's
889+ data-astro-cid attribute selector rewrite can't tag elements
890+ Mermaid generates at runtime — those nodes have no Astro CID, so
891+ scoped selectors silently miss every override. is:global keeps
892+ the rules untouched. -->
893+ <style is:global >
879894 .mermaid-host svg {
880895 background: transparent;
881896 max-width: 100%;
882897 height: auto;
898+ display: block;
899+ margin: 0 auto;
883900 }
884901 .mermaid-host svg .node rect,
885902 .mermaid-host svg .node polygon,
@@ -889,10 +906,12 @@ const formatPercent = (frac: number) => `${(frac * 100).toFixed(0)}%`;
889906 stroke: #363b46 !important;
890907 stroke-width: 1px !important;
891908 }
892- .mermaid-host svg .node .label {
909+ .mermaid-host svg .node .label,
910+ .mermaid-host svg .node text,
911+ .mermaid-host svg .nodeLabel {
893912 color: #e6e8eb !important;
894913 fill: #e6e8eb !important;
895- font-family: var(--font-mono) ;
914+ font-family: "JetBrains Mono", ui-monospace, monospace ;
896915 font-size: 0.78rem;
897916 }
898917 .mermaid-host svg .node.first rect,
@@ -920,11 +939,14 @@ const formatPercent = (frac: number) => `${(frac * 100).toFixed(0)}%`;
920939 background-color: #16191f !important;
921940 color: #b0bac6 !important;
922941 fill: #b0bac6 !important;
923- font-family: var(--font-mono) ;
942+ font-family: "JetBrains Mono", ui-monospace, monospace ;
924943 font-size: 0.7rem;
925944 }
926- .mermaid-host svg .edgeLabel rect {
945+ .mermaid-host svg .edgeLabel rect,
946+ .mermaid-host svg .edgeLabel foreignObject div {
947+ background-color: #16191f !important;
927948 fill: #16191f !important;
949+ color: #b0bac6 !important;
928950 }
929951 .mermaid-host svg .cluster rect {
930952 fill: #1c2027 !important;
@@ -1019,26 +1041,38 @@ const formatPercent = (frac: number) => `${(frac * 100).toFixed(0)}%`;
10191041
10201042<script >
10211043 // Render the LangGraph-generated Mermaid source into an SVG on mount.
1022- // Mermaid is lazy-loaded via dynamic import so the ~600 KB bundle
1023- // doesn't sit on the homepage's critical path; it only fetches once
1024- // the page has settled and the host element is in view. Theme
1025- // variables match the site's monochrome dark aesthetic; CSS in the
1026- // scoped <style> block above pins the SVG fills/strokes defensively
1027- // in case Mermaid's inline styles bleed through.
1044+ // Mermaid is lazy-loaded via dynamic import on intersection so the
1045+ // ~600 KB bundle doesn't sit on the homepage's critical path. Source
1046+ // is read from the host's data-mermaid-source attribute (not text
1047+ // content) so Astro's HTML escaping doesn't double-escape the
1048+ // <p>...</p> labels in the .mmd file. The rendered SVG replaces the
1049+ // host's children outright so Astro's scoped CSS doesn't try to
1050+ // attribute-tag elements Mermaid injected at runtime.
10281051
10291052 async function renderPipelineDiagram() {
1030- const host = document.querySelector<HTMLPreElement >(
1031- "pre .mermaid[data-pipeline -source]",
1053+ const host = document.querySelector<HTMLDivElement >(
1054+ "div .mermaid-host [data-mermaid -source]",
10321055 );
10331056 if (!host) return;
1034- const source = host.textContent ?? "";
1057+ const source = host.getAttribute("data-mermaid-source") ?? "";
10351058 if (!source.trim()) return;
10361059
1037- const { default: mermaid } = await import("mermaid");
1060+ let mermaid;
1061+ try {
1062+ ({ default: mermaid } = await import("mermaid"));
1063+ } catch (err) {
1064+ console.error("Mermaid import failed:", err);
1065+ return;
1066+ }
1067+
10381068 mermaid.initialize({
10391069 startOnLoad: false,
10401070 theme: "base",
1041- securityLevel: "strict",
1071+ // Default "strict" security level forces label sanitisation that
1072+ // mishandles the <p>__start__</p> wrappers LangGraph emits;
1073+ // "loose" preserves the labels and is safe here because the
1074+ // Mermaid source is a build-time artefact, not user input.
1075+ securityLevel: "loose",
10421076 themeVariables: {
10431077 primaryColor: "#16191f",
10441078 primaryTextColor: "#e6e8eb",
@@ -1058,23 +1092,23 @@ const formatPercent = (frac: number) => `${(frac * 100).toFixed(0)}%`;
10581092 });
10591093
10601094 try {
1061- const { svg } = await mermaid.render("attestloop-pipeline", source);
1095+ const { svg } = await mermaid.render("attestloop-pipeline-svg ", source);
10621096 host.innerHTML = svg;
10631097 host.setAttribute("data-rendered", "true");
10641098 } catch (err) {
1065- // If Mermaid fails (rare; usually a syntax issue), surface the
1066- // source as fallback rather than leaving a blank box.
10671099 console.error("Mermaid render failed:", err);
10681100 host.setAttribute("data-rendered", "error");
1069- host.style.visibility = "visible";
1101+ const fallback = host.querySelector<HTMLElement>(".mermaid-fallback");
1102+ if (fallback) {
1103+ fallback.textContent =
1104+ "Pipeline diagram failed to render. The Mermaid source is at docs/orchestration/v6_pipeline.mmd in the repo.";
1105+ }
10701106 }
10711107 }
10721108
1073- // Defer until the diagram approaches the viewport so the Mermaid
1074- // bundle doesn't compete with first paint.
10751109 function scheduleRender() {
1076- const host = document.querySelector<HTMLPreElement >(
1077- "pre .mermaid[data-pipeline -source]",
1110+ const host = document.querySelector<HTMLDivElement >(
1111+ "div .mermaid-host [data-mermaid -source]",
10781112 );
10791113 if (!host) return;
10801114 if ("IntersectionObserver" in window) {
0 commit comments