More from Daniel Marino
Beware! This post includes spoilers! I recently built an escape room game called CSScape Room. This isn’t my first JavaScript-free web game, but HTML and CSS have evolved significantly since my previous attempts, with newer additions allowing for more complex selectors and native interactions. Rather than saving this idea for a game jam, I built it purely for fun, which freed me from theme constraints and time pressure. I’ve enjoyed escape room games since childhood, and it was nostalgic to recreate that experience myself. This project pushed my artistic limits while challenging me to design puzzles and translate them into complex HTML and CSS. The learning process was fun, challenging, and sometimes tedious—mostly through trial and error. Process My creative process isn’t linear—it’s a blend of designing, puzzle creation, and coding that constantly influences each other. I frequently had to redesign or recode elements as the project evolved. There was also that time I accidentally deleted half my CSS because I wasn’t backing up to GitHub... lesson learned! 😬 This might sound chaotic, and honestly, it was. If you’re wondering where to start with a project like this, I began by prototyping the room navigation system. I figured that was the minimum viable feature—if I couldn’t make that work, I’d abandon the project. The solution I eventually found seems simple in retrospect, but I went through several iterations to discover it. This flexible approach makes sense for my creative projects. As I build something, both the in-progress work and my growing skills inevitably influences the entire project. I’m comfortable with this non-linear process—it also suits my ADHD brain, where I tend to lose interest if I work on the same thing for too long. Artwork I’d wanted to design a pixel art-styled game for some time but never felt confident enough to attempt it during a game jam because of the learning curve. I watched tutorials from Adam Yunis and Mort to get a crash course in pixel art best practices. Initially, progress was slow. I had to figure out 2D perspective with vanishing points, determine a color palette, practice shading techniques, and decide how much detail to include. While I tried to adhere to pixel art “rules,” I definitely broke some along the way. One challenge I set for myself was using only 32 colors to capture the feeling of an older gaming console. Once I got comfortable with shading and dithering, working within this constraint became easier. An added benefit to using 32 colors was it resulted in smaller image sizes—the game’s 79 images account for only about 25% of the total payload. I attempted to design sprites using dimensions in multiples of eight, but I’ll admit I became less strict about this as the project progressed. At a certain point, I was struggling enough with the color and styling limitations that this guideline became more of a starting point than a rule. I considered creating my own font, but after exhausting myself with all the artwork, I opted for Google’s PixelifySans instead. Almost all animation frames were individually drawn (except for the “one” TV animation). This was tedious, but I was determined to stay true to old-school techniques! I did use CSS to streamline some animations—for instance, I used animation-direction: alternate on the poster page curl to create a palindrome effect, halving the number of required sprites. Mechanics Like my previous game Heiro, this project primarily uses checkbox and radio button mechanics. However, the addition of the :has() pseudo-selector opened up many more possibilities. I also utilized the popover API to create more detailed interactions. Checkbox and Radio Selection Triggering interactions by toggling checkboxes and radio buttons isn’t new, but the :has() selector is a game-changer! Before this existed, you had to structure your markup so interactive elements were siblings. The :has() selector makes this far more flexible because you no longer need to rely on a specific HTML structure. #element { display: none; } :has(#checkbox:checked) #element { display: block; } Using this pattern, :has() looks for #checkbox anywhere on the page, meaning you don’t have to rely on #checkbox, its corresponding <label>, or #element being siblings. The markup structure is no longer a constraint. Most of this game functions on toggling checkboxes and radios to unlock, collect, and use items. Navigation I almost gave up on the current implementation, and used basic compass notation to avoid visual transitions between directions. After several failed attempts, I found a solution. The tricky part was determining how to transition into a direction from either left or right, depending on which arrow was clicked. My solution is conceptually simple but difficult to explain! First, I used radio buttons to determine which direction you’re facing (since you can only face one direction at a time). Second, I needed a way to determine if you’re entering a direction from west or east. This required eight radio buttons—two for each direction. For example, if you’re facing east (having come from facing north), you have two possible directions to go: west (returning to face north) or east (to face south). I needed to make the radio buttons visible that would take you north from east, and south from west. The CSS looks something like this: :has(#east-from-west:checked) :is( [for="south-from-west"], [for="north-from-east"]) { display: block; } This pattern was implemented for each direction, along with animations to ensure each room view slid in and out correctly. Zooming In I initially focused so much on checkbox mechanics that I assumed I’d need the same approach for zooming in on specific areas. Then I had a "Duh!" moment and realized the popover API would be perfect. Here’s the basic markup for looking at an individual book: <button popovertarget="book">Zoom in</button> <div id="book" popover> <!-- Book content goes here --> <button popovertarget="book" popovertargetaction="hide">Close</button> </div> Turning the Lights Off I procrastinated on implementing this feature because I thought I’d need to create darkened variations of all artwork. I don’t recall what inspired me to try blend modes, but I’m glad I did—the solution was surprisingly simple. When the light switch checkbox is toggled, a <div> becomes visible with a dark background color and mix-blend-mode: multiply. This multiplies the colors of the blending and base layers, resulting in a darker appearance. Playing the Crossword This required surprisingly complex CSS. Each square has three letters plus a blank tile, meaning four radio buttons. The :checked letter has a z-index of 3 to display above other letters, but also has pointer-events: none so clicks pass through to the next letter underneath (with z-index: 2). The remaining tiles have a z-index of 1. The CSS becomes even trickier when the last tile is :checked, requiring some creative selector gymnastics to target the first radio button in the stack again. Tools I created all artwork using Aseprite, which is specifically designed for pixel art. I probably only used a fraction of its features, and I’m not sure it actually made my life easier—it might have made things more difficult at times. I’m not giving up on it yet, though. I suspect I’ll occasionally discover features that make me think, “Oh, that’s way easier than what I was doing!” I started coding with basic HTML and CSS but eventually found navigation difficult with such a long HTML file. It also became tedious writing the same attributes for every <img /> element. I migrated the project to Eleventy to improve organization and create custom shortcodes for simplifying component creation. I used the html-minifier-terser npm package, which integrates well with Eleventy. I chose native CSS over Sass for several reasons: CSS now has native nesting for better organization and leaner code CSS has built-in variables HTTP/2 handles asset loading efficiently, eliminating the major benefit of bundling CSS files The game uses 12 CSS files with 12 <link rel="stylesheet" /> tags. The only Sass feature I missed was the ability to loop through style patterns for easier maintenance, but this wasn’t a significant issue. The game is hosted on GitHub Pages. During deployment, it runs an npm command to minify CSS using Lightning CSS. I mentioned accidentally deleting half my CSS earlier—this happened because I initially used Eleventy’s recommended approach with the clean-css npm package. I strongly advise against using this! This package doesn’t work with native CSS nesting. While losing code was frustrating, I rewrote much of it more efficiently, so there was a silver lining. Nice to Haves I initially wanted to make this game fully accessible, but the navigation system doesn’t translate well for screen reader users. I tried implementing a more compass-like navigation approach for keyboard users, but it proved unreliable and conflicted with the side-to-side approach. Adding text labels for interactive elements was challenging because you can’t track the :focus state of a <label> element. While you can track the :focus of the corresponding <input />, it wasn’t consistently reliable. The main keyboard accessibility issue is that the game exists as one long HTML page. When you navigate to face a different direction, keyboard focus remains elsewhere on the page, requiring extensive tabbing to reach navigation elements or item selection. I ultimately decided to make the game deliberately inaccessible by adding tabindex="-1" to all keyboard-accessible elements. I’d rather users recognize immediately that they can’t play with assistive technology than become frustrated with a partially broken experience. Sound would have been a nice addition, but I encountered the same issues as with my previous game Heiro. You can toggle the visibility of an <embed> element, but once it’s visible, you can’t hide it again—meaning there’s no way to toggle sound on and off. Conclusion CSScape Room was a fun but exhausting four-month project. It began as an experiment to see if creating a JavaScript-free escape room was possible—and the answer is definitely yes. I’ve only touched on some aspects here, so if you’re interested in the technical details, check out the source code on GitHub. Finally, I’d like to thank all my playtesters for their valuable feedback!
I’ve always been fascinated to see what other apps or workflows others are using in their day-to-day lives. Every now and then I learn about a new app or some cool trick I didn’t previously know. I doubt anyone seriously cares about what I’m using, but figured I’d list them out anyway—if for no other reason than to keep a historical record at this point in time. Applications Alfred — I have a lifelong license, and I like it. No point in fixing something that isn’t broken. I primarily use it for app switching, but also use it for math, and to search for gifs. Aseprite — Sometimes I do pixel art! Even if the UI is clunky, and some keyboard shortcuts aren’t always convenient, there are some unique features that help facilitate creating pixel art. Audacity — I rarely use it, but sometimes it’s easier to make some quick audio edits with Audacity than to use a full blown DAW. Bear — This is the note-taking, task-tacking app I’ve landed on. The UI is beautiful and it feels snappy. It syncs, so I can use it on my iPhone too. Chrome — I used Arc for the better part of 2024, but after they announced they were done working on it to focus on a new AI-powered browser, I peaced out. There are a couple of features I really missed, but was able to find some extensions to fill those gaps: Copy Current Tab URL, Meetings Page Auto Closer for Zoom, Open Figma app, and JSON Formatter. Figma — I use it because it’s what we use at work. I’m happy enough with Figma. iTerm2 — Has a few features that I like that makes me chose this over Mac’s native Terminal app. Pixelmator Pro — I haven’t paid the Adobe tax for a long time, and it feels good. I started using Pixelmator because at the time it was the best alternative available. I’m comfortable with Pixelmator at this point. I don’t really use image editors often these days, so I probably won’t switch anytime soon. Reaper — My DAW of choice when composing music. It’s very customizable, easyish enough to learn, and the price is right. It also has a die hard community, so I’m always able to find help when I need it. VS Code — I’ve tried a lot of code editors. I prefer Sublime’s UI over VS Code, but VS Code does a lot of things more easily than Sublime does, so I put up with the UI. YouTube Music — I still miss Rdio. YouTube Music works well enough I guess. Paying for YouTube Music has the benefit of not seeing ads on YouTube. Command-line Tools These aren’t apps per se, but these are some tools that I use to help manage packages or that I use regularly when developing. Deno Eleventy Homebrew pure statikk Vite Volta yt-dlp Equipment I have one computer and I use it for everything, and I’m okay with that. It’s more than powerful enough for work, composing music, making games, and occasionaly playing games. Although I have a dedicated home office, lately I tend to work more on the go, often with just my laptop—whether that’s at a cafe, a coworking space, or even just moving around the house. 2021 M1 MacBook Pro AKG K240 Studio Headphones Arturia MiniLab MKII Controller Behringer UMC202HD USB Audio Interface Fender Squire Strat Guitar Fender Squire Bass Guitar Shure SM57 Virtual Instruments This is quite specific for composing music, so if that does’t interest you, feel free to stop reading here. This list is not exhaustive as I’m regularly trying out new VSTs. These are some staples that I use: 🎹 Arturia Analog Lab V (Intro) — My Arturia controller came with this software. It has over 500 presets and I love exploring the variety of sounds. 🎸 Bass Grinder (Free) — I recently came across this VST, and it has a great crunchy overdrive sound for bass guitar. 🥁 Manda Audio Power Drum Kit — Even though you can use this for free, I paid the $9 because it is fantastic. The drums sound real and are great for all styles of music. 🎸 ML Amped Roots (Free) — What I like about this is that I get great metal guitar out of the bost without having to add pedals or chaining other effects. 🥁 ML Drums (Free) — I just started experimenting with this, and the drum tones are amazing. The free set up is pretty limited, but I like how I can add on to the base drum kit to meet my needs rather than having having to buy one big extensive drum VST. 🎹 Spitfire LABS — More variety of eclectic sounds. I also use several built-in VSTs made by Reaper for delay, EQ, reverb, pitch-shifting, and other effects. Reaper’s VSTs are insanely powerful enough for my needs and are much less CPU intensive.
Over the past couple of years I’ve gotten into journaling. Recently I’ve been using a method where you’re given a single inspirational word as a prompt, and go from there. Unfortunately, the process of finding, saving, and accessing inspirational words was a bit of a chore: Google a list of “366 inspirational words”. Get taken to a blog bloated with ads and useless content all in an effort to generate SEO cred. Copy/paste the words into Notion. Fix how the words get formatted becasue Notion is weird, and I have OCD about formatting text. While this gets the job done, I felt like there was room to make this a more pleasant experience. All I really wanted was a small website that serves a single inspirational word on a daily basis without cruft or ads. This would allow me to get the content I want without having to scroll through a long list. I also don't want to manage or store the list of words. Once I've curated a list of words, I want to be done with it. Creating a microsite I love a good microsite, and so I decided to create one that takes all the chore out of obtaining a daily inspirational word. The website is built with all vanilla tech, and doesn’t use any frameworks! It’s nice and lean, and it’s footprint is only 6.5kb. Inspirational words While I’m not a huge fan of AI, I did leverage ChatGPT on obtaining 366 inspirational words. The benefit to ChatGPT was being able to get it to return the words as an array—cutting down on the tedium of having to convert the words I already had into an array. The words are stored in it’s own JSON file, and I use an async/await function to pull in the words, and then process the data upon return. Worth the effort I find these little projects fun and exciting because the scope is super tight, and makes for a great opportunity to learn new things. It’s definitely an overengineered solution to my problem, but it is a much more pleasant experience. And perhaps it will serve other people as well.
Over the past couple of years I’ve gotten into journaling. Recently I’ve been using a method where you’re given a single inspirational word as a prompt, and go from there. Unfortunately, the process of finding, saving, and accessing inspirational words was a bit of a chore: 1. Google a list of “366 inspirational words”. 2. Get taken to a blog bloated with ads and useless content all in an effort to generate SEO cred. 3. Copy/paste the words into Notion. 4. Fix how the words get formatted becasue Notion is weird, and I have OCD about formatting text. While this gets the job done, I felt like there was room to make this a more pleasant experience. All I really wanted was a small website that serves a single inspirational word on a daily basis without cruft or ads. This would allow me to get the content I want without having to scroll through a long list. I also don't want to manage or store the list of words. Once I've curated a list of words, I want to be done with it. ## Creating a microsite I love a good microsite, and so I decided to create one that takes all the chore out of obtaining a [daily inspirational word](https://starzonmyarmz.github.io/daily-inspirational-word/).  The website is built with all vanilla tech, and doesn’t use any frameworks! It’s nice and lean, and it’s footprint is only 6.5kb. ### Inspirational words While I’m not a huge fan of AI, I did leverage ChatGPT on obtaining 366 inspirational words. The benefit to ChatGPT was being able to get it to return the words as an array—cutting down on the tedium of having to convert the words I already had into an array. The words are stored in it’s own JSON file, and I use an async/await function to pull in the words, and then process the data upon return. ## Worth the effort I find these little projects fun and exciting because the scope is super tight, and makes for a great opportunity to learn new things. It’s definitely an overengineered solution to my problem, but it is a much more pleasant experience. And perhaps it will serve other people as well.
More in programming
Linus Torvalds, Creator of Git and Linux, on reducing cognitive load
You heard there was money in tech. You never cared about technology. You are an entryist piece of shit. But you won’t leave willingly. Give it all away to everyone for free. Then you’ll have no reason to be here.
Understanding how the architecture of a remote build system for Bazel helps implement verifiable action execution and end-to-end builds
Debates, at their finest, are about exploring topics together in search for truth. That probably sounds hopelessly idealistic to anyone who've ever perused a comment section on the internet, but ideals are there to remind us of what's possible, to inspire us to reach higher — even if reality falls short. I've been reaching for those debating ideals for thirty years on the internet. I've argued with tens of thousands of people, first on Usenet, then in blog comments, then Twitter, now X, and also LinkedIn — as well as a million other places that have come and gone. It's mostly been about technology, but occasionally about society and morality too. There have been plenty of heated moments during those three decades. It doesn't take much for a debate between strangers on this internet to escalate into something far lower than a "search for truth", and I've often felt willing to settle for just a cordial tone! But for the majority of that time, I never felt like things might escalate beyond the keyboards and into the real world. That was until we had our big blow-up at 37signals back in 2021. I suddenly got to see a different darkness from the most vile corners of the internet. Heard from those who seem to prowl for a mob-sanctioned opportunity to threaten and intimidate those they disagree with. It fundamentally changed me. But I used the experience as a mirror to reflect on the ways my own engagement with the arguments occasionally felt too sharp, too personal. And I've since tried to refocus way more of my efforts on the positive and the productive. I'm by no means perfect, and the internet often tempts the worst in us, but I resist better now than I did then. What I cannot come to terms with, though, is the modern equation of words with violence. The growing sense of permission that if the disagreement runs deep enough, then violence is a justified answer to settle it. That sounds so obvious that we shouldn't need to state it in a civil society, but clearly it is not. Not even in technology. Not even in programming. There are plenty of factions here who've taken to justify their violent fantasies by referring to their ideological opponents as "nazis", "fascists", or "racists". And then follow that up with a call to "punch a nazi" or worse. When you hear something like that often enough, it's easy to grow glib about it. That it's just a saying. They don't mean it. But I'm afraid many of them really do. Which brings us to Charlie Kirk. And the technologists who name drinks at their bar after his mortal wound just hours after his death, to name but one of the many, morbid celebrations of the famous conservative debater's death. It's sickening. Deeply, profoundly sickening. And my first instinct was exactly what such people would delight in happening. To watch the rest of us recoil, then retract, and perhaps even eject. To leave the internet for a while or forever. But I can't do that. We shouldn't do that. Instead, we should double down on the opposite. Continue to show up with our ideals held high while we debate strangers in that noble search for the truth. Where we share our excitement, our enthusiasm, and our love of technology, country, and humanity. I think that's what Charlie Kirk did so well. Continued to show up for the debate. Even on hostile territory. Not because he thought he was ever going to convince everyone, but because he knew he'd always reach some with a good argument, a good insight, or at least a different perspective. You could agree or not. Counter or be quiet. But the earnest exploration of the topics in a live exchange with another human is as fundamental to our civilization as Socrates himself. Don't give up, don't give in. Keep debating.
In my old age I’ve mostly given up trying to convince anyone of anything. Most people do not care to find the truth, they care about what pumps their bags. Some people go as far as to believe that perception is reality and that truth is a construction. I hope there’s a special place in hell for those people. It’s why the world wasted $10B+ on self driving car companies that obviously made no sense. There’s a much bigger market for truths that pump bags vs truths that don’t. So here’s your new truth that there’s no market for. Do you believe a compiler can code? If so, then go right on believing that AI can code. But if you don’t, then AI is no better than a compiler, and arguably in its current form, worse. The best model of a programming AI is a compiler. You give it a prompt, which is “the code”, and it outputs a compiled version of that code. Sometimes you’ll use it interactively, giving updates to the prompt after it has returned code, but you find that, like most IDEs, this doesn’t work all that well and you are often better off adjusting the original prompt and “recompiling”. While noobs and managers are excited that the input language to this compiler is English, English is a poor language choice for many reasons. It’s not precise in specifying things. The only reason it works for many common programming workflows is because they are common. The minute you try to do new things, you need to be as verbose as the underlying language. AI workflows are, in practice, highly non-deterministic. While different versions of a compiler might give different outputs, they all promise to obey the spec of the language, and if they don’t, there’s a bug in the compiler. English has no similar spec. Prompts are highly non local, changes made in one part of the prompt can affect the entire output. tl;dr, you think AI coding is good because compilers, languages, and libraries are bad. This isn’t to say “AI” technology won’t lead to some extremely good tools. But I argue this comes from increased amounts of search and optimization and patterns to crib from, not from any magic “the AI is doing the coding”. You are still doing the coding, you are just using a different programming language. That anyone uses LLMs to code is a testament to just how bad tooling and languages are. And that LLMs can replace developers at companies is a testament to how bad that company’s codebase and hiring bar is. AI will eventually replace programming jobs in the same way compilers replaced programming jobs. In the same way spreadsheets replaced accounting jobs. But the sooner we start thinking about it as a tool in a workflow and a compiler—through a lens where tons of careful thought has been put in—the better. I can’t believe anyone bought those vibe coding crap things for billions. Many people in self driving accused me of just being upset that I didn’t get the billions, and I’m sure it’s the same thoughts this time. Is your way of thinking so fucking broken that you can’t believe anyone cares more about the actual truth than make believe dollars? From this study, AI makes you feel 20% more productive but in reality makes you 19% slower. How many more billions are we going to waste on this? Or we could, you know, do the hard work and build better programming languages, compilers, and libraries. But that can’t be hyped up for billions.