da335975e0d5712f47ee0d19b0eb5003d1a1b7f0
lrnassar
  Mon Apr 27 14:16:44 2026 -0700
Add sticky sidebar table of contents to /docs/ pages, ReadTheDocs-style. refs #36894

staticPage.lua: wrap doc body in a Bootstrap row with a col-md-2 sidebar
holding a .docs-toc nav list and a col-md-10 content column. Collect both
lev==1 and lev==2 headings into the TOC and emit id attributes on <h2>
elements (replacing the prior inline <a name=> anchors).

staticPage.html: add a small vanilla-JS scroll-spy that toggles .active
on the TOC <li> matching the section currently scrolled past the top.
No-ops on pages without #docs-toc.

gbStatic.css: add styles for .docs-toc / .docs-toc-row (position: sticky,
flex row for equal-height columns, link styling with active-state border).
All rules are scoped to the .docs-toc class so non-docs pages sharing
gbStatic.css are unaffected.

diff --git docs/staticPage.lua docs/staticPage.lua
index c14e6f33c1b..e0057e7df9c 100644
--- docs/staticPage.lua
+++ docs/staticPage.lua
@@ -124,37 +124,47 @@
     add("     INSTRUCTIONS TO COMMIT THIS PAGE INTO git.")
     add("     Please read the file kent/src/product/Note-To-QA.txt for details.")
     add("     -->")
     add("<!--#set var=\"TITLE\" value=\"" .. metadata["title"] .. "\" -->")
     add("<!--#set var=\"ROOT\" value=\"" .. (variables["ROOT"] or metadata["ROOT"] or "..") .. "\" -->")
     add("")
     add("<!-- Relative paths to support mirror sites with non-standard GB docs install -->")
     add("<!--#include virtual=\"$ROOT/inc/gbPageStart.html\" -->")
     add("<link rel=\"stylesheet\" href=\"<!--#echo var=\"ROOT\" -->/style/bootstrap-3-3-7.min.css\">")
     add("")
     add("<h1>" .. metadata["title"] .. "</h1>")
   else
     add("<h1>No title defined in document, first line must be % mytitle </h1>")
   end
 
+  -- Sidebar TOC + body in Bootstrap grid
+  add('<div class="row docs-toc-row">')
+  add('<div class="col-md-2 hidden-sm hidden-xs">')
+  add('<nav class="docs-toc" id="docs-toc">')
+  add('<ul>')
   for i, h in ipairs(headers) do
-    idStr = simplifyId(h)
-    add("<h6><a href='#" .. idStr .. "'>" .. h .. "</a></h6>")
+    add("<li><a href='#" .. h.id .. "'>" .. h.text .. "</a></li>")
   end
+  add('</ul>')
+  add('</nav>')
+  add('</div>')
+  add('<div class="col-md-10">')
   -- ucsc change end
 
   add(body)
+  add('</div>') -- close col-md-9
+  add('</div>') -- close row
   if #notes > 0 then
     add('<ol class="footnotes">')
     for _,note in pairs(notes) do
       add(note)
     end
     add('</ol>')
   end
   return table.concat(buffer,'\n') .. '\n'
 end
 
 -- The functions that follow render corresponding pandoc elements.
 -- s is always a string, attr is always a table of attributes, and
 -- items is always an array of strings (the items in a list).
 -- Comments indicate the types of other variables.
 
@@ -281,36 +291,41 @@
   return s
 end
 
 function Para(s)
   return "<p>\n" .. s .. "\n</p>"
 end
 
 -- lev is an integer, the header level.
 function Header(lev, s, attr)
   local lines = { }
 
   if lev == 1 then
 
     idStr = simplifyId(s)
 
-    table.insert(headers, s)
+    table.insert(headers, {text = s, id = idStr})
 
-    table.insert(lines, "<a name='" .. idStr .. "'></a>")
-    table.insert(lines, "<h2>"  .. s .. "</h2>")
+    table.insert(lines, "<h2 id='" .. idStr .. "'>"  .. s .. "</h2>")
     headerOpen = true
 
+  elseif lev == 2 then
+    -- Collect lev==2 headers for TOC (markdown ## headings)
+    local idStr = attr.id or simplifyId(s)
+    table.insert(headers, {text = s, id = idStr})
+    table.insert(lines, "<h" .. lev .. " id='" .. idStr .. "'" ..  ">" .. s .. "</h" .. lev .. ">")
+
   else
     table.insert(lines, "<h" .. lev .. attributes(attr) ..  ">" .. s .. "</h" .. lev .. ">")
   end
 
   return table.concat(lines, "\n")
 end
 
 function BlockQuote(s)
   return "<blockquote>\n" .. s .. "\n</blockquote>"
 end
 
 function HorizontalRule()
   return "<hr/>"
 end