diff --git a/mintlify/css-perf-patch.js b/mintlify/css-perf-patch.js
new file mode 100644
index 00000000..a85f2dc8
--- /dev/null
+++ b/mintlify/css-perf-patch.js
@@ -0,0 +1,276 @@
+// Neutralizes a Chrome/Blink pathology triggered by Mintlify's `:has()` CSS.
+//
+// THE BUG: three of Mintlify's code-block rules have selectors that start
+// with a bare `:has(` (unanchored subject). The first time a matching
+// standalone code block renders, Blink permanently marks the document root
+// ":has-affected". While such marks exist AND `:has()` rules are present in
+// active stylesheets, every forced style recalc walks the entire document
+// (~30x slower: ~0.1ms -> ~20ms per recalc). Mermaid's ~280 getBBox
+// measurements during navigation then freeze the page for many seconds
+// (hovers dead; scroll survives on the compositor thread).
+//
+// Verified properties (see PR #697 for the experiments):
+// - the marks survive rule removal, content replacement, and forced
+// recalcs — they last for the life of the tab;
+// - but they only COST while `:has()` rules are present: with every
+// :has rule removed, recalcs drop back to ~0.1ms instantly;
+// - partial removal does not help — any remaining :has rule keeps the
+// expensive path armed.
+//
+// TWO DEFENSES, both in this file:
+//
+// 1. PREVENTION (always on): rewrite the three bare-:has selectors to
+// preceding-sibling equivalents the moment stylesheets appear. In
+// Mintlify's DOM the ":has() parent" is always a preceding sibling, so
+// this is visually identical (verified pixel-identical and computed-
+// style-identical across all Global Accounts pages, light + dark).
+// Exact-match only: if Mintlify ships different CSS this is a no-op.
+//
+// 2. CURE (poisoned tabs only): custom JS runs after first paint, so a tab
+// whose FIRST page renders a matching code block is marked before we can
+// prevent it. For those tabs we detect the poisoned state (one cheap
+// probe) and, around each SPA navigation, temporarily stash-and-delete
+// ALL :has rules (fighting mid-navigation stylesheet re-injection), then
+// restore them in place — same owner sheet, same index, so cascade and
+// @layer order are preserved. Cosmetic :has styling (sidebar chevron
+// hiding, code-block corner effects) is simplified for the few seconds
+// of the transition on those rare tabs; steady-state look is untouched.
+//
+// The complete fix is Mintlify correcting the three selectors upstream —
+// delete 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',
+ };
+
+ // Rewrites a selector list via MAP. Returns null when nothing matched.
+ function mapSelectorList(sel) {
+ if (!sel || sel.indexOf(':has(') === -1) return null;
+ 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]);
+ }
+ }
+ return hit ? out.join(',') : null;
+ }
+
+ /* ------------------------- defense 1: prevention ------------------------ */
+
+ 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.selectorText && rule.cssRules && rule.cssRules.length) {
+ patchRules(rule); // @media / @layer / @supports groups
+ continue;
+ }
+ var mapped = mapSelectorList(rule.selectorText);
+ if (!mapped) continue;
+ try {
+ var css = rule.cssText;
+ var body = css.slice(css.indexOf('{'));
+ owner.deleteRule(i);
+ owner.insertRule(mapped + ' ' + body, i);
+ } catch (e) {
+ /* leave the rule as-is */
+ }
+ }
+ }
+
+ /* --------------------------- defense 2: cure ---------------------------- */
+
+ var poisoned = false;
+ var poisonKnown = false; // verdict is only trustworthy once the page has rendered
+ var curing = false;
+ var stash = []; // { owner, index, cssText } in deletion order (desc index per owner)
+ var restoreTimer = 0;
+
+ function probeCost() {
+ var root = document.documentElement;
+ void root.offsetHeight;
+ var d = document.createElement('div');
+ document.body.appendChild(d);
+ var t0 = performance.now();
+ void root.offsetHeight;
+ var cost = performance.now() - t0;
+ d.remove();
+ void root.offsetHeight;
+ return cost;
+ }
+
+ function detectPoison() {
+ if (!document.body) return;
+ try {
+ var runs = [probeCost(), probeCost(), probeCost()].sort(function (a, b) { return a - b; });
+ poisoned = runs[1] > 4; // healthy ~0.1-1ms, poisoned ~9-20ms
+ poisonKnown = true;
+ } catch (e) {
+ poisoned = false;
+ }
+ }
+
+ function stashDeleteIn(owner) {
+ var list;
+ try {
+ list = owner.cssRules;
+ } catch (e) {
+ return;
+ }
+ if (!list) return;
+ for (var i = list.length - 1; i >= 0; i--) {
+ var rule = list[i];
+ var sel = rule.selectorText;
+ if (sel && sel.indexOf(':has(') !== -1) {
+ try {
+ stash.push({ owner: owner, index: i, cssText: rule.cssText });
+ owner.deleteRule(i);
+ continue;
+ } catch (e) {
+ /* keep rule */
+ }
+ }
+ if (rule.cssRules && rule.cssRules.length) stashDeleteIn(rule);
+ }
+ }
+
+ function stashDeleteAll() {
+ for (var i = 0; i < document.styleSheets.length; i++) {
+ stashDeleteIn(document.styleSheets[i]);
+ }
+ }
+
+ function restoreStash() {
+ // Reinsert in reverse deletion order: per owner that yields ascending
+ // indexes, so each recorded index is valid again at insertion time.
+ for (var i = stash.length - 1; i >= 0; i--) {
+ var entry = stash[i];
+ var cssText = entry.cssText;
+ // Never restore a bare-:has original — apply the prevention rewrite.
+ var brace = cssText.indexOf('{');
+ var mapped = mapSelectorList(cssText.slice(0, brace).trim());
+ if (mapped) cssText = mapped + ' ' + cssText.slice(brace);
+ try {
+ var max = entry.owner.cssRules ? entry.owner.cssRules.length : 0;
+ entry.owner.insertRule(cssText, Math.min(entry.index, max));
+ } catch (e) {
+ /* owner sheet gone or rule now invalid — skip */
+ }
+ }
+ stash = [];
+ curing = false;
+ }
+
+ function startCure() {
+ if (!poisoned || curing) return;
+ curing = true;
+ stashDeleteAll();
+ clearTimeout(restoreTimer);
+ restoreTimer = setTimeout(restoreStash, 4000);
+ }
+
+ function extendCure() {
+ // another navigation while a window is open: re-sweep + push restore out
+ stashDeleteAll();
+ clearTimeout(restoreTimer);
+ restoreTimer = setTimeout(restoreStash, 4000);
+ }
+
+ function onNavigate() {
+ // The scheduled detection waits for the page to settle, but a user can
+ // navigate before that (land on a poisoning page, click within ~2s).
+ // By navigation time the landing page has rendered, so a probe taken
+ // right now is trustworthy — spend ~100ms to check rather than risk a
+ // multi-second freeze.
+ if (!poisonKnown) detectPoison();
+ if (!poisoned) return;
+ if (curing) extendCure();
+ else startCure();
+ }
+
+ // Navigation triggers: link clicks (earliest), history traversal, and a
+ // pathname watcher as fallback for programmatic navigation.
+ document.addEventListener(
+ 'click',
+ function (e) {
+ var a = e.target && e.target.closest && e.target.closest('a[href]');
+ if (!a) return;
+ var href = a.getAttribute('href') || '';
+ if (href.charAt(0) === '/' && href.indexOf('//') !== 0) onNavigate();
+ },
+ true
+ );
+ window.addEventListener('popstate', onNavigate);
+ var lastPath = location.pathname;
+
+ /* ------------------------------ scheduling ------------------------------ */
+
+ function sweep() {
+ if (curing) {
+ stashDeleteAll(); // mid-navigation sheet re-injection brings rules back
+ } else {
+ for (var i = 0; i < document.styleSheets.length; i++) {
+ patchRules(document.styleSheets[i]);
+ }
+ }
+ if (location.pathname !== lastPath) {
+ lastPath = location.pathname;
+ onNavigate();
+ }
+ }
+
+ // Sweep every frame while the page settles (idempotent), then only when
+ // new stylesheets appear (SPA navigations). Poison detection runs once,
+ // after the page has settled.
+ var settleAt = 0;
+ function loop(now) {
+ sweep();
+ if (document.readyState === 'complete') {
+ if (!settleAt) settleAt = now;
+ if (now - settleAt > 2000) {
+ detectPoison();
+ return watch();
+ }
+ }
+ requestAnimationFrame(loop);
+ }
+
+ 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') {
+ sweep();
+ return;
+ }
+ }
+ }
+ if (location.pathname !== lastPath) {
+ lastPath = location.pathname;
+ onNavigate();
+ }
+ }).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;
}