diff --git a/assets/js/resizable-panels.js b/assets/js/resizable-panels.js
new file mode 100644
index 00000000000..415294ffb72
--- /dev/null
+++ b/assets/js/resizable-panels.js
@@ -0,0 +1,331 @@
+/**
+ * Resizable docs panels.
+ *
+ * Lets readers adjust the left navigation and right TOC widths, persists those
+ * preferences in localStorage, and provides a reset control.
+ */
+(function () {
+ 'use strict';
+
+ const STORAGE_KEY = 'layer5-docs-panel-widths';
+ const RESIZABLE_QUERY = '(min-width: 768px)';
+ const STEP = 1;
+ const DEFAULT_WIDTHS = {
+ sidebar: 16.6667,
+ toc: 16.6667,
+ };
+ const LIMITS = {
+ sidebar: { min: 12, max: 32 },
+ toc: { min: 10, max: 28 },
+ main: { min: 42 },
+ };
+ const LEGACY_GRID_COLUMNS = 12;
+
+ function setupResizablePanels(row) {
+ const sidebar = row.querySelector('.td-sidebar');
+ const main = row.querySelector('main[role="main"]');
+ const toc = row.querySelector('.td-sidebar-toc');
+ const mediaQuery = window.matchMedia(RESIZABLE_QUERY);
+ let activeHandle = null;
+ let sidebarHandle = null;
+ let tocHandle = null;
+ let startX = 0;
+ let startWidths = null;
+ let widths = getStoredWidths();
+
+ if (!sidebar || !main) {
+ return;
+ }
+
+ row.classList.add('resizable-panels-ready');
+ applyWidths(widths);
+ createHandles();
+ createResetButton();
+ bindEvents();
+
+ function bindEvents() {
+ document.addEventListener('pointermove', onPointerMove);
+ document.addEventListener('pointerup', stopResize);
+ document.addEventListener('pointercancel', stopResize);
+
+ const onBreakpointChange = () => {
+ if (mediaQuery.matches) {
+ applyWidths(widths);
+ }
+ };
+
+ if (mediaQuery.addEventListener) {
+ mediaQuery.addEventListener('change', onBreakpointChange);
+ } else {
+ mediaQuery.addListener(onBreakpointChange);
+ }
+ }
+
+ function createHandles() {
+ sidebarHandle = createHandle('sidebar', 'Resize navigation sidebar');
+ sidebar.appendChild(sidebarHandle);
+
+ if (toc) {
+ tocHandle = createHandle('toc', 'Resize table of contents');
+ toc.appendChild(tocHandle);
+ }
+ }
+
+ function createHandle(target, label) {
+ const handle = document.createElement('div');
+ handle.className = `resizable-panel-handle resizable-panel-handle--${target}`;
+ handle.dataset.resizeTarget = target;
+ handle.tabIndex = 0;
+ handle.setAttribute('aria-label', label);
+ handle.setAttribute('aria-orientation', 'vertical');
+ handle.setAttribute('role', 'separator');
+ handle.title = label;
+
+ handle.addEventListener('pointerdown', (event) => {
+ startResize(event, handle);
+ });
+ handle.addEventListener('keydown', (event) => {
+ onHandleKeydown(event, target);
+ });
+
+ return handle;
+ }
+
+ function createResetButton() {
+ const resetButton = document.createElement('button');
+ resetButton.type = 'button';
+ resetButton.id = 'reset-panel-widths';
+ resetButton.className = 'resizable-panel-reset';
+ resetButton.innerHTML =
+ 'Reset layout';
+ resetButton.title = 'Reset panel widths to default';
+ resetButton.addEventListener('click', reset);
+
+ sidebar.appendChild(resetButton);
+ }
+
+ function startResize(event, handle) {
+ if (!mediaQuery.matches) {
+ return;
+ }
+
+ event.preventDefault();
+ activeHandle = handle;
+ startX = event.clientX;
+ startWidths = { ...widths };
+ handle.classList.add('resizable-panel-handle--active');
+ handle.setPointerCapture(event.pointerId);
+ document.body.classList.add('resizable-panels-dragging');
+ }
+
+ function onPointerMove(event) {
+ if (!activeHandle || !startWidths) {
+ return;
+ }
+
+ const rowWidth = row.getBoundingClientRect().width;
+ if (!rowWidth) {
+ return;
+ }
+
+ const target = activeHandle.dataset.resizeTarget;
+ const delta = ((event.clientX - startX) / rowWidth) * 100;
+ const nextWidths = { ...startWidths };
+
+ if (target === 'sidebar') {
+ nextWidths.sidebar = startWidths.sidebar + delta;
+ }
+
+ if (target === 'toc') {
+ nextWidths.toc = startWidths.toc - delta;
+ }
+
+ widths = normalizeWidths(nextWidths);
+ applyWidths(widths);
+ }
+
+ function stopResize() {
+ if (!activeHandle) {
+ return;
+ }
+
+ activeHandle.classList.remove('resizable-panel-handle--active');
+ activeHandle = null;
+ startWidths = null;
+ document.body.classList.remove('resizable-panels-dragging');
+ saveWidths();
+ }
+
+ function onHandleKeydown(event, target) {
+ if (!mediaQuery.matches) {
+ return;
+ }
+
+ const keys = ['ArrowLeft', 'ArrowRight', 'Home', 'End'];
+ if (!keys.includes(event.key)) {
+ return;
+ }
+
+ event.preventDefault();
+ const nextWidths = { ...widths };
+ const direction = event.key === 'ArrowRight' ? 1 : -1;
+
+ if (event.key === 'Home') {
+ nextWidths[target] = LIMITS[target].min;
+ } else if (event.key === 'End') {
+ nextWidths[target] = LIMITS[target].max;
+ } else if (target === 'toc') {
+ nextWidths.toc -= direction * STEP;
+ } else {
+ nextWidths.sidebar += direction * STEP;
+ }
+
+ widths = normalizeWidths(nextWidths);
+ applyWidths(widths);
+ saveWidths();
+ }
+
+ function applyWidths(nextWidths) {
+ const normalized = normalizeWidths(nextWidths);
+ const mainWidth = 100 - normalized.sidebar - normalized.toc;
+
+ row.style.setProperty('--docs-sidebar-width', `${normalized.sidebar}%`);
+ row.style.setProperty('--docs-toc-width', `${normalized.toc}%`);
+ row.style.setProperty('--docs-main-width', `${mainWidth}%`);
+ row.style.setProperty(
+ '--docs-main-without-toc-width',
+ `${100 - normalized.sidebar}%`,
+ );
+ updateHandleValues(normalized);
+ }
+
+ function updateHandleValues(nextWidths) {
+ if (sidebarHandle) {
+ sidebarHandle.setAttribute('aria-valuemin', LIMITS.sidebar.min);
+ sidebarHandle.setAttribute('aria-valuemax', LIMITS.sidebar.max);
+ sidebarHandle.setAttribute(
+ 'aria-valuenow',
+ Math.round(nextWidths.sidebar),
+ );
+ }
+
+ if (tocHandle) {
+ tocHandle.setAttribute('aria-valuemin', LIMITS.toc.min);
+ tocHandle.setAttribute('aria-valuemax', LIMITS.toc.max);
+ tocHandle.setAttribute('aria-valuenow', Math.round(nextWidths.toc));
+ }
+ }
+
+ function reset() {
+ widths = { ...DEFAULT_WIDTHS };
+ applyWidths(widths);
+ localStorage.removeItem(STORAGE_KEY);
+ }
+
+ function getStoredWidths() {
+ try {
+ const saved = JSON.parse(localStorage.getItem(STORAGE_KEY));
+
+ if (isLegacyColumnWidths(saved)) {
+ return normalizeWidths({
+ sidebar: saved.sidebar
+ ? (saved.sidebar / LEGACY_GRID_COLUMNS) * 100
+ : DEFAULT_WIDTHS.sidebar,
+ toc: saved.toc
+ ? (saved.toc / LEGACY_GRID_COLUMNS) * 100
+ : DEFAULT_WIDTHS.toc,
+ });
+ }
+
+ return normalizeWidths(saved || DEFAULT_WIDTHS);
+ } catch (error) {
+ return { ...DEFAULT_WIDTHS };
+ }
+ }
+
+ function isLegacyColumnWidths(saved) {
+ if (!saved) {
+ return false;
+ }
+
+ return ['sidebar', 'toc', 'main'].some((key) => {
+ const value = Number(saved[key]);
+ const limit = LIMITS[key];
+
+ if (!Number.isFinite(value) || !limit) {
+ return false;
+ }
+
+ return value < limit.min || (limit.max && value > limit.max);
+ });
+ }
+
+ function saveWidths() {
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(widths));
+ } catch (error) {
+ // Ignore storage failures so resizing still works in private modes.
+ }
+ }
+
+ function normalizeWidths(nextWidths) {
+ const next = {
+ sidebar: clamp(
+ Number(nextWidths && nextWidths.sidebar),
+ LIMITS.sidebar.min,
+ LIMITS.sidebar.max,
+ DEFAULT_WIDTHS.sidebar,
+ ),
+ toc: toc
+ ? clamp(
+ Number(nextWidths && nextWidths.toc),
+ LIMITS.toc.min,
+ LIMITS.toc.max,
+ DEFAULT_WIDTHS.toc,
+ )
+ : 0,
+ };
+
+ const availableForPanels = 100 - LIMITS.main.min;
+ const panelTotal = next.sidebar + next.toc;
+
+ if (panelTotal > availableForPanels) {
+ const overflow = panelTotal - availableForPanels;
+
+ if (next.sidebar >= next.toc) {
+ next.sidebar = Math.max(LIMITS.sidebar.min, next.sidebar - overflow);
+ } else {
+ next.toc = Math.max(LIMITS.toc.min, next.toc - overflow);
+ }
+ }
+
+ return {
+ sidebar: Number(next.sidebar.toFixed(4)),
+ toc: Number(next.toc.toFixed(4)),
+ };
+ }
+
+ function clamp(value, min, max, fallback) {
+ if (!Number.isFinite(value)) {
+ return fallback;
+ }
+
+ return Math.min(max, Math.max(min, value));
+ }
+ }
+
+ function initResizablePanels() {
+ document.querySelectorAll('.row.flex-xl-nowrap').forEach((row) => {
+ if (!row.dataset.resizablePanelsInitialized) {
+ row.dataset.resizablePanelsInitialized = 'true';
+ setupResizablePanels(row);
+ }
+ });
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initResizablePanels);
+ } else {
+ initResizablePanels();
+ }
+})();
diff --git a/assets/scss/_resizable-panels.scss b/assets/scss/_resizable-panels.scss
new file mode 100644
index 00000000000..9c8044cf161
--- /dev/null
+++ b/assets/scss/_resizable-panels.scss
@@ -0,0 +1,271 @@
+/**
+ * Resizable docs side panels.
+ *
+ * Docs widths are driven by CSS variables set from resizable-panels.js.
+ * Mobile keeps the existing Bootstrap column behavior.
+ */
+
+.resizable-panel-handle,
+.resizable-panel-reset {
+ display: none;
+}
+
+@media (min-width: 768px) {
+ .row.flex-xl-nowrap.resizable-panels-ready {
+ --docs-sidebar-width: 16.6667%;
+ --docs-main-width: 66.6666%;
+ --docs-main-without-toc-width: 83.3333%;
+ --docs-toc-width: 16.6667%;
+
+ display: flex;
+ flex-wrap: nowrap;
+ width: 100%;
+ min-width: 0;
+ }
+
+ /**
+ * LEFT SIDEBAR
+ */
+ .row.flex-xl-nowrap.resizable-panels-ready > .td-sidebar {
+ flex: 0 0 var(--docs-sidebar-width);
+ width: var(--docs-sidebar-width);
+ max-width: 420px;
+ min-width: 240px;
+ position: relative;
+ overflow-x: hidden;
+ padding-right: 12px;
+ order: 1;
+ }
+
+ /**
+ * MAIN CONTENT
+ */
+ .row.flex-xl-nowrap.resizable-panels-ready > main[role='main'] {
+ flex: 1 1 auto;
+ flex-basis: var(--docs-main-without-toc-width);
+ max-width: var(--docs-main-without-toc-width);
+ min-width: 500px;
+ overflow-x: auto;
+ position: relative;
+ order: 2;
+ }
+
+ /**
+ * Better readable content width
+ */
+ main[role='main'] article {
+ max-width: 900px;
+ }
+
+ /**
+ * TOC SIDEBAR
+ */
+ .row.flex-xl-nowrap.resizable-panels-ready > .td-sidebar-toc {
+ width: var(--docs-toc-width);
+ max-width: 320px;
+ min-width: 220px;
+ position: relative;
+ overflow-x: hidden;
+ padding-left: 12px;
+ order: 3;
+ }
+
+ /**
+ * EXISTING SIDEBAR STYLING
+ */
+ .td-sidebar,
+ .td-sidebar-toc {
+ position: sticky;
+ scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
+ }
+
+ /**
+ * Prevent text clipping
+ */
+ .td-sidebar nav,
+ .td-sidebar-toc nav {
+ overflow-wrap: break-word;
+ word-break: break-word;
+ }
+
+ .td-sidebar a,
+ .td-sidebar-toc a {
+ display: block;
+ max-width: 100%;
+ }
+
+ /**
+ * RESIZE HANDLE
+ */
+ .resizable-panel-handle {
+ appearance: none;
+ background: transparent;
+ border: 0;
+ cursor: col-resize;
+ display: block;
+ padding: 0;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 4px;
+ z-index: 20;
+ }
+
+ /**
+ * Subtle divider line
+ */
+ .resizable-panel-handle::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 1px;
+ background: rgba(255, 255, 255, 0.08);
+ }
+
+ /**
+ * Handle visual indicator
+ */
+ .resizable-panel-handle::after {
+ content: '';
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 2px;
+ height: 32px;
+ border-radius: 999px;
+ background-color: rgba(0, 211, 169, 0.55);
+ opacity: 0;
+ transition: opacity 0.16s ease;
+ }
+
+ /**
+ * SHOW HANDLE ON INTERACTION
+ */
+ .resizable-panel-handle:hover::after,
+ .resizable-panel-handle:focus-visible::after,
+ .resizable-panel-handle--active::after {
+ opacity: 1;
+ }
+
+ /**
+ * LEFT SIDEBAR HANDLE
+ */
+ .resizable-panel-handle--sidebar {
+ right: 0;
+ }
+
+ .resizable-panel-handle--sidebar::before,
+ .resizable-panel-handle--sidebar::after {
+ right: 1px;
+ }
+
+ /**
+ * TOC HANDLE
+ */
+ .resizable-panel-handle--toc {
+ display: none;
+ left: 0;
+ }
+
+ .resizable-panel-handle--toc::before,
+ .resizable-panel-handle--toc::after {
+ left: 1px;
+ }
+
+ /**
+ * CUSTOM SCROLLBARS
+ */
+ .row.flex-xl-nowrap.resizable-panels-ready
+ > .td-sidebar::-webkit-scrollbar,
+ .row.flex-xl-nowrap.resizable-panels-ready
+ > .td-sidebar-toc::-webkit-scrollbar {
+ width: 8px;
+ }
+
+ .row.flex-xl-nowrap.resizable-panels-ready
+ > .td-sidebar::-webkit-scrollbar-thumb,
+ .row.flex-xl-nowrap.resizable-panels-ready
+ > .td-sidebar-toc::-webkit-scrollbar-thumb {
+ background: rgba(255, 255, 255, 0.3) !important;
+ border-radius: 999px;
+ }
+
+ .row.flex-xl-nowrap.resizable-panels-ready
+ > .td-sidebar::-webkit-scrollbar-thumb:hover,
+ .row.flex-xl-nowrap.resizable-panels-ready
+ > .td-sidebar-toc::-webkit-scrollbar-thumb:hover {
+ background: rgba(255, 255, 255, 0.45) !important;
+ }
+
+ /**
+ * RESET BUTTON
+ */
+ .resizable-panel-reset {
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ background: rgba(0, 0, 0, 0.25);
+ border: 1px solid rgba(0, 211, 169, 0.45);
+ border-radius: 4px;
+ color: #ccc;
+ cursor: pointer;
+ display: inline-flex;
+ font-size: 0.8125rem;
+ gap: 0.35rem;
+ line-height: 1.2;
+ margin: 1rem 0 0;
+ padding: 0.45rem 0.65rem;
+ white-space: nowrap;
+ }
+
+ .resizable-panel-reset:hover,
+ .resizable-panel-reset:focus-visible {
+ background: #00d3a9;
+ border-color: #00d3a9;
+ color: #000;
+ outline: none;
+ }
+
+ /**
+ * DRAGGING STATE
+ */
+ .resizable-panels-dragging {
+ cursor: col-resize;
+ user-select: none;
+ }
+
+ .resizable-panels-dragging iframe,
+ .resizable-panels-dragging img,
+ .resizable-panels-dragging a {
+ pointer-events: none;
+ }
+}
+
+/**
+ * LARGE SCREENS
+ */
+@media (min-width: 1200px) {
+ .row.flex-xl-nowrap.resizable-panels-ready > main[role='main'] {
+ flex-basis: var(--docs-main-width);
+ max-width: var(--docs-main-width);
+ }
+
+ .row.flex-xl-nowrap.resizable-panels-ready > .td-sidebar-toc {
+ flex: 0 0 var(--docs-toc-width);
+ }
+
+ .resizable-panel-handle--toc {
+ display: block;
+ }
+}
+
+/**
+ * PRINT MODE
+ */
+@media print {
+ .resizable-panel-handle,
+ .resizable-panel-reset {
+ display: none !important;
+ }
+}
\ No newline at end of file
diff --git a/assets/scss/_styles_project.scss b/assets/scss/_styles_project.scss
index 00ca1b8770b..44038655971 100644
--- a/assets/scss/_styles_project.scss
+++ b/assets/scss/_styles_project.scss
@@ -15,6 +15,7 @@
@import "elements_project";
@import "summary.scss";
@import "_kanvas-corner-popup.scss";
+@import "_resizable-panels.scss";
.navbar-dark {
min-height: 5rem;
@@ -272,6 +273,7 @@ a:not([href]):not([class]):hover {
color: $gray-400;
font-weight: $font-weight-bold;
}
+ margin-top: 0.5rem;
}
.td-sidebar-link {
diff --git a/layouts/partials/scripts.html b/layouts/partials/scripts.html
new file mode 100644
index 00000000000..09d5183fe7f
--- /dev/null
+++ b/layouts/partials/scripts.html
@@ -0,0 +1,13 @@
+
+{{ $scripts := slice
+ "js/resizable-panels.js"
+ "js/kanvas-architectural-transition.js"
+ "js/kanvas-corner-popup.js"
+ "js/offline-search.js"
+-}}
+
+{{ range $scripts -}}
+ {{ with resources.Get . -}}
+
+ {{ end -}}
+{{ end -}}