Engineering
I told users to rotate their phone
VizLearn's first answer to a phone was an overlay telling you to rotate it. Taking that guard out meant admitting the fixed canvas had already solved more of mobile than I had noticed — shrink the picture, keep the input panel, widen the strokes that break.
For a while, the first thing VizLearn did when you opened it on a phone was tell you that you were holding it wrong. A full-screen overlay slid up over everything. The component was named LandscapeGuard, and it said Please Rotate Your Device. Landscape mode required. I wrote that, shipped it, and felt fine about it for longer than I should have. Taking it back out taught me more about what VizLearn actually is than building the feature did.
Telling the user to fix it
The guard wasn't unreasonable on its face. The whole stage is a 1920×1080 canvas, and 1920×1080 crushed into a portrait phone column is genuinely unusable: bars too thin to count, labels colliding, the code panel a sliver. Something had to give. The guard's answer was that you give. Rotate the phone, resize the window, come back in landscape. The product is correct as built; your viewport is the problem.
That's the move you reach for when you can't yet see the real fix. It isn't malicious, it's lazy in a specific direction: it relocates the work from the developer to every single visitor, forever, and calls it a requirement. And it has a tell. A healthy product almost never needs to instruct the user on how to hold their device.
The cowardly middle
The first thing I did was less bad and still wrong. When I moved the lessons to a doc-first model, where every lesson ships real written prose alongside the animation, I quietly deleted the guard and replaced it with a fallback: on a phone, show the doc and hide the player. No more nag screen. An honest improvement, on the surface.
Except the player is the product. The scrubbable, type-your-own-input animation is the entire reason VizLearn isn't a textbook, and I'd just decided that everyone on a phone gets the textbook. I had dressed a retreat up as responsive design. The overlay was gone, but I was still refusing to show the real thing on mobile, only more politely. Hiding the hard part is not the same as solving it.
It was a scale problem, not a layout problem
The reason I kept treating mobile as a redesign is that I'd forgotten what the fixed canvas already bought me. Every VizLearn stage is one frozen rectangle: 1920×1080, a single locked 16:9 aspect ratio, identical on every lesson and every screen. I'd been valuing that for authoring, where it means every lesson speaks one coordinate system. I'd missed that it also settles the mobile question outright.
A locked aspect ratio means a phone doesn't need a different layout of the stage. It needs the same rectangle, smaller. The composition stays 1920×1080; only the CSS width shrinks to whatever the phone gives it, and the 16:9 ratio carries the height along for free. So on mobile the canvas became exactly that: a fixed-ratio block pinned to the top of the screen, scaled down as one piece, controls underneath. No reflow, no rearranging, no separate phone layout to keep in sync. Reflow is for documents. A picture, you just make smaller.
Which is when the two interesting problems showed up. The first was a question of judgment. The second was a bug.
You only get to keep one panel
On a laptop the stage sits beside three panels: the code, the running log, and the inputs. On a 390-pixel-wide phone, stacked under a canvas that already eats the top third of the screen, you cannot have all three. There isn't room, and pretending there is just gives you three things too cramped to use. So you choose, and the choice is the most opinionated decision in the whole mobile build.
I kept the inputs and dropped the other two. The animation itself stays, pinned to the top of the screen, so what actually goes is the explanation wrapped around it: the code panel and the running log. Those carry real weight on desktop. A lot of VizLearn is built around the lockstep of code, stage, and log on a single instant, and dropping two-thirds of it is a genuine loss, not a thing I'll pretend doesn't sting. But that lockstep explains a run, and a run doesn't exist until you've fed it an input. Input sits upstream of everything the code and log describe. So when a phone gets one panel beside the animation, it goes to the box where you type your own array, because the run is yours, not the author's, and kill the input and you've shipped the exact passive thing I built this to escape. Code and log explain the run; the input is what makes it your run in the first place.
Which panel survives a brutal squeeze is the most honest one-line statement of what a product is for. Mine kept the text box you type into.
The one thing that doesn't survive shrinking
Then the bug. Scale the whole canvas down and almost everything simply works: positions, gaps, proportions, font sizes, all of it, because every one of them lives in the same coordinate space that's being scaled uniformly. Almost everything. The hand-drawn look has one property that refuses to scale linearly, and that's stroke width.
Here's the trap. A cell border is maybe 2 logical pixels. On desktop the canvas renders near its true size, so 2 pixels is 2 pixels, a clean wobbly line. On a phone the whole thing is scaled to roughly a fifth, so that 2-pixel border becomes about 0.4 of a physical pixel. A screen can't draw four-tenths of a pixel. It draws a faint, broken, dotted ghost of the line, and because a hand-drawn stroke is already varying its own width on purpose, the thin parts of that wobble drop below the pixel floor and disappear completely. The border stops looking hand-drawn and starts looking torn, a dashed line someone gave up on. Every box on the screen looked cracked.
A crisp vector line wouldn't show this nearly as badly; it antialiases down to a clean hairline. A deliberately imperfect line falls apart, because imperfection below one pixel just reads as gaps.
The fix is to stop letting the stroke scale with the picture. Everything else shrinks, but the stroke gets wider on the way down, exactly enough to hold one physical pixel:
function getReadableStrokeWidth(strokeWidth, visualScale, minVisibleWidth = 1) {
// visualScale = the canvas's on-screen size ÷ its 1920×1080 logical size.
// ≈ 1 on a laptop, ≈ 0.22 on a phone.
return Math.max(strokeWidth, minVisibleWidth / visualScale);
}On desktop visualScale is about 1, the max keeps your real stroke width, and nothing changes. On a phone it's about 0.22, so 1 / 0.22 ≈ 4.5 logical pixels, which scaled back down by that same 0.22 lands at roughly one physical pixel: the thinnest line the screen can render without breaking it. The stroke is the single element that has to defy the uniform scale in order to honor it. It's the same family of bug as my rounded rectangles falling apart: a hand-drawn line breaks in ways a plotted one never would, and the fix is a few lines once you can name what's actually happening.
What the guard was hiding
Mobile still isn't a nice way to study this, and I'd rather say that plainly than oversell the fix: no amount of scaling makes a dense 1920×1080 instrument on a portrait phone anything other than a compromise. Some of this genuinely wants a wide screen, and a few of the densest lessons are a real squint on a small one. A banner at the top of the player says as much, in plain words: best on a larger screen. That line is the whole distance traveled. Please rotate your device told the user they were doing it wrong and blocked the door until they complied. Best on a larger screen admits the same compromise and then shows them the thing anyway, at whatever size they brought.
The first version of mobile support was me deciding the product wouldn't fit on a phone. It fit the entire time. I just had to work out what it actually was, a picture you can shrink and a box you type into, and I worked it out by being forced to throw most of it overboard and watch which plank I grabbed for.