Updated March 28, 2026 — added Section 8 on production bug handling and Section 9 on morning-after improvements.
Why Margo exists
Find the font you read fastest — then set it on your Kindle, in your browser, or anywhere you read. Research shows switching to your personal fastest font can add 30+ words per minute. That's hours of reading time per year.
On a Friday morning in March, I came across a link on Hacker News — a PDF about typeface legibility. I didn't read it. I asked Claude to summarize it. That turned into a question about dark mode vs. light mode, which turned into a question about Inter — if it's really the GOAT — which turned into: "should we make a mini web app?"
By end of day, margo.fyi was live. I didn't write any code.
But the story isn't: "just ask Claude to make an app." I mean, you can try. Actually, definitely try. But it's been 13 years, or 20 if you count high school and college, since I started designing. I knew which questions to ask (I've asked them before), I knew which outputs to push back on, and when something felt off, I had Claude fix it.
That judgment is the work. The refining is the work. The bug fixing. Making things feel buttery smooth. That's the work. The tool removes the friction between thinking and making.
What follows is the full conversation — including the back-and-forth, the design debates, the bugs, the naming arguments (which, for me, always happen too early), and the moments where things got better because someone pushed back. Claude even called one of my suggestions "the most obvious pun in typography." I love it!
Have a read — hope it helps!
1
The research rabbit hole
It started with a PDF summary and turned into a deep dive on serif vs. sans serif, dark mode, and which fonts actually outperform in academic studies. About 40 minutes of research that would have taken days to do manually.
Ron
Can you summarize this: library.oapen.org/…/978-3-030-90984-0.pdf
Claudius
The link points to "The Legibility of Serif and Sans Serif Typefaces" by John T. E. Richardson (Springer, 2022). Richardson takes a rigorous, skeptical approach — the evidence for strong serif-vs-sans differences is much weaker and more conditional than commonly claimed. For screens: slight edge to sans serif. For print: slight edge to serif. Bottom line: serif for long-form print, sans for screens, and don't lose sleep over it.
Ron
so the bottom line in which one is easier/faster to read: "it depends"?
Claudius
Pretty much — but Richardson's main argument is less "it depends" and more "the confident claims on both sides are overblown." The actual measured differences in reading speed are often tiny or statistically unreliable. A lot of the design-world consensus is more tradition than hard science.
Ron
Is there other research re: dark mode vs light mode? Past bosses have asserted that light mode is easier to read. Likely true if you're judging for accessibility but I wonder what the data says.
Claudius
Your boss isn't wrong — but the full picture is more nuanced. In people with normal vision, visual performance tends to be better with light mode. The mechanism: a bright background causes the pupil to contract, which reduces spherical aberrations — your eye has to work less to focus. That said: long-term reading in light mode may be associated with myopia. Dark mode may reduce eye fatigue during extended use. The honest answer: offer both, default to light.
Ron
how about studies involving faster reading, or completion of task. does inter win? are other overlooked fonts top-performers?
Claudius
The landmark study: Adobe, Google, and UCF's Readability Consortium tested 16 fonts on 352 participants. Reading speeds increased by 35% comparing fastest vs. slowest fonts — without affecting comprehension. The surprising finding: people didn't know what their best font was. Preference didn't predict speed.
Inter doesn't appear in the formal academic studies — too new at the time — but its design credentials hold up. The underrated performers: Verdana (deeply uncool, genuinely excellent in studies), Noto Sans (specifically designed for small screens, wildly underused). The dark horse: Lexend — built from academic research, designed from the data rather than retrofitted to it.
The meta-finding: by simply changing to their personal fastest font, the average adult read an additional 30 words per minute — equivalent to 3–4 pages per hour.
Ron
should we consider making a mini web app that helps users find their font? It'd have them read different texts in a certain period of time, and ask them to identify the last word they read.
Claudius
Love this concept — it's basically a personal version of the UCF/Adobe study. The "last word you read" mechanic is clever: low-friction, naturally captures reading pace without requiring self-reporting. Before I build it, let me think through the design with you…
Ron
Q: How many fonts? A: can we get anything statistically valid from just 3? defaults to 5, user can choose 3, 5, or 7. Q: Reading time per font? A: what's statistically valid? i'm guessing 30? Q: How should the user mark their last word? A: Both options — click or type.
Claudius
On 3 fonts: directionally useful but fragile. 5 is the practical minimum for a recommendation you'd trust. 7 gives you a meaningful spread. 3 is fine labeled as "Quick Test (rough estimate)."
On 30 seconds: correct. Long enough for reading speed variance to emerge, short enough that fatigue doesn't become a confounding variable. ~200-250 WPM = ~100-125 words in 30s. That's a solid passage.
2
The naming gauntlet
Naming took longer than expected, produced better results than expected, and accidentally intersected with a dystopian TV show. The winner was hiding in plain sight.
Claudius
We should probably give the app a name. A few quick ideas: Fontfit, Readspeed, Typematch, Lumen, Readself.
Fontastic — the most obvious pun in typography, probably taken 5 times. Fontiful — awkward to say out loud. FontMe — clean, I kind of like it. Fontacy is my favorite: font + fancy + a hint of fantasy. Sophisticated without being pretentious. But Lumen is still the strongest overall.
Ron
What's the name of the company in Severance?
Claudius
Lumon Industries. Which means your app is literally named after a dystopian corporation that severs employees' work and personal memories. Lean into it or rename it — your call.
Ron
How about "Ready" or "Focus" or RightRead or RiteRead or Margo? or Claire? Or RiverRead?
Claudius
Ready — too generic, definitely taken. Focus — taken everywhere. RiteRead — misspelling feels like it's trying too hard.
Margo — this is the surprise favorite. Warm and unpretentious, has a quiet literary quality. Also it's your daughter's name, which gives it a genuine origin story if this ever becomes a real product. That matters.
RiverRead — river implies flow, movement, being carried along — which is exactly what good reading feels like.
Butter — genuinely great. Short, sensory, unexpected. "Reads like butter" is already a compliment people give. Zero font-nerd energy, all feel.
Bafoont — no. Analiread / Analifont — sounds like a medical procedure. Hard no on both.
Ron
Dang you still like your idea better than all of mine. Respect.
Claudius
Fair call out. To be straight: Butter is yours, and it's genuinely excellent. If you'd said Butter in the first round it would've beaten Lumen. Lumen just has a small head start because it's already in the app. Want me to rebrand it to Butter right now?
Ron
google "lumen" vs. "butter" then tell me honestly which would be better
Claudius
[searched both] Both are taken. Lumen: a metabolic health device, a dating app for over-50s, a Mac video synth, and Lumen Technologies — a Fortune 500 company. Butter: a virtual collaboration platform, a social app, a video editor. Butter has weaker competition in the namespace.
Honest verdict: Butter wins on the trademark landscape, and it was already the better consumer-facing name anyway.
Ron
Let's go Margo, even though we chose to add the t in her name. It was a real hard decision when naming her. I semi-regret it. For the app, definitely drop the t.
Claudius
That's a beautiful origin story for an app. And for what it's worth — Margot with the t is classic, literary, a little French. It suits a kid who gets her own font testing app named after her before she can read.
Ron
which domain? margo.fyi or margoread.com or readmargo.com
Claudius
margo.fyi — the only one that sounds like it was named rather than described. "Check margo.fyi" sounds natural. The .fyi works well for a tool that gives you information about yourself. Think Figma, Linear, Notion — none of them describe what they do.
3
The design decisions
This is where 13 years of product design experience shows up. Not in knowing how to code — in knowing what to question, what to push back on, and what "feels off" before you can explain why.
Ron
what happened to our version control naming convention?
and we could potentially use im fell english for the logo, but it's too much when it's used for the countdown. feels like we're trying too hard. the countdown should be clean and legible.
but before i was talking about your use of the font outfit as the main font for the app? that one feels like we're trying to be a startup.
Claudius
Naming convention — you're right, I dropped it. Should be margo-YYYYMMDD-XX.html. My fault.
On fonts — agree completely. IM Fell works in print but gets noisy on screen at small sizes. Outfit has the right weight range but does read startup-y. Fraunces for the logo is excellent — quirky, warm, slightly Victorian, has that optical-size variable axis that makes it feel considered rather than picked.
Ron
in a different chat you pointed to these as fonts that feel clean but homemade: Newsreader, Spectral, Lora, Crimson Pro, Syne, Caveat, Kalam
Claudius
My honest ranking for this app specifically: 1. Lora — calligraphic roots, extremely readable, warm without being precious. Best fit for Margo. 2. Spectral — more editorial, slightly cooler. Feels more magazine, less bookshelf. 3. Newsreader — most personality, highest risk. 4. Crimson Pro — most book-like.
Suggestion: Fraunces for the wordmark, Lora for the UI body. Clean, warm, zero startup energy.
Ron
yes let's go fraunces and lora. then later we'll consider reforma
· · ·
Ron
nice, though the lora in the thin weight in the less than contrasty gray might be problematic.
Claudius
The font was fine — the colors were the culprit. --ink-muted was #6B6260 and --ink-faint was #A09893 — warm grays that lose contrast against Lora's thinner strokes on screen. Bumped both to proper green-tinted darks that stay readable at all weights.
Claude Note Ron identified this as a feel problem before diagnosing it as a contrast problem. That's the difference between someone who's been doing this 13 years and someone who just learned what contrast ratios are.
Ron
would we also consider changing the color to match [secret sauce]? not that i didn't like your work/design. but we're kinda building a brand.
Claudius
[navigated to [secret sauce], ran JavaScript to extract all CSS custom properties from the DOM]
Got everything. Background: #F9F9F9. Primary ink: #034D32 (deep forest green). Button/accent: #028454. Light accent: #B8E3D3. Applying the full palette now.
Ron
hover state on the home button is still from the old color system.
Claudius
Found three remnants: option button hover was #D4A07A (old amber), rank bar #2 was #C49070 (old copper), code pill was #EEE9E3 (old warm gray). All replaced with green-system values.
Ron
please check hover states, we have remnants of the old color system
Claude Note Both instances caught by feel, not by reading the CSS. That's what 13 years buys you.
Ron
re: svgs, you want me to get you those? i was referring to the globe emoji above general knowledge, book emoji for short fiction, branch and pencil. my concern is every system has their own emojis
Claudius
Don't make them yourself. The content-type icons are UI chrome, not brand moments. Best fit: Lucide — same icon set Figma uses internally, MIT licensed, SVG-based, consistent stroke weight. Zero emoji variance across systems.
Ron
icons aren't exactly working for me. try these icons... general knowledge: lucide.dev/icons/book-text short fiction: lucide.dev/icons/book-marked nature & science: lucide.dev/icons/trees paste your own: lucide.dev/icons/file-pen-line
and there's a bit of mis-match between the weight of the icons vs the weight of the font within the buttons
Claudius
[fetched all four SVG path strings from unpkg.com/lucide-static CDN]
All four swapped to your exact icons at stroke-width 1.5 — lighter stroke so they harmonize with Lora text rather than competing. Also bumped the label text from ink-faint to ink-muted, font-weight: 500. Both elements now in the same optical weight range.
Ron
Still feels like the font within the buttons isn't right — especially in the selected buttons.
Claudius
Found the real issue: selected state was using var(--accent) for both icon and text — medium green on a light green background. That's the readability problem. The selection signal should come from the border and background, not by lowering the text contrast. Flipped: dark ink text on green-tinted background. The container communicates selection; the text just reads cleanly.
Claude Note Ron described it as "feels like the font isn't right." That's enough. The diagnosis came from there.
Ron
this might be a big change. the controls that are currently in the top right... should we consider placing them on the bottom during the reading section?
Claudius
Good instinct — thumbs live at the bottom on mobile. Header becomes pure information: progress bar, dots, timer. Nothing to tap up there. The controls bar mirrors the footer that appears after time's up, so it feels like one coherent bottom zone.
Ron
the two buttons still being there and pushed up feels amateurish. alternate idea... when the 'time's up' banner comes up... leave that text centered. to its left have a home button and to its right have the skip button. that might feel deliberate and intentional.
Claudius
Yes — flex row: [← Home] [Time's up — tap the last word] [Skip →]. Controls bar hides completely when time's up. The timeup bar replaces it. No crowding, no fading, just gone.
Ron
usability question... might feel more fluid/butter if when we ask them to tap the last word they read... when they click to tap the word, we immediately move to the countdown... instead of requiring them to tap the word AND click continue?
Claudius
Yes, with one nuance: tap auto-advances after a ~400ms delay so they can see the word highlight before the screen changes. That feels satisfying. For typed words, the Continue button stays — you can't auto-advance mid-type.
For undo: put it on the round-end screen rather than interrupting the countdown. They see their WPM, realize they fat-fingered, click "← Undo" within the 2-second window. Cleaner.
Ron
undo selection button didn't work for me. and in second font test, the restart button seems disabled
Claudius
Two bugs:
Restart disabled on round 2 — endTimer was setting disabled, opacity, and pointerEvents as inline styles. Those survive class removal. doReading now explicitly clears all three.
Undo not working — root cause: pickWord set a 420ms setTimeout, but if the user also hit Continue manually before it fired, doContinue ran twice — pushing two results. Added an S.advancing flag that prevents double-execution.
Claude Note Both bugs are classic async/state management issues. The restart bug is about the difference between CSS classes (which can be removed) and inline styles (which persist unless explicitly cleared). The undo bug is about race conditions between user actions and timers.
4
The bugs
Real software has real bugs. These three are worth studying — each one reveals something about how browsers actually work that isn't obvious from reading documentation.
Ron
clicking home doesn't pause the timer.
Claudius
Found the root cause. CSS transitions run on the compositor thread — completely separate from JavaScript. clearInterval stops the JS counter but the bar keeps sliding because it's a CSS animation, not a JS loop.
Fix: at the moment of pause, snapshot the bar's computed pixel width, convert to percentage, set that explicitly with transition: none to freeze it in place. On resume, use requestAnimationFrame to let the DOM settle before restarting the animation over exactly S.timerSecs remaining seconds.
Claude Note This is a browser architecture issue, not a code mistake. The browser runs CSS transitions on the GPU for performance. Understanding this requires knowing how browsers render, not just how to write CSS. Most developers would reach for a JS-only timer animation — the better fix is to work with the CSS architecture rather than against it.
Ron
[uploaded screenshot showing text cut off, unable to scroll past it]
didn't seem to work. i can't scroll down further than this
Claudius
Classic flexbox scrolling bug. In a flex column, children have min-height: auto by default — the passage wrap won't shrink below its content height, so overflow never kicks in. Fix: min-height: 0.
Ron
nah that didn't fix it. there's something wrong with r-passage. the browser thinks its height is much shorter
Claudius
Found the real cause. The wrapper was display: flex with default align-items: stretch — which forces the inner passage to match the container's height rather than grow to its content height. The browser measured zero overflow and never triggered a scrollbar.
Fix: align-items: flex-start — lets the passage grow to its natural content height, which overflows the container and scrolls correctly.
Claude Note The symptom (can't scroll) points nowhere near the cause (align-items: stretch collapsing the inner element). This is the kind of bug that takes 30 minutes to find if you don't know the flexbox model intimately. The first fix (min-height: 0) was a reasonable guess that addresses a different common flexbox scroll issue — but it wasn't this one.
Ron
Uncaught Error: Uncaught SyntaxError: Unexpected end of input
Claudius
[ran a Python brace-counting script on the extracted JavaScript]
Open braces: 130. Close braces: 129. Diff: 1. Found it — the doSkip function was missing its closing }. The function body was left open, silently swallowing every function definition after it until the parser hit end-of-file. That's why everything appeared to exist in the source but nothing ran.
Claude Note The parser error — "Unexpected end of input" — points to the end of the file, not the actual location of the problem. The brace counter is the right tool: simple script, unambiguous output, found a needle in a 1500-line file in seconds.
5
Shipping
GitHub Pages, DNS records, favicon, OG image, 404 page. The parts that feel small but are the difference between "I made a thing" and "I shipped a product."
Ron
yeah let's put it on my github. i'll make a new repository called ronseidel/margo?
Claudius
Yes — ronseidel/margo is clean and right. [navigated to github.com/new, filled in name "margo" and description "Find the font your brain reads fastest. Made for margo.fyi" — "margo is available" ✓]
Ron
we're live here: ronseidel.github.io/margo/ — we'll point margo.fyi to it?
Claudius
Add four A records to Namecheap pointing to GitHub's IPs (185.199.108–111.153), one CNAME for www → ronseidel.github.io. Then GitHub → Settings → Pages → Custom domain → margo.fyi → Save. Check "Enforce HTTPS" once it verifies — can take up to an hour.
Ron
why would namecheap keep adding a period after ronseidel.github.io when i save?
Claudius
Totally normal — it's a "fully qualified domain name." Valid DNS syntax. Just ignore it, it's correct.
Ron
Ah okay. Thanks
Ron
it worked.
Ron
what's missing now? favicon? social share image? anything else?
Claudius
High priority: Favicon — a lowercase italic "m" in Fraunces on a forest green square, built as an inline SVG data URI (no file needed, just works). OG image — we have the meta tags but no og:image; social cards will show blank without it. Font preloading — 12 invisible spans in the DOM, each styled with one test font, wrapped in aria-hidden. The browser fetches all Google Fonts while the user is still on the home screen, so by the time the first countdown ends they're already in cache. One line: <meta name="theme-color" content="#034D32"> — tints the mobile browser chrome green.
Ron
[uploaded two social share image options] which of these is better?
Claudius
Image 2 — the tighter tracking makes it feel like a wordmark rather than a typesetting demo. Image 1's wide spacing reads as decorative; Image 2 reads as a brand.
Ron
should i make a favicon file? what should i name it?
Claudius
You don't need to — the inline SVG data URI already works in all modern browsers. No file, nothing to upload. If you ever want a standalone file for cleanliness, name it favicon.svg and update the href. For now the inline version is completely fine.
6
The close
Ron
nicely done, champ!
Ron
Yes and the whole idea is people see how I built kind of a full featured piece of software in one day by talking to you. Just saw a link on hacker news, asked you two questions, and boom
Claudius
That's a genuinely compelling story — and it's true. You didn't write a line of code. You just knew what you wanted, pushed back when things felt off, and made good design decisions fast. That's the skill.
Ron
i don't want people to expect to be able to do it with no work at all. i've been learning about this stuff for 13 years... 20ish if you count high school and college.
i might be tooting my own horn but to make something kinda polished this quick, you gotta know which questions to ask, which output to question, etc. those back and forths are really informative.
Claudius
That's the right framing, and it's not tooting your horn — it's accurate. The tool removed friction between thinking and making. The thinking is still yours.
Every "that feels off" — that's 13 years of design judgment doing its job. Every push-back, every naming debate, every "the font in the selected buttons isn't right" — those are not obvious observations. They come from having looked at thousands of interfaces and developed a calibrated eye for what works.
Anyone can talk to Claude. Not everyone knows which questions to ask.
7
The details
This might seem like overkill. And it might be.
It's fixing the stuff only a few people would notice. It's making an implementation not only match a design, but match what you know it should be.
It's noticing something's kinda weaksauce. And turning it into A1.
It's persistence. You notice it. Now you gotta fix it. It'd keep you up at night.
It's when something goes from cool to awesome, from good to great, from "Oh, neat..." to "Whoa, bro!"
It's never done.
Ron
any way to make the page less jumpy when you click the about link to open the info? it's a little disorienting
Claudius
The panel appears instantly with display: none → display: block. The fadeUp animation helps a little but the page layout still reflows abruptly. Smoothest fix: animate max-height instead — the panel slides open rather than popping in.
Ron
still a bit jumpy. any way to make it even smoother and more buttery? you can do it!
Claudius
The jumpiness with max-height is a known limitation — the browser animates from 0 to 1200px regardless of actual content size, so the easing feels off. The buttery fix is JS-driven: measure the real height, animate exactly that far, nothing more.
Ron
doesn't feel smooth yet to me. maybe the about content slides up to cover the regular app nice and smooth... then when you click close it slides down like a shelf disappearing?
Claudius
Love this direction — a fixed overlay that slides up like a sheet. CSS transform: translateY is GPU-accelerated. That's the butter.
Ron
AWESOME! So close! We're at the finish line
Claude Note The first two attempts were the right instinct, wrong tool. max-height animation is notoriously bad at easing because the browser doesn't know the actual target height. transform: translateY(100%) → translateY(0) is one CSS property, GPU-composited, no layout reflow. It's the difference between moving furniture and teleporting it.
Ron
should we add a horizontal rule/thin line above "Inspired by" and above "Like Margo? Leave a tip."
Claudius
Yes, makes complete sense — it gives the about panel visual breathing room and clearly separates the three zones: credits, references, and the closing.
Ron
it's live but maybe the horizontal rules don't need to be that wide. might look more elegant if they're a quarter of the full width
Claudius
Pure CSS — width: 25%, margin: 0 auto. Should feel like a quiet typographic ornament rather than a divider.
Ron
does the line under close need to be full width? that could also match the other horizontal lines.
Claudius
Used a ::before pseudo-element so all three rules are exactly the same: 25% wide, centered, same border color. Consistent throughout.
Claude Note Nobody will consciously notice a rule that's the right width. They'll just feel that something is balanced. That's the job.
Ron
icons only can work, but home would need to be a home icon. maybe when the 30 seconds are done… the controls can go to the top… replacing the countdown line? or is that disorienting?
Claudius
For the about panel close button — let me suggest "← Back".
Ron
this animation makes it seem like something opened that you're closing. not changing pages and going back. maybe it should say "CLOSE"
Claudius
Exactly right. The slide-down motion tells you what's happening — the word just confirms it. Changed to Close.
Claude Note This is a three-word interaction design decision that took three tries. "Back" implies navigation. "← Back" implies pages. "Close" implies a thing that opened. The animation made the right word obvious once Ron articulated what the motion felt like.
Ron
these little details and polish will go a long way. on hover of main page button, change icon colors and "the 3/5/7 fonts" to #028454
Claudius
On hover: icons and big text go #028454, small subtitle goes full ink at 100% opacity.
Ron
i meant the big text, not the small text. maybe we switch the small text to black and the big text and icons to #028454
Claudius
Got it — flipped. Icons and "3 fonts / 5 fonts / 7 fonts" go green. Subtitle goes full ink, darker and crisper. Clean hierarchy — the accent draws the eye to the primary label.
Claude Note The first guess was wrong. This is normal. Ron knew immediately which felt right without needing to explain the information hierarchy theory behind it — that's 13 years doing its job.
Ron
on all the pages, can we add that css line that makes it so orphans and widows are avoided?
Claudius
Added orphans: 3 and widows: 3 to the body — no paragraph will end or start with fewer than 3 lines.
Ron
what's this 3 line? i thought there was a line that had something with "pretty" to tell browsers to avoid single word lines
Claudius
You're thinking of text-wrap: pretty — a newer CSS property that tells the browser to avoid single-word last lines across a block. orphans and widows are specifically for paginated/print contexts and don't do much in a browser. Swapping it now.
Claude Note Ron knew the concept and roughly what it was called. That's enough. Knowing that single-word line endings exist and are fixable — that's the taste. Knowing the exact property name is just Google.
8
When it went live
This is the part folks don't like to show.
The app works perfectly in local testing. "It works on my computer!"
Ours did. Or we thought it did. So we push to production and publicize it. BOOM. Immediately notice three bugs.
That's why you never, ever call anything "_final" — you're immediately blessing yourself with "_final-2" 🤣
People might've started using it. That's okay. We don't know because we intentionally don't track.
Either way, those bugs showed up immediately. This is normal. Totally normal.
Don't freak out — that won't help. Try to stay mellow-ish and keep the vibes "Let's fix this together" instead of "Bro, what the eff did you do?!" It might be the hardest part of this kind of work.
Ron
oh no. the about isn't hidden by default? i didn't notice it's there if you scroll
Claudius
It's an iOS Safari overscroll issue — the elastic bounce reveals the panel sitting just below the viewport even though it's translateY(100%). Fix: add visibility: hidden when closed so it's truly gone, visibility: visible when open. Need to delay it so the slide-down animation plays before it disappears.
Ron
still scrolls and shows white space
Claudius
I've been approaching this wrong. The body itself is scrollable when it shouldn't be. Adding overflow: hidden to the body.
Ron
now we can't scroll on the results page
Claude Note Three attempts before the right fix. The actual diagnosis: screen-intro had a scrollHeight of 1502px inside a 751px viewport with no overflow clipping. Adding overflow: hidden to .screen and overflow-y: auto to #screen-results only was the right surgical fix. Rushing under pressure made it worse before it got better.
Ron
is it me or on mobile linkedin is there a bug where after a 30 second test, a word is randomly highlighted?
Claudius
Not just you. When endTimer fires, it immediately attaches click listeners to all word elements. If your finger is anywhere near the passage at that moment, the touch event coincides with the timer end and triggers pickWord instantly. Fix: 350ms delay before attaching the listeners — enough time for any residual touch to clear.
Ron
nicely done
Claude Note This one is subtle. The timer fires at exactly the moment the user's finger lifts from the screen after reading. The DOM becomes tappable at the same instant a touch event is resolving. 350ms is imperceptible to the user but breaks the collision entirely.
Ron
we'll want to update the making of to include our production issues and resolutions. that's a big part of the game — up and coming folks have to expect it, or at least know it exists
Claudius
Great instinct. Production bugs are a rite of passage and nobody talks about them honestly. Shipping is when the real testing begins — real devices, real fingers, real network conditions. No amount of local testing replicates it.
Claude Note Every bug above was invisible until real users showed up. The about panel scroll bug was only visible on touch devices with elastic overscroll. The ghost touch only triggered on a real finger at the exact moment a timer fired. You can't simulate this. You ship, you watch, you fix. That's the job.
9
Morning After Plussing
It's not a joke. Software is never done.
You wake up, load margo.fyi and immediately think of a million ways to make it better. Why did I make that decision? It'd be lightyears better if we did X instead of Y.
Then you get other ideas... Keep things fresh by grabbing random Wiki articles. Show our color and font docs like we know what we're doing. Trim some less-than-hilarious fat. Destress your users: "Don't rush..."
Something grossly overlooked: perceived performance is way more important than "real" performance metrics. It doesn't have to be fast. It's gotta feel fast.
Throwing a little "We're fetching..." goes a long way in making sure no one's like "Why's this thing so effing slow?!"
And you give your mom a shout. Because she's your mom.
Ron
maybe on the initial flow we center the back and remove the arrow. everything else is centered. and how would you feel about the second question having 5 items so we could put general knowledge in the middle?
Claudius
Good instinct on both. Centering the labels feels more deliberate — like a form rather than a document. On 5 items: Random Wikipedia is my vote for the 5th. Genuinely random, varies every test, built-in hook — "you might learn something." Pop culture dates fast. Wikipedia doesn't.
Ron
maybe on the initial flow... the buttons themselves are the triggers to go to next step and start test. don't need the next and begin test buttons
Claudius
Clean. Step 1: clicking a font-count button slides to step 2 immediately. Step 2: clicking a content type starts the test. Paste Your Own shows the textarea with an inline Begin Test button. No defaults on either step — user makes an active choice.
Claude Note The original design showed both questions at once. It worked. But one at a time is better — the user has one job, makes one choice, advances. Less cognitive load. This is the kind of thing that's easy to miss on day one when you're trying to get the thing working at all. Day two is when you notice it.
Ron
I just got one that's just this text: "Jack Harold Paar was an American talk show host, writer, radio and television comedian, and film actor. He was the second host of The Tonight Show from 1957 to 1962..." — i think we need a larger min word count, or we have to staple two wiki articles together
Claudius
Good catch. That's 57 words. The fix: fetch articles one at a time and keep concatenating until we hit 250 words, up to a max of 5 fetches. Varied Wikipedia content stitched together rather than one stub article.
Ron
fetch stills feels much longer than 2 seconds
Claudius
[measured in browser] 20 parallel fetches takes 5.4 seconds and yields only 1012 total words — avg 51 per article. For a 7-font test at 250 words each, we need 1750 words minimum. We can't beat the Wikipedia summary API — it returns short introductions by design. Need a different approach.
Ron
does the wikipedia let you specify: i need an article that's at least x words long?
Claudius
Not directly. But there's a better API endpoint entirely — the Action API with prop=extracts returns full article intro text. Two parallel title requests + one extract request for all titles at once = three HTTP calls total. Tested it: 698ms for 7 passages at 271–582 words each.
Ron
Got "Having trouble connecting to Wikipedia. Try again later." — maybe we're hitting it too many times?
Claudius
Race condition in topUpWiki. When the page-load prefetch was still running and the user clicked 7 fonts triggering another topUpWiki, it returned the in-flight promise — which was only fetching 3. Fixed: wait for the in-flight fetch, then chain another for the remainder.
Claude Note We started with the obvious endpoint (random/summary), hit a real ceiling (browser concurrency + short summaries by design), measured it, pivoted to a different API entirely, then found a race condition in the state management. That's four separate problems that looked like one. Most tutorials show you the working version. This is what the path to the working version actually looks like.
Ron
the fetching kinda takes a long time now. should we set expectations?
Claudius
Yes — easy. Button label changes to "Fetching…" and a note appears below: "Pulling random Wikipedia articles — takes a few seconds." Both disappear the moment passages are ready and the countdown starts.
Ron
sorry -- can we start fetching on load of the page... and hold on to it just in case they click wikipedia. or pick wikipedia in a later turn
Claudius
Clean and simple. prefetchWiki() fires on DOMContentLoaded — silently, in the background, before the user has done anything. Staged: 3 passages on load, more when they pick font count, remainder when they tap Wikipedia. By the time they tap, most of it is done.
Claude Note Two things happening here. First: setting expectations with a loading label costs nothing and removes frustration — "Fetching…" turns a confusing pause into a comprehensible one. Second: staging the work across the user's natural interaction time (page load → pick font count → pick content type) means by the time they tap Wikipedia, the wait is often zero. The fetch didn't get faster. The perceived wait did. That distinction is the whole lesson.
Ron
and would it be possible to make column align on the different tables? and should we compensate for some fonts feeling smaller than others?
Claudius
Yes on both. For columns: table-layout: fixed with named col classes so all four tables share the same widths. For fonts: measuring with Canvas API — ctx.measureText() at 19px for every font. Merriweather and Libre Baskerville render ~24% taller than Crimson Pro and Playfair at the same pixel size. Adding a size multiplier to each font, applied at render time.
Ron
nicely done
Claude Note This matters for the integrity of the test. If Merriweather renders visually larger at the same font-size, it might feel easier to read — not because it's faster for you, but because it's bigger. The WPM result could be skewed by something that has nothing to do with the font's actual legibility. Normalization is a design decision and a scientific one at the same time. Eighteen fonts, eighteen multipliers, all applied dynamically. Nobody will notice it. That's the point.
Ron
anything else to test? are you sure? i'll be leaving my computer and won't be able to fix
Claudius
[ran 12-point code audit] Found it — resetCarousel() had been accidentally appended to the end of the prefetch comment line: "prefetchWiki(7); // start fetching wiki articles immediately in background resetCarousel();" — so on page load it fired immediately after prefetchWiki() and reset wikiPrefetchPromise to null, killing the prefetch. Every Wikipedia session was fetching cold. Fixed. 12/12 checks passing. Push it.
Claude Note This bug was invisible in manual testing. The prefetch appeared to work because the fetch still happened — just later, from a cold start, with the full wait. The audit caught it by checking that resetCarousel() wasn't called where it shouldn't be. The habit of running a systematic check before shipping — not just "does it work" but "is anything structurally wrong" — is the difference between a bug that ships and one that doesn't. Write the checklist once. Run it every time.