diff --git a/static/css/components.css b/static/css/components.css index a9b05dc..7c77f6a 100644 --- a/static/css/components.css +++ b/static/css/components.css @@ -291,10 +291,7 @@ nav.site-nav { @media (max-width: 540px) { .nav-logo { - position: static; - height: 2rem; - width: 2rem; - flex-shrink: 0; + display: none; } .nav-row-primary { @@ -345,6 +342,59 @@ nav.site-nav { .nav-portals a { padding: 0.3rem 0.55rem; } + + /* Extra bottom padding so the last content line isn't hidden + behind the fixed mobile TOC bar. */ + body { + padding-bottom: 2.5rem; + } + + /* Mobile TOC bar — fixed bottom strip: progress line + current + section label. toc.js sets --toc-progress and syncs the label. */ + #toc-mobile-bar { + display: flex; + align-items: center; + position: fixed; + bottom: 0; left: 0; right: 0; + height: 2.25rem; + background: var(--bg-nav); + border-top: 1px solid var(--border); + padding: 0 1rem; + z-index: 90; + overflow: hidden; + } + + /* Progress track (faint full-width line at top of bar) */ + #toc-mobile-bar::before { + content: ''; + position: absolute; + top: 0; left: 0; right: 0; + height: 2px; + background: var(--border); + } + + /* Progress fill — grows with scroll via --toc-progress */ + #toc-mobile-bar::after { + content: ''; + position: absolute; + top: 0; left: 0; + height: 2px; + width: calc(var(--toc-progress, 0) * 100%); + background: var(--text-muted); + transition: width 0.12s ease; + } + + .toc-mobile-label { + font-family: var(--font-sans); + font-size: 0.68rem; + font-weight: 600; + font-variant-caps: all-small-caps; + letter-spacing: 0.07em; + color: var(--text-faint); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } } /* Row 2: portal links — hidden until nav.js adds .is-open */ @@ -386,6 +436,12 @@ nav.site-nav { and the --toc-progress custom property for the progress bar. ============================================================ */ +/* Mobile TOC bar — hidden on all viewports by default; the ≤540px + media query below makes it visible on mobile only. */ +#toc-mobile-bar { + display: none; +} + #toc { font-family: var(--font-sans); font-size: 0.85rem; diff --git a/static/css/sidenotes.css b/static/css/sidenotes.css index b5e014e..5f1fafd 100644 --- a/static/css/sidenotes.css +++ b/static/css/sidenotes.css @@ -155,3 +155,64 @@ a.footnote-ref { a.footnote-ref:hover { color: var(--text-muted); } + + +/* ============================================================ + MOBILE SIDENOTE POPUP + Bottom sheet shown when tapping a sidenote ref on narrow + viewports (where .sidenote is hidden). The overlay is + display:none by default; sidenotes.js adds .is-open to show it. + No media query needed — JS only opens the popup when the + sidenote span is hidden, so it never fires on wide viewports. + ============================================================ */ + +.sidenote-popup-overlay { + display: none; + position: fixed; + inset: 0; + z-index: 500; + background: rgba(0, 0, 0, 0.45); + align-items: flex-end; +} + +.sidenote-popup-overlay.is-open { + display: flex; +} + +.sidenote-popup { + background: var(--bg); + border-top: 1px solid var(--border); + border-radius: 0.75rem 0.75rem 0 0; + padding: 1.25rem 1.25rem 2.75rem; + width: 100%; + max-height: 65vh; + overflow-y: auto; + position: relative; + font-family: var(--font-serif); + font-size: 0.9rem; + line-height: 1.6; + color: var(--text); + outline: none; +} + +.sidenote-popup-close { + position: absolute; + top: 0.7rem; + right: 0.9rem; + background: none; + border: none; + font-size: 1.5rem; + line-height: 1; + color: var(--text-faint); + cursor: pointer; + padding: 0.1rem 0.35rem; +} + +.sidenote-popup-close:hover { + color: var(--text); +} + +/* Preserve sidenote-num badge style inside the popup */ +.sidenote-popup .sidenote-num { + display: inline-block; +} diff --git a/static/js/sidenotes.js b/static/js/sidenotes.js index 656afe2..d2e52df 100644 --- a/static/js/sidenotes.js +++ b/static/js/sidenotes.js @@ -97,12 +97,16 @@ sn.addEventListener('mouseenter', function () { hovering = true; update(); }); sn.addEventListener('mouseleave', function () { hovering = false; update(); }); - /* Click on the superscript link: sticky focus on wide viewports, - normal anchor scroll on narrow viewports (sidenote hidden). */ + /* Click on the superscript link: sticky focus on wide viewports; + mobile popup on narrow viewports (sidenote hidden). */ const link = ref.querySelector('a'); if (link) { link.addEventListener('click', function (e) { e.preventDefault(); + if (getComputedStyle(sn).display === 'none') { + openMobilePopup(sn); + return; + } toggleFocus(); }); @@ -126,6 +130,65 @@ } } + /* ------------------------------------------------------------------ */ + /* Mobile popup — bottom sheet for narrow viewports where .sidenote */ + /* is display:none. Created lazily on first use. */ + /* ------------------------------------------------------------------ */ + + var mobileOverlay = null; + + function ensureMobileOverlay() { + if (mobileOverlay) return; + + var overlay = document.createElement('div'); + overlay.className = 'sidenote-popup-overlay'; + overlay.setAttribute('role', 'dialog'); + overlay.setAttribute('aria-modal', 'true'); + overlay.setAttribute('aria-label', 'Note'); + + var sheet = document.createElement('div'); + sheet.className = 'sidenote-popup'; + sheet.setAttribute('tabindex', '-1'); + + var closeBtn = document.createElement('button'); + closeBtn.className = 'sidenote-popup-close'; + closeBtn.setAttribute('aria-label', 'Close note'); + closeBtn.textContent = '\u00d7'; /* × */ + + var body = document.createElement('div'); + body.className = 'sidenote-popup-body'; + + sheet.appendChild(closeBtn); + sheet.appendChild(body); + overlay.appendChild(sheet); + document.body.appendChild(overlay); + + overlay.addEventListener('click', function (e) { + if (!sheet.contains(e.target)) closeMobilePopup(); + }); + closeBtn.addEventListener('click', closeMobilePopup); + + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape' && mobileOverlay && + mobileOverlay.classList.contains('is-open')) { + closeMobilePopup(); + } + }); + + mobileOverlay = overlay; + } + + function openMobilePopup(sn) { + ensureMobileOverlay(); + mobileOverlay.querySelector('.sidenote-popup-body').innerHTML = sn.innerHTML; + mobileOverlay.classList.add('is-open'); + mobileOverlay.querySelector('.sidenote-popup').focus(); + } + + function closeMobilePopup() { + if (mobileOverlay) mobileOverlay.classList.remove('is-open'); + } + /* Click anywhere outside a focused pair dismisses it. */ document.addEventListener('click', function (e) { if (focusedPair && diff --git a/static/js/toc.js b/static/js/toc.js index 91ae54f..217787e 100644 --- a/static/js/toc.js +++ b/static/js/toc.js @@ -20,8 +20,10 @@ if (!headings.length) return; - const label = toc.querySelector('.toc-active-label'); - const toggleBtn = toc.querySelector('.toc-toggle'); + const label = toc.querySelector('.toc-active-label'); + const toggleBtn = toc.querySelector('.toc-toggle'); + const mobileBar = document.getElementById('toc-mobile-bar'); + const mobileLabel = mobileBar && mobileBar.querySelector('.toc-mobile-label'); const pageTitleEl = document.querySelector('#markdownBody .page-title'); const pageTitle = pageTitleEl ? pageTitleEl.textContent.trim() : 'Contents'; @@ -29,14 +31,15 @@ function activateTitle() { links.forEach(a => a.classList.remove('is-active')); if (label) label.textContent = pageTitle; + if (mobileLabel) mobileLabel.textContent = pageTitle; } function activate(id) { links.forEach(a => a.classList.toggle('is-active', a.dataset.target === id)); - if (label) { - const activeLink = linkMap.get(id); - label.textContent = activeLink ? activeLink.textContent : pageTitle; - } + const activeLink = linkMap.get(id); + const text = activeLink ? activeLink.textContent : pageTitle; + if (label) label.textContent = text; + if (mobileLabel) mobileLabel.textContent = text; } // Collapse / expand. The collapsed state is hidden from @@ -70,11 +73,13 @@ } - // Progress indicator — drives the horizontal bar under .toc-header. + // Progress indicator — drives the horizontal bar under .toc-header + // and the mobile bar's progress line. function updateProgress() { const scrollable = document.documentElement.scrollHeight - window.innerHeight; const progress = scrollable > 0 ? Math.min(1, window.scrollY / scrollable) : 0; toc.style.setProperty('--toc-progress', progress); + if (mobileBar) mobileBar.style.setProperty('--toc-progress', progress); } window.addEventListener('scroll', updateProgress, { passive: true }); updateProgress(); diff --git a/templates/essay.html b/templates/essay.html index 8d19a84..2d112ae 100644 --- a/templates/essay.html +++ b/templates/essay.html @@ -14,4 +14,7 @@ $body$ + $partial("templates/partials/page-footer.html")$