diff --git a/src/md_to_html.py b/src/md_to_html.py
index 4c792a6..4cd10c7 100644
--- a/src/md_to_html.py
+++ b/src/md_to_html.py
@@ -42,6 +42,7 @@ CSS = """
}
}
* { box-sizing: border-box; }
+html { scroll-behavior: smooth; }
body {
margin: 0;
background: var(--bg);
@@ -49,11 +50,59 @@ body {
font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.55;
}
+.layout {
+ display: flex;
+ align-items: flex-start;
+ max-width: 1280px;
+ margin: 0 auto;
+}
+.toc {
+ flex: 0 0 240px;
+ position: sticky;
+ top: 0;
+ align-self: flex-start;
+ max-height: 100vh;
+ overflow-y: auto;
+ padding: 2.5rem 1rem 2rem 1.5rem;
+ font-size: 0.83rem;
+}
+.toc-title {
+ color: var(--text-dim);
+ font-size: 0.72rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ margin-bottom: 0.6rem;
+}
+.toc a {
+ display: block;
+ padding: 0.28rem 0 0.28rem 0.6rem;
+ color: var(--text-dim);
+ text-decoration: none;
+ border-left: 2px solid transparent;
+ line-height: 1.3;
+}
+.toc a:hover { color: var(--text); }
+.toc a.active { color: var(--accent); border-left-color: var(--accent); }
+.toc a.toc-h3 { padding-left: 1.5rem; font-size: 0.94em; }
.wrap {
- max-width: 1080px;
+ flex: 1 1 auto;
+ min-width: 0;
+ max-width: 960px;
margin: 0 auto;
padding: 2.5rem 1.5rem 5rem;
}
+@media (max-width: 900px) {
+ .layout { display: block; }
+ .toc {
+ position: static;
+ max-height: none;
+ width: 100%;
+ border-bottom: 1px solid var(--border);
+ padding: 1.25rem 1.5rem;
+ }
+ .wrap { max-width: 100%; }
+}
h1 {
font-size: 1.9rem;
margin-bottom: 0.25rem;
@@ -248,6 +297,34 @@ TZ_JS = """
})();
"""
+TOC_JS = """
+(function () {
+ var links = Array.prototype.slice.call(document.querySelectorAll(".toc a"));
+ if (!links.length) return;
+
+ var targets = links
+ .map(function (link) { return document.getElementById(link.getAttribute("href").slice(1)); })
+ .filter(Boolean);
+ if (!targets.length || !("IntersectionObserver" in window)) return;
+
+ function setActive(id) {
+ links.forEach(function (link) {
+ link.classList.toggle("active", link.getAttribute("href") === "#" + id);
+ });
+ }
+
+ var observer = new IntersectionObserver(function (entries) {
+ var visible = entries.filter(function (e) { return e.isIntersecting; });
+ if (visible.length) {
+ visible.sort(function (a, b) { return a.boundingClientRect.top - b.boundingClientRect.top; });
+ setActive(visible[0].target.id);
+ }
+ }, { rootMargin: "0px 0px -70% 0px", threshold: 0 });
+
+ targets.forEach(function (t) { observer.observe(t); });
+})();
+"""
+
SEVERITY_CLASS = {
"Critical": "badge-critical",
"High": "badge-high",
@@ -290,6 +367,23 @@ def wrap_timestamps(html_text):
return RFC3339_UTC_RE.sub(r'\1', html_text)
+HEADING_RE = re.compile(r'