diff --git a/layouts/partials/custom_js.html b/layouts/partials/custom_js.html
new file mode 100644
index 0000000000..6483386262
--- /dev/null
+++ b/layouts/partials/custom_js.html
@@ -0,0 +1,6 @@
+{{/* Custom JavaScript for FORRT site */}}
+
+{{/* Include clusters search functionality on clusters page */}}
+{{ if eq .RelPermalink "/clusters/" }}
+
+{{ end }}
diff --git a/static/js/clusters-search.js b/static/js/clusters-search.js
new file mode 100644
index 0000000000..12ea57b95a
--- /dev/null
+++ b/static/js/clusters-search.js
@@ -0,0 +1,582 @@
+/**
+ * Clusters Page Search Functionality
+ * Enables searching within Bootstrap tab content that would otherwise be hidden from Ctrl-F
+ */
+(function() {
+ 'use strict';
+
+ // Wait for DOM to be ready
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+
+ function init() {
+ // Only run on clusters page
+ if (!window.location.pathname.includes('/clusters')) {
+ return;
+ }
+
+ createSearchInterface();
+ setupSearchHandlers();
+ }
+
+ function createSearchInterface() {
+ // Create sidebar container
+ const sidebar = document.createElement('div');
+ sidebar.id = 'clusterSearchSidebar';
+ sidebar.className = 'cluster-search-sidebar';
+ sidebar.innerHTML = `
+
+
+
+
+
+
+
+
+
+ `;
+
+ // Insert at beginning of body
+ document.body.insertBefore(sidebar, document.body.firstChild);
+ }
+
+ function setupSearchHandlers() {
+ const searchInput = document.getElementById('clusterSearchInput');
+ const searchBtn = document.getElementById('clusterSearchBtn');
+ const clearBtn = document.getElementById('clusterClearBtn');
+ const resultsDiv = document.getElementById('clusterSearchResults');
+ const toggleBtn = document.getElementById('clusterSearchToggle');
+ const closeBtn = document.getElementById('clusterSearchClose');
+ const panel = document.getElementById('clusterSearchPanel');
+
+ if (!searchInput || !searchBtn || !clearBtn || !toggleBtn || !closeBtn || !panel) return;
+
+ // Toggle sidebar
+ toggleBtn.addEventListener('click', function() {
+ panel.classList.toggle('open');
+ if (panel.classList.contains('open')) {
+ searchInput.focus();
+ }
+ });
+
+ // Close sidebar
+ closeBtn.addEventListener('click', function() {
+ panel.classList.remove('open');
+ });
+
+ // Close on escape key
+ document.addEventListener('keydown', function(e) {
+ if (e.key === 'Escape' && panel.classList.contains('open')) {
+ panel.classList.remove('open');
+ }
+ });
+
+ // Search on button click
+ searchBtn.addEventListener('click', performSearch);
+
+ // Search on Enter key
+ searchInput.addEventListener('keypress', function(e) {
+ if (e.key === 'Enter') {
+ performSearch();
+ }
+ });
+
+ // Live search as user types (debounced)
+ var debounceTimer;
+ searchInput.addEventListener('input', function() {
+ clearTimeout(debounceTimer);
+ debounceTimer = setTimeout(performSearch, 300);
+ });
+
+ // Clear search
+ clearBtn.addEventListener('click', function() {
+ searchInput.value = '';
+ resultsDiv.innerHTML = '';
+ clearBtn.style.display = 'none';
+ removeAllHighlights();
+ collapseAllTabs();
+ });
+ }
+
+ function performSearch() {
+ const searchInput = document.getElementById('clusterSearchInput');
+ const clearBtn = document.getElementById('clusterClearBtn');
+ const resultsDiv = document.getElementById('clusterSearchResults');
+ const query = searchInput.value.trim();
+
+ if (!query || query.length < 2) {
+ resultsDiv.innerHTML = query.length > 0 ? 'Please enter at least 2 characters to search.
' : '';
+ clearBtn.style.display = 'none';
+ removeAllHighlights();
+ return;
+ }
+
+ // Remove previous highlights
+ removeAllHighlights();
+
+ // Search through all tab content
+ const results = searchAllTabs(query);
+
+ // Display results
+ displayResults(results, query);
+
+ // Show clear button
+ clearBtn.style.display = 'inline-block';
+ }
+
+ function searchAllTabs(query) {
+ const results = [];
+ const queryLower = query.toLowerCase();
+
+ // Find all cluster sections
+ const clusterSections = document.querySelectorAll('section[id^="cluster"]');
+
+ clusterSections.forEach(function(section) {
+ // Use h1 to get the actual cluster title (h3 is "Description" in the content)
+ const clusterTitle = section.querySelector('h1, .home-section-title');
+ const clusterName = clusterTitle ? clusterTitle.textContent.trim() : 'Unknown Cluster';
+
+ // Find all tab panes in this cluster
+ const tabPanes = section.querySelectorAll('.tab-pane');
+
+ tabPanes.forEach(function(tabPane) {
+ const tabId = tabPane.id;
+
+ // Get tab label
+ const tabLink = section.querySelector('a[href="#' + tabId + '"]');
+ const tabLabel = tabLink ? tabLink.textContent.trim() : tabId;
+ const tabLabelLower = tabLabel.toLowerCase();
+
+ // If the tab label itself matches, add a result for it
+ if (tabLabelLower.includes(queryLower)) {
+ var regex = new RegExp('(' + escapeRegExp(query) + ')', 'gi');
+ results.push({
+ cluster: clusterName,
+ tab: tabLabel,
+ tabId: tabId,
+ snippet: 'Tab: ' + tabLabel.replace(regex, '$1'),
+ section: section,
+ tabPane: tabPane,
+ tabLink: tabLink,
+ element: tabPane
+ });
+ }
+
+ // Search each block-level element individually for per-paragraph results
+ // Filter out nested blocks (e.g. inside
) to avoid duplicates
+ var allBlocks = tabPane.querySelectorAll('li, p, h2, h3, h4, h5, h6, blockquote');
+ var blocks = Array.from(allBlocks).filter(function(block) {
+ var parent = block.parentElement;
+ while (parent && parent !== tabPane) {
+ if (parent.matches('li, p, blockquote')) return false;
+ parent = parent.parentElement;
+ }
+ return true;
+ });
+
+ blocks.forEach(function(block) {
+ var text = block.textContent || '';
+ if (text.toLowerCase().includes(queryLower)) {
+ var snippetText = text.trim();
+ if (snippetText.length > 200) {
+ snippetText = snippetText.substring(0, 200) + '...';
+ }
+ var highlightRegex = new RegExp('(' + escapeRegExp(query) + ')', 'gi');
+ var snippet = escapeHtml(snippetText).replace(highlightRegex, '$1');
+
+ results.push({
+ cluster: clusterName,
+ tab: tabLabel,
+ tabId: tabId,
+ snippet: snippet,
+ section: section,
+ tabPane: tabPane,
+ tabLink: tabLink,
+ element: block
+ });
+ }
+ });
+ });
+ });
+
+ return results;
+ }
+
+ function escapeRegExp(string) {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ }
+
+ function displayResults(results, query) {
+ const resultsDiv = document.getElementById('clusterSearchResults');
+
+ if (results.length === 0) {
+ resultsDiv.innerHTML = 'No results found for "' + escapeHtml(query) + '".
';
+ return;
+ }
+
+ var html = 'Found ' + results.length + ' result(s)
';
+ html += '';
+
+ results.forEach(function(result, index) {
+ html += '
' +
+ '' +
+ '
' + escapeHtml(result.cluster) + '
' +
+ '
' + result.snippet + '
' +
+ '
';
+ });
+
+ html += '
';
+ resultsDiv.innerHTML = html;
+
+ // Add click handlers to results
+ var resultItems = resultsDiv.querySelectorAll('.search-result-item');
+ resultItems.forEach(function(item) {
+ item.addEventListener('click', function() {
+ var index = parseInt(this.getAttribute('data-result-index'));
+ var result = results[index];
+ if (!result) return;
+
+ removeAllHighlights();
+ activateTab(result);
+ highlightMatches(result.tabPane, query);
+
+ // Scroll to the specific element (paragraph/reference)
+ setTimeout(function() {
+ scrollToElement(result.element);
+ }, 100);
+
+ // Mark as active
+ resultItems.forEach(function(r) { r.classList.remove('active'); });
+ this.classList.add('active');
+ // Auto-hide the search panel
+ document.getElementById('clusterSearchPanel').classList.remove('open');
+ });
+ });
+
+ // Auto-expand first result
+ if (results.length > 0) {
+ resultItems[0].classList.add('active');
+ activateTab(results[0]);
+ highlightMatches(results[0].tabPane, query);
+ }
+ }
+
+ function escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+ }
+
+ function activateTab(result) {
+ // Collapse all tabs first
+ collapseAllTabs();
+
+ // Activate the target tab
+ if (result.tabLink) {
+ result.tabLink.click();
+ }
+ }
+
+ function collapseAllTabs() {
+ document.querySelectorAll('.tab-pane.show.active').forEach(function(pane) {
+ pane.classList.remove('show', 'active');
+ });
+ document.querySelectorAll('.nav-link.active').forEach(function(link) {
+ link.classList.remove('active');
+ });
+ }
+
+ function scrollToElement(element) {
+ // Get navbar height to offset the scroll
+ const navbar = document.querySelector('.navbar-fixed-top, .fixed-top, nav.navbar');
+ const navbarHeight = navbar ? navbar.offsetHeight : 70;
+
+ // Calculate the position to scroll to
+ const elementPosition = element.getBoundingClientRect().top + window.pageYOffset;
+ const offsetPosition = elementPosition - navbarHeight - 20; // Extra 20px padding
+
+ // Scroll to the calculated position
+ window.scrollTo({
+ top: offsetPosition,
+ behavior: 'smooth'
+ });
+ }
+
+ function highlightMatches(tabPane, query) {
+ if (!tabPane) return;
+
+ // Remove existing highlights first
+ removeHighlightsInElement(tabPane);
+
+ // Use mark.js-like approach to highlight matches
+ const regex = new RegExp(`(${escapeRegExp(query)})`, 'gi');
+
+ // Get all text nodes
+ const walker = document.createTreeWalker(
+ tabPane,
+ NodeFilter.SHOW_TEXT,
+ null,
+ false
+ );
+
+ const textNodes = [];
+ let node;
+ while ((node = walker.nextNode())) {
+ // Skip if parent is already a mark
+ if (node.parentElement.tagName !== 'MARK') {
+ textNodes.push(node);
+ }
+ }
+
+ textNodes.forEach(function(textNode) {
+ const text = textNode.textContent;
+ const testRegex = new RegExp(`(${escapeRegExp(query)})`, 'gi');
+ if (testRegex.test(text)) {
+ const fragment = document.createDocumentFragment();
+ const tempDiv = document.createElement('div');
+ tempDiv.innerHTML = text.replace(regex, '$1');
+ while (tempDiv.firstChild) {
+ fragment.appendChild(tempDiv.firstChild);
+ }
+ textNode.parentNode.replaceChild(fragment, textNode);
+ }
+ });
+ }
+
+ function removeAllHighlights() {
+ document.querySelectorAll('.cluster-highlight').forEach(function(mark) {
+ const parent = mark.parentNode;
+ parent.replaceChild(document.createTextNode(mark.textContent), mark);
+ parent.normalize();
+ });
+ }
+
+ function removeHighlightsInElement(element) {
+ element.querySelectorAll('.cluster-highlight').forEach(function(mark) {
+ const parent = mark.parentNode;
+ parent.replaceChild(document.createTextNode(mark.textContent), mark);
+ parent.normalize();
+ });
+ }
+
+ // Add CSS for sidebar and highlighting
+ const style = document.createElement('style');
+ style.textContent = `
+ /* Highlight styling */
+ .cluster-highlight {
+ background-color: #ffeb3b;
+ padding: 2px 0;
+ font-weight: bold;
+ }
+
+ /* Sidebar toggle button */
+ .cluster-search-toggle {
+ position: fixed;
+ left: 0;
+ top: 200px;
+ background: #007bff;
+ color: white;
+ border: none;
+ border-radius: 0 5px 5px 0;
+ padding: 12px 15px;
+ cursor: pointer;
+ z-index: 1034;
+ box-shadow: 2px 2px 5px rgba(0,0,0,0.2);
+ font-size: 14px;
+ transition: background 0.3s;
+ }
+
+ .cluster-search-toggle:hover {
+ background: #0056b3;
+ }
+
+ .cluster-search-toggle i {
+ margin-right: 5px;
+ }
+
+ /* Sidebar panel */
+ .cluster-search-panel {
+ position: fixed;
+ left: -350px;
+ top: 70px;
+ width: 350px;
+ height: calc(100vh - 70px);
+ background: white;
+ box-shadow: 2px 0 10px rgba(0,0,0,0.1);
+ z-index: 1035;
+ transition: left 0.3s ease;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ }
+
+ .cluster-search-panel.open {
+ left: 0;
+ }
+
+ /* Sidebar header */
+ .cluster-search-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 15px 20px;
+ border-bottom: 1px solid #dee2e6;
+ background: #f8f9fa;
+ }
+
+ .cluster-search-header h4 {
+ margin: 0;
+ font-size: 18px;
+ color: #333;
+ }
+
+ .cluster-search-close {
+ background: none;
+ border: none;
+ font-size: 20px;
+ color: #666;
+ cursor: pointer;
+ padding: 5px;
+ line-height: 1;
+ }
+
+ .cluster-search-close:hover {
+ color: #333;
+ }
+
+ /* Sidebar body */
+ .cluster-search-body {
+ padding: 20px;
+ flex: 1;
+ overflow-y: auto;
+ }
+
+ /* Search results summary */
+ .search-summary {
+ background: #e7f3ff;
+ padding: 10px;
+ border-radius: 5px;
+ margin-bottom: 15px;
+ font-size: 0.9rem;
+ color: #004085;
+ }
+
+ /* Search results list */
+ .search-results-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ /* Individual search result */
+ .search-result-item {
+ background: #f8f9fa;
+ border: 1px solid #dee2e6;
+ border-radius: 5px;
+ padding: 12px;
+ cursor: pointer;
+ transition: all 0.2s;
+ }
+
+ .search-result-item:hover {
+ background: #e9ecef;
+ border-color: #007bff;
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
+ }
+
+ .search-result-item.active {
+ background: #e7f3ff;
+ border-color: #007bff;
+ box-shadow: inset 0 0 0 1px #007bff;
+ }
+
+ .search-result-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 5px;
+ }
+
+ .search-result-header strong {
+ font-size: 0.95rem;
+ color: #333;
+ flex: 1;
+ margin-right: 10px;
+ }
+
+ .search-result-header .badge {
+ font-size: 0.75rem;
+ }
+
+ .search-result-cluster {
+ font-size: 0.8rem;
+ color: #666;
+ margin-bottom: 8px;
+ }
+
+ .search-result-snippet {
+ font-size: 0.8rem;
+ color: #555;
+ line-height: 1.4;
+ }
+
+ .search-result-snippet mark {
+ background-color: #ffeb3b;
+ padding: 1px 2px;
+ font-weight: 600;
+ }
+
+ /* Mobile responsive */
+ @media (max-width: 768px) {
+ .cluster-search-panel {
+ width: 70%;
+ left: -70%;
+ }
+
+ .cluster-search-panel.open {
+ left: 0;
+ }
+
+ .cluster-search-toggle {
+ top: 170px;
+ font-size: 14px;
+ padding: 10px;
+ }
+
+ .cluster-search-toggle-text {
+ display: none;
+ }
+ }
+
+ /* Alert styling in sidebar */
+ .cluster-search-body .alert {
+ font-size: 0.85rem;
+ padding: 8px 12px;
+ }
+ `;
+ document.head.appendChild(style);
+
+})();