ssh git@grit.deadkey.org man

Commit d592093 h

Pat Ambrosio · 26-08-26 09:06

Move the way back below the text, in posts and pages alike

Changes: 3 files +86 -100

Changes (Side-by-Side)

share/jenny/layout/page.sh +17 -54

cat << _EOF_
376 relation to the 109px the pair actually spans. */ 376 relation to the 109px the pair actually spans. */
377 /* No margin-bottom: post.sh's .heading has none, and the extra 1em put 48px 377 /* No margin-bottom: post.sh's .heading has none, and the extra 1em put 48px
378 between a page title and its body where a post has none. */ 378 between a page title and its body where a post has none. */
379 /* On a post the stamp holds the left column and the title starts where
380 the body does. A page has no stamp, so without a spacer here the
381 title sat at the far left while its own body was indented past it.
382 The heading reserves the same column the body does, by the same
383 mechanism, so the two line up whatever the width. */
384 /* Beside a sidebar the whole cell is already offset, so the heading
385 must not indent again -- the title would land a column right of its
386 own body. */
387 .page-wrapper:has(.page-nav) .heading-page .home-spacer { display: none; }
388 .home { display: inline-block; text-align: right; margin-right: 1.5em;
389 width: 5.4em; margin-bottom: 1.5em; text-decoration: none;
390 color: var(--link-home); }
391 .home:hover { color: var(--link-home-hover); }
392 379
393 /* Breadcrumbs, marked the same way an index or tag page marks where 380 /* Breadcrumbs, marked the same way an index or tag page marks where
394 you are: a rule under the current segment rather than an 381 you are: a rule under the current segment rather than an
cat << _EOF_
419 the spacer beside it. */ 406 the spacer beside it. */
420 .heading-page .title { display: inline-block; margin: 0 0 1em; 407 .heading-page .title { display: inline-block; margin: 0 0 1em;
421 vertical-align: top; max-width: 100%; } 408 vertical-align: top; max-width: 100%; }
422 .heading-page .home-spacer { margin-bottom: 0; }
423 .heading .stamp,
424 .home { display: inline-block; text-align:right; margin-right: 1.5em; }
425 .home { text-decoration: none; width: 5.4em; margin-bottom: 1.5em; text-align: left; color: var(--link-home); } .home:hover { color: var(--link-home-hover); }
409 .heading .stamp { display: inline-block; text-align: right; margin-right: 1.5em; }
410 /* Identical to the post layout's: the way back sits after the text, so
411 posts and pages end the same way. Held in a column before the body
412 it wrapped onto its own line on a phone, leaving a stray arrow
413 between the title and the first paragraph. */
414 .home { display: block; margin-top: 2.5em; text-decoration: none;
415 color: var(--link-home); }
416 .home:hover { color: var(--link-home-hover); }
426 .heading .stamp { color: var(--text-muted); } 417 .heading .stamp { color: var(--text-muted); }
427 /* A nav is an ordered list of names and places; what it means is 418 /* A nav is an ordered list of names and places; what it means is
428 left to this template. Rendered as a left sidebar beside the body. */ 419 left to this template. Rendered as a left sidebar beside the body. */
cat << _EOF_
489 .page-wrapper { display: table; table-layout: auto; width: auto; } 480 .page-wrapper { display: table; table-layout: auto; width: auto; }
490 .contents-cell { display: table-cell; vertical-align: top; 481 .contents-cell { display: table-cell; vertical-align: top;
491 padding-left: var(--page-nav-gap); } 482 padding-left: var(--page-nav-gap); }
492 /* With no nav, the body has to line up with a post's body, and a post
493 gets its indent from the back arrow sitting inline before the
494 contents -- not from a fixed length. That is why the indent is a
495 spacer of the arrow's exact size rather than a copied number: the
496 two are inline-blocks, so they sit side by side while they fit and
497 the body wraps under the spacer when the screen is too narrow,
498 reflowing at exactly the width a post reflows at. A hardcoded
499 padding could match at one width but broke at every other -- at
500 735px it put the page body 97px left of the post's. Pages have no
501 arrow (it means nothing on a page), so the spacer is empty and
502 hidden from assistive tech. */
483 /* With no nav there is no sidebar to sit beside, so the body starts at
484 the left margin -- the same place a post's body starts now that the
485 way back is below the text rather than beside it. */
503 .page-wrapper:not(:has(.page-nav)) .contents-cell { display: block; 486 .page-wrapper:not(:has(.page-nav)) .contents-cell { display: block;
504 padding-left: 0; } 487 padding-left: 0; }
505 .home-spacer { display: inline-block; width: 5.4em; margin-right: 1.5em;
506 margin-bottom: 1.5em; vertical-align: top; }
507 .page-wrapper:has(.page-nav) .home-spacer { display: none; }
508 .contents { display: inline-block; max-width: 60ch; vertical-align: top; width: 100%; } 488 .contents { display: inline-block; max-width: 60ch; vertical-align: top; width: 100%; }
509 489
510 /* Narrow: the sidebar has nowhere to go, so it stacks above the body 490 /* Narrow: the sidebar has nowhere to go, so it stacks above the body
cat << _EOF_
516 .page-nav, 496 .page-nav,
517 .contents-cell { display: block; } 497 .contents-cell { display: block; }
518 .page-nav { width: auto; min-width: 0; margin-bottom: 2em; } 498 .page-nav { width: auto; min-width: 0; margin-bottom: 2em; }
519 /* No indent at this width: 6.82rem plus a 60ch column overflows a
520 phone and forces horizontal scroll. */
499 /* The sidebar's gap goes with the sidebar: stacked above the body,
500 it is not a column to clear. Left in place it indented the body on
501 a phone and forced horizontal scroll. */
521 .contents-cell { padding-left: 0; } 502 .contents-cell { padding-left: 0; }
522 /* No spacer at this width, in the heading or the body. The indent
523 exists to reserve a post's back-arrow column; on a phone it costs
524 more than it reserves -- at 375px it takes the measure from 42
525 characters to 32, and at 320px down to 25.
526
527 The body already wraps under its spacer here, because a 60ch
528 column cannot sit beside it. The heading did not: a two-word
529 title fits beside the spacer at any width, so the title stayed
530 indented 97px above a body that had returned to the left margin.
531 That is the artifact -- a title hanging to the right of its own
532 text on a phone.
533
534 Removing both is what keeps them together. Matching the body's
535 content-driven turnover instead was tried and does not work: the
536 heading and the body hold different content, so they give out at
537 different widths, and tuning the title's column to compensate only
538 moved the disagreement (a 10px window at 614-622px became a 90px
539 one at 528-612px). */
540 .home-spacer { display: none; }
541 } 503 }
542 .contents :first-child { margin-top: 0; } 504 .contents :first-child { margin-top: 0; }
543 /* A diagram is a figure, not a paragraph: it takes the column's width 505 /* A diagram is a figure, not a paragraph: it takes the column's width
cat << _EOF_
649 <div class="page-wrapper"> 611 <div class="page-wrapper">
650 $(page_nav) 612 $(page_nav)
651 <div class="contents-cell"> 613 <div class="contents-cell">
652 <div class="heading heading-page"><span class="home-spacer" aria-hidden="true"></span><h1 class="title">$(html_escape "${POST_TITLE:-}")</h1></div>
653 <span class="home-spacer" aria-hidden="true"></span><div class="contents">
614 <div class="heading heading-page"><h1 class="title">$(html_escape "${POST_TITLE:-}")</h1></div>
615 <div class="contents">
654 $(echo "${POST_CONTENTS:-}") 616 $(echo "${POST_CONTENTS:-}")
655 <div class="tags">$(for i in $TAGS; do echo "<a href=\"../tag/$i/\">$(html_escape "$i")</a>"; done;)</div> 617 <div class="tags">$(for i in $TAGS; do echo "<a href=\"../tag/$i/\">$(html_escape "$i")</a>"; done;)</div>
618 <a href="$URL_PREFIX/" class="home">&#x2190;&#xFE0E; $(html_escape "${BLOG_TITLE:-}")</a>
656 </div> 619 </div>
657 </div> 620 </div>
658 </div> 621 </div>

share/jenny/layout/post.sh +17 -3

cat << _EOF_
250 .heading .title { color: var(--text-heading); display: table-cell; margin: 0 0 1em; font-weight:bold; max-width: 500px; } 250 .heading .title { color: var(--text-heading); display: table-cell; margin: 0 0 1em; font-weight:bold; max-width: 500px; }
251 .heading .title:hover { text-decoration: underline; } 251 .heading .title:hover { text-decoration: underline; }
252 .heading .stamp, 252 .heading .stamp,
253 .home { display: inline-block; text-align:right; margin-right: 1.5em; }
254 .home { text-decoration: none; width: 5.4em; margin-bottom: 1.5em; text-align: left; color: var(--link-home); } .home:hover { color: var(--link-home-hover); }
253 /* The way back sits after the text, where a reader finishes, rather
254 than beside it. Held in a fixed column before the body it wrapped
255 onto its own line once the body no longer fit next to it, leaving a
256 stray arrow floating between the title and the first paragraph on a
257 phone -- and on wider screens it reserved a column the body then had
258 to be indented past. Below the text it needs no column at either
259 width, so the body starts at the left margin everywhere.
260
261 It is a second route home: the site title in the header links there
262 from every page. Kept because the end of a long read is exactly
263 where that is useful, and named rather than left as a bare glyph so
264 it says where it goes. */
265 .home { display: block; margin-top: 2.5em; text-decoration: none;
266 color: var(--link-home); }
267 .home:hover { color: var(--link-home-hover); }
255 .heading .stamp { color: var(--text-muted); } 268 .heading .stamp { color: var(--text-muted); }
256 .contents { display: inline-block; max-width: 60ch; vertical-align: top; width: 100%; } 269 .contents { display: inline-block; max-width: 60ch; vertical-align: top; width: 100%; }
257 .contents :first-child { margin-top: 0; } 270 .contents :first-child { margin-top: 0; }
cat << _EOF_
361 <header><a href="$URL_PREFIX/">$(html_escape "${BLOG_TITLE:-}")</a></header> 374 <header><a href="$URL_PREFIX/">$(html_escape "${BLOG_TITLE:-}")</a></header>
362 <article> 375 <article>
363 <div class="heading$([ -n "${IS_PAGE:-}" ] && echo ' heading-page')"><a href="${POST_URL:-}">$([ -z "${IS_PAGE:-}" ] && printf '<span class="stamp">%s</span>' "$("$DATE" -d "${POST_DATE_RFC822:-}" +%m/%d/%Y 2>/dev/null || printf '%s' "${POST_DATE:-}")")<h1 class="title">$(html_escape "${POST_TITLE:-}")</h1></a></div> 376 <div class="heading$([ -n "${IS_PAGE:-}" ] && echo ' heading-page')"><a href="${POST_URL:-}">$([ -z "${IS_PAGE:-}" ] && printf '<span class="stamp">%s</span>' "$("$DATE" -d "${POST_DATE_RFC822:-}" +%m/%d/%Y 2>/dev/null || printf '%s' "${POST_DATE:-}")")<h1 class="title">$(html_escape "${POST_TITLE:-}")</h1></a></div>
364 <a href="$URL_PREFIX/" class="home">&#x2190;&#xFE0E;</a><div class="contents">
377 <div class="contents">
365 $(echo "${POST_CONTENTS:-}") 378 $(echo "${POST_CONTENTS:-}")
366 <div class="tags">$(for i in $TAGS; do echo "<a href=\"../tag/$i/\">$(html_escape "$i")</a>"; done;)</div> 379 <div class="tags">$(for i in $TAGS; do echo "<a href=\"../tag/$i/\">$(html_escape "$i")</a>"; done;)</div>
380 <a href="$URL_PREFIX/" class="home">&#x2190;&#xFE0E; $(html_escape "${BLOG_TITLE:-}")</a>
367 </div> 381 </div>
368 </article> 382 </article>
369 $(diagram_script) 383 $(diagram_script)

test/regressions.sh +52 -43

assert_file_exists "$nop/.dist/fallback.html"
1313 it "and does so without complaint" 1313 it "and does so without complaint"
1314 assert_eq "$stderr_output" "" 1314 assert_eq "$stderr_output" ""
1315 1315
1316 it "a page has no back arrow"
1317 # A post's arrow goes back to the index it was listed on. A page is not listed
1318 # anywhere -- it is reached by its own stable path -- so an arrow to the index
1319 # points at a place the reader did not come from. The breadcrumb in the header
1320 # is what a page navigates by instead.
1321 assert_file_not_contains "$pg/.dist/study/consciousness.html" 'class="home"'
1322
1323 it "the arrow's column is still reserved so the body lines up with a post's"
1324 # Removing the arrow must not move the body: a post indents by the arrow's
1325 # width, and a page has to match it.
1326 assert_file_contains "$pg/.dist/study/consciousness.html" 'class="home-spacer"'
1316 it "a page ends with the way back, like a post"
1317 # Held in a column before the body the arrow wrapped onto its own line once
1318 # the body no longer fit beside it, leaving a stray glyph between the title and
1319 # the first paragraph on a phone. Below the text it needs no column at any
1320 # width, and posts and pages end the same way.
1321 assert_file_contains "$pg/.dist/study/consciousness.html" 'class="home"'
1322
1323 it "the way back comes after the body, not before it"
1324 tags_line=$("$GREP" -n 'class="tags"' "$pg/.dist/study/consciousness.html" | head -1 | cut -d: -f1)
1325 home_line=$("$GREP" -n 'class="home"' "$pg/.dist/study/consciousness.html" | head -1 | cut -d: -f1)
1326 [ -n "$home_line" ] && [ "$home_line" -gt "$tags_line" ] \
1327 && pass "back link follows the content" \
1328 || fail "back link at line $home_line must come after the body at line $tags_line"
1327 1329
1328 it "a blog with only pages and no posts still builds" 1330 it "a blog with only pages and no posts still builds"
1329 only="$TMPROOT/onlypages" 1331 only="$TMPROOT/onlypages"
assert_file_not_contains "$nav_sb/.dist/test.html" '.page-nav { position: absolu
1685 it "a page with no nav takes the body indent, not the sidebar column" 1687 it "a page with no nav takes the body indent, not the sidebar column"
1686 assert_file_contains "$nav_sb/.dist/plain.html" '.page-wrapper:not(:has(.page-nav))' 1688 assert_file_contains "$nav_sb/.dist/plain.html" '.page-wrapper:not(:has(.page-nav))'
1687 1689
1688 it "a page with no nav reserves the arrow column the way a post does"
1689 # A post's indent comes from the back arrow sitting inline before the body, so
1690 # the two reflow together when the screen narrows. A page has no arrow, so it
1691 # reserves the same column with a spacer of the same size; a hardcoded padding
1692 # matched at one width and was 97px out at 735px.
1693 # Anchored to the body, not just "a spacer exists somewhere": the heading has
1694 # one too, and a check that matched either passed with the body's spacer
1695 # deleted -- which drops the body indent from 110px to 0 against a post's 110.
1696 assert_file_contains "$nav_sb/.dist/plain.html" '<span class="home-spacer" aria-hidden="true"></span><div class="contents">'
1697 assert_file_contains "$nav_sb/.dist/plain.html" '.home-spacer { display: inline-block'
1698
1699 it "the spacer is hidden from assistive technology"
1700 assert_file_contains "$nav_sb/.dist/plain.html" 'class="home-spacer" aria-hidden="true"'
1701
1702 it "a page with a sidebar drops the arrow spacer"
1703 assert_file_contains "$nav_sb/.dist/test.html" '.page-wrapper:has(.page-nav) .home-spacer { display: none'
1690 it "the body starts at the left margin, with no column reserved"
1691 # The indent existed only to make room for the back arrow beside the body.
1692 # With the arrow below the text there is nothing to clear, so the text uses the
1693 # full measure -- on a 375px phone the reserved column cost a quarter of the
1694 # line, 42 characters down to 32.
1695 assert_file_not_contains "$nav_sb/.dist/plain.html" 'home-spacer'
1696 assert_file_contains "$nav_sb/.dist/plain.html" '.home { display: block; margin-top: 2.5em;'
1704 1697
1705 it "a sidebar widens the article rather than narrowing the body" 1698 it "a sidebar widens the article rather than narrowing the body"
1706 # 74ch is sized for a post: arrow column plus a 60ch body. Letting the sidebar 1699 # 74ch is sized for a post: arrow column plus a 60ch body. Letting the sidebar
post_file=$(find "$nav_sb/.dist/post" -name '*.html' | head -1)
1719 post_contents=$("$GREP" -o '^ \.contents { [^}]*}' "$post_file" | head -1) 1712 post_contents=$("$GREP" -o '^ \.contents { [^}]*}' "$post_file" | head -1)
1720 assert_eq "$page_contents" "$post_contents" 1713 assert_eq "$page_contents" "$post_contents"
1721 1714
1722 it "the heading reserves the same column the body does"
1723 # On a post the stamp holds the left column, so the title starts where the
1724 # body starts. A page has no stamp; without the same spacer in the heading the
1725 # title sat at the far left while its own body was indented past it.
1726 assert_file_contains "$nav_sb/.dist/plain.html" 'class="heading heading-page"><span class="home-spacer"'
1715 it "the heading holds no reserved column either"
1716 # Nothing sits beside the title any more, so it starts where its body starts
1717 # without a spacer to keep in step.
1718 assert_file_contains "$nav_sb/.dist/plain.html" 'class="heading heading-page"><h1 class="title">'
1727 1719
1728 it "the heading has no breakpoint of its own" 1720 it "the heading has no breakpoint of its own"
1729 # A post has no media query at all -- heading and body collapse together by 1721 # A post has no media query at all -- heading and body collapse together by
it "the heading has no breakpoint of its own"
1733 narrow_head=$("$SED" -n '/@media (max-width: 735px)/,/^ }/p' "$nav_sb/.dist/plain.html") 1725 narrow_head=$("$SED" -n '/@media (max-width: 735px)/,/^ }/p' "$nav_sb/.dist/plain.html")
1734 assert_not_contains "$narrow_head" '.heading-page { margin-left: 0; display: block' 1726 assert_not_contains "$narrow_head" '.heading-page { margin-left: 0; display: block'
1735 1727
1736 it "a sidebar page does not indent its heading twice"
1737 # The cell is already offset by the nav column; indenting the heading again
1738 # would put the title a column right of its own body.
1739 assert_file_contains "$nav_sb/.dist/test.html" '.page-wrapper:has(.page-nav) .heading-page .home-spacer { display: none'
1728 it "a post ends with the way back"
1729 # The post side was unasserted: deleting the link from post.sh entirely broke
1730 # no test, because every other check here reads a page.
1731 post_file_bk=$(find "$nav_sb/.dist/post" -name '*.html' | head -1)
1732 assert_file_contains "$post_file_bk" 'class="home">'
1733
1734 it "a post's back link comes after its body"
1735 tl=$("$GREP" -n 'class="tags"' "$post_file_bk" | head -1 | cut -d: -f1)
1736 hl=$("$GREP" -n 'class="home"' "$post_file_bk" | head -1 | cut -d: -f1)
1737 [ -n "$hl" ] && [ "$hl" -gt "$tl" ] \
1738 && pass "post back link follows the content" \
1739 || fail "post back link at line $hl must come after the body at line $tl"
1740
1741 it "the back link names where it goes"
1742 # A bare arrow says only "away". Both layouts render the site title beside it.
1743 assert_file_contains "$post_file_bk" '&#x2190;&#xFE0E; '
1744
1745 it "posts and pages declare the same back link"
1746 # The whole point of moving it: both layouts end the same way. Compare the
1747 # declarations rather than trusting they look alike.
1748 page_home=$("$GREP" -o '\.home { display: block[^}]*}' "$nav_sb/.dist/plain.html" | head -1)
1749 post_home=$("$GREP" -o '\.home { display: block[^}]*}' "$(find "$nav_sb/.dist/post" -name '*.html' | head -1)" | head -1)
1750 assert_eq "$page_home" "$post_home"
1740 1751
1741 it "narrow screens collapse the whole table, not just one cell" 1752 it "narrow screens collapse the whole table, not just one cell"
1742 # A lone cell reverting to block still gets an anonymous table wrapper and 1753 # A lone cell reverting to block still gets an anonymous table wrapper and
it "a page title keeps a gap above its body"
1790 # paragraph, 0px where a post has 1em. 1801 # paragraph, 0px where a post has 1em.
1791 assert_file_contains "$ts/.dist/plain.html" '.heading-page .title { display: inline-block; margin: 0 0 1em;' 1802 assert_file_contains "$ts/.dist/plain.html" '.heading-page .title { display: inline-block; margin: 0 0 1em;'
1792 1803
1793 it "the arrow column is dropped on narrow screens"
1794 # The indent reserves a post's back-arrow column. On a phone it costs more
1795 # than it reserves -- at 375px it takes the measure from 42 characters to 32 --
1796 # and the body already wraps to the left margin there while the heading, whose
1797 # title always fits beside the spacer, did not. The title was left hanging
1798 # 97px right of its own text.
1804 it "narrow screens need no rule for the arrow column"
1805 # There is no column to drop: the back link sits below the text at every
1806 # width, so the narrow block only has to collapse the sidebar.
1799 narrow_sp=$("$SED" -n '/@media (max-width: 735px)/,/^ }/p' "$ts/.dist/plain.html") 1807 narrow_sp=$("$SED" -n '/@media (max-width: 735px)/,/^ }/p' "$ts/.dist/plain.html")
1800 assert_contains "$narrow_sp" '.home-spacer { display: none; }'
1808 assert_not_contains "$narrow_sp" 'home-spacer'
1809 assert_contains "$narrow_sp" '.contents-cell { padding-left: 0; }'
1801 1810
1802 it "the heading is not a flex row" 1811 it "the heading is not a flex row"
1803 # Flex items do not wrap, which is why the heading held its column at widths 1812 # Flex items do not wrap, which is why the heading held its column at widths

Raw

commit d59209365b84801839410842ae52a6c7d0aabaa3
Author: Pat Ambrosio <cp.ambrosio@gmail.com>
Date:   Wed Aug 26 09:06:08 2026 +0800

    Move the way back below the text, in posts and pages alike
    
    The back arrow sat in a fixed column before the body. Once the body no
    longer fit beside it -- below about 736px -- it wrapped onto its own line,
    leaving a stray arrow floating between the title and the first paragraph
    on a phone. Above that width it was fine, but it cost a column the body
    then had to be indented past.
    
    Below the text it needs no column at either width, so the body starts at
    the left margin everywhere and the title, the text and the link share one
    left edge. On a 375px phone the reserved column had been taking the
    measure from 42 characters to 32, and at 320px down to 25.
    
    It is a second route home -- the site title in the header links there from
    every page -- and it stays because the end of a long read is exactly where
    that is useful. It now names its destination rather than rendering as a
    bare glyph, which said only "away".
    
    Pages get the same ending, which is most of the deletion here. They had no
    arrow but carried a spacer of the arrow's exact width in both the heading
    and the body, purely to keep their left edge matching a post's, plus rules
    to suppress it beside a sidebar and again on narrow screens. With nothing
    to reserve a column for, all of it goes: two spacer elements, four CSS
    rules, and a media-query clause.
    
    Tests: the spacer suite is replaced by one asserting where the link sits
    relative to the body, that both layouts declare it identically, and that
    no reserved column survives at any width. Verified by measuring at
    320-1440px -- title, body and link share a left edge on posts, plain pages
    and sidebar pages alike, with no horizontal overflow.
    
    Checked by mutation. The first version missed one: deleting the link from
    post.sh entirely broke nothing, because every assertion read a page. The
    post side is now asserted directly and that mutation fails three tests.
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01WnSAstgnDYdKPwLGv1YZE5

diff --git a/share/jenny/layout/page.sh b/share/jenny/layout/page.sh
index fb0ebf1..663dabb 100755
--- a/share/jenny/layout/page.sh
+++ b/share/jenny/layout/page.sh
@@ -376,19 +376,6 @@ cat << _EOF_
            relation to the 109px the pair actually spans. */
         /* No margin-bottom: post.sh's .heading has none, and the extra 1em put 48px
            between a page title and its body where a post has none. */
-        /* On a post the stamp holds the left column and the title starts where
-           the body does. A page has no stamp, so without a spacer here the
-           title sat at the far left while its own body was indented past it.
-           The heading reserves the same column the body does, by the same
-           mechanism, so the two line up whatever the width. */
-        /* Beside a sidebar the whole cell is already offset, so the heading
-           must not indent again -- the title would land a column right of its
-           own body. */
-        .page-wrapper:has(.page-nav) .heading-page .home-spacer { display: none; }
-        .home { display: inline-block; text-align: right; margin-right: 1.5em;
-                width: 5.4em; margin-bottom: 1.5em; text-decoration: none;
-                color: var(--link-home); }
-        .home:hover { color: var(--link-home-hover); }
 
         /* Breadcrumbs, marked the same way an index or tag page marks where
            you are: a rule under the current segment rather than an
@@ -419,10 +406,14 @@ cat << _EOF_
            the spacer beside it. */
         .heading-page .title { display: inline-block; margin: 0 0 1em;
                                vertical-align: top; max-width: 100%; }
-        .heading-page .home-spacer { margin-bottom: 0; }
-        .heading .stamp,
-        .home { display: inline-block; text-align:right; margin-right: 1.5em; }
-        .home { text-decoration: none; width: 5.4em; margin-bottom: 1.5em; text-align: left;  color: var(--link-home); } .home:hover { color: var(--link-home-hover); }
+        .heading .stamp { display: inline-block; text-align: right; margin-right: 1.5em; }
+        /* Identical to the post layout's: the way back sits after the text, so
+           posts and pages end the same way. Held in a column before the body
+           it wrapped onto its own line on a phone, leaving a stray arrow
+           between the title and the first paragraph. */
+        .home { display: block; margin-top: 2.5em; text-decoration: none;
+                color: var(--link-home); }
+        .home:hover { color: var(--link-home-hover); }
         .heading .stamp { color: var(--text-muted); }
         /* A nav is an ordered list of names and places; what it means is
            left to this template. Rendered as a left sidebar beside the body. */
@@ -489,22 +480,11 @@ cat << _EOF_
         .page-wrapper { display: table; table-layout: auto; width: auto; }
         .contents-cell { display: table-cell; vertical-align: top;
                          padding-left: var(--page-nav-gap); }
-        /* With no nav, the body has to line up with a post's body, and a post
-           gets its indent from the back arrow sitting inline before the
-           contents -- not from a fixed length. That is why the indent is a
-           spacer of the arrow's exact size rather than a copied number: the
-           two are inline-blocks, so they sit side by side while they fit and
-           the body wraps under the spacer when the screen is too narrow,
-           reflowing at exactly the width a post reflows at. A hardcoded
-           padding could match at one width but broke at every other -- at
-           735px it put the page body 97px left of the post's. Pages have no
-           arrow (it means nothing on a page), so the spacer is empty and
-           hidden from assistive tech. */
+        /* With no nav there is no sidebar to sit beside, so the body starts at
+           the left margin -- the same place a post's body starts now that the
+           way back is below the text rather than beside it. */
         .page-wrapper:not(:has(.page-nav)) .contents-cell { display: block;
                                                             padding-left: 0; }
-        .home-spacer { display: inline-block; width: 5.4em; margin-right: 1.5em;
-                       margin-bottom: 1.5em; vertical-align: top; }
-        .page-wrapper:has(.page-nav) .home-spacer { display: none; }
         .contents { display: inline-block; max-width: 60ch; vertical-align: top; width: 100%; }
 
         /* Narrow: the sidebar has nowhere to go, so it stacks above the body
@@ -516,28 +496,10 @@ cat << _EOF_
           .page-nav,
           .contents-cell { display: block; }
           .page-nav { width: auto; min-width: 0; margin-bottom: 2em; }
-          /* No indent at this width: 6.82rem plus a 60ch column overflows a
-             phone and forces horizontal scroll. */
+          /* The sidebar's gap goes with the sidebar: stacked above the body,
+             it is not a column to clear. Left in place it indented the body on
+             a phone and forced horizontal scroll. */
           .contents-cell { padding-left: 0; }
-          /* No spacer at this width, in the heading or the body. The indent
-             exists to reserve a post's back-arrow column; on a phone it costs
-             more than it reserves -- at 375px it takes the measure from 42
-             characters to 32, and at 320px down to 25.
-
-             The body already wraps under its spacer here, because a 60ch
-             column cannot sit beside it. The heading did not: a two-word
-             title fits beside the spacer at any width, so the title stayed
-             indented 97px above a body that had returned to the left margin.
-             That is the artifact -- a title hanging to the right of its own
-             text on a phone.
-
-             Removing both is what keeps them together. Matching the body's
-             content-driven turnover instead was tried and does not work: the
-             heading and the body hold different content, so they give out at
-             different widths, and tuning the title's column to compensate only
-             moved the disagreement (a 10px window at 614-622px became a 90px
-             one at 528-612px). */
-          .home-spacer { display: none; }
         }
         .contents :first-child { margin-top: 0; }
         /* A diagram is a figure, not a paragraph: it takes the column's width
@@ -649,10 +611,11 @@ cat << _EOF_
         <div class="page-wrapper">
         $(page_nav)
           <div class="contents-cell">
-        <div class="heading heading-page"><span class="home-spacer" aria-hidden="true"></span><h1 class="title">$(html_escape "${POST_TITLE:-}")</h1></div>
-        <span class="home-spacer" aria-hidden="true"></span><div class="contents">
+        <div class="heading heading-page"><h1 class="title">$(html_escape "${POST_TITLE:-}")</h1></div>
+        <div class="contents">
         $(echo "${POST_CONTENTS:-}")
         <div class="tags">$(for i in $TAGS; do echo "<a href=\"../tag/$i/\">$(html_escape "$i")</a>"; done;)</div>
+        <a href="$URL_PREFIX/" class="home">&#x2190;&#xFE0E; $(html_escape "${BLOG_TITLE:-}")</a>
         </div>
           </div>
         </div>
diff --git a/share/jenny/layout/post.sh b/share/jenny/layout/post.sh
index 364c237..ef6c18e 100755
--- a/share/jenny/layout/post.sh
+++ b/share/jenny/layout/post.sh
@@ -250,8 +250,21 @@ cat << _EOF_
         .heading .title { color: var(--text-heading); display: table-cell; margin: 0 0 1em; font-weight:bold; max-width: 500px;  }
         .heading .title:hover { text-decoration: underline; }
         .heading .stamp,
-        .home { display: inline-block; text-align:right; margin-right: 1.5em; }
-        .home { text-decoration: none; width: 5.4em; margin-bottom: 1.5em; text-align: left;  color: var(--link-home); } .home:hover { color: var(--link-home-hover); }
+        /* The way back sits after the text, where a reader finishes, rather
+           than beside it. Held in a fixed column before the body it wrapped
+           onto its own line once the body no longer fit next to it, leaving a
+           stray arrow floating between the title and the first paragraph on a
+           phone -- and on wider screens it reserved a column the body then had
+           to be indented past. Below the text it needs no column at either
+           width, so the body starts at the left margin everywhere.
+
+           It is a second route home: the site title in the header links there
+           from every page. Kept because the end of a long read is exactly
+           where that is useful, and named rather than left as a bare glyph so
+           it says where it goes. */
+        .home { display: block; margin-top: 2.5em; text-decoration: none;
+                color: var(--link-home); }
+        .home:hover { color: var(--link-home-hover); }
         .heading .stamp { color: var(--text-muted); }
         .contents { display: inline-block; max-width: 60ch; vertical-align: top; width: 100%; }
         .contents :first-child { margin-top: 0; }
@@ -361,9 +374,10 @@ cat << _EOF_
       <header><a href="$URL_PREFIX/">$(html_escape "${BLOG_TITLE:-}")</a></header>
       <article>
       <div class="heading$([ -n "${IS_PAGE:-}" ] && echo ' heading-page')"><a href="${POST_URL:-}">$([ -z "${IS_PAGE:-}" ] && printf '<span class="stamp">%s</span>' "$("$DATE" -d "${POST_DATE_RFC822:-}" +%m/%d/%Y 2>/dev/null || printf '%s' "${POST_DATE:-}")")<h1 class="title">$(html_escape "${POST_TITLE:-}")</h1></a></div>
-        <a href="$URL_PREFIX/" class="home">&#x2190;&#xFE0E;</a><div class="contents">
+        <div class="contents">
         $(echo "${POST_CONTENTS:-}")
         <div class="tags">$(for i in $TAGS; do echo "<a href=\"../tag/$i/\">$(html_escape "$i")</a>"; done;)</div>
+        <a href="$URL_PREFIX/" class="home">&#x2190;&#xFE0E; $(html_escape "${BLOG_TITLE:-}")</a>
         </div>
       </article>
 $(diagram_script)
diff --git a/test/regressions.sh b/test/regressions.sh
index 8915343..0c385ea 100755
--- a/test/regressions.sh
+++ b/test/regressions.sh
@@ -1313,17 +1313,19 @@ assert_file_exists "$nop/.dist/fallback.html"
 it "and does so without complaint"
 assert_eq "$stderr_output" ""
 
-it "a page has no back arrow"
-# A post's arrow goes back to the index it was listed on. A page is not listed
-# anywhere -- it is reached by its own stable path -- so an arrow to the index
-# points at a place the reader did not come from. The breadcrumb in the header
-# is what a page navigates by instead.
-assert_file_not_contains "$pg/.dist/study/consciousness.html" 'class="home"'
-
-it "the arrow's column is still reserved so the body lines up with a post's"
-# Removing the arrow must not move the body: a post indents by the arrow's
-# width, and a page has to match it.
-assert_file_contains "$pg/.dist/study/consciousness.html" 'class="home-spacer"'
+it "a page ends with the way back, like a post"
+# Held in a column before the body the arrow wrapped onto its own line once
+# the body no longer fit beside it, leaving a stray glyph between the title and
+# the first paragraph on a phone. Below the text it needs no column at any
+# width, and posts and pages end the same way.
+assert_file_contains "$pg/.dist/study/consciousness.html" 'class="home"'
+
+it "the way back comes after the body, not before it"
+tags_line=$("$GREP" -n 'class="tags"' "$pg/.dist/study/consciousness.html" | head -1 | cut -d: -f1)
+home_line=$("$GREP" -n 'class="home"' "$pg/.dist/study/consciousness.html" | head -1 | cut -d: -f1)
+[ -n "$home_line" ] && [ "$home_line" -gt "$tags_line" ] \
+  && pass "back link follows the content" \
+  || fail "back link at line $home_line must come after the body at line $tags_line"
 
 it "a blog with only pages and no posts still builds"
 only="$TMPROOT/onlypages"
@@ -1685,22 +1687,13 @@ assert_file_not_contains "$nav_sb/.dist/test.html" '.page-nav { position: absolu
 it "a page with no nav takes the body indent, not the sidebar column"
 assert_file_contains "$nav_sb/.dist/plain.html" '.page-wrapper:not(:has(.page-nav))'
 
-it "a page with no nav reserves the arrow column the way a post does"
-# A post's indent comes from the back arrow sitting inline before the body, so
-# the two reflow together when the screen narrows. A page has no arrow, so it
-# reserves the same column with a spacer of the same size; a hardcoded padding
-# matched at one width and was 97px out at 735px.
-# Anchored to the body, not just "a spacer exists somewhere": the heading has
-# one too, and a check that matched either passed with the body's spacer
-# deleted -- which drops the body indent from 110px to 0 against a post's 110.
-assert_file_contains "$nav_sb/.dist/plain.html" '<span class="home-spacer" aria-hidden="true"></span><div class="contents">'
-assert_file_contains "$nav_sb/.dist/plain.html" '.home-spacer { display: inline-block'
-
-it "the spacer is hidden from assistive technology"
-assert_file_contains "$nav_sb/.dist/plain.html" 'class="home-spacer" aria-hidden="true"'
-
-it "a page with a sidebar drops the arrow spacer"
-assert_file_contains "$nav_sb/.dist/test.html" '.page-wrapper:has(.page-nav) .home-spacer { display: none'
+it "the body starts at the left margin, with no column reserved"
+# The indent existed only to make room for the back arrow beside the body.
+# With the arrow below the text there is nothing to clear, so the text uses the
+# full measure -- on a 375px phone the reserved column cost a quarter of the
+# line, 42 characters down to 32.
+assert_file_not_contains "$nav_sb/.dist/plain.html" 'home-spacer'
+assert_file_contains "$nav_sb/.dist/plain.html" '.home { display: block; margin-top: 2.5em;'
 
 it "a sidebar widens the article rather than narrowing the body"
 # 74ch is sized for a post: arrow column plus a 60ch body. Letting the sidebar
@@ -1719,11 +1712,10 @@ post_file=$(find "$nav_sb/.dist/post" -name '*.html' | head -1)
 post_contents=$("$GREP" -o '^        \.contents { [^}]*}' "$post_file" | head -1)
 assert_eq "$page_contents" "$post_contents"
 
-it "the heading reserves the same column the body does"
-# On a post the stamp holds the left column, so the title starts where the
-# body starts. A page has no stamp; without the same spacer in the heading the
-# title sat at the far left while its own body was indented past it.
-assert_file_contains "$nav_sb/.dist/plain.html" 'class="heading heading-page"><span class="home-spacer"'
+it "the heading holds no reserved column either"
+# Nothing sits beside the title any more, so it starts where its body starts
+# without a spacer to keep in step.
+assert_file_contains "$nav_sb/.dist/plain.html" 'class="heading heading-page"><h1 class="title">'
 
 it "the heading has no breakpoint of its own"
 # A post has no media query at all -- heading and body collapse together by
@@ -1733,10 +1725,29 @@ it "the heading has no breakpoint of its own"
 narrow_head=$("$SED" -n '/@media (max-width: 735px)/,/^        }/p' "$nav_sb/.dist/plain.html")
 assert_not_contains "$narrow_head" '.heading-page { margin-left: 0; display: block'
 
-it "a sidebar page does not indent its heading twice"
-# The cell is already offset by the nav column; indenting the heading again
-# would put the title a column right of its own body.
-assert_file_contains "$nav_sb/.dist/test.html" '.page-wrapper:has(.page-nav) .heading-page .home-spacer { display: none'
+it "a post ends with the way back"
+# The post side was unasserted: deleting the link from post.sh entirely broke
+# no test, because every other check here reads a page.
+post_file_bk=$(find "$nav_sb/.dist/post" -name '*.html' | head -1)
+assert_file_contains "$post_file_bk" 'class="home">'
+
+it "a post's back link comes after its body"
+tl=$("$GREP" -n 'class="tags"' "$post_file_bk" | head -1 | cut -d: -f1)
+hl=$("$GREP" -n 'class="home"' "$post_file_bk" | head -1 | cut -d: -f1)
+[ -n "$hl" ] && [ "$hl" -gt "$tl" ] \
+  && pass "post back link follows the content" \
+  || fail "post back link at line $hl must come after the body at line $tl"
+
+it "the back link names where it goes"
+# A bare arrow says only "away". Both layouts render the site title beside it.
+assert_file_contains "$post_file_bk" '&#x2190;&#xFE0E; '
+
+it "posts and pages declare the same back link"
+# The whole point of moving it: both layouts end the same way. Compare the
+# declarations rather than trusting they look alike.
+page_home=$("$GREP" -o '\.home { display: block[^}]*}' "$nav_sb/.dist/plain.html" | head -1)
+post_home=$("$GREP" -o '\.home { display: block[^}]*}' "$(find "$nav_sb/.dist/post" -name '*.html' | head -1)" | head -1)
+assert_eq "$page_home" "$post_home"
 
 it "narrow screens collapse the whole table, not just one cell"
 # A lone cell reverting to block still gets an anonymous table wrapper and
@@ -1790,14 +1801,12 @@ it "a page title keeps a gap above its body"
 # paragraph, 0px where a post has 1em.
 assert_file_contains "$ts/.dist/plain.html" '.heading-page .title { display: inline-block; margin: 0 0 1em;'
 
-it "the arrow column is dropped on narrow screens"
-# The indent reserves a post's back-arrow column. On a phone it costs more
-# than it reserves -- at 375px it takes the measure from 42 characters to 32 --
-# and the body already wraps to the left margin there while the heading, whose
-# title always fits beside the spacer, did not. The title was left hanging
-# 97px right of its own text.
+it "narrow screens need no rule for the arrow column"
+# There is no column to drop: the back link sits below the text at every
+# width, so the narrow block only has to collapse the sidebar.
 narrow_sp=$("$SED" -n '/@media (max-width: 735px)/,/^        }/p' "$ts/.dist/plain.html")
-assert_contains "$narrow_sp" '.home-spacer { display: none; }'
+assert_not_contains "$narrow_sp" 'home-spacer'
+assert_contains "$narrow_sp" '.contents-cell { padding-left: 0; }'
 
 it "the heading is not a flex row"
 # Flex items do not wrap, which is why the heading held its column at widths