diff --git a/blueprint/src/mobile.css b/blueprint/src/mobile.css
new file mode 100644
index 0000000..670a987
--- /dev/null
+++ b/blueprint/src/mobile.css
@@ -0,0 +1,233 @@
+/* Blueprint Mobile Responsive — all rules scoped to @media (max-width: 768px) */
+
+/* ── TOC backdrop (injected by mobile.js) ── */
+.mobile-toc-backdrop {
+ display: none;
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,0.4);
+ z-index: 99;
+}
+.mobile-toc-backdrop.visible {
+ display: block;
+}
+
+@media (max-width: 768px) {
+
+ /* ── 1. TOC as fixed slide-in overlay ── */
+ nav.toc {
+ position: fixed !important;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ width: min(280px, 80vw);
+ z-index: 100;
+ transform: translateX(-100%);
+ transition: transform 0.25s ease;
+ background: #fff;
+ box-shadow: 2px 0 12px rgba(0,0,0,0.15);
+ display: flex !important;
+ flex-direction: column;
+ overflow-y: auto;
+ -webkit-overflow-scrolling: touch;
+ margin-right: 0;
+ padding: 0.5rem;
+ padding-top: 3.5rem;
+ border-right: 1px solid #e0e0e0;
+ }
+ nav.toc.mobile-open {
+ transform: translateX(0);
+ }
+ body.mobile-toc-active {
+ overflow: hidden;
+ }
+
+ /* ── 2. MathJax overflow ── */
+ mjx-container[jax="CHTML"] {
+ overflow-x: auto;
+ max-width: 100%;
+ }
+ mjx-container[jax="CHTML"][display="true"] {
+ overflow-x: auto;
+ max-width: 100%;
+ padding: 4px 0;
+ }
+ div.displaymath {
+ max-width: 100%;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ /* ── 3. Content area ── */
+ div.content {
+ padding: 0.75rem;
+ margin-bottom: 3rem;
+ }
+ div.content-wrapper {
+ max-width: 100%;
+ }
+ div.content > p {
+ margin: 1rem 0;
+ }
+
+ /* Reduce theorem/proof indentation */
+ div[class$=_thmcontent] {
+ margin-left: 0.25rem;
+ padding-left: 0.5rem;
+ }
+ div.proof_content {
+ margin-left: 0.25rem;
+ padding-left: 0.5rem;
+ }
+
+ /* ── 4. Hover-only elements → always visible on touch ── */
+ div.thm_header_hidden_extras {
+ display: inline-block !important;
+ opacity: 0.6;
+ }
+
+ /* ── 5. Annotation system ── */
+
+ /* Enlarge touch targets (44px Apple HIG minimum) */
+ .annotation-indicator {
+ min-width: 44px;
+ min-height: 44px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 8px;
+ font-size: 1rem;
+ }
+
+ /* Float indicator: keep within viewport */
+ .ann-indicator-float {
+ right: 0;
+ position: static;
+ float: right;
+ }
+
+ /* Prevent iOS Safari auto-zoom on focus (font-size >= 16px) */
+ .ann-author-input,
+ .ann-text-input,
+ .ann-edit-textarea {
+ font-size: 16px !important;
+ }
+
+ /* Stack author label + input vertically */
+ .ann-input-row {
+ flex-direction: column;
+ gap: 4px;
+ }
+ .ann-input-row label {
+ min-width: auto;
+ }
+
+ /* Larger submit button */
+ .ann-submit-btn {
+ width: 100%;
+ min-height: 44px;
+ font-size: 1rem;
+ }
+
+ /* Larger action buttons */
+ .ann-actions button {
+ min-height: 36px;
+ padding: 6px 12px;
+ font-size: 0.85rem;
+ }
+
+ /* Toolbar: avoid overlap with fixed bottom nav */
+ #annotation-toolbar {
+ bottom: 3.5rem;
+ }
+ #annotation-toolbar .ann-toolbar-main {
+ flex-wrap: wrap;
+ gap: 4px;
+ }
+
+ /* ── 6. Bottom navigation bar ── */
+ nav.prev_up_next {
+ width: 100%;
+ left: 0;
+ justify-content: center;
+ gap: 1rem;
+ }
+ nav.prev_up_next a {
+ padding: 0.5rem 1.5rem;
+ }
+ /* showmore icons in bottom bar */
+ nav.prev_up_next svg.showmore {
+ min-width: 44px;
+ min-height: 44px;
+ padding: 0.5rem;
+ }
+
+ /* ── 7. Dependency graph pages ── */
+ div#graph {
+ height: 70vh;
+ touch-action: none;
+ overflow: hidden;
+ }
+
+ /* Node detail modal → full screen */
+ div.dep-modal-container {
+ width: 100vw !important;
+ margin-left: 0 !important;
+ margin-right: 0 !important;
+ margin-top: 0 !important;
+ max-height: 100vh;
+ top: 0 !important;
+ }
+ div.dep-modal-content {
+ max-height: calc(100vh - 2rem);
+ border-radius: 0;
+ }
+ button.dep-closebtn {
+ min-width: 44px;
+ min-height: 44px;
+ font-size: 1.5rem;
+ }
+
+ /* Legend: wider on mobile so text is readable */
+ #Legend dl {
+ max-width: 70%;
+ font-size: 100%;
+ }
+
+ /* ── 8. Lean declaration modal ── */
+ div.modal-content {
+ width: 95vw;
+ max-height: 80vh;
+ margin: 10vh auto;
+ }
+ div.modal-content header button.closebtn {
+ min-width: 44px;
+ min-height: 44px;
+ font-size: 1.5rem;
+ }
+
+ /* ── 9. Header ── */
+ body > header {
+ position: sticky;
+ top: 0;
+ z-index: 50;
+ }
+ #toc-toggle {
+ min-width: 44px;
+ min-height: 44px;
+ padding: 10px;
+ cursor: pointer;
+ }
+
+ /* ── 10. Local TOC (in-page) ── */
+ nav.local_toc ul {
+ padding-left: 0.5rem;
+ }
+
+ /* ── 11. Tables: horizontal scroll ── */
+ .tabular {
+ display: block;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+
+} /* end @media (max-width: 768px) */
diff --git a/blueprint/src/mobile.js b/blueprint/src/mobile.js
new file mode 100644
index 0000000..21f9f3c
--- /dev/null
+++ b/blueprint/src/mobile.js
@@ -0,0 +1,164 @@
+/* Blueprint Mobile Enhancements — TOC overlay, dep-graph pan-zoom, MathJax tweaks */
+(function() {
+ 'use strict';
+
+ var MOBILE_BREAKPOINT = 768;
+
+ function isMobile() {
+ return window.innerWidth <= MOBILE_BREAKPOINT;
+ }
+
+ /* ── 1. TOC slide-in overlay ──
+ *
+ * Overrides the simple jQuery .toggle() in plastex.js with a proper
+ * slide-in drawer + backdrop. Only activates on narrow screens.
+ */
+ function initTocOverlay($) {
+ if (!isMobile()) return;
+
+ var $toc = $('nav.toc');
+ if (!$toc.length) return;
+
+ // Undo any inline display style that plastex.js .toggle() may have set
+ $toc.css('display', '');
+
+ var $backdrop = $('
');
+ $('body').append($backdrop);
+
+ function openToc() {
+ $toc.addClass('mobile-open');
+ $backdrop.addClass('visible');
+ $('body').addClass('mobile-toc-active');
+ }
+
+ function closeToc() {
+ $toc.removeClass('mobile-open');
+ $backdrop.removeClass('visible');
+ $('body').removeClass('mobile-toc-active');
+ }
+
+ function toggleToc() {
+ if ($toc.hasClass('mobile-open')) {
+ closeToc();
+ } else {
+ openToc();
+ }
+ }
+
+ // Override the plastex.js click handler
+ $('#toc-toggle').off('click').on('click', function(e) {
+ e.stopPropagation();
+ toggleToc();
+ });
+
+ $backdrop.on('click', closeToc);
+
+ // Close after navigating
+ $toc.on('click', 'a', function() {
+ closeToc();
+ });
+
+ // Close on Escape
+ $(document).on('keydown', function(e) {
+ if (e.key === 'Escape' && $toc.hasClass('mobile-open')) {
+ closeToc();
+ }
+ });
+ }
+
+
+ /* ── 2. Dependency graph pan-zoom ──
+ *
+ * Uses d3.zoom (included in the d3 v5 full bundle already loaded by
+ * dep_graph pages) to enable pinch-zoom and pan on the SVG graph.
+ * Works on both desktop (scroll-wheel) and mobile (touch gestures).
+ */
+ function initGraphPanZoom() {
+ var graphEl = document.getElementById('graph');
+ if (!graphEl || typeof d3 === 'undefined' || typeof d3.zoom !== 'function') return;
+
+ var observer = new MutationObserver(function(mutations, obs) {
+ var svg = graphEl.querySelector('svg');
+ if (!svg) return;
+ obs.disconnect();
+ // Small delay to let graphviz finish its post-render callbacks
+ setTimeout(function() { enablePanZoom(svg); }, 200);
+ });
+ observer.observe(graphEl, { childList: true, subtree: true });
+ }
+
+ function enablePanZoom(svgEl) {
+ var svgSel = d3.select(svgEl);
+ var g = svgSel.select('g');
+ if (!g.node()) return;
+
+ var zoom = d3.zoom()
+ .scaleExtent([0.2, 6])
+ .on('zoom', function() {
+ // d3 v5 API: transform is on d3.event, not the callback argument
+ g.attr('transform', d3.event.transform);
+ });
+
+ svgSel.call(zoom);
+
+ // Disable double-click zoom to prevent accidental triggers on mobile
+ svgSel.on('dblclick.zoom', null);
+
+ // Let SVG fill its container instead of using fixed width/height
+ svgEl.removeAttribute('width');
+ svgEl.removeAttribute('height');
+ svgEl.style.width = '100%';
+ svgEl.style.height = '100%';
+
+ // Fit initial view: reset to identity transform
+ var bbox = g.node().getBBox();
+ var containerW = graphEl.clientWidth;
+ var containerH = graphEl.clientHeight;
+ if (bbox.width > 0 && bbox.height > 0 && containerW > 0 && containerH > 0) {
+ var scale = Math.min(
+ containerW / bbox.width,
+ containerH / bbox.height
+ ) * 0.9;
+ var tx = (containerW - bbox.width * scale) / 2 - bbox.x * scale;
+ var ty = (containerH - bbox.height * scale) / 2 - bbox.y * scale;
+ svgSel.call(zoom.transform, d3.zoomIdentity.translate(tx, ty).scale(scale));
+ }
+ }
+
+
+ /* ── 3. MathJax mobile tweaks ── */
+ function initMathJaxTweaks() {
+ if (!isMobile()) return;
+
+ // Disable MathJax context menu on mobile (prevents long-press conflicts)
+ if (window.MathJax && window.MathJax.startup && window.MathJax.startup.promise) {
+ window.MathJax.startup.promise.then(function() {
+ try {
+ var opts = MathJax.config.options || {};
+ opts.menuOptions = opts.menuOptions || {};
+ opts.menuOptions.settings = opts.menuOptions.settings || {};
+ opts.menuOptions.settings.zoom = 'None';
+ opts.enableMenu = false;
+ } catch(e) { /* non-critical */ }
+ });
+ }
+ }
+
+
+ /* ── Init ── */
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', boot);
+ } else {
+ boot();
+ }
+
+ function boot() {
+ // jQuery may not be loaded yet on dep_graph pages (different script order)
+ if (typeof jQuery !== 'undefined') {
+ initTocOverlay(jQuery);
+ }
+ initGraphPanZoom();
+ initMathJaxTweaks();
+ }
+
+})();
diff --git a/blueprint/src/plastex.cfg b/blueprint/src/plastex.cfg
index 734aeb7..43c3ba2 100644
--- a/blueprint/src/plastex.cfg
+++ b/blueprint/src/plastex.cfg
@@ -13,8 +13,8 @@ split-level=1
[html5]
localtoc-level=0
-extra-css=extra_styles.css annotations.css
-extra-js=annotations.js
+extra-css=extra_styles.css annotations.css mobile.css
+extra-js=annotations.js mobile.js
mathjax-dollars=False
[images]
diff --git a/ui/client/index.html b/ui/client/index.html
index 3c7b7ae..9a97766 100644
--- a/ui/client/index.html
+++ b/ui/client/index.html
@@ -2,7 +2,7 @@
-
+
KIP Dashboard
diff --git a/ui/client/src/components/LogEntryLine.module.css b/ui/client/src/components/LogEntryLine.module.css
index 1e161cc..c7a8a1f 100644
--- a/ui/client/src/components/LogEntryLine.module.css
+++ b/ui/client/src/components/LogEntryLine.module.css
@@ -7,3 +7,9 @@
.expandable { cursor: pointer; }
.expandable:hover { color: var(--text-primary); }
.expandHint { color: var(--text-muted); font-size: 10px; }
+
+@media (max-width: 768px) {
+ .line { gap: 6px; font-size: 12px; padding: 6px 0; }
+ .event { min-width: 60px; font-size: 11px; }
+ .ts { font-size: 11px; }
+}
diff --git a/ui/client/src/components/NodeDetail.module.css b/ui/client/src/components/NodeDetail.module.css
index 74d1f61..3877f10 100644
--- a/ui/client/src/components/NodeDetail.module.css
+++ b/ui/client/src/components/NodeDetail.module.css
@@ -320,3 +320,84 @@ details[open] > summary.sectionLabel { margin-bottom: 6px; }
.runStatus.error { background: rgba(203,36,49,0.12); color: var(--red); }
.runMeta { color: var(--text-muted); font-size: 10px; margin-top: 2px; font-variant-numeric: tabular-nums; }
.runHint { color: var(--text-secondary); font-size: 10px; font-family: var(--font-mono); margin-top: 2px; word-break: break-all; }
+
+.closeBtn {
+ margin-left: auto;
+ cursor: pointer;
+ border: none;
+ background: transparent;
+ font-size: 18px;
+ color: var(--text-muted);
+ line-height: 1;
+}
+
+@media (max-width: 768px) {
+ /* Keep the node id + close button visible while the user scrolls through
+ long Lean source / NL bodies on a small screen. */
+ .header {
+ position: sticky;
+ top: 0;
+ z-index: 5;
+ padding: 12px 12px 8px;
+ }
+ .id { font-size: 14px; }
+
+ .section { padding: 10px 12px; }
+
+ /* Two-column flag grid keeps each cell wide enough to read on a 375px
+ viewport. */
+ .flagsGrid { grid-template-columns: repeat(2, 1fr); }
+
+ /* Stack the "Reviewing as" label + input vertically so the input gets the
+ full row width. */
+ .reviewerLabel {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 4px;
+ }
+ /* font-size ≥ 16px on the actual
/