just a website
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Accessibility improvements (WCAG 2.1 AA)

Addresses six issues across three severity tiers:

Critical
- Add skip-to-content link (WCAG 2.4.1) via include-before-body
- Add tabindex="-1" to <main> skip link target for Safari compatibility
- Remove incorrect role="menu" from navbar toggle button (WCAG 4.1.2)

Major
- Restore keyboard focus after code copy button blur (WCAG 2.4.7)
- Add explicit focus-visible ring; navbar uses white outline on teal bg
- Add visually-hidden <thead> column headers to post listing table (WCAG 1.3.1)
- Increase listing table touch targets to ~51px via padding-block (WCAG 2.5.5)
- Replace font-size: 4vw with clamp(1.25rem, 4vw, 3rem) (WCAG 1.4.4)

Minor
- Replace Google Fonts @import with self-hosted WOFF2 files (privacy + perf)
- Add underline to listing table links as non-colour indicator (WCAG 1.4.1)
- Add ↗ indicator on external new-tab links; excludes navbar icon links
- Allow navbar brand to wrap at narrow viewports to prevent truncation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

+256 -8
+5 -1
_quarto.yml
··· 7 7 - .well-known/ 8 8 - _redirects 9 9 - _headers 10 + - assets/fonts/ 10 11 website: 11 12 description: "A personal website" 12 13 site-url: https://rorylawless.com ··· 54 55 linkcolor: "#183e4d" 55 56 code-overflow: wrap 56 57 email-obfuscation: references 57 - include-after-body: html/analytics.html 58 + include-before-body: html/skip-link.html 59 + include-after-body: 60 + - html/analytics.html 61 + - html/a11y.html 58 62 metadata-files: 59 63 - drafts.yml
+176 -7
assets/custom.scss
··· 3 3 4 4 /*-- scss:rules --*/ 5 5 6 - $web-font-path: "https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&family=Playfair+Display:ital,wght@0,400..900;1,400..900&display=swap" !default; 6 + // Skip-to-content link (WCAG 2.4.1) 7 + // First element in <body>; hidden off-screen until keyboard-focused, 8 + // then drops into view above the fixed navbar. 9 + .skip-to-content { 10 + background-color: #24617a; 11 + color: #fbf5f5; 12 + padding: 0.5rem 1.25rem; 13 + text-decoration: none; 14 + border-radius: 0 0 4px 4px; 15 + position: fixed; 16 + left: 50%; 17 + transform: translateX(-50%); 18 + top: -10rem; 19 + z-index: 9999; // Bootstrap navbar is z-index: 1030; must exceed it 20 + 21 + &:focus { 22 + top: 0; 23 + outline: none; // The styled box itself is the focus indicator; suppress the 24 + // teal outline that a:focus-visible would add on a teal background. 25 + } 26 + } 7 27 8 - @if $web-font-path { 9 - @import url($web-font-path); 28 + // Visible focus indicators (WCAG 2.4.7) 29 + // theme: none strips Bootstrap's default focus ring; re-establish an 30 + // explicit ring using the site's teal (#24617a), which contrasts at 31 + // 6.36:1 against the off-white page background (#fbf5f5). 32 + a:focus-visible, 33 + button:focus-visible, 34 + [tabindex]:focus-visible, 35 + input:focus-visible, 36 + select:focus-visible, 37 + textarea:focus-visible { 38 + outline: 2px solid #24617a; 39 + outline-offset: 3px; 40 + border-radius: 2px; 41 + } 42 + 43 + // Navbar links sit on the teal (#24617a) background, making the default 44 + // teal outline invisible (1:1 contrast). Switch to off-white (#fbf5f5) 45 + // which contrasts at 6.36:1 against the navbar background. (WCAG 2.4.7) 46 + .navbar a:focus-visible { 47 + outline-color: #fbf5f5; 48 + } 49 + 50 + // Screen-reader-only utility (WCAG 1.3.1) 51 + // Used for table headers in template.ejs. Defined here rather than 52 + // relying on Bootstrap's .visually-hidden to avoid dependence on 53 + // Bootstrap's internal class names across Quarto versions. 54 + .sr-only { 55 + position: absolute; 56 + width: 1px; 57 + height: 1px; 58 + padding: 0; 59 + margin: -1px; 60 + overflow: hidden; 61 + clip: rect(0, 0, 0, 0); 62 + white-space: nowrap; 63 + border: 0; 64 + } 65 + 66 + // Self-hosted fonts — eliminates the third-party Google Fonts dependency. 67 + // WOFF2 files are in assets/fonts/ (copied to _site/assets/fonts/ via 68 + // resources: in _quarto.yml). Trimmed to the weights used on the site: 69 + // Lato 300/400/700 (100 and 900 are unused); Playfair Display as a variable 70 + // font covering the full 400-900 range in a single file per style. 71 + // Root-relative paths (/assets/fonts/...) are passed through by Sass unchanged 72 + // and resolve correctly on the deployed site and in quarto preview. 73 + 74 + @font-face { 75 + font-family: 'Lato'; 76 + font-style: normal; 77 + font-weight: 300; 78 + font-display: swap; 79 + src: url('/assets/fonts/lato-300-normal.woff2') format('woff2'); 80 + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 81 + } 82 + @font-face { 83 + font-family: 'Lato'; 84 + font-style: italic; 85 + font-weight: 300; 86 + font-display: swap; 87 + src: url('/assets/fonts/lato-300-italic.woff2') format('woff2'); 88 + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 89 + } 90 + @font-face { 91 + font-family: 'Lato'; 92 + font-style: normal; 93 + font-weight: 400; 94 + font-display: swap; 95 + src: url('/assets/fonts/lato-400-normal.woff2') format('woff2'); 96 + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 97 + } 98 + @font-face { 99 + font-family: 'Lato'; 100 + font-style: italic; 101 + font-weight: 400; 102 + font-display: swap; 103 + src: url('/assets/fonts/lato-400-italic.woff2') format('woff2'); 104 + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 105 + } 106 + @font-face { 107 + font-family: 'Lato'; 108 + font-style: normal; 109 + font-weight: 700; 110 + font-display: swap; 111 + src: url('/assets/fonts/lato-700-normal.woff2') format('woff2'); 112 + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 113 + } 114 + @font-face { 115 + font-family: 'Lato'; 116 + font-style: italic; 117 + font-weight: 700; 118 + font-display: swap; 119 + src: url('/assets/fonts/lato-700-italic.woff2') format('woff2'); 120 + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 121 + } 122 + @font-face { 123 + font-family: 'Playfair Display'; 124 + font-style: normal; 125 + font-weight: 400 900; 126 + font-display: swap; 127 + src: url('/assets/fonts/playfair-display-400-900-normal.woff2') format('woff2'); 128 + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 129 + } 130 + @font-face { 131 + font-family: 'Playfair Display'; 132 + font-style: italic; 133 + font-weight: 400 900; 134 + font-display: swap; 135 + src: url('/assets/fonts/playfair-display-400-900-italic.woff2') format('woff2'); 136 + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 10 137 } 11 138 12 139 .navbar-container { 13 140 width: 820px !important; 14 141 } 15 142 143 + // Bootstrap sets white-space: nowrap and overflow: hidden on .navbar-brand, 144 + // which causes the site title to truncate with an ellipsis at narrow 145 + // viewports once the clamp() minimum font size kicks in. Allowing wrapping 146 + // keeps the full name readable without shrinking below the accessible minimum. 147 + // .navbar prefix raises specificity to (0,2,0), safely winning over 148 + // Bootstrap's (0,1,0) without relying on source order alone. 149 + .navbar .navbar-brand { 150 + white-space: normal; 151 + overflow: visible; 152 + } 153 + 16 154 .navbar-title { 17 155 font-family: 'Playfair Display', serif; 18 - font-size: 4vw; 156 + // clamp() keeps 4vw responsive scaling but adds rem-based bounds that 157 + // respond to browser text-zoom preferences (vw alone does not). Min of 158 + // 1.25rem ensures readability at narrow viewports; 3rem caps the size 159 + // on very wide screens. (WCAG 1.4.4) 160 + font-size: clamp(1.25rem, 4vw, 3rem); 19 161 } 20 162 21 163 .quarto-listing-table.no-borders, 164 + .quarto-listing-table.no-borders thead, 22 165 .quarto-listing-table.no-borders tbody, 23 166 .quarto-listing-table.no-borders tr, 24 - .quarto-listing-table.no-borders td { 167 + .quarto-listing-table.no-borders td, 168 + .quarto-listing-table.no-borders th { 25 169 border: none !important; 26 170 } 27 171 ··· 35 179 margin-top: 1rem; 36 180 } 37 181 38 - .table> :not(caption)>*>* { 39 - padding: 0; 182 + // Post listing links are distinguished from surrounding text by colour alone. 183 + // Adding an underline provides a non-colour indicator for users with colour 184 + // vision deficiencies, forced-colour modes, or custom stylesheets. (WCAG 1.4.1) 185 + .quarto-listing-table a { 186 + text-decoration: underline; 187 + } 188 + 189 + // Visual indicator for links that open in a new tab. Quarto adds 190 + // target="_blank" to external links but gives no visual or auditory warning. 191 + // Navbar icon links (.nav-link) are excluded — they already carry aria-labels 192 + // that describe their purpose and destination. (WCAG 3.2.5 / best practice) 193 + a[target="_blank"]:not(.nav-link):not(.navbar-brand)::after { 194 + content: "\00a0↗"; 195 + font-size: 0.75em; 196 + vertical-align: 0.15em; 197 + } 198 + 199 + // Scoped to the listing table only (previously a global .table rule that 200 + // stripped padding from every Bootstrap table on the site). Zero horizontal 201 + // padding preserves the flush-left aesthetic; 1rem vertical padding gives 202 + // ~50.7px touch targets, exceeding the WCAG 2.5.5 minimum of 44px. 203 + // Selector targets td (data cells) only — not th — so the padding rule does 204 + // not override the padding: 0 in .sr-only on the visually-hidden thead row. 205 + // (WCAG 2.5.5) 206 + .quarto-listing-table > :not(caption) > * > td { 207 + padding-inline: 0; 208 + padding-block: 1rem; 40 209 }
assets/fonts/lato-300-italic.woff2

This is a binary file and will not be displayed.

assets/fonts/lato-300-normal.woff2

This is a binary file and will not be displayed.

assets/fonts/lato-400-italic.woff2

This is a binary file and will not be displayed.

assets/fonts/lato-400-normal.woff2

This is a binary file and will not be displayed.

assets/fonts/lato-700-italic.woff2

This is a binary file and will not be displayed.

assets/fonts/lato-700-normal.woff2

This is a binary file and will not be displayed.

assets/fonts/playfair-display-400-900-italic.woff2

This is a binary file and will not be displayed.

assets/fonts/playfair-display-400-900-normal.woff2

This is a binary file and will not be displayed.

+6
assets/template.ejs
··· 3 3 ```{=html} 4 4 5 5 <table class="quarto-listing-table table no-borders"> 6 + <thead> 7 + <tr> 8 + <th scope="col" class="sr-only">Post</th> 9 + <th scope="col" class="sr-only">Date</th> 10 + </tr> 11 + </thead> 6 12 <tbody class="list"> 7 13 <% for (const item of items) { %> 8 14 <tr>
+68
html/a11y.html
··· 1 + <!-- Accessibility patches for Quarto-generated behaviour --> 2 + 3 + <!-- Fix #6: Add tabindex="-1" to the skip link target so Safari moves keyboard 4 + focus there when the skip link is activated. Without this, Safari scrolls to 5 + the fragment but leaves focus on the skip link itself — the next Tab press 6 + returns to the first navbar item instead of the main content. tabindex="-1" 7 + makes the element programmatically focusable without inserting it into the 8 + natural Tab order. Targets <main id="quarto-document-content">, which is 9 + present on every page type (index, about, resume, 404, posts). --> 10 + <script> 11 + function patchSkipLinkTarget() { 12 + var main = document.getElementById('quarto-document-content'); 13 + if (main && !main.hasAttribute('tabindex')) { 14 + main.setAttribute('tabindex', '-1'); 15 + } 16 + } 17 + 18 + if (document.readyState === 'loading') { 19 + document.addEventListener('DOMContentLoaded', patchSkipLinkTarget); 20 + } else { 21 + patchSkipLinkTarget(); 22 + } 23 + </script> 24 + 25 + <!-- Fix #4: Remove incorrect role="menu" from the navbar toggle button. 26 + Quarto's internal pandoc template bakes this attribute into the HTML at 27 + render time; it cannot be suppressed via _quarto.yml. The button is 28 + semantically correct as a plain <button> with aria-expanded and 29 + aria-controls already present — no role is needed. 30 + readyState guard: if this end-of-body script is reached after 31 + DOMContentLoaded has already fired (possible on fast parses), the 32 + event listener would never run — so we check first and run immediately 33 + if the DOM is already ready. --> 34 + <script> 35 + function removeNavToggleRole() { 36 + var toggler = document.querySelector('.navbar-toggler[role="menu"]'); 37 + if (toggler) { 38 + toggler.removeAttribute('role'); 39 + } 40 + } 41 + 42 + if (document.readyState === 'loading') { 43 + document.addEventListener('DOMContentLoaded', removeNavToggleRole); 44 + } else { 45 + removeNavToggleRole(); 46 + } 47 + </script> 48 + 49 + <!-- Fix #5: Restore keyboard focus after code copy button activation. 50 + Quarto's onCopySuccess callback calls button.blur() immediately after a 51 + successful copy. This silently drops keyboard focus. requestAnimationFrame 52 + defers the re-focus until after the current synchronous call stack 53 + (including blur()) has completed, so the focus ring reappears without 54 + interfering with the "Copied!" visual feedback. 55 + e.detail === 0 guard: the click event fires for both mouse and keyboard 56 + activations. e.detail is 0 for keyboard-triggered clicks and ≥1 for 57 + mouse clicks. Without this guard, the script would re-focus the button 58 + after every mouse click, potentially moving focus away from wherever the 59 + user clicked next. --> 60 + <script> 61 + document.addEventListener('click', function (e) { 62 + if (e.detail !== 0) { return; } 63 + var btn = e.target.closest('.code-copy-button'); 64 + if (btn) { 65 + requestAnimationFrame(function () { btn.focus(); }); 66 + } 67 + }); 68 + </script>
+1
html/skip-link.html
··· 1 + <a href="#quarto-document-content" class="skip-to-content">Skip to main content</a>