Merge "Add dark mode support to Gitiles"
diff --git a/external_deps.lock.json b/external_deps.lock.json
index 36df3f9..1c1f65b 100755
--- a/external_deps.lock.json
+++ b/external_deps.lock.json
@@ -39,19 +39,19 @@
     "org.apache.commons:commons-text": -1491671838,
     "org.apache.httpcomponents:httpclient": 1643860045,
     "org.apache.httpcomponents:httpcore": -1696303652,
-    "org.apache.sshd:sshd-osgi": 1681538032,
-    "org.apache.sshd:sshd-sftp": 2089761863,
+    "org.apache.sshd:sshd-osgi": -115413327,
+    "org.apache.sshd:sshd-sftp": 292810504,
     "org.assertj:assertj-core": -1145412507,
-    "org.bouncycastle:bcpg-jdk18on": -1572213535,
-    "org.bouncycastle:bcpkix-jdk18on": 146639060,
-    "org.bouncycastle:bcprov-jdk18on": -1405390253,
-    "org.bouncycastle:bcutil-jdk18on": -469511060,
+    "org.bouncycastle:bcpg-jdk18on": 915299298,
+    "org.bouncycastle:bcpkix-jdk18on": -1660815403,
+    "org.bouncycastle:bcprov-jdk18on": -1559819624,
+    "org.bouncycastle:bcutil-jdk18on": 2018001773,
     "org.commonmark:commonmark": 1129543740,
     "org.commonmark:commonmark-ext-autolink": -1853742120,
     "org.commonmark:commonmark-ext-gfm-strikethrough": 350394231,
     "org.commonmark:commonmark-ext-gfm-tables": 1881582931,
     "org.commonmark:commonmark-ext-yaml-front-matter": -1519651186,
-    "org.eclipse.jetty.ee10:jetty-ee10-servlet": 835495267,
+    "org.eclipse.jetty.ee11:jetty-ee11-servlet": 1925464069,
     "org.eclipse.jetty.ee8:jetty-ee8-nested": 1867536598,
     "org.eclipse.jetty.ee8:jetty-ee8-security": -374913491,
     "org.eclipse.jetty.ee8:jetty-ee8-servlet": 894276934,
@@ -61,7 +61,7 @@
     "org.eclipse.jetty:jetty-server": 1477679957,
     "org.eclipse.jetty:jetty-session": 1736408150,
     "org.eclipse.jetty:jetty-util": 1880514486,
-    "org.eclipse.jetty:jetty-util-ajax": -622085534,
+    "org.eclipse.jetty:jetty-util-ajax": 57972836,
     "org.hamcrest:hamcrest": 1547523135,
     "org.mockito:mockito-core": -133446801,
     "org.nibor.autolink:autolink": -342487050,
@@ -182,24 +182,24 @@
     "org.apache.httpcomponents:httpclient:jar:sources": -779528763,
     "org.apache.httpcomponents:httpcore": 67453319,
     "org.apache.httpcomponents:httpcore:jar:sources": 2134002822,
-    "org.apache.sshd:sshd-common": -1308783553,
-    "org.apache.sshd:sshd-common:jar:sources": 203202718,
-    "org.apache.sshd:sshd-core": 576544003,
-    "org.apache.sshd:sshd-core:jar:sources": 1066552279,
-    "org.apache.sshd:sshd-osgi": -880103583,
-    "org.apache.sshd:sshd-osgi:jar:sources": -2061003935,
-    "org.apache.sshd:sshd-sftp": -86158263,
-    "org.apache.sshd:sshd-sftp:jar:sources": 1034555480,
+    "org.apache.sshd:sshd-common": -461927487,
+    "org.apache.sshd:sshd-common:jar:sources": 402940425,
+    "org.apache.sshd:sshd-core": 1923342694,
+    "org.apache.sshd:sshd-core:jar:sources": -238465986,
+    "org.apache.sshd:sshd-osgi": 2100718013,
+    "org.apache.sshd:sshd-osgi:jar:sources": 710209222,
+    "org.apache.sshd:sshd-sftp": 805382931,
+    "org.apache.sshd:sshd-sftp:jar:sources": -1268961386,
     "org.assertj:assertj-core": 1683037634,
     "org.assertj:assertj-core:jar:sources": -697161745,
-    "org.bouncycastle:bcpg-jdk18on": -1258294405,
-    "org.bouncycastle:bcpg-jdk18on:jar:sources": -1733408473,
-    "org.bouncycastle:bcpkix-jdk18on": -294727450,
-    "org.bouncycastle:bcpkix-jdk18on:jar:sources": 1412420619,
-    "org.bouncycastle:bcprov-jdk18on": 1743462207,
-    "org.bouncycastle:bcprov-jdk18on:jar:sources": 1198104999,
-    "org.bouncycastle:bcutil-jdk18on": 968717615,
-    "org.bouncycastle:bcutil-jdk18on:jar:sources": -1508579276,
+    "org.bouncycastle:bcpg-jdk18on": 1314692470,
+    "org.bouncycastle:bcpg-jdk18on:jar:sources": -1476258101,
+    "org.bouncycastle:bcpkix-jdk18on": -1562281305,
+    "org.bouncycastle:bcpkix-jdk18on:jar:sources": 1945286569,
+    "org.bouncycastle:bcprov-jdk18on": -390275162,
+    "org.bouncycastle:bcprov-jdk18on:jar:sources": -2118706434,
+    "org.bouncycastle:bcutil-jdk18on": -1194874921,
+    "org.bouncycastle:bcutil-jdk18on:jar:sources": -1005159791,
     "org.checkerframework:checker-compat-qual": -1467964223,
     "org.checkerframework:checker-compat-qual:jar:sources": 187825033,
     "org.commonmark:commonmark": 965353610,
@@ -212,8 +212,8 @@
     "org.commonmark:commonmark-ext-yaml-front-matter": 1918089254,
     "org.commonmark:commonmark-ext-yaml-front-matter:jar:sources": -902462472,
     "org.commonmark:commonmark:jar:sources": -1511261547,
-    "org.eclipse.jetty.ee10:jetty-ee10-servlet": 349394374,
-    "org.eclipse.jetty.ee10:jetty-ee10-servlet:jar:sources": 501419503,
+    "org.eclipse.jetty.ee11:jetty-ee11-servlet": 1455490102,
+    "org.eclipse.jetty.ee11:jetty-ee11-servlet:jar:sources": -873060978,
     "org.eclipse.jetty.ee8:jetty-ee8-nested": -65830655,
     "org.eclipse.jetty.ee8:jetty-ee8-nested:jar:sources": -2141029892,
     "org.eclipse.jetty.ee8:jetty-ee8-security": -1787377969,
@@ -233,8 +233,8 @@
     "org.eclipse.jetty:jetty-session": 602757569,
     "org.eclipse.jetty:jetty-session:jar:sources": -1630590830,
     "org.eclipse.jetty:jetty-util": 1084512308,
-    "org.eclipse.jetty:jetty-util-ajax": -1660665108,
-    "org.eclipse.jetty:jetty-util-ajax:jar:sources": 455294676,
+    "org.eclipse.jetty:jetty-util-ajax": 889071774,
+    "org.eclipse.jetty:jetty-util-ajax:jar:sources": -884639814,
     "org.eclipse.jetty:jetty-util:jar:sources": -280937557,
     "org.hamcrest:hamcrest": 1282317766,
     "org.hamcrest:hamcrest-core": 649657847,
@@ -643,31 +643,31 @@
     },
     "org.apache.sshd:sshd-common": {
       "shasums": {
-        "jar": "f1e33994601b5acc4cbcf993271e9411b4231d921cc72a0ac40544cbb71ae06c",
-        "sources": "945b184ba2cc49144e7e98b3c23160330c1bea0c470d2a642967736aaab8f013"
+        "jar": "41cef234f894e435ae2141e24baa25baf238a273cd28c6bbadcf830b1b2d68cb",
+        "sources": "c0b0a66452b70e6e3195665bf17a28edcd4ddf2f49801fd4732a7c60e49e9d92"
       },
-      "version": "2.18.0"
+      "version": "2.19.0"
     },
     "org.apache.sshd:sshd-core": {
       "shasums": {
-        "jar": "5016bd515903b9c14d18f0ee8f4e1b65f228f794f64f67344687224e804c4318",
-        "sources": "0ffeed88040dc12a17bd6069d33fda2c5ac87bb69acdf5b6576654abed42be12"
+        "jar": "7215350bf71d95a34494f823a7451f966a2995b59e99fdf8ee9cbd63ef78cdb3",
+        "sources": "c1ced96338bb28821cfb63f86e4cd4a8c2b8b8ec078f85d0b7f91d4864dde0e0"
       },
-      "version": "2.18.0"
+      "version": "2.19.0"
     },
     "org.apache.sshd:sshd-osgi": {
       "shasums": {
-        "jar": "e771c4239b1c4818c102368d57d0cc082c224eefb76224524473bf7021d1a32f",
-        "sources": "dd5838716293c8aeaf9487c50c6b313dff304e7ddca84ce16923f73865ba4d1d"
+        "jar": "0b6b9371c6a97430390b84c23c58f4470927a747e74704a0ca7d19a09a66fe37",
+        "sources": "d11b69618dba155f075571cbd9b21741953c27dfa9a1af09b4024f2183226abe"
       },
-      "version": "2.18.0"
+      "version": "2.19.0"
     },
     "org.apache.sshd:sshd-sftp": {
       "shasums": {
-        "jar": "bf52fd81d63337cb40050c70ee460c80e78a0a4b77f15da7125eb1841f527e93",
-        "sources": "5171d9bac4ee76a013beccc1fdeab93fb43aeb84172d19b929ee158b9281c648"
+        "jar": "a3ad30d2feba9fad589c7ad9117fcd9d332823697d59eb424fe2902f4d805575",
+        "sources": "e26b7df972cead5c529bca6ab3e782c27dd43b323f9a54f98d9e23d4f5d03c30"
       },
-      "version": "2.18.0"
+      "version": "2.19.0"
     },
     "org.assertj:assertj-core": {
       "shasums": {
@@ -678,31 +678,31 @@
     },
     "org.bouncycastle:bcpg-jdk18on": {
       "shasums": {
-        "jar": "c0e6303a0d7589040f400950ecee87a14b81312e84ed15e5390ebb0c4566ddab",
-        "sources": "a8baa033c57614d36c3d2339a8c8e5902a8a2ed8cb7387cdb2b919e5a4b15f30"
+        "jar": "39426367dc247dfaae9b1253457a39298b6b9a6c28a39b908bca829e315323dd",
+        "sources": "605b09bd826d7e4e3e0cb4faf2e9b53a6bcb452be963c91499b79fd8497685be"
       },
-      "version": "1.84"
+      "version": "1.85"
     },
     "org.bouncycastle:bcpkix-jdk18on": {
       "shasums": {
-        "jar": "c87f16ed9e5ec61bc94151e9f3646ac44e50cd448121ce84367fa4b7ec7ec1bb",
-        "sources": "fe00c12243c28ead30ad6c7742be40ff005ab29f493c350b83b637fe4a9b5597"
+        "jar": "c9f82b2d4e99c4bbdfccf684e52cc06ea06a0b567bfd0d08f9c5a3f417055996",
+        "sources": "e5331f467331aba29bda6ddfb0df0da6d568928e29c2b0f20ea2fe123d802d20"
       },
-      "version": "1.84"
+      "version": "1.85"
     },
     "org.bouncycastle:bcprov-jdk18on": {
       "shasums": {
-        "jar": "64d6c5a6121fcd927152dd182cbed39afe0fda641a970d9bcc0c9cb1858b2731",
-        "sources": "e5f04550f7740e588edcbd1654c59277cd7ee8725d8b674e44f7f8f4b9c5674a"
+        "jar": "986b0fb92ec10e0c66b43e036ce0077e6150cfaecd1db9fb92b56672e157afe5",
+        "sources": "b37ac84b1d5435ab7b8d166c16ab9f75e09f68f8ec50479bae433939b241b03f"
       },
-      "version": "1.84"
+      "version": "1.85.2"
     },
     "org.bouncycastle:bcutil-jdk18on": {
       "shasums": {
-        "jar": "b374e16963421fb9cfb01cc20d7ad8fd2f8b8188e3eef0ec0a8965e245f7619a",
-        "sources": "192b719273dc33e8fd6edc3b30b126760b6740cf2e1ac3cc7cf845c7ffec9f2b"
+        "jar": "590f55ed5d68529239898a4a5c4f730b6e37f45d1cfa3fbe51f8485abe32c42d",
+        "sources": "b470a692878f92abf00b9c3af9147a45453250a80930df8f23f1d092c55e2d5e"
       },
-      "version": "1.84"
+      "version": "1.85"
     },
     "org.checkerframework:checker-compat-qual": {
       "shasums": {
@@ -746,12 +746,12 @@
       },
       "version": "0.24.0"
     },
-    "org.eclipse.jetty.ee10:jetty-ee10-servlet": {
+    "org.eclipse.jetty.ee11:jetty-ee11-servlet": {
       "shasums": {
-        "jar": "05cccc74bbe1478ceb765e967b1cb1a3c8d9b51e99acf586190887e467ffd87d",
-        "sources": "51a734b4bc04235b24ccd759515dd2947e5b675c227c556f02262b765bce47eb"
+        "jar": "af76ee845345f184d96d8f6c926e64f8bda4cf858013598d392ad604d37e6bac",
+        "sources": "0daaa96e80d48f516c580b7f18bcdf2ebdea75654a7fddb1a1a933af36ae2d16"
       },
-      "version": "12.1.10"
+      "version": "12.1.12"
     },
     "org.eclipse.jetty.ee8:jetty-ee8-nested": {
       "shasums": {
@@ -825,10 +825,10 @@
     },
     "org.eclipse.jetty:jetty-util-ajax": {
       "shasums": {
-        "jar": "73dfbf388b46c9e2afbc78823ea556f8d25cf851902f99873d03df90becc2b97",
-        "sources": "4617309092393ed2f6ed76d915f24b518ccbff8c8731124bf109e346607e86b3"
+        "jar": "ca4d49183b8f92af96c04dab7a40e70cc2ebac554e6de010e1b9a983ae342cb2",
+        "sources": "41bcaeb259a63a898885dd05bfa6b8231051079e38140bbb4f18181a9037b319"
       },
-      "version": "12.1.10"
+      "version": "12.1.12"
     },
     "org.hamcrest:hamcrest": {
       "shasums": {
@@ -1101,7 +1101,7 @@
     "org.commonmark:commonmark-ext-yaml-front-matter": [
       "org.commonmark:commonmark"
     ],
-    "org.eclipse.jetty.ee10:jetty-ee10-servlet": [
+    "org.eclipse.jetty.ee11:jetty-ee11-servlet": [
       "jakarta.servlet:jakarta.servlet-api",
       "org.eclipse.jetty:jetty-security",
       "org.eclipse.jetty:jetty-server",
@@ -2124,19 +2124,20 @@
       "org.bouncycastle.openpgp.api.jcajce",
       "org.bouncycastle.openpgp.api.util",
       "org.bouncycastle.openpgp.bc",
-      "org.bouncycastle.openpgp.examples",
       "org.bouncycastle.openpgp.jcajce",
       "org.bouncycastle.openpgp.operator",
       "org.bouncycastle.openpgp.operator.bc",
       "org.bouncycastle.openpgp.operator.jcajce"
     ],
     "org.bouncycastle:bcpkix-jdk18on": [
+      "org.bouncycastle.cades",
       "org.bouncycastle.cert",
       "org.bouncycastle.cert.bc",
       "org.bouncycastle.cert.cmp",
       "org.bouncycastle.cert.crmf",
       "org.bouncycastle.cert.crmf.bc",
       "org.bouncycastle.cert.crmf.jcajce",
+      "org.bouncycastle.cert.ct",
       "org.bouncycastle.cert.dane",
       "org.bouncycastle.cert.dane.fetcher",
       "org.bouncycastle.cert.jcajce",
@@ -2144,6 +2145,9 @@
       "org.bouncycastle.cert.ocsp.jcajce",
       "org.bouncycastle.cert.path",
       "org.bouncycastle.cert.path.validations",
+      "org.bouncycastle.cert.plants",
+      "org.bouncycastle.cert.plants.bc",
+      "org.bouncycastle.cert.plants.jcajce",
       "org.bouncycastle.cert.selector",
       "org.bouncycastle.cert.selector.jcajce",
       "org.bouncycastle.cmc",
@@ -2175,6 +2179,7 @@
       "org.bouncycastle.pkcs",
       "org.bouncycastle.pkcs.bc",
       "org.bouncycastle.pkcs.jcajce",
+      "org.bouncycastle.pkcs.util",
       "org.bouncycastle.pkix",
       "org.bouncycastle.pkix.jcajce",
       "org.bouncycastle.pkix.util",
@@ -2191,9 +2196,11 @@
       "org.bouncycastle.asn1.bc",
       "org.bouncycastle.asn1.cryptopro",
       "org.bouncycastle.asn1.gm",
+      "org.bouncycastle.asn1.iana",
       "org.bouncycastle.asn1.nist",
       "org.bouncycastle.asn1.ocsp",
       "org.bouncycastle.asn1.pkcs",
+      "org.bouncycastle.asn1.plants",
       "org.bouncycastle.asn1.sec",
       "org.bouncycastle.asn1.teletrust",
       "org.bouncycastle.asn1.ua",
@@ -2209,14 +2216,15 @@
       "org.bouncycastle.crypto.agreement.ecjpake",
       "org.bouncycastle.crypto.agreement.jpake",
       "org.bouncycastle.crypto.agreement.kdf",
+      "org.bouncycastle.crypto.agreement.owl",
       "org.bouncycastle.crypto.agreement.srp",
+      "org.bouncycastle.crypto.bls",
       "org.bouncycastle.crypto.commitments",
       "org.bouncycastle.crypto.constraints",
       "org.bouncycastle.crypto.digests",
       "org.bouncycastle.crypto.ec",
       "org.bouncycastle.crypto.encodings",
       "org.bouncycastle.crypto.engines",
-      "org.bouncycastle.crypto.examples",
       "org.bouncycastle.crypto.fpe",
       "org.bouncycastle.crypto.generators",
       "org.bouncycastle.crypto.hash2curve",
@@ -2225,6 +2233,8 @@
       "org.bouncycastle.crypto.hpke",
       "org.bouncycastle.crypto.io",
       "org.bouncycastle.crypto.kems",
+      "org.bouncycastle.crypto.kems.cmce",
+      "org.bouncycastle.crypto.kems.frodo",
       "org.bouncycastle.crypto.kems.mlkem",
       "org.bouncycastle.crypto.macs",
       "org.bouncycastle.crypto.modes",
@@ -2243,14 +2253,12 @@
       "org.bouncycastle.crypto.util",
       "org.bouncycastle.i18n",
       "org.bouncycastle.i18n.filter",
-      "org.bouncycastle.iana",
       "org.bouncycastle.internal.asn1.bsi",
       "org.bouncycastle.internal.asn1.cms",
       "org.bouncycastle.internal.asn1.cryptlib",
       "org.bouncycastle.internal.asn1.eac",
       "org.bouncycastle.internal.asn1.edec",
       "org.bouncycastle.internal.asn1.gnu",
-      "org.bouncycastle.internal.asn1.iana",
       "org.bouncycastle.internal.asn1.isara",
       "org.bouncycastle.internal.asn1.isismtt",
       "org.bouncycastle.internal.asn1.iso",
@@ -2265,6 +2273,8 @@
       "org.bouncycastle.jcajce.interfaces",
       "org.bouncycastle.jcajce.io",
       "org.bouncycastle.jcajce.provider.asymmetric",
+      "org.bouncycastle.jcajce.provider.asymmetric.cmce",
+      "org.bouncycastle.jcajce.provider.asymmetric.compositekem",
       "org.bouncycastle.jcajce.provider.asymmetric.compositesignatures",
       "org.bouncycastle.jcajce.provider.asymmetric.dh",
       "org.bouncycastle.jcajce.provider.asymmetric.dsa",
@@ -2274,6 +2284,7 @@
       "org.bouncycastle.jcajce.provider.asymmetric.ecgost12",
       "org.bouncycastle.jcajce.provider.asymmetric.edec",
       "org.bouncycastle.jcajce.provider.asymmetric.elgamal",
+      "org.bouncycastle.jcajce.provider.asymmetric.frodokem",
       "org.bouncycastle.jcajce.provider.asymmetric.gost",
       "org.bouncycastle.jcajce.provider.asymmetric.ies",
       "org.bouncycastle.jcajce.provider.asymmetric.mldsa",
@@ -2319,46 +2330,64 @@
       "org.bouncycastle.math.raw",
       "org.bouncycastle.pqc.asn1",
       "org.bouncycastle.pqc.crypto",
+      "org.bouncycastle.pqc.crypto.aimer",
       "org.bouncycastle.pqc.crypto.cmce",
       "org.bouncycastle.pqc.crypto.crystals.dilithium",
+      "org.bouncycastle.pqc.crypto.faest",
       "org.bouncycastle.pqc.crypto.falcon",
       "org.bouncycastle.pqc.crypto.frodo",
+      "org.bouncycastle.pqc.crypto.haetae",
+      "org.bouncycastle.pqc.crypto.hawk",
       "org.bouncycastle.pqc.crypto.hqc",
       "org.bouncycastle.pqc.crypto.lms",
       "org.bouncycastle.pqc.crypto.mayo",
       "org.bouncycastle.pqc.crypto.mldsa",
       "org.bouncycastle.pqc.crypto.mlkem",
+      "org.bouncycastle.pqc.crypto.mqom",
       "org.bouncycastle.pqc.crypto.newhope",
       "org.bouncycastle.pqc.crypto.ntru",
       "org.bouncycastle.pqc.crypto.ntruplus",
       "org.bouncycastle.pqc.crypto.ntruprime",
+      "org.bouncycastle.pqc.crypto.qruov",
       "org.bouncycastle.pqc.crypto.saber",
+      "org.bouncycastle.pqc.crypto.sdith",
       "org.bouncycastle.pqc.crypto.slhdsa",
       "org.bouncycastle.pqc.crypto.snova",
       "org.bouncycastle.pqc.crypto.sphincs",
+      "org.bouncycastle.pqc.crypto.sqisign",
+      "org.bouncycastle.pqc.crypto.uov",
       "org.bouncycastle.pqc.crypto.util",
       "org.bouncycastle.pqc.crypto.xmss",
       "org.bouncycastle.pqc.crypto.xwing",
       "org.bouncycastle.pqc.jcajce.interfaces",
       "org.bouncycastle.pqc.jcajce.provider",
+      "org.bouncycastle.pqc.jcajce.provider.aimer",
       "org.bouncycastle.pqc.jcajce.provider.bike",
       "org.bouncycastle.pqc.jcajce.provider.cmce",
       "org.bouncycastle.pqc.jcajce.provider.dilithium",
+      "org.bouncycastle.pqc.jcajce.provider.faest",
       "org.bouncycastle.pqc.jcajce.provider.falcon",
       "org.bouncycastle.pqc.jcajce.provider.frodo",
+      "org.bouncycastle.pqc.jcajce.provider.haetae",
+      "org.bouncycastle.pqc.jcajce.provider.hawk",
       "org.bouncycastle.pqc.jcajce.provider.hqc",
       "org.bouncycastle.pqc.jcajce.provider.kyber",
       "org.bouncycastle.pqc.jcajce.provider.lms",
       "org.bouncycastle.pqc.jcajce.provider.mayo",
+      "org.bouncycastle.pqc.jcajce.provider.mqom",
       "org.bouncycastle.pqc.jcajce.provider.newhope",
       "org.bouncycastle.pqc.jcajce.provider.ntru",
       "org.bouncycastle.pqc.jcajce.provider.ntruplus",
       "org.bouncycastle.pqc.jcajce.provider.ntruprime",
       "org.bouncycastle.pqc.jcajce.provider.picnic",
+      "org.bouncycastle.pqc.jcajce.provider.qruov",
       "org.bouncycastle.pqc.jcajce.provider.saber",
+      "org.bouncycastle.pqc.jcajce.provider.sdith",
       "org.bouncycastle.pqc.jcajce.provider.snova",
       "org.bouncycastle.pqc.jcajce.provider.sphincs",
       "org.bouncycastle.pqc.jcajce.provider.sphincsplus",
+      "org.bouncycastle.pqc.jcajce.provider.sqisign",
+      "org.bouncycastle.pqc.jcajce.provider.uov",
       "org.bouncycastle.pqc.jcajce.provider.util",
       "org.bouncycastle.pqc.jcajce.provider.xmss",
       "org.bouncycastle.pqc.jcajce.spec",
@@ -2392,7 +2421,6 @@
       "org.bouncycastle.asn1.ess",
       "org.bouncycastle.asn1.est",
       "org.bouncycastle.asn1.gnu",
-      "org.bouncycastle.asn1.iana",
       "org.bouncycastle.asn1.icao",
       "org.bouncycastle.asn1.isara",
       "org.bouncycastle.asn1.isismtt",
@@ -2464,14 +2492,14 @@
       "org.commonmark.ext.front.matter",
       "org.commonmark.ext.front.matter.internal"
     ],
-    "org.eclipse.jetty.ee10:jetty-ee10-servlet": [
-      "org.eclipse.jetty.ee10.servlet",
-      "org.eclipse.jetty.ee10.servlet.internal",
-      "org.eclipse.jetty.ee10.servlet.jmx",
-      "org.eclipse.jetty.ee10.servlet.listener",
-      "org.eclipse.jetty.ee10.servlet.security",
-      "org.eclipse.jetty.ee10.servlet.security.authentication",
-      "org.eclipse.jetty.ee10.servlet.util"
+    "org.eclipse.jetty.ee11:jetty-ee11-servlet": [
+      "org.eclipse.jetty.ee11.servlet",
+      "org.eclipse.jetty.ee11.servlet.internal",
+      "org.eclipse.jetty.ee11.servlet.jmx",
+      "org.eclipse.jetty.ee11.servlet.listener",
+      "org.eclipse.jetty.ee11.servlet.security",
+      "org.eclipse.jetty.ee11.servlet.security.authentication",
+      "org.eclipse.jetty.ee11.servlet.util"
     ],
     "org.eclipse.jetty.ee8:jetty-ee8-nested": [
       "org.eclipse.jetty.ee8.nested",
@@ -2839,8 +2867,8 @@
       "org.commonmark:commonmark-ext-yaml-front-matter",
       "org.commonmark:commonmark-ext-yaml-front-matter:jar:sources",
       "org.commonmark:commonmark:jar:sources",
-      "org.eclipse.jetty.ee10:jetty-ee10-servlet",
-      "org.eclipse.jetty.ee10:jetty-ee10-servlet:jar:sources",
+      "org.eclipse.jetty.ee11:jetty-ee11-servlet",
+      "org.eclipse.jetty.ee11:jetty-ee11-servlet:jar:sources",
       "org.eclipse.jetty.ee8:jetty-ee8-nested",
       "org.eclipse.jetty.ee8:jetty-ee8-nested:jar:sources",
       "org.eclipse.jetty.ee8:jetty-ee8-security",
diff --git a/java/com/google/gitiles/blame/cache/BlameCacheImpl.java b/java/com/google/gitiles/blame/cache/BlameCacheImpl.java
index 2d831a4..ba21aaa 100644
--- a/java/com/google/gitiles/blame/cache/BlameCacheImpl.java
+++ b/java/com/google/gitiles/blame/cache/BlameCacheImpl.java
@@ -20,6 +20,7 @@
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Interner;
 import com.google.common.collect.Interners;
 import com.google.common.collect.Lists;
@@ -29,6 +30,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import org.eclipse.jgit.blame.BlameGenerator;
@@ -56,10 +58,16 @@
   public static class Key {
     private final ObjectId commitId;
     private final String path;
+    private final ImmutableSet<ObjectId> ignoreIds;
 
     public Key(ObjectId commitId, String path) {
+      this(commitId, path, ImmutableSet.of());
+    }
+
+    public Key(ObjectId commitId, String path, Set<ObjectId> ignoreIds) {
       this.commitId = commitId;
       this.path = path;
+      this.ignoreIds = ignoreIds != null ? ImmutableSet.copyOf(ignoreIds) : ImmutableSet.of();
     }
 
     public ObjectId getCommitId() {
@@ -70,23 +78,44 @@
       return path;
     }
 
+    public ImmutableSet<ObjectId> getIgnoreIds() {
+      return ignoreIds;
+    }
+
     @Override
     public boolean equals(Object o) {
       if (o instanceof Key) {
         Key k = (Key) o;
-        return Objects.equals(commitId, k.commitId) && Objects.equals(path, k.path);
+        return Objects.equals(commitId, k.commitId)
+            && Objects.equals(path, k.path)
+            && Objects.equals(ignoreIds, k.ignoreIds);
       }
       return false;
     }
 
     @Override
     public int hashCode() {
-      return hash(commitId, path);
+      return hash(commitId, path, ignoreIds);
     }
 
     @Override
     public String toString() {
-      return commitId.name() + ":" + QuotedString.GIT_PATH.quote(path);
+      if (ignoreIds.isEmpty()) {
+        return commitId.name() + ":" + QuotedString.GIT_PATH.quote(path);
+      }
+      StringBuilder sb = new StringBuilder();
+      sb.append(commitId.name()).append(':').append(QuotedString.GIT_PATH.quote(path));
+      sb.append(" ignore=[");
+      boolean first = true;
+      for (ObjectId id : ignoreIds.stream().sorted().toList()) {
+        if (!first) {
+          sb.append(", ");
+        }
+        sb.append(id.name());
+        first = false;
+      }
+      sb.append(']');
+      return sb.toString();
     }
   }
 
diff --git a/java/com/google/gitiles/doc/MarkdownConfig.java b/java/com/google/gitiles/doc/MarkdownConfig.java
index 53d979a..d16380a 100644
--- a/java/com/google/gitiles/doc/MarkdownConfig.java
+++ b/java/com/google/gitiles/doc/MarkdownConfig.java
@@ -53,6 +53,7 @@
   final boolean strikethrough;
   final boolean tables;
   final boolean toc;
+  final boolean mermaid;
 
   private final boolean allowAnyIFrame;
   private final ImmutableList<String> allowIFrame;
@@ -75,6 +76,7 @@
     strikethrough = cfg.getBoolean("markdown", "strikethrough", githubFlavor);
     tables = cfg.getBoolean("markdown", "tables", githubFlavor);
     toc = cfg.getBoolean("markdown", "toc", true);
+    mermaid = cfg.getBoolean("markdown", "mermaid", githubFlavor);
 
     String[] f = {};
     if (safeHtml) {
@@ -105,6 +107,7 @@
     strikethrough = on("strikethrough", p.strikethrough, enable, disable);
     tables = on("tables", p.tables, enable, disable);
     toc = on("toc", p.toc, enable, disable);
+    mermaid = on("mermaid", p.mermaid, enable, disable);
 
     allowAnyIFrame = safeHtml ? p.allowAnyIFrame : false;
     allowIFrame = safeHtml ? p.allowIFrame : ImmutableList.of();
diff --git a/java/com/google/gitiles/doc/MarkdownToHtml.java b/java/com/google/gitiles/doc/MarkdownToHtml.java
index 03cc78b..d36e18b 100644
--- a/java/com/google/gitiles/doc/MarkdownToHtml.java
+++ b/java/com/google/gitiles/doc/MarkdownToHtml.java
@@ -311,9 +311,22 @@
 
   @Override
   public void visit(FencedCodeBlock node) {
+    if (config != null && config.mermaid && isMermaid(node.getInfo())) {
+      java.util.Optional<String> svg = SimpleMermaidRenderer.renderToSvg(node.getLiteral());
+      if (svg.isPresent()) {
+        html.open("div").attribute("class", "mermaid-container");
+        html.append(com.google.common.html.types.LegacyConversions.riskilyAssumeSafeHtml(svg.get()));
+        html.close("div");
+        return;
+      }
+    }
     codeInPre(node.getInfo(), node.getLiteral());
   }
 
+  private static boolean isMermaid(@Nullable String info) {
+    return info != null && "mermaid".equalsIgnoreCase(info.trim());
+  }
+
   @Override
   public void visit(IndentedCodeBlock node) {
     codeInPre(null, node.getLiteral());
diff --git a/java/com/google/gitiles/doc/SimpleMermaidRenderer.java b/java/com/google/gitiles/doc/SimpleMermaidRenderer.java
new file mode 100644
index 0000000..d4d5d56
--- /dev/null
+++ b/java/com/google/gitiles/doc/SimpleMermaidRenderer.java
@@ -0,0 +1,2644 @@
+// Copyright 2026 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gitiles.doc;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.annotation.Nullable;
+
+/**
+ * Server-side AST parser, layout engine, and SVG renderer for Mermaid flowchart and graph diagrams.
+ *
+ * <p>Implements a pure streaming character-scanner AST parser without regex splits, hierarchical
+ * Sugiyama DAG layout with cycle breaking, crossing reduction, arbitrary nested subgraphs,
+ * dynamic edge clearances, bidirectional curved paths, and responsive SVG emission.
+ */
+public class SimpleMermaidRenderer {
+
+  public enum Direction {
+    LR,
+    TD,
+    TB,
+    RL,
+    BT
+  }
+
+  public enum NodeShape {
+    RECTANGLE,
+    ROUNDED,
+    STADIUM,
+    SUBROUTINE,
+    CYLINDER,
+    CIRCLE,
+    DIAMOND,
+    HEXAGON,
+    FLAG
+  }
+
+  public enum EdgeStroke {
+    SOLID,
+    DASHED,
+    THICK
+  }
+
+  // =========================================================================
+  // AST Model Objects
+  // =========================================================================
+
+  public static class Node {
+    public final String id;
+    public String label;
+    public final List<String> labelLines = new ArrayList<>();
+    public NodeShape shape = NodeShape.RECTANGLE;
+    public int layer = 0;
+    public double relX;
+    public double relY;
+    public double x;
+    public double y;
+    public double width = 160;
+    public double height = 44;
+    public Subgraph parentSubgraph;
+    public double barycenter = 0;
+    public boolean isVirtual = false;
+    public @Nullable String customFill;
+    public @Nullable String customStroke;
+
+    public Node(String id) {
+      this.id = id;
+      setLabel(id);
+    }
+
+    public void setLabel(String rawLabel) {
+      this.label = rawLabel != null ? rawLabel : id;
+      this.labelLines.clear();
+      this.labelLines.addAll(parseLabelLines(this.label));
+    }
+  }
+
+  public static class Subgraph {
+    public final String id;
+    public String title;
+    public Direction direction;
+    public Subgraph parent;
+    public final List<Subgraph> children = new ArrayList<>();
+    public final List<Node> nodes = new ArrayList<>();
+    public double relX;
+    public double relY;
+    public double x;
+    public double y;
+    public double width;
+    public double height;
+    public @Nullable String customFill;
+    public @Nullable String customStroke;
+
+    public Subgraph(String id, String title) {
+      this.id = id;
+      this.title = title;
+    }
+  }
+
+  public static class Edge {
+    public final String fromId;
+    public final String toId;
+    public final String label;
+    public final EdgeStroke stroke;
+    public final boolean arrow;
+    public boolean isBackEdge = false;
+    public final List<Node> virtualNodes = new ArrayList<>();
+
+    public Edge(String fromId, String toId, String label, EdgeStroke stroke, boolean arrow) {
+      this.fromId = fromId;
+      this.toId = toId;
+      this.label = label;
+      this.stroke = stroke;
+      this.arrow = arrow;
+    }
+  }
+
+  public static class SubgraphEdge {
+    public final String fromSgId;
+    public final String toSgId;
+    public final String label;
+    public final EdgeStroke stroke;
+    public final boolean arrow;
+
+    public SubgraphEdge(
+        String fromSgId, String toSgId, String label, EdgeStroke stroke, boolean arrow) {
+      this.fromSgId = fromSgId;
+      this.toSgId = toSgId;
+      this.label = label;
+      this.stroke = stroke;
+      this.arrow = arrow;
+    }
+  }
+
+  public static class MermaidGraph {
+    public Direction direction = Direction.TD;
+    public final Map<String, Node> nodes = new LinkedHashMap<>();
+    public final Map<String, Subgraph> subgraphsMap = new LinkedHashMap<>();
+    public final List<Subgraph> rootSubgraphs = new ArrayList<>();
+    public final List<Subgraph> allSubgraphs = new ArrayList<>();
+    public final List<Edge> edges = new ArrayList<>();
+    public final List<SubgraphEdge> subgraphEdges = new ArrayList<>();
+
+    public Node ensureNode(String id, @Nullable Subgraph currentSubgraph) {
+      Node node = nodes.get(id);
+      if (node == null) {
+        node = new Node(id);
+        nodes.put(id, node);
+        if (currentSubgraph != null) {
+          node.parentSubgraph = currentSubgraph;
+          currentSubgraph.nodes.add(node);
+        }
+      } else if (node.parentSubgraph == null && currentSubgraph != null) {
+        node.parentSubgraph = currentSubgraph;
+        currentSubgraph.nodes.add(node);
+      }
+      return node;
+    }
+
+    public @Nullable Subgraph lookupSubgraph(String name) {
+      if (name == null) return null;
+      String clean = name.trim();
+      Subgraph sg = subgraphsMap.get(clean);
+      if (sg != null) return sg;
+      sg = subgraphsMap.get(stripWhitespace(clean));
+      if (sg != null) return sg;
+      sg = subgraphsMap.get(clean.toLowerCase());
+      if (sg != null) return sg;
+      sg = subgraphsMap.get(stripWhitespace(clean).toLowerCase());
+      return sg;
+    }
+  }
+
+  // =========================================================================
+  // Character Stream Scanner & Tokenizer
+  // =========================================================================
+
+  private static class CharScanner {
+    final String text;
+    int pos;
+
+    CharScanner(String text) {
+      this.text = text != null ? text : "";
+      this.pos = 0;
+    }
+
+    boolean isEof() {
+      return pos >= text.length();
+    }
+
+    char peek() {
+      return isEof() ? '\0' : text.charAt(pos);
+    }
+
+    char next() {
+      return isEof() ? '\0' : text.charAt(pos++);
+    }
+
+    boolean startsWith(String prefix) {
+      return text.startsWith(prefix, pos);
+    }
+
+    boolean startsWithIgnoreCase(String prefix) {
+      if (text.length() - pos < prefix.length()) return false;
+      return text.substring(pos, pos + prefix.length()).equalsIgnoreCase(prefix);
+    }
+
+    boolean consume(String prefix) {
+      if (startsWith(prefix)) {
+        pos += prefix.length();
+        return true;
+      }
+      return false;
+    }
+
+    boolean consumeIgnoreCase(String prefix) {
+      if (startsWithIgnoreCase(prefix)) {
+        pos += prefix.length();
+        return true;
+      }
+      return false;
+    }
+
+    void skipWhitespace() {
+      while (!isEof() && (text.charAt(pos) == ' ' || text.charAt(pos) == '\t')) {
+        pos++;
+      }
+    }
+
+    void skipWhitespaceAndNewlines() {
+      while (!isEof()) {
+        char c = text.charAt(pos);
+        if (c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == ';') {
+          pos++;
+        } else {
+          break;
+        }
+      }
+    }
+
+    void skipLine() {
+      while (!isEof()) {
+        char c = text.charAt(pos++);
+        if (c == '\n') break;
+      }
+    }
+
+    void skipToStatementEnd() {
+      while (!isEof()) {
+        char c = text.charAt(pos);
+        if (c == ';' || c == '\n') {
+          pos++;
+          break;
+        }
+        pos++;
+      }
+    }
+
+    String scanIdentifier() {
+      skipWhitespace();
+      int start = pos;
+      while (!isEof()) {
+        char c = text.charAt(pos);
+        if (Character.isLetterOrDigit(c) || c == '_' || c == '-' || c == '.') {
+          pos++;
+        } else {
+          break;
+        }
+      }
+      return text.substring(start, pos);
+    }
+  }
+
+  private static class RawNodeToken {
+    final String id;
+    final NodeShape shape;
+    final String label;
+
+    RawNodeToken(String id, NodeShape shape, @Nullable String label) {
+      this.id = id;
+      this.shape = shape;
+      this.label = label;
+    }
+  }
+
+  private static class RawEdgeToken {
+    final EdgeStroke stroke;
+    final boolean arrow;
+    final String label;
+
+    RawEdgeToken(EdgeStroke stroke, boolean arrow, @Nullable String label) {
+      this.stroke = stroke;
+      this.arrow = arrow;
+      this.label = label;
+    }
+  }
+
+  // =========================================================================
+  // Parser Implementation
+  // =========================================================================
+
+  /**
+   * Attempts to render a Mermaid code string into SVG XML.
+   *
+   * @param mermaidCode source Mermaid definition.
+   * @return rendered SVG XML string, or empty if unsupported / invalid syntax.
+   */
+  public static Optional<String> renderToSvg(String mermaidCode) {
+    if (mermaidCode == null || mermaidCode.trim().isEmpty()) {
+      return Optional.empty();
+    }
+
+    Optional<MermaidGraph> graphOpt = parse(mermaidCode);
+    if (!graphOpt.isPresent()) {
+      return Optional.empty();
+    }
+
+    MermaidGraph graph = graphOpt.get();
+    if (graph.nodes.isEmpty()) {
+      return Optional.empty();
+    }
+
+    return Optional.of(layoutAndRenderSvg(graph));
+  }
+
+  /** Parses Mermaid source code into a {@link MermaidGraph} AST. */
+  public static Optional<MermaidGraph> parse(String mermaidCode) {
+    CharScanner s = new CharScanner(mermaidCode);
+    MermaidGraph graph = new MermaidGraph();
+    boolean headerFound = false;
+
+    // Scan for diagram type and direction
+    while (!s.isEof()) {
+      s.skipWhitespaceAndNewlines();
+      if (s.isEof()) break;
+
+      if (s.startsWith("%%")) {
+        s.skipLine();
+        continue;
+      }
+
+      if (s.consumeIgnoreCase("graph") || s.consumeIgnoreCase("flowchart")) {
+        s.skipWhitespace();
+        String dirStr = s.scanIdentifier().toUpperCase();
+        try {
+          if (!dirStr.isEmpty()) {
+            graph.direction = Direction.valueOf(dirStr);
+          } else {
+            graph.direction = Direction.TD;
+          }
+        } catch (IllegalArgumentException e) {
+          graph.direction = Direction.TD;
+        }
+        headerFound = true;
+        s.skipToStatementEnd();
+        break;
+      }
+
+      // Check unsupported non-graph diagrams for quick fallback
+      if (s.startsWithIgnoreCase("sequenceDiagram")
+          || s.startsWithIgnoreCase("classDiagram")
+          || s.startsWithIgnoreCase("erDiagram")
+          || s.startsWithIgnoreCase("gantt")
+          || s.startsWithIgnoreCase("pie")
+          || s.startsWithIgnoreCase("gitGraph")
+          || s.startsWithIgnoreCase("xychart-beta")
+          || s.startsWithIgnoreCase("stateDiagram")) {
+        return Optional.empty();
+      }
+
+      s.skipLine();
+    }
+
+    if (!headerFound) {
+      return Optional.empty();
+    }
+
+    Deque<Subgraph> subgraphStack = new ArrayDeque<>();
+
+    // Parse diagram statements into AST
+    while (!s.isEof()) {
+      s.skipWhitespaceAndNewlines();
+      if (s.isEof()) break;
+
+      if (s.startsWith("%%")) {
+        s.skipLine();
+        continue;
+      }
+
+      if (s.startsWithIgnoreCase("style ")) {
+        parseStyleDirective(s, graph);
+        continue;
+      }
+
+      // Skip other meta directives
+      if (s.startsWithIgnoreCase("classDef ")
+          || s.startsWithIgnoreCase("class ")
+          || s.startsWithIgnoreCase("click ")
+          || s.startsWithIgnoreCase("linkStyle ")
+          || s.startsWithIgnoreCase("accTitle")
+          || s.startsWithIgnoreCase("accDescr")) {
+        s.skipToStatementEnd();
+        continue;
+      }
+
+      if (s.startsWithIgnoreCase("graph") || s.startsWithIgnoreCase("flowchart")) {
+        s.skipToStatementEnd();
+        continue;
+      }
+
+      if (s.consumeIgnoreCase("direction")) {
+        s.skipWhitespace();
+        String dirStr = s.scanIdentifier().toUpperCase();
+        if (!subgraphStack.isEmpty() && !dirStr.isEmpty()) {
+          try {
+            subgraphStack.peek().direction = Direction.valueOf(dirStr);
+          } catch (IllegalArgumentException e) {
+            // ignore
+          }
+        }
+        s.skipToStatementEnd();
+        continue;
+      }
+
+      if (s.consumeIgnoreCase("end")) {
+        char nextC = s.peek();
+        if (nextC == '\0' || Character.isWhitespace(nextC) || nextC == ';') {
+          if (!subgraphStack.isEmpty()) {
+            subgraphStack.pop();
+          }
+          s.skipToStatementEnd();
+          continue;
+        }
+      }
+
+      if (s.consumeIgnoreCase("subgraph")) {
+        parseSubgraphHeader(s, graph, subgraphStack);
+        s.skipToStatementEnd();
+        continue;
+      }
+
+      Subgraph currentSg = subgraphStack.isEmpty() ? null : subgraphStack.peek();
+      parseStatement(s, graph, currentSg);
+      s.skipToStatementEnd();
+    }
+
+    return Optional.of(graph);
+  }
+
+  private static void parseSubgraphHeader(
+      CharScanner s, MermaidGraph graph, Deque<Subgraph> subgraphStack) {
+    s.skipWhitespace();
+    String sgId, sgTitle;
+
+    // Check for `subgraph "Title Only"`
+    if (s.startsWith("\"")) {
+      s.consume("\"");
+      int start = s.pos;
+      while (!s.isEof() && !s.startsWith("\"")) s.next();
+      sgTitle = s.text.substring(start, s.pos);
+      s.consume("\"");
+      sgId = "sg_" + graph.allSubgraphs.size();
+    } else {
+      int start = s.pos;
+      while (!s.isEof() && !s.startsWith("[") && !s.startsWith("\"") && s.peek() != '\n' && s.peek() != ';') {
+        s.next();
+      }
+      String rawName = s.text.substring(start, s.pos).trim();
+      s.skipWhitespace();
+      if (s.startsWith("[")) {
+        s.consume("[");
+        s.skipWhitespace();
+        boolean quoted = s.consume("\"");
+        int tstart = s.pos;
+        if (quoted) {
+          while (!s.isEof() && !s.startsWith("\"]") && !s.startsWith("\"")) s.next();
+          sgTitle = s.text.substring(tstart, s.pos);
+          s.consume("\"");
+          s.consume("]");
+        } else {
+          while (!s.isEof() && !s.startsWith("]")) s.next();
+          sgTitle = s.text.substring(tstart, s.pos);
+          s.consume("]");
+        }
+        sgId = rawName;
+      } else {
+        sgTitle = rawName;
+        sgId = rawName;
+      }
+    }
+
+    Subgraph sg = new Subgraph(sgId, sgTitle);
+    if (!subgraphStack.isEmpty()) {
+      Subgraph parent = subgraphStack.peek();
+      sg.parent = parent;
+      parent.children.add(sg);
+    } else {
+      graph.rootSubgraphs.add(sg);
+    }
+    subgraphStack.push(sg);
+    graph.subgraphsMap.put(sgId, sg);
+    graph.subgraphsMap.put(sgTitle, sg);
+    graph.subgraphsMap.put(sgId.toLowerCase(), sg);
+    graph.subgraphsMap.put(sgTitle.toLowerCase(), sg);
+    graph.allSubgraphs.add(sg);
+  }
+
+  private static void parseStyleDirective(CharScanner s, MermaidGraph graph) {
+    s.consumeIgnoreCase("style");
+    s.skipWhitespace();
+    String targetId = s.scanIdentifier();
+    if (targetId.isEmpty()) {
+      s.skipToStatementEnd();
+      return;
+    }
+    s.skipWhitespace();
+    int start = s.pos;
+    while (!s.isEof()) {
+      char c = s.peek();
+      if (c == '\n' || c == '\r' || c == ';') break;
+      s.pos++;
+    }
+    String rest = s.text.substring(start, s.pos);
+    s.skipToStatementEnd();
+
+    String fill = null;
+    String stroke = null;
+    int p = 0;
+    while (p < rest.length()) {
+      int nextSep = rest.length();
+      for (int i = p; i < rest.length(); i++) {
+        char ch = rest.charAt(i);
+        if (ch == ',' || ch == ';') {
+          nextSep = i;
+          break;
+        }
+      }
+      String part = rest.substring(p, nextSep).trim();
+      int colonIdx = part.indexOf(':');
+      if (colonIdx != -1) {
+        String key = part.substring(0, colonIdx).trim().toLowerCase();
+        String val = part.substring(colonIdx + 1).trim();
+        if (key.equals("fill")) {
+          fill = val;
+        } else if (key.equals("stroke")) {
+          stroke = val;
+        }
+      }
+      p = nextSep + 1;
+    }
+
+    if (fill != null && !isValidCssColor(fill)) {
+      fill = null;
+    }
+    if (stroke != null && !isValidCssColor(stroke)) {
+      stroke = null;
+    }
+
+    Subgraph sg = graph.lookupSubgraph(targetId);
+    if (sg != null) {
+      if (fill != null) sg.customFill = fill;
+      if (stroke != null) sg.customStroke = stroke;
+    }
+    Node n = graph.nodes.get(targetId);
+    if (n != null) {
+      if (fill != null) n.customFill = fill;
+      if (stroke != null) n.customStroke = stroke;
+    }
+  }
+
+  private static boolean isValidCssColor(@Nullable String val) {
+    if (val == null || val.isEmpty()) return false;
+    String v = val.trim().toLowerCase();
+    if (v.startsWith("javascript:")
+        || v.startsWith("data:")
+        || v.contains("url(")
+        || v.contains("\"")
+        || v.contains("'")
+        || v.contains("<")
+        || v.contains(">")) {
+      return false;
+    }
+    if (v.matches("^#[0-9a-f]{3,8}$")) {
+      return true;
+    }
+    if (v.matches("^[a-z]{3,20}$")) {
+      return true;
+    }
+    return v.matches("^(rgb|hsl)a?\\([0-9%,. ]+\\)$");
+  }
+
+  private static void parseStatement(
+      CharScanner s, MermaidGraph graph, @Nullable Subgraph currentSubgraph) {
+    List<RawNodeToken> prevGroup = scanNodeGroup(s);
+    if (prevGroup.isEmpty()) return;
+
+    for (RawNodeToken token : prevGroup) {
+      applyNodeToken(token, graph, currentSubgraph);
+    }
+
+    while (!s.isEof()) {
+      char c = s.peek();
+      if (c == ';' || c == '\n' || c == '\r') break;
+
+      RawEdgeToken edge = scanEdgeToken(s);
+      if (edge == null) break;
+
+      List<RawNodeToken> nextGroup = scanNodeGroup(s);
+      if (nextGroup.isEmpty()) break;
+
+      for (RawNodeToken token : nextGroup) {
+        applyNodeToken(token, graph, currentSubgraph);
+      }
+
+      for (RawNodeToken fromToken : prevGroup) {
+        for (RawNodeToken toToken : nextGroup) {
+          Subgraph fromSg = graph.lookupSubgraph(fromToken.id);
+          Subgraph toSg = graph.lookupSubgraph(toToken.id);
+
+          if (fromSg != null && toSg != null) {
+            graph.subgraphEdges.add(
+                new SubgraphEdge(fromSg.id, toSg.id, edge.label, edge.stroke, edge.arrow));
+          } else if (fromSg == null && toSg != null) {
+            if (!toSg.nodes.isEmpty()) {
+              Node targetNode = toSg.nodes.get(toSg.nodes.size() / 2);
+              graph.edges.add(
+                  new Edge(fromToken.id, targetNode.id, edge.label, edge.stroke, edge.arrow));
+            }
+          } else if (fromSg != null && toSg == null) {
+            if (!fromSg.nodes.isEmpty()) {
+              Node sourceNode = fromSg.nodes.get(fromSg.nodes.size() / 2);
+              graph.edges.add(
+                  new Edge(sourceNode.id, toToken.id, edge.label, edge.stroke, edge.arrow));
+            }
+          } else {
+            graph.edges.add(
+                new Edge(fromToken.id, toToken.id, edge.label, edge.stroke, edge.arrow));
+          }
+        }
+      }
+
+      prevGroup = nextGroup;
+    }
+  }
+
+  private static List<RawNodeToken> scanNodeGroup(CharScanner s) {
+    List<RawNodeToken> group = new ArrayList<>();
+    RawNodeToken first = scanNodeToken(s);
+    if (first == null) return group;
+    group.add(first);
+
+    while (!s.isEof()) {
+      s.skipWhitespace();
+      if (s.startsWith("&")) {
+        s.consume("&");
+        s.skipWhitespace();
+        RawNodeToken next = scanNodeToken(s);
+        if (next != null) {
+          group.add(next);
+        } else {
+          break;
+        }
+      } else {
+        break;
+      }
+    }
+    return group;
+  }
+
+  private static void applyNodeToken(
+      RawNodeToken token, MermaidGraph graph, @Nullable Subgraph currentSubgraph) {
+    if (graph.lookupSubgraph(token.id) != null) {
+      return;
+    }
+    Node node = graph.ensureNode(token.id, currentSubgraph);
+    if (token.label != null) {
+      node.shape = token.shape;
+      node.setLabel(token.label);
+      if (node.shape == NodeShape.DIAMOND && node.labelLines.size() == 1) {
+        node.labelLines.clear();
+        node.labelLines.addAll(wrapDiamondLabel(node.label));
+      }
+    }
+  }
+
+  private static List<String> wrapDiamondLabel(String text) {
+    List<String> result = new ArrayList<>();
+    String trimmed = (text != null) ? text.trim() : "";
+    if (trimmed.length() <= 16 || !trimmed.contains(" ")) {
+      result.add(trimmed);
+      return result;
+    }
+    List<String> words = new ArrayList<>();
+    StringBuilder curWord = new StringBuilder();
+    for (int i = 0; i < trimmed.length(); i++) {
+      char c = trimmed.charAt(i);
+      if (Character.isWhitespace(c)) {
+        if (curWord.length() > 0) {
+          words.add(curWord.toString());
+          curWord.setLength(0);
+        }
+      } else {
+        curWord.append(c);
+      }
+    }
+    if (curWord.length() > 0) {
+      words.add(curWord.toString());
+    }
+
+    int targetLines = Math.max(2, (int) Math.ceil(trimmed.length() / 16.0));
+    int targetLen = (int) Math.ceil((double) trimmed.length() / targetLines);
+
+    StringBuilder cur = new StringBuilder();
+    for (String w : words) {
+      if (cur.length() == 0) {
+        cur.append(w);
+      } else if (cur.length() + 1 + w.length() <= Math.max(targetLen + 4, 18)) {
+        cur.append(" ").append(w);
+      } else {
+        result.add(cur.toString());
+        cur = new StringBuilder(w);
+      }
+    }
+    if (cur.length() > 0) {
+      result.add(cur.toString());
+    }
+    return result;
+  }
+
+  private static @Nullable RawNodeToken scanNodeToken(CharScanner s) {
+    s.skipWhitespace();
+    if (s.isEof()) return null;
+
+    String id = s.scanIdentifier();
+    if (id.isEmpty()) return null;
+
+    s.skipWhitespace();
+    NodeShape shape = NodeShape.RECTANGLE;
+    String label = null;
+
+    String[][] delims = {
+      {"[[", "]]", "SUBROUTINE"},
+      {"[(", ")]", "CYLINDER"},
+      {"([", "])", "STADIUM"},
+      {"((", "))", "CIRCLE"},
+      {"{{", "}}", "HEXAGON"},
+      {"[", "]", "RECTANGLE"},
+      {"(", ")", "ROUNDED"},
+      {"{", "}", "DIAMOND"},
+      {">", "]", "FLAG"}
+    };
+
+    for (String[] d : delims) {
+      String open = d[0];
+      String close = d[1];
+      String shapeName = d[2];
+      if (s.startsWith(open)) {
+        s.consume(open);
+        shape = NodeShape.valueOf(shapeName);
+        s.skipWhitespace();
+        if (s.startsWith("\"")) {
+          s.consume("\"");
+          int start = s.pos;
+          while (!s.isEof() && s.peek() != '\n' && s.peek() != '\r') {
+            if (s.startsWith("\\\"")) {
+              s.pos += 2;
+            } else if (s.startsWith("\"")) {
+              break;
+            } else {
+              s.next();
+            }
+          }
+          label = s.text.substring(start, s.pos);
+          s.consume("\"");
+          s.skipWhitespace();
+          s.consume(close);
+        } else {
+          int start = s.pos;
+          while (!s.isEof() && s.peek() != '\n' && !s.startsWith(close)) {
+            s.next();
+          }
+          label = s.text.substring(start, s.pos);
+          s.consume(close);
+        }
+        break;
+      }
+    }
+
+    return new RawNodeToken(id, shape, label != null ? cleanLabel(label) : null);
+  }
+
+  private static @Nullable RawEdgeToken scanEdgeToken(CharScanner s) {
+    s.skipWhitespace();
+    if (s.isEof()) return null;
+
+    // 1. Infix labels: -- label -->, -- "label" -->, == label ==>, -. label .->, -- label ---
+    if ((s.startsWith("-- ") || s.startsWith("--\"") || s.startsWith("--\t"))
+        && !s.startsWith("-->")
+        && !s.startsWith("---|")) {
+      s.consume("--");
+      s.skipWhitespace();
+      int start = s.pos;
+      while (!s.isEof() && s.peek() != '\n' && !s.startsWith("-->") && !s.startsWith("---")) {
+        s.next();
+      }
+      String label = cleanLabel(s.text.substring(start, s.pos));
+      boolean arrow = s.consume("-->");
+      if (!arrow) s.consume("---");
+      return new RawEdgeToken(EdgeStroke.SOLID, arrow, label);
+    }
+
+    if ((s.startsWith("== ") || s.startsWith("==\"") || s.startsWith("==\t"))
+        && !s.startsWith("==>")
+        && !s.startsWith("===|")) {
+      s.consume("==");
+      s.skipWhitespace();
+      int start = s.pos;
+      while (!s.isEof() && s.peek() != '\n' && !s.startsWith("==>") && !s.startsWith("===")) {
+        s.next();
+      }
+      String label = cleanLabel(s.text.substring(start, s.pos));
+      boolean arrow = s.consume("==>");
+      if (!arrow) s.consume("===");
+      return new RawEdgeToken(EdgeStroke.THICK, arrow, label);
+    }
+
+    if (s.startsWith("-. ") || s.startsWith("-.\"") || s.startsWith("-.\t")) {
+      s.consume("-.");
+      s.skipWhitespace();
+      int start = s.pos;
+      while (!s.isEof() && s.peek() != '\n' && !s.startsWith(".->") && !s.startsWith(".-")) {
+        s.next();
+      }
+      String label = cleanLabel(s.text.substring(start, s.pos));
+      boolean arrow = s.consume(".->");
+      if (!arrow) s.consume(".-");
+      return new RawEdgeToken(EdgeStroke.DASHED, arrow, label);
+    }
+
+    // 2. Standard edge operators with optional |pipe label|
+    String[][] ops = {
+      {"-.->", "DASHED", "true"},
+      {"-.-", "DASHED", "false"},
+      {"==>", "THICK", "true"},
+      {"===", "THICK", "false"},
+      {"-->", "SOLID", "true"},
+      {"---", "SOLID", "false"},
+      {"<-->", "SOLID", "true"}
+    };
+
+    for (String[] op : ops) {
+      String prefix = op[0];
+      EdgeStroke stroke = EdgeStroke.valueOf(op[1]);
+      boolean arrow = Boolean.parseBoolean(op[2]);
+      if (s.startsWith(prefix)) {
+        s.consume(prefix);
+        s.skipWhitespace();
+        String label = null;
+        if (s.startsWith("|")) {
+          s.consume("|");
+          int start = s.pos;
+          while (!s.isEof() && s.peek() != '\n' && !s.startsWith("|")) {
+            s.next();
+          }
+          label = cleanLabel(s.text.substring(start, s.pos));
+          s.consume("|");
+        }
+        return new RawEdgeToken(stroke, arrow, label);
+      }
+    }
+
+    return null;
+  }
+
+  private static String cleanLabel(String raw) {
+    if (raw == null) return "";
+    String s = raw.trim();
+    if (s.startsWith("\"") && s.endsWith("\"") && s.length() >= 2) {
+      s = s.substring(1, s.length() - 1);
+    } else if (s.startsWith("\\\"") && s.endsWith("\\\"") && s.length() >= 4) {
+      s = s.substring(2, s.length() - 2);
+    }
+    if (s.contains("\\\"")) {
+      s = s.replace("\\\"", "\"");
+    }
+    return s;
+  }
+
+  private static String stripWhitespace(String s) {
+    if (s == null) return "";
+    StringBuilder sb = new StringBuilder(s.length());
+    for (int i = 0; i < s.length(); i++) {
+      char c = s.charAt(i);
+      if (!Character.isWhitespace(c)) {
+        sb.append(c);
+      }
+    }
+    return sb.toString();
+  }
+
+  private static final Pattern BR_PATTERN = Pattern.compile("(?i)<br\\s*/?>");
+
+  private static List<String> parseLabelLines(String label) {
+    List<String> lines = new ArrayList<>();
+    if (label == null || label.isEmpty()) {
+      lines.add("");
+      return lines;
+    }
+    Matcher matcher = BR_PATTERN.matcher(label);
+    int lastEnd = 0;
+    while (matcher.find()) {
+      lines.add(label.substring(lastEnd, matcher.start()).trim());
+      lastEnd = matcher.end();
+    }
+    lines.add(label.substring(lastEnd).trim());
+    return lines;
+  }
+
+  // =========================================================================
+  // Layout Engine
+  // =========================================================================
+
+  private static String layoutAndRenderSvg(MermaidGraph graph) {
+    boolean isHorizontal = (graph.direction == Direction.LR || graph.direction == Direction.RL);
+
+    // Calculate node dimensions using structured AST labelLines
+    for (Node n : graph.nodes.values()) {
+      int maxLineLen = 0;
+      for (String l : n.labelLines) {
+        maxLineLen = Math.max(maxLineLen, l.trim().length());
+      }
+      if (n.shape == NodeShape.DIAMOND) {
+        double tw = maxLineLen * 7.5;
+        double th = n.labelLines.size() * 18.0;
+        n.width = Math.max(90, tw * 1.5 + 36);
+        n.height = Math.max(50, Math.max(th * 2.2 + 24, n.width * 0.65));
+      } else if (n.shape == NodeShape.CYLINDER) {
+        n.width = Math.max(80, maxLineLen * 7.5 + 32);
+        n.height = Math.max(50, n.labelLines.size() * 18 + 26);
+      } else {
+        n.width = Math.max(70, maxLineLen * 7.5 + 28);
+        n.height = Math.max(38, n.labelLines.size() * 18 + 16);
+        if (n.shape == NodeShape.HEXAGON) {
+          n.width += 36;
+          n.height += 16;
+        } else if (n.shape == NodeShape.CIRCLE) {
+          double d = Math.max(n.width, n.height) + 10;
+          n.width = d;
+          n.height = d;
+        }
+      }
+    }
+
+    // 1. Partition graph into connected components
+    Map<String, String> parent = new HashMap<>();
+    for (String id : graph.nodes.keySet()) {
+      parent.put(id, id);
+    }
+    for (Edge e : graph.edges) {
+      if (parent.containsKey(e.fromId) && parent.containsKey(e.toId)) {
+        unionSets(parent, e.fromId, e.toId);
+      }
+    }
+    for (Subgraph sg : graph.allSubgraphs) {
+      String sampleId = getSubgraphSampleNodeId(sg);
+      if (sg.nodes.size() > 1) {
+        String firstId = sg.nodes.get(0).id;
+        for (int i = 1; i < sg.nodes.size(); i++) {
+          unionSets(parent, firstId, sg.nodes.get(i).id);
+        }
+      }
+      for (Subgraph child : sg.children) {
+        String childSample = getSubgraphSampleNodeId(child);
+        if (sampleId != null && childSample != null) {
+          unionSets(parent, sampleId, childSample);
+        }
+      }
+    }
+    for (SubgraphEdge se : graph.subgraphEdges) {
+      Subgraph fromSg = graph.lookupSubgraph(se.fromSgId);
+      Subgraph toSg = graph.lookupSubgraph(se.toSgId);
+      if (fromSg != null && toSg != null) {
+        String fromSample = getSubgraphSampleNodeId(fromSg);
+        String toSample = getSubgraphSampleNodeId(toSg);
+        if (fromSample != null && toSample != null) {
+          unionSets(parent, fromSample, toSample);
+        }
+      }
+    }
+
+    Map<String, GraphComponent> compMap = new LinkedHashMap<>();
+    for (Node n : graph.nodes.values()) {
+      String root = findRoot(parent, n.id);
+      GraphComponent comp = compMap.computeIfAbsent(root, k -> new GraphComponent());
+      comp.nodes.put(n.id, n);
+    }
+
+    for (Edge e : graph.edges) {
+      String root = findRoot(parent, e.fromId);
+      GraphComponent comp = compMap.get(root);
+      if (comp != null && comp.nodes.containsKey(e.fromId) && comp.nodes.containsKey(e.toId)) {
+        comp.edges.add(e);
+      }
+    }
+
+    for (Subgraph sg : graph.allSubgraphs) {
+      String sampleId = getSubgraphSampleNodeId(sg);
+      if (sampleId != null) {
+        String root = findRoot(parent, sampleId);
+        GraphComponent comp = compMap.get(root);
+        if (comp != null && !comp.subgraphs.contains(sg)) {
+          comp.subgraphs.add(sg);
+        }
+      }
+    }
+
+    List<GraphComponent> components = new ArrayList<>(compMap.values());
+    components.sort((c1, c2) -> Boolean.compare(!c2.subgraphs.isEmpty(), !c1.subgraphs.isEmpty()));
+
+    for (GraphComponent comp : components) {
+      if (!comp.subgraphs.isEmpty() && comp.edges.isEmpty() && graph.subgraphEdges.isEmpty()) {
+        layoutIsolatedSubgraphs(graph.direction, comp.subgraphs);
+      } else if (!comp.subgraphs.isEmpty()) {
+        layoutCompoundComponent(graph, graph.direction, isHorizontal, comp);
+      } else {
+        layoutBySugiyamaDAG(isHorizontal, comp.nodes, comp.edges);
+      }
+
+      double cMinX = Double.MAX_VALUE, cMinY = Double.MAX_VALUE;
+      double cMaxX = Double.MIN_VALUE, cMaxY = Double.MIN_VALUE;
+      for (Node n : comp.nodes.values()) {
+        cMinX = Math.min(cMinX, n.x);
+        cMinY = Math.min(cMinY, n.y);
+        cMaxX = Math.max(cMaxX, n.x + n.width);
+        cMaxY = Math.max(cMaxY, n.y + n.height);
+      }
+      for (Subgraph sg : comp.subgraphs) {
+        cMinX = Math.min(cMinX, sg.x);
+        cMinY = Math.min(cMinY, sg.y);
+        cMaxX = Math.max(cMaxX, sg.x + sg.width);
+        cMaxY = Math.max(cMaxY, sg.y + sg.height);
+      }
+
+      if (cMinX != Double.MAX_VALUE) {
+        comp.width = cMaxX - cMinX;
+        comp.height = cMaxY - cMinY;
+        for (Node n : comp.nodes.values()) {
+          n.x -= cMinX;
+          n.y -= cMinY;
+        }
+        for (Subgraph sg : comp.subgraphs) {
+          sg.x -= cMinX;
+          sg.y -= cMinY;
+        }
+        for (Edge e : comp.edges) {
+          for (Node v : e.virtualNodes) {
+            v.x -= cMinX;
+            v.y -= cMinY;
+          }
+        }
+      }
+    }
+
+    if (!isHorizontal) {
+      double curX = 0;
+      for (GraphComponent comp : components) {
+        for (Node n : comp.nodes.values()) {
+          n.x += curX;
+        }
+        for (Subgraph sg : comp.subgraphs) {
+          sg.x += curX;
+        }
+        for (Edge e : comp.edges) {
+          for (Node v : e.virtualNodes) {
+            v.x += curX;
+          }
+        }
+        curX += comp.width + 48;
+      }
+    } else {
+      double curY = 0;
+      for (GraphComponent comp : components) {
+        for (Node n : comp.nodes.values()) {
+          n.y += curY;
+        }
+        for (Subgraph sg : comp.subgraphs) {
+          sg.y += curY;
+        }
+        for (Edge e : comp.edges) {
+          for (Node v : e.virtualNodes) {
+            v.y += curY;
+          }
+        }
+        curY += comp.height + 48;
+      }
+    }
+
+    // Compute bounding box
+    double minX = Double.MAX_VALUE, minY = Double.MAX_VALUE;
+    double maxX = Double.MIN_VALUE, maxY = Double.MIN_VALUE;
+
+    for (Node n : graph.nodes.values()) {
+      minX = Math.min(minX, n.x);
+      minY = Math.min(minY, n.y);
+      maxX = Math.max(maxX, n.x + n.width);
+      maxY = Math.max(maxY, n.y + n.height);
+    }
+    for (Subgraph sg : graph.allSubgraphs) {
+      minX = Math.min(minX, sg.x);
+      minY = Math.min(minY, sg.y);
+      maxX = Math.max(maxX, sg.x + sg.width);
+      maxY = Math.max(maxY, sg.y + sg.height);
+    }
+    for (Edge e : graph.edges) {
+      for (Node v : e.virtualNodes) {
+        minX = Math.min(minX, v.x);
+        minY = Math.min(minY, v.y);
+        maxX = Math.max(maxX, v.x + v.width);
+        maxY = Math.max(maxY, v.y + v.height);
+      }
+    }
+
+    for (Edge e : graph.edges) {
+      Node src = graph.nodes.get(e.fromId);
+      Node dst = graph.nodes.get(e.toId);
+      if (src != null && dst != null) {
+        double labelW =
+            (e.label != null && !e.label.trim().isEmpty()) ? (e.label.length() * 6.5 + 16) : 0;
+        if (e.isBackEdge || src.layer > dst.layer) {
+          int minL = Math.min(src.layer, dst.layer);
+          int maxL = Math.max(src.layer, dst.layer);
+          if (!isHorizontal) {
+            double maxRight = Math.max(src.x + src.width, dst.x + dst.width);
+            for (Node n : graph.nodes.values()) {
+              if (n.layer >= minL && n.layer <= maxL) {
+                maxRight = Math.max(maxRight, n.x + n.width);
+              }
+            }
+            for (Edge oe : graph.edges) {
+              for (Node v : oe.virtualNodes) {
+                if (v.layer >= minL && v.layer <= maxL) {
+                  maxRight = Math.max(maxRight, v.x + v.width);
+                }
+              }
+            }
+            double loopOffset =
+                Math.max(45.0, labelW / 2.0 + 36.0)
+                    + (maxRight - Math.min(src.x + src.width / 2.0, dst.x + dst.width / 2.0)) * 0.4;
+            maxX = Math.max(maxX, maxRight + loopOffset + labelW / 2.0 + 12);
+          } else {
+            double minTop = Math.min(src.y, dst.y);
+            for (Node n : graph.nodes.values()) {
+              if (n.layer >= minL && n.layer <= maxL) {
+                minTop = Math.min(minTop, n.y);
+              }
+            }
+            for (Edge oe : graph.edges) {
+              for (Node v : oe.virtualNodes) {
+                if (v.layer >= minL && v.layer <= maxL) {
+                  minTop = Math.min(minTop, v.y);
+                }
+              }
+            }
+            double loopOffset =
+                Math.max(45.0, 18.0 + 36.0)
+                    + (Math.max(src.y + src.height / 2.0, dst.y + dst.height / 2.0) - minTop) * 0.4;
+            minY = Math.min(minY, minTop - loopOffset - 24);
+          }
+        }
+      }
+    }
+
+    double padding = 28;
+    double offsetX = padding - minX;
+    double offsetY = padding - minY;
+    double totalWidth = (maxX - minX) + padding * 2;
+    double totalHeight = (maxY - minY) + padding * 2;
+
+    for (Node n : graph.nodes.values()) {
+      n.x += offsetX;
+      n.y += offsetY;
+    }
+    for (Subgraph sg : graph.allSubgraphs) {
+      sg.x += offsetX;
+      sg.y += offsetY;
+    }
+    for (Edge e : graph.edges) {
+      for (Node v : e.virtualNodes) {
+        v.x += offsetX;
+        v.y += offsetY;
+      }
+    }
+
+    return renderSvg(graph, isHorizontal, totalWidth, totalHeight);
+  }
+
+  private static void layoutBySugiyamaDAG(
+      boolean isHorizontal,
+      Map<String, Node> allNodes,
+      List<Edge> edges) {
+
+    // 1. Cycle Breaking via DFS
+    Map<String, List<Edge>> adj = new HashMap<>();
+    for (String id : allNodes.keySet()) {
+      adj.put(id, new ArrayList<>());
+    }
+    for (Edge e : edges) {
+      if (adj.containsKey(e.fromId)) {
+        adj.get(e.fromId).add(e);
+      }
+    }
+
+    Map<String, Integer> color = new HashMap<>();
+    for (String id : allNodes.keySet()) {
+      if (color.getOrDefault(id, 0) == 0) {
+        findCyclesDFS(id, adj, color);
+      }
+    }
+
+    // 2. Layer Assignment (Longest Path in DAG)
+    for (Node n : allNodes.values()) {
+      n.layer = 0;
+    }
+    boolean changed = true;
+    int maxIterations = allNodes.size() + 1;
+    int iter = 0;
+    while (changed && iter++ < maxIterations) {
+      changed = false;
+      for (Edge e : edges) {
+        if (!e.isBackEdge) {
+          Node src = allNodes.get(e.fromId);
+          Node dst = allNodes.get(e.toId);
+          if (src != null && dst != null) {
+            if (dst.layer < src.layer + 1) {
+              dst.layer = src.layer + 1;
+              changed = true;
+            }
+          }
+        }
+      }
+    }
+
+    // 2a. Source Node Sinking / Compaction (ALAP for loose source nodes)
+    for (Node n : allNodes.values()) {
+      int inCount = 0;
+      int minOutLayer = Integer.MAX_VALUE;
+      for (Edge e : edges) {
+        if (!e.isBackEdge) {
+          if (e.toId.equals(n.id)) inCount++;
+          if (e.fromId.equals(n.id)) {
+            Node dst = allNodes.get(e.toId);
+            if (dst != null) {
+              minOutLayer = Math.min(minOutLayer, dst.layer);
+            }
+          }
+        }
+      }
+      if (inCount == 0 && minOutLayer != Integer.MAX_VALUE && minOutLayer - 1 > n.layer) {
+        n.layer = minOutLayer - 1;
+      }
+    }
+
+    // 2b. Virtual Dummy Node Insertion for Long Edges
+    for (Edge e : edges) {
+      e.virtualNodes.clear();
+      if (!e.isBackEdge) {
+        Node src = allNodes.get(e.fromId);
+        Node dst = allNodes.get(e.toId);
+        if (src != null && dst != null && dst.layer - src.layer > 1) {
+          for (int l = src.layer + 1; l < dst.layer; l++) {
+            Node dummy = new Node("__v_" + src.id + "_" + dst.id + "_" + l);
+            dummy.isVirtual = true;
+            dummy.layer = l;
+            if (e.label != null && !e.label.trim().isEmpty() && l == src.layer + 1) {
+              dummy.width = Math.max(e.label.trim().length() * 6.5 + 24, 60);
+              dummy.height = 20;
+            } else {
+              dummy.width = 24;
+              dummy.height = 20;
+            }
+            e.virtualNodes.add(dummy);
+          }
+        }
+      }
+    }
+
+    // 3. Layer Ordering & Barycentric Crossing Reduction
+    Map<Integer, List<Node>> layerMap = new TreeMap<>();
+    for (Node n : allNodes.values()) {
+      layerMap.computeIfAbsent(n.layer, k -> new ArrayList<>()).add(n);
+    }
+    for (Edge e : edges) {
+      for (Node v : e.virtualNodes) {
+        layerMap.computeIfAbsent(v.layer, k -> new ArrayList<>()).add(v);
+      }
+    }
+
+    int maxLayer = layerMap.isEmpty() ? 0 : Collections.max(layerMap.keySet());
+    for (int l = 1; l <= maxLayer; l++) {
+      List<Node> currentLayer = layerMap.get(l);
+      List<Node> prevLayer = layerMap.get(l - 1);
+      if (currentLayer == null) continue;
+      for (Node n : currentLayer) {
+        double sum = 0;
+        int count = 0;
+        if (prevLayer != null) {
+          if (n.isVirtual) {
+            for (Edge e : edges) {
+              int vIdx = e.virtualNodes.indexOf(n);
+              if (vIdx != -1) {
+                Node pred = vIdx == 0 ? allNodes.get(e.fromId) : e.virtualNodes.get(vIdx - 1);
+                if (pred != null) {
+                  int pos = prevLayer.indexOf(pred);
+                  if (pos != -1) {
+                    sum += pos;
+                    count++;
+                  }
+                }
+                break;
+              }
+            }
+          } else {
+            for (Edge e : edges) {
+              if (e.toId.equals(n.id) && !e.isBackEdge) {
+                Node pred;
+                if (!e.virtualNodes.isEmpty()) {
+                  pred = e.virtualNodes.get(e.virtualNodes.size() - 1);
+                } else {
+                  pred = allNodes.get(e.fromId);
+                }
+                if (pred != null && pred.layer == l - 1) {
+                  int pos = prevLayer.indexOf(pred);
+                  if (pos != -1) {
+                    sum += pos;
+                    count++;
+                  }
+                }
+              }
+            }
+          }
+        }
+        n.barycenter = count > 0 ? sum / count : currentLayer.indexOf(n);
+      }
+      currentLayer.sort(Comparator.comparingDouble(n -> n.barycenter));
+    }
+
+    // 4. Coordinate Assignment
+    if (!isHorizontal) {
+      // Top-Down
+      double maxGraphWidth = 0;
+      Map<Integer, Double> layerWidths = new HashMap<>();
+      Map<Integer, Double> layerMaxHeights = new HashMap<>();
+
+      for (Map.Entry<Integer, List<Node>> entry : layerMap.entrySet()) {
+        int l = entry.getKey();
+        List<Node> nodes = entry.getValue();
+        double totalW = 0;
+        double maxH = 38;
+        for (int i = 0; i < nodes.size(); i++) {
+          Node n = nodes.get(i);
+          totalW += n.width + (i < nodes.size() - 1 ? 32 : 0);
+          if (!n.isVirtual) {
+            maxH = Math.max(maxH, n.height);
+          }
+        }
+        layerWidths.put(l, totalW);
+        layerMaxHeights.put(l, maxH);
+        maxGraphWidth = Math.max(maxGraphWidth, totalW);
+      }
+
+      double curY = 0;
+      for (Map.Entry<Integer, List<Node>> entry : layerMap.entrySet()) {
+        int l = entry.getKey();
+        List<Node> nodes = entry.getValue();
+        double w = layerWidths.get(l);
+        double curX = (maxGraphWidth - w) / 2.0;
+        double maxH = layerMaxHeights.get(l);
+
+        if (nodes.size() == 1 && l > 0) {
+          Node single = nodes.get(0);
+          double parentAvgX = 0;
+          int parentCount = 0;
+          for (Edge e : edges) {
+            if (e.toId.equals(single.id)) {
+              Node p = allNodes.get(e.fromId);
+              if (p != null && p.layer == l - 1) {
+                parentAvgX += p.x + p.width / 2.0;
+                parentCount++;
+              }
+            }
+          }
+          if (parentCount > 0) {
+            double targetX = (parentAvgX / parentCount) - single.width / 2.0;
+            curX = Math.max(0, Math.min(targetX, maxGraphWidth - single.width));
+          }
+        }
+
+        for (int i = 0; i < nodes.size(); i++) {
+          Node n = nodes.get(i);
+          n.x = curX;
+          n.y = curY + (maxH - n.height) / 2.0;
+          curX += n.width + 32;
+        }
+
+        double layerGap = 48;
+        for (Edge e : edges) {
+          Node src = allNodes.get(e.fromId);
+          Node dst = allNodes.get(e.toId);
+          if (src != null && dst != null) {
+            if ((e.virtualNodes.isEmpty() && src.layer == l && dst.layer == l + 1)
+                || (!e.virtualNodes.isEmpty() && src.layer <= l && l < dst.layer)) {
+              if (e.label != null && !e.label.trim().isEmpty()) {
+                layerGap = Math.max(layerGap, 60);
+              }
+            }
+          }
+        }
+        curY += maxH + layerGap;
+      }
+    } else {
+      // Left-to-Right
+      double maxGraphHeight = 0;
+      Map<Integer, Double> layerHeights = new HashMap<>();
+      Map<Integer, Double> layerMaxWidths = new HashMap<>();
+
+      for (Map.Entry<Integer, List<Node>> entry : layerMap.entrySet()) {
+        int l = entry.getKey();
+        List<Node> nodes = entry.getValue();
+        double totalH = 0;
+        double maxW = 120;
+        for (int i = 0; i < nodes.size(); i++) {
+          Node n = nodes.get(i);
+          totalH += n.height + (i < nodes.size() - 1 ? 24 : 0);
+          if (!n.isVirtual) {
+            maxW = Math.max(maxW, n.width);
+          }
+        }
+        layerHeights.put(l, totalH);
+        layerMaxWidths.put(l, maxW);
+        maxGraphHeight = Math.max(maxGraphHeight, totalH);
+      }
+
+      double curX = 0;
+      for (Map.Entry<Integer, List<Node>> entry : layerMap.entrySet()) {
+        int l = entry.getKey();
+        List<Node> nodes = entry.getValue();
+        double h = layerHeights.get(l);
+        double curY = (maxGraphHeight - h) / 2.0;
+        double maxW = layerMaxWidths.get(l);
+
+        if (nodes.size() == 1 && l > 0) {
+          Node single = nodes.get(0);
+          double parentAvgY = 0;
+          int parentCount = 0;
+          for (Edge e : edges) {
+            if (e.toId.equals(single.id)) {
+              Node p = allNodes.get(e.fromId);
+              if (p != null && p.layer == l - 1) {
+                parentAvgY += p.y + p.height / 2.0;
+                parentCount++;
+              }
+            }
+          }
+          if (parentCount > 0) {
+            double targetY = (parentAvgY / parentCount) - single.height / 2.0;
+            curY = Math.max(0, Math.min(targetY, maxGraphHeight - single.height));
+          }
+        }
+
+        for (int i = 0; i < nodes.size(); i++) {
+          Node n = nodes.get(i);
+          n.x = curX + (maxW - n.width) / 2.0;
+          n.y = curY;
+          curY += n.height + 24;
+        }
+
+        double layerGap = 55;
+        for (Edge e : edges) {
+          Node src = allNodes.get(e.fromId);
+          Node dst = allNodes.get(e.toId);
+          if (src != null && dst != null) {
+            if ((e.virtualNodes.isEmpty() && src.layer == l && dst.layer == l + 1)
+                || (!e.virtualNodes.isEmpty() && src.layer <= l && l < dst.layer)) {
+              if (e.label != null && !e.label.trim().isEmpty()) {
+                double lw = e.label.trim().length() * 6.5 + 24;
+                layerGap = Math.max(layerGap, lw + 24);
+              }
+            }
+          }
+        }
+        curX += maxW + layerGap;
+      }
+    }
+  }
+
+  private static void findCyclesDFS(
+      String u, Map<String, List<Edge>> adj, Map<String, Integer> color) {
+    color.put(u, 1); // Gray
+    List<Edge> uEdges = adj.get(u);
+    if (uEdges != null) {
+      for (Edge e : uEdges) {
+        String v = e.toId;
+        int vColor = color.getOrDefault(v, 0);
+        if (vColor == 1) {
+          e.isBackEdge = true;
+        } else if (vColor == 0) {
+          findCyclesDFS(v, adj, color);
+        }
+      }
+    }
+    color.put(u, 2); // Black
+  }
+
+  private static @Nullable String getSubgraphSampleNodeId(Subgraph sg) {
+    if (!sg.nodes.isEmpty()) {
+      return sg.nodes.get(0).id;
+    }
+    for (Subgraph child : sg.children) {
+      String id = getSubgraphSampleNodeId(child);
+      if (id != null) return id;
+    }
+    return null;
+  }
+
+  private static int getSubgraphDepth(Subgraph sg) {
+    int depth = 0;
+    Subgraph cur = sg.parent;
+    while (cur != null) {
+      depth++;
+      cur = cur.parent;
+    }
+    return depth;
+  }
+
+  private static class LayoutUnit {
+    final String id;
+    final @Nullable Subgraph subgraph;
+    final @Nullable Node node;
+    double width;
+    double height;
+    double x;
+    double y;
+    int layer = 0;
+    double barycenter = 0;
+
+    LayoutUnit(Subgraph sg) {
+      this.id = "sg_" + sg.id;
+      this.subgraph = sg;
+      this.node = null;
+      this.width = sg.width;
+      this.height = sg.height;
+    }
+
+    LayoutUnit(Node n) {
+      this.id = "n_" + n.id;
+      this.subgraph = null;
+      this.node = n;
+      this.width = n.width;
+      this.height = n.height;
+    }
+  }
+
+  private static void registerNodesToUnit(
+      Subgraph sg, LayoutUnit u, Map<String, LayoutUnit> map) {
+    for (Node n : sg.nodes) {
+      map.put(n.id, u);
+    }
+    for (Subgraph child : sg.children) {
+      registerNodesToUnit(child, u, map);
+    }
+  }
+
+  private static void layoutUnits(
+      boolean isHorizontal,
+      List<LayoutUnit> units,
+      Map<String, LayoutUnit> unitMap,
+      List<Edge> unitEdges) {
+    if (units.size() <= 1) return;
+
+    // 1. Cycle Breaking
+    Map<String, List<Edge>> uAdj = new HashMap<>();
+    for (LayoutUnit u : units) {
+      uAdj.put(u.id, new ArrayList<>());
+    }
+    for (Edge ue : unitEdges) {
+      if (uAdj.containsKey(ue.fromId)) {
+        uAdj.get(ue.fromId).add(ue);
+      }
+    }
+    Map<String, Integer> uColor = new HashMap<>();
+    for (LayoutUnit u : units) {
+      if (uColor.getOrDefault(u.id, 0) == 0) {
+        findCyclesDFS(u.id, uAdj, uColor);
+      }
+    }
+
+    // 2. Layer Assignment
+    for (LayoutUnit u : units) {
+      u.layer = 0;
+    }
+    boolean changed = true;
+    int maxIter = units.size() + 1;
+    int iter = 0;
+    while (changed && iter++ < maxIter) {
+      changed = false;
+      for (Edge ue : unitEdges) {
+        if (!ue.isBackEdge) {
+          LayoutUnit src = unitMap.get(ue.fromId);
+          LayoutUnit dst = unitMap.get(ue.toId);
+          if (src != null && dst != null) {
+            if (dst.layer < src.layer + 1) {
+              dst.layer = src.layer + 1;
+              changed = true;
+            }
+          }
+        }
+      }
+    }
+
+    // 2a. Source Unit Sinking / Compaction
+    for (LayoutUnit u : units) {
+      int inCount = 0;
+      int minOutLayer = Integer.MAX_VALUE;
+      for (Edge ue : unitEdges) {
+        if (!ue.isBackEdge) {
+          LayoutUnit src = unitMap.get(ue.fromId);
+          LayoutUnit dst = unitMap.get(ue.toId);
+          if (dst != null && dst.id.equals(u.id) && (src == null || !src.id.equals(u.id))) inCount++;
+          if (src != null && src.id.equals(u.id) && dst != null && !dst.id.equals(u.id)) {
+            minOutLayer = Math.min(minOutLayer, dst.layer);
+          }
+        }
+      }
+      if (inCount == 0 && minOutLayer != Integer.MAX_VALUE && minOutLayer - 1 > u.layer) {
+        u.layer = minOutLayer - 1;
+      }
+    }
+
+    // 3. Layer Map & Barycentric Ordering
+    Map<Integer, List<LayoutUnit>> layerMap = new TreeMap<>();
+    for (LayoutUnit u : units) {
+      layerMap.computeIfAbsent(u.layer, k -> new ArrayList<>()).add(u);
+    }
+    int maxLayer = layerMap.isEmpty() ? 0 : Collections.max(layerMap.keySet());
+    for (int l = 1; l <= maxLayer; l++) {
+      List<LayoutUnit> currentLayer = layerMap.get(l);
+      List<LayoutUnit> prevLayer = layerMap.get(l - 1);
+      if (currentLayer == null) continue;
+      for (LayoutUnit u : currentLayer) {
+        double sum = 0;
+        int count = 0;
+        if (prevLayer != null) {
+          for (Edge ue : unitEdges) {
+            if (ue.toId.equals(u.id) && !ue.isBackEdge) {
+              LayoutUnit src = unitMap.get(ue.fromId);
+              if (src != null && src.layer == l - 1) {
+                int pos = prevLayer.indexOf(src);
+                if (pos != -1) {
+                  sum += pos;
+                  count++;
+                }
+              }
+            }
+          }
+        }
+        u.barycenter = count > 0 ? sum / count : currentLayer.indexOf(u);
+      }
+      currentLayer.sort(Comparator.comparingDouble(u -> u.barycenter));
+    }
+
+    // 4. Coordinate Assignment
+    if (!isHorizontal) {
+      // Top-Down
+      double maxW = 0;
+      Map<Integer, Double> layerWidths = new HashMap<>();
+      Map<Integer, Double> layerMaxHeights = new HashMap<>();
+      for (Map.Entry<Integer, List<LayoutUnit>> entry : layerMap.entrySet()) {
+        int l = entry.getKey();
+        List<LayoutUnit> lUnits = entry.getValue();
+        double tw = 0;
+        double mh = 0;
+        for (int i = 0; i < lUnits.size(); i++) {
+          LayoutUnit u = lUnits.get(i);
+          tw += u.width + (i < lUnits.size() - 1 ? 36 : 0);
+          mh = Math.max(mh, u.height);
+        }
+        layerWidths.put(l, tw);
+        layerMaxHeights.put(l, mh);
+        maxW = Math.max(maxW, tw);
+      }
+
+      double curY = 0;
+      for (Map.Entry<Integer, List<LayoutUnit>> entry : layerMap.entrySet()) {
+        int l = entry.getKey();
+        List<LayoutUnit> lUnits = entry.getValue();
+        double w = layerWidths.get(l);
+        double curX = (maxW - w) / 2.0;
+        double mh = layerMaxHeights.get(l);
+        for (LayoutUnit u : lUnits) {
+          u.x = curX;
+          u.y = curY + (mh - u.height) / 2.0;
+          curX += u.width + 36;
+        }
+        double layerGap = 45;
+        for (Edge ue : unitEdges) {
+          LayoutUnit src = unitMap.get(ue.fromId);
+          LayoutUnit dst = unitMap.get(ue.toId);
+          if (src != null && dst != null && src.layer <= l && l < dst.layer) {
+            if (ue.label != null && !ue.label.trim().isEmpty()) {
+              layerGap = Math.max(layerGap, 55);
+            }
+          }
+        }
+        curY += mh + layerGap;
+      }
+    } else {
+      // Left-to-Right
+      double maxH = 0;
+      Map<Integer, Double> layerHeights = new HashMap<>();
+      Map<Integer, Double> layerMaxWidths = new HashMap<>();
+      for (Map.Entry<Integer, List<LayoutUnit>> entry : layerMap.entrySet()) {
+        int l = entry.getKey();
+        List<LayoutUnit> lUnits = entry.getValue();
+        double th = 0;
+        double mw = 0;
+        for (int i = 0; i < lUnits.size(); i++) {
+          LayoutUnit u = lUnits.get(i);
+          th += u.height + (i < lUnits.size() - 1 ? 36 : 0);
+          mw = Math.max(mw, u.width);
+        }
+        layerHeights.put(l, th);
+        layerMaxWidths.put(l, mw);
+        maxH = Math.max(maxH, th);
+      }
+
+      double curX = 0;
+      for (Map.Entry<Integer, List<LayoutUnit>> entry : layerMap.entrySet()) {
+        int l = entry.getKey();
+        List<LayoutUnit> lUnits = entry.getValue();
+        double h = layerHeights.get(l);
+        double curY = (maxH - h) / 2.0;
+        double mw = layerMaxWidths.get(l);
+        for (LayoutUnit u : lUnits) {
+          u.x = curX + (mw - u.width) / 2.0;
+          u.y = curY;
+          curY += u.height + 36;
+        }
+
+        double layerGap = 45;
+        for (Edge ue : unitEdges) {
+          LayoutUnit src = unitMap.get(ue.fromId);
+          LayoutUnit dst = unitMap.get(ue.toId);
+          if (src != null && dst != null && src.layer <= l && l < dst.layer) {
+            if (ue.label != null && !ue.label.trim().isEmpty()) {
+              double lw = ue.label.trim().length() * 6.5 + 24;
+              layerGap = Math.max(layerGap, lw + 24);
+            }
+          }
+        }
+        curX += mw + layerGap;
+      }
+    }
+  }
+
+  private static void layoutCompoundComponent(
+      MermaidGraph graph, Direction compDir, boolean isHorizontal, GraphComponent comp) {
+
+    // 1. Find root subgraphs and recursively compute internal sizes
+    List<Subgraph> rootSgs = new ArrayList<>();
+    for (Subgraph sg : comp.subgraphs) {
+      if (sg.parent == null || !comp.subgraphs.contains(sg.parent)) {
+        rootSgs.add(sg);
+      }
+    }
+    for (Subgraph sg : rootSgs) {
+      computeSubgraphSizes(
+          sg,
+          rootSgs.size() == 1 && sg.direction != null ? sg.direction : compDir,
+          comp.edges,
+          graph.subgraphEdges,
+          comp.nodes);
+    }
+
+    // 2. Build LayoutUnits for root subgraphs and standalone nodes
+    List<LayoutUnit> units = new ArrayList<>();
+    Map<String, LayoutUnit> unitMap = new LinkedHashMap<>();
+
+    for (Subgraph sg : rootSgs) {
+      LayoutUnit u = new LayoutUnit(sg);
+      units.add(u);
+      unitMap.put(u.id, u);
+    }
+    for (Node n : comp.nodes.values()) {
+      if (n.parentSubgraph == null) {
+        LayoutUnit u = new LayoutUnit(n);
+        units.add(u);
+        unitMap.put(u.id, u);
+      }
+    }
+
+    if (units.size() == 1) {
+      LayoutUnit u = units.get(0);
+      u.x = 0;
+      u.y = 0;
+      if (u.subgraph != null) {
+        u.subgraph.x = 0;
+        u.subgraph.y = 0;
+        assignAbsoluteCoordinates(u.subgraph, 0, 0);
+      }
+      return;
+    }
+
+    // 3. Map each Node ID to its root LayoutUnit
+    Map<String, LayoutUnit> nodeToUnit = new HashMap<>();
+    for (LayoutUnit u : units) {
+      if (u.node != null) {
+        nodeToUnit.put(u.node.id, u);
+      } else if (u.subgraph != null) {
+        registerNodesToUnit(u.subgraph, u, nodeToUnit);
+      }
+    }
+
+    // 4. Build Unit Edges (meta-graph)
+    List<Edge> unitEdges = new ArrayList<>();
+    Set<String> seenUnitEdges = new HashSet<>();
+    for (Edge e : comp.edges) {
+      LayoutUnit u1 = nodeToUnit.get(e.fromId);
+      LayoutUnit u2 = nodeToUnit.get(e.toId);
+      if (u1 != null && u2 != null && !u1.id.equals(u2.id)) {
+        String key = u1.id + "->" + u2.id;
+        if (!seenUnitEdges.contains(key)) {
+          seenUnitEdges.add(key);
+          Edge ue = new Edge(u1.id, u2.id, e.label, e.stroke, e.arrow);
+          unitEdges.add(ue);
+        }
+      }
+    }
+    for (SubgraphEdge se : graph.subgraphEdges) {
+      Subgraph sg1 = graph.lookupSubgraph(se.fromSgId);
+      Subgraph sg2 = graph.lookupSubgraph(se.toSgId);
+      if (sg1 != null && sg2 != null) {
+        String s1Id = getSubgraphSampleNodeId(sg1);
+        String s2Id = getSubgraphSampleNodeId(sg2);
+        LayoutUnit u1 = s1Id != null ? nodeToUnit.get(s1Id) : unitMap.get("sg_" + sg1.id);
+        LayoutUnit u2 = s2Id != null ? nodeToUnit.get(s2Id) : unitMap.get("sg_" + sg2.id);
+        if (u1 != null && u2 != null && !u1.id.equals(u2.id)) {
+          String key = u1.id + "->" + u2.id;
+          if (!seenUnitEdges.contains(key)) {
+            seenUnitEdges.add(key);
+            Edge ue = new Edge(u1.id, u2.id, se.label, se.stroke, se.arrow);
+            unitEdges.add(ue);
+          }
+        }
+      }
+    }
+
+    // 5. Run Sugiyama Layout on Units
+    layoutUnits(isHorizontal, units, unitMap, unitEdges);
+
+    // 6. Assign Absolute Coordinates
+    for (LayoutUnit u : units) {
+      if (u.subgraph != null) {
+        u.subgraph.x = u.x;
+        u.subgraph.y = u.y;
+        assignAbsoluteCoordinates(u.subgraph, u.x, u.y);
+      } else if (u.node != null) {
+        u.node.x = u.x;
+        u.node.y = u.y;
+      }
+    }
+  }
+
+  private static void computeSubgraphSizes(
+      Subgraph sg,
+      Direction parentDirection,
+      List<Edge> edges,
+      List<SubgraphEdge> subgraphEdges,
+      Map<String, Node> allNodes) {
+    Direction dir = sg.direction != null ? sg.direction : parentDirection;
+    boolean isHorizontal = (dir == Direction.LR || dir == Direction.RL);
+
+    // 1. Recursively compute internal sizes of all child subgraphs
+    for (Subgraph child : sg.children) {
+      computeSubgraphSizes(child, dir, edges, subgraphEdges, allNodes);
+    }
+
+    double padding = 20;
+    double headerH = 28;
+
+    // 2. Build LayoutUnits for direct child subgraphs and direct child nodes
+    List<LayoutUnit> units = new ArrayList<>();
+    Map<String, LayoutUnit> unitMap = new LinkedHashMap<>();
+
+    for (Subgraph child : sg.children) {
+      LayoutUnit u = new LayoutUnit(child);
+      units.add(u);
+      unitMap.put(u.id, u);
+    }
+    for (Node n : sg.nodes) {
+      LayoutUnit u = new LayoutUnit(n);
+      units.add(u);
+      unitMap.put(u.id, u);
+    }
+
+    if (units.isEmpty()) {
+      sg.width = 100;
+      sg.height = 60;
+      return;
+    }
+
+    if (units.size() == 1) {
+      LayoutUnit u = units.get(0);
+      u.x = padding;
+      u.y = headerH + padding;
+      if (u.subgraph != null) {
+        u.subgraph.relX = u.x;
+        u.subgraph.relY = u.y;
+      } else if (u.node != null) {
+        u.node.relX = u.x;
+        u.node.relY = u.y;
+      }
+      sg.width = u.width + padding * 2;
+      sg.height = u.height + padding * 2 + headerH;
+      return;
+    }
+
+    // 3. Map each Node ID to its immediate LayoutUnit inside sg
+    Map<String, LayoutUnit> nodeToUnit = new HashMap<>();
+    for (LayoutUnit u : units) {
+      if (u.node != null) {
+        nodeToUnit.put(u.node.id, u);
+      } else if (u.subgraph != null) {
+        registerNodesToUnit(u.subgraph, u, nodeToUnit);
+      }
+    }
+
+    // 4. Build Unit Edges (meta-graph) for edges where both endpoints are in sg
+    List<Edge> unitEdges = new ArrayList<>();
+    Set<String> seenUnitEdges = new HashSet<>();
+    for (Edge e : edges) {
+      LayoutUnit u1 = nodeToUnit.get(e.fromId);
+      LayoutUnit u2 = nodeToUnit.get(e.toId);
+      if (u1 != null && u2 != null && !u1.id.equals(u2.id)) {
+        String key = u1.id + "->" + u2.id;
+        if (!seenUnitEdges.contains(key)) {
+          seenUnitEdges.add(key);
+          Edge ue = new Edge(u1.id, u2.id, e.label, e.stroke, e.arrow);
+          unitEdges.add(ue);
+        }
+      }
+    }
+    for (SubgraphEdge se : subgraphEdges) {
+      Subgraph sg1 = lookupSubgraphInTree(sg, se.fromSgId);
+      Subgraph sg2 = lookupSubgraphInTree(sg, se.toSgId);
+      if (sg1 != null && sg2 != null) {
+        String s1Id = getSubgraphSampleNodeId(sg1);
+        String s2Id = getSubgraphSampleNodeId(sg2);
+        LayoutUnit u1 = s1Id != null ? nodeToUnit.get(s1Id) : unitMap.get("sg_" + sg1.id);
+        LayoutUnit u2 = s2Id != null ? nodeToUnit.get(s2Id) : unitMap.get("sg_" + sg2.id);
+        if (u1 != null && u2 != null && !u1.id.equals(u2.id)) {
+          String key = u1.id + "->" + u2.id;
+          if (!seenUnitEdges.contains(key)) {
+            seenUnitEdges.add(key);
+            Edge ue = new Edge(u1.id, u2.id, se.label, se.stroke, se.arrow);
+            unitEdges.add(ue);
+          }
+        }
+      }
+    }
+
+    // 5. Run Sugiyama DAG layout on units
+    layoutUnits(isHorizontal, units, unitMap, unitEdges);
+
+    // 6. Assign relative coordinates inside sg and compute sg dimensions
+    double minX = Double.MAX_VALUE, minY = Double.MAX_VALUE;
+    double maxX = Double.MIN_VALUE, maxY = Double.MIN_VALUE;
+    for (LayoutUnit u : units) {
+      minX = Math.min(minX, u.x);
+      minY = Math.min(minY, u.y);
+      maxX = Math.max(maxX, u.x + u.width);
+      maxY = Math.max(maxY, u.y + u.height);
+    }
+
+    for (LayoutUnit u : units) {
+      double rx = padding + (u.x - minX);
+      double ry = headerH + padding + (u.y - minY);
+      if (u.subgraph != null) {
+        u.subgraph.relX = rx;
+        u.subgraph.relY = ry;
+      } else if (u.node != null) {
+        u.node.relX = rx;
+        u.node.relY = ry;
+      }
+    }
+
+    sg.width = (maxX - minX) + padding * 2;
+    sg.height = (maxY - minY) + padding * 2 + headerH;
+  }
+
+  private static @Nullable Subgraph lookupSubgraphInTree(Subgraph root, String id) {
+    if (root.id.equals(id)) return root;
+    for (Subgraph child : root.children) {
+      Subgraph res = lookupSubgraphInTree(child, id);
+      if (res != null) return res;
+    }
+    return null;
+  }
+
+  private static void assignAbsoluteCoordinates(Subgraph sg, double parentAbsX, double parentAbsY) {
+    for (Subgraph child : sg.children) {
+      child.x = parentAbsX + child.relX;
+      child.y = parentAbsY + child.relY;
+      assignAbsoluteCoordinates(child, child.x, child.y);
+    }
+    for (Node n : sg.nodes) {
+      n.x = parentAbsX + n.relX;
+      n.y = parentAbsY + n.relY;
+    }
+  }
+
+  private static Direction getSubgraphEffectiveDirection(@Nullable Subgraph sg, Direction fallback) {
+    Subgraph cur = sg;
+    while (cur != null) {
+      if (cur.direction != null) return cur.direction;
+      cur = cur.parent;
+    }
+    return fallback;
+  }
+
+  // =========================================================================
+  // SVG Renderer
+  // =========================================================================
+
+  private static String renderSvg(
+      MermaidGraph graph,
+      boolean isHorizontal,
+      double width,
+      double height) {
+
+    StringBuilder svg = new StringBuilder(4096);
+    svg.append(
+        String.format(
+            "<svg class=\"mermaid-svg\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 %.0f %.0f\" style=\"max-width: %.0fpx; width: 100%%; height: auto;\">\n",
+            width, height, width));
+
+    svg.append("  <defs>\n");
+    svg.append(
+        "    <marker id=\"mermaid-arrow\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\">\n");
+    svg.append("      <path d=\"M 0 1.5 L 10 5 L 0 8.5 z\" fill=\"#64748b\" />\n");
+    svg.append("    </marker>\n");
+    svg.append("    <filter id=\"node-shadow\" x=\"-5%\" y=\"-5%\" width=\"115%\" height=\"120%\">\n");
+    svg.append("      <feDropShadow dx=\"0\" dy=\"1.5\" stdDeviation=\"2\" flood-color=\"#0f172a\" flood-opacity=\"0.06\" />\n");
+    svg.append("    </filter>\n");
+    svg.append("  </defs>\n");
+
+    // 1. Render Subgraphs (sorted by depth so parent containers render before child containers)
+    List<Subgraph> sortedSubgraphs = new ArrayList<>(graph.allSubgraphs);
+    sortedSubgraphs.sort(Comparator.comparingInt(SimpleMermaidRenderer::getSubgraphDepth));
+    for (Subgraph sg : sortedSubgraphs) {
+      renderSubgraph(svg, sg);
+    }
+
+    // 2. Render Subgraph Edges
+    for (SubgraphEdge se : graph.subgraphEdges) {
+      Subgraph sg1 = graph.subgraphsMap.get(se.fromSgId);
+      Subgraph sg2 = graph.subgraphsMap.get(se.toSgId);
+      if (sg1 != null && sg2 != null) {
+        boolean sgEdgeHorizontal = isHorizontal;
+        if (sg1.parent != null && sg1.parent.equals(sg2.parent)) {
+          Direction sgDir = getSubgraphEffectiveDirection(sg1.parent, graph.direction);
+          sgEdgeHorizontal = (sgDir == Direction.LR || sgDir == Direction.RL);
+        }
+        renderSubgraphEdge(svg, sgEdgeHorizontal, graph, sg1, sg2, se);
+      }
+    }
+
+    // 3. Render Node Edges
+    for (Edge e : graph.edges) {
+      Node src = graph.nodes.get(e.fromId);
+      Node dst = graph.nodes.get(e.toId);
+      if (src != null && dst != null) {
+        boolean edgeHorizontal = isHorizontal;
+        if (src.parentSubgraph != null && src.parentSubgraph.equals(dst.parentSubgraph)) {
+          Direction sgDir = getSubgraphEffectiveDirection(src.parentSubgraph, graph.direction);
+          edgeHorizontal = (sgDir == Direction.LR || sgDir == Direction.RL);
+        }
+        renderEdge(svg, edgeHorizontal, graph, src, dst, e);
+      }
+    }
+
+    // 4. Render Nodes
+    for (Node n : graph.nodes.values()) {
+      renderNode(svg, n);
+    }
+
+    svg.append("</svg>");
+    return svg.toString();
+  }
+
+  private static void renderSubgraph(StringBuilder svg, Subgraph sg) {
+    int depth = getSubgraphDepth(sg);
+    String fill = sg.customFill != null ? sg.customFill : (depth % 2 == 0 ? "#fafafa" : "#f8fafc");
+    String stroke = sg.customStroke != null ? sg.customStroke : "#cbd5e1";
+    svg.append(
+        String.format(
+            "  <rect x=\"%.1f\" y=\"%.1f\" width=\"%.1f\" height=\"%.1f\" rx=\"8\" fill=\"%s\" stroke=\"%s\" stroke-width=\"1.5\" stroke-dasharray=\"4,4\" />\n",
+            sg.x, sg.y, sg.width, sg.height, fill, stroke));
+    if (sg.title != null && !sg.title.isEmpty()) {
+      svg.append(
+          String.format(
+              "  <text x=\"%.1f\" y=\"%.1f\" font-size=\"12\" font-weight=\"600\" fill=\"#334155\">%s</text>\n",
+              sg.x + 14, sg.y + 18, escapeXml(sg.title)));
+    }
+  }
+
+  private static void renderNode(StringBuilder svg, Node n) {
+    double rx = 6;
+    if (n.shape == NodeShape.ROUNDED) {
+      rx = 10;
+    } else if (n.shape == NodeShape.STADIUM) {
+      rx = n.height / 2.0;
+    }
+
+    String fill = n.customFill != null ? n.customFill : "#ffffff";
+    String stroke = n.customStroke != null ? n.customStroke : "#64748b";
+
+    // Shape Geometry
+    if (n.shape == NodeShape.CIRCLE) {
+      double r = n.width / 2.0;
+      svg.append(
+          String.format(
+              "  <circle cx=\"%.1f\" cy=\"%.1f\" r=\"%.1f\" fill=\"%s\" stroke=\"%s\" stroke-width=\"1.5\" filter=\"url(#node-shadow)\" />\n",
+              n.x + r, n.y + r, r, fill, stroke));
+    } else if (n.shape == NodeShape.DIAMOND) {
+      double cx = n.x + n.width / 2.0;
+      double cy = n.y + n.height / 2.0;
+      svg.append(
+          String.format(
+              "  <polygon points=\"%.1f,%.1f %.1f,%.1f %.1f,%.1f %.1f,%.1f\" fill=\"%s\" stroke=\"%s\" stroke-width=\"1.5\" filter=\"url(#node-shadow)\" />\n",
+              cx, n.y, n.x + n.width, cy, cx, n.y + n.height, n.x, cy, fill, stroke));
+    } else if (n.shape == NodeShape.HEXAGON) {
+      double h2 = n.height / 2.0;
+      double indent = 16;
+      svg.append(
+          String.format(
+              "  <polygon points=\"%.1f,%.1f %.1f,%.1f %.1f,%.1f %.1f,%.1f %.1f,%.1f %.1f,%.1f\" fill=\"%s\" stroke=\"%s\" stroke-width=\"1.5\" filter=\"url(#node-shadow)\" />\n",
+              n.x + indent, n.y,
+              n.x + n.width - indent, n.y,
+              n.x + n.width, n.y + h2,
+              n.x + n.width - indent, n.y + n.height,
+              n.x + indent, n.y + n.height,
+              n.x, n.y + h2, fill, stroke));
+    } else if (n.shape == NodeShape.CYLINDER) {
+      double ry = 7.0;
+      double rxCyl = n.width / 2.0;
+      double h = n.height;
+      svg.append(
+          String.format(
+              "  <path d=\"M %.1f %.1f a %.1f,%.1f 0 1,0 %.1f,0 a %.1f,%.1f 0 1,0 -%.1f,0 l 0,%.1f a %.1f,%.1f 0 0,0 %.1f,0 l 0,-%.1f Z\" fill=\"%s\" stroke=\"%s\" stroke-width=\"1.5\" filter=\"url(#node-shadow)\" />\n",
+              n.x, n.y + ry, rxCyl, ry, n.width, rxCyl, ry, n.width, h - ry * 2, rxCyl, ry, n.width, h - ry * 2, fill, stroke));
+      svg.append(
+          String.format(
+              "  <path d=\"M %.1f %.1f a %.1f,%.1f 0 0,0 %.1f,0\" fill=\"none\" stroke=\"%s\" stroke-width=\"1.5\" />\n",
+              n.x, n.y + ry, rxCyl, ry, n.width, stroke));
+    } else if (n.shape == NodeShape.FLAG) {
+      double notch = 12;
+      svg.append(
+          String.format(
+              "  <polygon points=\"%.1f,%.1f %.1f,%.1f %.1f,%.1f %.1f,%.1f %.1f,%.1f\" fill=\"%s\" stroke=\"%s\" stroke-width=\"1.5\" filter=\"url(#node-shadow)\" />\n",
+              n.x, n.y, n.x + n.width, n.y, n.x + n.width - notch, n.y + n.height / 2.0, n.x + n.width, n.y + n.height, n.x, n.y + n.height, fill, stroke));
+    } else if (n.shape == NodeShape.SUBROUTINE) {
+      svg.append(
+          String.format(
+              "  <rect x=\"%.1f\" y=\"%.1f\" width=\"%.1f\" height=\"%.1f\" rx=\"4\" fill=\"%s\" stroke=\"%s\" stroke-width=\"1.5\" filter=\"url(#node-shadow)\" />\n",
+              n.x, n.y, n.width, n.height, fill, stroke));
+      svg.append(
+          String.format(
+              "  <line x1=\"%.1f\" y1=\"%.1f\" x2=\"%.1f\" y2=\"%.1f\" stroke=\"%s\" stroke-width=\"1.5\" />\n",
+              n.x + 10, n.y, n.x + 10, n.y + n.height, stroke));
+      svg.append(
+          String.format(
+              "  <line x1=\"%.1f\" y1=\"%.1f\" x2=\"%.1f\" y2=\"%.1f\" stroke=\"%s\" stroke-width=\"1.5\" />\n",
+              n.x + n.width - 10, n.y, n.x + n.width - 10, n.y + n.height, stroke));
+    } else {
+      svg.append(
+          String.format(
+              "  <rect x=\"%.1f\" y=\"%.1f\" width=\"%.1f\" height=\"%.1f\" rx=\"%.1f\" fill=\"%s\" stroke=\"%s\" stroke-width=\"1.5\" filter=\"url(#node-shadow)\" />\n",
+              n.x, n.y, n.width, n.height, rx, fill, stroke));
+    }
+
+    // Node Text using structured AST labelLines
+    double cx = n.x + n.width / 2.0;
+    double textYOffset = n.shape == NodeShape.CYLINDER ? 4.0 : 0.0;
+    double startTextY = n.y + textYOffset + (n.height - (n.labelLines.size() - 1) * 16) / 2.0;
+
+    if (n.labelLines.size() == 1) {
+      svg.append(
+          String.format(
+              "  <text x=\"%.1f\" y=\"%.1f\" font-size=\"12\" font-weight=\"500\" fill=\"#0f172a\" text-anchor=\"middle\" dominant-baseline=\"central\">%s</text>\n",
+              cx, n.y + textYOffset + n.height / 2.0, escapeXml(n.labelLines.get(0).trim())));
+    } else {
+      svg.append(
+          String.format(
+              "  <text x=\"%.1f\" y=\"%.1f\" font-size=\"12\" text-anchor=\"middle\">\n",
+              cx, startTextY));
+      for (int i = 0; i < n.labelLines.size(); i++) {
+        String weight = i == 0 ? "600" : "400";
+        String textColor = i == 0 ? "#0f172a" : "#475569";
+        String fontSize = i == 0 ? "12" : "10.5";
+        svg.append(
+            String.format(
+                "    <tspan x=\"%.1f\" dy=\"%s\" font-size=\"%s\" font-weight=\"%s\" fill=\"%s\">%s</tspan>\n",
+                cx, i == 0 ? "0" : "16", fontSize, weight, textColor, escapeXml(n.labelLines.get(i).trim())));
+      }
+      svg.append("  </text>\n");
+    }
+  }
+
+  private static void renderSubgraphEdge(
+      StringBuilder svg,
+      boolean isHorizontal,
+      MermaidGraph graph,
+      Subgraph sg1,
+      Subgraph sg2,
+      SubgraphEdge se) {
+
+    String strokeDash = se.stroke == EdgeStroke.DASHED ? "stroke-dasharray=\"4,4\" " : "";
+    String strokeWidth = se.stroke == EdgeStroke.THICK ? "2.5" : "1.5";
+    String marker = se.arrow ? "marker-end=\"url(#mermaid-arrow)\" " : "";
+
+    double startX, startY, endX, endY;
+    if (isHorizontal) {
+      startX = sg1.x + sg1.width;
+      startY = sg1.y + sg1.height / 2.0;
+      endX = sg2.x;
+      endY = sg2.y + sg2.height / 2.0;
+
+      double minY = Math.min(startY, endY) - 10;
+      double maxY = Math.max(startY, endY) + 10;
+      boolean blocked = false;
+      double maxBottom = Math.max(sg1.y + sg1.height, sg2.y + sg2.height);
+      for (Node n : graph.nodes.values()) {
+        if (n.x >= startX - 10 && n.x + n.width <= endX + 10 && n.y <= maxY && n.y + n.height >= minY) {
+          blocked = true;
+          maxBottom = Math.max(maxBottom, n.y + n.height);
+        }
+      }
+
+      if (blocked) {
+        double labelW = (se.label != null && !se.label.trim().isEmpty()) ? se.label.trim().length() * 6.5 + 12 : 20;
+        double loopOffset = Math.max(35.0, labelW / 2.0 + 20.0);
+        double cpY = maxBottom + loopOffset;
+        svg.append(
+            String.format(
+                "  <path d=\"M %.1f %.1f C %.1f %.1f, %.1f %.1f, %.1f %.1f\" fill=\"none\" stroke=\"#64748b\" stroke-width=\"%s\" %s%s/>\n",
+                startX, startY, startX + 20, cpY, endX - 20, cpY, endX, endY, strokeWidth, strokeDash, marker));
+        if (se.label != null && !se.label.trim().isEmpty()) {
+          double midX = (startX + endX) / 2.0;
+          renderEdgeLabelBadge(svg, midX, cpY, se.label.trim());
+        }
+        return;
+      }
+    } else {
+      startX = sg1.x + sg1.width / 2.0;
+      startY = sg1.y + sg1.height;
+      endX = sg2.x + sg2.width / 2.0;
+      endY = sg2.y;
+
+      double minX = Math.min(startX, endX) - 10;
+      double maxX = Math.max(startX, endX) + 10;
+      boolean blocked = false;
+      double maxRight = Math.max(sg1.x + sg1.width, sg2.x + sg2.width);
+      for (Node n : graph.nodes.values()) {
+        if (n.y >= startY - 10 && n.y + n.height <= endY + 10 && n.x <= maxX && n.x + n.width >= minX) {
+          blocked = true;
+          maxRight = Math.max(maxRight, n.x + n.width);
+        }
+      }
+
+      if (blocked) {
+        double labelW = (se.label != null && !se.label.trim().isEmpty()) ? se.label.trim().length() * 6.5 + 12 : 20;
+        double loopOffset = Math.max(35.0, labelW / 2.0 + 20.0);
+        double cpX = maxRight + loopOffset;
+        svg.append(
+            String.format(
+                "  <path d=\"M %.1f %.1f C %.1f %.1f, %.1f %.1f, %.1f %.1f\" fill=\"none\" stroke=\"#64748b\" stroke-width=\"%s\" %s%s/>\n",
+                startX, startY, cpX, startY + 20, cpX, endY - 20, endX, endY, strokeWidth, strokeDash, marker));
+        if (se.label != null && !se.label.trim().isEmpty()) {
+          double midY = (startY + endY) / 2.0;
+          renderEdgeLabelBadge(svg, cpX, midY, se.label.trim());
+        }
+        return;
+      }
+    }
+
+    svg.append(
+        String.format(
+            "  <line x1=\"%.1f\" y1=\"%.1f\" x2=\"%.1f\" y2=\"%.1f\" stroke=\"#64748b\" stroke-width=\"%s\" %s%s/>\n",
+            startX, startY, endX, endY, strokeWidth, strokeDash, marker));
+
+    if (se.label != null && !se.label.trim().isEmpty()) {
+      double midX = (startX + endX) / 2.0;
+      double midY = (startY + endY) / 2.0;
+      renderEdgeLabelBadge(svg, midX, midY, se.label.trim());
+    }
+  }
+
+  private static void renderEdge(
+      StringBuilder svg,
+      boolean isHorizontal,
+      MermaidGraph graph,
+      Node src,
+      Node dst,
+      Edge e) {
+
+    String strokeDash = e.stroke == EdgeStroke.DASHED ? "stroke-dasharray=\"4,4\" " : "";
+    String strokeWidth = e.stroke == EdgeStroke.THICK ? "2.5" : "1.5";
+    String marker = e.arrow ? "marker-end=\"url(#mermaid-arrow)\" " : "";
+
+    double x1, y1, x2, y2;
+    double cp1x, cp1y, cp2x, cp2y;
+
+    if (!isHorizontal) {
+      if (src.id.equals(dst.id)) {
+        // Self-loop
+        x1 = src.x + src.width;
+        y1 = src.y + src.height * 0.3;
+        x2 = src.x + src.width;
+        y2 = src.y + src.height * 0.7;
+        cp1x = x1 + 35;
+        cp1y = y1 - 20;
+        cp2x = x2 + 35;
+        cp2y = y2 + 20;
+      } else if (e.isBackEdge || src.layer > dst.layer) {
+        // Cycle back-edge loop
+        x1 = src.x + src.width;
+        y1 = src.y + src.height / 2.0;
+        x2 = dst.x + dst.width;
+        y2 = dst.y + dst.height / 2.0;
+        double maxRight = Math.max(x1, x2);
+        int minL = Math.min(src.layer, dst.layer);
+        int maxL = Math.max(src.layer, dst.layer);
+        for (Node n : graph.nodes.values()) {
+          if (n.layer >= minL && n.layer <= maxL) {
+            maxRight = Math.max(maxRight, n.x + n.width);
+          }
+        }
+        for (Edge oe : graph.edges) {
+          for (Node v : oe.virtualNodes) {
+            if (v.layer >= minL && v.layer <= maxL) {
+              maxRight = Math.max(maxRight, v.x + v.width);
+            }
+          }
+        }
+        double labelW =
+            (e.label != null && !e.label.trim().isEmpty()) ? (e.label.trim().length() * 6.5 + 16) : 0;
+        double loopOffset =
+            Math.max(45.0, labelW / 2.0 + 36.0) + (maxRight - Math.min(x1, x2)) * 0.4;
+        cp1x = maxRight + loopOffset;
+        cp1y = y1;
+        cp2x = maxRight + loopOffset;
+        cp2y = y2;
+      } else if (!e.virtualNodes.isEmpty()) {
+        List<Double> px = new ArrayList<>();
+        List<Double> py = new ArrayList<>();
+        px.add(src.x + src.width / 2.0);
+        py.add(src.y + src.height);
+        for (Node v : e.virtualNodes) {
+          px.add(v.x + v.width / 2.0);
+          py.add(v.y + v.height / 2.0);
+        }
+        px.add(dst.x + dst.width / 2.0);
+        py.add(dst.y);
+
+        StringBuilder pathD = new StringBuilder();
+        pathD.append(String.format("M %.1f %.1f", px.get(0), py.get(0)));
+        for (int i = 0; i < px.size() - 1; i++) {
+          double xA = px.get(i), yA = py.get(i);
+          double xB = px.get(i + 1), yB = py.get(i + 1);
+          double dy = yB - yA;
+          pathD.append(
+              String.format(
+                  " C %.1f %.1f, %.1f %.1f, %.1f %.1f",
+                  xA, yA + dy * 0.5, xB, yB - dy * 0.5, xB, yB));
+        }
+        svg.append(
+            String.format(
+                "  <path d=\"%s\" fill=\"none\" stroke=\"#64748b\" stroke-width=\"%s\" %s%s/>\n",
+                pathD.toString(), strokeWidth, strokeDash, marker));
+
+        if (e.label != null && !e.label.trim().isEmpty()) {
+          Node firstV = e.virtualNodes.get(0);
+          renderEdgeLabelBadge(svg, firstV.x + firstV.width / 2.0, firstV.y + firstV.height / 2.0, e.label.trim());
+        }
+        return;
+      } else {
+        // Standard forward edge
+        x1 = src.x + src.width / 2.0;
+        y1 = src.y + src.height;
+        x2 = dst.x + dst.width / 2.0;
+        y2 = dst.y;
+        double dy = y2 - y1;
+        cp1x = x1;
+        cp1y = y1 + dy * 0.5;
+        cp2x = x2;
+        cp2y = y1 + dy * 0.5;
+      }
+    } else {
+      // Horizontal (LR)
+      if (src.id.equals(dst.id)) {
+        x1 = src.x + src.width * 0.3;
+        y1 = src.y;
+        x2 = src.x + src.width * 0.7;
+        y2 = src.y;
+        cp1x = x1 - 20;
+        cp1y = y1 - 35;
+        cp2x = x2 + 20;
+        cp2y = y2 - 35;
+      } else if (e.isBackEdge || src.layer > dst.layer) {
+        x1 = src.x + src.width / 2.0;
+        y1 = src.y;
+        x2 = dst.x + dst.width / 2.0;
+        y2 = dst.y;
+        double minTop = Math.min(y1, y2);
+        int minL = Math.min(src.layer, dst.layer);
+        int maxL = Math.max(src.layer, dst.layer);
+        for (Node n : graph.nodes.values()) {
+          if (n.layer >= minL && n.layer <= maxL) {
+            minTop = Math.min(minTop, n.y);
+          }
+        }
+        for (Edge oe : graph.edges) {
+          for (Node v : oe.virtualNodes) {
+            if (v.layer >= minL && v.layer <= maxL) {
+              minTop = Math.min(minTop, v.y);
+            }
+          }
+        }
+        double labelH = 18.0;
+        double loopOffset =
+            Math.max(45.0, labelH + 36.0) + (Math.max(y1, y2) - minTop) * 0.4;
+        cp1x = x1;
+        cp1y = minTop - loopOffset;
+        cp2x = x2;
+        cp2y = minTop - loopOffset;
+      } else if (!e.virtualNodes.isEmpty()) {
+        List<Double> px = new ArrayList<>();
+        List<Double> py = new ArrayList<>();
+        px.add(src.x + src.width);
+        py.add(src.y + src.height / 2.0);
+        for (Node v : e.virtualNodes) {
+          px.add(v.x + v.width / 2.0);
+          py.add(v.y + v.height / 2.0);
+        }
+        px.add(dst.x);
+        py.add(dst.y + dst.height / 2.0);
+
+        StringBuilder pathD = new StringBuilder();
+        pathD.append(String.format("M %.1f %.1f", px.get(0), py.get(0)));
+        for (int i = 0; i < px.size() - 1; i++) {
+          double xA = px.get(i), yA = py.get(i);
+          double xB = px.get(i + 1), yB = py.get(i + 1);
+          double dx = xB - xA;
+          pathD.append(
+              String.format(
+                  " C %.1f %.1f, %.1f %.1f, %.1f %.1f",
+                  xA + dx * 0.5, yA, xB - dx * 0.5, yB, xB, yB));
+        }
+        svg.append(
+            String.format(
+                "  <path d=\"%s\" fill=\"none\" stroke=\"#64748b\" stroke-width=\"%s\" %s%s/>\n",
+                pathD.toString(), strokeWidth, strokeDash, marker));
+
+        if (e.label != null && !e.label.trim().isEmpty()) {
+          Node firstV = e.virtualNodes.get(0);
+          renderEdgeLabelBadge(svg, firstV.x + firstV.width / 2.0, firstV.y + firstV.height / 2.0, e.label.trim());
+        }
+        return;
+      } else {
+        x1 = src.x + src.width;
+        y1 = src.y + src.height / 2.0;
+        x2 = dst.x;
+        y2 = dst.y + dst.height / 2.0;
+        double dx = x2 - x1;
+        cp1x = x1 + dx * 0.5;
+        cp1y = y1;
+        cp2x = x1 + dx * 0.5;
+        cp2y = y2;
+      }
+    }
+
+    svg.append(
+        String.format(
+            "  <path d=\"M %.1f %.1f C %.1f %.1f, %.1f %.1f, %.1f %.1f\" fill=\"none\" stroke=\"#64748b\" stroke-width=\"%s\" %s%s/>\n",
+            x1, y1, cp1x, cp1y, cp2x, cp2y, x2, y2, strokeWidth, strokeDash, marker));
+
+    if (e.label != null && !e.label.trim().isEmpty()) {
+      // Evaluate Cubic Bézier midpoint at t = 0.5
+      double midX = 0.125 * x1 + 0.375 * cp1x + 0.375 * cp2x + 0.125 * x2;
+      double midY = 0.125 * y1 + 0.375 * cp1y + 0.375 * cp2y + 0.125 * y2;
+      renderEdgeLabelBadge(svg, midX, midY, e.label.trim());
+    }
+  }
+
+  private static void renderEdgeLabelBadge(StringBuilder svg, double midX, double midY, String label) {
+    double textLen = label.length() * 6.5;
+    double rectW = textLen + 12;
+    double rectH = 18;
+    svg.append(
+        String.format(
+            "  <rect x=\"%.1f\" y=\"%.1f\" width=\"%.1f\" height=\"%.1f\" rx=\"3\" fill=\"#ffffff\" fill-opacity=\"0.95\" />\n",
+            midX - rectW / 2.0, midY - rectH / 2.0, rectW, rectH));
+    svg.append(
+        String.format(
+            "  <text x=\"%.1f\" y=\"%.1f\" font-size=\"10.5\" fill=\"#475569\" text-anchor=\"middle\" dominant-baseline=\"central\">%s</text>\n",
+            midX, midY, escapeXml(label)));
+  }
+
+  private static String escapeXml(String text) {
+    if (text == null) return "";
+    StringBuilder sb = new StringBuilder(text.length() + 16);
+    for (int i = 0; i < text.length(); i++) {
+      char c = text.charAt(i);
+      // Strip invalid XML 1.0 control characters (valid chars: 0x9, 0xA, 0xD, 0x20+)
+      if (c < 0x20 && c != '\t' && c != '\n' && c != '\r') {
+        continue;
+      }
+      switch (c) {
+        case '&':
+          sb.append("&amp;");
+          break;
+        case '<':
+          sb.append("&lt;");
+          break;
+        case '>':
+          sb.append("&gt;");
+          break;
+        case '"':
+          sb.append("&quot;");
+          break;
+        case '\'':
+          sb.append("&apos;");
+          break;
+        default:
+          sb.append(c);
+          break;
+      }
+    }
+    return sb.toString();
+  }
+
+  private static class GraphComponent {
+    final Map<String, Node> nodes = new LinkedHashMap<>();
+    final List<Edge> edges = new ArrayList<>();
+    final List<Subgraph> subgraphs = new ArrayList<>();
+    double width = 0;
+    double height = 0;
+  }
+
+  private static String findRoot(Map<String, String> parent, String id) {
+    String p = parent.get(id);
+    if (p == null || p.equals(id)) {
+      return id;
+    }
+    String root = findRoot(parent, p);
+    parent.put(id, root);
+    return root;
+  }
+
+  private static void unionSets(Map<String, String> parent, String id1, String id2) {
+    String r1 = findRoot(parent, id1);
+    String r2 = findRoot(parent, id2);
+    if (!r1.equals(r2)) {
+      parent.put(r1, r2);
+    }
+  }
+
+  private static void layoutIsolatedSubgraphs(Direction dir, List<Subgraph> subgraphs) {
+    boolean isHorizontal = (dir == Direction.LR || dir == Direction.RL);
+    double padding = 20;
+    double headerH = 22;
+
+    for (Subgraph sg : subgraphs) {
+      if (sg.nodes.isEmpty()) continue;
+      if (!isHorizontal) {
+        double maxW = 0;
+        for (Node n : sg.nodes) {
+          maxW = Math.max(maxW, n.width);
+        }
+        double curY = headerH + padding;
+        for (Node n : sg.nodes) {
+          n.x = padding + (maxW - n.width) / 2.0;
+          n.y = curY;
+          curY += n.height + 24;
+        }
+        sg.x = 0;
+        sg.y = 0;
+        sg.width = maxW + padding * 2;
+        sg.height = curY + padding;
+      } else {
+        double maxH = 0;
+        for (Node n : sg.nodes) {
+          maxH = Math.max(maxH, n.height);
+        }
+        double curX = padding;
+        for (Node n : sg.nodes) {
+          n.x = curX;
+          n.y = headerH + padding + (maxH - n.height) / 2.0;
+          curX += n.width + 32;
+        }
+        sg.x = 0;
+        sg.y = 0;
+        sg.width = curX + padding;
+        sg.height = maxH + padding * 2 + headerH;
+      }
+    }
+  }
+}
diff --git a/javatests/com/google/gitiles/BUILD b/javatests/com/google/gitiles/BUILD
index b2410ff..03a9841 100644
--- a/javatests/com/google/gitiles/BUILD
+++ b/javatests/com/google/gitiles/BUILD
@@ -40,6 +40,7 @@
     runtime_deps = ["//lib/junit:hamcrest"],
     deps = DEPS + [
         "//java/com/google/gitiles:servlet",
+        "//java/com/google/gitiles/blame/cache",
         ":testutil",
         "//lib:servlet-api",
         "//lib:commonmark",
diff --git a/javatests/com/google/gitiles/blame/cache/BlameCacheTest.java b/javatests/com/google/gitiles/blame/cache/BlameCacheTest.java
new file mode 100644
index 0000000..1ead02a
--- /dev/null
+++ b/javatests/com/google/gitiles/blame/cache/BlameCacheTest.java
@@ -0,0 +1,67 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gitiles.blame.cache;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link BlameCacheImpl.Key}. */
+@RunWith(JUnit4.class)
+public class BlameCacheTest {
+  @Test
+  public void keyEqualsAndHashCode() {
+    ObjectId c1 = ObjectId.fromString("1111111111111111111111111111111111111111");
+    ObjectId c2 = ObjectId.fromString("2222222222222222222222222222222222222222");
+    ObjectId c3 = ObjectId.fromString("3333333333333333333333333333333333333333");
+
+    BlameCacheImpl.Key k1 = new BlameCacheImpl.Key(c1, "foo.txt");
+    BlameCacheImpl.Key k2 = new BlameCacheImpl.Key(c1, "foo.txt", ImmutableSet.of());
+    BlameCacheImpl.Key k3 = new BlameCacheImpl.Key(c1, "foo.txt", ImmutableSet.of(c2, c3));
+    BlameCacheImpl.Key k4 = new BlameCacheImpl.Key(c1, "foo.txt", ImmutableSet.of(c3, c2));
+    BlameCacheImpl.Key k5 = new BlameCacheImpl.Key(c1, "foo.txt", ImmutableSet.of(c2));
+
+    assertThat(k1).isEqualTo(k2);
+    assertThat(k1.hashCode()).isEqualTo(k2.hashCode());
+
+    assertThat(k3).isEqualTo(k4);
+    assertThat(k3.hashCode()).isEqualTo(k4.hashCode());
+
+    assertThat(k1).isNotEqualTo(k3);
+    assertThat(k3).isNotEqualTo(k5);
+  }
+
+  @Test
+  public void keyToString() {
+    ObjectId c1 = ObjectId.fromString("1111111111111111111111111111111111111111");
+    ObjectId c2 = ObjectId.fromString("2222222222222222222222222222222222222222");
+    ObjectId c3 = ObjectId.fromString("3333333333333333333333333333333333333333");
+
+    BlameCacheImpl.Key kNoIgnore = new BlameCacheImpl.Key(c1, "foo.txt");
+    assertThat(kNoIgnore.toString()).isEqualTo("1111111111111111111111111111111111111111:foo.txt");
+
+    BlameCacheImpl.Key kWithIgnore =
+        new BlameCacheImpl.Key(c1, "foo.txt", ImmutableSet.of(c3, c2));
+    assertThat(kWithIgnore.toString())
+        .isEqualTo(
+            "1111111111111111111111111111111111111111:foo.txt"
+                + " ignore=[2222222222222222222222222222222222222222,"
+                + " 3333333333333333333333333333333333333333]");
+  }
+}
diff --git a/javatests/com/google/gitiles/doc/GitilesMarkdownTest.java b/javatests/com/google/gitiles/doc/GitilesMarkdownTest.java
index 74e4c3e..2e51b5f 100644
--- a/javatests/com/google/gitiles/doc/GitilesMarkdownTest.java
+++ b/javatests/com/google/gitiles/doc/GitilesMarkdownTest.java
@@ -74,6 +74,231 @@
     assertThat(html).contains("title: Kittens");
   }
 
+  @Test
+  public void renderMermaidDiagram() {
+    String md =
+        "```mermaid\n"
+            + "graph LR\n"
+            + "    subgraph CoreGarden\n"
+            + "        A[\"Fluffy Puppy\"]\n"
+            + "    end\n"
+            + "    subgraph GreenLawn\n"
+            + "        B[\"Playful Kitten\"]\n"
+            + "    end\n"
+            + "    B --> A\n"
+            + "```\n";
+    Config cfg = new Config();
+    cfg.setBoolean("markdown", null, "mermaid", true);
+    MarkdownConfig mc = new MarkdownConfig(cfg);
+    Node node = GitilesMarkdown.parse(mc, md);
+    SafeHtml html =
+        MarkdownToHtml.builder()
+            .setConfig(mc)
+            .setFilePath("index.md")
+            .build()
+            .toSoyHtml(node);
+    assertThat(html).isNotNull();
+    String htmlStr = html.getSafeHtmlString();
+    assertThat(htmlStr).contains("class=\"mermaid-container\"");
+    assertThat(htmlStr).contains("<svg class=\"mermaid-svg\"");
+    assertThat(htmlStr).contains("Fluffy Puppy");
+    assertThat(htmlStr).contains("Playful Kitten");
+  }
+
+  @Test
+  public void renderMermaidWithSubgraphAndDAG() {
+    String md =
+        "```mermaid\n"
+            + "graph TD\n"
+            + "    ClientApp[Little Puppy Plays] --> Extras(Sweet Kitten)\n"
+            + "    ClientApp --> Utils(Happy Bunny)\n"
+            + "    Utils --> ServiceDiscovery[Red Apple Berry]\n"
+            + "    Utils --> ModelManager[Yellow Banana Snack]\n"
+            + "    Extras --> Recognition(Fluffy Duckling)\n"
+            + "    Recognition --> SODA(Green Frog Jump)\n"
+            + "    Recognition --> S3(Sunny Daisy Flower)\n"
+            + "    subgraph Play Park Garden\n"
+            + "        Executors(Teddy Bear)\n"
+            + "        Errors(Wooden Blocks)\n"
+            + "        Protos(Toy Wagon)\n"
+            + "    end\n"
+            + "    Recognition -.-> PlayParkGarden\n"
+            + "```\n";
+    Config cfg = new Config();
+    cfg.setBoolean("markdown", null, "mermaid", true);
+    MarkdownConfig mc = new MarkdownConfig(cfg);
+    Node node = GitilesMarkdown.parse(mc, md);
+    SafeHtml html =
+        MarkdownToHtml.builder()
+            .setConfig(mc)
+            .setFilePath("index.md")
+            .build()
+            .toSoyHtml(node);
+    assertThat(html).isNotNull();
+    String htmlStr = html.getSafeHtmlString();
+    assertThat(htmlStr).contains("Play Park Garden");
+    assertThat(htmlStr).contains("Little Puppy Plays");
+    assertThat(htmlStr).contains("Sweet Kitten");
+    assertThat(htmlStr).contains("Happy Bunny");
+    assertThat(htmlStr).contains("Teddy Bear");
+    assertThat(htmlStr).contains("Wooden Blocks");
+    assertThat(htmlStr).contains("Toy Wagon");
+  }
+
+  @Test
+  public void renderMermaidWithQuotedBracketsAndDiamond() {
+    String md =
+        "```mermaid\n"
+            + "graph TD\n"
+            + "    A[\"Happy little bunny jumps\"]\n"
+            + "    B[\"Locate [TeddyBear] in garden\"]\n"
+            + "    A --> B\n"
+            + "    B --> C[\"Sing Sweet Melody\"]\n"
+            + "    C --> D[\"Dance Around Blossom Tree\"]\n"
+            + "    E{\"Is kitten happy & pure?\"}\n"
+            + "    E -- Yes --> F[\"Give Tasty Cookie Treat\"]\n"
+            + "    F --> G[\"Play With Soft Yarn Ball\"]\n"
+            + "    E -- No --> H[\"Read Gentle Story Book\"]\n"
+            + "    H --> I[\"Warm Cozy Blanket Nap\"]\n"
+            + "    G --> J[\"Wake Up In Morning Sun\"]\n"
+            + "    I --> J\n"
+            + "    J --> K[\"Smile At Rainbow Sky\"]\n"
+            + "```\n";
+    Config cfg = new Config();
+    cfg.setBoolean("markdown", null, "mermaid", true);
+    MarkdownConfig mc = new MarkdownConfig(cfg);
+    Node node = GitilesMarkdown.parse(mc, md);
+    SafeHtml html =
+        MarkdownToHtml.builder()
+            .setConfig(mc)
+            .setFilePath("index.md")
+            .build()
+            .toSoyHtml(node);
+    assertThat(html).isNotNull();
+    String htmlStr = html.getSafeHtmlString();
+    assertThat(htmlStr).contains("Is kitten happy &amp;");
+    assertThat(htmlStr).contains("pure?");
+  }
+
+  @Test
+  public void renderMermaidWithBidirectionalEdges() {
+    String md =
+        "```mermaid\n"
+            + "graph TD\n"
+            + "    Puppy[\"Fluffy Puppy\"]\n"
+            + "    Kitten[\"Playful Kitten\"]\n"
+            + "    Bunny[\"Little Bunny\"]\n"
+            + "    Puppy -->|Roll Ball| Kitten\n"
+            + "    Kitten -->|Chase Toy| Bunny\n"
+            + "    Bunny -->|Share Snack| Kitten\n"
+            + "    Kitten -->|Give Hug| Puppy\n"
+            + "```\n";
+    Config cfg = new Config();
+    cfg.setBoolean("markdown", null, "mermaid", true);
+    MarkdownConfig mc = new MarkdownConfig(cfg);
+    Node node = GitilesMarkdown.parse(mc, md);
+    SafeHtml html =
+        MarkdownToHtml.builder()
+            .setConfig(mc)
+            .setFilePath("index.md")
+            .build()
+            .toSoyHtml(node);
+    assertThat(html).isNotNull();
+    String htmlStr = html.getSafeHtmlString();
+    assertThat(htmlStr).contains("Fluffy Puppy");
+    assertThat(htmlStr).contains("Playful Kitten");
+    assertThat(htmlStr).contains("Roll Ball");
+    assertThat(htmlStr).contains("Give Hug");
+  }
+
+  @Test
+  public void testMarkdownConfigIFrameAndExtensions() {
+    Config cfg = new Config();
+    cfg.setBoolean("markdown", null, "safehtml", true);
+    cfg.setString("markdown", null, "allowiframe", "https://example.com/embed/");
+    MarkdownConfig mc = new MarkdownConfig(cfg);
+    assertThat(mc.isIFrameAllowed("https://example.com/embed/123")).isTrue();
+    assertThat(mc.isIFrameAllowed("https://other.com/embed/123")).isFalse();
+
+    Config anyCfg = new Config();
+    anyCfg.setBoolean("markdown", null, "safehtml", true);
+    anyCfg.setBoolean("markdown", null, "allowiframe", true);
+    MarkdownConfig anyMc = new MarkdownConfig(anyCfg);
+    assertThat(anyMc.isIFrameAllowed("https://anything.com")).isTrue();
+
+    MarkdownConfig copied = mc.copyWithExtensions(
+        java.util.Collections.singleton("toc"),
+        java.util.Collections.singleton("autolink"));
+    assertThat(copied).isNotNull();
+  }
+
+  @Test
+  public void testMarkdownConfigMermaidDisabledFallsBackToCodeBlock() {
+    String md = "```mermaid\ngraph TD\n  A --> B\n```\n";
+    Config cfg = new Config();
+    cfg.setBoolean("markdown", null, "mermaid", false);
+    MarkdownConfig mc = new MarkdownConfig(cfg);
+    Node node = GitilesMarkdown.parse(mc, md);
+    SafeHtml html =
+        MarkdownToHtml.builder()
+            .setConfig(mc)
+            .setFilePath("index.md")
+            .build()
+            .toSoyHtml(node);
+    assertThat(html).isNotNull();
+    String htmlStr = html.getSafeHtmlString();
+    // When disabled, it renders as a code pre block, not a mermaid svg container
+    assertThat(htmlStr).doesNotContain("class=\"mermaid-container\"");
+    assertThat(htmlStr).contains("<pre class=\"code\">");
+    assertThat(htmlStr).contains("graph TD");
+  }
+
+  @Test
+  public void testSecurityMermaidMarkdownEndToEndNoScriptOrIframeInjection() {
+    String md =
+        "# Diagram Title\n\n"
+            + "```mermaid\n"
+            + "graph TD\n"
+            + "  A[\"<script>alert('xss-1')</script>\"]\n"
+            + "  B[\"<iframe src='javascript:alert(2)'></iframe>\"]\n"
+            + "  C[\"<img src=x onerror=alert('xss-3')>\"]\n"
+            + "  D[\"<foreignObject><iframe src='https://evil.com'></iframe></foreignObject>\"]\n"
+            + "  A -->|\"<script>alert('edge')</script>\"| B\n"
+            + "  B --> C --> D\n"
+            + "  click A href \"javascript:alert('click')\"\n"
+            + "```\n";
+
+    Config cfg = new Config();
+    cfg.setBoolean("markdown", null, "mermaid", true);
+    MarkdownConfig mc = new MarkdownConfig(cfg);
+    Node node = GitilesMarkdown.parse(mc, md);
+    SafeHtml html =
+        MarkdownToHtml.builder()
+            .setConfig(mc)
+            .setFilePath("index.md")
+            .build()
+            .toSoyHtml(node);
+
+    assertThat(html).isNotNull();
+    String htmlStr = html.getSafeHtmlString();
+
+    // Verify rendered HTML has the mermaid-container and SVG
+    assertThat(htmlStr).contains("<div class=\"mermaid-container\"><svg class=\"mermaid-svg\"");
+    String mermaidHtml = htmlStr.substring(htmlStr.indexOf("<div class=\"mermaid-container\">"));
+
+    // Verify absolutely no executable script, iframe, or event handlers exist in Mermaid HTML
+    assertThat(mermaidHtml).doesNotContain("<script");
+    assertThat(mermaidHtml).doesNotContain("<iframe");
+    assertThat(mermaidHtml).doesNotContain("<foreignObject");
+    assertThat(mermaidHtml).doesNotContain("<img");
+    assertThat(mermaidHtml).doesNotContain("<a ");
+
+    // Verify payload is safely escaped as XML text inside SVG tspans
+    assertThat(htmlStr).contains("&lt;script&gt;alert(&apos;xss-1&apos;)&lt;/script&gt;");
+    assertThat(htmlStr).contains("&lt;iframe src=&apos;javascript:alert(2)&apos;&gt;&lt;/iframe&gt;");
+    assertThat(htmlStr).contains("&lt;img src=x onerror=alert(&apos;xss-3&apos;)&gt;");
+  }
+
   private static String render(String markdown, boolean frontMatter) {
     Config cfg = new Config();
     cfg.setBoolean("markdown", null, "frontmatter", frontMatter);
diff --git a/javatests/com/google/gitiles/doc/SimpleMermaidRendererSecurityTest.java b/javatests/com/google/gitiles/doc/SimpleMermaidRendererSecurityTest.java
new file mode 100644
index 0000000..ecaa1e3
--- /dev/null
+++ b/javatests/com/google/gitiles/doc/SimpleMermaidRendererSecurityTest.java
@@ -0,0 +1,431 @@
+// Copyright 2026 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gitiles.doc;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Deep security test suite for {@link SimpleMermaidRenderer} covering SVG injection, XSS vectors,
+ * XML entity attacks, protocol exploits, tag breakouts, and parser robustness.
+ */
+@RunWith(JUnit4.class)
+public class SimpleMermaidRendererSecurityTest {
+
+  // =========================================================================
+  // 1. Script Element Injections
+  // =========================================================================
+
+  @Test
+  public void testScriptTagVariationsInNodeLabels() {
+    List<String> payloads =
+        Arrays.asList(
+            "<script>alert(1)</script>",
+            "<script src='https://evil.com/xss.js'></script>",
+            "<script src='//evil.com/xss.js'/>",
+            "<sCrIpT>alert('case')</ScRiPt>",
+            "<SCRIPT/SRC=\"data:text/javascript,alert(1)\">",
+            "<script xmlns=\"http://www.w3.org/1999/xhtml\">alert(1)</script>",
+            "<script><![CDATA[alert(1)]]></script>",
+            "<script>/*<![CDATA[*/alert(1)/*]]>*/</script>",
+            "<script defer>alert(1)</script>",
+            "<script async>alert(1)</script>",
+            "<script type=\"module\">import 'https://evil.com/x.js';</script>",
+            "<script type=\"text/javascript\">alert(String.fromCharCode(88,83,83))</script>");
+
+    for (String payload : payloads) {
+      String code = "graph TD\n  A[\"" + payload.replace("\"", "\\\"") + "\"] --> B\n";
+      assertSafeSvg(code, payload);
+    }
+  }
+
+  // =========================================================================
+  // 2. ForeignObject and Embedded HTML Injections
+  // =========================================================================
+
+  @Test
+  public void testForeignObjectAndEmbeddedHtmlInLabels() {
+    List<String> payloads =
+        Arrays.asList(
+            "<foreignObject><body xmlns=\"http://www.w3.org/1999/xhtml\"><script>alert(1)</script></body></foreignObject>",
+            "<foreignObject><iframe src=\"javascript:alert(1)\"></iframe></foreignObject>",
+            "<foreignObject><iframe src=\"https://evil.com\"></iframe></foreignObject>",
+            "<foreignObject><form action=\"//evil.com\"><input type=\"password\" name=\"pass\"></form></foreignObject>",
+            "<foreignObject><embed src=\"evil.swf\"></embed></foreignObject>",
+            "<foreignObject><object data=\"javascript:alert(1)\"></object></foreignObject>",
+            "<foreignObject><audio src=\"x\" onerror=\"alert(1)\"></audio></foreignObject>",
+            "<foreignObject><video src=\"x\" onerror=\"alert(1)\"></video></foreignObject>",
+            "<foreignObject width=\"100\" height=\"100\"><div xmlns=\"http://www.w3.org/1999/xhtml\"><span>HTML Content</span></div></foreignObject>");
+
+    for (String payload : payloads) {
+      String code = "graph TD\n  A[\"" + payload.replace("\"", "\\\"") + "\"] --> B\n";
+      assertSafeSvg(code, payload);
+    }
+  }
+
+  // =========================================================================
+  // 3. Event Handler Attributes Injections
+  // =========================================================================
+
+  @Test
+  public void testEventHandlerAttributesInLabels() {
+    List<String> payloads =
+        Arrays.asList(
+            "<svg onload=\"alert('svg-onload')\">",
+            "<svg onresize=\"alert(1)\">",
+            "<img src=\"invalid-image.jpg\" onerror=\"alert('img-onerror')\">",
+            "<body onload=\"alert('body-onload')\">",
+            "<rect onmouseover=\"alert('rect-hover')\">",
+            "<circle onclick=\"alert('circle-click')\">",
+            "<polygon onfocus=\"alert('poly-focus')\">",
+            "<text onpointerdown=\"alert('pointer')\">",
+            "<path onanimationstart=\"alert('anim')\">",
+            "<g ontouchstart=\"alert('touch')\">",
+            "x\" onfocus=\"alert(1)\" autofocus=\"",
+            "x' onclick='alert(1)' x='",
+            "x` onmouseover=`alert(1)` `");
+
+    for (String payload : payloads) {
+      String code = "graph TD\n  A[\"" + payload.replace("\"", "\\\"") + "\"] --> B\n";
+      assertSafeSvg(code, payload);
+    }
+  }
+
+  // =========================================================================
+  // 4. Anchor and Protocol Injections
+  // =========================================================================
+
+  @Test
+  public void testAnchorAndProtocolInjectionsInLabels() {
+    List<String> payloads =
+        Arrays.asList(
+            "<a href=\"javascript:alert('a-href')\">Click Link</a>",
+            "<a xlink:href=\"javascript:alert('xlink-href')\">Click Link</a>",
+            "<a href=\"data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==\">Data Link</a>",
+            "<a href=\"vbscript:msgbox(1)\">VBScript Link</a>",
+            "<a href=\"java\0script:alert(1)\">Null Byte Protocol</a>",
+            "<a href=\"java&#x0A;script:alert(1)\">Newline Protocol</a>",
+            "<a href=\"java&#x09;script:alert(1)\">Tab Protocol</a>",
+            "<a href=\"javascript&colon;alert(1)\">Entity Colon Protocol</a>",
+            "<a href=\"blob:https://evil.com/uuid\">Blob Link</a>");
+
+    for (String payload : payloads) {
+      String code = "graph TD\n  A[\"" + payload.replace("\"", "\\\"") + "\"] --> B\n";
+      assertSafeSvg(code, payload);
+    }
+  }
+
+  // =========================================================================
+  // 5. SVG Animation Exploits (<animate>, <set>)
+  // =========================================================================
+
+  @Test
+  public void testSvgAnimationExploitsInLabels() {
+    List<String> payloads =
+        Arrays.asList(
+            "<animate onbegin=\"alert('anim-begin')\" attributeName=\"x\" dur=\"1s\" />",
+            "<animate onend=\"alert('anim-end')\" attributeName=\"x\" dur=\"1s\" />",
+            "<animate onrepeat=\"alert('anim-repeat')\" attributeName=\"x\" dur=\"1s\" />",
+            "<animate attributeName=\"href\" values=\"javascript:alert(1)\" dur=\"1s\" />",
+            "<animate attributeName=\"xlink:href\" values=\"javascript:alert(1)\" dur=\"1s\" />",
+            "<set onbegin=\"alert('set-begin')\" attributeName=\"x\" dur=\"1s\" />",
+            "<set attributeName=\"onmouseover\" to=\"alert(1)\" />",
+            "<animateTransform attributeName=\"transform\" type=\"rotate\" from=\"0\" to=\"360\" onend=\"alert(1)\" />");
+
+    for (String payload : payloads) {
+      String code = "graph TD\n  A[\"" + payload.replace("\"", "\\\"") + "\"] --> B\n";
+      assertSafeSvg(code, payload);
+    }
+  }
+
+  // =========================================================================
+  // 6. SVG Resource and Reference Exploits (<use>, <image>, <feImage>)
+  // =========================================================================
+
+  @Test
+  public void testSvgResourceAndReferenceExploitsInLabels() {
+    List<String> payloads =
+        Arrays.asList(
+            "<use href=\"javascript:alert(1)\" />",
+            "<use xlink:href=\"javascript:alert(1)\" />",
+            "<use href=\"data:image/svg+xml;utf8,<svg id='x' xmlns='http://www.w3.org/2000/svg'><script>alert(1)</script></svg>#x\" />",
+            "<use xlink:href=\"https://evil.com/payload.svg#icon\" />",
+            "<image href=\"javascript:alert(1)\" />",
+            "<image xlink:href=\"javascript:alert(1)\" />",
+            "<image href=\"data:image/svg+xml;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==\" />",
+            "<feImage href=\"javascript:alert(1)\" />",
+            "<feImage xlink:href=\"https://evil.com/tracker.png\" />",
+            "<pattern href=\"javascript:alert(1)\" />",
+            "<pattern xlink:href=\"https://evil.com/xss.svg\" />");
+
+    for (String payload : payloads) {
+      String code = "graph TD\n  A[\"" + payload.replace("\"", "\\\"") + "\"] --> B\n";
+      assertSafeSvg(code, payload);
+    }
+  }
+
+  // =========================================================================
+  // 7. CSS and Style Injections
+  // =========================================================================
+
+  @Test
+  public void testCssAndStyleInjectionsInLabels() {
+    List<String> payloads =
+        Arrays.asList(
+            "<style>@import 'javascript:alert(1)';</style>",
+            "<style>body { background: url(\"javascript:alert(1)\"); }</style>",
+            "<style>* { -moz-binding: url('http://evil.com/xss.xml#test'); }</style>",
+            "<style>svg { behavior: url(xss.htc); }</style>",
+            "<style>@keyframes xss { from { background-image: url('javascript:alert(1)'); } } </style>",
+            "<div style=\"fill:expression(alert(1))\">Styled Div</div>",
+            "<div style=\"background-image:url(javascript:alert(1))\">Background</div>",
+            "<div style=\"behavior:url(xss.htc)\">HTC Component</div>");
+
+    for (String payload : payloads) {
+      String code = "graph TD\n  A[\"" + payload.replace("\"", "\\\"") + "\"] --> B\n";
+      assertSafeSvg(code, payload);
+    }
+  }
+
+  // =========================================================================
+  // 8. XML Structure and CDATA Breakout Payloads
+  // =========================================================================
+
+  @Test
+  public void testXmlStructureAndCdataBreakoutPayloads() {
+    List<String> payloads =
+        Arrays.asList(
+            "</text></svg><script>alert('tag-breakout')</script><svg><text>",
+            "</tspan></text><iframe src='javascript:alert(1)'></iframe><text><tspan>",
+            "]]><script>alert('cdata-breakout')</script><![CDATA[",
+            "--> <script>alert('comment-breakout')</script> <!--",
+            "<?xml-stylesheet href=\"javascript:alert(1)\"?>",
+            "<!DOCTYPE svg [ <!ENTITY xxe SYSTEM \"file:///etc/passwd\"> ]>&xxe;",
+            "&lt;script&gt;alert(1)&lt;/script&gt;",
+            "\"><script>alert('quote-breakout')</script>",
+            "'\"><script>alert('double-quote-breakout')</script>",
+            "`><script>alert('backtick-breakout')</script>");
+
+    for (String payload : payloads) {
+      String code = "graph TD\n  A[\"" + payload.replace("\"", "\\\"") + "\"] --> B\n";
+      assertSafeSvg(code, payload);
+    }
+  }
+
+  // =========================================================================
+  // 9. Edge Labels and Infix Strokes Injection
+  // =========================================================================
+
+  @Test
+  public void testEdgeLabelsAndInfixStrokesSecurity() {
+    List<String> payloads =
+        Arrays.asList(
+            "<script>alert('pipe-edge')</script>",
+            "<iframe src='javascript:alert(1)'></iframe>",
+            "<img src=x onerror=alert('edge-onerror')>",
+            "<svg onload=alert('edge-onload')>",
+            "<foreignObject><script>alert(1)</script></foreignObject>",
+            "</text></svg><script>alert('edge-breakout')</script><svg><text>");
+
+    for (String payload : payloads) {
+      String codePipe = "graph TD\n  A -->|\"" + payload.replace("\"", "\\\"") + "\"| B\n";
+      assertSafeSvg(codePipe, payload);
+
+      String codeInfixSolid = "graph TD\n  A -- \"" + payload.replace("\"", "\\\"") + "\" --> B\n";
+      assertSafeSvg(codeInfixSolid, payload);
+
+      String codeInfixThick = "graph TD\n  A == \"" + payload.replace("\"", "\\\"") + "\" ==> B\n";
+      assertSafeSvg(codeInfixThick, payload);
+
+      String codeInfixDashed = "graph TD\n  A -. \"" + payload.replace("\"", "\\\"") + "\" .-> B\n";
+      assertSafeSvg(codeInfixDashed, payload);
+    }
+  }
+
+  // =========================================================================
+  // 10. Subgraph Titles and Aliases Injection
+  // =========================================================================
+
+  @Test
+  public void testSubgraphTitlesAndAliasesSecurity() {
+    List<String> payloads =
+        Arrays.asList(
+            "<script>alert('subgraph')</script>",
+            "<iframe src='javascript:alert(1)'></iframe>",
+            "<img src=x onerror=alert('sg-img')>",
+            "<foreignObject><iframe src='https://evil.com'></iframe></foreignObject>",
+            "</text></svg><script>alert('sg-breakout')</script><svg><text>");
+
+    for (String payload : payloads) {
+      String code =
+          "graph TD\n"
+              + "  subgraph Sg [\"" + payload.replace("\"", "\\\"") + "\"]\n"
+              + "    A[Node A]\n"
+              + "  end\n"
+              + "  A --> B\n";
+      assertSafeSvg(code, payload);
+    }
+  }
+
+  // =========================================================================
+  // 11. Mermaid Directive Stripping Verification
+  // =========================================================================
+
+  @Test
+  public void testMermaidDirectivesCannotInjectCode() {
+    String code =
+        "graph TD\n"
+            + "  A[Node A] --> B[Node B]\n"
+            + "  click A href \"javascript:alert('click-href')\"\n"
+            + "  click B call alert('click-call')\n"
+            + "  click A \"javascript:alert('click-positional')\"\n"
+            + "  style A fill:url(javascript:alert('style-fill'))\n"
+            + "  style B stroke:url(data:image/svg+xml,<svg onload=alert(1)>)\n"
+            + "  classDef evil fill:red,stroke:url(javascript:alert(1));\n"
+            + "  class A evil\n"
+            + "  linkStyle 0 stroke:url(javascript:alert(1));\n";
+
+    SvgDoc svg = SvgDoc.render(code);
+    svg.assertNoDangerousTags();
+
+    String raw = SimpleMermaidRenderer.renderToSvg(code).get();
+    assertThat(raw).doesNotContain("javascript:");
+    assertThat(raw).doesNotContain("href=");
+    assertThat(raw).doesNotContain("xlink:href");
+    assertThat(raw).doesNotContain("onclick=");
+    assertThat(raw).doesNotContain("onmouseover=");
+  }
+
+  // =========================================================================
+  // 12. Parser Stress, Recursion, and Malformed Characters
+  // =========================================================================
+
+  @Test
+  public void testNullBytesAndControlCharactersInInput() {
+    String codeWithNulls = "graph TD\n  A[\"Null\0Byte\u0001Control\u0008Test\"] --> B\n";
+    SvgDoc svg = SvgDoc.render(codeWithNulls);
+    svg.assertNoDangerousTags();
+  }
+
+  @Test
+  public void testDeeplyNestedSubgraphsDosResistance() {
+    StringBuilder sb = new StringBuilder();
+    sb.append("graph TD\n");
+    int depth = 25;
+    for (int i = 0; i < depth; i++) {
+      sb.append("  subgraph Level").append(i).append(" [\"Level ").append(i).append("\"]\n");
+    }
+    sb.append("    A[Deep Node]\n");
+    for (int i = 0; i < depth; i++) {
+      sb.append("  end\n");
+    }
+    sb.append("  A --> B[Outer Node]\n");
+
+    SvgDoc svg = SvgDoc.render(sb.toString());
+    svg.assertNoDangerousTags();
+  }
+
+  @Test
+  public void testDenseCyclesAndMutualEdgesDosResistance() {
+    StringBuilder sb = new StringBuilder();
+    sb.append("graph TD\n");
+    for (int i = 0; i < 30; i++) {
+      sb.append("  N").append(i).append(" --> N").append((i + 1) % 30).append("\n");
+      sb.append("  N").append((i + 1) % 30).append(" --> N").append(i).append("\n");
+    }
+    SvgDoc svg = SvgDoc.render(sb.toString());
+    svg.assertNoDangerousTags();
+  }
+
+  // =========================================================================
+  // 13. End-to-End MarkdownToHtml & Gitiles Integration
+  // =========================================================================
+
+  @Test
+  public void testEndToEndMarkdownXssPrevention() {
+    String md =
+        "# Security Audit Title\n\n"
+            + "```mermaid\n"
+            + "graph TD\n"
+            + "  A[\"<script>alert('e2e-node')</script>\"]\n"
+            + "  B[\"<iframe src='javascript:alert(1)'></iframe>\"]\n"
+            + "  C[\"<img src=x onerror=alert('e2e-img')>\"]\n"
+            + "  D[\"<foreignObject><iframe src='http://evil.com'></iframe></foreignObject>\"]\n"
+            + "  A -->|\"<script>alert('e2e-edge')</script>\"| B\n"
+            + "  B --> C --> D\n"
+            + "  click A href \"javascript:alert('e2e-click')\"\n"
+            + "```\n";
+
+    Config cfg = new Config();
+    cfg.setBoolean("markdown", null, "mermaid", true);
+    MarkdownConfig mc = new MarkdownConfig(cfg);
+    org.commonmark.node.Node node = GitilesMarkdown.parse(mc, md);
+    com.google.common.html.types.SafeHtml html =
+        MarkdownToHtml.builder()
+            .setConfig(mc)
+            .setFilePath("security_test.md")
+            .build()
+            .toSoyHtml(node);
+
+    assertThat(html).isNotNull();
+    String htmlStr = html.getSafeHtmlString();
+
+    // Verify container and SVG exist
+    assertThat(htmlStr).contains("<div class=\"mermaid-container\"><svg class=\"mermaid-svg\"");
+    String mermaidPart = htmlStr.substring(htmlStr.indexOf("<div class=\"mermaid-container\">"));
+
+    // Verify no executable HTML tags exist in the Mermaid output
+    for (String tag : SvgDoc.DANGEROUS_TAGS) {
+      assertThat(mermaidPart).doesNotContain("<" + tag);
+      assertThat(mermaidPart).doesNotContain("</" + tag + ">");
+    }
+
+    // Verify all payloads are safely encoded as XML entity text
+    assertThat(htmlStr).contains("&lt;script&gt;alert(&apos;e2e-node&apos;)&lt;/script&gt;");
+    assertThat(htmlStr).contains("&lt;iframe src=&apos;javascript:alert(1)&apos;&gt;&lt;/iframe&gt;");
+    assertThat(htmlStr).contains("&lt;img src=x onerror=alert(&apos;e2e-img&apos;)&gt;");
+  }
+
+  // =========================================================================
+  // Helper Methods for Strict Security Validation
+  // =========================================================================
+
+  private void assertSafeSvg(String mermaidCode, String originalPayload) {
+    Optional<String> svgOpt = SimpleMermaidRenderer.renderToSvg(mermaidCode);
+    assertThat(svgOpt.isPresent()).isTrue();
+
+    String rawSvg = svgOpt.get();
+
+    // 1. Raw string inspection for any unescaped tags
+    for (String tag : SvgDoc.DANGEROUS_TAGS) {
+      assertThat(rawSvg).doesNotContain("<" + tag);
+      assertThat(rawSvg).doesNotContain("</" + tag + ">");
+    }
+
+    // 2. XML DOM inspection ensuring strictly valid XML document with 0 dangerous elements
+    SvgDoc svg = new SvgDoc(rawSvg);
+    svg.assertAllInvariants();
+
+    // 3. Confirm original payload text is preserved in text nodes
+    assertThat(svg.findText(originalPayload)).isNotNull();
+  }
+}
diff --git a/javatests/com/google/gitiles/doc/SimpleMermaidRendererTest.java b/javatests/com/google/gitiles/doc/SimpleMermaidRendererTest.java
new file mode 100644
index 0000000..6ae35c1
--- /dev/null
+++ b/javatests/com/google/gitiles/doc/SimpleMermaidRendererTest.java
@@ -0,0 +1,1411 @@
+// Copyright 2026 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gitiles.doc;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Splitter;
+import com.google.common.primitives.Doubles;
+import java.util.List;
+import java.util.Optional;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.w3c.dom.Element;
+
+/** Comprehensive XML DOM-based unit tests for {@link SimpleMermaidRenderer}. */
+@RunWith(JUnit4.class)
+public class SimpleMermaidRendererTest {
+
+  @Test
+  public void testConstructorInstantiation() {
+    SimpleMermaidRenderer renderer = new SimpleMermaidRenderer();
+    assertThat(renderer).isNotNull();
+  }
+
+  @Test
+  public void testBasicGraphTDExactSvgStructure() {
+    SvgDoc svg = render("graph TD\n  A --> B\n");
+    svg.assertDefs();
+
+    // Verify exactly 2 node rects and 1 edge path
+    assertThat(svg.getElementsByTag("rect").size()).isEqualTo(2);
+    assertThat(svg.getElementsByTag("path").size()).isEqualTo(2); // 1 arrow in defs + 1 edge
+
+    // Verify exact node texts
+    List<String> texts = svg.getAllTextContents();
+    assertThat(texts).containsExactly("A", "B").inOrder();
+
+    // Verify edge has marker and cubic bezier curve
+    Element edgePath = svg.getEdgePaths().get(0);
+    assertThat(edgePath.getAttribute("marker-end")).isEqualTo("url(#mermaid-arrow)");
+    assertThat(edgePath.getAttribute("stroke")).isEqualTo("#64748b");
+    assertThat(edgePath.getAttribute("fill")).isEqualTo("none");
+    assertThat(edgePath.getAttribute("d")).startsWith("M ");
+    assertThat(edgePath.getAttribute("d")).contains(" C ");
+  }
+
+  @Test
+  public void testGraphDirectionsAndFallbacks() {
+    for (String dir : new String[] {"TD", "TB", "LR", "RL", "BT"}) {
+      SvgDoc svg = render("graph " + dir + "\n  A --> B\n");
+      assertThat(svg.getAllTextContents()).containsExactly("A", "B").inOrder();
+    }
+    // Default direction when omitted
+    SvgDoc defSvg = render("graph\n  A --> B\n");
+    assertThat(defSvg.getAllTextContents()).containsExactly("A", "B").inOrder();
+
+    // Invalid direction fallback to TD
+    SvgDoc fallbackSvg = render("flowchart INVALID\n  A --> B\n");
+    assertThat(fallbackSvg.getAllTextContents()).containsExactly("A", "B").inOrder();
+  }
+
+  @Test
+  public void testAllNodeShapesExactSvgElements() {
+    String code =
+        "graph TD\n"
+            + "  A[Rectangle Box]\n"
+            + "  B(Rounded Ball)\n"
+            + "  C([Stadium Ring])\n"
+            + "  D[[Subroutine Cart]]\n"
+            + "  E[(Cylinder Drum)]\n"
+            + "  F((Circle Star))\n"
+            + "  G{{Hexagon Block}}\n"
+            + "  H{Diamond Kite}\n"
+            + "  I>Asymmetric Flag]\n";
+    SvgDoc svg = render(code);
+
+    // Exact text elements
+    List<String> texts = svg.getAllTextContents();
+    assertThat(texts).containsExactly(
+        "Rectangle Box", "Rounded Ball", "Stadium Ring", "Subroutine Cart", "Cylinder Drum", "Circle Star", "Hexagon Block", "Diamond Kite", "Asymmetric Flag")
+        .inOrder();
+
+    // Verify Diamond has a polygon with 4 vertices
+    Element diamond = svg.findPolygonWithVertices(4);
+    assertThat(diamond).isNotNull();
+    assertThat(diamond.getAttribute("fill")).isEqualTo("#ffffff");
+    assertThat(diamond.getAttribute("stroke")).isEqualTo("#64748b");
+
+    // Verify Hexagon has a polygon with 6 vertices
+    Element hexagon = svg.findPolygonWithVertices(6);
+    assertThat(hexagon).isNotNull();
+
+    // Verify Asymmetric flag has a polygon with 5 vertices
+    Element flag = svg.findPolygonWithVertices(5);
+    assertThat(flag).isNotNull();
+
+    // Verify Subroutine has rect with 2 inner border lines
+    List<Element> lines = svg.getElementsByTag("line");
+    assertThat(lines.size()).isEqualTo(2);
+
+    // Verify Cylinder has 2 paths (body + top rim arc)
+    List<Element> paths = svg.getEdgePaths();
+    assertThat(paths.size()).isEqualTo(2);
+  }
+
+  @Test
+  public void testMultilineNodeLabelsWithTspans() {
+    String code =
+        "graph TD\n"
+            + "  A[\"Sunny Blue Sky<br/>Warm Golden Sun<br/>Soft Green Grass\"]\n"
+            + "  B[\"Little Red Apple\"]\n"
+            + "  A --> B\n";
+    SvgDoc svg = render(code);
+
+    List<Element> tspans = svg.getElementsByTag("tspan");
+    assertThat(tspans.size()).isEqualTo(3);
+    assertThat(tspans.get(0).getTextContent()).isEqualTo("Sunny Blue Sky");
+    assertThat(tspans.get(0).getAttribute("font-weight")).isEqualTo("600");
+    assertThat(tspans.get(0).getAttribute("dy")).isEqualTo("0");
+
+    assertThat(tspans.get(1).getTextContent()).isEqualTo("Warm Golden Sun");
+    assertThat(tspans.get(1).getAttribute("font-weight")).isEqualTo("400");
+    assertThat(tspans.get(1).getAttribute("dy")).isEqualTo("16");
+
+    assertThat(tspans.get(2).getTextContent()).isEqualTo("Soft Green Grass");
+    assertThat(tspans.get(2).getAttribute("font-weight")).isEqualTo("400");
+    assertThat(tspans.get(2).getAttribute("dy")).isEqualTo("16");
+
+    Element singleLineText = svg.findText("Little Red Apple");
+    assertThat(singleLineText).isNotNull();
+    assertThat(singleLineText.getAttribute("dominant-baseline")).isEqualTo("central");
+  }
+
+  @Test
+  public void testQuotedLabelsWithBracketsAndEntities() {
+    String code =
+        "graph TD\n"
+            + "  A[\"Play with teddy bear\"]\n"
+            + "  B[\"Find [Puppy] in cozy room\"]\n"
+            + "  C{\"Is kitten <tiny> & 'sweet'?\"}\n"
+            + "  A --> B --> C\n";
+    SvgDoc svg = render(code);
+
+    // XML parsing confirms correct unescaping of &lt;, &gt;, &apos;, &amp;
+    assertThat(svg.getAllTextContents()).containsExactly(
+        "Play with teddy bear",
+        "Find [Puppy] in cozy room",
+        "Is kitten <tiny> & 'sweet'?")
+        .inOrder();
+  }
+
+  @Test
+  public void testQuotedCleanLabel() {
+    SvgDoc svg = render("graph TD\n  A[\"\\\"Sleepy Kitten\\\"\"] --> B\n");
+    assertThat(svg.getAllTextContents()).containsExactly("Sleepy Kitten", "B").inOrder();
+  }
+
+  @Test
+  public void testAllEdgeTypesAndInfixLabelsExactAttributes() {
+    String code =
+        "graph LR\n"
+            + "  A -->|Yellow Duck| B\n"
+            + "  B ---|Blue Bird| C\n"
+            + "  C -.->|Green Frog| D\n"
+            + "  D ==>|Red Puppy| E\n"
+            + "  E -- Orange Kitten --> F\n"
+            + "  F -- Purple Bunny --- G\n"
+            + "  G == White Lamb ==> H\n"
+            + "  H == Pink Piggy === I\n"
+            + "  I -. Brown Bear .-> J\n"
+            + "  J -. Gray Mouse .- K\n"
+            + "  K -.- L\n"
+            + "  L === M\n"
+            + "  M <--> N\n";
+    SvgDoc svg = render(code);
+
+    List<Element> edgePaths = svg.getEdgePaths();
+    assertThat(edgePaths.size()).isEqualTo(13);
+
+    // Verify dashed stroke
+    Element dashedEdge = edgePaths.get(2);
+    assertThat(dashedEdge.getAttribute("stroke-dasharray")).isEqualTo("4,4");
+    assertThat(dashedEdge.getAttribute("marker-end")).isEqualTo("url(#mermaid-arrow)");
+
+    // Verify thick stroke
+    Element thickEdge = edgePaths.get(3);
+    assertThat(thickEdge.getAttribute("stroke-width")).isEqualTo("2.5");
+    assertThat(thickEdge.getAttribute("marker-end")).isEqualTo("url(#mermaid-arrow)");
+
+    // Verify unarrowed solid line
+    Element unarrowedLine = edgePaths.get(1);
+    assertThat(unarrowedLine.getAttribute("marker-end")).isEmpty();
+
+    // Verify edge labels
+    List<String> texts = svg.getAllTextContents();
+    assertThat(texts).contains("Yellow Duck");
+    assertThat(texts).contains("Blue Bird");
+    assertThat(texts).contains("Green Frog");
+    assertThat(texts).contains("Red Puppy");
+    assertThat(texts).contains("Orange Kitten");
+    assertThat(texts).contains("Purple Bunny");
+    assertThat(texts).contains("White Lamb");
+    assertThat(texts).contains("Pink Piggy");
+    assertThat(texts).contains("Brown Bear");
+    assertThat(texts).contains("Gray Mouse");
+  }
+
+  @Test
+  public void testSubgraphsWithTitlesAliasesAndDirectionOverrides() {
+    String code =
+        "graph TD\n"
+            + "  subgraph Playground Park\n"
+            + "    direction LR\n"
+            + "    E1(puppy)\n"
+            + "    E2(kitten)\n"
+            + "  end\n"
+            + "  subgraph ToyHouse [\"Magic Toy House\"]\n"
+            + "    direction INVALID_DIR\n"
+            + "    S1[(Teddy)]\n"
+            + "  end\n"
+            + "  subgraph \"Music Tree Castle\"\n"
+            + "    T1[Wooden Blocks]\n"
+            + "  end\n"
+            + "  subgraph MeadowHill [Sunny Meadow Hill]\n"
+            + "    U1[Little Duck]\n"
+            + "  end\n"
+            + "  Baby --> E1\n"
+            + "  E1 --> ToyHouse\n"
+            + "  ToyHouse --> T1\n"
+            + "  T1 --> MeadowHill\n";
+    SvgDoc svg = render(code);
+    svg.assertNoLabelNodeOverlaps();
+    svg.assertSubgraphsDoNotOverlap();
+
+    // Verify 4 subgraph boundary rects (stroke-dasharray="4,4")
+    List<Element> subgraphs = svg.findSubgraphRects();
+    assertThat(subgraphs.size()).isEqualTo(4);
+    for (Element sgRect : subgraphs) {
+      assertThat(sgRect.getAttribute("stroke-dasharray")).isEqualTo("4,4");
+      assertThat(sgRect.getAttribute("rx")).isEqualTo("8");
+    }
+
+    // Verify subgraph titles
+    assertThat(svg.findText("Playground Park")).isNotNull();
+    assertThat(svg.findText("Magic Toy House")).isNotNull();
+    assertThat(svg.findText("Music Tree Castle")).isNotNull();
+    assertThat(svg.findText("Sunny Meadow Hill")).isNotNull();
+  }
+
+  @Test
+  public void testSubgraphToSubgraphAndSubgraphToNodeEdges() {
+    String codeTD =
+        "graph TD\n"
+            + "  subgraph SubA [\"Garden A\"]\n"
+            + "    A1[Daisy Flower]\n"
+            + "  end\n"
+            + "  subgraph SubB [\"Garden B\"]\n"
+            + "    B1[Tulip Flower]\n"
+            + "  end\n"
+            + "  SubA -->|Garden Link TD| SubB\n"
+            + "  SubA -->|Flower Link| NodeC[Red Rose]\n"
+            + "  NodeC -->|Petal Link| SubB\n";
+    SvgDoc svgTD = render(codeTD);
+    assertThat(svgTD.findText("Garden Link TD")).isNotNull();
+    assertThat(svgTD.findText("Flower Link")).isNotNull();
+    assertThat(svgTD.findText("Petal Link")).isNotNull();
+    assertThat(svgTD.getElementsByTag("path")).isNotEmpty();
+
+    String codeLR =
+        "graph LR\n"
+            + "  subgraph SubA [\"Garden A\"]\n"
+            + "    A1[Daisy Flower]\n"
+            + "  end\n"
+            + "  subgraph SubB [\"Garden B\"]\n"
+            + "    B1[Tulip Flower]\n"
+            + "  end\n"
+            + "  SubA -->|Garden Link LR| SubB\n";
+    SvgDoc svgLR = render(codeLR);
+    assertThat(svgLR.findText("Garden Link LR")).isNotNull();
+
+    String codeLRWithBlockedNode =
+        "graph LR\n"
+            + "  subgraph SubA [\"Garden A\"]\n"
+            + "    A1[Daisy Flower]\n"
+            + "  end\n"
+            + "  subgraph SubB [\"Garden B\"]\n"
+            + "    B1[Tulip Flower]\n"
+            + "  end\n"
+            + "  SubA -->|Garden Link LR Blocked| SubB\n"
+            + "  SubA -->|Flower Link LR| NodeC[Red Rose]\n"
+            + "  NodeC -->|Petal Link LR| SubB\n";
+    SvgDoc svgLRBlocked = render(codeLRWithBlockedNode);
+    assertThat(svgLRBlocked.findText("Garden Link LR Blocked")).isNotNull();
+    assertThat(svgLRBlocked.findText("Flower Link LR")).isNotNull();
+    assertThat(svgLRBlocked.findText("Petal Link LR")).isNotNull();
+  }
+
+  @Test
+  public void testNestedSubgraphsExactHierarchy() {
+    String code =
+        "graph TD\n"
+            + "  subgraph Sandbox [\"Play Sandbox\"]\n"
+            + "    subgraph SandCastle [\"Sand Castle\"]\n"
+            + "      SPA[\"Red Bucket\"]\n"
+            + "    end\n"
+            + "    subgraph ToyPond [\"Toy Pond\"]\n"
+            + "      CS[\"Yellow Boat\"]\n"
+            + "    end\n"
+            + "  end\n"
+            + "  SPA -->|Water Splash| CS\n";
+    SvgDoc svg = render(code);
+
+    // Exactly 3 subgraph boxes (1 outer + 2 inner)
+    List<Element> subgraphs = svg.findSubgraphRects();
+    assertThat(subgraphs.size()).isEqualTo(3);
+
+    assertThat(svg.findText("Play Sandbox")).isNotNull();
+    assertThat(svg.findText("Sand Castle")).isNotNull();
+    assertThat(svg.findText("Toy Pond")).isNotNull();
+    assertThat(svg.findText("Red Bucket")).isNotNull();
+    assertThat(svg.findText("Yellow Boat")).isNotNull();
+    assertThat(svg.findText("Water Splash")).isNotNull();
+  }
+
+  @Test
+  public void testNestedSubgraphsHorizontal() {
+    String code =
+        "graph LR\n"
+            + "  subgraph Outer [\"Playhouse\"]\n"
+            + "    subgraph InnerA [\"Kitten Corner\"]\n"
+            + "      A1[Soft Pillow]\n"
+            + "    end\n"
+            + "    subgraph InnerB [\"Puppy Corner\"]\n"
+            + "      B1[Squeaky Ball]\n"
+            + "    end\n"
+            + "  end\n"
+            + "  A1 -->|Play Time| B1\n";
+    SvgDoc svg = render(code);
+    assertThat(svg.findText("Play Time")).isNotNull();
+  }
+
+  @Test
+  public void testSubgraphWithMixedChildrenAndDirectNodes() {
+    String codeTD =
+        "graph TD\n"
+            + "  subgraph OuterTD [\"Tree House TD\"]\n"
+            + "    subgraph InnerTD [\"Bird Nest TD\"]\n"
+            + "      A1[Baby Bird TD]\n"
+            + "    end\n"
+            + "    D1[Little Squirrel TD]\n"
+            + "  end\n"
+            + "  A1 --> D1\n";
+    SvgDoc svgTD = render(codeTD);
+    assertThat(svgTD.findText("Tree House TD")).isNotNull();
+    assertThat(svgTD.findText("Bird Nest TD")).isNotNull();
+    assertThat(svgTD.findText("Little Squirrel TD")).isNotNull();
+
+    String codeLR =
+        "graph LR\n"
+            + "  subgraph OuterLR [\"Tree House LR\"]\n"
+            + "    subgraph InnerLR [\"Bird Nest LR\"]\n"
+            + "      A1[Baby Bird LR]\n"
+            + "    end\n"
+            + "    D1[Little Squirrel LR]\n"
+            + "  end\n"
+            + "  A1 --> D1\n";
+    SvgDoc svgLR = render(codeLR);
+    assertThat(svgLR.findText("Tree House LR")).isNotNull();
+    assertThat(svgLR.findText("Bird Nest LR")).isNotNull();
+    assertThat(svgLR.findText("Little Squirrel LR")).isNotNull();
+  }
+
+  @Test
+  public void testNestedSubgraphWithLabeledAdjacentEdge() {
+    String codeTD =
+        "graph TD\n"
+            + "  subgraph SubTD [\"Animal Farm TD\"]\n"
+            + "    A[Happy Lamb]\n"
+            + "    B[Little Pony]\n"
+            + "    A -->|Green Grass TD| B\n"
+            + "  end\n";
+    SvgDoc svgTD = render(codeTD);
+    assertThat(svgTD.findText("Green Grass TD")).isNotNull();
+
+    String codeLR =
+        "graph LR\n"
+            + "  subgraph SubLR [\"Animal Farm LR\"]\n"
+            + "    A[Happy Lamb]\n"
+            + "    B[Little Pony]\n"
+            + "    A -->|Green Grass LR| B\n"
+            + "  end\n";
+    SvgDoc svgLR = render(codeLR);
+    assertThat(svgLR.findText("Green Grass LR")).isNotNull();
+  }
+
+  @Test
+  public void testSugiyamaLateralAdjacentLabeledEdge() {
+    String code =
+        "graph TD\n"
+            + "  A1[Fuzzy Panda] --> B1[Baby Giraffe]\n"
+            + "  A2[Little Koala] --> B2[Tiny Hamster]\n"
+            + "  A1 -->|Sunny Day| A2\n"
+            + "  B1 -->|Happy Play| B2\n";
+    SvgDoc svg = render(code);
+    assertThat(svg.findText("Sunny Day")).isNotNull();
+    assertThat(svg.findText("Happy Play")).isNotNull();
+  }
+
+  @Test
+  public void testBidirectionalMutualEdgesBothOrientations() {
+    String codeTD =
+        "graph TD\n"
+            + "  A[\"Little Lamb\"]\n"
+            + "  B[\"Sweet Bunny\"]\n"
+            + "  A -->|Hop Down| B\n"
+            + "  B -->|Jump Up| A\n";
+    SvgDoc svgTD = render(codeTD);
+    assertThat(svgTD.findText("Hop Down")).isNotNull();
+    assertThat(svgTD.findText("Jump Up")).isNotNull();
+    // Exactly 2 mutual curved edge paths
+    assertThat(svgTD.getEdgePaths().size()).isEqualTo(2);
+
+    String codeLR =
+        "graph LR\n"
+            + "  A[\"Little Lamb\"]\n"
+            + "  B[\"Sweet Bunny\"]\n"
+            + "  A -->|Run Forward| B\n"
+            + "  B -->|Run Backward| A\n";
+    SvgDoc svgLR = render(codeLR);
+    assertThat(svgLR.findText("Run Forward")).isNotNull();
+    assertThat(svgLR.findText("Run Backward")).isNotNull();
+    assertThat(svgLR.getEdgePaths().size()).isEqualTo(2);
+  }
+
+  @Test
+  public void testCycleDetectionAndLoopbackBothOrientations() {
+    String codeTD =
+        "graph TD\n"
+            + "  A --> B\n"
+            + "  B --> C\n"
+            + "  C -->|Loop TD| A\n";
+    SvgDoc svgTD = render(codeTD);
+    assertThat(svgTD.findText("Loop TD")).isNotNull();
+    assertThat(svgTD.getEdgePaths().size()).isEqualTo(3);
+
+    String codeLR =
+        "graph LR\n"
+            + "  A --> B\n"
+            + "  B --> C\n"
+            + "  C -->|Loop LR| A\n";
+    SvgDoc svgLR = render(codeLR);
+    assertThat(svgLR.findText("Loop LR")).isNotNull();
+    assertThat(svgLR.getEdgePaths().size()).isEqualTo(3);
+  }
+
+  @Test
+  public void testSkipLayerBypassBothOrientations() {
+    String codeTD =
+        "graph TD\n"
+            + "  A --> B\n"
+            + "  B --> C\n"
+            + "  A -->|Skip TD| C\n";
+    SvgDoc svgTD = render(codeTD);
+    assertThat(svgTD.findText("Skip TD")).isNotNull();
+
+    String codeLR =
+        "graph LR\n"
+            + "  A --> B\n"
+            + "  B --> C\n"
+            + "  A -->|Skip LR| C\n";
+    SvgDoc svgLR = render(codeLR);
+    assertThat(svgLR.findText("Skip LR")).isNotNull();
+  }
+
+  @Test
+  public void testHorizontalGraphWithCycleAndLongSpanEdge() {
+    String code =
+        "graph LR\n"
+            + "  A[Happy Kitten] --> B[Playful Puppy]\n"
+            + "  B --> C[Cozy Hamster]\n"
+            + "  C -->|Run Back| A\n"
+            + "  A -->|Long Leap| C\n";
+    SvgDoc doc = render(code);
+    assertThat(doc.findText("Happy Kitten")).isNotNull();
+    assertThat(doc.findText("Playful Puppy")).isNotNull();
+    assertThat(doc.findText("Cozy Hamster")).isNotNull();
+    assertThat(doc.findText("Run Back")).isNotNull();
+    assertThat(doc.findText("Long Leap")).isNotNull();
+  }
+
+  @Test
+  public void testNestedSubgraphWithLooseSourceCompaction() {
+    String code =
+        "graph TD\n"
+            + "  subgraph Meadow [\"Sunny Green Meadow\"]\n"
+            + "    A[Bright Buttercup] --> B[Busy Ant]\n"
+            + "    B --> C[Tall Oak Tree]\n"
+            + "    D[Quiet Snail] --> C\n"
+            + "  end\n";
+    SvgDoc doc = render(code);
+    assertThat(doc.findText("Sunny Green Meadow")).isNotNull();
+    assertThat(doc.findText("Bright Buttercup")).isNotNull();
+    assertThat(doc.findText("Busy Ant")).isNotNull();
+    assertThat(doc.findText("Tall Oak Tree")).isNotNull();
+    assertThat(doc.findText("Quiet Snail")).isNotNull();
+  }
+
+  @Test
+  public void testDirectivesAndStylingIgnoredGracefully() {
+    String code =
+        "graph TD\n"
+            + "  accTitle: Cheerful Morning Playground\n"
+            + "  accDescr: Story of fluffy puppy and kitten\n"
+            + "  classDef default fill:#f9f,stroke:#333;\n"
+            + "  classDef special fill:#bbf,stroke:#333;\n"
+            + "  class A special\n"
+            + "  style B fill:#dfd,stroke:#333;\n"
+            + "  click A href \"https://example.com\"\n"
+            + "  linkStyle 0 stroke:#ff3,stroke-width:4px;\n"
+            + "  A[Little Kitten] --> B[Fluffy Bunny]\n";
+    SvgDoc svg = render(code);
+    assertThat(svg.getAllTextContents()).containsExactly("Little Kitten", "Fluffy Bunny").inOrder();
+  }
+
+  @Test
+  public void testDisconnectedNodes() {
+    SvgDoc svg = render("graph TD\n  A[Quiet Mouse]\n  B[Sleeping Turtle]\n  C --> D\n");
+    assertThat(svg.getAllTextContents()).containsExactly("Quiet Mouse", "Sleeping Turtle", "C", "D").inOrder();
+    assertThat(svg.getElementsByTag("rect").size()).isEqualTo(4);
+    assertThat(svg.getEdgePaths().size()).isEqualTo(1);
+  }
+
+  @Test
+  public void testSelfLoop() {
+    SvgDoc svg = render("graph TD\n  A --> A\n");
+    assertThat(svg.getAllTextContents()).containsExactly("A");
+  }
+
+  @Test
+  public void testWhitespaceAndEmptyBlocks() {
+    assertThat(SimpleMermaidRenderer.renderToSvg("   \n\n\t").isPresent()).isFalse();
+    assertThat(SimpleMermaidRenderer.renderToSvg("graph TD\n").isPresent()).isFalse();
+    assertThat(SimpleMermaidRenderer.renderToSvg("graph TD\n%% only comments\n").isPresent()).isFalse();
+    assertThat(SimpleMermaidRenderer.renderToSvg("").isPresent()).isFalse();
+    assertThat(SimpleMermaidRenderer.renderToSvg(null).isPresent()).isFalse();
+  }
+
+  @Test
+  public void testEmptyLinesAndCommentsAtStart() {
+    String code = "\n\n%% Leading comment\n  \ngraph TD\n\n%% Inner comment\n  A --> B\n";
+    SvgDoc svg = render(code);
+    assertThat(svg.getAllTextContents()).containsExactly("A", "B").inOrder();
+
+    assertThat(SimpleMermaidRenderer.renderToSvg("%% only comments\nsome random text\n").isPresent())
+        .isFalse();
+  }
+
+  @Test
+  public void testMalformedDelimitersGracefulHandling() {
+    String code = "graph TD\n  A[\"Unclosed String\n  B --> A\n";
+    SvgDoc svg = render(code);
+    assertThat(svg.getAllTextContents()).contains("B");
+  }
+
+  @Test
+  public void testAllUnsupportedDiagramTypesReturnEmpty() {
+    assertThat(SimpleMermaidRenderer.renderToSvg("sequenceDiagram\nAlice->>Bob: Hello\n").isPresent()).isFalse();
+    assertThat(SimpleMermaidRenderer.renderToSvg("classDiagram\nClass01 <|-- Class02\n").isPresent()).isFalse();
+    assertThat(SimpleMermaidRenderer.renderToSvg("erDiagram\nCUSTOMER ||--o{ ORDER : places\n").isPresent()).isFalse();
+    assertThat(SimpleMermaidRenderer.renderToSvg("gantt\ntitle A Gantt Diagram\n").isPresent()).isFalse();
+    assertThat(SimpleMermaidRenderer.renderToSvg("pie title Pets\n\"Dogs\" : 386\n").isPresent()).isFalse();
+    assertThat(SimpleMermaidRenderer.renderToSvg("gitGraph\ncommit\n").isPresent()).isFalse();
+    assertThat(SimpleMermaidRenderer.renderToSvg("xychart-beta\ntitle \"Score\"\n").isPresent()).isFalse();
+    assertThat(SimpleMermaidRenderer.renderToSvg("stateDiagram\n[*] --> Still\n").isPresent()).isFalse();
+    assertThat(SimpleMermaidRenderer.renderToSvg("stateDiagram-v2\n[*] --> Still\n").isPresent()).isFalse();
+  }
+
+  @Test
+  public void testNodeReassignmentToSubgraph() {
+    String code =
+        "graph TD\n"
+            + "  A[Singing Robin]\n"
+            + "  subgraph Sub\n"
+            + "    A\n"
+            + "    B[Flying Bluebird]\n"
+            + "  end\n"
+            + "  A --> B\n";
+    SvgDoc svg = render(code);
+    assertThat(svg.getAllTextContents()).containsExactly("Sub", "Singing Robin", "Flying Bluebird").inOrder();
+  }
+
+  @Test
+  public void testRemainingEdgeCasesForFullCoverage() {
+    // Quoted pipe label triggering cleanLabel unwrap
+    String code1 = "graph TD\n  A -->|\"Sweet Honey Pie\"| B\n";
+    SvgDoc doc1 = render(code1);
+    assertThat(doc1.findText("Sweet Honey Pie")).isNotNull();
+
+    // Reverse edge in vertical nested subgraph
+    String code2 = "graph TD\n  subgraph Sub\n    A[Baby Chick]\n    B[Mama Hen]\n    B -->|Chirp Vert| A\n  end\n";
+    SvgDoc doc2 = render(code2);
+    assertThat(doc2.findText("Chirp Vert")).isNotNull();
+
+    // Reverse edge in horizontal nested subgraph
+    String code3 = "graph LR\n  subgraph Sub\n    A[Baby Chick]\n    B[Mama Hen]\n    B -->|Chirp Horiz| A\n  end\n";
+    SvgDoc doc3 = render(code3);
+    assertThat(doc3.findText("Chirp Horiz")).isNotNull();
+
+    // Mutual same-layer edge with label
+    String code5 = "graph TD\n  A --> B\n  B --> A\n  A -->|Golden Star| B\n  C --> D\n";
+    render(code5);
+
+    // Trailing non-edge characters to hit scanEdgeToken default return null
+    String code6 = "graph TD\n  A 12345\n";
+    assertThat(SimpleMermaidRenderer.renderToSvg(code6).isPresent()).isTrue();
+  }
+
+  private static SvgDoc render(String code) {
+    return SvgDoc.render(code);
+  }
+
+  @Test
+  public void testHorizontalSelfLoopAndExtraHeaders() {
+    // Horizontal self loop
+    String code1 = "graph LR\n  A --> A\n";
+    SvgDoc doc1 = render(code1);
+    assertThat(doc1.findText("A")).isNotNull();
+
+    // Redundant header line in body
+    String code2 = "graph TD\n  graph TD\n  A --> B\n";
+    assertThat(SimpleMermaidRenderer.renderToSvg(code2).isPresent()).isTrue();
+
+    // Direct AST Node empty label setter
+    SimpleMermaidRenderer.Node n = new SimpleMermaidRenderer.Node("testNode");
+    n.setLabel("");
+    assertThat(n.labelLines).containsExactly("");
+
+    // Double quotes in label and title to exercise escapeXml
+    String code3 = "graph TD\n  subgraph Sg [\"Magic Castle with \\\"Stars\\\"\"]\n    A[\"Has \\\"Glitter\\\" in pocket\"]\n  end\n";
+    assertThat(SimpleMermaidRenderer.renderToSvg(code3).isPresent()).isTrue();
+  }
+
+  @Test
+  public void testSecurityNoScriptOrIframeExecutionInNodeLabels() {
+    String code =
+        "graph TD\n"
+            + "  A[\"<script>alert('xss-script')</script>\"]\n"
+            + "  B[\"<iframe src='javascript:alert(1)'></iframe>\"]\n"
+            + "  C[\"<img src=x onerror=alert('img-onerror')>\"]\n"
+            + "  D[\"<svg onload=alert('svg-onload')>\"]\n"
+            + "  E[\"<foreignObject><iframe src='https://evil.com'></iframe></foreignObject>\"]\n"
+            + "  F[\"<a href='javascript:alert(1)'>Click Me</a>\"]\n"
+            + "  A --> B --> C --> D --> E --> F\n";
+
+    Optional<String> svgOpt = SimpleMermaidRenderer.renderToSvg(code);
+    assertThat(svgOpt.isPresent()).isTrue();
+
+    SvgDoc svg = new SvgDoc(svgOpt.get());
+    svg.assertRootSvg();
+
+    // Verify absolutely no executable or iframe elements exist in the DOM
+    assertThat(svg.getElementsByTag("script")).isEmpty();
+    assertThat(svg.getElementsByTag("iframe")).isEmpty();
+    assertThat(svg.getElementsByTag("foreignObject")).isEmpty();
+    assertThat(svg.getElementsByTag("img")).isEmpty();
+    assertThat(svg.getElementsByTag("a")).isEmpty();
+
+    // Verify raw SVG string contains no unescaped tags
+    String raw = svgOpt.get();
+    assertThat(raw).doesNotContain("<script");
+    assertThat(raw).doesNotContain("</script>");
+    assertThat(raw).doesNotContain("<iframe");
+    assertThat(raw).doesNotContain("</iframe>");
+    assertThat(raw).doesNotContain("<foreignObject");
+    assertThat(raw).doesNotContain("<img");
+    assertThat(raw).doesNotContain("<a ");
+
+    // Verify content is preserved safely as inert escaped text
+    assertThat(svg.findText("<script>alert('xss-script')</script>")).isNotNull();
+    assertThat(svg.findText("<iframe src='javascript:alert(1)'></iframe>")).isNotNull();
+    assertThat(svg.findText("<img src=x onerror=alert('img-onerror')>")).isNotNull();
+    assertThat(svg.findText("<svg onload=alert('svg-onload')>")).isNotNull();
+    assertThat(svg.findText("<foreignObject><iframe src='https://evil.com'></iframe></foreignObject>")).isNotNull();
+    assertThat(svg.findText("<a href='javascript:alert(1)'>Click Me</a>")).isNotNull();
+  }
+
+  @Test
+  public void testSecurityNoScriptOrIframeInEdgeLabels() {
+    String code =
+        "graph TD\n"
+            + "  A -->|\"<script>alert('edge-pipe')</script>\"| B\n"
+            + "  B -- \"<iframe src='http://evil.com'></iframe>\" --> C\n"
+            + "  C == \"<img src=x onerror=alert('thick-edge')>\" ==> D\n"
+            + "  D -. \"<svg onload=alert('dashed-edge')>\" .-> E\n";
+    SvgDoc svg = render(code);
+
+    assertThat(svg.getElementsByTag("script")).isEmpty();
+    assertThat(svg.getElementsByTag("iframe")).isEmpty();
+    assertThat(svg.getElementsByTag("foreignObject")).isEmpty();
+
+    String raw = SimpleMermaidRenderer.renderToSvg(code).get();
+    assertThat(raw).doesNotContain("<script");
+    assertThat(raw).doesNotContain("</script>");
+    assertThat(raw).doesNotContain("<iframe");
+    assertThat(raw).doesNotContain("</iframe>");
+    assertThat(raw).doesNotContain("<img");
+    assertThat(raw).doesNotContain("<svg onload");
+
+    assertThat(svg.findText("<script>alert('edge-pipe')</script>")).isNotNull();
+    assertThat(svg.findText("<iframe src='http://evil.com'></iframe>")).isNotNull();
+    assertThat(svg.findText("<img src=x onerror=alert('thick-edge')>")).isNotNull();
+    assertThat(svg.findText("<svg onload=alert('dashed-edge')>")).isNotNull();
+  }
+
+  @Test
+  public void testSecurityNoScriptOrIframeInSubgraphTitles() {
+    String code =
+        "graph TD\n"
+            + "  subgraph Sg1 [\"<script>alert('subgraph-title')</script>\"]\n"
+            + "    A[Node A]\n"
+            + "  end\n"
+            + "  subgraph Sg2 [\"<iframe src='javascript:alert(2)'></iframe>\"]\n"
+            + "    B[Node B]\n"
+            + "  end\n"
+            + "  A --> B\n";
+    SvgDoc svg = render(code);
+
+    assertThat(svg.getElementsByTag("script")).isEmpty();
+    assertThat(svg.getElementsByTag("iframe")).isEmpty();
+
+    String raw = SimpleMermaidRenderer.renderToSvg(code).get();
+    assertThat(raw).doesNotContain("<script");
+    assertThat(raw).doesNotContain("</script>");
+    assertThat(raw).doesNotContain("<iframe");
+    assertThat(raw).doesNotContain("</iframe>");
+
+    assertThat(svg.findText("<script>alert('subgraph-title')</script>")).isNotNull();
+    assertThat(svg.findText("<iframe src='javascript:alert(2)'></iframe>")).isNotNull();
+  }
+
+  @Test
+  public void testSecurityDirectivesCannotInjectJavascriptUrls() {
+    String code =
+        "graph TD\n"
+            + "  A[Node A] --> B[Node B]\n"
+            + "  click A href \"javascript:alert('click-href')\"\n"
+            + "  click B call alert('click-call')\n"
+            + "  click A \"javascript:alert('positional-href')\"\n"
+            + "  style A fill:url(javascript:alert(1))\n"
+            + "  classDef evil fill:red,color:white;\n"
+            + "  linkStyle 0 stroke:red;\n";
+    SvgDoc svg = render(code);
+
+    assertThat(svg.getElementsByTag("a")).isEmpty();
+    assertThat(svg.getElementsByTag("script")).isEmpty();
+
+    String raw = SimpleMermaidRenderer.renderToSvg(code).get();
+    assertThat(raw).doesNotContain("javascript:");
+    assertThat(raw).doesNotContain("href=");
+    assertThat(raw).doesNotContain("onclick=");
+    assertThat(raw).doesNotContain("xlink:href");
+  }
+
+  @Test
+  public void testSecurityXmlBreakoutPayloads() {
+    String code =
+        "graph TD\n"
+            + "  A[\"</text></svg><script>alert('breakout')</script><svg><text>\"]\n"
+            + "  B[\"'\"><script>alert('quote-breakout')</script>\"]\n"
+            + "  A --> B\n";
+    SvgDoc svg = render(code);
+
+    // Verify the document root remains the only SVG element and no script elements were injected
+    assertThat(svg.getElementsByTag("script")).isEmpty();
+    assertThat(svg.getElementsByTag("svg").size()).isEqualTo(1);
+
+    String raw = SimpleMermaidRenderer.renderToSvg(code).get();
+    assertThat(raw).doesNotContain("<script");
+    assertThat(raw).doesNotContain("</svg><script>");
+  }
+
+  @Test
+  public void testIsolatedSubgraphAlongsideMainDagTree() {
+    String code =
+        "graph TD\n"
+            + "    ClientApp[Little Puppy Plays] --> Extras(Sweet Kitten)\n"
+            + "    ClientApp --> Utils(Happy Bunny)\n"
+            + "    \n"
+            + "    Utils --> ServiceDiscovery[Red Apple Berry]\n"
+            + "    Utils --> ModelManager[Yellow Banana Snack]\n"
+            + "    \n"
+            + "    Extras --> Recognition(Fluffy Duckling)\n"
+            + "    \n"
+            + "    Recognition --> SODA(Green Frog Jump)\n"
+            + "    Recognition --> S3(Sunny Daisy Flower)\n"
+            + "    \n"
+            + "    subgraph Play Park Garden\n"
+            + "        Executors(Teddy Bear)\n"
+            + "        Errors(Wooden Blocks)\n"
+            + "        Protos(Toy Wagon)\n"
+            + "    end\n"
+            + "    \n"
+            + "    Recognition -.-> PlayParkGarden\n";
+    SvgDoc svg = render(code);
+
+    // Verify all nodes and subgraph title exist
+    assertThat(svg.findText("Play Park Garden")).isNotNull();
+    assertThat(svg.findText("Teddy Bear")).isNotNull();
+    assertThat(svg.findText("Wooden Blocks")).isNotNull();
+    assertThat(svg.findText("Toy Wagon")).isNotNull();
+    assertThat(svg.findText("Little Puppy Plays")).isNotNull();
+    assertThat(svg.findText("Sweet Kitten")).isNotNull();
+    assertThat(svg.findText("Happy Bunny")).isNotNull();
+    assertThat(svg.findText("Red Apple Berry")).isNotNull();
+    assertThat(svg.findText("Yellow Banana Snack")).isNotNull();
+    assertThat(svg.findText("Fluffy Duckling")).isNotNull();
+    assertThat(svg.findText("Green Frog Jump")).isNotNull();
+    assertThat(svg.findText("Sunny Daisy Flower")).isNotNull();
+    assertThat(svg.findText("PlayParkGarden")).isNotNull();
+
+    // Verify vertical stack in Play Park Garden subgraph (Teddy Bear above Wooden Blocks above Toy Wagon)
+    Element executorsText = svg.findText("Teddy Bear");
+    Element errorsText = svg.findText("Wooden Blocks");
+    Element protoText = svg.findText("Toy Wagon");
+    double execY = Double.parseDouble(executorsText.getAttribute("y"));
+    double errY = Double.parseDouble(errorsText.getAttribute("y"));
+    double protoY = Double.parseDouble(protoText.getAttribute("y"));
+    assertThat(execY).isLessThan(errY);
+    assertThat(errY).isLessThan(protoY);
+
+    // Verify Play Park Garden is placed on the left of ClientApp
+    Element clientAppText = svg.findText("Little Puppy Plays");
+    double execX = Double.parseDouble(executorsText.getAttribute("x"));
+    double clientX = Double.parseDouble(clientAppText.getAttribute("x"));
+    assertThat(execX).isLessThan(clientX);
+  }
+
+  @Test
+  public void testMultiNodeChainingWithAmpersand() {
+    String code =
+        "graph TD\n"
+            + "    A[Little Star] --> CheckJDAA{Is Puppy Sleepy?}\n"
+            + "    CheckJDAA -- No --> InstallJDA[Play With Soft Ball]\n"
+            + "    InstallJDA --> CheckJDAA\n"
+            + "    CheckJDAA -- Yes --> B{Wants Sweet Cookie?}\n"
+            + "    B -- Yes --> C[Drink Warm Milk Cup]\n"
+            + "    B -- No --> D[Sing Happy Lullaby]\n"
+            + "    D --> E[Cuddle Warm Blanket]\n"
+            + "    D --> F[Hug Fluffy Panda]\n"
+            + "    D --> G[Close Shiny Eyes]\n"
+            + "    E & F & G --> H[Sweet Dreams Forest]\n"
+            + "    C & H --> I[Gentle Good Night]\n"
+            + "    I --> J[Sleep Until Morning]\n"
+            + "    J --> K[Wake Up Happy Sun]\n";
+    SvgDoc svg = render(code);
+
+    // Verify all nodes exist
+    assertThat(svg.findText("Little Star")).isNotNull();
+    assertThat(svg.findText("Is Puppy Sleepy?")).isNotNull();
+    assertThat(svg.findText("Play With Soft Ball")).isNotNull();
+    assertThat(svg.findText("Wants Sweet Cookie?")).isNotNull();
+    assertThat(svg.findText("Drink Warm Milk Cup")).isNotNull();
+    assertThat(svg.findText("Sing Happy Lullaby")).isNotNull();
+    assertThat(svg.findText("Cuddle Warm Blanket")).isNotNull();
+    assertThat(svg.findText("Hug Fluffy Panda")).isNotNull();
+    assertThat(svg.findText("Close Shiny Eyes")).isNotNull();
+    assertThat(svg.findText("Sweet Dreams Forest")).isNotNull();
+    assertThat(svg.findText("Gentle Good Night")).isNotNull();
+    assertThat(svg.findText("Sleep Until Morning")).isNotNull();
+    assertThat(svg.findText("Wake Up Happy Sun")).isNotNull();
+    // Verify diamond shape polygons exist for decision nodes CheckJDAA and B
+    List<Element> polygons = svg.getElementsByTag("polygon");
+    assertThat(polygons.size()).isAtLeast(2); // CheckJDAA and B are diamonds
+
+    // Verify vertical order down the DAG
+    Element queryText = svg.findText("Little Star");
+    Element hText = svg.findText("Sweet Dreams Forest");
+    Element iText = svg.findText("Gentle Good Night");
+    Element jText = svg.findText("Sleep Until Morning");
+    Element kText = svg.findText("Wake Up Happy Sun");
+
+    double queryY = Double.parseDouble(queryText.getAttribute("y"));
+    double hY = Double.parseDouble(hText.getAttribute("y"));
+    double iY = Double.parseDouble(iText.getAttribute("y"));
+    double jY = Double.parseDouble(jText.getAttribute("y"));
+    double kY = Double.parseDouble(kText.getAttribute("y"));
+
+    assertThat(queryY).isLessThan(hY);
+    assertThat(hY).isLessThan(iY);
+    assertThat(iY).isLessThan(jY);
+    assertThat(jY).isLessThan(kY);
+
+    // Also test multi-source to multi-target chaining (A & B --> C & D)
+    String multiCode = "graph TD\n  A & B --> C & D\n";
+    SvgDoc multiDoc = render(multiCode);
+    assertThat(multiDoc.findText("A")).isNotNull();
+    assertThat(multiDoc.findText("B")).isNotNull();
+    assertThat(multiDoc.findText("C")).isNotNull();
+    assertThat(multiDoc.findText("D")).isNotNull();
+    assertThat(multiDoc.getElementsByTag("path").size()).isEqualTo(5); // 1 marker path in <defs> + 4 edge paths
+  }
+
+  @Test
+  public void testSubgraphDirectionOverrideWithCrossEdges() {
+    String code =
+        "graph TD\n"
+            + "  subgraph Castle [\"Toy Castle\"]\n"
+            + "    direction LR\n"
+            + "    A[Happy Bear] --> B[Silly Goose]\n"
+            + "  end\n"
+            + "  subgraph Garden [\"Flower Garden\"]\n"
+            + "    C[Sunny Daisy]\n"
+            + "  end\n"
+            + "  B --> C\n";
+    SvgDoc doc = render(code);
+    assertThat(doc.findText("Toy Castle")).isNotNull();
+    assertThat(doc.findText("Flower Garden")).isNotNull();
+    assertThat(doc.findText("Happy Bear")).isNotNull();
+    assertThat(doc.findText("Silly Goose")).isNotNull();
+    assertThat(doc.findText("Sunny Daisy")).isNotNull();
+  }
+
+  @Test
+  public void testNestedSubgraphsWithLabeledInterChildEdge() {
+    String code =
+        "graph LR\n"
+            + "  subgraph ToyBox [\"Big Toy Box\"]\n"
+            + "    subgraph PuzzleA [\"Puppy Puzzle\"]\n"
+            + "      A[Little Dog]\n"
+            + "    end\n"
+            + "    subgraph PuzzleB [\"Kitten Puzzle\"]\n"
+            + "      B[Little Cat]\n"
+            + "    end\n"
+            + "    A -->|Friendly Meow| B\n"
+            + "  end\n"
+            + "  subgraph BedTime [\"Sleepy Pillow\"]\n"
+            + "    C[Cozy Blanket]\n"
+            + "  end\n"
+            + "  B -->|Soft Hug| C\n";
+    SvgDoc doc = render(code);
+    assertThat(doc.findText("Big Toy Box")).isNotNull();
+    assertThat(doc.findText("Puppy Puzzle")).isNotNull();
+    assertThat(doc.findText("Kitten Puzzle")).isNotNull();
+    assertThat(doc.findText("Little Dog")).isNotNull();
+    assertThat(doc.findText("Little Cat")).isNotNull();
+    assertThat(doc.findText("Friendly Meow")).isNotNull();
+    assertThat(doc.findText("Sleepy Pillow")).isNotNull();
+    assertThat(doc.findText("Cozy Blanket")).isNotNull();
+    assertThat(doc.findText("Soft Hug")).isNotNull();
+  }
+
+  @Test
+  public void testNestedSubgraphsVerticalWithCrossEdgesAndEmptySubgraphs() {
+    String code =
+        "graph TD\n"
+            + "  subgraph WonderLand [\"Magic Wonderland\"]\n"
+            + "    subgraph EmptyBox [\"Empty Treasure Chest\"]\n"
+            + "    end\n"
+            + "    subgraph ZoneA [\"Butterfly Valley\"]\n"
+            + "      A[Shiny Butterfly]\n"
+            + "    end\n"
+            + "    subgraph ZoneB [\"Rainbow Hill\"]\n"
+            + "      B[Glowing Rainbow]\n"
+            + "    end\n"
+            + "    B -->|Sweet Melody| A\n"
+            + "  end\n";
+    SvgDoc doc = render(code);
+    assertThat(doc.findText("Magic Wonderland")).isNotNull();
+    assertThat(doc.findText("Empty Treasure Chest")).isNotNull();
+    assertThat(doc.findText("Butterfly Valley")).isNotNull();
+    assertThat(doc.findText("Rainbow Hill")).isNotNull();
+    assertThat(doc.findText("Shiny Butterfly")).isNotNull();
+    assertThat(doc.findText("Glowing Rainbow")).isNotNull();
+    assertThat(doc.findText("Sweet Melody")).isNotNull();
+  }
+
+  @Test
+  public void testSkipLayerBypassWithDummyNodesTD() {
+    String code =
+        "graph TD\n"
+            + "  A[Teddy Bear] -->|Blue Balloon| B[Silly Monkey]\n"
+            + "  A -->|Red Apple| C[Happy Puppy]\n"
+            + "  B --> C\n"
+            + "  C --> D[Little Kitten]\n";
+    SvgDoc doc = render(code);
+    assertThat(doc.findText("Teddy Bear")).isNotNull();
+    assertThat(doc.findText("Silly Monkey")).isNotNull();
+    assertThat(doc.findText("Happy Puppy")).isNotNull();
+    assertThat(doc.findText("Little Kitten")).isNotNull();
+    assertThat(doc.findText("Blue Balloon")).isNotNull();
+    assertThat(doc.findText("Red Apple")).isNotNull();
+  }
+
+  @Test
+  public void testSkipLayerBypassWithDummyNodesLR() {
+    String code =
+        "graph LR\n"
+            + "  A[Teddy Bear] -->|Blue Balloon| B[Silly Monkey]\n"
+            + "  A -->|Red Apple| C[Happy Puppy]\n"
+            + "  B --> C\n"
+            + "  C --> D[Little Kitten]\n";
+    SvgDoc doc = render(code);
+    assertThat(doc.findText("Teddy Bear")).isNotNull();
+    assertThat(doc.findText("Silly Monkey")).isNotNull();
+    assertThat(doc.findText("Happy Puppy")).isNotNull();
+    assertThat(doc.findText("Little Kitten")).isNotNull();
+    assertThat(doc.findText("Blue Balloon")).isNotNull();
+    assertThat(doc.findText("Red Apple")).isNotNull();
+  }
+
+  @Test
+  public void testCompoundSubgraphAndStandaloneNodesLayoutWithStyles() {
+    String code =
+        "graph LR\n"
+            + "  subgraph CastleBox [\"Play Castle\"]\n"
+            + "    direction TB\n"
+            + "    ToyA[Magic Wand] <--> ToyB[Cozy Teddy]\n"
+            + "  end\n"
+            + "  subgraph GardenBox [\"Flower Garden\"]\n"
+            + "    ToyC[Pink Blossom] --> ToyD[Sweet Daisy]\n"
+            + "  end\n"
+            + "  ToyB --> ToyC\n"
+            + "  ToyE[Happy Butterfly] --> ToyB\n"
+            + "  style CastleBox fill:#e3f2fd,stroke:#1e88e5\n"
+            + "  style GardenBox fill:rgb(240,250,240),stroke:#43a047\n"
+            + "  style ToyE fill:hsl(120,50%,90%),stroke:blue\n"
+            + "  style ToyA fill:rgba(255,255,255,0.8),stroke:purple\n"
+            + "  style ToyC fill:#ffb300,stroke:#333333\n"
+            + "  style \"\" fill:#fff\n"
+            + "  style NonExistent fill:#fff\n";
+    SvgDoc doc = render(code);
+    assertThat(doc.findText("Play Castle")).isNotNull();
+    assertThat(doc.findText("Flower Garden")).isNotNull();
+    assertThat(doc.findText("Magic Wand")).isNotNull();
+    assertThat(doc.findText("Cozy Teddy")).isNotNull();
+    assertThat(doc.findText("Pink Blossom")).isNotNull();
+    assertThat(doc.findText("Sweet Daisy")).isNotNull();
+    assertThat(doc.findText("Happy Butterfly")).isNotNull();
+  }
+
+  @Test
+  public void testStyledCustomShapes() {
+    String code =
+        "graph TD\n"
+            + "  N1((Sun Ball)) --> N2{Magic Gem}\n"
+            + "  N2 --> N3{{Toy Boat}}\n"
+            + "  N3 --> N4[(Toy Castle)]\n"
+            + "  N4 --> N5>Sweet Candy]\n"
+            + "  N5 --> N6[[Puppy House]]\n"
+            + "  style N1 fill:#ffecb3,stroke:#ffa000\n"
+            + "  style N2 fill:#e1bee7,stroke:#8e24aa\n"
+            + "  style N3 fill:#c8e6c9,stroke:#388e3c\n"
+            + "  style N4 fill:#b2ebf2,stroke:#00838f\n"
+            + "  style N5 fill:#ffcdd2,stroke:#c62828\n"
+            + "  style N6 fill:#d1c4e9,stroke:#512da8\n";
+    SvgDoc doc = render(code);
+    assertThat(doc.findText("Sun Ball")).isNotNull();
+    assertThat(doc.findText("Magic Gem")).isNotNull();
+    assertThat(doc.findText("Toy Boat")).isNotNull();
+    assertThat(doc.findText("Toy Castle")).isNotNull();
+    assertThat(doc.findText("Sweet Candy")).isNotNull();
+    assertThat(doc.findText("Puppy House")).isNotNull();
+  }
+
+  @Test
+  public void testSingleUnitCompoundComponent() {
+    String code =
+        "graph TD\n"
+            + "  subgraph SoloBox [\"Secret Clubhouse\"]\n"
+            + "    KidA[Little Star] --> KidB[Bright Moon]\n"
+            + "  end\n";
+    SvgDoc doc = render(code);
+    assertThat(doc.findText("Secret Clubhouse")).isNotNull();
+    assertThat(doc.findText("Little Star")).isNotNull();
+    assertThat(doc.findText("Bright Moon")).isNotNull();
+  }
+
+  @Test
+  public void testSequentialSubgraphsWithoutInternalDag() {
+    String code =
+        "graph TD\n"
+            + "  subgraph VertBox [\"Stacking Blocks\"]\n"
+            + "    BoxA[Red Block]\n"
+            + "    BoxB[Blue Block]\n"
+            + "    BoxC[Green Block]\n"
+            + "  end\n"
+            + "  subgraph HorizBox [\"Toy Train\"]\n"
+            + "    direction LR\n"
+            + "    CarA[Train Engine]\n"
+            + "    CarB[Train Caboose]\n"
+            + "  end\n";
+    SvgDoc doc = render(code);
+    assertThat(doc.findText("Stacking Blocks")).isNotNull();
+    assertThat(doc.findText("Toy Train")).isNotNull();
+  }
+
+  @Test
+  public void testSequentialSubgraphsWithLabels() {
+    String code =
+        "graph TD\n"
+            + "  subgraph VertBox [\"Stacking Blocks\"]\n"
+            + "    BoxA[Red Block]\n"
+            + "    BoxB[Blue Block]\n"
+            + "  end\n"
+            + "  subgraph HorizBox [\"Toy Train\"]\n"
+            + "    direction LR\n"
+            + "    CarA[Train Engine]\n"
+            + "    CarB[Train Caboose]\n"
+            + "  end\n"
+            + "  BoxA -->|Stack On| BoxB\n"
+            + "  CarA -->|Pull Car| CarB\n"
+            + "  style BoxA fill:#112233;stroke:#445566\n"
+            + "  style BoxB fill:#112233,stroke:#445566\n";
+    SvgDoc doc = render(code);
+    assertThat(doc.findText("Stacking Blocks")).isNotNull();
+    assertThat(doc.findText("Toy Train")).isNotNull();
+  }
+
+  @Test
+  public void testMultiLayerCompoundComponent() {
+    String code =
+        "graph LR\n"
+            + "  subgraph Box1 [\"First Box\"]\n"
+            + "    A[Puppy Dog]\n"
+            + "  end\n"
+            + "  subgraph Box2 [\"Second Box\"]\n"
+            + "    B[Kitty Cat]\n"
+            + "  end\n"
+            + "  subgraph Box3 [\"Third Box\"]\n"
+            + "    C[Bunny Rabbit]\n"
+            + "  end\n"
+            + "  A --> B\n"
+            + "  B --> C\n";
+    SvgDoc doc = render(code);
+    assertThat(doc.findText("First Box")).isNotNull();
+    assertThat(doc.findText("Second Box")).isNotNull();
+    assertThat(doc.findText("Third Box")).isNotNull();
+  }
+
+  @Test
+  public void testTripleNestedSubgraphsWithCrossChildEdges() {
+    String code =
+        "graph TD\n"
+            + "  subgraph OuterCastle [\"Giant Castle\"]\n"
+            + "    subgraph MidTower [\"High Tower\"]\n"
+            + "      subgraph InnerRoom [\"Secret Room\"]\n"
+            + "        Gem[Magic Ruby]\n"
+            + "      end\n"
+            + "    end\n"
+            + "    subgraph SecondTower [\"Low Tower\"]\n"
+            + "      OtherGem[Shiny Emerald]\n"
+            + "    end\n"
+            + "    Gem -->|Sparkle Magic| OtherGem\n"
+            + "  end\n"
+            + "  Dragon[Friendly Dragon] --> Gem\n";
+    SvgDoc doc = render(code);
+    assertThat(doc.findText("Giant Castle")).isNotNull();
+    assertThat(doc.findText("High Tower")).isNotNull();
+    assertThat(doc.findText("Secret Room")).isNotNull();
+    assertThat(doc.findText("Low Tower")).isNotNull();
+    assertThat(doc.findText("Magic Ruby")).isNotNull();
+    assertThat(doc.findText("Shiny Emerald")).isNotNull();
+    assertThat(doc.findText("Friendly Dragon")).isNotNull();
+    assertThat(doc.findText("Sparkle Magic")).isNotNull();
+  }
+
+  @Test
+  public void testDecisionTreeBranchingWithFeedbackLoopAndChildrenWords() {
+    String code =
+        "graph TD\n"
+            + "  A[Little Bunny Play] --> B{Choose Sweet Snack}\n"
+            + "  B -->|Crisp Red Apple| C[Happy Bunny Chew]\n"
+            + "  B -->|Sweet Yellow Banana| D[Joyful Bunny Hop]\n"
+            + "  B -->|Crunchy Orange Carrot| E[Cheerful Bunny Munch]\n"
+            + "  E -->|Ask For More Treats| B\n"
+            + "  E -->|Tired Little Nap| F[Sleepy Cozy Blanket]\n";
+    SvgDoc doc = render(code);
+    assertThat(doc.findText("Little Bunny Play")).isNotNull();
+    assertThat(doc.findText("Choose Sweet Snack")).isNotNull();
+    assertThat(doc.findText("Happy Bunny Chew")).isNotNull();
+    assertThat(doc.findText("Joyful Bunny Hop")).isNotNull();
+    assertThat(doc.findText("Cheerful Bunny Munch")).isNotNull();
+    assertThat(doc.findText("Sleepy Cozy Blanket")).isNotNull();
+    assertThat(doc.findText("Crisp Red Apple")).isNotNull();
+    assertThat(doc.findText("Sweet Yellow Banana")).isNotNull();
+    assertThat(doc.findText("Crunchy Orange Carrot")).isNotNull();
+    assertThat(doc.findText("Ask For More Treats")).isNotNull();
+    assertThat(doc.findText("Tired Little Nap")).isNotNull();
+
+    Element eText = doc.findText("Cheerful Bunny Munch");
+    Element fText = doc.findText("Sleepy Cozy Blanket");
+    assertThat(eText).isNotNull();
+    assertThat(fText).isNotNull();
+    double eX = Double.parseDouble(eText.getAttribute("x"));
+    double fX = Double.parseDouble(fText.getAttribute("x"));
+    assertThat(Math.abs(eX - fX)).isLessThan(2.0);
+  }
+
+  @Test
+  public void testLongNodeLabelDoesNotOverflowOrGetCapped() {
+    String longLabel = "Happy Little Friendly Puppy Plays With Big Red Ball In The Green Garden";
+    String code = "graph TD\n  A[\"" + longLabel + "\"]\n";
+    SvgDoc doc = render(code);
+    assertThat(doc.findText(longLabel)).isNotNull();
+
+    List<Element> rects = doc.getElementsByTag("rect");
+    assertThat(rects.size()).isEqualTo(1);
+    double rectWidth = Double.parseDouble(rects.get(0).getAttribute("width"));
+    double expectedMin = longLabel.length() * 7.5;
+    assertThat(rectWidth).isGreaterThan(expectedMin);
+    assertThat(rectWidth).isGreaterThan(350.0);
+  }
+
+  @Test
+  public void testNestedSubgraphWithSiblingNodesAndCrossLayerEdges() {
+    String code =
+        "graph TD\n"
+            + "  subgraph ToyBox [\"Big Toy Box\"]\n"
+            + "    A[Magic Wand] --> B[Golden Crown]\n"
+            + "    B --> C[Shiny Sparkles]\n"
+            + "    D[Toy Train] --> E{Has Train Track?}\n"
+            + "    E -->|Yes| F[Start Train Engine]\n"
+            + "    subgraph TrainCars [\"Little Train Cars\"]\n"
+            + "      F --> G[Red Caboose]\n"
+            + "      G --> H[Blue Engine]\n"
+            + "    end\n"
+            + "  end\n"
+            + "  subgraph Playroom [\"Sunny Playroom\"]\n"
+            + "    Target[Happy Child Playing]\n"
+            + "  end\n"
+            + "  H --> Target\n";
+    SvgDoc doc = render(code);
+
+    assertThat(doc.findText("Big Toy Box")).isNotNull();
+    assertThat(doc.findText("Magic Wand")).isNotNull();
+    assertThat(doc.findText("Golden Crown")).isNotNull();
+    assertThat(doc.findText("Shiny Sparkles")).isNotNull();
+    assertThat(doc.findText("Toy Train")).isNotNull();
+    assertThat(doc.findText("Has Train Track?")).isNotNull();
+    assertThat(doc.findText("Start Train Engine")).isNotNull();
+    assertThat(doc.findText("Little Train Cars")).isNotNull();
+    assertThat(doc.findText("Red Caboose")).isNotNull();
+    assertThat(doc.findText("Blue Engine")).isNotNull();
+    assertThat(doc.findText("Sunny Playroom")).isNotNull();
+    assertThat(doc.findText("Happy Child Playing")).isNotNull();
+
+    Element fEl = doc.findText("Start Train Engine");
+    Element gEl = doc.findText("Red Caboose");
+    assertThat(fEl).isNotNull();
+    assertThat(gEl).isNotNull();
+    double fY = Double.parseDouble(fEl.getAttribute("y"));
+    double gY = Double.parseDouble(gEl.getAttribute("y"));
+    assertThat(fY).isLessThan(gY);
+  }
+
+  @Test
+  public void testCylinderShapeWithAlapCompactionAndBackEdgeClearance() {
+    String code =
+        "graph TD\n"
+            + "  A[(\"Honey Pot <br> Sweet & Yummy\")] -->|Morning Buzz| B[Busy Little Bumblebee]\n"
+            + "  B -->|Happy Flight| C[Flower Garden Patch]\n"
+            + "  D[Playful Garden Snail] -->|Slow Crawl| C\n"
+            + "  C -->|Gather Nectar| A\n"
+            + "  C -->|Pollinate Plants| E[Bright Sunflower]\n";
+    SvgDoc doc = render(code);
+
+    assertThat(doc.findText("Honey Pot")).isNotNull();
+    assertThat(doc.findText("Sweet & Yummy")).isNotNull();
+    assertThat(doc.findText("Busy Little Bumblebee")).isNotNull();
+    assertThat(doc.findText("Flower Garden Patch")).isNotNull();
+    assertThat(doc.findText("Playful Garden Snail")).isNotNull();
+    assertThat(doc.findText("Bright Sunflower")).isNotNull();
+
+    Element bEl = doc.findText("Busy Little Bumblebee");
+    Element dEl = doc.findText("Playful Garden Snail");
+    assertThat(bEl).isNotNull();
+    assertThat(dEl).isNotNull();
+    double bY = Double.parseDouble(bEl.getAttribute("y"));
+    double dY = Double.parseDouble(dEl.getAttribute("y"));
+    assertThat(Math.abs(bY - dY)).isLessThan(2.0);
+
+    List<Element> paths = doc.getEdgePaths();
+    assertThat(paths.size()).isAtLeast(5);
+
+    // Verify back-edge from C to A loops with rightward clearance beyond D
+    Element dText = doc.findText("Playful Garden Snail");
+    double dRight = Double.parseDouble(dText.getAttribute("x")) + 80.0;
+    boolean foundClearanceLoop = false;
+    for (Element p : paths) {
+      String d = p.getAttribute("d");
+      if (d.contains(" C ")) {
+        for (String part : Splitter.onPattern("[,\\s]+").omitEmptyStrings().split(d)) {
+          Double val = Doubles.tryParse(part);
+          if (val != null && val > dRight) {
+            foundClearanceLoop = true;
+            break;
+          }
+        }
+      }
+    }
+    assertThat(foundClearanceLoop).isTrue();
+  }
+
+  @Test
+  public void testSubgraphEdgeWithDirectionOverrideAndDynamicLabelSpacing() {
+    String code =
+        "flowchart TD\n"
+            + "  subgraph StoryOne [\"Teddy Bear Adventure\"]\n"
+            + "    direction LR\n"
+            + "    P1[Cozy Blanket] ---|Soft Fluffy Hug| P2[Sweet Dream]\n"
+            + "    P2 ---|Gentle Night Song| P3[Morning Sun]\n"
+            + "  end\n"
+            + "  subgraph StoryTwo [\"Puppy Playground\"]\n"
+            + "    direction LR\n"
+            + "    Q1[Rubber Ball] ---|Happy Bouncy Leap| Q2[Flying Frisbee]\n"
+            + "    Q2 ---|Wagging Tail Jump| Q3[Green Lawn]\n"
+            + "  end\n"
+            + "  StoryOne ==>|Wake Up Early| StoryTwo\n";
+    SvgDoc doc = render(code);
+
+    // 1. Subgraph container layout and node containment checks
+    List<SvgDoc.Rect2D> sgs = doc.getSubgraphBoundingBoxes();
+    assertThat(sgs.size()).isEqualTo(2);
+    SvgDoc.Rect2D sg1 = sgs.get(0);
+    SvgDoc.Rect2D sg2 = sgs.get(1);
+
+    // StoryOne must be completely vertically above StoryTwo
+    assertThat(sg1.bottom()).isLessThan(sg2.y);
+
+    List<SvgDoc.Rect2D> nodes = doc.getNodeBoundingBoxes();
+    assertThat(nodes.size()).isEqualTo(6);
+    for (int i = 0; i < 3; i++) {
+      assertThat(sg1.contains(nodes.get(i), 10.0)).isTrue();
+    }
+    for (int i = 3; i < 6; i++) {
+      assertThat(sg2.contains(nodes.get(i), 10.0)).isTrue();
+    }
+
+    // 2. Subgraph connecting edge geometry
+    List<Element> lines = doc.getElementsByTag("line");
+    assertThat(lines.size()).isEqualTo(1);
+    Element seLine = lines.get(0);
+    double lx1 = Double.parseDouble(seLine.getAttribute("x1"));
+    double ly1 = Double.parseDouble(seLine.getAttribute("y1"));
+    double lx2 = Double.parseDouble(seLine.getAttribute("x2"));
+    double ly2 = Double.parseDouble(seLine.getAttribute("y2"));
+    assertThat(Math.abs(lx1 - sg1.centerX())).isLessThan(1.0);
+    assertThat(Math.abs(ly1 - sg1.bottom())).isLessThan(1.0);
+    assertThat(Math.abs(lx2 - sg2.centerX())).isLessThan(1.0);
+    assertThat(Math.abs(ly2 - sg2.y)).isLessThan(1.0);
+
+    // 3. Intra-subgraph horizontal edge paths
+    List<Element> paths = doc.getEdgePaths();
+    assertThat(paths.size()).isEqualTo(4);
+    for (Element p : paths) {
+      String d = p.getAttribute("d");
+      assertThat(d).startsWith("M ");
+    }
+  }
+
+  @Test
+  public void testIsolatedSubgraphInHorizontalGraph() {
+    String code =
+        "graph LR\n"
+            + "  subgraph Garden [\"Flower Garden\"]\n"
+            + "    A[Bright Tulip]\n"
+            + "    B[Daisy Flower]\n"
+            + "  end\n";
+    SvgDoc doc = render(code);
+
+    List<SvgDoc.Rect2D> sgs = doc.getSubgraphBoundingBoxes();
+    assertThat(sgs.size()).isEqualTo(1);
+    List<SvgDoc.Rect2D> nodes = doc.getNodeBoundingBoxes();
+    assertThat(nodes.size()).isEqualTo(2);
+    assertThat(sgs.get(0).contains(nodes.get(0), 10.0)).isTrue();
+    assertThat(sgs.get(0).contains(nodes.get(1), 10.0)).isTrue();
+  }
+
+  @Test
+  public void testNestedSubgraphsWithInternalSubgraphEdgeAndInheritedDirection() {
+    String code =
+        "graph TD\n"
+            + "  subgraph MainBox [\"Toy Warehouse\"]\n"
+            + "    direction LR\n"
+            + "    subgraph BoxOne [\"Teddy Room\"]\n"
+            + "      A[Brown Bear]\n"
+            + "    end\n"
+            + "    subgraph BoxTwo [\"Puppy Room\"]\n"
+            + "      B[Happy Dog]\n"
+            + "    end\n"
+            + "    BoxOne --> BoxTwo\n"
+            + "  end\n";
+    SvgDoc doc = render(code);
+
+    List<SvgDoc.Rect2D> sgs = doc.getSubgraphBoundingBoxes();
+    assertThat(sgs.size()).isEqualTo(3);
+  }
+}
diff --git a/javatests/com/google/gitiles/doc/SvgDoc.java b/javatests/com/google/gitiles/doc/SvgDoc.java
new file mode 100644
index 0000000..0463ee7
--- /dev/null
+++ b/javatests/com/google/gitiles/doc/SvgDoc.java
@@ -0,0 +1,377 @@
+// Copyright 2026 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gitiles.doc;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import com.google.common.base.Splitter;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+/**
+ * Shared DOM and geometric assertion helper for Mermaid SVG test suites.
+ */
+public class SvgDoc {
+
+  public static final List<String> DANGEROUS_TAGS =
+      Arrays.asList(
+          "script",
+          "iframe",
+          "foreignObject",
+          "img",
+          "a",
+          "object",
+          "embed",
+          "applet",
+          "audio",
+          "video",
+          "form",
+          "input",
+          "button",
+          "style",
+          "link",
+          "meta",
+          "use",
+          "image",
+          "animate",
+          "set",
+          "animateTransform",
+          "feImage",
+          "pattern",
+          "xml-stylesheet");
+
+  public final Document doc;
+  public final Element root;
+
+  public SvgDoc(String svg) {
+    try {
+      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+      factory.setNamespaceAware(false);
+      DocumentBuilder builder = factory.newDocumentBuilder();
+      this.doc = builder.parse(new InputSource(new StringReader(svg)));
+      this.root = doc.getDocumentElement();
+    } catch (Exception e) {
+      throw new AssertionError("SVG string is not valid XML: " + e.getMessage(), e);
+    }
+  }
+
+  /**
+   * Renders the given Mermaid diagram code and verifies all geometric and structural invariants.
+   */
+  public static SvgDoc render(String mermaidCode) {
+    Optional<String> svgOpt = SimpleMermaidRenderer.renderToSvg(mermaidCode);
+    assertThat(svgOpt.isPresent()).isTrue();
+    SvgDoc doc = new SvgDoc(svgOpt.get());
+    doc.assertAllInvariants();
+    return doc;
+  }
+
+  /**
+   * Renders the given Mermaid diagram code without asserting collision invariants (e.g. for malformed tests).
+   */
+  public static SvgDoc renderRaw(String mermaidCode) {
+    Optional<String> svgOpt = SimpleMermaidRenderer.renderToSvg(mermaidCode);
+    assertThat(svgOpt.isPresent()).isTrue();
+    return new SvgDoc(svgOpt.get());
+  }
+
+  /**
+   * Asserts root SVG structure, safe tags, and non-overlapping nodes, edge labels, and subgraphs.
+   */
+  public void assertAllInvariants() {
+    assertRootSvg();
+    assertNoDangerousTags();
+    assertNoNodeOverlaps();
+    assertNoLabelNodeOverlaps();
+    assertSubgraphsDoNotOverlap();
+  }
+
+  public void assertRootSvg() {
+    assertThat(root.getTagName()).isEqualTo("svg");
+    assertThat(root.getAttribute("class")).isEqualTo("mermaid-svg");
+    assertThat(root.getAttribute("xmlns")).isEqualTo("http://www.w3.org/2000/svg");
+    assertThat(root.getAttribute("viewBox")).matches("^0 0 \\d+ \\d+$");
+    assertThat(root.getAttribute("style")).contains("max-width:");
+  }
+
+  public void assertDefs() {
+    List<Element> defs = getElementsByTag("defs");
+    assertThat(defs.size()).isEqualTo(1);
+    Element def = defs.get(0);
+    NodeList markers = def.getElementsByTagName("marker");
+    assertThat(markers.getLength()).isEqualTo(1);
+    Element marker = (Element) markers.item(0);
+    assertThat(marker.getAttribute("id")).isEqualTo("mermaid-arrow");
+
+    NodeList filters = def.getElementsByTagName("filter");
+    assertThat(filters.getLength()).isEqualTo(1);
+    Element filter = (Element) filters.item(0);
+    assertThat(filter.getAttribute("id")).isEqualTo("node-shadow");
+  }
+
+  public void assertNoDangerousTags() {
+    assertThat(root.getTagName()).isEqualTo("svg");
+    for (String tag : DANGEROUS_TAGS) {
+      NodeList nl = doc.getElementsByTagName(tag);
+      assertThat(nl.getLength()).isEqualTo(0);
+    }
+  }
+
+  public List<Element> getElementsByTag(String tagName) {
+    List<Element> list = new ArrayList<>();
+    NodeList nl = doc.getElementsByTagName(tagName);
+    for (int i = 0; i < nl.getLength(); i++) {
+      Node n = nl.item(i);
+      if (n instanceof Element) {
+        list.add((Element) n);
+      }
+    }
+    return list;
+  }
+
+  public List<Element> getEdgePaths() {
+    List<Element> list = new ArrayList<>();
+    for (Element p : getElementsByTag("path")) {
+      if (!"M 0 1.5 L 10 5 L 0 8.5 z".equals(p.getAttribute("d"))) {
+        list.add(p);
+      }
+    }
+    return list;
+  }
+
+  public List<Element> findSubgraphRects() {
+    List<Element> list = new ArrayList<>();
+    for (Element r : getElementsByTag("rect")) {
+      if ("4,4".equals(r.getAttribute("stroke-dasharray"))) {
+        list.add(r);
+      }
+    }
+    return list;
+  }
+
+  public @Nullable Element findPolygonWithVertices(int count) {
+    for (Element p : getElementsByTag("polygon")) {
+      String pts = p.getAttribute("points").trim();
+      if (!pts.isEmpty() && pts.split("\\s+").length == count) {
+        return p;
+      }
+    }
+    return null;
+  }
+
+  public @Nullable Element findText(String text) {
+    String expected = text.replace("\0", "").trim();
+    for (Element t : getElementsByTag("text")) {
+      String full = t.getTextContent().trim().replaceAll("\\s+", " ");
+      if (expected.equals(full) || expected.equals(t.getTextContent().trim()) || full.contains(expected)) {
+        return t;
+      }
+    }
+    for (Element t : getElementsByTag("tspan")) {
+      String full = t.getTextContent().trim();
+      if (expected.equals(full) || full.contains(expected)) {
+        return t;
+      }
+    }
+    return null;
+  }
+
+  public double getAttrDouble(Element el, String attr) {
+    return Double.parseDouble(el.getAttribute(attr));
+  }
+
+  public List<String> getAllTextContents() {
+    List<String> list = new ArrayList<>();
+    for (Element t : getElementsByTag("text")) {
+      String txt = t.getTextContent().trim().replaceAll("\\s+", " ");
+      if (!txt.isEmpty()) {
+        list.add(txt);
+      }
+    }
+    return list;
+  }
+
+  public static class Rect2D {
+    public final double x;
+    public final double y;
+    public final double width;
+    public final double height;
+    public final String label;
+
+    public Rect2D(double x, double y, double width, double height, String label) {
+      this.x = x;
+      this.y = y;
+      this.width = width;
+      this.height = height;
+      this.label = label;
+    }
+
+    public double right() {
+      return x + width;
+    }
+
+    public double bottom() {
+      return y + height;
+    }
+
+    public double centerX() {
+      return x + width / 2.0;
+    }
+
+    public boolean overlaps(Rect2D other, double tolerance) {
+      return (this.x + this.width - tolerance > other.x)
+          && (other.x + other.width - tolerance > this.x)
+          && (this.y + this.height - tolerance > other.y)
+          && (other.y + other.height - tolerance > this.y);
+    }
+
+    public boolean contains(Rect2D inner, double padding) {
+      return inner.x >= this.x + padding - 0.5
+          && inner.y >= this.y + padding - 0.5
+          && inner.right() <= this.right() - padding + 0.5
+          && inner.bottom() <= this.bottom() - padding + 0.5;
+    }
+
+    @Override
+    public String toString() {
+      return String.format("[%s: (%.1f, %.1f) %.1fx%.1f]", label, x, y, width, height);
+    }
+  }
+
+  public List<Rect2D> getNodeBoundingBoxes() {
+    List<Rect2D> list = new ArrayList<>();
+    for (Element r : getElementsByTag("rect")) {
+      String dash = r.getAttribute("stroke-dasharray");
+      String opacity = r.getAttribute("fill-opacity");
+      if ("4,4".equals(dash) || "0.95".equals(opacity)) {
+        continue;
+      }
+      double x = Double.parseDouble(r.getAttribute("x"));
+      double y = Double.parseDouble(r.getAttribute("y"));
+      double w = Double.parseDouble(r.getAttribute("width"));
+      double h = Double.parseDouble(r.getAttribute("height"));
+      list.add(new Rect2D(x, y, w, h, "NodeRect"));
+    }
+    for (Element c : getElementsByTag("circle")) {
+      double cx = Double.parseDouble(c.getAttribute("cx"));
+      double cy = Double.parseDouble(c.getAttribute("cy"));
+      double cr = Double.parseDouble(c.getAttribute("r"));
+      list.add(new Rect2D(cx - cr, cy - cr, cr * 2, cr * 2, "NodeCircle"));
+    }
+    for (Element p : getElementsByTag("polygon")) {
+      String pts = p.getAttribute("points").trim();
+      if (!pts.isEmpty()) {
+        double minX = Double.MAX_VALUE, minY = Double.MAX_VALUE;
+        double maxX = Double.MIN_VALUE, maxY = Double.MIN_VALUE;
+        for (String pair : Splitter.onPattern("\\s+").omitEmptyStrings().split(pts)) {
+          List<String> xy = Splitter.on(',').splitToList(pair);
+          if (xy.size() == 2) {
+            double px = Double.parseDouble(xy.get(0));
+            double py = Double.parseDouble(xy.get(1));
+            minX = Math.min(minX, px);
+            minY = Math.min(minY, py);
+            maxX = Math.max(maxX, px);
+            maxY = Math.max(maxY, py);
+          }
+        }
+        if (minX != Double.MAX_VALUE) {
+          list.add(new Rect2D(minX, minY, maxX - minX, maxY - minY, "NodePolygon"));
+        }
+      }
+    }
+    return list;
+  }
+
+  public List<Rect2D> getEdgeLabelBadgeBoundingBoxes() {
+    List<Rect2D> list = new ArrayList<>();
+    for (Element r : getElementsByTag("rect")) {
+      if ("0.95".equals(r.getAttribute("fill-opacity"))) {
+        double x = Double.parseDouble(r.getAttribute("x"));
+        double y = Double.parseDouble(r.getAttribute("y"));
+        double w = Double.parseDouble(r.getAttribute("width"));
+        double h = Double.parseDouble(r.getAttribute("height"));
+        list.add(new Rect2D(x, y, w, h, "LabelBadge"));
+      }
+    }
+    return list;
+  }
+
+  public List<Rect2D> getSubgraphBoundingBoxes() {
+    List<Rect2D> list = new ArrayList<>();
+    for (Element r : getElementsByTag("rect")) {
+      if ("4,4".equals(r.getAttribute("stroke-dasharray"))) {
+        double x = Double.parseDouble(r.getAttribute("x"));
+        double y = Double.parseDouble(r.getAttribute("y"));
+        double w = Double.parseDouble(r.getAttribute("width"));
+        double h = Double.parseDouble(r.getAttribute("height"));
+        list.add(new Rect2D(x, y, w, h, "SubgraphBox"));
+      }
+    }
+    return list;
+  }
+
+  public void assertNoNodeOverlaps() {
+    List<Rect2D> nodes = getNodeBoundingBoxes();
+    for (int i = 0; i < nodes.size(); i++) {
+      for (int j = i + 1; j < nodes.size(); j++) {
+        Rect2D a = nodes.get(i);
+        Rect2D b = nodes.get(j);
+        assertWithMessage("Node overlap detected between " + a + " and " + b)
+            .that(a.overlaps(b, 2.0))
+            .isFalse();
+      }
+    }
+  }
+
+  public void assertNoLabelNodeOverlaps() {
+    List<Rect2D> nodes = getNodeBoundingBoxes();
+    List<Rect2D> labels = getEdgeLabelBadgeBoundingBoxes();
+    for (Rect2D badge : labels) {
+      for (Rect2D node : nodes) {
+        assertWithMessage("Edge label badge " + badge + " overlaps node " + node)
+            .that(badge.overlaps(node, 2.0))
+            .isFalse();
+      }
+    }
+  }
+
+  public void assertSubgraphsDoNotOverlap() {
+    List<Rect2D> sgs = getSubgraphBoundingBoxes();
+    for (int i = 0; i < sgs.size(); i++) {
+      for (int j = i + 1; j < sgs.size(); j++) {
+        Rect2D a = sgs.get(i);
+        Rect2D b = sgs.get(j);
+        boolean oneContainsOther = a.contains(b, 0) || b.contains(a, 0);
+        if (!oneContainsOther) {
+          assertWithMessage("Sibling subgraphs overlap: " + a + " and " + b)
+              .that(a.overlaps(b, 2.0))
+              .isFalse();
+        }
+      }
+    }
+  }
+}
diff --git a/modules/jgit b/modules/jgit
index 200be16..fe01de6 160000
--- a/modules/jgit
+++ b/modules/jgit
@@ -1 +1 @@
-Subproject commit 200be16661d3b9983bd34affd8e424daf5ad23d7
+Subproject commit fe01de69f44611049b664188652bf9e340f0b1f8
diff --git a/resources/com/google/gitiles/static/doc.css b/resources/com/google/gitiles/static/doc.css
index 3164268..dde3aa7 100644
--- a/resources/com/google/gitiles/static/doc.css
+++ b/resources/com/google/gitiles/static/doc.css
@@ -376,3 +376,13 @@
 .col-11 { width: 88.690%; }
 .col-12 { width: 97.024%; }
 .cols hr { width: 80%; }
+
+.mermaid-container {
+  margin: 1.5em 0;
+  text-align: center;
+  overflow-x: auto;
+}
+.mermaid-container svg {
+  display: inline-block;
+  vertical-align: middle;
+}