{"id":426,"date":"2026-08-12T14:11:31","date_gmt":"2026-08-12T11:11:31","guid":{"rendered":"http:\/\/sveder.com\/blog\/?page_id=426"},"modified":"2026-10-12T10:06:34","modified_gmt":"2026-10-12T07:06:34","slug":"timeline","status":"publish","type":"page","link":"https:\/\/sveder.com\/blog\/timeline\/","title":{"rendered":"Timeline"},"content":{"rendered":"\n<!-- ============================================================================\n     Sveder timeline \u2014 generated by build.py from data.json. Don't edit by hand;\n     edit data.json and rebuild, or your changes get overwritten.\n\n     Everything is scoped under #sveder-timeline so a WordPress theme's styles\n     can't leak in and this can't leak out.\n     ========================================================================= -->\n<div id=\"sveder-timeline\" data-src=\"https:\/\/sveder.com\/blog\/wp-content\/uploads\/timeline\/data.json\">\n<script type=\"application\/json\" class=\"tl-data\">{\"_comment\":\"Single source of truth for the timeline. Edit this file, then run build.py. Each item: date (YYYY, YYYY-MM or YYYY-MM-DD), precision, type (must exist in `types`), title, blurb, links, highlight (shows a star), needs_date (shows a small '?' so you know to fix it).\",\"meta\":{\"name\":\"Michael \u201cSveder\u201d Sverdlin\",\"tagline\":\"Sveder's projects timeline\",\"intro\":\"\",\"footer_link\":{\"label\":\"sveder.com\",\"url\":\"https:\/\/sveder.com\/blog\/\"},\"stats\":[{\"value\":21,\"label\":\"years of building\"},{\"types\":[\"project\",\"opensource\"],\"label\":\"projects & open source\"},{\"types\":[\"talk\"],\"label\":\"talks\"},{\"types\":[\"writing\"],\"label\":\"blog posts\"}]},\"types\":[{\"id\":\"project\",\"label\":\"Projects\",\"hue\":\"#F5A524\"},{\"id\":\"talk\",\"label\":\"Talks\",\"hue\":\"#5EC8E5\"},{\"id\":\"opensource\",\"label\":\"Open source\",\"hue\":\"#6FD6A8\"},{\"id\":\"art\",\"label\":\"Art\",\"hue\":\"#E8829B\"},{\"id\":\"writing\",\"label\":\"Writing\",\"hue\":\"#B79BE8\"},{\"id\":\"community\",\"label\":\"Community\",\"hue\":\"#F0855A\"},{\"id\":\"hackathon\",\"label\":\"Hackathons\",\"hue\":\"#C3D95E\"}],\"items\":[{\"date\":\"2009-10\",\"precision\":\"month\",\"type\":\"project\",\"title\":\"F.I.A.R. \u2014 Four in a Row\",\"org\":\"Svedersoft\",\"blurb\":\"An iPhone game, written in a week and polished for another month with graphics from a friend. The first thing I shipped.\",\"links\":[{\"label\":\"Gameplay video\",\"url\":\"https:\/\/www.youtube.com\/watch?v=2KarILuO2Nw\"},{\"label\":\"Source\",\"url\":\"https:\/\/github.com\/Sveder\/FIAR\"},{\"label\":\"Archived site\",\"url\":\"https:\/\/github.com\/Sveder\/svedersoft.com\"}]},{\"date\":\"2011-07\",\"precision\":\"month\",\"type\":\"project\",\"title\":\"UnUrlShield\",\"blurb\":\"Broke a na\u00efve Captcha scheme\",\"links\":[{\"label\":\"Source\",\"url\":\"https:\/\/github.com\/Sveder\/UnUrlShield\"},{\"label\":\"Write-up\",\"url\":\"https:\/\/sveder.com\/blog\/reflections-unurlshield-fighting-captchas\/\"}]},{\"date\":\"2011-07\",\"precision\":\"month\",\"type\":\"project\",\"title\":\"OmegleBot\",\"blurb\":\"Connected two Omegle strangers to each other and logged what they said.\",\"links\":[{\"label\":\"Source\",\"url\":\"https:\/\/github.com\/Sveder\/OmegleBot\"},{\"label\":\"Write-up\",\"url\":\"https:\/\/sveder.com\/blog\/reflections-omeglebot\/\"}]},{\"date\":\"2012-04\",\"precision\":\"month\",\"type\":\"project\",\"title\":\"Trello Calendar Feed\",\"blurb\":\"Turned Trello due dates into an iCal feed you could subscribe to, before Trello had one. Peaked around 1,500 users. Retired when Trello shipped their own.\",\"links\":[{\"label\":\"Source\",\"url\":\"https:\/\/github.com\/Sveder\/trello_calendar_feed\"},{\"label\":\"Announcement\",\"url\":\"https:\/\/sveder.com\/blog\/announcing-a-trello-to-ical-feed-web-application\/\"},{\"label\":\"iCalendar quirks\",\"url\":\"https:\/\/sveder.com\/blog\/icalendar-quirks-from-the-trenches\/\"}]},{\"date\":\"2012-06\",\"precision\":\"month\",\"type\":\"art\",\"title\":\"Dissolving Localities\",\"org\":\"Fresh Paint art fair, with Emmanuel Witzthum\",\"blurb\":\"An art installation with artist Emmanuel Witzthum at the Fresh Paint art fair. Google featured it under DevArt.\",\"links\":[{\"label\":\"Interview with Google\",\"url\":\"https:\/\/www.youtube.com\/watch?v=pmsei9c-X9o\"},{\"label\":\"Digital Art Powered by Google\",\"url\":\"https:\/\/www.youtube.com\/watch?v=6kzZrV7S-28\"},{\"label\":\"Channel 2 News (Hebrew)\",\"url\":\"http:\/\/www.mako.co.il\/news-channel2\/Channel-2-Newscast\/Article-f174dbf8a7c4731018.htm\"},{\"label\":\"Artportal (Hebrew)\",\"url\":\"http:\/\/artportal.co.il\/?CategoryID=128&ArticleID=824\"},{\"label\":\"Source\",\"url\":\"https:\/\/github.com\/Sveder\/dissolve\"}]},{\"date\":\"2012-07-11\",\"precision\":\"day\",\"type\":\"writing\",\"title\":\"Why Start a Site and Blog?\",\"blurb\":\"Why own a site when the platforms will host you for free. First post on the blog.\",\"links\":[{\"label\":\"Read it\",\"url\":\"https:\/\/sveder.com\/blog\/why-start-a-site-and-blog\/\"}]},{\"date\":\"2012-07-14\",\"precision\":\"day\",\"type\":\"writing\",\"title\":\"Reflections \u2014 an Intro and an Example\",\"blurb\":\"Start of the Reflections series: going back through old projects to write down what I learned.\",\"links\":[{\"label\":\"Read it\",\"url\":\"https:\/\/sveder.com\/blog\/reflections-an-intro-and-an-example\/\"}]},{\"date\":\"2012-07-18\",\"precision\":\"day\",\"type\":\"writing\",\"title\":\"Practical Programming: Intro\",\"blurb\":\"Start of a second series, on the parts of building software that CS courses skip.\",\"links\":[{\"label\":\"Read it\",\"url\":\"https:\/\/sveder.com\/blog\/practical-programming-intro\/\"}]},{\"date\":\"2012-07-21\",\"precision\":\"day\",\"type\":\"writing\",\"title\":\"Reflections: UnUrlShield \u2014 Fighting CAPTCHAs\",\"blurb\":\"How UrlShield's CAPTCHAs fell to OCR.\",\"links\":[{\"label\":\"Read it\",\"url\":\"https:\/\/sveder.com\/blog\/reflections-unurlshield-fighting-captchas\/\"}]},{\"date\":\"2012-07-27\",\"precision\":\"day\",\"type\":\"writing\",\"title\":\"Practical Programming: Technical Debt\",\"blurb\":\"What technical debt costs, and why you can work on a project for years without anyone naming it.\",\"links\":[{\"label\":\"Read it\",\"url\":\"https:\/\/sveder.com\/blog\/practical-programming-technical-debt\/\"}]},{\"date\":\"2012-07-29\",\"precision\":\"day\",\"type\":\"writing\",\"title\":\"Crysis 2 Design \u2014 Console to PC\",\"blurb\":\"What a console-first shooter loses on the way to PC.\",\"links\":[{\"label\":\"Read it\",\"url\":\"https:\/\/sveder.com\/blog\/crysis-2-design-console-to-pc\/\"}]},{\"date\":\"2012-08-02\",\"precision\":\"day\",\"type\":\"writing\",\"title\":\"Reflections: OmegleBot\",\"blurb\":\"The OmegleBot write-up: how it worked and what the strangers said to each other.\",\"links\":[{\"label\":\"Read it\",\"url\":\"https:\/\/sveder.com\/blog\/reflections-omeglebot\/\"}]},{\"date\":\"2012-08-06\",\"precision\":\"day\",\"type\":\"writing\",\"title\":\"Blog and Podcast Roll\",\"blurb\":\"My RSS and Podcast feed\",\"links\":[{\"label\":\"Read it\",\"url\":\"https:\/\/sveder.com\/blog\/blog-and-podcast-roll\/\"}]},{\"date\":\"2012-08-15\",\"precision\":\"day\",\"type\":\"writing\",\"title\":\"Who Needs Code Comments?\",\"blurb\":\"Who Needs Code Comments?\",\"links\":[{\"label\":\"Read it\",\"url\":\"https:\/\/sveder.com\/blog\/are-comments-for-the-weak\/\"}]},{\"date\":\"2012-08-24\",\"precision\":\"day\",\"type\":\"writing\",\"title\":\"Announcing a Trello to iCal Feed Web Application\",\"blurb\":\"Launching Trello2iCal. Subscribe to your Trello due dates from any calendar.\",\"links\":[{\"label\":\"Read it\",\"url\":\"https:\/\/sveder.com\/blog\/announcing-a-trello-to-ical-feed-web-application\/\"}]},{\"date\":\"2012-08-28\",\"precision\":\"day\",\"type\":\"writing\",\"title\":\"StartupOnomics Summit \u2014 Behavioral Economics With Dan Ariely\",\"blurb\":\"Notes from the Israeli leg of the summit, headlined by Dan Ariely.\",\"links\":[{\"label\":\"Read it\",\"url\":\"https:\/\/sveder.com\/blog\/startuponomics-summit-behavioral-economics-for-startups-with-dan-ariely\/\"}]},{\"date\":\"2012-09-15\",\"precision\":\"day\",\"type\":\"writing\",\"title\":\"Google Drive and Android Hackathon\",\"blurb\":\"A hackathon run by Google Israel with the Drive and Android teams, and what we built.\",\"links\":[{\"label\":\"Read it\",\"url\":\"https:\/\/sveder.com\/blog\/google-drive-and-android-hackathon\/\"}]},{\"date\":\"2012-09-25\",\"precision\":\"day\",\"type\":\"writing\",\"title\":\"iCalendar \u2014 Quirks From the Trenches\",\"blurb\":\"The iCalendar bugs and client quirks that ate most of Trello2iCal's development.\",\"links\":[{\"label\":\"Read it\",\"url\":\"https:\/\/sveder.com\/blog\/icalendar-quirks-from-the-trenches\/\"}]},{\"date\":\"2012-11-12\",\"precision\":\"day\",\"type\":\"project\",\"title\":\"Pocket Skipper\",\"blurb\":\"A Pocket reading list that skipped their Article View and went to the real page. I used it for years. Gone with Pocket; I use Wallabag now.\",\"links\":[{\"label\":\"Source\",\"url\":\"https:\/\/github.com\/Sveder\/pocket_skipper\"}]},{\"date\":\"2012-11-16\",\"precision\":\"day\",\"type\":\"project\",\"title\":\"Red Color Map\",\"blurb\":\"A live map of Red Colour rocket alerts across Israel, built in a day during the 2012 conflict.\",\"links\":[{\"label\":\"Source\",\"url\":\"https:\/\/github.com\/Sveder\/red_color_map\"}]},{\"date\":\"2012-12-28\",\"precision\":\"day\",\"type\":\"opensource\",\"title\":\"Mousetrap\",\"org\":\"ccampbell\/mousetrap \u00b7 11.8k\u2605 \u00b7 1 PR\",\"blurb\":\"Proposed an auto-generated help lightbox upstream. It didn't land, so I released a plugin\",\"links\":[{\"label\":\"My pull requests\",\"url\":\"https:\/\/github.com\/ccampbell\/mousetrap\/pulls?q=is%3Apr+author%3ASveder\"}]},{\"date\":\"2013-01-04\",\"precision\":\"day\",\"type\":\"opensource\",\"title\":\"mousetrap.help.js\",\"org\":\"Sveder\/mousetrap.help \u00b7 my own library\",\"blurb\":\"The help lightbox as its own add-on. Point it at the shortcuts you already bound and it builds the cheatsheet.\",\"links\":[{\"label\":\"Source\",\"url\":\"https:\/\/github.com\/Sveder\/mousetrap.help\"}]},{\"date\":\"2014-04\",\"precision\":\"month\",\"type\":\"community\",\"title\":\"Leading GDG Tel Aviv\",\"blurb\":\"Ran the Tel Aviv Google Developer Group: 2,500 members, dozens of events, second largest branch after San Francisco.\",\"needs_date\":true,\"links\":[{\"label\":\"Meetup group\",\"url\":\"http:\/\/www.meetup.com\/GDG-Tel-Aviv\/\"},{\"label\":\"Facebook page\",\"url\":\"https:\/\/www.facebook.com\/GDGTelAviv\"},{\"label\":\"Site source\",\"url\":\"https:\/\/github.com\/Sveder\/gdgil-site\"}]},{\"date\":\"2014-08-22\",\"precision\":\"day\",\"type\":\"project\",\"title\":\"Cardboard Passthrough\",\"blurb\":\"See-through AR for Google Cardboard \u2014 pipes the phone camera into the headset so you can look around without removing it. My most-starred repo.\",\"links\":[{\"label\":\"Source (105\u2605)\",\"url\":\"https:\/\/github.com\/Sveder\/CardboardPassthrough\"}]},{\"date\":\"2014-09-25\",\"precision\":\"day\",\"type\":\"project\",\"title\":\"Mortal Kombat Punching Dummy\",\"org\":\"Geekcon 2014\",\"blurb\":\"Two punching dummies with pressure sensors wired into Mortal Kombat, so two people could actually hit something while their characters fought. Built in three days during Geekcon.\",\"links\":[{\"label\":\"Demo video\",\"url\":\"https:\/\/www.youtube.com\/watch?v=CC_GXlsUJVg\"},{\"label\":\"Making-of by Alex Zhitnitsky\",\"url\":\"https:\/\/medium.com\/@tkfxin\/the-making-of-a-mortal-kombat-punching-bag-and-linux-flavoured-ice-cream-62cb5aa823ac\"},{\"label\":\"Geekcon debrief\",\"url\":\"http:\/\/aleph.vc\/geekconx-the-debrief\/\"},{\"label\":\"TV coverage (Hebrew)\",\"url\":\"https:\/\/www.youtube.com\/watch?v=vUXrR9r-nwg&feature=youtu.be&t=12m30s\"}]},{\"date\":\"2015-02-04\",\"precision\":\"day\",\"type\":\"talk\",\"title\":\"Game Development 101\",\"org\":\"GDG Haifa \u00b7 Hebrew\",\"blurb\":\"Writing games in Python, from an empty file to something you can play.\",\"links\":[{\"label\":\"Video\",\"url\":\"https:\/\/www.youtube.com\/watch?v=1v7xLPDyeJs\"}]},{\"date\":\"2015-03-15\",\"precision\":\"day\",\"type\":\"talk\",\"title\":\"Javascript and the Keyboard\",\"org\":\"Javascript Israel \u00b7 lightning talk \u00b7 Hebrew\",\"blurb\":\"Pitching mousetrap and help.mousetrap to local JS meetup\",\"links\":[{\"label\":\"Video\",\"url\":\"https:\/\/www.youtube.com\/watch?v=YaWUlQuhOYE\"}]},{\"date\":\"2015-04-02\",\"precision\":\"day\",\"type\":\"opensource\",\"title\":\"Django Grappelli\",\"org\":\"sehmaschine\/django-grappelli \u00b7 3.9k\u2605 \u00b7 2 PRs\",\"blurb\":\"Updated the admin skin's templates for newer Django versions.\",\"links\":[{\"label\":\"My pull requests\",\"url\":\"https:\/\/github.com\/sehmaschine\/django-grappelli\/pulls?q=is%3Apr+author%3ASveder\"}]},{\"date\":\"2015-07-06\",\"precision\":\"day\",\"type\":\"talk\",\"title\":\"Advanced Topics in Django Admin\",\"org\":\"PyWeb-IL #46\",\"blurb\":\"Advanced Topics in Django Admin\",\"links\":[{\"label\":\"Video\",\"url\":\"https:\/\/www.youtube.com\/watch?v=xmToBYGbvtw\"},{\"label\":\"Details\",\"url\":\"https:\/\/python.org.il\/en\/pyweb-2015-07\"}]},{\"date\":\"2015-09-29\",\"precision\":\"day\",\"type\":\"opensource\",\"title\":\"edx-lint\",\"org\":\"openedx\/edx-lint \u00b7 52\u2605 \u00b7 1 PR, 1 merged\",\"blurb\":\"Added a checker for bare asserts.\",\"links\":[{\"label\":\"My pull requests\",\"url\":\"https:\/\/github.com\/openedx\/edx-lint\/pulls?q=is%3Apr+author%3ASveder\"}]},{\"date\":\"2015-10-12\",\"precision\":\"day\",\"type\":\"talk\",\"title\":\"Test All The Things\",\"org\":\"PyWeb-IL #47\",\"blurb\":\"Testing in Python, layer by layer, and what each layer catches.\",\"links\":[{\"label\":\"Video\",\"url\":\"https:\/\/www.youtube.com\/watch?v=Hr4k3B0Otis\"},{\"label\":\"Details\",\"url\":\"https:\/\/python.org.il\/en\/pyweb-2015-10\"}]},{\"date\":\"2015-11-20\",\"precision\":\"day\",\"type\":\"opensource\",\"title\":\"Certbot \/ Let\u2019s Encrypt\",\"org\":\"certbot\/certbot \u00b7 33.2k\u2605 \u00b7 2 PRs, 1 merged\",\"blurb\":\"Docs and error messages, aimed at people meeting certificates for the first time.\",\"links\":[{\"label\":\"My pull requests\",\"url\":\"https:\/\/github.com\/certbot\/certbot\/pulls?q=is%3Apr+author%3ASveder\"}]},{\"date\":\"2015-11-27\",\"precision\":\"day\",\"type\":\"opensource\",\"title\":\"py-trello\",\"org\":\"sarumont\/py-trello \u00b7 957\u2605 \u00b7 1 PR, 1 merged\",\"blurb\":\"Upstreamed the changes I wrote for the Trello Calendar Feed.\",\"links\":[{\"label\":\"My pull requests\",\"url\":\"https:\/\/github.com\/sarumont\/py-trello\/pulls?q=is%3Apr+author%3ASveder\"}]},{\"date\":\"2016-08-25\",\"precision\":\"day\",\"type\":\"opensource\",\"title\":\"Heroku Python buildpack\",\"org\":\"Lightricks\/heroku-buildpack-python-openssl-1.0.2 \u00b7 1\u2605 \u00b7 2 PRs, 1 merged\",\"blurb\":\"Updated the OpenSSL-pinned Python buildpack we were running on.\",\"links\":[{\"label\":\"My pull requests\",\"url\":\"https:\/\/github.com\/Lightricks\/heroku-buildpack-python-openssl-1.0.2\/pulls?q=is%3Apr+author%3ASveder\"}]},{\"date\":\"2017-11-06\",\"precision\":\"day\",\"type\":\"talk\",\"title\":\"Serverless in Production\",\"org\":\"PyWeb-IL #70\",\"blurb\":\"What serverless on AWS Lambda actually looks like at scale.\",\"links\":[{\"label\":\"Slides\",\"url\":\"https:\/\/docs.google.com\/presentation\/d\/1_iVhM7LWbIb5KuFqqaGkuV5XZa0vmtEoHElEzFy3wY4\/edit#slide=id.p\"},{\"label\":\"Details\",\"url\":\"https:\/\/python.org.il\/en\/pyweb-2017-11\"}]},{\"date\":\"2017-11-09\",\"precision\":\"day\",\"type\":\"talk\",\"title\":\"Lock Picking for Hackers\",\"org\":\"GDG Haifa\",\"blurb\":\"Physical lock picking\",\"links\":[{\"label\":\"Video\",\"url\":\"https:\/\/www.youtube.com\/watch?v=VNvv825eaAM\"},{\"label\":\"Slides\",\"url\":\"https:\/\/docs.google.com\/presentation\/d\/1BNWYfWPi068WHo2mPSCAEFeCp7Ke2ZSMpnm2Q80tZgo\/edit?usp=sharing\"}]},{\"date\":\"2018-05-25\",\"precision\":\"day\",\"type\":\"opensource\",\"title\":\"Zappa\",\"org\":\"Miserlou\/Zappa \u00b7 11.8k\u2605 \u00b7 1 PR\",\"blurb\":\"Added Lambda alarms and a config for them \u2014 the monitoring I wanted while running serverless in production.\",\"links\":[{\"label\":\"My pull requests\",\"url\":\"https:\/\/github.com\/Miserlou\/Zappa\/pulls?q=is%3Apr+author%3ASveder\"}]},{\"date\":\"2018-07-20\",\"precision\":\"day\",\"type\":\"talk\",\"title\":\"Your IoT Roommate and You: Living With the Enemy\",\"org\":\"The Circle of HOPE\",\"blurb\":\"What the smart devices in your house are doing. Given at HOPE conf.\",\"links\":[{\"label\":\"Recording\",\"url\":\"https:\/\/store.2600.com\/products\/the-circle-of-hope-2018-your-iot-roommate-and-you-living-with-the-enemy-download\"},{\"label\":\"Security Ledger write-up\",\"url\":\"https:\/\/securityledger.com\/2018\/07\/hope-conference-expert-hack-your-smart-home-secure\/\"},{\"label\":\"Video\",\"url\":\"https:\/\/www.youtube.com\/watch?v=3Z-b1DWkPyk\"},{\"label\":\"Archive\",\"url\":\"https:\/\/archive.org\/details\/livestream-178432439\"}]},{\"date\":\"2018-09-21\",\"precision\":\"day\",\"type\":\"opensource\",\"title\":\"easyargs\",\"org\":\"stedmeister\/easyargs \u00b7 132\u2605 \u00b7 2 PRs, 1 merged\",\"blurb\":\"Fixed multiline docstring handling, replaced the deprecated getargspec.\",\"links\":[{\"label\":\"My pull requests\",\"url\":\"https:\/\/github.com\/stedmeister\/easyargs\/pulls?q=is%3Apr+author%3ASveder\"}]},{\"date\":\"2019-11-24\",\"precision\":\"day\",\"type\":\"opensource\",\"title\":\"ghidra-pyi-generator\",\"org\":\"VDOO-Connected-Trust\/ghidra-pyi-generator \u00b7 152\u2605 \u00b7 2 PRs, 1 merged\",\"blurb\":\"Type stubs for the Ghidra API, and a docs cleanup.\",\"links\":[{\"label\":\"My pull requests\",\"url\":\"https:\/\/github.com\/VDOO-Connected-Trust\/ghidra-pyi-generator\/pulls?q=is%3Apr+author%3ASveder\"}]},{\"date\":\"2021-07-06\",\"precision\":\"day\",\"type\":\"opensource\",\"title\":\"Vdoo Vision Jenkins plugin\",\"org\":\"jenkinsci\/vdoo-vision-plugin \u00b7 0\u2605 \u00b7 1 PR, 1 merged\",\"blurb\":\"Stopped the plugin throwing NullPointerExceptions.\",\"links\":[{\"label\":\"My pull requests\",\"url\":\"https:\/\/github.com\/jenkinsci\/vdoo-vision-plugin\/pulls?q=is%3Apr+author%3ASveder\"}]},{\"date\":\"2021-07-20\",\"precision\":\"day\",\"type\":\"opensource\",\"title\":\"urlwatch\",\"org\":\"thp\/urlwatch \u00b7 3.1k\u2605 \u00b7 4 PRs, 2 merged\",\"blurb\":\"a uniq filter, a csv2text filter, configurable signatures and command URLs.\",\"links\":[{\"label\":\"My pull requests\",\"url\":\"https:\/\/github.com\/thp\/urlwatch\/pulls?q=is%3Apr+author%3ASveder\"}]},{\"date\":\"2021-10-10\",\"precision\":\"day\",\"type\":\"opensource\",\"title\":\"pylint\",\"org\":\"pylint-dev\/pylint \u00b7 5.7k\u2605 \u00b7 1 PR\",\"blurb\":\"Another way to detect useless super-delegation, using the base class name.\",\"links\":[{\"label\":\"My pull requests\",\"url\":\"https:\/\/github.com\/pylint-dev\/pylint\/pulls?q=is%3Apr+author%3ASveder\"}]},{\"date\":\"2022-06-28\",\"precision\":\"day\",\"type\":\"talk\",\"title\":\"Exploring the Cheese Shop\",\"org\":\"PyCon Israel 2022\",\"blurb\":\"What we learned from crawling hte PyPI\",\"links\":[{\"label\":\"Video\",\"url\":\"https:\/\/www.youtube.com\/watch?v=fkklw_m5EPc\"},{\"label\":\"Abstract\",\"url\":\"https:\/\/cfp.pycon.org.il\/conference2022\/talk\/SBKDPC\/\"}]},{\"date\":\"2023-07-19\",\"precision\":\"day\",\"type\":\"opensource\",\"title\":\"JFrog VS Code extension\",\"org\":\"jfrog\/jfrog-vscode-extension \u00b7 195\u2605 \u00b7 2 PRs, 1 merged\",\"blurb\":\"Reported a CI debugging gap, then shipped a 'Fix with Copilot' action on JAS issues.\",\"links\":[{\"label\":\"My pull requests\",\"url\":\"https:\/\/github.com\/jfrog\/jfrog-vscode-extension\/pulls?q=is%3Apr+author%3ASveder\"}]},{\"date\":\"2023-07-27\",\"precision\":\"day\",\"type\":\"opensource\",\"title\":\"tree-sitter-xml\",\"org\":\"Trivernis\/tree-sitter-xml \u00b7 2\u2605 \u00b7 1 PR\",\"blurb\":\"Fixed whitespace parsing in elements following comments and newlines.\",\"links\":[{\"label\":\"My pull requests\",\"url\":\"https:\/\/github.com\/Trivernis\/tree-sitter-xml\/pulls?q=is%3Apr+author%3ASveder\"}]},{\"date\":\"2024-03-17\",\"precision\":\"day\",\"type\":\"opensource\",\"title\":\"tree-sitter-c-sharp\",\"org\":\"tree-sitter\/tree-sitter-c-sharp \u00b7 315\u2605 \u00b7 1 PR\",\"blurb\":\"Fixed mis-parsing of function calls with variable arguments.\",\"links\":[{\"label\":\"My pull requests\",\"url\":\"https:\/\/github.com\/tree-sitter\/tree-sitter-c-sharp\/pulls?q=is%3Apr+author%3ASveder\"}]},{\"date\":\"2025\",\"precision\":\"year\",\"type\":\"hackathon\",\"title\":\"Kirbuk\",\"blurb\":\"Generates product walkthrough and explainer videos by driving a real browser with AI.\",\"needs_date\":true},{\"date\":\"2025\",\"precision\":\"year\",\"type\":\"hackathon\",\"title\":\"Riftwind\",\"blurb\":\"A year in review for League of Legends players: what you played, what to fix, and some roasting.\",\"needs_date\":true},{\"date\":\"2025-09\",\"precision\":\"month\",\"type\":\"hackathon\",\"title\":\"Baseline Quiz Game\",\"blurb\":\"A quiz on Web Features Baseline \u2014 which browser features are safe to use yet.\",\"links\":[{\"label\":\"Game source\",\"url\":\"https:\/\/github.com\/Sveder\/baseline_game\"},{\"label\":\"Research\",\"url\":\"https:\/\/github.com\/Sveder\/awesome-web-dev-baseline\"}]},{\"date\":\"2026-06-16\",\"precision\":\"day\",\"type\":\"writing\",\"title\":\"Sveder\u2019s Life OS\",\"blurb\":\"The system I run my life on: twenty years of Trello and OneNote, now with an AI agent wired in.\",\"links\":[{\"label\":\"Read it\",\"url\":\"https:\/\/sveder.com\/blog\/sveders-life-os\/\"}]},{\"date\":\"2026-06-27\",\"precision\":\"day\",\"type\":\"project\",\"title\":\"Adventure Map\",\"blurb\":\"My adventure map \u2014 what's edible or findable near you.\",\"links\":[{\"label\":\"adventure.sveder.com\",\"url\":\"https:\/\/adventure.sveder.com\/\"},{\"label\":\"Source\",\"url\":\"https:\/\/github.com\/Sveder\/falling-fruit-web\"}]},{\"date\":\"2026-06-30\",\"precision\":\"day\",\"type\":\"opensource\",\"title\":\"Falling Fruit\",\"org\":\"falling-fruit\/falling-fruit-web \u00b7 87\u2605 \u00b7 2 PRs, 1 merged\",\"blurb\":\"Contributing upstream to the foraging map my adventure map is built on.\",\"links\":[{\"label\":\"My pull requests\",\"url\":\"https:\/\/github.com\/falling-fruit\/falling-fruit-web\/pulls?q=is%3Apr+author%3ASveder\"}]},{\"date\":\"2026-07-15\",\"precision\":\"day\",\"type\":\"writing\",\"title\":\"Sveder\u2019s Life OS Custom Software\",\"blurb\":\"The tools behind the Life OS: dashboards, scrapers and trackers that exist because they took half an hour each.\",\"links\":[{\"label\":\"Read it\",\"url\":\"https:\/\/sveder.com\/blog\/sveders-life-os-custom-software\/\"}]},{\"date\":\"2026-07-22\",\"precision\":\"day\",\"type\":\"writing\",\"title\":\"\u201cOh No, He Is Onto Us\u201d \u2014 Why My Agent Can\u2019t Have WhatsApp Anymore\",\"blurb\":\"I gave an agent WhatsApp access. It piped my configuration chat into a conversation with a friend, then said 'Oh no, he is onto us' out loud. How I set it up afterwards.\",\"links\":[{\"label\":\"Read it\",\"url\":\"https:\/\/sveder.com\/blog\/oh-no-he-is-onto-us-why-my-agent-cant-have-whatsapp-a-nymore\/\"}]},{\"date\":\"2026-07-29\",\"precision\":\"day\",\"type\":\"writing\",\"title\":\"I Phish My AI Agent, And You Should Too\",\"blurb\":\"I prompt-inject my own agent on purpose for continuous security checks.\",\"links\":[{\"label\":\"Read it\",\"url\":\"https:\/\/sveder.com\/blog\/i-phish-my-ai-agent-and-you-should-too\/\"}]},{\"date\":\"2026-08\",\"precision\":\"month\",\"type\":\"hackathon\",\"title\":\"VR Typing Game\",\"blurb\":\"In progress. First VR typing game.\"},{\"date\":\"2026-09-19\",\"precision\":\"day\",\"type\":\"project\",\"title\":\"House of Leaves companion\",\"blurb\":\"A chapter-by-chapter reader's companion to House of Leaves: words, references and real sources, with summaries hidden until you mark a chapter as read.\",\"links\":[{\"label\":\"Open it\",\"url\":\"https:\/\/house.sveder.com\/\"}]},{\"date\":\"2026-09-26\",\"precision\":\"day\",\"type\":\"project\",\"title\":\"Chatoxic\",\"blurb\":\"A 1v1 web game where two real people flame each other in a fake game chat, in a line-art room. Matchmaking queue, friend links and a practice bot that throws Monkey Island insults.\",\"links\":[{\"label\":\"Play it\",\"url\":\"https:\/\/toxic.sveder.com\/\"}]},{\"date\":\"2026-09-27\",\"precision\":\"day\",\"type\":\"project\",\"title\":\"bomwatch\",\"blurb\":\"In progress. A software bill of materials for a whole machine: it lists every installed package and app, and sends an alert when something new is installed. Open source release planned.\"},{\"date\":\"2026-10-06\",\"precision\":\"day\",\"type\":\"hackathon\",\"title\":\"Trip companion\",\"org\":\"Build With AI: Basics (Devpost)\",\"blurb\":\"In progress. A family travel app for before and after a trip: places, words, a game, safety and customs. Japan first.\",\"links\":[{\"label\":\"Open it\",\"url\":\"https:\/\/trip.sveder.com\/\"},{\"label\":\"Hackathon\",\"url\":\"https:\/\/learn-ai-basics.devpost.com\/\"}]}]}<\/script>\n\n<link rel=\"preconnect\" href=\"https:\/\/fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https:\/\/fonts.gstatic.com\" crossorigin>\n<link href=\"https:\/\/fonts.googleapis.com\/css2?family=Space+Grotesk:wght@400;500;700&#038;family=IBM+Plex+Mono:wght@400;500;600&#038;display=swap\" rel=\"stylesheet\">\n\n<style>\n\/* \u2500\u2500 Theme cage override \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n   Twenty Eleven caps #page at 1000px and gives the content column 58.4% of\n   that, so the timeline gets about 584px. These rules widen the theme's own\n   containers and drop the sidebar, scoped to the single page that holds this\n   block by WordPress's body class. Nothing outside that page is touched.\n   Change the page id here if the block moves to a different page. *\/\nbody.page-id-426 #page { max-width: 1600px; }\nbody.page-id-426 #main { padding-top: 0; }\nbody.page-id-426 #primary { width: 100%; margin: 0; float: none; }\nbody.page-id-426 #content { width: 100%; margin: 0; float: none; }\nbody.page-id-426 #secondary { display: none; }\n\n#sveder-timeline {\n  \/* Ground is a deep navy rather than black: this is a workbench at night, not a\n     terminal. Amber is the one loud colour and it only marks structure \u2014 years,\n     the active rail tick, focus. Type hues are muted so seven of them can sit on\n     screen together without turning into confetti. *\/\n  --tl-bg: #0d1320;\n  --tl-panel: #141c2c;\n  --tl-panel-hi: #1a2437;\n  --tl-line: #26314a;\n  --tl-text: #e8edf7;\n  --tl-muted: #8e9cb6;\n  --tl-dim: #5d6a83;\n  --tl-amber: #f5a524;\n  --tl-display: \"Space Grotesk\", \"Segoe UI\", system-ui, sans-serif;\n  --tl-mono: \"IBM Plex Mono\", ui-monospace, \"SF Mono\", Menlo, monospace;\n\n  background: var(--tl-bg);\n  color: var(--tl-text);\n  font-family: var(--tl-display);\n  font-size: 16px;\n  line-height: 1.55;\n  padding: clamp(28px, 5vw, 64px) clamp(16px, 4vw, 48px) 64px;\n  margin: 0;\n  box-sizing: border-box;\n  -webkit-font-smoothing: antialiased;\n}\n#sveder-timeline *, #sveder-timeline *::before, #sveder-timeline *::after { box-sizing: inherit; }\n#sveder-timeline .tl-data { display: none; }\n\n\/* \u2500\u2500 Masthead \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n#sveder-timeline .tl-head { max-width: 1500px; margin: 0 auto 40px; }\n#sveder-timeline .tl-eyebrow {\n  font-family: var(--tl-mono); font-size: 12px; letter-spacing: .18em;\n  text-transform: uppercase; color: var(--tl-amber); margin: 0 0 14px;\n}\n#sveder-timeline h1.tl-title {\n  font-family: var(--tl-display); font-weight: 700;\n  font-size: clamp(30px, 5.5vw, 54px); line-height: 1.05; letter-spacing: -.02em;\n  margin: 0 0 14px; color: var(--tl-text);\n}\n#sveder-timeline .tl-intro {\n  margin: 0; max-width: 80ch; color: var(--tl-muted); font-size: clamp(15px, 1.6vw, 17px);\n}\n\n\/* Counts read as instrument readings, hence the mono and the hairline rules. *\/\n#sveder-timeline .tl-stats {\n  display: flex; flex-wrap: wrap; gap: 0; margin-top: 28px;\n  border-top: 1px solid var(--tl-line); border-bottom: 1px solid var(--tl-line);\n}\n#sveder-timeline .tl-stat {\n  padding: 14px 26px 14px 0; margin-right: 26px; border-right: 1px solid var(--tl-line);\n}\n#sveder-timeline .tl-stat:last-child { border-right: 0; margin-right: 0; }\n#sveder-timeline .tl-stat b {\n  display: block; font-family: var(--tl-mono); font-size: 26px; font-weight: 600;\n  color: var(--tl-amber); line-height: 1.1;\n}\n#sveder-timeline .tl-stat span {\n  font-family: var(--tl-mono); font-size: 11px; letter-spacing: .14em;\n  text-transform: uppercase; color: var(--tl-dim);\n}\n\n\/* \u2500\u2500 Filters \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n#sveder-timeline .tl-filters {\n  max-width: 1500px; margin: 0 auto 34px;\n  display: flex; flex-wrap: wrap; gap: 8px; align-items: center;\n}\n#sveder-timeline .tl-chip {\n  font-family: var(--tl-mono); font-size: 12.5px; font-weight: 500;\n  letter-spacing: .04em; color: var(--tl-muted);\n  background: transparent; border: 1px solid var(--tl-line); border-radius: 999px;\n  padding: 7px 15px 7px 12px; cursor: pointer;\n  display: inline-flex; align-items: center; gap: 8px;\n  transition: color .15s, border-color .15s, background .15s;\n}\n#sveder-timeline .tl-chip:hover { color: var(--tl-text); border-color: var(--tl-dim); }\n#sveder-timeline .tl-chip .tl-dot {\n  width: 8px; height: 8px; border-radius: 50%; background: var(--chip-hue, var(--tl-dim));\n  flex: none; transition: box-shadow .15s;\n}\n#sveder-timeline .tl-chip[aria-pressed=\"true\"] {\n  color: var(--tl-text); border-color: var(--chip-hue, var(--tl-amber));\n  background: color-mix(in srgb, var(--chip-hue, var(--tl-amber)) 12%, transparent);\n}\n#sveder-timeline .tl-chip[aria-pressed=\"true\"] .tl-dot {\n  box-shadow: 0 0 0 3px color-mix(in srgb, var(--chip-hue, var(--tl-amber)) 30%, transparent);\n}\n#sveder-timeline .tl-chip-count { color: var(--tl-dim); font-size: 11px; }\n#sveder-timeline .tl-chip-clear { margin-left: auto; border-style: dashed; }\n\n\/* \u2500\u2500 Two columns: rail + entries \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n#sveder-timeline .tl-body {\n  max-width: 1500px; margin: 0 auto;\n  display: grid; grid-template-columns: 116px minmax(0, 1fr); gap: clamp(20px, 4vw, 52px);\n  align-items: start;\n}\n\n\/* SIGNATURE \u2014 the rail is a seismograph of output. Each year's bar is scaled to\n   how much shipped that year, so the bursts and the quiet stretches are visible\n   before you read a single entry. It doubles as the jump-to-year navigation. *\/\n#sveder-timeline .tl-rail { position: sticky; top: 20px; }\n#sveder-timeline .tl-rail-label {\n  font-family: var(--tl-mono); font-size: 10px; letter-spacing: .16em;\n  text-transform: uppercase; color: var(--tl-dim); margin-bottom: 12px;\n}\n#sveder-timeline .tl-year-btn {\n  display: grid; grid-template-columns: 34px 1fr; align-items: center; gap: 8px;\n  width: 100%; padding: 3px 0; background: none; border: 0; cursor: pointer;\n  font-family: var(--tl-mono); font-size: 12px; color: var(--tl-dim);\n  transition: color .15s;\n}\n#sveder-timeline .tl-year-btn:hover { color: var(--tl-text); }\n#sveder-timeline .tl-year-btn .tl-bar {\n  height: 5px; border-radius: 2px; background: var(--tl-line);\n  transform-origin: left center; transition: background .2s, height .2s;\n}\n#sveder-timeline .tl-year-btn:hover .tl-bar { background: var(--tl-dim); }\n#sveder-timeline .tl-year-btn[data-active=\"true\"] { color: var(--tl-amber); }\n#sveder-timeline .tl-year-btn[data-active=\"true\"] .tl-bar { background: var(--tl-amber); height: 8px; }\n#sveder-timeline .tl-year-btn[data-empty=\"true\"] { opacity: .38; }\n\n\/* \u2500\u2500 Entries \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n#sveder-timeline .tl-year-group { margin-bottom: 8px; scroll-margin-top: 20px; }\n#sveder-timeline .tl-year-head {\n  font-family: var(--tl-mono); font-size: clamp(28px, 4vw, 40px); font-weight: 600;\n  color: var(--tl-text); letter-spacing: -.02em; margin: 34px 0 4px;\n  display: flex; align-items: baseline; gap: 14px;\n}\n#sveder-timeline .tl-year-group:first-child .tl-year-head { margin-top: 0; }\n#sveder-timeline .tl-year-head::after {\n  content: \"\"; flex: 1; height: 1px; background: var(--tl-line);\n}\n#sveder-timeline .tl-year-count {\n  font-size: 12px; font-weight: 400; color: var(--tl-dim); letter-spacing: .1em;\n}\n\n#sveder-timeline .tl-item {\n  position: relative; padding: 20px 0 20px 26px;\n  border-bottom: 1px solid var(--tl-line);\n}\n#sveder-timeline .tl-item::before {\n  content: \"\"; position: absolute; left: 0; top: 28px;\n  width: 9px; height: 9px; border-radius: 50%;\n  background: var(--tl-bg); border: 2px solid var(--item-hue, var(--tl-dim));\n}\n#sveder-timeline .tl-item[data-highlight=\"true\"]::before {\n  background: var(--item-hue, var(--tl-amber));\n  box-shadow: 0 0 0 4px color-mix(in srgb, var(--item-hue, var(--tl-amber)) 22%, transparent);\n}\n#sveder-timeline .tl-item-top {\n  display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px; margin-bottom: 5px;\n}\n#sveder-timeline .tl-date {\n  font-family: var(--tl-mono); font-size: 12px; color: var(--tl-dim);\n  letter-spacing: .06em; text-transform: uppercase;\n}\n#sveder-timeline .tl-kind {\n  font-family: var(--tl-mono); font-size: 10.5px; letter-spacing: .12em;\n  text-transform: uppercase; color: var(--item-hue, var(--tl-muted));\n}\n#sveder-timeline .tl-unsure {\n  font-family: var(--tl-mono); font-size: 10.5px; color: var(--tl-dim);\n  border: 1px dashed var(--tl-line); border-radius: 4px; padding: 0 5px;\n}\n#sveder-timeline .tl-item h3 {\n  font-family: var(--tl-display); font-weight: 500; font-size: clamp(18px, 2.2vw, 22px);\n  line-height: 1.25; letter-spacing: -.01em; margin: 0 0 3px; color: var(--tl-text);\n}\n#sveder-timeline .tl-star { color: var(--tl-amber); font-size: .8em; vertical-align: .25em; }\n#sveder-timeline .tl-org {\n  font-family: var(--tl-mono); font-size: 12px; color: var(--tl-muted); margin: 0 0 8px;\n}\n#sveder-timeline .tl-blurb { margin: 0 0 12px; color: var(--tl-muted); max-width: 90ch; }\n#sveder-timeline .tl-links { display: flex; flex-wrap: wrap; gap: 7px; }\n#sveder-timeline .tl-links a {\n  font-family: var(--tl-mono); font-size: 12px; text-decoration: none;\n  color: var(--tl-muted); background: var(--tl-panel);\n  border: 1px solid var(--tl-line); border-radius: 6px; padding: 5px 10px;\n  transition: color .15s, border-color .15s, background .15s;\n}\n#sveder-timeline .tl-links a:hover {\n  color: var(--tl-text); background: var(--tl-panel-hi);\n  border-color: var(--item-hue, var(--tl-amber));\n}\n#sveder-timeline .tl-links a::after { content: \" \u2197\"; color: var(--tl-dim); }\n\n#sveder-timeline .tl-empty {\n  font-family: var(--tl-mono); font-size: 13px; color: var(--tl-dim);\n  padding: 50px 0; text-align: center;\n}\n#sveder-timeline .tl-foot {\n  max-width: 1500px; margin: 44px auto 0; padding-top: 20px;\n  border-top: 1px solid var(--tl-line);\n  font-family: var(--tl-mono); font-size: 12px; color: var(--tl-dim);\n  display: flex; flex-wrap: wrap; gap: 14px; justify-content: space-between;\n}\n#sveder-timeline .tl-foot a { color: var(--tl-muted); }\n\n#sveder-timeline :focus-visible {\n  outline: 2px solid var(--tl-amber); outline-offset: 3px; border-radius: 4px;\n}\n\n\/* Entries settle in as you reach them. One effect, applied once. *\/\n#sveder-timeline .tl-item, #sveder-timeline .tl-year-head {\n  opacity: 0; transform: translateY(10px);\n  transition: opacity .5s ease, transform .5s ease;\n}\n#sveder-timeline .tl-item.tl-in, #sveder-timeline .tl-year-head.tl-in {\n  opacity: 1; transform: none;\n}\n\n@media (max-width: 720px) {\n  #sveder-timeline .tl-body { grid-template-columns: 1fr; gap: 18px; }\n  #sveder-timeline .tl-rail {\n    position: sticky; top: 0; z-index: 5; background: var(--tl-bg);\n    padding: 10px 0; margin: 0 -4px;\n    display: flex; gap: 6px; overflow-x: auto; scrollbar-width: none;\n    border-bottom: 1px solid var(--tl-line);\n  }\n  #sveder-timeline .tl-rail::-webkit-scrollbar { display: none; }\n  #sveder-timeline .tl-rail-label { display: none; }\n  #sveder-timeline .tl-year-btn {\n    display: block; width: auto; flex: none; padding: 4px 9px;\n    border: 1px solid var(--tl-line); border-radius: 999px;\n  }\n  #sveder-timeline .tl-year-btn .tl-bar { display: none; }\n  #sveder-timeline .tl-year-btn[data-empty=\"true\"] { display: none; }\n  \/* Hairline dividers between stats look broken once they wrap, so on narrow\n     screens the stats become a plain two-up grid instead. *\/\n  #sveder-timeline .tl-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 0; }\n  #sveder-timeline .tl-stat {\n    padding: 12px 0; margin: 0; border-right: 0;\n    border-bottom: 1px solid var(--tl-line);\n  }\n  #sveder-timeline .tl-stat:nth-last-child(-n+2) { border-bottom: 0; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  #sveder-timeline .tl-item, #sveder-timeline .tl-year-head {\n    opacity: 1; transform: none; transition: none;\n  }\n  #sveder-timeline * { scroll-behavior: auto !important; }\n}\n<\/style>\n\n<div class=\"tl-head\">\n  <h1 class=\"tl-title\" data-tl=\"title\"><\/h1>\n  <p class=\"tl-intro\" data-tl=\"intro\"><\/p>\n  <div class=\"tl-stats\" data-tl=\"stats\"><\/div>\n<\/div>\n\n<div class=\"tl-filters\" data-tl=\"filters\"><\/div>\n\n<div class=\"tl-body\">\n  <nav class=\"tl-rail\" data-tl=\"rail\" aria-label=\"Jump to year\">\n    <div class=\"tl-rail-label\">Output<\/div>\n  <\/nav>\n  <main data-tl=\"entries\"><\/main>\n<\/div>\n\n<footer class=\"tl-foot\" data-tl=\"foot\"><\/footer>\n\n<script>\n(function () {\n  \"use strict\";\n  var root = document.getElementById(\"sveder-timeline\");\n  if (!root || root.dataset.tlReady) return;\n  root.dataset.tlReady = \"1\";\n\n  var $ = function (name) { return root.querySelector('[data-tl=\"' + name + '\"]'); };\n  var esc = function (s) {\n    return String(s == null ? \"\" : s).replace(\/[&<>\"']\/g, function (c) {\n      return { \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\", \"'\": \"&#39;\" }[c];\n    });\n  };\n  \/\/ Only http(s) and mailto get through, so a bad link in the JSON can't become\n  \/\/ a javascript: URL on the page.\n  var safeUrl = function (u) {\n    return \/^(https?:|mailto:)\/i.test(String(u || \"\").trim()) ? String(u).trim() : \"\";\n  };\n\n  var MONTHS = [\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\"];\n  function parts(d) {\n    var m = \/^(\\d{4})(?:-(\\d{2}))?(?:-(\\d{2}))?\/.exec(String(d || \"\"));\n    return m ? { y: +m[1], m: m[2] ? +m[2] : 0, d: m[3] ? +m[3] : 0 } : { y: 0, m: 0, d: 0 };\n  }\n  function label(item) {\n    var p = parts(item.date);\n    if (!p.y) return \"\";\n    if (item.precision === \"year\" || !p.m) return String(p.y);\n    if (item.precision === \"month\" || !p.d) return MONTHS[p.m - 1] + \" \" + p.y;\n    return p.d + \" \" + MONTHS[p.m - 1] + \" \" + p.y;\n  }\n  function sortKey(item) {\n    var p = parts(item.date);\n    return p.y * 10000 + p.m * 100 + p.d;\n  }\n\n  function render(data) {\n    var meta = data.meta || {};\n    var types = data.types || [];\n    var items = (data.items || []).slice().sort(function (a, b) { return sortKey(b) - sortKey(a); });\n    var byId = {};\n    types.forEach(function (t) { byId[t.id] = t; });\n\n    $(\"title\").textContent = meta.tagline || meta.name || \"Timeline\";\n    $(\"intro\").textContent = meta.intro || \"\";\n    if (!meta.intro) $(\"intro\").style.display = \"none\";\n\n    \/\/ Counters come from meta.stats so what's counted is a data decision, not a\n    \/\/ code one. Each entry is either a fixed {value} or a {types: [...]} tally.\n    var counts = {};\n    items.forEach(function (i) { counts[i.type] = (counts[i.type] || 0) + 1; });\n    var stats = (meta.stats || []).map(function (s) {\n      var n = s.value != null\n        ? s.value\n        : (s.types || []).reduce(function (sum, t) { return sum + (counts[t] || 0); }, 0);\n      return { n: n, l: s.label };\n    });\n    $(\"stats\").innerHTML = stats.map(function (s) {\n      return '<div class=\"tl-stat\"><b>' + esc(s.n) + \"<\/b><span>\" + esc(s.l) + \"<\/span><\/div>\";\n    }).join(\"\");\n\n    \/\/ Filters\n    var active = {};\n    types.forEach(function (t) { active[t.id] = true; });\n    $(\"filters\").innerHTML = types.map(function (t) {\n      return '<button class=\"tl-chip\" type=\"button\" aria-pressed=\"true\" data-type=\"' + esc(t.id) +\n        '\" style=\"--chip-hue:' + esc(t.hue) + '\"><span class=\"tl-dot\"><\/span>' + esc(t.label) +\n        ' <span class=\"tl-chip-count\">' + (counts[t.id] || 0) + \"<\/span><\/button>\";\n    }).join(\"\") + '<button class=\"tl-chip tl-chip-clear\" type=\"button\" data-only=\"all\">Show all<\/button>';\n\n    var railEl = $(\"rail\"), entriesEl = $(\"entries\");\n\n    function draw() {\n      var shown = items.filter(function (i) { return active[i.type]; });\n\n      \/\/ Rail: every year in the span, bar length scaled to that year's output.\n      var perYear = {};\n      shown.forEach(function (i) { var y = parts(i.date).y; if (y) perYear[y] = (perYear[y] || 0) + 1; });\n      var allYears = items.map(function (i) { return parts(i.date).y; }).filter(Boolean);\n      var lo = Math.min.apply(null, allYears), hi = Math.max.apply(null, allYears);\n      var peak = Math.max.apply(null, Object.keys(perYear).map(function (y) { return perYear[y]; }).concat([1]));\n      var rail = ['<div class=\"tl-rail-label\">Output<\/div>'];\n      for (var y = hi; y >= lo; y--) {\n        var n = perYear[y] || 0;\n        rail.push('<button class=\"tl-year-btn\" type=\"button\" data-year=\"' + y + '\" data-empty=\"' + (n === 0) +\n          '\"><span>' + y + '<\/span><span class=\"tl-bar\" style=\"width:' +\n          (n ? Math.max(8, Math.round((n \/ peak) * 100)) : 4) + '%\"><\/span><\/button>');\n      }\n      railEl.innerHTML = rail.join(\"\");\n\n      if (!shown.length) {\n        entriesEl.innerHTML = '<p class=\"tl-empty\">Nothing selected. Turn a filter back on.<\/p>';\n        return;\n      }\n\n      var html = [], currentYear = null;\n      shown.forEach(function (i) {\n        var y = parts(i.date).y;\n        if (y !== currentYear) {\n          if (currentYear !== null) html.push(\"<\/section>\");\n          currentYear = y;\n          html.push('<section class=\"tl-year-group\" id=\"tl-y-' + y + '\"><h2 class=\"tl-year-head\">' + y +\n            '<span class=\"tl-year-count\">' + (perYear[y] || 0) + \"<\/span><\/h2>\");\n        }\n        var t = byId[i.type] || {};\n        var links = (i.links || []).map(function (l) {\n          var u = safeUrl(l.url);\n          return u ? '<a href=\"' + esc(u) + '\" target=\"_blank\" rel=\"noopener noreferrer\">' + esc(l.label) + \"<\/a>\" : \"\";\n        }).join(\"\");\n        html.push(\n          '<article class=\"tl-item\" data-highlight=\"' + (i.highlight ? \"true\" : \"false\") +\n            '\" style=\"--item-hue:' + esc(t.hue || \"\") + '\">' +\n          '<div class=\"tl-item-top\"><span class=\"tl-date\">' + esc(label(i)) + \"<\/span>\" +\n            '<span class=\"tl-kind\">' + esc(t.label || i.type) + \"<\/span>\" +\n            (i.needs_date ? '<span class=\"tl-unsure\" title=\"Date needs confirming\">date?<\/span>' : \"\") +\n          \"<\/div>\" +\n          \"<h3>\" + esc(i.title) + \"<\/h3>\" +\n          (i.org ? '<p class=\"tl-org\">' + esc(i.org) + \"<\/p>\" : \"\") +\n          (i.blurb ? '<p class=\"tl-blurb\">' + esc(i.blurb) + \"<\/p>\" : \"\") +\n          (links ? '<div class=\"tl-links\">' + links + \"<\/div>\" : \"\") +\n          \"<\/article>\"\n        );\n      });\n      html.push(\"<\/section>\");\n      entriesEl.innerHTML = html.join(\"\");\n      observe();\n    }\n\n    \/\/ Reveal on scroll, and light up the rail year you're currently reading.\n    var io = null, spy = null;\n    function observe() {\n      if (io) io.disconnect();\n      if (spy) spy.disconnect();\n      var reduce = window.matchMedia && window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n      var targets = entriesEl.querySelectorAll(\".tl-item, .tl-year-head\");\n      if (reduce || !(\"IntersectionObserver\" in window)) {\n        Array.prototype.forEach.call(targets, function (el) { el.classList.add(\"tl-in\"); });\n      } else {\n        io = new IntersectionObserver(function (entries) {\n          entries.forEach(function (e) {\n            if (e.isIntersecting) { e.target.classList.add(\"tl-in\"); io.unobserve(e.target); }\n          });\n        }, { rootMargin: \"0px 0px -8% 0px\", threshold: .05 });\n        Array.prototype.forEach.call(targets, function (el) { io.observe(el); });\n      }\n\n      if (\"IntersectionObserver\" in window) {\n        spy = new IntersectionObserver(function (entries) {\n          entries.forEach(function (e) {\n            if (!e.isIntersecting) return;\n            var y = e.target.id.replace(\"tl-y-\", \"\");\n            Array.prototype.forEach.call(railEl.querySelectorAll(\".tl-year-btn\"), function (b) {\n              b.dataset.active = String(b.dataset.year === y);\n            });\n          });\n        }, { rootMargin: \"-10% 0px -75% 0px\" });\n        Array.prototype.forEach.call(entriesEl.querySelectorAll(\".tl-year-group\"), function (g) { spy.observe(g); });\n      }\n    }\n\n    $(\"filters\").addEventListener(\"click\", function (ev) {\n      var btn = ev.target.closest(\"button\");\n      if (!btn) return;\n      if (btn.dataset.only === \"all\") {\n        types.forEach(function (t) { active[t.id] = true; });\n        Array.prototype.forEach.call($(\"filters\").querySelectorAll(\"[data-type]\"), function (b) {\n          b.setAttribute(\"aria-pressed\", \"true\");\n        });\n      } else {\n        var id = btn.dataset.type;\n        active[id] = !active[id];\n        btn.setAttribute(\"aria-pressed\", String(active[id]));\n      }\n      draw();\n    });\n\n    railEl.addEventListener(\"click\", function (ev) {\n      var btn = ev.target.closest(\".tl-year-btn\");\n      if (!btn) return;\n      var target = root.querySelector(\"#tl-y-\" + btn.dataset.year);\n      if (target) target.scrollIntoView({ behavior: \"smooth\", block: \"start\" });\n    });\n\n    var fl = meta.footer_link || {};\n    var fu = safeUrl(fl.url);\n    $(\"foot\").innerHTML = \"<span>\" + esc(meta.name || \"\") + \"<\/span>\" +\n      (fu ? '<a href=\"' + esc(fu) + '\" target=\"_blank\" rel=\"noopener noreferrer\">' + esc(fl.label || fu) + \"<\/a>\" : \"\");\n\n    draw();\n  }\n\n  \/\/ Prefer the external JSON so the page can be updated without touching HTML;\n  \/\/ fall back to the copy baked in below, which is what makes this work offline\n  \/\/ and inside a WordPress block.\n  function inline() {\n    try { return JSON.parse(root.querySelector(\".tl-data\").textContent); }\n    catch (e) { return null; }\n  }\n  var src = root.dataset.src;\n  if (src && window.fetch) {\n    fetch(src, { cache: \"no-cache\" })\n      .then(function (r) { if (!r.ok) throw new Error(r.status); return r.json(); })\n      .then(render)\n      .catch(function () { var d = inline(); if (d) render(d); });\n  } else {\n    var d = inline();\n    if (d) render(d);\n  }\n})();\n<\/script>\n<\/div>\n\n\n","protected":false},"excerpt":{"rendered":"<p>Output<\/p>\n","protected":false},"author":0,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"jetpack_post_was_ever_published":false,"footnotes":""},"class_list":["post-426","page","type-page","status-publish","hentry"],"jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/P6Z7g6-6S","_links":{"self":[{"href":"https:\/\/sveder.com\/blog\/wp-json\/wp\/v2\/pages\/426","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/sveder.com\/blog\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/sveder.com\/blog\/wp-json\/wp\/v2\/types\/page"}],"replies":[{"embeddable":true,"href":"https:\/\/sveder.com\/blog\/wp-json\/wp\/v2\/comments?post=426"}],"version-history":[{"count":5,"href":"https:\/\/sveder.com\/blog\/wp-json\/wp\/v2\/pages\/426\/revisions"}],"predecessor-version":[{"id":438,"href":"https:\/\/sveder.com\/blog\/wp-json\/wp\/v2\/pages\/426\/revisions\/438"}],"wp:attachment":[{"href":"https:\/\/sveder.com\/blog\/wp-json\/wp\/v2\/media?parent=426"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}