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);
|
line-height: var(--line-height);
|
||||||
-webkit-text-size-adjust: 100%;
|
-webkit-text-size-adjust: 100%;
|
||||||
scroll-behavior: smooth;
|
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 {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow-x: clip;
|
|
||||||
background-color: var(--bg);
|
background-color: var(--bg);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
transition: background-color var(--transition-fast),
|
transition: background-color var(--transition-fast),
|
||||||
|
|
|
||||||
|
|
@ -543,12 +543,28 @@ nav.site-nav {
|
||||||
using `aria-hidden="true"` (set by toc.js). The transition still
|
using `aria-hidden="true"` (set by toc.js). The transition still
|
||||||
works because we keep `max-height: 0` for the visual collapse. */
|
works because we keep `max-height: 0` for the visual collapse. */
|
||||||
.toc-nav {
|
.toc-nav {
|
||||||
overflow: hidden;
|
/* Fill the sticky sidebar's remaining height and scroll when the
|
||||||
max-height: 80vh;
|
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;
|
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 {
|
#toc.is-collapsed .toc-nav {
|
||||||
max-height: 0;
|
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 a,
|
||||||
#toc.is-collapsed .toc-nav button {
|
#toc.is-collapsed .toc-nav button {
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,16 @@
|
||||||
The outer shell. Wide enough for TOC + body + sidenotes.
|
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;
|
display: flex;
|
||||||
flex-direction: column;
|
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)
|
(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%;
|
width: 100%;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
background-color: var(--bg-nav);
|
background-color: var(--bg-nav);
|
||||||
|
|
@ -78,18 +91,20 @@ body > header {
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
STANDALONE PAGES (no #content wrapper)
|
STANDALONE PAGES (no #content wrapper)
|
||||||
essay-index, blog-index, tag-index, page, blog-post, search —
|
essay-index, blog-index, tag-index, page, blog-post, search —
|
||||||
these emit #markdownBody as a direct child of <body>. Without
|
these emit #markdownBody as a direct child of .page-shell.
|
||||||
the #content flex-row wrapper there is no centering; fix it here.
|
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;
|
align-self: center;
|
||||||
padding: 2rem var(--page-padding);
|
padding: 2rem var(--page-padding);
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 680px) {
|
@media (max-width: 680px) {
|
||||||
body > #markdownBody {
|
.page-shell > #markdownBody {
|
||||||
padding: 1.25rem var(--page-padding);
|
padding: 1.25rem var(--page-padding);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -99,7 +114,7 @@ body > #markdownBody {
|
||||||
FOOTER
|
FOOTER
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
body > footer {
|
.page-shell > footer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
padding: 1.5rem var(--page-padding);
|
padding: 1.5rem var(--page-padding);
|
||||||
|
|
@ -109,6 +124,7 @@ body > footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-left {
|
.footer-left {
|
||||||
|
|
@ -217,7 +233,7 @@ body > footer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Footer: stack vertically so three sections don't fight for width. */
|
/* Footer: stack vertically so three sections don't fight for width. */
|
||||||
body > footer {
|
.page-shell > footer {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.3rem;
|
gap: 0.3rem;
|
||||||
|
|
|
||||||
|
|
@ -48,9 +48,10 @@ body.reading-mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Reading body: narrower than the essay default (800px → ~62ch).
|
/* Reading body: narrower than the essay default (800px → ~62ch).
|
||||||
Since reading.html emits body > #markdownBody (no #content grid),
|
reading.html emits #markdownBody as a direct child of .page-shell
|
||||||
the centering is handled by the existing layout.css rule. */
|
(no #content grid); centering is handled by the matching
|
||||||
body.reading-mode > #markdownBody {
|
.page-shell > #markdownBody rule in layout.css. */
|
||||||
|
body.reading-mode .page-shell > #markdownBody {
|
||||||
max-width: 62ch;
|
max-width: 62ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,10 @@ $if(search)$
|
||||||
<script src="/js/semantic-search.js" defer></script>
|
<script src="/js/semantic-search.js" defer></script>
|
||||||
<script src="/js/search-filters.js" defer></script>
|
<script src="/js/search-filters.js" defer></script>
|
||||||
$endif$
|
$endif$
|
||||||
|
<div class="page-shell">
|
||||||
$body$
|
$body$
|
||||||
$partial("templates/partials/footer.html")$
|
$partial("templates/partials/footer.html")$
|
||||||
|
</div>
|
||||||
<!-- JS — all deferred -->
|
<!-- JS — all deferred -->
|
||||||
<script src="/js/popups.js" defer></script>
|
<script src="/js/popups.js" defer></script>
|
||||||
<script src="/js/annotations.js" defer></script>
|
<script src="/js/annotations.js" defer></script>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue