Layout: page-shell wrapper for iOS sticky, scrollable TOC
iOS WebKit silently degrades position: sticky to static on direct
flex/grid children of <body>, so the sticky nav was breaking on
mobile. Wrapping everything below the nav in a .page-shell flex
column keeps the sticky-footer math out of body { } and restores
sticky behaviour across browsers. The essay-frontmatter hoisted in
the Marks II commit becomes a body-level sibling of .page-shell so
its monogram and epistemic-figure columns can span viewport width.
* templates/default.html wraps $body$ + footer in .page-shell.
* static/css/layout.css moves the flex-column + min-height math from
body to .page-shell; the body > header rule now excludes
.essay-frontmatter so the essay header does not inherit nav chrome
(sticky, nav-bg, border-bottom).
* static/css/base.css drops the html/body overflow-x: clip — the
page-shell wrapper handles horizontal containment and clipping at
the viewport level was interfering with position: sticky.
* static/css/reading.css updates its #markdownBody centering selector
to match .page-shell > #markdownBody.
* static/css/components.css makes the TOC outline scrollable when
it overflows: bounded max-height tied to the sticky budget plus a
thin themed scrollbar, with overflow: hidden preserved for the
collapse transition.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
fad8719045
commit
802fc75968
|
|
@ -278,15 +278,11 @@ html {
|
|||
line-height: var(--line-height);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
scroll-behavior: smooth;
|
||||
/* clip (not hidden) — prevents horizontal scroll at the viewport level
|
||||
without creating a scroll container, so position:sticky still works. */
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: clip;
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
transition: background-color var(--transition-fast),
|
||||
|
|
|
|||
|
|
@ -543,12 +543,28 @@ nav.site-nav {
|
|||
using `aria-hidden="true"` (set by toc.js). The transition still
|
||||
works because we keep `max-height: 0` for the visual collapse. */
|
||||
.toc-nav {
|
||||
overflow: hidden;
|
||||
max-height: 80vh;
|
||||
/* Fill the sticky sidebar's remaining height and scroll when the
|
||||
outline is taller than the viewport. The subtracted ~2.6rem
|
||||
mirrors #toc's own max-height budget (layout.css) minus the
|
||||
.toc-header row (label + progress rule + its bottom margin). */
|
||||
max-height: calc(100vh - var(--nav-height, 4rem) - 3rem - 2.6rem);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior: contain;
|
||||
transition: max-height 0.3s ease;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--border) transparent;
|
||||
}
|
||||
.toc-nav::-webkit-scrollbar { width: 6px; }
|
||||
.toc-nav::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
#toc.is-collapsed .toc-nav {
|
||||
max-height: 0;
|
||||
/* overflow:hidden so the outline is clipped (not scrolled) while
|
||||
the max-height transition runs down to 0. */
|
||||
overflow: hidden;
|
||||
}
|
||||
#toc.is-collapsed .toc-nav a,
|
||||
#toc.is-collapsed .toc-nav button {
|
||||
|
|
|
|||
|
|
@ -5,10 +5,16 @@
|
|||
The outer shell. Wide enough for TOC + body + sidenotes.
|
||||
============================================================ */
|
||||
|
||||
body {
|
||||
/* Body is plain block — keeps the sticky nav header (a direct body
|
||||
child) working reliably on iOS WebKit, where position: sticky on a
|
||||
direct flex/grid child silently degrades to static. The sticky-footer
|
||||
math (full-viewport min-height + flex column to push footer down)
|
||||
moves to .page-shell, which wraps everything below the nav. */
|
||||
|
||||
.page-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
min-height: calc(100dvh - var(--nav-height, 4rem));
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
|
|
@ -17,7 +23,14 @@ body {
|
|||
(Nav styles live in components.css)
|
||||
============================================================ */
|
||||
|
||||
body > header {
|
||||
/* Site-nav header only — exclude the essay-frontmatter <header> that
|
||||
essay/reading/blog templates emit as a body-level sibling so its
|
||||
monogram and figure columns can span full viewport width. Without
|
||||
the :not() guard, the essay header inherits the sticky / nav-bg /
|
||||
border-bottom chrome meant only for the top navigation, painting
|
||||
the wrong color band over the page and pinning the frontmatter to
|
||||
the viewport top. */
|
||||
body > header:not(.essay-frontmatter) {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background-color: var(--bg-nav);
|
||||
|
|
@ -78,18 +91,20 @@ body > header {
|
|||
/* ============================================================
|
||||
STANDALONE PAGES (no #content wrapper)
|
||||
essay-index, blog-index, tag-index, page, blog-post, search —
|
||||
these emit #markdownBody as a direct child of <body>. Without
|
||||
the #content flex-row wrapper there is no centering; fix it here.
|
||||
these emit #markdownBody as a direct child of .page-shell.
|
||||
Without the #content flex-row wrapper there is no centering;
|
||||
fix it here. (Was body > #markdownBody before the page-shell
|
||||
wrapper was introduced to keep iOS sticky working.)
|
||||
============================================================ */
|
||||
|
||||
body > #markdownBody {
|
||||
.page-shell > #markdownBody {
|
||||
align-self: center;
|
||||
padding: 2rem var(--page-padding);
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
body > #markdownBody {
|
||||
.page-shell > #markdownBody {
|
||||
padding: 1.25rem var(--page-padding);
|
||||
}
|
||||
}
|
||||
|
|
@ -99,7 +114,7 @@ body > #markdownBody {
|
|||
FOOTER
|
||||
============================================================ */
|
||||
|
||||
body > footer {
|
||||
.page-shell > footer {
|
||||
width: 100%;
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 1.5rem var(--page-padding);
|
||||
|
|
@ -109,6 +124,7 @@ body > footer {
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
|
|
@ -217,7 +233,7 @@ body > footer {
|
|||
}
|
||||
|
||||
/* Footer: stack vertically so three sections don't fight for width. */
|
||||
body > footer {
|
||||
.page-shell > footer {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
|
|
|
|||
|
|
@ -48,9 +48,10 @@ body.reading-mode {
|
|||
}
|
||||
|
||||
/* Reading body: narrower than the essay default (800px → ~62ch).
|
||||
Since reading.html emits body > #markdownBody (no #content grid),
|
||||
the centering is handled by the existing layout.css rule. */
|
||||
body.reading-mode > #markdownBody {
|
||||
reading.html emits #markdownBody as a direct child of .page-shell
|
||||
(no #content grid); centering is handled by the matching
|
||||
.page-shell > #markdownBody rule in layout.css. */
|
||||
body.reading-mode .page-shell > #markdownBody {
|
||||
max-width: 62ch;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,10 @@ $if(search)$
|
|||
<script src="/js/semantic-search.js" defer></script>
|
||||
<script src="/js/search-filters.js" defer></script>
|
||||
$endif$
|
||||
<div class="page-shell">
|
||||
$body$
|
||||
$partial("templates/partials/footer.html")$
|
||||
</div>
|
||||
<!-- JS — all deferred -->
|
||||
<script src="/js/popups.js" defer></script>
|
||||
<script src="/js/annotations.js" defer></script>
|
||||
|
|
|
|||
Loading…
Reference in New Issue