diff --git a/mintlify/css-perf-patch.js b/mintlify/css-perf-patch.js
new file mode 100644
index 00000000..07cf1633
--- /dev/null
+++ b/mintlify/css-perf-patch.js
@@ -0,0 +1,109 @@
+// Rewrites three of Mintlify's code-block CSS rules whose selectors start
+// with a bare `:has(` — an unanchored subject that, in Chrome/Blink,
+// permanently flags the document root as ":has-affected" the first time a
+// matching code block renders. From then on, every forced style recalc in
+// the tab walks the entire document (~30x slower), which turns mermaid's
+// ~280 getBBox measurements during navigation into a multi-second freeze
+// (hover effects dead; scroll survives on the compositor thread).
+//
+// In Mintlify's DOM the ":has() parent" is always expressible as a preceding
+// sibling instead, which Blink matches cheaply. Verified pixel-identical and
+// computed-style-identical across all Global Accounts pages, light + dark.
+//
+// Rules are only touched on an exact selector match, so if Mintlify ships
+// different CSS this becomes a no-op — the look can never break; worst case
+// the perf fix silently stops applying.
+//
+// Coverage caveat: custom JS runs after first paint, so a tab whose FIRST
+// page is one that renders standalone code blocks (e.g. implementation-
+// overview) is poisoned before this runs, and that can't be undone. All
+// pages rendered after this script runs (every SPA navigation) are safe.
+// The complete fix is Mintlify correcting these selectors upstream — remove
+// this file when they do.
+(function () {
+ var MAP = {
+ ':has([data-floating-buttons]) > [data-component-part="code-block-root"] pre > code':
+ '[data-floating-buttons] ~ [data-component-part="code-block-root"] pre > code',
+ ':has([data-component-part="code-block-header"]) > [data-component-part="code-block-root"] pre > code':
+ '[data-component-part="code-block-header"] ~ [data-component-part="code-block-root"] pre > code',
+ ':has([data-component-part="code-group-tab-bar"]) [data-component-part="code-block-root"] pre > code':
+ '[data-component-part="code-group-tab-bar"] ~ [data-component-part="code-block-root"] pre > code, ' +
+ '[data-component-part="code-group-tab-bar"] ~ * [data-component-part="code-block-root"] pre > code',
+ };
+
+ function patchRules(owner) {
+ var list;
+ try {
+ list = owner.cssRules;
+ } catch (e) {
+ return; // cross-origin sheet
+ }
+ if (!list) return;
+ for (var i = list.length - 1; i >= 0; i--) {
+ var rule = list[i];
+ if (rule.cssRules && rule.cssRules.length) {
+ patchRules(rule); // @media / @layer / @supports groups
+ continue;
+ }
+ var sel = rule.selectorText;
+ if (!sel || sel.indexOf(':has(') === -1) continue;
+ var parts = sel.split(',');
+ var out = [];
+ var hit = false;
+ for (var j = 0; j < parts.length; j++) {
+ var trimmed = parts[j].trim();
+ if (MAP[trimmed]) {
+ out.push(MAP[trimmed]);
+ hit = true;
+ } else {
+ out.push(parts[j]);
+ }
+ }
+ if (!hit) continue;
+ try {
+ var css = rule.cssText;
+ var body = css.slice(css.indexOf('{'));
+ owner.deleteRule(i);
+ owner.insertRule(out.join(',') + ' ' + body, i);
+ } catch (e) {
+ /* leave the rule as-is */
+ }
+ }
+ }
+
+ function scanAll() {
+ for (var i = 0; i < document.styleSheets.length; i++) {
+ patchRules(document.styleSheets[i]);
+ }
+ }
+
+ // Scan every frame while the page is still settling (idempotent: once a
+ // rule is rewritten its selector no longer matches the map)...
+ var settleAt = 0;
+ function loop(now) {
+ scanAll();
+ if (document.readyState === 'complete') {
+ if (!settleAt) settleAt = now;
+ if (now - settleAt > 2000) return watch();
+ }
+ requestAnimationFrame(loop);
+ }
+
+ // ...then only re-scan when new stylesheets appear (SPA navigations).
+ function watch() {
+ new MutationObserver(function (muts) {
+ for (var m = 0; m < muts.length; m++) {
+ var added = muts[m].addedNodes;
+ for (var n = 0; n < added.length; n++) {
+ var tag = added[n].tagName;
+ if (tag === 'STYLE' || tag === 'LINK') {
+ scanAll();
+ return;
+ }
+ }
+ }
+ }).observe(document.documentElement, { childList: true, subtree: true });
+ }
+
+ loop(performance.now());
+})();
diff --git a/mintlify/docs.json b/mintlify/docs.json
index e7296c73..41d5b8ec 100644
--- a/mintlify/docs.json
+++ b/mintlify/docs.json
@@ -457,7 +457,7 @@
},
"footer": {},
"head": {
- "raw": "",
+ "raw": "",
"links": [
{
"rel": "preload",
diff --git a/mintlify/sidebar-toggle.js b/mintlify/sidebar-toggle.js
index 34244460..28a01459 100644
--- a/mintlify/sidebar-toggle.js
+++ b/mintlify/sidebar-toggle.js
@@ -226,8 +226,26 @@
});
}
+ // Page-scoped classes on — the cheap replacement for html:has(...)
+ // selectors in style.css / head.raw. A :has() anchored at the root gets
+ // re-evaluated on DOM mutations anywhere in the document, which froze
+ // navigation for seconds on pages that build DOM incrementally (mermaid
+ // diagrams). This is the effective setter on both full loads and SPA
+ // navigations (head.raw carries a pre-paint copy, but the hosted renderer
+ // currently strips head.raw scripts).
+ function updatePageClasses() {
+ var root = document.documentElement;
+ var path = location.pathname.replace(/\/+$/, '') || '/';
+ root.classList.toggle('ls-page-flow-builder', path === '/flow-builder');
+ root.classList.toggle('ls-page-wallet-demo', path === '/global-accounts/demo');
+ // Custom-layout pages (frontmatter mode: "custom") — detected from the
+ // DOM so future custom pages are covered without listing paths here.
+ root.classList.toggle('ls-page-custom', !!document.querySelector('.is-custom'));
+ }
+
function sync() {
var root = document.documentElement;
+ updatePageClasses();
// Navigation/first paint must never animate (only deliberate toggles do —
// see the click handler). Clear the animate flag, and snap the rail button
// for this navigation-driven state change so its icon doesn't ghost in/out
@@ -249,10 +267,11 @@
}
// SPA navigation: Mintlify swaps content without a full reload. On a path
- // change, re-sync. Otherwise only re-add the rail if Mintlify wiped it — a
- // cheap guard so we don't read layout every frame. Custom-layout pages are
- // handled by sync() on navigation plus the CSS :has(.is-custom) rule, so the
- // rail never lingers visibly even without per-frame polling here.
+ // change, re-sync. Otherwise only schedule the rAF-debounced ensure pass
+ // when something is actually out of sync: the rail/footer button got wiped,
+ // or the .is-custom marker (which mounts after the pathname flips) doesn't
+ // match the ls-page-custom class yet — a cheap guard so we don't read
+ // layout every frame.
var lastPath = location.pathname;
var ensureScheduled = false;
function scheduleEnsure() {
@@ -260,6 +279,7 @@
ensureScheduled = true;
requestAnimationFrame(function () {
ensureScheduled = false;
+ updatePageClasses();
ensureRail();
ensureFooterBtn();
});
@@ -268,7 +288,13 @@
if (location.pathname !== lastPath) {
lastPath = location.pathname;
sync();
- } else if (
+ return;
+ }
+ var customStale =
+ !!document.querySelector('.is-custom') !==
+ document.documentElement.classList.contains('ls-page-custom');
+ if (
+ customStale ||
!rail ||
!document.body.contains(rail) ||
!footerBtn ||
diff --git a/mintlify/style.css b/mintlify/style.css
index e33a33c0..191d93b8 100644
--- a/mintlify/style.css
+++ b/mintlify/style.css
@@ -4096,18 +4096,29 @@ html.dark #flow-builder-container {
}
}
-/* Prevent outer page scroll when flow-builder is present */
-html:has(#flow-builder-container),
-html:has(#flow-builder-container) body {
+/* Prevent outer page scroll when flow-builder is present.
+
+ PERF: these page-scoped rules key off html.ls-page-flow-builder — a class
+ set from the pathname by sidebar-toggle.js (on load and on SPA navigation)
+ — NOT html:has(#flow-builder-container). A root-anchored :has() marks the
+ document root ":has-affected" in Blink, after which every forced style
+ recalc walks the whole document; pages that build DOM incrementally (e.g.
+ mermaid diagrams) then freeze navigation for seconds. (head.raw also
+ carries a pre-paint class setter, but the hosted renderer currently strips
+ head.raw scripts, so the JS is the effective mechanism; the sub-second
+ window before it runs on a full load of these two embed pages is not
+ noticeable.) */
+html.ls-page-flow-builder,
+html.ls-page-flow-builder body {
overflow: hidden !important;
}
/* Flow Builder mobile breadcrumb — hide text + chevron, keep hamburger, show (0, 0, 0) */
-html:has(#flow-builder-container) #navbar button[class*="h-14"][class*="text-left"] > *:not(:first-child) {
+html.ls-page-flow-builder #navbar button[class*="h-14"][class*="text-left"] > *:not(:first-child) {
display: none !important;
}
-html:has(#flow-builder-container) #navbar button[class*="h-14"][class*="text-left"]::after {
+html.ls-page-flow-builder #navbar button[class*="h-14"][class*="text-left"]::after {
content: "(0, 0, 0)";
margin-left: auto;
font-family: "Suisse Intl Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
@@ -4116,7 +4127,7 @@ html:has(#flow-builder-container) #navbar button[class*="h-14"][class*="text-lef
color: var(--ls-gray-400);
}
-html.dark:has(#flow-builder-container) #navbar button[class*="h-14"][class*="text-left"]::after {
+html.dark.ls-page-flow-builder #navbar button[class*="h-14"][class*="text-left"]::after {
color: var(--ls-gray-700);
}
@@ -4252,19 +4263,20 @@ html.dark #wallet-demo-host {
}
}
-/* Prevent outer page scroll when the wallet demo is present */
-html:has(#wallet-demo-container),
-html:has(#wallet-demo-container) body {
+/* Prevent outer page scroll when the wallet demo is present.
+ PERF: class-keyed, not html:has() — see the flow-builder note above. */
+html.ls-page-wallet-demo,
+html.ls-page-wallet-demo body {
overflow: hidden !important;
}
/* Wallet demo mobile breadcrumb — hide text + chevron, keep hamburger, show
(0, 0, 0) on the right (matches the flow-builder tab). */
-html:has(#wallet-demo-container) #navbar button[class*="h-14"][class*="text-left"] > *:not(:first-child) {
+html.ls-page-wallet-demo #navbar button[class*="h-14"][class*="text-left"] > *:not(:first-child) {
display: none !important;
}
-html:has(#wallet-demo-container) #navbar button[class*="h-14"][class*="text-left"]::after {
+html.ls-page-wallet-demo #navbar button[class*="h-14"][class*="text-left"]::after {
content: "(0, 0, 0)";
margin-left: auto;
font-family: "Suisse Intl Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
@@ -4273,7 +4285,7 @@ html:has(#wallet-demo-container) #navbar button[class*="h-14"][class*="text-left
color: var(--ls-gray-400);
}
-html.dark:has(#wallet-demo-container) #navbar button[class*="h-14"][class*="text-left"]::after {
+html.dark.ls-page-wallet-demo #navbar button[class*="h-14"][class*="text-left"]::after {
color: var(--ls-gray-700);
}
@@ -4823,8 +4835,12 @@ html.ls-nav-collapsed .ls-nav-rail::after {
/* Custom-layout pages (frontmatter mode: "custom", e.g. the flow builder) have
no docs sidebar — Mintlify tags them with .is-custom — so there's nothing to
toggle. The JS won't inject the rail there either; this is the no-flash guard
- during SPA navigation. */
-html:has(.is-custom) .ls-nav-rail {
+ during SPA navigation. PERF: class-keyed, not html:has() — the class is set
+ only by sidebar-toggle.js, from the DOM, once the body exists. That's
+ sufficient: on a full load of a custom page the rail is never injected in
+ the first place (hasVisibleSidebar() is false), so this rule only matters
+ for SPA navigation, where the JS is already running. */
+html.ls-page-custom .ls-nav-rail {
display: none !important;
}