PageSourceSearch

https://help.blueshift.com/hc/theming_assets/872242/715757/script.js?digest=54311429016083

js blueshift.com collected 2026-09-24 18:10:31 UTC 118,243 bytes, 3,434 lines download raw bytes

1/* ==========================================================
2   Zendesk Theme Core JS
3   Last updated: 17 Oct 2025
4   Purpose: Core UI scripts + all original utilities + Recipes Explorers
5   ========================================================== */
6
7hljs.initHighlightingOnLoad();
8
9var HC_SETTINGS = {
10  css: {
11    activeClass: 'is-active',
12    hiddenClass: 'is-hidden'
13  }
14};
15
16$(function() {
17  // =========================================================
18  // 🔹 Global UI (menus, overlay, parallax, scroll-to-top)
19  // =========================================================
20  SearchResultsFilters.init();
21
22  var $window = $(window);
23  var $body = $('body');
24  var $topbar = $('[data-topbar]');
25  var $scrollToTop = $('[data-scroll-to-top]');
26  var $mainMenu = $('[data-menu]');
27  var $mobileSandwich = $('[data-toggle-menu]');
28  var $overlay = $('<div class="body-overlay"></div>');
29
30  var bodyEl = document.querySelector('body');
31  var shouldShowSideNav = !!document.querySelector('[data-show-sidenav]');
32  var sidenavBox = document.querySelector('[data-sidenav-box]');
33
34  if (shouldShowSideNav) {
35    bodyEl.classList.add('sidenav-layout');
36    sidenavBox.classList.remove(HC_SETTINGS.css.hiddenClass);
37  }
38
39  // Parallax for hero elements
40  var $heroElement = $('.hero__element');
41  var evenEl = $heroElement.filter(':even');
42  var oddEl = $heroElement.filter(':odd');
43  var VELOCITY = 5;
44  var wScrollTop;
45
46  $body.append($overlay[0]);
47
48  $scrollToTop.click(function() {
49    $('html, body').animate({ scrollTop: 0 }, 1000);
50    return false;
51  });
52
53  $mobileSandwich.click(function() {
54    $(this).add($mainMenu).add($topbar).toggleClass(HC_SETTINGS.css.activeClass);
55    $mainMenu.slideToggle(300);
56    $body.toggleClass('menu-opened');
57  });
58
59  $overlay.on('click', mobileMenuClose);
60
61  $window.on('resize', function() {
62    if ($window.width() > 991) {
63      mobileMenuClose(null, true);
64    }
65  });
66
67  function mobileMenuClose(e, desktop) {
68    if ($mobileSandwich.hasClass(HC_SETTINGS.css.activeClass)) {
69      $mobileSandwich.add($mainMenu).add($topbar).removeClass(HC_SETTINGS.css.activeClass);
70      $mainMenu.slideUp(desktop ? 0 : 300);
71      $body.removeClass('menu-opened');
72    }
73  }
74
75  // Scroll & parallax
76  $window.on('scroll', function() {
77    wScrollTop = $window.scrollTop();
78
79    (wScrollTop > 400)
80      ? $scrollToTop.addClass(HC_SETTINGS.css.activeClass)
81      : $scrollToTop.removeClass(HC_SETTINGS.css.activeClass);
82
83    if ($heroElement.length && $window.width() > 1024) {
84      evenEl.css('transform', 'translateY(' + (wScrollTop / VELOCITY) + 'px)');
85      oddEl.css('transform', 'translateY(' + (wScrollTop / (VELOCITY * -2)) + 'px)');
86    }
87  });
88
89  // =========================================================
90  // 🔹 Social share + dropdowns + search helpers
91  // =========================================================
92  $('.share a').click(function(e) {
93    e.preventDefault();
94    window.open(this.href, '', 'height=500,width=500');
95  });
96
97  $('.share-label').on('click', function(e) {
98    e.stopPropagation();
99    var isSelected = this.getAttribute('aria-selected') === 'true';
100    this.setAttribute('aria-selected', !isSelected);
101    $('.share-label').not(this).attr('aria-selected', 'false');
102  });
103
104  $(document).on('click', function() {
105    $('.share-label').attr('aria-selected', 'false');
106  });
107
108  $('.dropdown-toggle').on('click', function(e) { e.preventDefault(); });
109
110  // Submit search on select change / input enter
111  $('#request-status-select, #request-organization-select').on('change', search);
112  $('#quick-search').on('keypress', function(e) { if (e.which === 13) search(); });
113
114  function search() {
115    window.location.search = $.param({
116      query: $('#quick-search').val(),
117      status: $('#request-status-select').val(),
118      organization_id: $('#request-organization-select').val()
119    });
120  }
121
122  // =========================================================
123  // 🔹 Old Lightbox (Magnific Popup) — keep for legacy content
124  // =========================================================
125  $('.image-with-lightbox').magnificPopup({
126    type: 'image',
127    closeOnContentClick: true,
128    closeBtnInside: false,
129    fixedContentPos: true,
130    mainClass: 'mfp-with-zoom',
131    image: { verticalFit: true },
132    zoom: { enabled: true, duration: 300 }
133  });
134
135  $('.image-with-video-icon').magnificPopup({
136    disableOn: 700,
137    type: 'iframe',
138    mainClass: 'mfp-fade',
139    removalDelay: 160,
140    preloader: false,
141    fixedContentPos: false
142  });
143
144  // =========================================================
145  // 🔹 Accordion / Tabs / Icons
146  // =========================================================
147  $('.accordion__item-title').on('click', function() {
148    var $title = $(this);
149    $title.toggleClass('accordion__item-title--active');
150    $title.parents('.accordion__item').find('.accordion__item-content').slideToggle();
151  });
152
153  $('.tabs-link').click(function(e) {
154    e.preventDefault();
155    var $link = $(this);
156    var tabIndex = $link.index();
157    var $tab = $link.parents('.tabs').find('.tab').eq(tabIndex);
158    $link.addClass(HC_SETTINGS.css.activeClass).siblings().removeClass(HC_SETTINGS.css.activeClass);
159    $tab.removeClass(HC_SETTINGS.css.hiddenClass).siblings('.tab').addClass(HC_SETTINGS.css.hiddenClass);
160  });
161
162  // Fix animated icons
163  $('.fa-spin').empty();
164
165  // =========================================================
166  // 🔹 Asset part / underline / SVG replacement / Category tree
167  // =========================================================
168  var assetPart = $('[property]').attr('content'),
169      $assetElement = $('[data-asset]');
170
171  if (assetPart) {
172    assetPart = assetPart.slice(0, -5);
173    $assetElement.each(function() {
174      var $this = $(this), link = $this.data('asset');
175      $this.attr('src', assetPart + link).css('opacity', 1);
176    });
177  }
178
179  // Underline matching word
180  $('[data-underline]').each(function() {
181    var $text = $(this);
182    var word = $text.data('underline');
183    var html = $text.html();
184    $text.html(html.replace(new RegExp(word, 'g'), '<span class="underline">' + word + '</span>'));
185  });
186
187  // Replace SVG images (inline)
188  $('img.custom-block__image').each(function() {
189    var $img = $(this);
190    var imgID = $img.attr('id');
191    var imgClass = $img.attr('class');
192    var imgURL = $img.attr('src') + '?reset';
193
194    $.get(imgURL, function(data) {
195      var $svg = $(data).find('svg');
196      if (typeof imgID !== 'undefined') $svg = $svg.attr('id', imgID);
197      if (typeof imgClass !== 'undefined') $svg = $svg.attr('class', imgClass + ' replaced-svg');
198      $svg = $svg.removeAttr('xmlns:a');
199      $img.replaceWith($svg);
200    }, 'xml');
201  });
202
203  // Category tree
204  $('[data-tree]').each(function(i, element) {
205    var $item = $(element);
206    var $link = $item.find('[data-tree-link]');
207    var $box = $item.find('[data-tree-box]');
208    $link.on('click', function(e) {
209      e.preventDefault();
210      $link.add($item).add($box).toggleClass(HC_SETTINGS.css.activeClass);
211      $box.slideToggle(300);
212    });
213  });
214
215  // Add custom links to side navigation
216  $(window).on('sidenavRendered', function(e, sidenav) {
217    if (!window.LotusConfig || !Array.isArray(LotusConfig.sidenavLinks)) return;
218    LotusConfig.sidenavLinks.forEach((el)=>{
219      let linkList = '';
220      el.links.forEach((link)=> { linkList += link; });
221      $('<span class="sidenav__custom-links">' + linkList + '</span>')
222        .insertAfter('[data-sidenav-category-id="' + el.categoryId + '"] .sidenav__category-name');
223    });
224  });
225
226  // =========================================================
227  // 🔹 FAQ Expand/Collapse
228  // =========================================================
229  $('.faq-question').on('click', function () {
230    const $question = $(this);
231    const $item = $question.closest('.faq-item');
232    const $answer = $item.find('.faq-answer');
233    const $icon = $question.find('.faq-toggle-icon');
234    $answer.toggleClass('active');
235    $icon.text($answer.hasClass('active') ? '–' : '+');
236  });
237
238  // =========================================================
239  // 🔹 Generative AI access check (agent/admin reveal)
240  // =========================================================
241  console.log("Script is loaded ✅");
242  fetch('/hc/en-us/articles/41196780578707-Generative-AI-search-access-check-do-not-delete')
243    .then(response => {
244      console.log("Fetch response status:", response.status);
245      if (response.status === 200) {
246        console.log("Agent/admin confirmed ✅");
247        document.querySelector('.ai-disclaimer')?.classList.remove('hidden');
248        document.querySelector('.generative-answers-wrapper')?.classList.remove('hidden');
249      } else {
250        console.log("User is not agent/admin.");
251      }
252    })
253    .catch(error => {
254      console.error("Agent detection failed:", error);
255    });
256
257  // =========================================================
258  // 🔹 New Lightbox (pure JS) — for elements with [data-fullsize]
259  //     (kept alongside Magnific to avoid breaking legacy content)
260  // =========================================================
261  (function lightboxNew() {
262    const modal = document.createElement("div");
263    modal.className = "lightbox-modal";
264    modal.setAttribute("role", "dialog");
265    modal.setAttribute("aria-modal", "true");
266
267    const content = document.createElement("div");
268    content.className = "lightbox-content";
269
270    const closeBtn = document.createElement("button");
271    closeBtn.className = "lightbox-close";
272    closeBtn.type = "button";
273    closeBtn.setAttribute("aria-label", "Close");
274    closeBtn.textContent = "×";
275
276    const scrollWrap = document.createElement("div");
277    scrollWrap.className = "lightbox-scroll";
278
279    const img = document.createElement("img");
280    img.alt = "Full-size preview";
281
282    scrollWrap.appendChild(img);
283    content.appendChild(closeBtn);
284    content.appendChild(scrollWrap);
285    modal.appendChild(content);
286    document.body.appendChild(modal);
287
288    const open = (src) => {
289      const temp = new Image();
290      temp.crossOrigin = "use-credentials";
291      temp.onload = temp.onerror = () => {
292        img.src = src;
293        modal.classList.add("active");
294        document.body.classList.add("lb-lock");
295        closeBtn.focus();
296      };
297      temp.src = src;
298    };
299
300    const close = () => {
301      modal.classList.remove("active");
302      document.body.classList.remove("lb-lock");
303      img.removeAttribute("src");
304    };
305
306    document.addEventListener("click", (e) => {
307      const thumb = e.target.closest("[data-fullsize]");
308      if (thumb) {
309        e.preventDefault();
310        const full = thumb.getAttribute("data-fullsize") || thumb.src;
311        if (full) open(full);
312      } else if (e.target === modal) {
313        close();
314      }
315    });
316
317    closeBtn.addEventListener("click", close);
318    document.addEventListener("keydown", (e) => {
319      if (e.key === "Escape" && modal.classList.contains("active")) close();
320    });
321  })();
322
323 // =========================================================
324 // 🔹 Copy Button (Updated for "Pro" Icon-Only CSS)
325 // =========================================================
326 document.querySelectorAll("pre").forEach(pre => {
327   const codeBlock = pre.querySelector("code") || pre;
328   
329   // Prevent duplicates
330   if (pre.querySelector(".copy-btn")) return;
331
332   const button = document.createElement("button");
333   button.className = "copy-btn";
334   
335   // ACCESSIBILITY ONLY: We don't set .textContent because CSS hides it.
336   // We use aria-label so screen readers still know what it is.
337   button.setAttribute("aria-label", "Copy code");
338
339   button.addEventListener("click", () => {
340     navigator.clipboard.writeText(codeBlock.innerText).then(() => {
341       
342       // 1. ADD CLASS: This triggers the Green Checkmark Icon in CSS
343       button.classList.add("copied");
344       button.setAttribute("aria-label", "Copied!");
345
346       // 2. REMOVE CLASS: After 2s, reverts to Gray Clipboard Icon
347       setTimeout(() => {
348         button.classList.remove("copied");
349         button.setAttribute("aria-label", "Copy code");
350       }, 2000);
351     });
352   });
353
354   // Ensure the button stays in the top-right corner
355   pre.style.position = "relative";
356   pre.prepend(button);
357
358 });
359
360
361// =========================================================
362// Recipe Studio Explorer V2
363// Last Updated: 7 Apr 2026
364// 
365// Updates: Fixed detection order (lists before code),
366// improved mixed-content handling for JSON in descriptions
367// 
368// Sheet Columns: ID, Title, Short Desc, Full Desc, 
369// Requirements, Inputs, Use Cases, Vertical, Lifecycle, 
370// Priority, Published, Slug, Feed Type
371// =========================================================
372
373(function recipeExplorer() {
374  // ---------- DOM Hooks ----------
375  const appEl = document.getElementById('recipes-app');
376  const listEl = document.getElementById('recipes-list');
377  const pagerEl = document.getElementById('recipes-pagination');
378  const searchEl = document.getElementById('recipes-search');
379  const pageSizeEl = document.getElementById('recipes-page-size');
380  const lifeCycleEl = document.getElementById('recipes-lifecycle-filter');
381  const verticalEl = document.getElementById('recipes-vertical-filter');
382  const modalOverlay = document.getElementById('details-modal');
383  const modalTitle = document.getElementById('modal-title');
384  const modalContentArea = document.getElementById('modal-content-area');
385  const modalCloseBtn = modalOverlay?.querySelector('.modal-close-btn');
386  const modalTagVertical = document.getElementById('modal-tag-vertical');
387  const modalTagLifecycle = document.getElementById('modal-tag-lifecycle');
388
389  if (!appEl || !listEl || !pagerEl || !modalOverlay) {
390    return console.error('[recipes] Missing essential DOM elements.');
391  }
392
393  // ---------- Config ----------
394  
395  const DEBUG = false;
396  const SHEET_ID = (appEl.dataset.sheetId || '').trim();
397  const TAB_NAME = (appEl.dataset.tab || 'All_Recipes').trim();
398  const IS_PREVIEW = appEl.dataset.preview === 'true';
399  const FALLBACK_MSG = appEl.dataset.fallback || 'No recipes available.';
400  
401  const DEFAULT_DESKTOP_SIZE = 12;
402const DEFAULT_NON_DESKTOP_SIZE = 6;
403const isNonDesktop = () => window.matchMedia('(max-width: 1024px)').matches;
404
405  // ---------- State ----------
406let rawRows = [];
407let page = 1;
408
409// ✅ set pageSize
410let pageSize = isNonDesktop()
411  ? DEFAULT_NON_DESKTOP_SIZE
412  : parseInt(pageSizeEl?.value || String(DEFAULT_DESKTOP_SIZE), 10);
413
414// ✅ then override dropdown on mobile
415if (isNonDesktop() && pageSizeEl) {
416  pageSizeEl.value = String(DEFAULT_NON_DESKTOP_SIZE);
417}
418
419let userSetPageSize = false;
420
421
422
423  // ---------- Utility Functions ----------
424  const debounce = (fn, ms = 400) => {
425    let timer;
426    return (...args) => {
427      clearTimeout(timer);
428      timer = setTimeout(() => fn(...args), ms);
429    };
430  };
431
432  const parseGviz = (txt) => {
433    const start = txt.indexOf('{');
434    const end = txt.lastIndexOf('}') + 1;
435    return JSON.parse(txt.slice(start, end));
436  };
437
438  const getSafeString = (val) => String(val || '').trim();
439
440  const escapeHtml = (str) => 
441    String(str).replace(/[&<>"']/g, m => ({
442      '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
443    }[m]));
444
445  const toSentenceCase = (str) => {
446    if (!str) return '';
447    return str.charAt(0).toUpperCase() + str.slice(1);
448  };
449
450  // ---------- Text Processing Helpers (FIXED ORDER) ----------
451  
452  // Parse numbered or bulleted lists
453  const parseList = (text) => {
454    if (!text) return null;
455    
456    // Split by numbered pattern (1. 2. 3.) OR bullet patterns (- • *)
457    const splitPattern = /(?=\d+\.\s+|[-•*]\s+)/g;
458    const items = text.split(splitPattern)
459      .map(item => item.replace(/^(\d+\.|[-•*])\s*/, '').trim())
460      .filter(item => item.length > 0);
461    
462    // Must have at least 2 items to be a list
463    if (items.length < 2) return null;
464    
465    return items;
466  };
467
468  // Try to parse and pretty-print JSON, with fallback formatting
469const tryFormatJSON = (text) => {
470  try {
471    const parsed = JSON.parse(text);
472    return JSON.stringify(parsed, null, 2);
473  } catch (e) {
474    // JSON parsing failed - try to at least format it nicely
475    return text
476      .replace(/\s*,\s*/g, ',\n  ')      // Add line breaks after commas
477      .replace(/\{\s*/g, '{\n  ')         // Open brace + newline
478      .replace(/\s*\}/g, '\n}')           // Close brace on new line
479      .replace(/:\s*/g, ': ');            // Space after colons
480  }
481};
482
483  // Detect if ENTIRE text is pure code/JSON (not just contains it)
484  const isPureCode = (text) => {
485    if (!text) return false;
486    const trimmed = text.trim();
487    
488    // Must START with JSON structure
489    if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
490        (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
491      return true;
492    }
493    
494    return false;
495  };
496
497  // Format text based on detected type (CRITICAL: CHECK LISTS FIRST!)
498  const formatText = (text, isDescription = false) => {
499    if (!text) return '';
500    
501    // Helper: Add period if text doesn't end with punctuation
502    const ensurePeriod = (str) => {
503      const trimmed = str.trim();
504      if (/[.!?;]$/.test(trimmed)) return trimmed;
505      return trimmed + '.';
506    };
507    
508    // Special handling for descriptions with mixed content (text + JSON)
509    if (isDescription) {
510      // Look for JSON blocks within text
511      const jsonMatch = text.match(/\{[^}]+\}/s);
512      if (jsonMatch) {
513        const jsonPart = jsonMatch[0];
514        const beforeJSON = text.substring(0, jsonMatch.index).trim();
515        const afterJSON = text.substring(jsonMatch.index + jsonPart.length).trim();
516        
517        let html = '';
518        if (beforeJSON) html += `<p>${escapeHtml(ensurePeriod(beforeJSON))}</p>`;
519        html += `<pre><code>${escapeHtml(tryFormatJSON(jsonPart))}</code></pre>`;
520        if (afterJSON) html += `<p>${escapeHtml(ensurePeriod(afterJSON))}</p>`;
521        return html;
522      }
523      return `<p>${escapeHtml(ensurePeriod(text))}</p>`;
524    }
525    
526    // For non-descriptions: CHECK LISTS FIRST (before code detection)
527    const listItems = parseList(text);
528    if (listItems) {
529      // Multi-item list - format as ordered list
530      const itemsHtml = listItems.map(item => `<li>${escapeHtml(ensurePeriod(item))}</li>`).join('');
531      return `<ol>${itemsHtml}</ol>`;
532    }
533    
534    // Check if text LOOKS like a single numbered/bulleted item
535    const singleItemPattern = /^(\d+\.|[-•*])\s+(.+)$/s;
536    const singleMatch = text.match(singleItemPattern);
537    if (singleMatch) {
538      // Strip the number/bullet and treat as paragraph
539      return `<p>${escapeHtml(ensurePeriod(singleMatch[2]))}</p>`;
540    }
541    
542    // Then check if ENTIRE content is pure code/JSON
543    if (isPureCode(text)) {
544      const formatted = tryFormatJSON(text);
545      return `<pre><code>${escapeHtml(formatted)}</code></pre>`;
546    }
547    
548    // Default to paragraph
549    return `<p>${escapeHtml(ensurePeriod(text))}</p>`;
550  };
551
552  // ---------- Data Normalization ----------
553  
554  // Map sheet columns (A-K = 0-10)
555  const normalizeRow = (row) => ({
556  id: getSafeString(row.c?.[0]?.v),           // ID
557  title: getSafeString(row.c?.[1]?.v),        // Title
558  shortDesc: getSafeString(row.c?.[2]?.v),    // Short Description
559  fullDesc: getSafeString(row.c?.[3]?.v),     // Full Description
560  requirements: getSafeString(row.c?.[4]?.v), // Requirements
561  inputs: getSafeString(row.c?.[5]?.v),       // Inputs
562  useCases: getSafeString(row.c?.[6]?.v),     // Use Cases
563  vertical: getSafeString(row.c?.[7]?.v),     // Vertical
564  lifeCycle: getSafeString(row.c?.[8]?.v),    // Lifecycle Stage
565  priority: Number(row.c?.[9]?.v) || 5,       // Priority
566  publish: getSafeString(row.c?.[10]?.v).trim().toLowerCase(), // Published
567  slug: getSafeString(row.c?.[14]?.v), // 👈 Column O (15th column, index 14)
568  feedType: getSafeString(row.c?.[15]?.v).toLowerCase()     // Feed Type (Column P)
569});
570
571
572
573  const isValid = (row) => row.title && row.publish === 'yes';
574
575  const matchesFilters = (row, query, lifeCycle, vertical) => {
576    const searchText = `${row.title} ${row.shortDesc} ${row.fullDesc}`.toLowerCase();
577    // ✅ Enhanced multi-word & fuzzy search
578function fuzzyMatch(text, term) {
579  const pattern = term.split('').join('.{0,2}'); // allows small typos
580  return new RegExp(pattern, 'i').test(text);
581}
582
583const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
584const searchMatch = !query || terms.every(term => fuzzyMatch(searchText, term));
585
586    const lifeCycleMatch = !lifeCycle || row.lifeCycle === lifeCycle;
587    const verticalMatch = !vertical || row.vertical === vertical;
588    
589    return searchMatch && lifeCycleMatch && verticalMatch;
590  };
591
592  // ---------- Pagination ----------
593  
594  const slicePage = (rows, currentPage, size) => {
595    const total = rows.length;
596    const pages = Math.max(1, Math.ceil(total / size));
597    const cur = Math.min(Math.max(1, currentPage), pages);
598    const start = (cur - 1) * size;
599    const pageRows = rows.slice(start, start + size);
600    
601    return { pageRows, cur, pages, total };
602  };
603
604  // ---------- Data Fetching ----------
605async function fetchSheet(sheetId, tabName) {
606  if (!sheetId) {
607    throw new Error('Missing data-sheet-id on #recipes-app');
608  }
609
610  // ✅ Use cache only if not in preview mode
611  const cached = sessionStorage.getItem('recipesCache');
612  if (cached && !IS_PREVIEW) {
613    if (DEBUG) console.log('[recipes] Using cached data');
614    return JSON.parse(cached);
615  }
616
617  const cacheBuster = IS_PREVIEW ? `&_=${Date.now()}` : '';
618  const baseUrl = `https://docs.google.com/spreadsheets/d/${encodeURIComponent(sheetId)}/gviz/tq`;
619  const url = `${baseUrl}?tqx=out:json&sheet=${encodeURIComponent(tabName)}${cacheBuster}`;
620
621  if (DEBUG) console.log(`[recipes] Fetching: ${url}`);
622
623  try {
624    const response = await fetch(url, { cache: 'no-store' });
625    if (!response.ok) throw new Error(`HTTP ${response.status}`);
626
627    const jsonData = parseGviz(await response.text());
628    const allRows = jsonData.table?.rows || [];
629    const validRows = allRows.map(normalizeRow).filter(isValid);
630
631    if (DEBUG) {
632      console.log('[recipes] Total rows:', allRows.length);
633      console.log('[recipes] Valid published rows:', validRows.length);
634      console.log('[recipes] Sample valid row:', validRows[0]);
635    }
636
637    if (validRows.length === 0 && allRows.length > 0 && DEBUG) {
638      console.warn('[recipes] No valid rows found. Check Title (col B) and Published=yes (col K)');
639    }
640
641    if (!IS_PREVIEW) {
642      sessionStorage.setItem('recipesCache', JSON.stringify(validRows));
643    }
644
645    return validRows;
646  } catch (err) {
647    if (DEBUG) console.error('[recipes] Initialization failed:', err);
648    return [];
649  }
650}
651  
652  // -----------Render for feed recipe lists --------------
653  
654  function renderFeedRecipeList(list, containerId) {
655  const container = document.getElementById(containerId);
656  if (!container) return;
657
658  const PAGE_SIZE = 10;
659  let currentPage = 1;
660
661  function renderPage() {
662    const start = (currentPage - 1) * PAGE_SIZE;
663    const pageItems = list.slice(start, start + PAGE_SIZE);
664
665    container.innerHTML = pageItems.map(r => `
666      <button class="feed-recipe-link" data-id="${r.id}">
667        ${r.title}
668      </button>
669    `).join('') + renderPager();
670
671    attachClicks();
672  }
673
674  function renderPager() {
675    const totalPages = Math.ceil(list.length / PAGE_SIZE);
676    if (totalPages <= 1) return '';
677
678    return `
679      <div class="feed-pager">
680        <button ${currentPage === 1 ? 'disabled' : ''} data-prev>Prev</button>
681        <span>${currentPage} / ${totalPages}</span>
682        <button ${currentPage === totalPages ? 'disabled' : ''} data-next>Next</button>
683      </div>
684    `;
685  }
686
687  function attachClicks() {
688    container.querySelectorAll('.feed-recipe-link').forEach(btn => {
689      btn.addEventListener('click', () => {
690        const recipe = rawRows.find(r => r.id === btn.dataset.id);
691        if (recipe) showModal(recipe); // 👈 reuse modal
692      });
693    });
694
695    const prev = container.querySelector('[data-prev]');
696    const next = container.querySelector('[data-next]');
697
698    if (prev) prev.onclick = () => { currentPage--; renderPage(); };
699    if (next) next.onclick = () => { currentPage++; renderPage(); };
700  }
701
702  renderPage();
703}
704
705
706
707  // ---------- Modal Functions ----------
708
709const showModal = (recipe) => {
710  if (!modalOverlay || !modalTitle || !modalContentArea) {
711    return console.error('[recipes] Modal elements missing at runtime.');
712  }
713
714  // 1. Build the sections HTML, running content through formatText() immediately
715  const sections = [
716    { title: 'Description', content: recipe.fullDesc, isDescription: true },
717    { title: 'Requirements', content: recipe.requirements },
718    { title: 'Input / Configuration', content: recipe.inputs },
719    { title: 'Suggested Use Cases', content: recipe.useCases }
720  ];
721
722  const sectionsHtml = sections
723    .filter(section => section.content && formatText(section.content, section.isDescription)) // Filter out empty content after formatting
724    .map(section => `
725      <div class="modal-section">
726        <h3>${section.title}</h3>
727        ${formatText(section.content, section.isDescription)}
728      </div>
729    `)
730    .join('');
731    
732  // If sectionsHtml is empty, show a fallback message instead of crashing
733  if (!sectionsHtml) {
734    modalContentArea.innerHTML = `<p>Error: Recipe details are missing or could not be formatted correctly.</p>`;
735  } else {
736    modalContentArea.innerHTML = sectionsHtml;
737  }
738
739  // 2. Update modal header content
740  modalTitle.textContent = toSentenceCase(recipe.title);
741  
742  // Safely set tag content
743  if (modalTagLifecycle) {
744    modalTagLifecycle.textContent = toSentenceCase(recipe.lifeCycle || 'N/A');
745    modalTagLifecycle.style.display = recipe.lifeCycle ? 'inline-block' : 'none';
746  }
747  if (modalTagVertical) {
748    modalTagVertical.textContent = toSentenceCase(recipe.vertical || 'N/A');
749    modalTagVertical.style.display = recipe.vertical ? 'inline-block' : 'none';
750  }
751  
752  // 3. Show the modal
753  modalOverlay.classList.remove('hidden');
754  document.body.style.overflow = 'hidden';
755};
756
757const hideModal = () => {
758  modalOverlay.classList.add('hidden');
759  document.body.style.overflow = '';
760  const baseUrl = window.location.origin + window.location.pathname;
761const qs = new URLSearchParams(window.location.search);
762qs.delete('slug');
763const newUrl = qs.toString() ? `${baseUrl}?${qs.toString()}` : baseUrl;
764window.history.replaceState({}, '', newUrl);
765
766};
767
768  // ---------- Rendering ----------
769  
770  function showLoading() {
771    listEl.innerHTML = '<div class="recipes-loading">Loading recipes…</div>';
772    pagerEl.innerHTML = '';
773  }
774
775  function renderCards(rows) {
776    if (!rows.length) {
777      const query = searchEl?.value?.trim() || '';
778const lifeCycle = lifeCycleEl?.value?.trim() || '';
779const vertical = verticalEl?.value?.trim() || '';
780
781const isFilterActive = query || lifeCycle || vertical;
782
783const message = isFilterActive
784  ? `Try adjusting your filters or search to discover other recommendation recipes.`
785  : FALLBACK_MSG;
786
787const title = isFilterActive
788  ? 'No recommendation recipes found'
789  : 'No recommendation recipes published yet';
790
791listEl.innerHTML = `
792  <div class="recipes-empty">
793    <h3>${title}</h3>
794    <p>${message}</p>
795  </div>`;
796
797
798      pagerEl.innerHTML = '';
799      return;
800    }
801
802    listEl.innerHTML = rows.map(recipe => `
803      <article class="recipe-card-v2" data-recipe-id="${escapeHtml(recipe.id)}" data-slug="${escapeHtml(recipe.slug)}">
804        <div>
805          <h3 class="recipe-card-title">${escapeHtml(toSentenceCase(recipe.title))}</h3>
806          <p class="recipe-card-desc">${escapeHtml(recipe.shortDesc)}</p>
807        </div>
808        <div class="recipe-card-tags">
809          <span class="tag tag-lifecycle">${escapeHtml(toSentenceCase(recipe.lifeCycle))}</span>
810          <span class="tag tag-vertical">${escapeHtml(toSentenceCase(recipe.vertical))}</span>
811        </div>
812      </article>
813    `).join('');
814
815    // Attach click handlers
816    listEl.querySelectorAll('.recipe-card-v2').forEach(card => {
817      card.addEventListener('click', () => {
818        const recipeId = card.dataset.recipeId;
819        const recipe = rawRows.find(r => r.id === recipeId);
820        if (recipe) {
821  showModal(recipe);
822  if (recipe.slug) {
823    const baseUrl = window.location.origin + window.location.pathname;
824const qs = new URLSearchParams(window.location.search);
825qs.set('slug', recipe.slug);
826window.history.pushState({}, '', `${baseUrl}?${qs.toString()}`);
827
828  }
829}
830
831      });
832    });
833  }
834
835  function renderPagination(current, total, itemCount) {
836    if (total <= 1) {
837      pagerEl.innerHTML = '';
838      return;
839    }
840
841    const btn = (label, targetPage, disabled = false) =>
842      `<button class="pg-btn" data-page="${targetPage}" ${disabled ? 'disabled' : ''}>${label}</button>`;
843
844    pagerEl.innerHTML = `
845      <div class="pager">
846        ${btn('« First', 1, current <= 1)}
847        ${btn('‹ Prev', current - 1, current <= 1)}
848        <span class="pg-status">Page ${current} of ${total} · ${itemCount} items</span>
849        ${btn('Next ›', current + 1, current >= total)}
850        ${btn('Last »', total, current >= total)}
851      </div>`;
852  }
853  
854  // ------------ Render functions for feed lists ---------
855  
856  function renderFeedRecipeGroups() {
857  const userProductRecipes = rawRows
858    .filter(r => r.feedType === 'user-product')
859    .sort((a, b) => a.title.localeCompare(b.title));
860
861  const productProductRecipes = rawRows
862    .filter(r => r.feedType === 'product-product')
863    .sort((a, b) => a.title.localeCompare(b.title));
864
865  renderFeedRecipeList(userProductRecipes, 'feed-user-recipes');
866  renderFeedRecipeList(productProductRecipes, 'feed-product-recipes');
867}
868
869  // ---------- Apply View ----------
870  
871  function applyView() {
872    const query = searchEl?.value?.trim() || '';
873    const lifeCycle = lifeCycleEl?.value?.trim() || '';
874    const vertical = verticalEl?.value?.trim() || '';
875
876    const filtered = rawRows.filter(r => matchesFilters(r, query, lifeCycle, vertical));
877    const { pageRows, cur, pages, total } = slicePage(filtered, page, pageSize);
878
879    renderCards(pageRows);
880    renderPagination(cur, pages, total);
881  }
882
883  // ---------- Event Handlers ----------
884  
885  // Modal events
886  if (modalCloseBtn) {
887    modalCloseBtn.addEventListener('click', hideModal);
888  }
889  
890  if (modalOverlay) {
891    modalOverlay.addEventListener('click', (e) => {
892      if (e.target === modalOverlay) hideModal();
893    });
894  }
895
896  // Filter events
897  if (searchEl) {
898    searchEl.addEventListener('input', debounce(() => {
899      page = 1;
900      applyView();
901    }));
902  }
903
904  if (lifeCycleEl) {
905    lifeCycleEl.addEventListener('change', () => {
906      page = 1;
907      applyView();
908    });
909  }
910
911  if (verticalEl) {
912    verticalEl.addEventListener('change', () => {
913      page = 1;
914      applyView();
915    });
916  }
917
918 if (pageSizeEl) {
919  pageSizeEl.addEventListener('change', () => {
920    userSetPageSize = true;
921    pageSize = parseInt(pageSizeEl.value, 10) || DEFAULT_DESKTOP_SIZE;
922    page = 1;
923    applyView();
924  });
925}
926  
927   // ✅ Reset All Button: clears search, filters, page size, and resets view
928  const clearBtn = document.getElementById('recipes-clear-filters');
929  if (clearBtn) {
930    clearBtn.addEventListener('click', () => {
931      // Clear search input
932      searchEl.value = '';
933
934      // Reset dropdown filters
935      lifeCycleEl.value = '';
936      verticalEl.value = '';
937
938      // Reset page size based on screen type
939const resetSize = isNonDesktop() ? DEFAULT_NON_DESKTOP_SIZE : DEFAULT_DESKTOP_SIZE;
940pageSizeEl.value = String(resetSize);
941pageSize = resetSize;
942
943
944      // Reset pagination
945      page = 1;
946
947      // Re-render
948      applyView();
949    });
950  }
951
952
953  // Pagination events
954pagerEl.addEventListener('click', (e) => {
955  const btn = e.target.closest('button[data-page]');
956  if (btn) {
957    page = parseInt(btn.dataset.page, 10);
958    applyView();
959    
960    // Scroll to recipe list with reliable positioning
961    setTimeout(() => {
962      const rect = listEl.getBoundingClientRect();
963      const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
964      const targetPosition = rect.top + scrollTop - 300; // 300px above the list
965      
966      window.scrollTo({
967        top: targetPosition,
968        behavior: 'smooth'
969      });
970    }, 50); // Small delay to ensure content is rendered
971  }
972});
973
974  // ---------- Initialize ----------
975(async function init() {
976  try {
977    if (!SHEET_ID) {
978      throw new Error('Missing data-sheet-id on #recipes-app');
979    }
980
981    showLoading();
982    rawRows = await fetchSheet(SHEET_ID, TAB_NAME);
983    renderFeedRecipeGroups();
984    
985    // ✅ Deep-link + filter support (Zendesk-friendly: ?slug=)
986const urlParams = new URLSearchParams(window.location.search);
987
988const slugFromUrl = urlParams.get('slug');
989if (slugFromUrl) {
990  const matched = rawRows.find(r => r.slug === slugFromUrl);
991  if (matched) showModal(matched);
992}
993
994if (pageSizeEl) {
995  pageSizeEl.value = String(pageSize);
996}
997
998// ✅ Apply filters from URL parameters
999const presetVertical = urlParams.get('vertical');
1000const presetLifecycle = urlParams.get('lifecycle');
1001const presetSearch = urlParams.get('q');
1002
1003if (presetVertical && verticalEl) verticalEl.value = presetVertical;
1004if (presetLifecycle && lifeCycleEl) lifeCycleEl.value = presetLifecycle;
1005if (presetSearch && searchEl) searchEl.value = presetSearch;
1006
1007page = 1;
1008applyView();
1009
1010
1011
1012    // ✅ Render vertical sections after data load
1013    renderVerticalSections();
1014    
1015    /* for SEO */
1016    setTimeout(renderCrawlableList, 2000); // delay SEO block load
1017
1018   // renderCrawlableList(); //
1019// autoCycleModals(); // disable automatic modal cycling
1020
1021
1022  } catch (error) {
1023    if (DEBUG) console.error('[recipes] Initialization failed:', error);
1024    listEl.innerHTML = `
1025      <div class="recipes-empty">
1026        <h3>Failed to Load</h3>
1027        <p>${FALLBACK_MSG}</p>
1028      </div>`;
1029    pagerEl.innerHTML = '';
1030  }
1031})();
1032
1033  
1034 // =========================================================
1035// Render Vertical Sections (Retail / Marketplaces / etc.)
1036// This uses column H (Vertical) and column J (Priority).
1037// Priority order: 1–4 first, then fallback rows in sheet order.
1038// Renders max 4 cards per vertical section and shows
1039// fallback message if no recipes found.
1040// =========================================================
1041function renderVerticalSections() {
1042  document.querySelectorAll('.recipe-section-x[data-vertical]').forEach(section => {
1043    const verticalName = section.dataset.vertical || '';
1044    const listEl = section.querySelector('.recipe-cards-x');
1045
1046    // Step 1: Filter all rows for this vertical
1047    const verticalRows = rawRows.filter(r => r.vertical === verticalName);
1048
1049    // Step 2: Get priority 1–4 rows in order
1050    const priorityRows = [1, 2, 3, 4]
1051      .map(p => verticalRows.find(r => r.priority === p))
1052      .filter(Boolean);
1053
1054    // Step 3: Fill remaining slots with other rows (excluding already selected)
1055    const remainingRows = verticalRows
1056      .filter(r => !priorityRows.includes(r))
1057      .slice(0, 4 - priorityRows.length);
1058
1059    // Step 4: Final selection (max 4, no duplicates)
1060    const rowsForVertical = [...priorityRows, ...remainingRows];
1061
1062    // Step 5: Debug logs
1063 if (DEBUG)   console.log(`[recipes] Vertical: ${verticalName}`);
1064 if (DEBUG)   console.log('Priority rows:', priorityRows);
1065  if (DEBUG)  console.log('Remaining rows:', remainingRows);
1066  if (DEBUG)  console.log('Selected rows:', rowsForVertical);
1067
1068    // Step 6: Fallback message if nothing to show
1069    if (!rowsForVertical.length) {
1070      listEl.innerHTML = `
1071        <div class="recipes-empty">
1072          <h3>No recommendation recipes published yet</h3>
1073          <p>We’re updating recommendation recipes soon. Check back later.</p>
1074        </div>`;
1075      return;
1076    }
1077
1078    // Step 7: Render recipe cards
1079    listEl.innerHTML = rowsForVertical.map(recipe => `
1080      <article class="recipe-card-v2" data-recipe-id="${escapeHtml(recipe.id)}" data-slug="${escapeHtml(recipe.slug)}">
1081        <div>
1082          <h3 class="recipe-card-title">${escapeHtml(recipe.title)}</h3>
1083          <p class="recipe-card-desc">${escapeHtml(recipe.shortDesc)}</p>
1084        </div>
1085        <div class="recipe-card-tags">
1086          <span class="tag tag-lifecycle">${escapeHtml(recipe.lifeCycle)}</span>
1087          <span class="tag tag-vertical">${escapeHtml(recipe.vertical)}</span>
1088        </div>
1089      </article>
1090    `).join('');
1091
1092    // Step 8: Modal click handlers
1093    listEl.querySelectorAll('.recipe-card-v2').forEach(card => {
1094      card.addEventListener('click', () => {
1095        const recipeId = card.dataset.recipeId;
1096        const recipe = rawRows.find(r => r.id === recipeId);
1097        if (recipe) {
1098  showModal(recipe);
1099  if (recipe.slug) {
1100    const baseUrl = window.location.origin + window.location.pathname;
1101const qs = new URLSearchParams(window.location.search);
1102qs.set('slug', recipe.slug);
1103window.history.pushState({}, '', `${baseUrl}?${qs.toString()}`);
1104
1105  }
1106}
1107
1108      });
1109    });
1110  });
1111}
1112
1113// =========================================================
1114// Hidden Crawlable List + Auto Modal Cycle (for SEO)
1115// =========================================================
1116function renderCrawlableList() {
1117  const container = document.getElementById('recipes-crawlable');
1118  if (!container) return;
1119
1120  const ul = container.querySelector('ul');
1121  if (!ul) return;
1122
1123  ul.innerHTML = rawRows
1124    .map(r => `<li><a href="?slug=${r.slug}">${escapeHtml(r.title)} (${escapeHtml(r.vertical)})</a></li>`)
1125    .join('');
1126
1127if (DEBUG) console.log(`[recipes] Crawlable list generated with ${rawRows.length} entries`);
1128}
1129
1130function autoCycleModals() {
1131  const links = document.querySelectorAll('#recipes-crawlable a');
1132  if (!links.length) return;
1133
1134  let index = 0;
1135  const openNext = () => {
1136    if (index >= links.length) return;
1137    const slug = new URL(links[index].href).searchParams.get('slug');
1138    const recipe = rawRows.find(r => r.slug === slug);
1139
1140    if (recipe) {
1141      showModal(recipe);
1142   if (DEBUG) console.log(`[SEO] Opened modal for: ${slug}`);
1143    }
1144
1145    index++;
1146    setTimeout(() => {
1147      hideModal();
1148      setTimeout(openNext, 300);
1149    }, 300);
1150  };
1151
1152  // Run this only in preview mode
1153  if (IS_PREVIEW) setTimeout(openNext, 1000);
1154}
1155
1156
1157
1158})();
1159
1160
1161
1162
1163
1164
1165
1166}); // ✅ end $(function(){ ... })
1167
1168
1169
1170// =========================================================
1171  // Deliverability Glossary (bs-gl)
1172  // Feb 2026 · v8
1173  // Auto-sort, auto-group, auto alpha nav
1174  // HTML is just a flat bag of entries — JS does everything
1175  // =========================================================
1176  (function() {
1177    var $list = $('#bsGlList');
1178    if (!$list.length) return;
1179
1180    var $search   = $('#bsGlSearch');
1181    var $clearBtn = $('#bsGlClear');
1182    var $alpha    = $('#bsGlAlpha');
1183    var $empty    = $('#bsGlEmpty');
1184    var $info     = $('#bsGlInfo');
1185    var $count    = $('#bsGlCount');
1186    var $controls = $('#bsGlControls');
1187    var $topBtn   = $('#bsGlTop');
1188
1189    // Toast: append to body so it doesn't affect article layout
1190    var $toast = $('<div class="bs-gl-toast" id="bsGlToast" aria-live="polite"></div>');
1191    $('body').append($toast);
1192
1193    var H = 'bs-gl-hidden';
1194
1195    // ─── Step 1: Read all entries and sort ───
1196
1197    var entries = [];
1198    $list.find('.bs-gl-entry').each(function() {
1199      var $el = $(this);
1200      var term = ($el.data('term') || '') + '';
1201      var full = ($el.data('full') || '') + '';
1202      var displayName = full || term;
1203      var firstChar = displayName.charAt(0).toUpperCase();
1204      var group = /[A-Z]/.test(firstChar) ? firstChar : 'num';
1205
1206      entries.push({
1207        $el: $el,
1208        term: term,
1209        full: full,
1210        displayName: displayName,
1211        group: group,
1212        sortKey: displayName.toLowerCase()
1213      });
1214    });
1215
1216    // Sort: non-alpha group first, then A-Z, alphabetical within each group
1217    entries.sort(function(a, b) {
1218      if (a.group === 'num' && b.group !== 'num') return -1;
1219      if (a.group !== 'num' && b.group === 'num') return 1;
1220      if (a.group !== b.group) return a.group < b.group ? -1 : 1;
1221      return a.sortKey < b.sortKey ? -1 : a.sortKey > b.sortKey ? 1 : 0;
1222    });
1223
1224    // ─── Step 2: Rebuild DOM with letter headers ───
1225
1226    $list.empty();
1227    var currentGroup = null;
1228    var hasNonAlpha = entries.some(function(e) { return e.group === 'num'; });
1229
1230    entries.forEach(function(entry) {
1231      if (entry.group !== currentGroup) {
1232        currentGroup = entry.group;
1233        var display = currentGroup === 'num' ? '#' : currentGroup;
1234        var $header = $('<div class="bs-gl-letter" id="letter-' + currentGroup + '"><span>' + display + '</span></div>');
1235        $list.append($header);
1236      }
1237      $list.append(entry.$el);
1238    });
1239
1240    // ─── Step 3: Re-query DOM after rebuild ───
1241
1242    var $entries = $list.find('.bs-gl-entry');
1243    var $letters = $list.find('.bs-gl-letter');
1244    var $children = $list.children();
1245    var total = $entries.length;
1246
1247    // Auto-count
1248    $count.text(total + ' terms');
1249
1250    // ─── Step 4: Build search index ───
1251
1252    var idx = [];
1253    $entries.each(function() {
1254      var $el = $(this);
1255      var term = ($el.data('term') || '') + '';
1256      var full = ($el.data('full') || '') + '';
1257      var def  = $el.find('p').not('.bs-gl-term-ex').not(':has(.bs-gl-term-link)').last().text() || '';
1258      idx.push({
1259        $el: $el,
1260        text: (term + ' ' + full + ' ' + def).toLowerCase()
1261      });
1262    });
1263
1264    // ─── Stemmer ───
1265
1266    function stem(w) {
1267      if (w.length < 4) return w;
1268      return w.replace(/(ations?|tions?|ments?|ing|ed|ly|ic|al|es|s)$/, '');
1269    }
1270
1271    // Multi-layer match
1272    function matches(hay, needle) {
1273      if (hay.indexOf(needle) >= 0) return true;
1274      var root = stem(needle);
1275      if (root.length >= 3 && hay.indexOf(root) >= 0) return true;
1276      if (needle.length >= 4) {
1277        try {
1278          if (new RegExp(needle.split('').join('.?'), 'i').test(hay)) return true;
1279        } catch(e) {}
1280      }
1281      return false;
1282    }
1283
1284    // ─── Separators (JS-managed) ───
1285
1286    function updateSeps() {
1287      $entries.removeClass('bs-gl-sep');
1288      var prev = false;
1289      $children.each(function() {
1290        var $c = $(this);
1291        if ($c.hasClass('bs-gl-letter')) {
1292          prev = $c.hasClass(H) ? prev : false;
1293        } else if ($c.hasClass('bs-gl-entry') && !$c.hasClass(H)) {
1294          if (prev) $c.addClass('bs-gl-sep');
1295          prev = true;
1296        }
1297      });
1298    }
1299
1300    // ─── Alpha nav ───
1301
1302    function buildAlpha() {
1303      var abc = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
1304      var active = {};
1305      $letters.not('.' + H).each(function() { active[this.id.replace('letter-', '')] = true; });
1306
1307      var html = '';
1308
1309      // # button — only if non-alpha entries exist
1310      if (hasNonAlpha) {
1311        html += '<a href="#letter-num"' + (active['num'] ? '' : ' class="bs-gl-off"') + '>#</a>';
1312      }
1313
1314      // A-Z — always shown
1315      html += abc.map(function(c) {
1316        return '<a href="#letter-' + c + '"' + (active[c] ? '' : ' class="bs-gl-off"') + '>' + c + '</a>';
1317      }).join('');
1318
1319      $alpha.html(html);
1320    }
1321
1322    // ─── Search ───
1323
1324    function doSearch(q) {
1325      q = (q || '').trim().toLowerCase();
1326      var terms = q ? q.split(/\s+/) : [];
1327      var found = 0;
1328
1329      // Clear highlights
1330      $entries.find('mark').each(function() {
1331        var p = this.parentNode;
1332        p.replaceChild(document.createTextNode(this.textContent), this);
1333        p.normalize();
1334      });
1335      $entries.find('span').not('.bs-gl-badge').each(function() {
1336        if (!this.className && this.childNodes.length) $(this).replaceWith($(this).contents());
1337      });
1338
1339      // Show/hide
1340      idx.forEach(function(item) {
1341        var ok = !terms.length || terms.every(function(t) { return matches(item.text, t); });
1342        item.$el.toggleClass(H, !ok);
1343        if (ok) found++;
1344      });
1345
1346      // Highlight
1347      if (q && found) {
1348        try {
1349          var re = new RegExp('(' + q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
1350          $entries.not('.' + H).find('p').each(function() {
1351            $(this).contents().filter(function() { return this.nodeType === 3; }).each(function() {
1352              re.lastIndex = 0;
1353              if (re.test(this.textContent)) {
1354                re.lastIndex = 0;
1355                var s = document.createElement('span');
1356                s.innerHTML = this.textContent.replace(re, '<mark>$1</mark>');
1357                this.parentNode.replaceChild(s, this);
1358              }
1359            });
1360          });
1361        } catch(e) {}
1362      }
1363
1364      // Letter headers — hide if no visible entries beneath
1365      $letters.each(function() {
1366        var $h = $(this);
1367        $h.toggleClass(H, !$h.nextUntil('.bs-gl-letter', '.bs-gl-entry').not('.' + H).length);
1368      });
1369
1370      buildAlpha();
1371      updateSeps();
1372
1373      // Count logic
1374      if (terms.length) {
1375        $count.hide();
1376        $info.html('<b>' + found + '</b> of ' + total + ' terms').addClass('bs-gl-show');
1377        $empty.toggle(found === 0);
1378      } else {
1379        $count.show();
1380        $info.removeClass('bs-gl-show').empty();
1381        $empty.hide();
1382      }
1383    }
1384
1385    // ─── Events ───
1386
1387    $search.on('input', function() {
1388      $clearBtn.toggleClass('bs-gl-show', this.value.length > 0);
1389      doSearch(this.value);
1390    });
1391
1392    $clearBtn.on('click', function() { $search.val('').trigger('input').focus(); });
1393    $(document).on('click', '#bsGlClearLink', function(e) { e.preventDefault(); $search.val('').trigger('input'); });
1394
1395    // Keyboard
1396    $(document).on('keydown', function(e) {
1397      if (e.key === '/' && !$(e.target).is('input,textarea,select')) { e.preventDefault(); $search.focus(); }
1398      if (e.key === 'Escape' && $search.is(':focus')) { $search.val('').trigger('input').blur(); }
1399    });
1400
1401    // ─── Clipboard ───
1402
1403    function copyText(text, ok, fail) {
1404      if (navigator.clipboard && navigator.clipboard.writeText) {
1405        navigator.clipboard.writeText(text).then(ok).catch(function() { fbCopy(text, ok, fail); });
1406      } else {
1407        fbCopy(text, ok, fail);
1408      }
1409    }
1410
1411    function fbCopy(text, ok, fail) {
1412      try {
1413        var ta = document.createElement('textarea');
1414        ta.value = text;
1415        ta.style.cssText = 'position:fixed;left:-9999px';
1416        document.body.appendChild(ta);
1417        ta.select();
1418        document.execCommand('copy');
1419        document.body.removeChild(ta);
1420        ok();
1421      } catch(e) { if (fail) fail(); }
1422    }
1423
1424    function toast(msg) {
1425      $toast.text(msg).addClass('bs-gl-show');
1426      setTimeout(function() { $toast.removeClass('bs-gl-show'); }, 1800);
1427    }
1428
1429    // ─── Permalink ───
1430
1431    $list.on('click', '.bs-gl-hash', function(e) {
1432      e.preventDefault();
1433      var hash = $(this).attr('href');
1434      var url = location.href.split('#')[0] + hash;
1435      history.replaceState(null, null, hash);
1436      copyText(url, function() { toast('Link copied'); }, function() { toast('URL updated'); });
1437    });
1438
1439    // ─── Back-to-top: smooth brake at content boundary ───
1440
1441    // Move button inside list so it can use absolute positioning relative to list
1442    $list.append($topBtn);
1443
1444    function positionTopBtnFixed() {
1445      var $anchor = $list.closest('.article-body, .article__body, [role="main"]');
1446      if (!$anchor.length) $anchor = $list;
1447      var rect = $anchor[0].getBoundingClientRect();
1448      var right = rect.right - 16;
1449      $topBtn.css({
1450        position: 'fixed',
1451        bottom: '24px',
1452        top: 'auto',
1453        left: (right - $topBtn.outerWidth()) + 'px',
1454        right: 'auto'
1455      });
1456    }
1457
1458    function positionTopBtnAbsolute() {
1459      // Park just below the last entry
1460      $topBtn.css({
1461        position: 'absolute',
1462        bottom: '0',
1463        top: 'auto',
1464        left: 'auto',
1465        right: '8px'
1466      });
1467    }
1468
1469    // ─── Scroll: sticky shadow + back-to-top + scroll-spy ───
1470
1471    $(window).on('scroll', function() {
1472      var st = $(window).scrollTop();
1473      $controls.toggleClass('bs-gl-scrolled', st > 20);
1474
1475      // Back to top with brake
1476      if (st > 400) {
1477        $topBtn.addClass('bs-gl-visible');
1478
1479        // Check if fixed position would overflow past parking zone
1480        var listBottom = $list.offset().top + $list.outerHeight();
1481        var winH = $(window).height();
1482        var fixedBtnBottom = st + winH - 24; // where btn bottom edge would be if fixed
1483
1484        if (fixedBtnBottom > listBottom - 14) {
1485          // Past content: park in padding zone (absolute)
1486          positionTopBtnAbsolute();
1487        } else {
1488          // Inside content: float (fixed)
1489          positionTopBtnFixed();
1490        }
1491      } else {
1492        $topBtn.removeClass('bs-gl-visible');
1493      }
1494
1495      var cur = '';
1496      $letters.not('.' + H).each(function() {
1497        if ($(this).offset().top - 180 <= st) cur = this.id.replace('letter-', '');
1498      });
1499      $alpha.find('a').removeClass('bs-gl-on');
1500      if (cur) $alpha.find('a[href="#letter-' + cur + '"]').addClass('bs-gl-on');
1501    });
1502
1503    $(window).on('resize', function() {
1504      if ($topBtn.hasClass('bs-gl-visible')) {
1505        $(window).trigger('scroll'); // recalculate position
1506      }
1507    });
1508
1509    $topBtn.on('click', function() {
1510      $('html, body').animate({ scrollTop: 0 }, 400);
1511    });
1512
1513    // ─── Init ───
1514
1515    // Assign IDs to entries
1516    $entries.each(function() {
1517      var t = ($(this).data('term') || '') + '';
1518      var slug = t.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-$/, '');
1519      if (slug) this.id = 'term-' + slug;
1520    });
1521
1522    buildAlpha();
1523    updateSeps();
1524
1525    // Deep link
1526    if (location.hash && location.hash.indexOf('#term-') === 0) {
1527      var $t = $(location.hash);
1528      if ($t.length) {
1529        setTimeout(function() {
1530          $('html, body').animate({ scrollTop: $t.offset().top - 150 }, 300);
1531          $t.css('background', '#EBF2FF');
1532          setTimeout(function() { $t.css('transition', 'background 1s').css('background', ''); }, 100);
1533        }, 400);
1534      }
1535    }
1536  })();
1537
1538
1539/* ==========================================================================
1540   FAQs and troubleshooting — back-to-top with brake
1541   Append to bottom of existing script.js.
1542   ========================================================================== */
1543(function () {
1544  var $topBtn = $('#bsFaqTop');
1545  var $page   = $('#bs-faq-page');
1546
1547  if (!$topBtn.length || !$page.length) return;
1548
1549  $page.css('position', 'relative').append($topBtn);
1550
1551  function positionFixed() {
1552    var $anchor = $page.closest('.article-body, .article__body, [role="main"]');
1553    if (!$anchor.length) $anchor = $page;
1554    var rect  = $anchor[0].getBoundingClientRect();
1555    var right = rect.right - 16;
1556    $topBtn.css({
1557      position: 'fixed',
1558      bottom:   '24px',
1559      top:      'auto',
1560      left:     (right - $topBtn.outerWidth()) + 'px',
1561      right:    'auto'
1562    });
1563  }
1564
1565  function positionAbsolute() {
1566    $topBtn.css({
1567      position: 'absolute',
1568      bottom:   '0',
1569      top:      'auto',
1570      left:     'auto',
1571      right:    '8px'
1572    });
1573  }
1574
1575  $(window).on('scroll.bsfaq resize.bsfaq', function () {
1576    var st = $(window).scrollTop();
1577
1578    if (st > 400) {
1579      $topBtn.addClass('bs-gl-visible');
1580
1581      var pageBottom     = $page.offset().top + $page.outerHeight();
1582      var fixedBtnBottom = st + $(window).height() - 24;
1583
1584      if (fixedBtnBottom > pageBottom - 14) {
1585        positionAbsolute();
1586      } else {
1587        positionFixed();
1588      }
1589    } else {
1590      $topBtn.removeClass('bs-gl-visible');
1591    }
1592  });
1593
1594  $topBtn.on('click', function () {
1595    $('html, body').animate({ scrollTop: 0 }, 400);
1596  });
1597}());
1598
1599
1600// =========================================================
1601// MCP Tools Explorer — Script
1602// Created: 07 Mar 2026
1603// Last Updated: 07 Mar 2026
1604//
1605// Changelog:
1606// 07 Mar 2026 — v1. Initial working version.
1607// 07 Mar 2026 — v3. Added fuzzy search, search highlight,
1608//               intro count badge, status range, category
1609//               counts in dropdown, improved filter reset.
1610//
1611// Sheet columns: Tool (A), Description (B), Category (C),
1612//                Type (D), Published (E)
1613// =========================================================
1614
1615(function mcpToolsExplorer() {
1616  var appEl = document.getElementById('mcp-tools-app');
1617  var tbodyEl = document.getElementById('mcp-tbody');
1618  var searchEl = document.getElementById('mcp-search');
1619  var searchClearEl = document.getElementById('mcp-search-clear');
1620  var catFilterEl = document.getElementById('mcp-filter-category');
1621  var typeFilterEl = document.getElementById('mcp-filter-type');
1622  var clearBtn = document.getElementById('mcp-clear');
1623  var countEl = document.getElementById('mcp-count');
1624  var introCountEl = document.getElementById('mcp-intro-count');
1625  var pageSizeEl = document.getElementById('mcp-page-size');
1626  var paginationEl = document.getElementById('mcp-pagination');
1627  var theadCols = document.querySelectorAll('.mcp-table th[data-col]');
1628
1629  if (!appEl || !tbodyEl) return;
1630
1631  var SHEET_ID = (appEl.dataset.sheetId || '').trim();
1632  var TAB_NAME = (appEl.dataset.tab || 'MCP Tools').trim();
1633  var IS_PREVIEW = appEl.dataset.preview === 'true';
1634
1635  var allRows = [];
1636  var filtered = [];
1637  var page = 1;
1638  var pageSize = parseInt(pageSizeEl ? pageSizeEl.value : '25', 10) || 25;
1639  var sortCol = 'tool';
1640  var sortDir = 'asc';
1641
1642  function debounce(fn, ms) {
1643    var timer;
1644    return function() {
1645      clearTimeout(timer);
1646      timer = setTimeout(fn, ms || 300);
1647    };
1648  }
1649
1650  function escapeHtml(str) {
1651    return String(str)
1652      .replace(/&/g, '&amp;')
1653      .replace(/</g, '&lt;')
1654      .replace(/>/g, '&gt;')
1655      .replace(/"/g, '&quot;');
1656  }
1657
1658  function fuzzyMatch(text, term) {
1659    var pattern = term.split('').join('.{0,2}');
1660    return new RegExp(pattern, 'i').test(text);
1661  }
1662
1663  function matchesSearch(row, query) {
1664    if (!query) return true;
1665    var searchText = (row.tool + ' ' + row.description + ' ' + row.category).toLowerCase();
1666    var terms = query.toLowerCase().split(/\s+/).filter(Boolean);
1667    return terms.every(function(term) {
1668      return fuzzyMatch(searchText, term);
1669    });
1670  }
1671
1672  function highlightText(text, query) {
1673    if (!query) return escapeHtml(text);
1674    var escaped = escapeHtml(text);
1675    var terms = query.trim().split(/\s+/).filter(Boolean);
1676    terms.forEach(function(term) {
1677      var safe = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1678      escaped = escaped.replace(new RegExp('(' + safe + ')', 'gi'),
1679        '<span class="mcp-highlight">$1</span>');
1680    });
1681    return escaped;
1682  }
1683
1684  function syncSearchClear() {
1685    if (!searchClearEl) return;
1686    if ((searchEl.value || '').trim()) {
1687      searchClearEl.classList.add('is-visible');
1688    } else {
1689      searchClearEl.classList.remove('is-visible');
1690    }
1691  }
1692
1693  function fetchSheet(cb) {
1694    var cached = sessionStorage.getItem('mcpToolsV3');
1695    if (cached && !IS_PREVIEW) return cb(JSON.parse(cached));
1696
1697    var bust = IS_PREVIEW ? '&_=' + Date.now() : '';
1698    var url = 'https://docs.google.com/spreadsheets/d/' + SHEET_ID +
1699      '/gviz/tq?tqx=out:json&sheet=' + encodeURIComponent(TAB_NAME) + bust;
1700
1701    fetch(url, { cache: 'no-store' })
1702      .then(function(r) { return r.text(); })
1703      .then(function(txt) {
1704        var s = txt.indexOf('{');
1705        var e = txt.lastIndexOf('}') + 1;
1706        var data = JSON.parse(txt.slice(s, e));
1707        var rows = (data.table && data.table.rows) || [];
1708        var tools = [];
1709
1710        for (var i = 0; i < rows.length; i++) {
1711          var c = rows[i].c;
1712          var tool = String(c && c[0] && c[0].v || '').trim();
1713          var desc = String(c && c[1] && c[1].v || '').trim();
1714          var cat = String(c && c[2] && c[2].v || '').trim();
1715          var typ = String(c && c[3] && c[3].v || '').trim();
1716          var pub = String(c && c[4] && c[4].v || '').trim().toLowerCase();
1717          if (tool && pub === 'yes') {
1718            tools.push({ tool: tool, description: desc, category: cat, type: typ });
1719          }
1720        }
1721
1722        if (!IS_PREVIEW) {
1723          sessionStorage.setItem('mcpToolsV3', JSON.stringify(tools));
1724        }
1725        cb(tools);
1726      })
1727      .catch(function(err) {
1728        tbodyEl.innerHTML = '<tr><td colspan="4" style="text-align:center;padding:2rem;color:#dc2626;">' +
1729          'Failed to load tools: ' + err.message + '</td></tr>';
1730      });
1731  }
1732
1733  function populateFilters(rows) {
1734    var cats = {};
1735    var types = {};
1736
1737    for (var i = 0; i < rows.length; i++) {
1738      cats[rows[i].category] = (cats[rows[i].category] || 0) + 1;
1739      types[rows[i].type] = (types[rows[i].type] || 0) + 1;
1740    }
1741
1742    var catKeys = Object.keys(cats).sort();
1743    for (var j = 0; j < catKeys.length; j++) {
1744      var opt = document.createElement('option');
1745      opt.value = catKeys[j];
1746      opt.textContent = catKeys[j] + ' (' + cats[catKeys[j]] + ')';
1747      catFilterEl.appendChild(opt);
1748    }
1749
1750    var typeKeys = Object.keys(types).sort();
1751    for (var k = 0; k < typeKeys.length; k++) {
1752      var opt2 = document.createElement('option');
1753      opt2.value = typeKeys[k];
1754      opt2.textContent = typeKeys[k] + ' (' + types[typeKeys[k]] + ')';
1755      typeFilterEl.appendChild(opt2);
1756    }
1757
1758    if (introCountEl) {
1759      introCountEl.textContent = rows.length + ' tools';
1760    }
1761  }
1762
1763  function applyFilters() {
1764    var query = (searchEl.value || '').trim();
1765    var catVal = catFilterEl.value;
1766    var typeVal = typeFilterEl.value;
1767
1768    filtered = allRows.filter(function(r) {
1769      var matchSearch = matchesSearch(r, query);
1770      var matchCat = !catVal || r.category === catVal;
1771      var matchType = !typeVal || r.type === typeVal;
1772      return matchSearch && matchCat && matchType;
1773    });
1774
1775    filtered.sort(function(a, b) {
1776      var aVal = (a[sortCol] || '').toLowerCase();
1777      var bVal = (b[sortCol] || '').toLowerCase();
1778      if (aVal < bVal) return sortDir === 'asc' ? -1 : 1;
1779      if (aVal > bVal) return sortDir === 'asc' ? 1 : -1;
1780      return 0;
1781    });
1782
1783    page = 1;
1784    render();
1785  }
1786
1787  function render() {
1788    var query = (searchEl.value || '').trim();
1789    var total = filtered.length;
1790    var totalPages = Math.max(1, Math.ceil(total / pageSize));
1791    if (page > totalPages) page = totalPages;
1792
1793    var start = (page - 1) * pageSize;
1794    var end = Math.min(start + pageSize, total);
1795    var pageRows = filtered.slice(start, end);
1796
1797    if (countEl) {
1798      if (total === allRows.length) {
1799        countEl.textContent = total + ' tools';
1800      } else {
1801        countEl.textContent = total + ' of ' + allRows.length + ' tools';
1802      }
1803    }
1804
1805    if (!pageRows.length) {
1806      tbodyEl.innerHTML = '<tr><td colspan="4">' +
1807        '<div class="mcp-empty"><h3>No tools found</h3>' +
1808        '<p>Try adjusting your search or filters.</p></div></td></tr>';
1809    } else {
1810      var html = '';
1811      for (var i = 0; i < pageRows.length; i++) {
1812        var r = pageRows[i];
1813        var badge = r.type === 'Write'
1814          ? '<span class="mcp-badge mcp-badge-write">Write</span>'
1815          : '<span class="mcp-badge mcp-badge-read">Read</span>';
1816
1817        html += '<tr>';
1818        html += '<td><code>' + highlightText(r.tool, query) + '</code></td>';
1819        html += '<td>' + highlightText(r.description, query) + '</td>';
1820        html += '<td>' + highlightText(r.category, query) + '</td>';
1821        html += '<td>' + badge + '</td>';
1822        html += '</tr>';
1823      }
1824      tbodyEl.innerHTML = html;
1825    }
1826
1827    theadCols.forEach(function(th) {
1828      var icon = th.querySelector('.sort-icon');
1829      if (th.dataset.col === sortCol) {
1830        icon.textContent = sortDir === 'asc' ? '\u25B2' : '\u25BC';
1831        icon.classList.add('active');
1832      } else {
1833        icon.textContent = '\u25B2\u25BC';
1834        icon.classList.remove('active');
1835      }
1836    });
1837
1838    renderPagination(totalPages, total, start + 1, end);
1839  }
1840
1841  function renderPagination(totalPages, total, from, to) {
1842    if (totalPages <= 1) {
1843      paginationEl.innerHTML = '';
1844      return;
1845    }
1846
1847    var html = '';
1848    html += pgBtn('First', 1, page <= 1);
1849    html += pgBtn('Previous', page - 1, page <= 1);
1850
1851    var startPg = Math.max(1, page - 2);
1852    var endPg = Math.min(totalPages, startPg + 4);
1853    if (endPg - startPg < 4) startPg = Math.max(1, endPg - 4);
1854
1855    for (var i = startPg; i <= endPg; i++) {
1856      html += '<button class="mcp-pg-btn' + (i === page ? ' active' : '') + '" data-pg="' + i + '">' + i + '</button>';
1857    }
1858
1859    html += pgBtn('Next', page + 1, page >= totalPages);
1860    html += pgBtn('Last', totalPages, page >= totalPages);
1861    html += '<span class="mcp-pg-info">' + from + '–' + to + ' of ' + total + '</span>';
1862
1863    paginationEl.innerHTML = html;
1864  }
1865
1866  function pgBtn(label, pg, disabled) {
1867    return '<button class="mcp-pg-btn" data-pg="' + pg + '"' +
1868      (disabled ? ' disabled' : '') + '>' + label + '</button>';
1869  }
1870
1871  searchEl.addEventListener('input', debounce(function() {
1872    syncSearchClear();
1873    applyFilters();
1874  }, 300));
1875
1876  if (searchClearEl) {
1877    searchClearEl.addEventListener('click', function() {
1878      searchEl.value = '';
1879      syncSearchClear();
1880      applyFilters();
1881      searchEl.focus();
1882    });
1883  }
1884
1885  catFilterEl.addEventListener('change', applyFilters);
1886  typeFilterEl.addEventListener('change', applyFilters);
1887
1888  pageSizeEl.addEventListener('change', function() {
1889    pageSize = parseInt(pageSizeEl.value, 10) || 25;
1890    if (pageSize > 200) pageSize = allRows.length;
1891    page = 1;
1892    render();
1893  });
1894
1895  clearBtn.addEventListener('click', function() {
1896    searchEl.value = '';
1897    catFilterEl.value = '';
1898    typeFilterEl.value = '';
1899    sortCol = 'tool';
1900    sortDir = 'asc';
1901    if (pageSizeEl) pageSizeEl.value = '10'; // Change from '25' to '10'
1902    pageSize = 10; // Change from 25 to 10
1903    syncSearchClear();
1904    applyFilters();
1905  });
1906
1907  theadCols.forEach(function(th) {
1908    th.addEventListener('click', function() {
1909      var col = th.dataset.col;
1910      if (sortCol === col) {
1911        sortDir = sortDir === 'asc' ? 'desc' : 'asc';
1912      } else {
1913        sortCol = col;
1914        sortDir = 'asc';
1915      }
1916      applyFilters();
1917    });
1918  });
1919
1920  paginationEl.addEventListener('click', function(e) {
1921    var btn = e.target.closest('[data-pg]');
1922    if (!btn || btn.disabled) return;
1923    page = parseInt(btn.dataset.pg, 10);
1924    render();
1925    appEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
1926  });
1927
1928  tbodyEl.innerHTML = '<tr><td colspan="4" class="mcp-loading">Loading tools…</td></tr>';
1929
1930  fetchSheet(function(rows) {
1931    allRows = rows;
1932    populateFilters(rows);
1933    applyFilters();
1934    syncSearchClear();
1935  });
1936})();
1937
1938// ====================== //
1939// Added March 18, 2026: Intercepts same-page target="_blank" links to prevent new tabs and scroll smoothly.
1940// ========================= //
1941
1942document.addEventListener('click', function(e) {
1943  // Find the closest anchor tag that was clicked
1944  const link = e.target.closest('a[target="_blank"]');
1945  if (!link) return;
1946
1947  // SAFETY CHECK: Skip links meant for downloading files.
1948  // Checks for the 'download' attribute OR common file extensions in the URL.
1949  const isDownload = link.hasAttribute('download') || link.pathname.match(/\.(pdf|zip|csv|doc|docx|xls|xlsx)$/i);
1950  if (isDownload) return; // Let the browser handle the download natively
1951
1952  // Get current page URL and the link's URL (ignoring the #hash)
1953  const currentUrl = window.location.origin + window.location.pathname;
1954  const linkUrl = link.origin + link.pathname;
1955
1956  // Check if it is the EXACT same page
1957  if (currentUrl === linkUrl) {
1958    
1959    // Stop the new tab from opening immediately
1960    e.preventDefault(); 
1961
1962    if (link.hash) {
1963      // SCENARIO 1: There IS an anchor tag (e.g., #h_01MULTIPLE...)
1964      const targetElement = document.querySelector(link.hash);
1965      if (targetElement) {
1966        targetElement.scrollIntoView({ behavior: 'smooth' });
1967        history.pushState(null, null, link.hash); // Update the URL bar
1968      }
1969    } else {
1970      // SCENARIO 2: There is NO anchor tag. Just a link to the current page.
1971      // Smoothly scroll the user back to the very top of the page
1972      window.scrollTo({ top: 0, behavior: 'smooth' });
1973      
1974      // Clean up the URL bar to remove any old hashes
1975      history.pushState(null, null, currentUrl); 
1976    }
1977  }
1978});
1979
1980// ====================== //
1981// Added March 18, 2026: Simple collapsible section using details tag
1982// ========================= //
1983
1984document.addEventListener("DOMContentLoaded", function () {
1985  const reportFieldDetails = document.querySelectorAll(".report-fields-details");
1986
1987  reportFieldDetails.forEach((details) => {
1988    const summary = details.querySelector("summary");
1989
1990    function updateSummaryText() {
1991      summary.innerHTML = details.open
1992        ? "<strong>Hide report fields</strong>"
1993        : "<strong>Click to view report fields</strong>";
1994    }
1995
1996    updateSummaryText();
1997    details.addEventListener("toggle", updateSummaryText);
1998  });
1999});
2000
2001// Added - 19 March 2026 - Fix: Reposition instant search dropdown below search bar
2002// Fix: Reposition instant search dropdown below search bar (device agnostic)
2003(function() {
2004  var observer = new MutationObserver(function() {
2005    var dropdown = document.querySelector('zd-autocomplete[role="listbox"]');
2006    if (!dropdown) return;
2007
2008    var input = document.querySelector('.search-box input[type="search"]') 
2009             || document.querySelector('.search-box input[type="text"]');
2010    if (!input) return;
2011
2012    var rect = input.getBoundingClientRect();
2013    var newTop = rect.bottom + window.scrollY;
2014    var availableHeight = window.innerHeight - rect.bottom - 20;
2015
2016    if (parseFloat(dropdown.style.top) < newTop - 10) {
2017      dropdown.style.setProperty('top', newTop + 'px', 'important');
2018      dropdown.style.setProperty('left', rect.left + 'px', 'important');
2019      dropdown.style.setProperty('width', rect.width + 'px', 'important');
2020    }
2021
2022    dropdown.style.setProperty('max-height', availableHeight + 'px', 'important');
2023    dropdown.style.setProperty('overflow-y', 'auto', 'important');
2024  });
2025
2026  observer.observe(document.body, { 
2027    childList: true, 
2028    subtree: true, 
2029    attributes: true, 
2030    attributeFilter: ['style'] 
2031  });
2032})();
2033
2034// Universal doc navigation behavior — simplified 2026-03-29
2035document.querySelectorAll('.doc-nav').forEach((nav) => {
2036  const prev = nav.querySelector('.doc-nav-link.prev');
2037  const next = nav.querySelector('.doc-nav-link.next');
2038
2039  const isEmpty = (link) => {
2040    if (!link) return true;
2041    const href = (link.getAttribute('href') || '').trim();
2042    const text = (link.textContent || '').trim();
2043    return !href || !text;
2044  };
2045
2046  const prevEmpty = isEmpty(prev);
2047  const nextEmpty = isEmpty(next);
2048
2049  if (!prevEmpty && prev) {
2050    prev.setAttribute('aria-label', `Previous: ${prev.textContent.trim()}`);
2051  }
2052
2053  if (!nextEmpty && next) {
2054    next.setAttribute('aria-label', `Next: ${next.textContent.trim()}`);
2055  }
2056
2057  if (prevEmpty && prev) prev.remove();
2058  if (nextEmpty && next) next.remove();
2059
2060  if (prevEmpty && !nextEmpty) {
2061    nav.classList.add('is-next-only');
2062  } else if (!prevEmpty && nextEmpty) {
2063    nav.classList.add('is-prev-only');
2064  } else if (prevEmpty && nextEmpty) {
2065    nav.style.display = 'none';
2066  }
2067});
2068
2069/* ==========================================================================
2070   PROMPT COPY BLOCKS — 31 Mar 2026
2071   Copies .lp-prompt__text content to clipboard on button click.
2072   Uses event delegation — works with any number of prompts.
2073   Icon swap handled by CSS background-image + .is-copied class.
2074   Smooth crossfade: fade out → swap icon → fade in → hold → reverse.
2075   ========================================================================== */
2076document.addEventListener('click', function (e) {
2077  var btn = e.target.closest('.lp-prompt__copy');
2078  if (!btn) return;
2079  var box = btn.closest('.lp-prompt__box');
2080  if (!box) return;
2081  var text = box.querySelector('.lp-prompt__text');
2082  if (!text) return;
2083  navigator.clipboard.writeText(text.textContent.trim()).then(function () {
2084    btn.style.opacity = '0';
2085    setTimeout(function () {
2086      btn.classList.add('is-copied');
2087      btn.setAttribute('aria-label', 'Copied');
2088      btn.style.opacity = '1';
2089    }, 200);
2090    setTimeout(function () {
2091      btn.style.opacity = '0';
2092      setTimeout(function () {
2093        btn.classList.remove('is-copied');
2094        btn.setAttribute('aria-label', 'Copy prompt');
2095        btn.style.opacity = '';
2096      }, 200);
2097    }, 1500);
2098  });
2099});
2100
2101/**
2102 * HC → Ask AI / Markdown
2103 *
2104 * ALL article pages:
2105 *   Ask AI dropdown with Claude + ChatGPT (no loading wait)
2106 *
2107 * MARKDOWN-ENABLED pages (hardcoded list):
2108 *   Same dropdown + Copy Markdown / View as Markdown (waits for full render)
2109 */
2110
2111(function () {
2112  "use strict";
2113
2114  // ━━ CONFIG ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2115  // Pages that also get markdown options
2116  var MARKDOWN_URLS = [
2117    "/hc/en-us/articles/49713147943187-The-Blueshift-MCP-Server-Beta",
2118    "/hc/en-us/articles/49714422787347-Blueshift-MCP-Server-tools"
2119  ];
2120
2121  var QUIET_PERIOD = 3000;
2122
2123  var AI_PROMPTS = {
2124    claude: "Hi Claude! Can you please read [this page]({url}) and prepare to answer questions about it?",
2125    chatgpt: "Hi ChatGPT! Can you please read [this page]({url}) and prepare to answer questions about it?"
2126  };
2127
2128  // ━━ PAGE DETECTION ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2129  var articleBody = document.querySelector(
2130    ".article-body, article.article-body, .article__body"
2131  );
2132  if (!articleBody) return; // Not an article page
2133
2134  var currentPath = window.location.pathname;
2135  var hasMarkdown = MARKDOWN_URLS.some(function (url) {
2136    return currentPath.indexOf(url) !== -1;
2137  });
2138
2139  // ━━ STATE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2140  var turndownLoaded = false;
2141  var markdownReady = false;
2142
2143  // ━━ LOAD TURNDOWN (only if markdown enabled) ━━━━━━━━━━━━━━━━━━
2144  function loadTurndown(cb) {
2145    if (window.TurndownService) { turndownLoaded = true; return cb && cb(); }
2146    var s = document.createElement("script");
2147    s.src = "https://cdnjs.cloudflare.com/ajax/libs/turndown/7.2.0/turndown.min.js";
2148    s.onload = function () { turndownLoaded = true; if (cb) cb(); };
2149    document.head.appendChild(s);
2150  }
2151
2152  // ━━ EXTRACT + CONVERT ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2153  function getArticleTitle() {
2154    var el = document.querySelector(
2155      ".article-title, .article__title, h1.article-header"
2156    );
2157    return el ? el.textContent.trim() : document.title.split("–")[0].trim();
2158  }
2159
2160  function getArticleHTML() {
2161    var clone = articleBody.cloneNode(true);
2162    [
2163      "script", "style", "noscript",
2164      ".article-votes", ".article-comments", ".article-footer",
2165      ".article-sidebar", ".article-relatives", ".article-attachments",
2166      "[data-purpose='vote']", ".share-links", ".article-subscribe",
2167      "#hc-md-wrap"
2168    ].forEach(function (sel) {
2169      clone.querySelectorAll(sel).forEach(function (el) { el.remove(); });
2170    });
2171    return clone.innerHTML;
2172  }
2173
2174  function toMarkdown() {
2175    var html = getArticleHTML();
2176    if (!html) return null;
2177
2178    var td = new window.TurndownService({
2179      headingStyle: "atx",
2180      codeBlockStyle: "fenced",
2181      bulletListMarker: "-",
2182      emDelimiter: "*",
2183      strongDelimiter: "**",
2184      linkStyle: "inlined"
2185    });
2186
2187    td.addRule("callouts", {
2188      filter: function (node) {
2189        return node.nodeName === "DIV" &&
2190          /callout|note|tip|warning|info|important/i.test(node.className);
2191      },
2192      replacement: function (content) {
2193        return "\n> " + content.trim().replace(/\n/g, "\n> ") + "\n";
2194      }
2195    });
2196
2197    td.addRule("details", {
2198      filter: ["details"],
2199      replacement: function (content, node) {
2200        var summary = node.querySelector("summary");
2201        var title = summary ? summary.textContent.trim() : "Details";
2202        var body = content.replace(title, "").trim();
2203        return "\n**" + title + "**\n\n" + body + "\n";
2204      }
2205    });
2206
2207    var md = td.turndown(html);
2208    md = md.replace(/\n{3,}/g, "\n\n").trim();
2209    return "# " + getArticleTitle() + "\n\n" + md;
2210  }
2211
2212  function withTurndown(fn) {
2213    if (turndownLoaded) return fn();
2214    loadTurndown(fn);
2215  }
2216
2217  // ━━ TOAST ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2218  function showToast(message) {
2219    var existing = document.getElementById("hc-md-toast");
2220    if (existing) existing.remove();
2221
2222    var toast = document.createElement("div");
2223    toast.id = "hc-md-toast";
2224    toast.textContent = message;
2225    document.body.appendChild(toast);
2226
2227    setTimeout(function () { toast.classList.add("hc-md-toast-show"); }, 10);
2228    setTimeout(function () {
2229      toast.classList.remove("hc-md-toast-show");
2230      setTimeout(function () { toast.remove(); }, 300);
2231    }, 2500);
2232  }
2233
2234  // ━━ ACTIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2235  function openAI(platform, baseUrl, paramName) {
2236    var prompt = AI_PROMPTS[platform].replace("{url}", window.location.href);
2237    var chatUrl = baseUrl + "?" + paramName + "=" + encodeURIComponent(prompt);
2238    var tab = window.open(chatUrl, "_blank");
2239    if (!tab) showToast("Pop-up blocked — please allow pop-ups for this site");
2240  }
2241
2242  function doCopy() {
2243    withTurndown(function () {
2244      var md = toMarkdown();
2245      if (!md) return;
2246      navigator.clipboard.writeText(md).then(function () {
2247        showToast("Markdown copied to clipboard");
2248      });
2249    });
2250  }
2251
2252  function doView() {
2253    withTurndown(function () {
2254      var md = toMarkdown();
2255      if (!md) return;
2256
2257      var title = getArticleTitle();
2258      var tokens = Math.ceil(md.length / 4);
2259
2260      var html = [
2261        "<!DOCTYPE html><html lang='en'><head><meta charset='utf-8'>",
2262        "<title>" + title + " — Markdown</title>",
2263        "<style>",
2264        "* { margin:0; padding:0; box-sizing:border-box; }",
2265        "body { font-family:'SF Mono','Fira Code','Consolas',monospace; background:#f8f9fa; color:#2d3748; }",
2266        ".bar { position:sticky; top:0; z-index:10; background:#fff; border-bottom:1px solid #e2e8f0; padding:12px 24px; display:flex; align-items:center; gap:12px; }",
2267        ".bar-title { font-size:13px; font-weight:600;
2267 color:#1a202c; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }",
2268        ".bar-meta { font-size:11px; color:#a0aec0; white-space:nowrap; }",
2269        ".bar-btn { padding:6px 14px; font-size:12px; font-weight:600; font-family:inherit; border-radius:6px; cursor:pointer; border:1px solid #3182ce; background:#3182ce; color:#fff; }",
2270        ".bar-btn:hover { background:#2b6cb0; }",
2271        "pre { padding:24px; font-size:13px; line-height:1.7; white-space:pre-wrap; word-break:break-word; min-height:calc(100vh - 50px); }",
2272        "</style></head><body>",
2273        "<div class='bar'>",
2274        "<div class='bar-title'>" + title + "</div>",
2275        "<div class='bar-meta'>~" + tokens.toLocaleString() + " tokens &middot; " + md.length.toLocaleString() + " chars</div>",
2276        "<button class='bar-btn' onclick='copyMd()'>Copy</button>",
2277        "</div>",
2278        "<pre id='md'></pre>",
2279        "<script>function copyMd(){navigator.clipboard.writeText(document.getElementById(\"md\").textContent).then(function(){var b=document.querySelector(\".bar-btn\");b.textContent=\"\\u2713 Copied\";setTimeout(function(){b.textContent=\"Copy\"},2000)})}<\/script>",
2280        "</body></html>"
2281      ].join("\n");
2282
2283      var tab = window.open("", "_blank");
2284      if (!tab) { showToast("Pop-up blocked — please allow pop-ups"); return; }
2285      tab.document.write(html);
2286      tab.document.getElementById("md").textContent = md;
2287      tab.document.close();
2288    });
2289  }
2290
2291  // ━━ ICONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2292  var ICONS = {
2293    sparkle: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M9.5 2l1.5 4.5L15.5 8l-4.5 1.5L9.5 14l-1.5-4.5L3.5 8l4.5-1.5zm7 8l1 3 3 1-3 1-1 3-1-3-3-1 3-1z"/></svg>',
2294    claude: '<svg width="14" height="14" viewBox="0 0 100 100" fill="currentColor"><path d="m19.6 66.5 19.7-11 .3-1-.3-.5h-1l-3.3-.2-11.2-.3L14 53l-9.5-.5-2.4-.5L0 49l.2-1.5 2-1.3 2.9.2 6.3.5 9.5.6 6.9.4L38 49.1h1.6l.2-.7-.5-.4-.4-.4L29 41l-10.6-7-5.6-4.1-3-2-1.5-2-.6-4.2 2.7-3 3.7.3.9.2 3.7 2.9 8 6.1L37 36l1.5 1.2.6-.4.1-.3-.7-1.1L33 25l-6-10.4-2.7-4.3-.7-2.6c-.3-1-.4-2-.4-3l3-4.2L28 0l4.2.6L33.8 2l2.6 6 4.1 9.3L47 29.9l2 3.8 1 3.4.3 1h.7v-.5l.5-7.2 1-8.7 1-11.2.3-3.2 1.6-3.8 3-2L61 2.6l2 2.9-.3 1.8-1.1 7.7L59 27.1l-1.5 8.2h.9l1-1.1 4.1-5.4 6.9-8.6 3-3.5L77 13l2.3-1.8h4.3l3.1 4.7-1.4 4.9-4.4 5.6-3.7 4.7-5.3 7.1-3.2 5.7.3.4h.7l12-2.6 6.4-1.1 7.6-1.3 3.5 1.6.4 1.6-1.4 3.4-8.2 2-9.6 2-14.3 3.3-.2.1.2.3 6.4.6 2.8.2h6.8l12.6 1 3.3 2 1.9 2.7-.3 2-5.1 2.6-6.8-1.6-16-3.8-5.4-1.3h-.8v.4l4.6 4.5 8.3 7.5L89 80.1l.5 2.4-1.3 2-1.4-.2-9.2-7-3.6-3-8-6.8h-.5v.7l1.8 2.7 9.8 14.7.5 4.5-.7 1.4-2.6 1-2.7-.6-5.8-8-6-9-4.7-8.2-.5.4-2.9 30.2-1.3 1.5-3 1.2-2.5-2-1.4-3 1.4-6.2 1.6-8 1.3-6.4 1.2-7.9.7-2.6v-.2H49L43 72l-9 12.3-7.2 7.6-1.7.7-3-1.5.3-2.8L24 86l10-12.8 6-7.9 4-4.6-.1-.5h-.3L17.2 77.4l-4.7.6-2-2 .2-3 1-1 8-5.5Z"/></svg>',
2295    chatgpt: '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm
22951.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg>',
2296    copy: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>',
2297    md: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M7 15V9l3 3 3-3v6"/><path d="M17 9v6m0-6l-2 3m2-3l2 3"/></svg>',
2298    spinner: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#9ca3af" stroke-width="2" class="hc-md-spin"><path d="M12 2v4m0 12v4m-10-10h4m12 0h4m-3.5-7.5l-2.8 2.8m-7.4 7.4l-2.8 2.8m0-13l2.8 2.8m7.4 7.4l2.8 2.8"/></svg>',
2299    ext: '<svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M5 1h6v6M11 1L4.5 7.5"/></svg>',
2300    chev: '<svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 5l3 3 3-3"/></svg>'
2301  };
2302
2303  // ━━ STYLES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2304  function injectStyles() {
2305    var style = document.createElement("style");
2306    style.textContent = [
2307      "#hc-md-wrap { position:relative; display:inline-block; margin:0 0 20px 0; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; }",
2308
2309      "#hc-md-btn { display:inline-flex; align-items:center; gap:6px; padding:8px 14px; font-size:13px; font-weight:600; background:#fff; color:#374151; border:1px solid #d1d5db; border-radius:8px; cursor:pointer; font-family:inherit; transition:all 0.15s; }",
2310      "#hc-md-btn:hover { border-color:#9ca3af; background:#f9fafb; }",
2311
2312      "#hc-md-menu { display:none; position:absolute; top:calc(100% + 4px); left:0; min-width:180px; background:#fff; border:1px solid #e5e7eb; border-radius:10px; box-shadow:0 4px 16px rgba(0,0,0,0.08),0 1px 3px rgba(0,0,0,0.06); z-index:9999; padding:4px 0; }",
2313      "#hc-md-menu.open { display:block; }",
2314
2315      ".hc-md-sep { height:1px; background:#f3f4f6; margin:4px 0; }",
2316
2317      ".hc-md-item { display:flex; align-items:center; gap:10px; padding:9px 14px; font-size:13px; font-weight:500; color:#374151; cursor:pointer; border:none; background:none; width:100%; text-align:left; font-family:inherit; transition:background 0.1s; }",
2318      ".hc-md-item:hover { background:#f3f4f6; }",
2319      ".hc-md-item:disabled { opacity:0.35; cursor:not-allowed; }",
2320      ".hc-md-item:disabled:hover { background:none; }",
2321      ".hc-md-item .ic { display:flex; align-items:center; color:#6b7280; }",
2322      ".hc-md-item .lb { flex:1; }",
2323      ".hc-md-item .ex { display:flex; align-items:center; color:#9ca3af; }",
2324
2325      "@keyframes hc-md-spin { to { transform:rotate(360deg); } }",
2326      ".hc-md-spin { animation:hc-md-spin 1s linear infinite; }",
2327
2328      "#hc-md-toast { position:fixed; bottom:24px; left:50%; transform:translateX(-50%); padding:10px 20px; font-size:13px; font-weight:500; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; background:#1f2937; color:#fff; border-radius:8px; box-shadow:0 4px 12px rgba(0,0,0,0.15); opacity:0; transition:opacity 0.3s; z-index:99999; }",
2329      "#hc-md-toast.hc-md-toast-show { opacity:1; }"
2330    ].join("\n");
2331    document.head.appendChild(style);
2332  }
2333
2334  // ━━ BUILD DROPDOWN ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2335  function injectDropdown() {
2336    var wrap = document.createElement("div");
2337    wrap.id = "hc-md-wrap";
2338
2339    // Trigger button — always enabled (Ask AI works immediately)
2340    var btn = document.createElement("button");
2341    btn.id = "hc-md-btn";
2342    btn.innerHTML = ICONS.sparkle + " Ask AI " + ICONS.chev;
2343
2344    // Menu
2345    var menu = document.createElement("div");
2346    menu.id = "hc-md-menu";
2347
2348    // AI options (always present, always enabled)
2349    var menuHTML =
2350      '<button class="hc-md-item" data-action="claude">' +
2351        '<span class="ic">' + ICONS.claude + '</span>' +
2352        '<span class="lb">Claude</span>' +
2353        '<span class="ex">' + ICONS.ext + '</span>' +
2354      '</button>' +
2355      '<button class="hc-md-item" data-action="chatgpt">' +
2356        '<span class="ic">' + ICONS.chatgpt + '</span>' +
2357        '<span class="lb">ChatGPT</span>' +
2358        '<span class="ex">' + ICONS.ext + '</span>' +
2359      '</button>';
2360
2361    // Markdown options (only on enabled pages, start disabled with spinner)
2362    if (hasMarkdown) {
2363      menuHTML +=
2364        '<div class="hc-md-sep"></div>' +
2365        '<button class="hc-md-item hc-md-needs-load" data-action="copy" disabled>' +
2366          '<span class="ic">' + ICONS.spinner + '</span>' +
2367          '<span class="lb">Copy Markdown</span>' +
2368        '</button>' +
2369        '<button class="hc-md-item hc-md-needs-load" data-action="view" disabled>' +
2370          '<span class="ic">' + ICONS.spinner + '</span>' +
2371          '<span class="lb">View as Markdown</span>' +
2372        '</button>';
2373    }
2374
2375    menu.innerHTML = menuHTML;
2376
2377    wrap.appendChild(btn);
2378    wrap.appendChild(menu);
2379
2380    articleBody.parentNode.insertBefore(wrap, articleBody);
2381
2382    // Toggle menu
2383    btn.addEventListener("click", function (e) {
2384      e.stopPropagation();
2385      menu.classList.toggle("open");
2386    });
2387
2388    // Close on outside click
2389    document.addEventListener("click", function () {
2390      menu.classList.remove("open");
2391    });
2392
2393    // Menu item clicks
2394    menu.addEventListener("click", function (e) {
2395      e.stopPropagation();
2396      var item = e.target.closest(".hc-md-item");
2397      if (!item || item.disabled) return;
2398
2399      menu.classList.remove("open");
2400
2401      switch (item.dataset.action) {
2402        case "claude":
2403          openAI("claude", "https://claude.ai/new", "q");
2404          break;
2405        case "chatgpt":
2406          openAI("chatgpt", "https://chatgpt.com", "prompt");
2407          break;
2408        case "copy":
2409          doCopy();
2410          break;
2411        case "view":
2412          doView();
2413          break;
2414      }
2415    });
2416  }
2417
2418  // ━━ ENABLE MARKDOWN OPTIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2419  function enableMarkdown() {
2420    markdownReady = true;
2421
2422    var items = document.querySelectorAll(".hc-md-needs-load");
2423    items.forEach(function (item) {
2424      item.disabled = false;
2425      var ic = item.querySelector(".ic");
2426      if (item.dataset.action === "copy") ic.innerHTML = ICONS.copy;
2427      if (item.dataset.action === "view") ic.innerHTML = ICONS.md;
2428    });
2429  }
2430
2431  // ━━ WAIT FOR FULL RENDER (markdown pages only) ━━━━━━━━━━━━━━━
2432  function waitForReady() {
2433    var timer = null;
2434    var observer = new MutationObserver(function () {
2435      clearTimeout(timer);
2436      timer = setTimeout(function () {
2437        observer.disconnect();
2438        enableMarkdown();
2439      }, QUIET_PERIOD);
2440    });
2441
2442    observer.observe(articleBody, {
2443      childList: true, subtree: true,
2444      characterData: true, attributes: true
2445    });
2446
2447    timer = setTimeout(function () {
2448      observer.disconnect();
2449      enableMarkdown();
2450    }, QUIET_PERIOD);
2451  }
2452
2453  // ━━ INIT ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2454  function init() {
2455    injectStyles();
2456    injectDropdown();
2457
2458    // Only load Turndown + wait for render on markdown-enabled pages
2459    if (hasMarkdown) {
2460      loadTurndown(null);
2461
2462      if (document.readyState === "complete") {
2463        waitForReady();
2464      } else {
2465        window.addEventListener("load", waitForReady);
2466      }
2467    }
2468  }
2469
2470  if (document.readyState === "loading") {
2471    document.addEventListener("DOMContentLoaded", init);
2472  } else {
2473    init();
2474  }
2475})();
2476
2477/* =========================================================
2478   Feed Recipe Callout
2479   Renders feed recipes on any page with the feed containers
2480   ========================================================= */
2481
2482(function feedRecipeCallout() {
2483  const userEl = document.getElementById('feed-user-recipes');
2484  const productEl = document.getElementById('feed-product-recipes');
2485
2486  // Modal hooks
2487  const modalOverlay = document.getElementById('details-modal');
2488  const modalTitle = document.getElementById('modal-title');
2489  const modalContentArea = document.getElementById('modal-content-area');
2490  const modalCloseBtn = modalOverlay?.querySelector('.modal-close-btn');
2491  const modalTagVertical = document.getElementById('modal-tag-vertical');
2492  const modalTagLifecycle = document.getElementById('modal-tag-lifecycle');
2493
2494  if (!userEl && !productEl) return;
2495
2496  const SHEET_ID = '15bhdBYVol09wC9air6kU-HfdDbf-_bSNzJWJJJlFfTQ';
2497  const TAB_NAME = 'All_Recipes';
2498  const PAGE_SIZE = 10;
2499
2500  let allRows = [];
2501
2502  function parseGviz(txt) {
2503    const start = txt.indexOf('{');
2504    const end = txt.lastIndexOf('}') + 1;
2505    return JSON.parse(txt.slice(start, end));
2506  }
2507
2508  function getSafeString(val) {
2509    return String(val || '').trim();
2510  }
2511
2512  function escapeHtml(str) {
2513    return String(str).replace(/[&<>"']/g, m => ({
2514      '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
2515    }[m]));
2516  }
2517
2518  function toSentenceCase(str) {
2519    if (!str) return '';
2520    return str.charAt(0).toUpperCase() + str.slice(1);
2521  }
2522
2523  function parseList(text) {
2524    if (!text) return null;
2525    const splitPattern = /(?=\d+\.\s+|[-•*]\s+)/g;
2526    const items = text.split(splitPattern)
2527      .map(item => item.replace(/^(\d+\.|[-•*])\s*/, '').trim())
2528      .filter(Boolean);
2529    return items.length >= 2 ? items : null;
2530  }
2531
2532  function tryFormatJSON(text) {
2533    try {
2534      const parsed = JSON.parse(text);
2535      return JSON.stringify(parsed, null, 2);
2536    } catch (e) {
2537      return text
2538        .replace(/\s*,\s*/g, ',\n  ')
2539        .replace(/\{\s*/g, '{\n  ')
2540        .replace(/\s*\}/g, '\n}')
2541        .replace(/:\s*/g, ': ');
2542    }
2543  }
2544
2545  function isPureCode(text) {
2546    if (!text) return false;
2547    const trimmed = text.trim();
2548    return (
2549      (trimmed.startsWith('{') && trimmed.endsWith('}')) ||
2550      (trimmed.startsWith('[') && trimmed.endsWith(']'))
2551    );
2552  }
2553
2554  function formatText(text, isDescription = false) {
2555    if (!text) return '';
2556
2557    const ensurePeriod = (str) => {
2558      const trimmed = str.trim();
2559      return /[.!?;]$/.test(trimmed) ? trimmed : trimmed + '.';
2560    };
2561
2562    if (isDescription) {
2563      const jsonMatch = text.match(/\{[^}]+\}/s);
2564      if (jsonMatch) {
2565        const jsonPart = jsonMatch[0];
2566        const beforeJSON = text.substring(0, jsonMatch.index).trim();
2567        const afterJSON = text.substring(jsonMatch.index + jsonPart.length).trim();
2568
2569        let html = '';
2570        if (beforeJSON) html += `<p>${escapeHtml(ensurePeriod(beforeJSON))}</p>`;
2571        html += `<pre><code>${escapeHtml(tryFormatJSON(jsonPart))}</code></pre>`;
2572        if (afterJSON) html += `<p>${escapeHtml(ensurePeriod(afterJSON))}</p>`;
2573        return html;
2574      }
2575      return `<p>${escapeHtml(ensurePeriod(text))}</p>`;
2576    }
2577
2578    const listItems = parseList(text);
2579    if (listItems) {
2580      return `<ol>${listItems.map(item => `<li>${escapeHtml(ensurePeriod(item))}</li>`).join('')}</ol>`;
2581    }
2582
2583    const singleItemPattern = /^(\d+\.|[-•*])\s+(.+)$/s;
2584    const singleMatch = text.match(singleItemPattern);
2585    if (singleMatch) {
2586      return `<p>${escapeHtml(ensurePeriod(singleMatch[2]))}</p>`;
2587    }
2588
2589    if (isPureCode(text)) {
2590      return `<pre><code>${escapeHtml(tryFormatJSON(text))}</code></pre>`;
2591    }
2592
2593    return `<p>${escapeHtml(ensurePeriod(text))}</p>`;
2594  }
2595
2596  function normalizeRow(row) {
2597    return {
2598      id: getSafeString(row.c?.[0]?.v),
2599      title: getSafeString(row.c?.[1]?.v),
2600      shortDesc: getSafeString(row.c?.[2]?.v),
2601      fullDesc: getSafeString(row.c?.[3]?.v),
2602      requirements: getSafeString(row.c?.[4]?.v),
2603      inputs: getSafeString(row.c?.[5]?.v),
2604      useCases: getSafeString(row.c?.[6]?.v),
2605      vertical: getSafeString(row.c?.[7]?.v),
2606      lifeCycle: getSafeString(row.c?.[8]?.v),
2607      priority: Number(row.c?.[9]?.v) || 5,
2608      publish: getSafeString(row.c?.[10]?.v).toLowerCase(),
2609      slug: getSafeString(row.c?.[14]?.v),
2610      feedType: getSafeString(row.c?.[15]?.v).toLowerCase()
2611    };
2612  }
2613
2614  function showModal(recipe) {
2615    if (!modalOverlay || !modalTitle || !modalContentArea) return;
2616
2617    const sections = [
2618      { title: 'Description', content: recipe.fullDesc, isDescription: true },
2619      { title: 'Requirements', content: recipe.requirements },
2620      { title: 'Input / Configuration', content: recipe.inputs },
2621      { title: 'Suggested Use Cases', content: recipe.useCases }
2622    ];
2623
2624    const sectionsHtml = sections
2625      .filter(section => section.content && formatText(section.content, section.isDescription))
2626      .map(section => `
2627        <div class="modal-section">
2628          <h3>${section.title}</h3>
2629          ${formatText(section.content, section.isDescription)}
2630        </div>
2631      `)
2632      .join('');
2633
2634    modalTitle.textContent = toSentenceCase(recipe.title);
2635    modalContentArea.innerHTML = sectionsHtml || '<p>Recipe details are unavailable.</p>';
2636
2637    if (modalTagLifecycle) {
2638      modalTagLifecycle.textContent = toSentenceCase(recipe.lifeCycle || '');
2639      modalTagLifecycle.style.display = recipe.lifeCycle ? 'inline-block' : 'none';
2640    }
2641
2642    if (modalTagVertical) {
2643      modalTagVertical.textContent = toSentenceCase(recipe.vertical || '');
2644      modalTagVertical.style.display = recipe.vertical ? 'inline-block' : 'none';
2645    }
2646
2647    modalOverlay.classList.remove('hidden');
2648    document.body.style.overflow = 'hidden';
2649  }
2650
2651  function hideModal() {
2652    if (!modalOverlay) return;
2653    modalOverlay.classList.add('hidden');
2654    document.body.style.overflow = '';
2655  }
2656
2657  if (modalCloseBtn) {
2658    modalCloseBtn.addEventListener('click', hideModal);
2659  }
2660
2661  if (modalOverlay) {
2662    modalOverlay.addEventListener('click', (e) => {
2663      if (e.target === modalOverlay) hideModal();
2664    });
2665  }
2666
2667  function renderFeedRecipeList(list, container) {
2668  if (!container) return;
2669
2670  if (!list.length) {
2671    container.innerHTML = '<span>No recipes available.</span>';
2672    return;
2673  }
2674
2675  let currentPage = 1;
2676  const totalPages = Math.max(1, Math.ceil(list.length / PAGE_SIZE));
2677
2678  function renderPage() {
2679    const start = (currentPage - 1) * PAGE_SIZE;
2680    const pageItems = list.slice(start, start + PAGE_SIZE);
2681
2682    const itemsHtml = `
2683  <ul class="feed-recipe-items">
2684    ${pageItems.map(recipe => `
2685      <li>
2686        <button 
2687          type="button" 
2688          class="feed-recipe-link" 
2689          data-recipe-id="${escapeHtml(recipe.id)}"
2690          title="${escapeHtml(recipe.title)}">
2691          ${escapeHtml(recipe.title)}
2692        </button>
2693      </li>
2694    `).join('')}
2695  </ul>
2696`;
2697
2698    const pagerHtml = totalPages > 1 ? `
2699      <div class="feed-pager">
2700        <button type="button" class="feed-pager-btn" data-feed-prev ${currentPage === 1 ? 'disabled' : ''}>Prev</button>
2701        <span class="feed-pager-status">${currentPage} / ${totalPages}</span>
2702        <button type="button" class="feed-pager-btn" data-feed-next ${currentPage === totalPages ? 'disabled' : ''}>Next</button>
2703      </div>
2704    ` : '';
2705
2706    container.innerHTML = itemsHtml + pagerHtml;
2707
2708    container.querySelectorAll('.feed-recipe-link').forEach(btn => {
2709      btn.addEventListener('click', () => {
2710        const recipe = allRows.find(r => r.id === btn.dataset.recipeId);
2711        if (recipe) showModal(recipe);
2712      });
2713    });
2714
2715    const prevBtn = container.querySelector('[data-feed-prev]');
2716    const nextBtn = container.querySelector('[data-feed-next]');
2717
2718    if (prevBtn) {
2719      prevBtn.addEventListener('click', () => {
2720        if (currentPage > 1) {
2721          currentPage -= 1;
2722          renderPage();
2723        }
2724      });
2725    }
2726
2727    if (nextBtn) {
2728      nextBtn.addEventListener('click', () => {
2729        if (currentPage < totalPages) {
2730          currentPage += 1;
2731          renderPage();
2732        }
2733      });
2734    }
2735  }
2736
2737  renderPage();
2738}
2739
2740  function renderFeedRecipeGroups() {
2741    const userProductRecipes = allRows
2742      .filter(r => r.feedType === 'user-product')
2743      .sort((a, b) => a.title.localeCompare(b.title));
2744
2745    const productProductRecipes = allRows
2746      .filter(r => r.feedType === 'product-product')
2747      .sort((a, b) => a.title.localeCompare(b.title));
2748
2749    renderFeedRecipeList(userProductRecipes, userEl);
2750    renderFeedRecipeList(productProductRecipes, productEl);
2751  }
2752
2753  fetch(`https://docs.google.com/spreadsheets/d/${encodeURIComponent(SHEET_ID)}/gviz/tq?tqx=out:json&sheet=${encodeURIComponent(TAB_NAME)}`, { cache: 'no-store' })
2754    .then(res => res.text())
2755    .then(txt => {
2756      const data = parseGviz(txt);
2757      allRows = (data.table?.rows || [])
2758        .map(normalizeRow)
2759        .filter(r => r.title && r.publish === 'yes');
2760
2761      renderFeedRecipeGroups();
2762    })
2763    .catch(() => {
2764      if (userEl) userEl.innerHTML = 'Unable to load';
2765      if (productEl) productEl.innerHTML = 'Unable to load';
2766    });
2767})();
2768
2769
2770// --------------------- Recommendations Studio Sections Reorder ---------------
2771
2772(function () {
2773  var SECTION_ID = '360000298254';
2774
2775  console.log("Reorder Script: Loaded and checking for menu...");
2776
2777  function attemptReorder() {
2778    // Look for the specific menu anywhere on the current page
2779    var parentUl = document.querySelector('ul[data-sidenav-parent-section="' + SECTION_ID + '"]');
2780    
2781    if (!parentUl) {
2782      return false; // Menu isn't on the page yet
2783    }
2784
2785    // Prevent infinite loops if the menu is already reordered
2786    if (parentUl.getAttribute('data-reordered') === 'true') {
2787        return true; 
2788    }
2789
2790    console.log("Reorder Script: Menu found! Starting reorder.");
2791
2792    var DESIRED_ORDER = [
2793      { type: 'article', id: '45868378331667' },  // The Blueshift Recommendations Studio
2794      { type: 'article', id: '45874935379475' },  // Explore Blueshift recommendation recipes
2795      { type: 'article', id: '9063525871891'  },  // How Recommendations Studio works
2796      { type: 'article', id: '115002726694'   },  // Recommendations Studio interface
2797      { type: 'section', id: '50603236413587' },  // --> Build recommendation schemes (MOVED HERE)
2798      { type: 'article', id: '9063463203219'  },  // Add recommendations to messages
2799      { type: 'article', id: '4815071670291'  }   // Measure performance
2800    ];
2801
2802    var itemMap = {};
2803    var listItems = Array.from(parentUl.children);
2804
2805    listItems.forEach(function(li) {
2806      if (li.hasAttribute('data-sidenav-article-id')) {
2807        itemMap['article_' + li.getAttribute('data-sidenav-article-id')] = li;
2808      } else if (li.hasAttribute('data-sidenav-section-id')) {
2809        itemMap['section_' + li.getAttribute('data-sidenav-section-id')] = li;
2810      }
2811    });
2812
2813    // Re-append in the exact order requested
2814    DESIRED_ORDER.forEach(function(item) {
2815      var key = item.type + '_' + item.id;
2816      if (itemMap[key]) {
2817        parentUl.appendChild(itemMap[key]);
2818        delete itemMap[key]; // Mark as moved
2819      }
2820    });
2821
2822    // Append any leftovers (like 'Import recommendations') to the bottom
2823    Object.keys(itemMap).forEach(function(key) {
2824      parentUl.appendChild(itemMap[key]);
2825    });
2826
2827    // Mark as finished so we don't keep reordering it
2828    parentUl.setAttribute('data-reordered', 'true');
2829    console.log("Reorder Script: Success!");
2830    return true; 
2831  }
2832
2833  // Check every 500ms for the menu. Stop checking after 10 seconds.
2834  var attempts = 0;
2835  var checkInterval = setInterval(function() {
2836    attempts++;
2837    if (attemptReorder() || attempts > 20) {
2838      clearInterval(checkInterval);
2839      if (attempts > 20) console.log("Reorder Script: Gave up waiting for menu. Menu not found on this page.");
2840    }
2841  }, 500);
2842})();
2843
2844/* ── Back to top (global) -- updated 15-April-2026 ── */
2845(function() {
2846  var btn = document.getElementById('bsftTopBtn');
2847  if (!btn) return;
2848
2849  if (document.getElementById('bsGlTop')) {
2850    btn.style.display = 'none';
2851    return;
2852  }
2853
2854  function getAnchor() {
2855    return document.querySelector('.article__body') ||
2856           document.querySelector('[role="main"]');
2857  }
2858
2859  function positionFixed() {
2860    var anchor = getAnchor();
2861    if (!anchor) return;
2862    var rect = anchor.getBoundingClientRect();
2863    btn.style.position = 'fixed';
2864    btn.style.bottom = '24px';
2865    btn.style.top = 'auto';
2866    btn.style.left = (rect.right - btn.offsetWidth - 16) + 'px';
2867    btn.style.right = 'auto';
2868  }
2869
2870  function positionAbsolute() {
2871    var anchor = getAnchor();
2872    if (!anchor) return;
2873    anchor.style.position = 'relative';
2874    btn.style.position = 'absolute';
2875    btn.style.bottom = '0';
2876    btn.style.top = 'auto';
2877    btn.style.left = 'auto';
2878    btn.style.right = '8px';
2879    anchor.appendChild(btn);
2880  }
2881
2882  function update() {
2883    var anchor = getAnchor();
2884    if (!anchor) return;
2885    var st = window.scrollY;
2886
2887    if (st > 400) {
2888      btn.classList.add('is-visible');
2889      var anchorBottom = anchor.getBoundingClientRect().bottom + st;
2890      var fixedBtnBottom = st + window.innerHeight - 24;
2891      if (fixedBtnBottom > anchorBottom - 14) {
2892        positionAbsolute();
2893      } else {
2894        positionFixed();
2895      }
2896    } else {
2897      btn.classList.remove('is-visible');
2898    }
2899  }
2900
2901  window.addEventListener('scroll', update);
2902  window.addEventListener('resize', update);
2903
2904  btn.addEventListener('click', function() {
2905    window.scrollTo({ top: 0, behavior: 'smooth' });
2906  });
2907
2908  update();
2909})();
2910
2911/* --------------------- Launchpad x Compass - Move Release notes to top ---------------
2912Updated - 30-June-2026 ------------------- */
2913
2914(function () {
2915  var SECTION_ID = '50363396358419'; // Launchpad x Compass (Beta)
2916  var RELEASE_NOTES_SECTION_ID = '53035039589267'; // Release notes
2917
2918  function attemptReorderLaunchpad() {
2919    var parentUl = document.querySelector(
2920      'ul[data-sidenav-parent-section="' + SECTION_ID + '"]'
2921    );
2922
2923    if (!parentUl) {
2924      return false;
2925    }
2926
2927    if (parentUl.getAttribute('data-launchpad-reordered') === 'true') {
2928      return true;
2929    }
2930
2931    var releaseNotesItem = parentUl.querySelector(
2932      'li[data-sidenav-section-id="' + RELEASE_NOTES_SECTION_ID + '"]'
2933    );
2934
2935    if (!releaseNotesItem) {
2936      return false;
2937    }
2938
2939    // Move Release notes before the direct articles
2940    parentUl.insertBefore(releaseNotesItem, parentUl.firstElementChild);
2941
2942    parentUl.setAttribute('data-launchpad-reordered', 'true');
2943    return true;
2944  }
2945
2946  var attempts = 0;
2947  var checkInterval = setInterval(function () {
2948    attempts++;
2949
2950    if (attemptReorderLaunchpad() || attempts > 20) {
2951      clearInterval(checkInterval);
2952    }
2953  }, 500);
2954})();
2955
2956/* ---------- Launchpad Release Notes: sort section + prev/next nav ----------
2957Updated - 30-June-2026 ------------------- */
2958
2959/* Clubbed together with latest release note load below ------
2960
2961(function () {
2962  var RELEASE_NOTES_SECTION_ID = "53035039589267";
2963
2964  function getLocale() {
2965    var m = window.location.pathname.match(/\/hc\/([^/]+)/);
2966    return m ? m[1] : "en-us";
2967  }
2968  function getCurrentArticleId() {
2969    var m = window.location.pathname.match(/\/articles\/(\d+)/);
2970    return m ? m[1] : null;
2971  }
2972  function isReleaseSectionPage() {
2973    return window.location.pathname.indexOf("/sections/" + RELEASE_NOTES_SECTION_ID) !== -1;
2974  }
2975
2976  function getDateFromTitle(title) {
2977    var months = { january:0, february:1, march:2, april:3, may:4, june:5,
2978      july:6, august:7, september:8, october:9, november:10, december:11 };
2979    var m = title.match(/\b(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2}),\s*(\d{4})\b/i);
2980    if (!m) return null;
2981    return new Date(Number(m[3]), months[m[1].toLowerCase()], Number(m[2]));
2982  }
2983
2984  function fetchAll(url, acc) {
2985    acc = acc || [];
2986    return fetch(url)
2987      .then(function (r) { return r.json(); })
2988      .then(function (data) {
2989        acc = acc.concat(data.articles || []);
2990        return data.next_page ? fetchAll(data.next_page, acc) : acc;
2991      });
2992  }
2993
2994  function getSorted() {
2995    var url = "/api/v2/help_center/" + getLocale() +
2996      "/sections/" + RELEASE_NOTES_SECTION_ID + "/articles.json?per_page=100";
2997    return fetchAll(url).then(function (articles) {
2998      return articles
2999        .filter(function (a) { return getDateFromTitle(a.title); })
3000        .sort(function (a, b) { return getDateFromTitle(b.title) - getDateFromTitle(a.title); });
3001    });
3002  }
3003
3004  function sortSectionPage(articles) {
3005    if (!isReleaseSectionPage()) return;
3006    var list = document.getElementById("article-list") ||
3007               document.querySelector(".article-list");
3008    if (!list) return;
3009    list.innerHTML = "";
3010    articles.forEach(function (a) {
3011      var li = document.createElement("li");
3012      li.className = "article-list__item";
3013      var link = document.createElement("a");
3014      link.className = "article-list__item-link link-black";
3015      link.href = a.html_url;
3016      link.textContent = a.title;
3017      li.appendChild(link);
3018      list.appendChild(li);
3019    });
3020  }
3021
3022  function addBottomNav(articles) {
3023    var id = getCurrentArticleId();
3024    if (!id) return;
3025    var idx = articles.findIndex(function (a) { return String(a.id) === String(id); });
3026    if (idx === -1) return;          // not a release note
3027    if (articles.length <= 1) return; // only one: no nav
3028
3029    var older = articles[idx + 1];   // previous = older
3030    var newer = articles[idx - 1];   // next = newer
3031
3032    var nav = document.createElement("nav");
3033    nav.className = "release-notes-bottom-nav";
3034
3035    var left = document.createElement("div");
3036    left.className = "release-notes-bottom-nav__left";
3037    var right = document.createElement("div");
3038    right.className = "release-notes-bottom-nav__right";
3039
3040    if (older) {
3041      var prev = document.createElement("a");
3042      prev.href = older.html_url;
3043      prev.innerHTML = "← Previous release note<br><span></span>";
3044      prev.querySelector("span").textContent = older.title;
3045      left.appendChild(prev);
3046    }
3047    if (newer) {
3048      var next = document.createElement("a");
3049      next.href = newer.html_url;
3050      next.innerHTML = "Next release note →<br><span></span>";
3051      next.querySelector("span").textContent = newer.title;
3052      right.appendChild(next);
3053    }
3054
3055    nav.appendChild(left);
3056    nav.appendChild(right);
3057
3058    var body = document.querySelector(".article-body") ||
3059               document.querySelector(".article__body") ||
3060               document.querySelector(".article-content") ||
3061               document.querySelector(".article");
3062    if (body) body.appendChild(nav);
3063  }
3064
3065  document.addEventListener("DOMContentLoaded", function () {
3066    getSorted()
3067      .then(function (articles) {
3068        sortSectionPage(articles);
3069        addBottomNav(articles);
3070      })
3071      .catch(function (e) { console.log("Release notes nav error:", e); });
3072  });
3073})();
3074
3075*/
3076
3077// --------------------- Launchpad Release Notes: Sort articles by title date --------------- updated 30-June-2026
3078
3079(function () {
3080  var RELEASE_NOTES_SECTION_ID = "53035039589267";
3081
3082  function getDateFromTitle(title) {
3083    var months = {
3084      january: 0,
3085      february: 1,
3086      march: 2,
3087      april: 3,
3088      may: 4,
3089      june: 5,
3090      july: 6,
3091      august: 7,
3092      september: 8,
3093      october: 9,
3094      november: 10,
3095      december: 11
3096    };
3097
3098    var match = title.match(
3099      /\b(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2}),\s*(\d{4})\b/i
3100    );
3101
3102    if (!match) return 0;
3103
3104    return new Date(
3105      Number(match[3]),
3106      months[match[1].toLowerCase()],
3107      Number(match[2])
3108    ).getTime();
3109  }
3110
3111  function sortListItems(parent) {
3112    if (!parent) return false;
3113
3114    var items = Array.from(parent.children);
3115
3116    items.sort(function (a, b) {
3117      return getDateFromTitle(b.textContent.trim()) - getDateFromTitle(a.textContent.trim());
3118    });
3119
3120    items.forEach(function (item) {
3121      parent.appendChild(item);
3122    });
3123
3124    return true;
3125  }
3126
3127  function sortReleaseNotesSectionPage() {
3128    if (!window.location.pathname.includes("/sections/" + RELEASE_NOTES_SECTION_ID)) {
3129      return;
3130    }
3131
3132    var articleList = document.getElementById("article-list");
3133    sortListItems(articleList);
3134  }
3135
3136  function sortReleaseNotesSidebar() {
3137    var sidebarList = document.querySelector(
3138      'ul[data-sidenav-parent-section="' + RELEASE_NOTES_SECTION_ID + '"]'
3139    );
3140
3141    if (!sidebarList) return false;
3142
3143    if (sidebarList.getAttribute("data-release-notes-sorted") === "true") {
3144      return true;
3145    }
3146
3147    sortListItems(sidebarList);
3148    sidebarList.setAttribute("data-release-notes-sorted", "true");
3149
3150    return true;
3151  }
3152
3153  document.addEventListener("DOMContentLoaded", function () {
3154    // Sort main Release notes section page
3155    sortReleaseNotesSectionPage();
3156
3157    // Sort sidebar after sidenav loads
3158    var attempts = 0;
3159    var interval = setInterval(function () {
3160      attempts++;
3161
3162      if (sortReleaseNotesSidebar() || attempts > 20) {
3163        clearInterval(interval);
3164      }
3165    }, 500);
3166  });
3167})();
3168
3169
3170
3171/* Launchpad Release Notes: sort + prev/next nav + #latest redirect | 01-July-2026 */
3172(function () {
3173  var RELEASE_NOTES_SECTION_ID = "53035039589267";
3174
3175  var WANTS_LATEST = window.location.hash === "#latest" &&
3176      window.location.pathname.indexOf("/sections/" + RELEASE_NOTES_SECTION_ID) !== -1;
3177
3178  /* instantly hide everything + paint a spinner overlay ON <html> (no TOC flash) */
3179  if (WANTS_LATEST) {
3180    var st = document.createElement("style");
3181    st.id = "rn-latest-hide";
3182    st.textContent =
3183      "body{display:none!important;}" +   /* kill the section page immediately */
3184      "html{background:#fff;}" +
3185      "#rn-latest-loader{position:fixed;inset:0;z-index:2147483647;background:#fff;" +
3186        "display:flex;flex-direction:column;align-items:center;justify-content:center;" +
3187        "font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;}" +
3188      "#rn-latest-loader .rn-spin{width:34px;height:34px;border-radius:50%;" +
3189        "border:3px solid #e3e7ee;border-top-color:#2790ff;animation:rnspin .7s linear infinite;}" +
3190      "#rn-latest-loader .rn-msg{margin-top:16px;font-size:14px;color:#4c6072;}" +
3191      "@keyframes rnspin{to{transform:rotate(360deg);}}";
3192    (document.head || document.documentElement).appendChild(st);
3193
3194    // mount spinner directly on <html> so it shows even before <body> exists
3195    var loader = document.createElement("div");
3196    loader.id = "rn-latest-loader";
3197    loader.innerHTML = '<div class="rn-spin"></div>' +
3198                       '<div class="rn-msg">Taking you to the latest release\u2026</div>';
3199    document.documentElement.appendChild(loader);
3200  }
3201
3202  function getLocale() {
3203    var m = window.location.pathname.match(/\/hc\/([^/]+)/);
3204    return m ? m[1] : "en-us";
3205  }
3206  function getCurrentArticleId() {
3207    var m = window.location.pathname.match(/\/articles\/(\d+)/);
3208    return m ? m[1] : null;
3209  }
3210  function isReleaseSectionPage() {
3211    return window.location.pathname.indexOf("/sections/" + RELEASE_NOTES_SECTION_ID) !== -1;
3212  }
3213
3214  function getDateFromTitle(title) {
3215    var months = { january:0, february:1, march:2, april:3, may:4, june:5,
3216      july:6, august:7, september:8, october:9, november:10, december:11 };
3217    var m = title.match(/\b(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2}),\s*(\d{4})\b/i);
3218    if (!m) return null;
3219    return new Date(Number(m[3]), months[m[1].toLowerCase()], Number(m[2]));
3220  }
3221
3222  function fetchAll(url, acc) {
3223    acc = acc || [];
3224    return fetch(url)
3225      .then(function (r) { return r.json(); })
3226      .then(function (data) {
3227        acc = acc.concat(data.articles || []);
3228        return data.next_page ? fetchAll(data.next_page, acc) : acc;
3229      });
3230  }
3231
3232  function getSorted() {
3233    var url = "/api/v2/help_center/" + getLocale() +
3234      "/sections/" + RELEASE_NOTES_SECTION_ID + "/articles.json?per_page=100";
3235    return fetchAll(url).then(function (articles) {
3236      return articles
3237        .filter(function (a) { return getDateFromTitle(a.title); })
3238        .sort(function (a, b) { return getDateFromTitle(b.title) - getDateFromTitle(a.title); });
3239    });
3240  }
3241
3242  /* fast path: single page, sorted by the API, newest first by title date */
3243  function getLatestFast() {
3244    var url = "/api/v2/help_center/" + getLocale() +
3245      "/sections/" + RELEASE_NOTES_SECTION_ID +
3246      "/articles.json?per_page=30&sort_by=created_at&sort_order=desc";
3247    return fetch(url)
3248      .then(function (r) { return r.json(); })
3249      .then(function (data) {
3250        var arts = (data.articles || []).filter(function (a) { return getDateFromTitle(a.title); });
3251        arts.sort(function (a, b) { return getDateFromTitle(b.title) - getDateFromTitle(a.title); });
3252        return arts.length ? arts[0] : null;
3253      });
3254  }
3255
3256  var LATEST_CACHE_KEY = "rn_latest_url_" + RELEASE_NOTES_SECTION_ID;
3257
3258  function revealPage() {
3259    var h = document.getElementById("rn-latest-hide");
3260    if (h) h.remove();
3261    var o = document.getElementById("rn-latest-loader");
3262    if (o) o.remove();
3263  }
3264
3265  /* #latest redirect, cache-first for an instant repeat experience */
3266  function redirectToLatestIfRequested() {
3267    if (!WANTS_LATEST) return false;
3268
3269    // 1) instant path: use cached URL if we have one
3270    var cached = null;
3271    try { cached = window.localStorage.getItem(LATEST_CACHE_KEY); } catch (e) {}
3272
3273    if (cached) {
3274      window.location.replace(cached);           // instant, no API wait
3275      getLatestFast().then(function (a) {         // refresh cache in background
3276        if (a) { try { window.localStorage.setItem(LATEST_CACHE_KEY, a.html_url); } catch (e) {} }
3277      }).catch(function () {});
3278      return true;
3279    }
3280
3281    // first visit: fast single-page fetch, redirect, cache
3282    getLatestFast().then(function (a) {
3283      if (a) {
3284        try { window.localStorage.setItem(LATEST_CACHE_KEY, a.html_url); } catch (e) {}
3285        window.location.replace(a.html_url);
3286      } else {
3287        revealPage();
3288      }
3289    }).catch(function () {
3290      revealPage();
3291    });
3292    return true;
3293  }
3294
3295  function sortSectionPage(articles) {
3296    if (!isReleaseSectionPage()) return;
3297    var list = document.getElementById("article-list") ||
3298               document.querySelector(".article-list");
3299    if (!list) return;
3300    list.innerHTML = "";
3301    articles.forEach(function (a) {
3302      var li = document.createElement("li");
3303      li.className = "article-list__item";
3304      var link = document.createElement("a");
3305      link.className = "article-list__item-link link-black";
3306      link.href = a.html_url;
3307      link.textContent = a.title;
3308      li.appendChild(link);
3309      list.appendChild(li);
3310    });
3311  }
3312
3313  function addBottomNav(articles) {
3314    var id = getCurrentArticleId();
3315    if (!id) return;
3316    var idx = articles.findIndex(function (a) { return String(a.id) === String(id); });
3317    if (idx === -1) return;
3318    if (articles.length <= 1) return;
3319
3320    var older = articles[idx + 1];
3321    var newer = articles[idx - 1];
3322
3323    var nav = document.createElement("nav");
3324    nav.className = "release-notes-bottom-nav";
3325    var left = document.createElement("div");
3326    left.className = "release-notes-bottom-nav__left";
3327    var right = document.createElement("div");
3328    right.className = "release-notes-bottom-nav__right";
3329
3330    if (older) {
3331      var prev = document.createElement("a");
3332      prev.href = older.html_url;
3333      prev.innerHTML = "← Previous release note<br><span></span>";
3334      prev.querySelector("span").textContent = older.title;
3335      left.appendChild(prev);
3336    }
3337    if (newer) {
3338      var next = document.createElement("a");
3339      next.href = newer.html_url;
3340      next.innerHTML = "Next release note →<br><span></span>";
3341      next.querySelector("span").textContent = newer.title;
3342      right.appendChild(next);
3343    }
3344
3345    nav.appendChild(left);
3346    nav.appendChild(right);
3347
3348    var body = document.querySelector(".article-body") ||
3349               document.querySelector(".article__body") ||
3350               document.querySelector(".article-content") ||
3351               document.querySelector(".article");
3352    if (body) body.appendChild(nav);
3353  }
3354
3355  /* start the #latest redirect as early as possible, not waiting for DOMContentLoaded */
3356  if (WANTS_LATEST) {
3357    redirectToLatestIfRequested();
3358  }
3359
3360  document.addEventListener("DOMContentLoaded", function () {
3361    if (WANTS_LATEST) return;  // redirect already in flight
3362
3363    getSorted()
3364      .then(function (articles) {
3365        sortSectionPage(articles);
3366        addBottomNav(articles);
3367      })
3368      .catch(function (e) { console.log("Release notes nav error:", e); });
3369  });
3370})();
3371
3372
3373// --------------------- Advertising: LiveRamp first, rest alphabetical --------------- updated 06-July-2026
3374(function () {
3375  var ADVERTISING_SECTION_ID = "360011629493";
3376  var PINNED_FIRST_TITLE = "liveramp";
3377
3378  function sortKey(title) {
3379    var t = title.trim().toLowerCase();
3380    // Pinned title always sorts first, everything else alphabetical after it
3381    if (t === PINNED_FIRST_TITLE) return "";
3382    return t;
3383  }
3384
3385  function sortListItems(parent) {
3386    if (!parent) return false;
3387    var items = Array.from(parent.children);
3388    items.sort(function (a, b) {
3389      var ka = sortKey(a.textContent.trim());
3390      var kb = sortKey(b.textContent.trim());
3391      if (ka < kb) return -1;
3392      if (ka > kb) return 1;
3393      return 0;
3394    });
3395    items.forEach(function (item) {
3396      parent.appendChild(item);
3397    });
3398    return true;
3399  }
3400
3401  function sortAdvertisingSectionPage() {
3402    if (!window.location.pathname.includes("/sections/" + ADVERTISING_SECTION_ID)) {
3403      return;
3404    }
3405    var articleList = document.getElementById("article-list");
3406    sortListItems(articleList);
3407  }
3408
3409  function sortAdvertisingSidebar() {
3410    var sidebarList = document.querySelector(
3411      'ul[data-sidenav-parent-section="' + ADVERTISING_SECTION_ID + '"]'
3412    );
3413    if (!sidebarList) return false;
3414    if (sidebarList.getAttribute("data-advertising-sorted") === "true") {
3415      return true;
3416    }
3417    sortListItems(sidebarList);
3418    sidebarList.setAttribute("data-advertising-sorted", "true");
3419    return true;
3420  }
3421
3422  document.addEventListener("DOMContentLoaded", function () {
3423    // Sort main Advertising section page
3424    sortAdvertisingSectionPage();
3425    // Sort sidebar after sidenav loads
3426    var attempts = 0;
3427    var interval = setInterval(function () {
3428      attempts++;
3429      if (sortAdvertisingSidebar() || attempts > 20) {
3430        clearInterval(interval);
3431      }
3432    }, 500);
3433  });
3434})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.