just a website
0
fork

Configure Feed

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

at main 610 lines 24 kB view raw
1<!DOCTYPE html> 2<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"><head> 3 4<meta charset="utf-8"> 5<meta name="generator" content="quarto-1.9.37"> 6 7<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes"> 8 9 10<title>about – Rory Lawless</title> 11<style> 12/* Default styles provided by pandoc. 13** See https://pandoc.org/MANUAL.html#variables-for-html for config info. 14*/ 15code{white-space: pre-wrap;} 16span.smallcaps{font-variant: small-caps;} 17div.columns{display: flex; gap: min(4vw, 1.5em);} 18div.column{flex: auto; overflow-x: auto;} 19div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;} 20ul.task-list{list-style: none;} 21ul.task-list li input[type="checkbox"] { 22 width: 0.8em; 23 margin: 0 0.8em 0.2em -1em; /* quarto-specific, see https://github.com/quarto-dev/quarto-cli/issues/4556 */ 24 vertical-align: middle; 25} 26</style> 27 28 29<script src="site_libs/quarto-nav/quarto-nav.js"></script> 30<script src="site_libs/quarto-nav/headroom.min.js"></script> 31<script src="site_libs/clipboard/clipboard.min.js"></script> 32<script src="site_libs/quarto-html/quarto.js" type="module"></script> 33<script src="site_libs/quarto-html/tabsets/tabsets.js" type="module"></script> 34<script src="site_libs/quarto-html/popper.min.js"></script> 35<script src="site_libs/quarto-html/tippy.umd.min.js"></script> 36<link href="site_libs/quarto-html/tippy.css" rel="stylesheet"> 37<link href="site_libs/quarto-html/quarto-syntax-highlighting-cad2a2f8a56dfe71ee895a68e79c9b9a.css" rel="stylesheet" id="quarto-text-highlighting-styles"> 38<script src="site_libs/bootstrap/bootstrap.min.js"></script> 39<link href="site_libs/bootstrap/bootstrap-icons.css" rel="stylesheet"> 40<link href="site_libs/bootstrap/bootstrap-af4d1ddd89e46b498a8a5f4923be9e26.min.css" rel="stylesheet" append-hash="true" id="quarto-bootstrap" data-mode="light"> 41<script id="quarto-search-options" type="application/json">{ 42 "language": { 43 "search-no-results-text": "No results", 44 "search-matching-documents-text": "matching documents", 45 "search-copy-link-title": "Copy link to search", 46 "search-hide-matches-text": "Hide additional matches", 47 "search-more-match-text": "more match in this document", 48 "search-more-matches-text": "more matches in this document", 49 "search-clear-button-title": "Clear", 50 "search-text-placeholder": "", 51 "search-detached-cancel-button-title": "Cancel", 52 "search-submit-button-title": "Submit", 53 "search-label": "Search" 54 } 55}</script> 56 57 58</head> 59 60<body class="nav-fixed fullcontent quarto-light"> 61 62<div id="quarto-search-results"></div> 63 <header id="quarto-header" class="headroom fixed-top"> 64 <nav class="navbar navbar-expand-lg " data-bs-theme="dark"> 65 <div class="navbar-container container-fluid"> 66 <div class="navbar-brand-container mx-auto"> 67 <a class="navbar-brand" href="./index.html"> 68 <span class="navbar-title">Rory Lawless</span> 69 </a> 70 </div> 71 <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" role="menu" aria-expanded="false" aria-label="Toggle navigation" onclick="if (window.quartoToggleHeadroom) { window.quartoToggleHeadroom(); }"> 72 <span class="navbar-toggler-icon"></span> 73</button> 74 <div class="collapse navbar-collapse" id="navbarCollapse"> 75 <ul class="navbar-nav navbar-nav-scroll ms-auto"> 76 <li class="nav-item"> 77 <a class="nav-link" href="./resume.html"> <i class="bi bi-file-earmark-person-fill" role="img" aria-label="Rory's Resume"> 78</i> 79<span class="menu-text"> </span></a> 80 </li> 81 <li class="nav-item compact"> 82 <a class="nav-link" href="mailto:rory@rorylawless.com"> <i class="bi bi-envelope-at-fill" role="img" aria-label="Email Rory"> 83</i> 84<span class="menu-text"></span></a> 85 </li> 86 <li class="nav-item compact"> 87 <a class="nav-link" href="https://bsky.app/profile/rorylawless.com"> <i class="bi bi-bluesky" role="img" aria-label="Rory's Bluesky"> 88</i> 89<span class="menu-text"></span></a> 90 </li> 91 <li class="nav-item compact"> 92 <a class="nav-link" href="https://www.linkedin.com/in/rory-lawless/"> <i class="bi bi-linkedin" role="img" aria-label="Rory's LinkedIn"> 93</i> 94<span class="menu-text"></span></a> 95 </li> 96 <li class="nav-item compact"> 97 <a class="nav-link" href="https://tangled.org/rorylawless.com"> <i class="bi bi-git" role="img" aria-label="Rory's Tangled"> 98</i> 99<span class="menu-text"></span></a> 100 </li> 101</ul> 102 </div> <!-- /navcollapse --> 103 <div class="quarto-navbar-tools"> 104</div> 105 </div> <!-- /container-fluid --> 106 </nav> 107</header> 108<!-- content --> 109<div id="quarto-content" class="quarto-container page-columns page-rows-contents page-layout-article page-navbar"> 110<!-- sidebar --> 111<!-- margin-sidebar --> 112 113<!-- main --> 114<div class="quarto-about-marquee"> 115 <div class="about-image-container"> 116 </div> 117 <div class="about-contents"> 118 <header id="title-block-header" class="quarto-title-block"></header> <main class="content" id="quarto-document-content"> 119<a href="#quarto-document-content" class="skip-to-content">Skip to main content</a> 120<section id="about-me" class="level2"> 121<h2 data-anchor-id="about-me">About Me</h2> 122<p>Data analyst based in Washington, DC. I apply a political economy and public policy lens to my work across education policy, financial services regulation, and mental health outcomes, with a focus on how policies and the institutions that design them affect people with the least power to shape them.</p> 123<p>Analysis I have worked on has been picked up by the <a href="https://www.ft.com/content/398fe5be-cf44-459c-83d1-5105adef7337?syn-25a6b1a6=1">Financial Times</a> and I have written about my work on the <a href="https://dme.dc.gov/page/inside-dc-education-blog#10082025-2">Inside DC Education Blog</a>.</p> 124 125 126</section> 127</main> 128 </div> 129</div> 130 <!-- /main --> 131<!-- Accessibility patches for Quarto-generated behaviour --> 132 133<!-- Fix #6: Add tabindex="-1" to the skip link target so Safari moves keyboard 134 focus there when the skip link is activated. Without this, Safari scrolls to 135 the fragment but leaves focus on the skip link itself — the next Tab press 136 returns to the first navbar item instead of the main content. tabindex="-1" 137 makes the element programmatically focusable without inserting it into the 138 natural Tab order. Targets <main id="quarto-document-content">, which is 139 present on every page type (index, about, resume, 404, posts). --> 140<script> 141 function patchSkipLinkTarget() { 142 var main = document.getElementById('quarto-document-content'); 143 if (main && !main.hasAttribute('tabindex')) { 144 main.setAttribute('tabindex', '-1'); 145 } 146 } 147 148 if (document.readyState === 'loading') { 149 document.addEventListener('DOMContentLoaded', patchSkipLinkTarget); 150 } else { 151 patchSkipLinkTarget(); 152 } 153</script> 154 155<!-- Fix #4: Remove incorrect role="menu" from the navbar toggle button. 156 Quarto's internal pandoc template bakes this attribute into the HTML at 157 render time; it cannot be suppressed via _quarto.yml. The button is 158 semantically correct as a plain <button> with aria-expanded and 159 aria-controls already present — no role is needed. 160 readyState guard: if this end-of-body script is reached after 161 DOMContentLoaded has already fired (possible on fast parses), the 162 event listener would never run — so we check first and run immediately 163 if the DOM is already ready. --> 164<script> 165 function removeNavToggleRole() { 166 var toggler = document.querySelector('.navbar-toggler[role="menu"]'); 167 if (toggler) { 168 toggler.removeAttribute('role'); 169 } 170 } 171 172 if (document.readyState === 'loading') { 173 document.addEventListener('DOMContentLoaded', removeNavToggleRole); 174 } else { 175 removeNavToggleRole(); 176 } 177</script> 178 179<!-- Fix #5: Restore keyboard focus after code copy button activation. 180 Quarto's onCopySuccess callback calls button.blur() immediately after a 181 successful copy. This silently drops keyboard focus. requestAnimationFrame 182 defers the re-focus until after the current synchronous call stack 183 (including blur()) has completed, so the focus ring reappears without 184 interfering with the "Copied!" visual feedback. 185 e.detail === 0 guard: the click event fires for both mouse and keyboard 186 activations. e.detail is 0 for keyboard-triggered clicks and ≥1 for 187 mouse clicks. Without this guard, the script would re-focus the button 188 after every mouse click, potentially moving focus away from wherever the 189 user clicked next. --> 190<script> 191 document.addEventListener('click', function (e) { 192 if (e.detail !== 0) { return; } 193 var btn = e.target.closest('.code-copy-button'); 194 if (btn) { 195 requestAnimationFrame(function () { btn.focus(); }); 196 } 197 }); 198</script> 199<script id="quarto-html-after-body" type="application/javascript"> 200 window.document.addEventListener("DOMContentLoaded", function (event) { 201 const isCodeAnnotation = (el) => { 202 for (const clz of el.classList) { 203 if (clz.startsWith('code-annotation-')) { 204 return true; 205 } 206 } 207 return false; 208 } 209 const onCopySuccess = function(e) { 210 // button target 211 const button = e.trigger; 212 // don't keep focus 213 button.blur(); 214 // flash "checked" 215 button.classList.add('code-copy-button-checked'); 216 var currentTitle = button.getAttribute("title"); 217 button.setAttribute("title", "Copied!"); 218 let tooltip; 219 if (window.bootstrap) { 220 button.setAttribute("data-bs-toggle", "tooltip"); 221 button.setAttribute("data-bs-placement", "left"); 222 button.setAttribute("data-bs-title", "Copied!"); 223 tooltip = new bootstrap.Tooltip(button, 224 { trigger: "manual", 225 customClass: "code-copy-button-tooltip", 226 offset: [0, -8]}); 227 tooltip.show(); 228 } 229 setTimeout(function() { 230 if (tooltip) { 231 tooltip.hide(); 232 button.removeAttribute("data-bs-title"); 233 button.removeAttribute("data-bs-toggle"); 234 button.removeAttribute("data-bs-placement"); 235 } 236 button.setAttribute("title", currentTitle); 237 button.classList.remove('code-copy-button-checked'); 238 }, 1000); 239 // clear code selection 240 e.clearSelection(); 241 } 242 const getTextToCopy = function(trigger) { 243 const outerScaffold = trigger.parentElement.cloneNode(true); 244 const codeEl = outerScaffold.querySelector('code'); 245 for (const childEl of codeEl.children) { 246 if (isCodeAnnotation(childEl)) { 247 childEl.remove(); 248 } 249 } 250 return codeEl.innerText; 251 } 252 const clipboard = new window.ClipboardJS('.code-copy-button:not([data-in-quarto-modal])', { 253 text: getTextToCopy 254 }); 255 clipboard.on('success', onCopySuccess); 256 if (window.document.getElementById('quarto-embedded-source-code-modal')) { 257 const clipboardModal = new window.ClipboardJS('.code-copy-button[data-in-quarto-modal]', { 258 text: getTextToCopy, 259 container: window.document.getElementById('quarto-embedded-source-code-modal') 260 }); 261 clipboardModal.on('success', onCopySuccess); 262 } 263 var localhostRegex = new RegExp(/^(?:http|https):\/\/localhost\:?[0-9]*\//); 264 var mailtoRegex = new RegExp(/^mailto:/); 265 var filterRegex = new RegExp("https:\/\/rorylawless\.com"); 266 var isInternal = (href) => { 267 return filterRegex.test(href) || localhostRegex.test(href) || mailtoRegex.test(href); 268 } 269 // Inspect non-navigation links and adorn them if external 270 var links = window.document.querySelectorAll('a[href]:not(.nav-link):not(.navbar-brand):not(.toc-action):not(.sidebar-link):not(.sidebar-item-toggle):not(.pagination-link):not(.no-external):not([aria-hidden]):not(.dropdown-item):not(.quarto-navigation-tool):not(.about-link)'); 271 for (var i=0; i<links.length; i++) { 272 const link = links[i]; 273 if (!isInternal(link.href)) { 274 // undo the damage that might have been done by quarto-nav.js in the case of 275 // links that we want to consider external 276 if (link.dataset.originalHref !== undefined) { 277 link.href = link.dataset.originalHref; 278 } 279 } 280 } 281 function tippyHover(el, contentFn, onTriggerFn, onUntriggerFn) { 282 const config = { 283 allowHTML: true, 284 maxWidth: 500, 285 delay: 100, 286 arrow: false, 287 appendTo: function(el) { 288 return el.parentElement; 289 }, 290 interactive: true, 291 interactiveBorder: 10, 292 theme: 'quarto', 293 placement: 'bottom-start', 294 }; 295 if (contentFn) { 296 config.content = contentFn; 297 } 298 if (onTriggerFn) { 299 config.onTrigger = onTriggerFn; 300 } 301 if (onUntriggerFn) { 302 config.onUntrigger = onUntriggerFn; 303 } 304 window.tippy(el, config); 305 } 306 const noterefs = window.document.querySelectorAll('a[role="doc-noteref"]'); 307 for (var i=0; i<noterefs.length; i++) { 308 const ref = noterefs[i]; 309 tippyHover(ref, function() { 310 // use id or data attribute instead here 311 let href = ref.getAttribute('data-footnote-href') || ref.getAttribute('href'); 312 try { href = new URL(href).hash; } catch {} 313 const id = href.replace(/^#\/?/, ""); 314 const note = window.document.getElementById(id); 315 if (note) { 316 return note.innerHTML; 317 } else { 318 return ""; 319 } 320 }); 321 } 322 const xrefs = window.document.querySelectorAll('a.quarto-xref'); 323 const processXRef = (id, note) => { 324 // Strip column container classes 325 const stripColumnClz = (el) => { 326 el.classList.remove("page-full", "page-columns"); 327 if (el.children) { 328 for (const child of el.children) { 329 stripColumnClz(child); 330 } 331 } 332 } 333 stripColumnClz(note) 334 if (id === null || id.startsWith('sec-')) { 335 // Special case sections, only their first couple elements 336 const container = document.createElement("div"); 337 if (note.children && note.children.length > 2) { 338 container.appendChild(note.children[0].cloneNode(true)); 339 for (let i = 1; i < note.children.length; i++) { 340 const child = note.children[i]; 341 if (child.tagName === "P" && child.innerText === "") { 342 continue; 343 } else { 344 container.appendChild(child.cloneNode(true)); 345 break; 346 } 347 } 348 if (window.Quarto?.typesetMath) { 349 window.Quarto.typesetMath(container); 350 } 351 return container.innerHTML 352 } else { 353 if (window.Quarto?.typesetMath) { 354 window.Quarto.typesetMath(note); 355 } 356 return note.innerHTML; 357 } 358 } else { 359 // Remove any anchor links if they are present 360 const anchorLink = note.querySelector('a.anchorjs-link'); 361 if (anchorLink) { 362 anchorLink.remove(); 363 } 364 if (window.Quarto?.typesetMath) { 365 window.Quarto.typesetMath(note); 366 } 367 if (note.classList.contains("callout")) { 368 return note.outerHTML; 369 } else { 370 return note.innerHTML; 371 } 372 } 373 } 374 for (var i=0; i<xrefs.length; i++) { 375 const xref = xrefs[i]; 376 tippyHover(xref, undefined, function(instance) { 377 instance.disable(); 378 let url = xref.getAttribute('href'); 379 let hash = undefined; 380 if (url.startsWith('#')) { 381 hash = url; 382 } else { 383 try { hash = new URL(url).hash; } catch {} 384 } 385 if (hash) { 386 const id = hash.replace(/^#\/?/, ""); 387 const note = window.document.getElementById(id); 388 if (note !== null) { 389 try { 390 const html = processXRef(id, note.cloneNode(true)); 391 instance.setContent(html); 392 } finally { 393 instance.enable(); 394 instance.show(); 395 } 396 } else { 397 // See if we can fetch this 398 fetch(url.split('#')[0]) 399 .then(res => res.text()) 400 .then(html => { 401 const parser = new DOMParser(); 402 const htmlDoc = parser.parseFromString(html, "text/html"); 403 const note = htmlDoc.getElementById(id); 404 if (note !== null) { 405 const html = processXRef(id, note); 406 instance.setContent(html); 407 } 408 }).finally(() => { 409 instance.enable(); 410 instance.show(); 411 }); 412 } 413 } else { 414 // See if we can fetch a full url (with no hash to target) 415 // This is a special case and we should probably do some content thinning / targeting 416 fetch(url) 417 .then(res => res.text()) 418 .then(html => { 419 const parser = new DOMParser(); 420 const htmlDoc = parser.parseFromString(html, "text/html"); 421 const note = htmlDoc.querySelector('main.content'); 422 if (note !== null) { 423 // This should only happen for chapter cross references 424 // (since there is no id in the URL) 425 // remove the first header 426 if (note.children.length > 0 && note.children[0].tagName === "HEADER") { 427 note.children[0].remove(); 428 } 429 const html = processXRef(null, note); 430 instance.setContent(html); 431 } 432 }).finally(() => { 433 instance.enable(); 434 instance.show(); 435 }); 436 } 437 }, function(instance) { 438 }); 439 } 440 let selectedAnnoteEl; 441 const selectorForAnnotation = ( cell, annotation) => { 442 let cellAttr = 'data-code-cell="' + cell + '"'; 443 let lineAttr = 'data-code-annotation="' + annotation + '"'; 444 const selector = 'span[' + cellAttr + '][' + lineAttr + ']'; 445 return selector; 446 } 447 const selectCodeLines = (annoteEl) => { 448 const doc = window.document; 449 const targetCell = annoteEl.getAttribute("data-target-cell"); 450 const targetAnnotation = annoteEl.getAttribute("data-target-annotation"); 451 const annoteSpan = window.document.querySelector(selectorForAnnotation(targetCell, targetAnnotation)); 452 const lines = annoteSpan.getAttribute("data-code-lines").split(","); 453 const lineIds = lines.map((line) => { 454 return targetCell + "-" + line; 455 }) 456 let top = null; 457 let height = null; 458 let parent = null; 459 if (lineIds.length > 0) { 460 //compute the position of the single el (top and bottom and make a div) 461 const el = window.document.getElementById(lineIds[0]); 462 top = el.offsetTop; 463 height = el.offsetHeight; 464 parent = el.parentElement.parentElement; 465 if (lineIds.length > 1) { 466 const lastEl = window.document.getElementById(lineIds[lineIds.length - 1]); 467 const bottom = lastEl.offsetTop + lastEl.offsetHeight; 468 height = bottom - top; 469 } 470 if (top !== null && height !== null && parent !== null) { 471 // cook up a div (if necessary) and position it 472 let div = window.document.getElementById("code-annotation-line-highlight"); 473 if (div === null) { 474 div = window.document.createElement("div"); 475 div.setAttribute("id", "code-annotation-line-highlight"); 476 div.style.position = 'absolute'; 477 parent.appendChild(div); 478 } 479 div.style.top = top - 2 + "px"; 480 div.style.height = height + 4 + "px"; 481 div.style.left = 0; 482 let gutterDiv = window.document.getElementById("code-annotation-line-highlight-gutter"); 483 if (gutterDiv === null) { 484 gutterDiv = window.document.createElement("div"); 485 gutterDiv.setAttribute("id", "code-annotation-line-highlight-gutter"); 486 gutterDiv.style.position = 'absolute'; 487 const codeCell = window.document.getElementById(targetCell); 488 const gutter = codeCell.querySelector('.code-annotation-gutter'); 489 gutter.appendChild(gutterDiv); 490 } 491 gutterDiv.style.top = top - 2 + "px"; 492 gutterDiv.style.height = height + 4 + "px"; 493 } 494 selectedAnnoteEl = annoteEl; 495 } 496 }; 497 const unselectCodeLines = () => { 498 const elementsIds = ["code-annotation-line-highlight", "code-annotation-line-highlight-gutter"]; 499 elementsIds.forEach((elId) => { 500 const div = window.document.getElementById(elId); 501 if (div) { 502 div.remove(); 503 } 504 }); 505 selectedAnnoteEl = undefined; 506 }; 507 // Handle positioning of the toggle 508 window.addEventListener( 509 "resize", 510 throttle(() => { 511 elRect = undefined; 512 if (selectedAnnoteEl) { 513 selectCodeLines(selectedAnnoteEl); 514 } 515 }, 10) 516 ); 517 function throttle(fn, ms) { 518 let throttle = false; 519 let timer; 520 return (...args) => { 521 if(!throttle) { // first call gets through 522 fn.apply(this, args); 523 throttle = true; 524 } else { // all the others get throttled 525 if(timer) clearTimeout(timer); // cancel #2 526 timer = setTimeout(() => { 527 fn.apply(this, args); 528 timer = throttle = false; 529 }, ms); 530 } 531 }; 532 } 533 // Attach click handler to the DT 534 const annoteDls = window.document.querySelectorAll('dt[data-target-cell]'); 535 for (const annoteDlNode of annoteDls) { 536 annoteDlNode.addEventListener('click', (event) => { 537 const clickedEl = event.target; 538 if (clickedEl !== selectedAnnoteEl) { 539 unselectCodeLines(); 540 const activeEl = window.document.querySelector('dt[data-target-cell].code-annotation-active'); 541 if (activeEl) { 542 activeEl.classList.remove('code-annotation-active'); 543 } 544 selectCodeLines(clickedEl); 545 clickedEl.classList.add('code-annotation-active'); 546 } else { 547 // Unselect the line 548 unselectCodeLines(); 549 clickedEl.classList.remove('code-annotation-active'); 550 } 551 }); 552 } 553 const findCites = (el) => { 554 const parentEl = el.parentElement; 555 if (parentEl) { 556 const cites = parentEl.dataset.cites; 557 if (cites) { 558 return { 559 el, 560 cites: cites.split(' ') 561 }; 562 } else { 563 return findCites(el.parentElement) 564 } 565 } else { 566 return undefined; 567 } 568 }; 569 var bibliorefs = window.document.querySelectorAll('a[role="doc-biblioref"]'); 570 for (var i=0; i<bibliorefs.length; i++) { 571 const ref = bibliorefs[i]; 572 const citeInfo = findCites(ref); 573 if (citeInfo) { 574 tippyHover(citeInfo.el, function() { 575 var popup = window.document.createElement('div'); 576 citeInfo.cites.forEach(function(cite) { 577 var citeDiv = window.document.createElement('div'); 578 citeDiv.classList.add('hanging-indent'); 579 citeDiv.classList.add('csl-entry'); 580 var biblioDiv = window.document.getElementById('ref-' + cite); 581 if (biblioDiv) { 582 citeDiv.innerHTML = biblioDiv.innerHTML; 583 } 584 popup.appendChild(citeDiv); 585 }); 586 return popup.innerHTML; 587 }); 588 } 589 } 590 }); 591 </script> 592</div> <!-- /content --> 593<footer class="footer"> 594 <div class="nav-footer"> 595 <div class="nav-footer-left"> 596 &nbsp; 597 </div> 598 <div class="nav-footer-center"> 599<p>Made with <a href="https://quarto.org/">Quarto</a> in DC.<br><a href="https://rorylawless.com/">Website</a> © 2026 by <a href="./about.html">Rory Lawless</a> is licensed under <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a>.</p> 600</div> 601 <div class="nav-footer-right"> 602 &nbsp; 603 </div> 604 </div> 605</footer> 606 607 608 609 610</body></html>