diff --git a/.gitignore b/.gitignore
index e802a4b96..2635e08ae 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,8 +50,9 @@ figma-*.md
# Cursor IDE rules (personal config)
.cursor/rules/
-# Personal todo files
+# Personal todo + notes files
TODO-*.md
+pat-notes.md
# Icon build script (local tool, requires license key)
scripts/export-icons.js
diff --git a/components/grid-wallet-demo/src/components/ApiPanel/ApiPanelEmpty.module.scss b/components/grid-wallet-demo/src/components/ApiPanel/ApiPanelEmpty.module.scss
index 1bf43b2f2..a84b608e7 100644
--- a/components/grid-wallet-demo/src/components/ApiPanel/ApiPanelEmpty.module.scss
+++ b/components/grid-wallet-demo/src/components/ApiPanel/ApiPanelEmpty.module.scss
@@ -121,9 +121,10 @@
font-feature-settings: 'salt' 1;
}
-/* Mobile (Playground): one screenful with the centered message, not the full
- intrinsic skeleton height — so the API section isn't a tall empty void. */
-@media (max-width: $breakpoint-layout-mobile) {
+/* Stacked layouts (mobile + laptop): cap to one screenful with the centered
+ message, not the full intrinsic skeleton height — so the API section isn't a
+ tall empty void below the app. */
+@media (max-width: ($breakpoint-layout-wide - 1px)) {
.root {
flex: none;
height: calc(100dvh - 52px);
diff --git a/components/grid-wallet-demo/src/components/ColumnResizeHandle/ColumnResizeHandle.module.scss b/components/grid-wallet-demo/src/components/ColumnResizeHandle/ColumnResizeHandle.module.scss
index 322d3d8a6..8dd6e157d 100644
--- a/components/grid-wallet-demo/src/components/ColumnResizeHandle/ColumnResizeHandle.module.scss
+++ b/components/grid-wallet-demo/src/components/ColumnResizeHandle/ColumnResizeHandle.module.scss
@@ -8,7 +8,9 @@
margin-right: -6px;
cursor: col-resize;
position: relative;
- z-index: 1;
+ // Above the panel headers (sticky, z-index 3) so the divider + hover highlight
+ // run the full height instead of being clipped by the "API calls" header.
+ z-index: 4;
&::before {
content: '';
@@ -20,18 +22,24 @@
border-left: var(--stroke-xs) solid var(--border-primary);
}
+ // Hover highlight: a full-height line over the divider, matching the Mintlify
+ // docs sidebar resize edge exactly (2px; gray-300 light / gray-700 dark — the
+ // same values as the docs' --ls-gray-300 / --ls-gray-700).
&::after {
content: '';
position: absolute;
- top: 50%;
+ top: 0;
+ bottom: 0;
left: 50%;
- transform: translate(-50%, -50%);
- width: 3px;
- height: 32px;
- border-radius: var(--corner-radius-2xs);
- background: var(--border-secondary);
+ transform: translateX(-50%);
+ width: 2px;
+ background: var(--color-gray-300);
opacity: 0;
transition: opacity 150ms ease;
+
+ :global([data-theme='dark']) & {
+ background: var(--color-gray-700);
+ }
}
&:hover::after,
diff --git a/components/grid-wallet-demo/src/hooks/useColumnResize.ts b/components/grid-wallet-demo/src/hooks/useColumnResize.ts
index 2b0f53e86..32dcdc39d 100644
--- a/components/grid-wallet-demo/src/hooks/useColumnResize.ts
+++ b/components/grid-wallet-demo/src/hooks/useColumnResize.ts
@@ -4,7 +4,9 @@ import { useCallback, useLayoutEffect, useRef, useState } from 'react';
const CONFIGURE_WIDTH = 475;
const MIN_APP = 320;
-const MIN_API = 320;
+// Never resize the API column below its default/snap width (= the configure
+// column width); dragging only widens it from there.
+const MIN_API = CONFIGURE_WIDTH;
const SNAP_THRESHOLD = 28;
/** Default + snap target — matches configure column width. */
diff --git a/components/grid-wallet-demo/src/styles/breakpoints.scss b/components/grid-wallet-demo/src/styles/breakpoints.scss
index f13e6a64a..603a6df49 100644
--- a/components/grid-wallet-demo/src/styles/breakpoints.scss
+++ b/components/grid-wallet-demo/src/styles/breakpoints.scss
@@ -1,5 +1,10 @@
-/** App + API side-by-side inside the right column (large desktop). */
-$breakpoint-layout-wide: 1800px;
+/** App + API side-by-side inside the right column. Above this the layout is 3
+ columns: Configure (475) | App (phone) | API (475 default), so the App column
+ is roughly (viewport − 950). The phone needs ~466px (434 shell + 16px inset
+ each side) to render full size, i.e. ~1416px of viewport; below that the App
+ column squeezes the phone tiny. Stack at 1440px so the phone never shrinks
+ before App + API stack vertically (each full-width). Tune to taste. */
+$breakpoint-layout-wide: 1440px;
/** Configure stacks above app + API (phone). */
$breakpoint-layout-mobile: 767px;
diff --git a/mintlify/docs.json b/mintlify/docs.json
index cdfb7a9e7..478528995 100644
--- a/mintlify/docs.json
+++ b/mintlify/docs.json
@@ -446,7 +446,7 @@
},
"footer": {},
"head": {
- "raw": "",
+ "raw": "",
"links": [
{
"rel": "preload",
diff --git a/mintlify/images/icons/IconSidebarLeftArrow.svg b/mintlify/images/icons/IconSidebarLeftArrow.svg
new file mode 100644
index 000000000..cc171afec
--- /dev/null
+++ b/mintlify/images/icons/IconSidebarLeftArrow.svg
@@ -0,0 +1,5 @@
+
diff --git a/mintlify/images/icons/IconSidebarRightArrow.svg b/mintlify/images/icons/IconSidebarRightArrow.svg
new file mode 100644
index 000000000..66faa8fdd
--- /dev/null
+++ b/mintlify/images/icons/IconSidebarRightArrow.svg
@@ -0,0 +1,5 @@
+
diff --git a/mintlify/sidebar-toggle.js b/mintlify/sidebar-toggle.js
new file mode 100644
index 000000000..a48c758c1
--- /dev/null
+++ b/mintlify/sidebar-toggle.js
@@ -0,0 +1,243 @@
+// Collapsible + resizable docs sidebar (desktop).
+//
+// A rail on the right edge of #sidebar-content:
+// - drag it to resize the sidebar (clamped MIN..MAX); release below the snap
+// threshold to collapse.
+// - click it (no drag) to toggle collapse.
+// Collapsed shows a slim visible rail with a bare icon (click/pointer to
+// reopen); expanded shows the toggle on hover at the edge (col-resize). Width +
+// collapsed state persist in localStorage and are restored pre-paint by the
+// inline script in docs.json head.raw, so there's no flash.
+
+(function () {
+ var DESKTOP_MIN = 1024;
+ var KEY = 'ls-nav-collapsed';
+ // NOTE: keep in sync with the demo-path check in docs.json head.raw — the
+ // pre-paint script collapses the playground before this runs (no flash). The
+ // path lives in both because the pre-paint must run inline, before this file.
+ var DEMO_PATHS = ['/global-accounts/demo', '/global-accounts/demo/'];
+ var MIN_WIDTH = 280; // the original sidebar width — only resizes wider
+ var MAX_WIDTH = 420;
+ var SNAP_COLLAPSE = 240; // drag left past this x -> collapse
+ var DRAG_THRESHOLD = 4; // px of movement before a press counts as a drag
+
+ var rail = null;
+
+ function isDesktop() {
+ return window.innerWidth >= DESKTOP_MIN;
+ }
+
+ function isDemo() {
+ return DEMO_PATHS.indexOf(location.pathname) !== -1;
+ }
+
+ // #sidebar-content is in the DOM on every docs page, but custom-layout pages
+ // (frontmatter mode: "custom", e.g. the flow builder) keep it and hide it
+ // (display:none). getClientRects() is empty for a non-rendered element, so this
+ // is true only when there's a real sidebar to toggle — no sidebar, no rail.
+ function hasVisibleSidebar() {
+ var sc = document.getElementById('sidebar-content');
+ return !!sc && sc.getClientRects().length > 0;
+ }
+
+ function getPref() {
+ try {
+ return localStorage.getItem(KEY);
+ } catch (e) {
+ return null;
+ }
+ }
+
+ function setPref(value) {
+ try {
+ localStorage.setItem(KEY, value);
+ } catch (e) {
+ /* private mode — toggle still works for the session */
+ }
+ }
+
+ function clampWidth(w) {
+ return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, Math.round(w)));
+ }
+
+ // Width is session-only — not persisted, so a refresh resets to the 280px
+ // default (the CSS var fallback) with no post-paint resize jump.
+ function applyWidth(w) {
+ document.documentElement.style.setProperty('--ls-sidebar-width', w + 'px');
+ }
+
+ // The playground (demo) always starts collapsed — it needs the horizontal
+ // space — regardless of the saved preference. Every other page follows the
+ // remembered preference (default expanded).
+ function shouldCollapse() {
+ if (isDemo()) return true;
+ return getPref() === '1';
+ }
+
+ function isCollapsed() {
+ return document.documentElement.classList.contains('ls-nav-collapsed');
+ }
+
+ function updateRail() {
+ if (!rail) return;
+ var collapsed = isCollapsed();
+ rail.setAttribute('aria-label', collapsed ? 'Show navigation' : 'Hide navigation');
+ rail.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
+ }
+
+ function applyState(collapsed) {
+ document.documentElement.classList.toggle('ls-nav-collapsed', collapsed);
+ updateRail();
+ }
+
+ function removeRail() {
+ if (rail && rail.parentNode) rail.parentNode.removeChild(rail);
+ rail = null;
+ }
+
+ function ensureRail() {
+ if (!isDesktop() || !hasVisibleSidebar()) {
+ removeRail();
+ return;
+ }
+ if (rail && document.body.contains(rail)) return;
+
+ rail = document.createElement('button');
+ rail.type = 'button';
+ rail.className = 'ls-nav-rail';
+ rail.innerHTML = '';
+ attachInteractions(rail);
+ document.body.appendChild(rail);
+ updateRail();
+ }
+
+ // Drag = resize (expanded only); plain click = toggle collapse (either state,
+ // mouse or keyboard).
+ function attachInteractions(el) {
+ var startX = 0;
+ var moved = false;
+ var dragging = false;
+ var dragEndAt = 0;
+ var animTimer = 0;
+
+ function onMove(e) {
+ if (!moved && Math.abs(e.clientX - startX) > DRAG_THRESHOLD) {
+ moved = true;
+ dragging = true;
+ document.documentElement.classList.add('ls-nav-dragging');
+ document.body.style.userSelect = 'none';
+ }
+ if (!dragging) return;
+ // Live: crossing the snap threshold collapses immediately (no release
+ // needed); dragging back out reopens and resumes resizing.
+ if (e.clientX < SNAP_COLLAPSE) {
+ if (!isCollapsed()) applyState(true);
+ } else {
+ if (isCollapsed()) applyState(false);
+ applyWidth(clampWidth(e.clientX));
+ }
+ }
+
+ function onUp(e) {
+ document.removeEventListener('mousemove', onMove, true);
+ document.removeEventListener('mouseup', onUp, true);
+ if (!moved) return; // a click — handled by the click listener
+ dragEndAt = Date.now();
+ document.documentElement.classList.remove('ls-nav-dragging');
+ document.body.style.userSelect = '';
+ // State was already applied live during the drag — just persist it.
+ if (isCollapsed()) {
+ setPref('1');
+ } else {
+ applyWidth(clampWidth(e.clientX));
+ setPref('0');
+ }
+ }
+
+ // Resize only from the expanded edge; the collapsed rail is click-only.
+ el.addEventListener('mousedown', function (e) {
+ if (e.button !== 0 || isCollapsed()) return;
+ e.preventDefault();
+ startX = e.clientX;
+ moved = false;
+ dragging = false;
+ document.addEventListener('mousemove', onMove, true);
+ document.addEventListener('mouseup', onUp, true);
+ });
+
+ el.addEventListener('click', function () {
+ if (Date.now() - dragEndAt < 300) return; // swallow the click after a drag
+ var next = !isCollapsed();
+ setPref(next ? '1' : '0');
+ // ls-nav-animating turns the collapse transition on for this deliberate
+ // toggle (it's off by default so navigation never animates) and suppresses
+ // the hover reveal so the button/edge don't flash from the cursor sitting
+ // over the rail mid-transition. Add it + force a reflow before applyState
+ // so the width/opacity change animates from the current value, not snaps.
+ document.documentElement.classList.add('ls-nav-animating');
+ document.documentElement.getBoundingClientRect(); // force reflow to arm the transition
+ applyState(next);
+ clearTimeout(animTimer);
+ animTimer = setTimeout(function () {
+ document.documentElement.classList.remove('ls-nav-animating');
+ }, 320);
+ });
+ }
+
+ function sync() {
+ var root = document.documentElement;
+ // 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
+ // between pages; restore its transition next frame so hover reveals animate.
+ root.classList.remove('ls-nav-animating');
+ root.classList.add('ls-nav-snap');
+ applyState(shouldCollapse());
+ ensureRail();
+ requestAnimationFrame(function () {
+ root.classList.remove('ls-nav-snap');
+ });
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', sync);
+ } else {
+ sync();
+ }
+
+ // 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.
+ var lastPath = location.pathname;
+ var ensureScheduled = false;
+ function scheduleEnsureRail() {
+ if (ensureScheduled) return;
+ ensureScheduled = true;
+ requestAnimationFrame(function () {
+ ensureScheduled = false;
+ ensureRail();
+ });
+ }
+ var observer = new MutationObserver(function () {
+ if (location.pathname !== lastPath) {
+ lastPath = location.pathname;
+ sync();
+ } else if (!rail || !document.body.contains(rail)) {
+ scheduleEnsureRail();
+ }
+ });
+ observer.observe(document.body, { childList: true, subtree: true });
+ window.addEventListener('popstate', sync);
+
+ var rafPending = false;
+ window.addEventListener('resize', function () {
+ if (rafPending) return;
+ rafPending = true;
+ requestAnimationFrame(function () {
+ rafPending = false;
+ ensureRail();
+ });
+ });
+})();
diff --git a/mintlify/style.css b/mintlify/style.css
index 9ec1c9a96..8b3fbe9ac 100644
--- a/mintlify/style.css
+++ b/mintlify/style.css
@@ -277,9 +277,13 @@ h1#page-title,
text-shadow: none !important;
}
-/* Navbar links: Medium weight (500) — avoids font flash during SPA navigation
- since Medium is preloaded via next/font/local in docs.json */
+/* Navbar links (GitHub, Book a live demo, Dashboard): Medium weight (500). The
+ hosted Mintlify build wraps the link text in a and applies a body-weight
+ rule to descendants, which drops the text back to 400 even though the anchor is
+ 500 — so target the inner elements too, not just the anchor. (Medium is
+ preloaded via next/font/local in docs.json to avoid a flash on SPA nav.) */
.navbar-link a,
+.navbar-link a *,
#navbar a.link:not(.nav-tabs-item) {
font-weight: 500 !important;
}
@@ -780,9 +784,9 @@ button[class*="text-primary"] * {
#sidebar-content {
background-color: var(--ls-gray-100) !important;
border-right: 0.5px solid var(--ls-black-10) !important;
- width: 280px !important;
- min-width: 280px !important;
- max-width: 280px !important;
+ width: var(--ls-sidebar-width, 280px) !important;
+ min-width: var(--ls-sidebar-width, 280px) !important;
+ max-width: var(--ls-sidebar-width, 280px) !important;
}
html.dark #sidebar-content {
@@ -4233,3 +4237,286 @@ html.dark:has(#wallet-demo-container) #navbar button[class*="h-14"][class*="text
color: var(--ls-gray-700);
}
+/* ===========================================
+ Collapsible sidebar (desktop)
+ A full-height "rail" (injected by sidebar-toggle.js) sits on the sidebar's
+ right edge; hovering it reveals a centered panel-toggle button. Clicking
+ toggles html.ls-nav-collapsed and the sidebar wipes closed with an
+ ease-out-snappy curve. State + the playground default are handled in JS;
+ first paint is set by the inline script in docs.json head.raw.
+ (The class is ls-nav-collapsed, not "sidebar-collapsed", so the broad
+ [class*="sidebar-collapse"] hide rule above doesn't match .)
+ =========================================== */
+
+:root {
+ --ls-sidebar-ease: cubic-bezier(0.19, 1, 0.22, 1); /* easeOutSnappy */
+ --ls-sidebar-dur: 260ms;
+ --ls-sidebar-width: 280px; /* resizable: drag the rail to change */
+ --ls-nav-rail-hit: 24px; /* expanded: invisible hover/click zone at the edge */
+ --ls-nav-rail-w: 48px; /* collapsed: visible left rail */
+}
+
+/* The inner nav is pinned to the expanded width and clipped (overflow-x) so it
+ wipes cleanly instead of squishing/reflowing. */
+#sidebar-content {
+ overflow-x: hidden !important;
+}
+
+/* Pin children to the expanded width so they clip (not squish/reflow) as the
+ column animates closed. Width is the cross axis here (#sidebar-content is a
+ flex COLUMN), so this must NOT touch `flex` — the nav keeps its flex-1 growth
+ and the footer row stays pinned to the bottom. */
+#sidebar-content > * {
+ width: var(--ls-sidebar-width, 280px) !important;
+ box-sizing: border-box !important;
+}
+
+/* Animate the wipe ONLY on a deliberate toggle (ls-nav-animating is set by the
+ click handler). Off by default, so navigation/first paint render the final
+ state instantly — a collapsed sidebar landing on a new page never fades in
+ from expanded, which is what flashed nav content inside the rail. */
+html.ls-nav-animating #sidebar-content {
+ transition: width var(--ls-sidebar-dur) var(--ls-sidebar-ease),
+ min-width var(--ls-sidebar-dur) var(--ls-sidebar-ease),
+ max-width var(--ls-sidebar-dur) var(--ls-sidebar-ease) !important;
+}
+
+html.ls-nav-animating #sidebar-content > * {
+ transition: opacity var(--ls-sidebar-dur) var(--ls-sidebar-ease);
+}
+
+/* During an active drag, kill transitions so width tracks the cursor 1:1. */
+html.ls-nav-dragging #sidebar-content,
+html.ls-nav-dragging #sidebar-content > *,
+html.ls-nav-dragging .ls-nav-rail {
+ transition: none !important;
+}
+
+/* While dragging, let pointer events pass through the playground iframe so the
+ drag keeps tracking when the cursor moves over it (otherwise the iframe
+ swallows mousemove/mouseup and the resize stalls on the demo page). */
+html.ls-nav-dragging #wallet-demo-host,
+html.ls-nav-dragging #wallet-demo-iframe {
+ pointer-events: none !important;
+}
+
+/* Collapse to the rail's width (not 0) so the rail reserves real layout space —
+ content/iframe starts after it instead of being overlaid. */
+html.ls-nav-collapsed #sidebar-content {
+ width: var(--ls-nav-rail-w) !important;
+ min-width: var(--ls-nav-rail-w) !important;
+ max-width: var(--ls-nav-rail-w) !important;
+ border-right-color: transparent !important;
+}
+
+/* Hide the (clipped) nav + footer in the collapsed slot so only the rail shows. */
+html.ls-nav-collapsed #sidebar-content > * {
+ opacity: 0 !important;
+ pointer-events: none !important;
+}
+
+/* EXPANDED: an invisible hover/click zone straddling the sidebar's right edge —
+ no visible rail here, just the hover-revealed button (below). */
+.ls-nav-rail {
+ position: fixed;
+ top: 112px; /* below navbar: 64px bar + 48px tabs */
+ bottom: 0;
+ left: calc(var(--ls-sidebar-width, 280px) - (var(--ls-nav-rail-hit) / 2));
+ width: var(--ls-nav-rail-hit);
+ z-index: 30;
+ margin: 0;
+ padding: 0;
+ border: none;
+ background: transparent;
+ cursor: col-resize; /* match the demo's ColumnResizeHandle */
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ -webkit-appearance: none;
+ appearance: none;
+}
+
+/* Like the column, the rail only animates its slide on a deliberate toggle
+ (ls-nav-animating). On navigation it snaps straight to the collapsed/expanded
+ position instead of sliding across — so landing on the playground from a page
+ with the sidebar open collapses instantly, with no stray animation. */
+html.ls-nav-animating .ls-nav-rail {
+ transition: left var(--ls-sidebar-dur) var(--ls-sidebar-ease),
+ width var(--ls-sidebar-dur) var(--ls-sidebar-ease),
+ background-color var(--ls-sidebar-dur) var(--ls-sidebar-ease);
+}
+
+/* COLLAPSED: a visible left rail, full height, flush at the screen edge. */
+html.ls-nav-collapsed .ls-nav-rail {
+ left: 0;
+ width: var(--ls-nav-rail-w);
+ background-color: var(--ls-gray-100);
+ border-right: 0.5px solid var(--ls-black-10);
+ cursor: pointer; /* collapsed: it's a button to reopen, not a resize handle */
+}
+
+html.dark.ls-nav-collapsed .ls-nav-rail {
+ background-color: var(--ls-gray-975);
+ border-right-color: var(--ls-white-06);
+}
+
+/* EXPANDED toggle: a styled button, hidden until you hover the edge. */
+.ls-nav-rail-btn {
+ display: grid;
+ place-items: center;
+ flex: none;
+ position: relative;
+ z-index: 1; /* keep the toggle above the edge highlight line */
+ width: 30px;
+ height: 30px;
+ border-radius: var(--ls-radius-md);
+ border: 0.5px solid var(--ls-black-10);
+ background-color: var(--ls-gray-100);
+ color: var(--ls-gray-500);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
+ opacity: 0;
+ transform: scale(0.92);
+ /* auto (not none) so the button itself is hoverable; clicks/drags still
+ bubble up to the rail, which owns the handlers. */
+ pointer-events: auto;
+ transition: opacity 150ms var(--ls-sidebar-ease),
+ transform 150ms var(--ls-sidebar-ease), background-color 150ms ease,
+ color 150ms ease;
+}
+
+html.dark .ls-nav-rail-btn {
+ border-color: var(--ls-white-06);
+ background-color: var(--ls-gray-975);
+ color: var(--ls-gray-600);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
+}
+
+/* Navigation snaps the button (no opacity fade) so its icon doesn't ghost in/out
+ between pages as the collapse state changes; sync() adds this for the state
+ change and drops it next frame, leaving the hover reveal still animated. */
+html.ls-nav-snap .ls-nav-rail-btn {
+ transition: none !important;
+}
+
+/* Edge hover: reveal the button in its resting state (colors stay at base). */
+html:not(.ls-nav-collapsed) .ls-nav-rail:hover .ls-nav-rail-btn {
+ opacity: 1;
+ transform: scale(1);
+}
+
+/* Button hover: the button's own active state. */
+html:not(.ls-nav-collapsed) .ls-nav-rail-btn:hover {
+ color: var(--ls-gray-950);
+ background-color: var(--ls-gray-050);
+}
+
+html.dark:not(.ls-nav-collapsed) .ls-nav-rail-btn:hover {
+ color: var(--ls-gray-050);
+ background-color: var(--ls-gray-950);
+}
+
+/* While the sidebar animates open from a click, suppress the hover reveal so a
+ cursor sitting over the rail mid-transition doesn't flash the button or edge
+ highlight — they reveal together on a genuine hover once it settles. */
+html.ls-nav-animating:not(.ls-nav-collapsed) .ls-nav-rail-btn,
+html.ls-nav-animating:not(.ls-nav-collapsed) .ls-nav-rail::after {
+ opacity: 0 !important;
+}
+
+/* COLLAPSED toggle: just the icon — no fill/border/shadow — always visible,
+ centered in the rail. */
+html.ls-nav-collapsed .ls-nav-rail-btn {
+ opacity: 1;
+ transform: none;
+ border: none;
+ background: transparent;
+ box-shadow: none;
+ color: var(--ls-gray-500);
+}
+
+html.dark.ls-nav-collapsed .ls-nav-rail-btn {
+ color: var(--ls-gray-600);
+}
+
+html.ls-nav-collapsed .ls-nav-rail:hover .ls-nav-rail-btn {
+ color: var(--ls-gray-950);
+}
+
+html.dark.ls-nav-collapsed .ls-nav-rail:hover .ls-nav-rail-btn {
+ color: var(--ls-gray-050);
+}
+
+.ls-nav-rail-btn::before {
+ content: "";
+ width: 20px;
+ height: 20px;
+ background-color: currentColor;
+ -webkit-mask: url("/images/icons/IconSidebarLeftArrow.svg") center / 20px 20px
+ no-repeat;
+ mask: url("/images/icons/IconSidebarLeftArrow.svg") center / 20px 20px
+ no-repeat;
+}
+
+html.ls-nav-collapsed .ls-nav-rail-btn::before {
+ -webkit-mask-image: url("/images/icons/IconSidebarRightArrow.svg");
+ mask-image: url("/images/icons/IconSidebarRightArrow.svg");
+}
+
+/* Hover/drag edge highlight: a 2px line at the divider, drawn as an overlay so
+ it never shifts layout (the real border stays 0.5px). Expanded only. */
+.ls-nav-rail::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 50%;
+ width: 2px;
+ transform: translateX(-50%);
+ background-color: var(--ls-gray-300);
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 150ms var(--ls-sidebar-ease);
+}
+
+html.dark .ls-nav-rail::after {
+ background-color: var(--ls-gray-700);
+}
+
+/* Delay the hover reveal so a brief hover — e.g. the cursor sitting over the
+ rail the instant it slides back out on expand — doesn't flash the highlight. */
+.ls-nav-rail:hover::after,
+html.ls-nav-dragging .ls-nav-rail::after {
+ opacity: 1;
+}
+
+html.ls-nav-collapsed .ls-nav-rail::after {
+ display: none;
+}
+
+/* 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 {
+ display: none !important;
+}
+
+/* Desktop only — mobile uses the hamburger nav (no #sidebar-content). */
+@media (max-width: 1023px) {
+ .ls-nav-rail {
+ display: none !important;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ /* Match the gated selectors' specificity (html.ls-nav-animating …) so these
+ actually win and the toggle is instant for reduced-motion users. */
+ html.ls-nav-animating #sidebar-content,
+ html.ls-nav-animating #sidebar-content > *,
+ html.ls-nav-animating .ls-nav-rail,
+ .ls-nav-rail-btn,
+ .ls-nav-rail::after {
+ transition: none !important;
+ }
+}
+