ssh git@grit.deadkey.org man

Commit 7d61ceb h

Pat Ambrosio · 26-09-01 09:14

Make the deep-link mark an ordinary link in the corner

Changes: 3 files +63 -92

Changes (Side-by-Side)

share/jenny/layout/page.sh +23 -42

cat << _EOF_
890 while their siblings were bold. Deeper headings stay regular -- they 890 while their siblings were bold. Deeper headings stay regular -- they
891 sit inside a section rather than opening one. */ 891 sit inside a section rather than opening one. */
892 .contents :is(h1, h2) { font-weight: bold; } 892 .contents :is(h1, h2) { font-weight: bold; }
893 /* Deep-link mark. Hidden until the passage is hovered or the mark
894 itself is focused, so a page of prose is not littered with symbols;
895 keyboard users reach it by tabbing, which is why focus counts. */
893 /* Deep-link mark, pinned to the top right of the block rather than
894 trailing the last word: at the end of a paragraph it sat wherever
895 the text happened to stop, which is a different place on every
896 block and reads as punctuation. A fixed corner is somewhere the eye
897 can learn.
898
899 The block is the containing block, hence position: relative on it.
900 The mark sits outside the measure, in the margin the bleed already
901 uses, so it never reflows the text it belongs to.
902
903 Hidden until the passage is hovered or the mark itself is focused,
904 so a page of prose is not littered with symbols; keyboard users
905 reach it by tabbing, which is why focus counts. */
906 .contents :is(h1, h2, h3, h4)[id],
907 .contents p[id] { position: relative; }
896 .anchor-link { 908 .anchor-link {
897 margin-left: .4em; text-decoration: none; opacity: 0;
898 color: var(--text-muted); font-weight: normal; cursor: pointer;
909 position: absolute; top: 0; left: 100%;
910 margin-left: .5em; line-height: inherit;
911 text-decoration: none; opacity: 0;
912 color: var(--text-muted); font-weight: normal;
899 } 913 }
900 :is(h1, h2, h3, h4, p):hover > .anchor-link, 914 :is(h1, h2, h3, h4, p):hover > .anchor-link,
901 .anchor-link:focus { opacity: 1; } 915 .anchor-link:focus { opacity: 1; }
cat << _EOF_
910 box-shadow: 0 0 0 .4em var(--bg-secondary); 924 box-shadow: 0 0 0 .4em var(--bg-secondary);
911 border-radius: 2px; 925 border-radius: 2px;
912 } 926 }
913 .copy-toast {
914 position: fixed; left: 50%; bottom: 2em; transform: translate(-50%, .5em);
915 background-color: var(--bg-secondary); color: var(--text-primary);
916 border: 1px solid var(--border-light); border-radius: 3px;
917 padding: .5em 1em; font-size: .875em; z-index: 3000;
918 opacity: 0; pointer-events: none;
919 }
920 .copy-toast.show { opacity: 1; transform: translate(-50%, 0); }
921 /* Every heading is body size, which keeps the page even but leaves a 927 /* Every heading is body size, which keeps the page even but leaves a
922 section heading reading as a stray line of capitals: uppercase alone 928 section heading reading as a stray line of capitals: uppercase alone
923 does not say "new section". A rule above says it structurally rather 929 does not say "new section". A rule above says it structurally rather
cat << _EOF_
1219 var targets = document.querySelectorAll('.contents :is(h1,h2,h3,h4)[id], .contents p[id]'); 1225 var targets = document.querySelectorAll('.contents :is(h1,h2,h3,h4)[id], .contents p[id]');
1220 if (!targets.length) return; 1226 if (!targets.length) return;
1221 1227
1222 var toast = null;
1223 function flash(msg) {
1224 if (!toast) {
1225 toast = document.createElement('div');
1226 toast.className = 'copy-toast';
1227 document.body.appendChild(toast);
1228 }
1229 toast.textContent = msg;
1230 toast.classList.add('show');
1231 clearTimeout(toast._t);
1232 toast._t = setTimeout(function() { toast.classList.remove('show'); }, 1600);
1233 }
1234
1228 // An ordinary link, not a copy button: following it puts the
1229 // address in the bar, and right-clicking offers copy-link the way
1230 // it does for every other link on the web. Nothing to learn.
1235 targets.forEach(function(el) { 1231 targets.forEach(function(el) {
1236 var a = document.createElement('a'); 1232 var a = document.createElement('a');
1237 a.className = 'anchor-link'; 1233 a.className = 'anchor-link';
1238 a.href = '#' + el.id; 1234 a.href = '#' + el.id;
1239 a.setAttribute('aria-label', 'Copy link to this passage');
1235 a.setAttribute('aria-label', 'Link to this passage');
1240 a.textContent = '\u00B6'; 1236 a.textContent = '\u00B6';
1241 a.addEventListener('click', function(e) {
1242 e.preventDefault();
1243 var url = location.origin + location.pathname + '#' + el.id;
1244 // The address bar is updated either way, so the link is
1245 // obtainable even where the clipboard is unavailable.
1246 history.replaceState(null, '', '#' + el.id);
1247 if (navigator.clipboard && navigator.clipboard.writeText) {
1248 navigator.clipboard.writeText(url).then(
1249 function() { flash('Link copied'); },
1250 function() { flash('Press \u2318/Ctrl+C to copy'); }
1251 );
1252 } else {
1253 flash('Press \u2318/Ctrl+C to copy');
1254 }
1255 });
1256 el.appendChild(a); 1237 el.appendChild(a);
1257 }); 1238 });
1258 1239

share/jenny/layout/post.sh +23 -42

cat << _EOF_
711 while their siblings were bold. Deeper headings stay regular -- they 711 while their siblings were bold. Deeper headings stay regular -- they
712 sit inside a section rather than opening one. */ 712 sit inside a section rather than opening one. */
713 .contents :is(h1, h2) { font-weight: bold; } 713 .contents :is(h1, h2) { font-weight: bold; }
714 /* Deep-link mark. Hidden until the passage is hovered or the mark
715 itself is focused, so a page of prose is not littered with symbols;
716 keyboard users reach it by tabbing, which is why focus counts. */
714 /* Deep-link mark, pinned to the top right of the block rather than
715 trailing the last word: at the end of a paragraph it sat wherever
716 the text happened to stop, which is a different place on every
717 block and reads as punctuation. A fixed corner is somewhere the eye
718 can learn.
719
720 The block is the containing block, hence position: relative on it.
721 The mark sits outside the measure, in the margin the bleed already
722 uses, so it never reflows the text it belongs to.
723
724 Hidden until the passage is hovered or the mark itself is focused,
725 so a page of prose is not littered with symbols; keyboard users
726 reach it by tabbing, which is why focus counts. */
727 .contents :is(h1, h2, h3, h4)[id],
728 .contents p[id] { position: relative; }
717 .anchor-link { 729 .anchor-link {
718 margin-left: .4em; text-decoration: none; opacity: 0;
719 color: var(--text-muted); font-weight: normal; cursor: pointer;
730 position: absolute; top: 0; left: 100%;
731 margin-left: .5em; line-height: inherit;
732 text-decoration: none; opacity: 0;
733 color: var(--text-muted); font-weight: normal;
720 } 734 }
721 :is(h1, h2, h3, h4, p):hover > .anchor-link, 735 :is(h1, h2, h3, h4, p):hover > .anchor-link,
722 .anchor-link:focus { opacity: 1; } 736 .anchor-link:focus { opacity: 1; }
cat << _EOF_
731 box-shadow: 0 0 0 .4em var(--bg-secondary); 745 box-shadow: 0 0 0 .4em var(--bg-secondary);
732 border-radius: 2px; 746 border-radius: 2px;
733 } 747 }
734 .copy-toast {
735 position: fixed; left: 50%; bottom: 2em; transform: translate(-50%, .5em);
736 background-color: var(--bg-secondary); color: var(--text-primary);
737 border: 1px solid var(--border-light); border-radius: 3px;
738 padding: .5em 1em; font-size: .875em; z-index: 3000;
739 opacity: 0; pointer-events: none;
740 }
741 .copy-toast.show { opacity: 1; transform: translate(-50%, 0); }
742 /* Every heading is body size, which keeps the page even but leaves a 748 /* Every heading is body size, which keeps the page even but leaves a
743 section heading reading as a stray line of capitals: uppercase alone 749 section heading reading as a stray line of capitals: uppercase alone
744 does not say "new section". A rule above says it structurally rather 750 does not say "new section". A rule above says it structurally rather
cat << _EOF_
991 var targets = document.querySelectorAll('.contents :is(h1,h2,h3,h4)[id], .contents p[id]'); 997 var targets = document.querySelectorAll('.contents :is(h1,h2,h3,h4)[id], .contents p[id]');
992 if (!targets.length) return; 998 if (!targets.length) return;
993 999
994 var toast = null;
995 function flash(msg) {
996 if (!toast) {
997 toast = document.createElement('div');
998 toast.className = 'copy-toast';
999 document.body.appendChild(toast);
1000 }
1001 toast.textContent = msg;
1002 toast.classList.add('show');
1003 clearTimeout(toast._t);
1004 toast._t = setTimeout(function() { toast.classList.remove('show'); }, 1600);
1005 }
1006
1000 // An ordinary link, not a copy button: following it puts the
1001 // address in the bar, and right-clicking offers copy-link the way
1002 // it does for every other link on the web. Nothing to learn.
1007 targets.forEach(function(el) { 1003 targets.forEach(function(el) {
1008 var a = document.createElement('a'); 1004 var a = document.createElement('a');
1009 a.className = 'anchor-link'; 1005 a.className = 'anchor-link';
1010 a.href = '#' + el.id; 1006 a.href = '#' + el.id;
1011 a.setAttribute('aria-label', 'Copy link to this passage');
1007 a.setAttribute('aria-label', 'Link to this passage');
1012 a.textContent = '\u00B6'; 1008 a.textContent = '\u00B6';
1013 a.addEventListener('click', function(e) {
1014 e.preventDefault();
1015 var url = location.origin + location.pathname + '#' + el.id;
1016 // The address bar is updated either way, so the link is
1017 // obtainable even where the clipboard is unavailable.
1018 history.replaceState(null, '', '#' + el.id);
1019 if (navigator.clipboard && navigator.clipboard.writeText) {
1020 navigator.clipboard.writeText(url).then(
1021 function() { flash('Link copied'); },
1022 function() { flash('Press \u2318/Ctrl+C to copy'); }
1023 );
1024 } else {
1025 flash('Press \u2318/Ctrl+C to copy');
1026 }
1027 });
1028 el.appendChild(a); 1009 el.appendChild(a);
1029 }); 1010 });
1030 1011

test/regressions.sh +17 -8

assert_file_contains "$dg/.dist/post/charted.html" '.contents :is(h1, h2) {'
1423 assert_file_contains "$dg/.dist/chart.html" 'width: 3ch;' 1423 assert_file_contains "$dg/.dist/chart.html" 'width: 3ch;'
1424 assert_file_contains "$dg/.dist/chart.html" '.contents :is(h1, h2)::before {' 1424 assert_file_contains "$dg/.dist/chart.html" '.contents :is(h1, h2)::before {'
1425 1425
1426 it "a passage offers a mark that copies its link"
1426 it "a passage offers a mark linking to itself"
1427 # Ids alone are undiscoverable -- a reader would have to open the source to 1427 # Ids alone are undiscoverable -- a reader would have to open the source to
1428 # find one. Hovering a heading or paragraph reveals a mark that copies the
1429 # full URL to that place.
1428 # find one. Hovering a heading or paragraph reveals an ordinary link to it.
1430 assert_file_contains "$dg/.dist/chart.html" "a.className = 'anchor-link';" 1429 assert_file_contains "$dg/.dist/chart.html" "a.className = 'anchor-link';"
1431 assert_file_contains "$dg/.dist/post/charted.html" "a.className = 'anchor-link';" 1430 assert_file_contains "$dg/.dist/post/charted.html" "a.className = 'anchor-link';"
1432 1431
1432 it "the mark is a plain link, not a copy button"
1433 # Following it puts the address in the bar, and right-clicking offers
1434 # copy-link the way it does for every other link on the web. Nothing to learn,
1435 # and nothing to go wrong where the clipboard is unavailable.
1436 assert_file_not_contains "$dg/.dist/chart.html" "navigator.clipboard"
1437 assert_file_not_contains "$dg/.dist/chart.html" "copy-toast"
1438
1439 it "the mark is pinned to the top right of its block"
1440 # Trailing the last word put it wherever the text happened to stop, which is a
1441 # different place on every block and reads as punctuation. A fixed corner is
1442 # somewhere the eye can learn. Measured in Chrome: flush with the block top,
1443 # 8px past its right edge, and no mark overflows the viewport at 700px.
1444 assert_file_contains "$dg/.dist/chart.html" 'position: absolute; top: 0; left: 100%;'
1445 assert_file_contains "$dg/.dist/chart.html" '.contents p[id] { position: relative; }'
1446
1433 it "the mark is hidden until the passage is hovered or the mark focused" 1447 it "the mark is hidden until the passage is hovered or the mark focused"
1434 # Otherwise a page of prose is littered with symbols. Focus counts too, or a 1448 # Otherwise a page of prose is littered with symbols. Focus counts too, or a
1435 # keyboard user could never reach it. 1449 # keyboard user could never reach it.
1436 assert_file_contains "$dg/.dist/chart.html" '.anchor-link:focus { opacity: 1; }' 1450 assert_file_contains "$dg/.dist/chart.html" '.anchor-link:focus { opacity: 1; }'
1437 assert_file_contains "$dg/.dist/chart.html" '@media (hover: none) { .anchor-link { display: none; } }' 1451 assert_file_contains "$dg/.dist/chart.html" '@media (hover: none) { .anchor-link { display: none; } }'
1438 1452
1439 it "the link is obtainable even without the clipboard"
1440 # The address bar is updated whether or not the copy succeeds, so the reader
1441 # can always take the link from there.
1442 assert_file_contains "$dg/.dist/chart.html" "history.replaceState(null, '', '#' + el.id);"
1443
1444 it "following a permalink marks what was linked" 1453 it "following a permalink marks what was linked"
1445 # A reader landing mid-document needs to see which passage was meant. 1454 # A reader landing mid-document needs to see which passage was meant.
1446 assert_file_contains "$dg/.dist/chart.html" "t.classList.add('target-flash');" 1455 assert_file_contains "$dg/.dist/chart.html" "t.classList.add('target-flash');"

Raw

commit 7d61ceb33b32c6c0e4e0bbc26cbdaeb3143b4d58
Author: Pat Ambrosio <cp.ambrosio@gmail.com>
Date:   Tue Sep 1 09:14:52 2026 +0800

    Make the deep-link mark an ordinary link in the corner
    
    Two changes to a feature that was doing more than it needed to.
    
    The mark no longer copies to the clipboard. It is a plain anchor, so
    following it puts the address in the bar and right-clicking offers
    copy-link the way it does for every other link on the web -- nothing to
    learn, nothing to fail where the clipboard is unavailable, and no toast
    to explain what just happened. The toast styles go with it; nothing
    created the element any more.
    
    And it is pinned to the top right of its block rather than trailing the
    last word. At the end of a paragraph it sat wherever the text happened to
    stop, which is a different place on every block and reads as punctuation
    attached to the final sentence. In the corner the marks line up in a
    column the eye can learn, and being absolutely positioned they cannot
    reflow the text they belong to.
    
    The block is the containing block, so it takes position: relative. The
    mark sits in the margin the figure bleed already uses; measured at 1440
    and at 700, no mark overflows the viewport and the page gains no
    sideways scroll. Both layouts measured: flush with the block top, 8px
    past its right edge.

diff --git a/share/jenny/layout/page.sh b/share/jenny/layout/page.sh
index 3c48745..a8ad0d9 100755
--- a/share/jenny/layout/page.sh
+++ b/share/jenny/layout/page.sh
@@ -890,12 +890,26 @@ cat << _EOF_
            while their siblings were bold. Deeper headings stay regular -- they
            sit inside a section rather than opening one. */
         .contents :is(h1, h2) { font-weight: bold; }
-        /* Deep-link mark. Hidden until the passage is hovered or the mark
-           itself is focused, so a page of prose is not littered with symbols;
-           keyboard users reach it by tabbing, which is why focus counts. */
+        /* Deep-link mark, pinned to the top right of the block rather than
+           trailing the last word: at the end of a paragraph it sat wherever
+           the text happened to stop, which is a different place on every
+           block and reads as punctuation. A fixed corner is somewhere the eye
+           can learn.
+
+           The block is the containing block, hence position: relative on it.
+           The mark sits outside the measure, in the margin the bleed already
+           uses, so it never reflows the text it belongs to.
+
+           Hidden until the passage is hovered or the mark itself is focused,
+           so a page of prose is not littered with symbols; keyboard users
+           reach it by tabbing, which is why focus counts. */
+        .contents :is(h1, h2, h3, h4)[id],
+        .contents p[id] { position: relative; }
         .anchor-link {
-          margin-left: .4em; text-decoration: none; opacity: 0;
-          color: var(--text-muted); font-weight: normal; cursor: pointer;
+          position: absolute; top: 0; left: 100%;
+          margin-left: .5em; line-height: inherit;
+          text-decoration: none; opacity: 0;
+          color: var(--text-muted); font-weight: normal;
         }
         :is(h1, h2, h3, h4, p):hover > .anchor-link,
         .anchor-link:focus { opacity: 1; }
@@ -910,14 +924,6 @@ cat << _EOF_
           box-shadow: 0 0 0 .4em var(--bg-secondary);
           border-radius: 2px;
         }
-        .copy-toast {
-          position: fixed; left: 50%; bottom: 2em; transform: translate(-50%, .5em);
-          background-color: var(--bg-secondary); color: var(--text-primary);
-          border: 1px solid var(--border-light); border-radius: 3px;
-          padding: .5em 1em; font-size: .875em; z-index: 3000;
-          opacity: 0; pointer-events: none;
-        }
-        .copy-toast.show { opacity: 1; transform: translate(-50%, 0); }
         /* Every heading is body size, which keeps the page even but leaves a
            section heading reading as a stray line of capitals: uppercase alone
            does not say "new section". A rule above says it structurally rather
@@ -1219,40 +1225,15 @@ cat << _EOF_
             var targets = document.querySelectorAll('.contents :is(h1,h2,h3,h4)[id], .contents p[id]');
             if (!targets.length) return;
 
-            var toast = null;
-            function flash(msg) {
-              if (!toast) {
-                toast = document.createElement('div');
-                toast.className = 'copy-toast';
-                document.body.appendChild(toast);
-              }
-              toast.textContent = msg;
-              toast.classList.add('show');
-              clearTimeout(toast._t);
-              toast._t = setTimeout(function() { toast.classList.remove('show'); }, 1600);
-            }
-
+            // An ordinary link, not a copy button: following it puts the
+            // address in the bar, and right-clicking offers copy-link the way
+            // it does for every other link on the web. Nothing to learn.
             targets.forEach(function(el) {
               var a = document.createElement('a');
               a.className = 'anchor-link';
               a.href = '#' + el.id;
-              a.setAttribute('aria-label', 'Copy link to this passage');
+              a.setAttribute('aria-label', 'Link to this passage');
               a.textContent = '\u00B6';
-              a.addEventListener('click', function(e) {
-                e.preventDefault();
-                var url = location.origin + location.pathname + '#' + el.id;
-                // The address bar is updated either way, so the link is
-                // obtainable even where the clipboard is unavailable.
-                history.replaceState(null, '', '#' + el.id);
-                if (navigator.clipboard && navigator.clipboard.writeText) {
-                  navigator.clipboard.writeText(url).then(
-                    function() { flash('Link copied'); },
-                    function() { flash('Press \u2318/Ctrl+C to copy'); }
-                  );
-                } else {
-                  flash('Press \u2318/Ctrl+C to copy');
-                }
-              });
               el.appendChild(a);
             });
 
diff --git a/share/jenny/layout/post.sh b/share/jenny/layout/post.sh
index 1f05596..0785ce5 100755
--- a/share/jenny/layout/post.sh
+++ b/share/jenny/layout/post.sh
@@ -711,12 +711,26 @@ cat << _EOF_
            while their siblings were bold. Deeper headings stay regular -- they
            sit inside a section rather than opening one. */
         .contents :is(h1, h2) { font-weight: bold; }
-        /* Deep-link mark. Hidden until the passage is hovered or the mark
-           itself is focused, so a page of prose is not littered with symbols;
-           keyboard users reach it by tabbing, which is why focus counts. */
+        /* Deep-link mark, pinned to the top right of the block rather than
+           trailing the last word: at the end of a paragraph it sat wherever
+           the text happened to stop, which is a different place on every
+           block and reads as punctuation. A fixed corner is somewhere the eye
+           can learn.
+
+           The block is the containing block, hence position: relative on it.
+           The mark sits outside the measure, in the margin the bleed already
+           uses, so it never reflows the text it belongs to.
+
+           Hidden until the passage is hovered or the mark itself is focused,
+           so a page of prose is not littered with symbols; keyboard users
+           reach it by tabbing, which is why focus counts. */
+        .contents :is(h1, h2, h3, h4)[id],
+        .contents p[id] { position: relative; }
         .anchor-link {
-          margin-left: .4em; text-decoration: none; opacity: 0;
-          color: var(--text-muted); font-weight: normal; cursor: pointer;
+          position: absolute; top: 0; left: 100%;
+          margin-left: .5em; line-height: inherit;
+          text-decoration: none; opacity: 0;
+          color: var(--text-muted); font-weight: normal;
         }
         :is(h1, h2, h3, h4, p):hover > .anchor-link,
         .anchor-link:focus { opacity: 1; }
@@ -731,14 +745,6 @@ cat << _EOF_
           box-shadow: 0 0 0 .4em var(--bg-secondary);
           border-radius: 2px;
         }
-        .copy-toast {
-          position: fixed; left: 50%; bottom: 2em; transform: translate(-50%, .5em);
-          background-color: var(--bg-secondary); color: var(--text-primary);
-          border: 1px solid var(--border-light); border-radius: 3px;
-          padding: .5em 1em; font-size: .875em; z-index: 3000;
-          opacity: 0; pointer-events: none;
-        }
-        .copy-toast.show { opacity: 1; transform: translate(-50%, 0); }
         /* Every heading is body size, which keeps the page even but leaves a
            section heading reading as a stray line of capitals: uppercase alone
            does not say "new section". A rule above says it structurally rather
@@ -991,40 +997,15 @@ cat << _EOF_
             var targets = document.querySelectorAll('.contents :is(h1,h2,h3,h4)[id], .contents p[id]');
             if (!targets.length) return;
 
-            var toast = null;
-            function flash(msg) {
-              if (!toast) {
-                toast = document.createElement('div');
-                toast.className = 'copy-toast';
-                document.body.appendChild(toast);
-              }
-              toast.textContent = msg;
-              toast.classList.add('show');
-              clearTimeout(toast._t);
-              toast._t = setTimeout(function() { toast.classList.remove('show'); }, 1600);
-            }
-
+            // An ordinary link, not a copy button: following it puts the
+            // address in the bar, and right-clicking offers copy-link the way
+            // it does for every other link on the web. Nothing to learn.
             targets.forEach(function(el) {
               var a = document.createElement('a');
               a.className = 'anchor-link';
               a.href = '#' + el.id;
-              a.setAttribute('aria-label', 'Copy link to this passage');
+              a.setAttribute('aria-label', 'Link to this passage');
               a.textContent = '\u00B6';
-              a.addEventListener('click', function(e) {
-                e.preventDefault();
-                var url = location.origin + location.pathname + '#' + el.id;
-                // The address bar is updated either way, so the link is
-                // obtainable even where the clipboard is unavailable.
-                history.replaceState(null, '', '#' + el.id);
-                if (navigator.clipboard && navigator.clipboard.writeText) {
-                  navigator.clipboard.writeText(url).then(
-                    function() { flash('Link copied'); },
-                    function() { flash('Press \u2318/Ctrl+C to copy'); }
-                  );
-                } else {
-                  flash('Press \u2318/Ctrl+C to copy');
-                }
-              });
               el.appendChild(a);
             });
 
diff --git a/test/regressions.sh b/test/regressions.sh
index 6315aac..416e45e 100755
--- a/test/regressions.sh
+++ b/test/regressions.sh
@@ -1423,24 +1423,33 @@ assert_file_contains "$dg/.dist/post/charted.html" '.contents :is(h1, h2) {'
 assert_file_contains "$dg/.dist/chart.html" 'width: 3ch;'
 assert_file_contains "$dg/.dist/chart.html" '.contents :is(h1, h2)::before {'
 
-it "a passage offers a mark that copies its link"
+it "a passage offers a mark linking to itself"
 # Ids alone are undiscoverable -- a reader would have to open the source to
-# find one. Hovering a heading or paragraph reveals a mark that copies the
-# full URL to that place.
+# find one. Hovering a heading or paragraph reveals an ordinary link to it.
 assert_file_contains "$dg/.dist/chart.html" "a.className = 'anchor-link';"
 assert_file_contains "$dg/.dist/post/charted.html" "a.className = 'anchor-link';"
 
+it "the mark is a plain link, not a copy button"
+# Following it puts the address in the bar, and right-clicking offers
+# copy-link the way it does for every other link on the web. Nothing to learn,
+# and nothing to go wrong where the clipboard is unavailable.
+assert_file_not_contains "$dg/.dist/chart.html" "navigator.clipboard"
+assert_file_not_contains "$dg/.dist/chart.html" "copy-toast"
+
+it "the mark is pinned to the top right of its block"
+# Trailing the last word put it wherever the text happened to stop, which is a
+# different place on every block and reads as punctuation. A fixed corner is
+# somewhere the eye can learn. Measured in Chrome: flush with the block top,
+# 8px past its right edge, and no mark overflows the viewport at 700px.
+assert_file_contains "$dg/.dist/chart.html" 'position: absolute; top: 0; left: 100%;'
+assert_file_contains "$dg/.dist/chart.html" '.contents p[id] { position: relative; }' 
+
 it "the mark is hidden until the passage is hovered or the mark focused"
 # Otherwise a page of prose is littered with symbols. Focus counts too, or a
 # keyboard user could never reach it.
 assert_file_contains "$dg/.dist/chart.html" '.anchor-link:focus { opacity: 1; }'
 assert_file_contains "$dg/.dist/chart.html" '@media (hover: none) { .anchor-link { display: none; } }'
 
-it "the link is obtainable even without the clipboard"
-# The address bar is updated whether or not the copy succeeds, so the reader
-# can always take the link from there.
-assert_file_contains "$dg/.dist/chart.html" "history.replaceState(null, '', '#' + el.id);"
-
 it "following a permalink marks what was linked"
 # A reader landing mid-document needs to see which passage was meant.
 assert_file_contains "$dg/.dist/chart.html" "t.classList.add('target-flash');"