In short
- What it is. Fifty-two courses that drill a topic with spaced repetition, free, with no account to make. Live at learncogito.com and the thing I open instead of scrolling.
- Why it exists. The subscription I wanted cost more than I had, so I built the parts of it I actually used.
- How it was built. About twenty hours through a chatbot in a browser tab that could not see my files. Every course is a copy of the previous one with the contents replaced, which is the whole story.
- What that cost. Five weeks later I went back and read my own source: one stylesheet had become thirty-two different versions, the two largest groups differing by a single line.
- Why it is on this site. It works, and it is the clearest argument I have for why everything after it was built differently. Four failures written up below.
The problem
I liked using Brilliant every day and could not justify paying for it. What I actually wanted from it was narrow: a short lesson, a set of flashcards that came back on a schedule, and some visible sense that I was getting better at something. All three of those are mechanisms, not content, and mechanisms are buildable.
So the goal was not to compete with anything. It was to have something on my phone worth opening when the alternative was scrolling.
What it is
Fifty-two self-contained courses, each named after a thinker, each one lesson modules plus flashcards plus a problem arena plus a project track. Progress lives in the browser. There is no backend and no account.
The scheduling is the SM-2 spaced repetition algorithm: you rate how well you knew a card, and the interval to its next appearance grows or collapses accordingly. A card is mature at twenty-one days. A hub reads every course's saved state and renders one picture of where you are across seven disciplines, and a separate daily queue pulls cards due today from every course at once.
The mechanisms were the easy part. The architecture was where it went wrong.
How it was actually built
I built this before I had ever used a coding agent. The model was GLM 5.2 and I reached it through a chat window, which meant it could not see my files, could not run anything, and could not remember what it had told me an hour earlier. I was the filesystem. Every file was one I created by hand and every change was one I pasted in myself.
That one constraint produced every interesting problem in this project.
It began as a single course. Then three. I refined the third until it worked properly and then used it as the template for everything after it, which is the sensible move when you have no other way to reuse anything. The build instructions I wrote for myself say it plainly: copy the most stable course, then replace the storage key, the modules, the flashcards, the problems, the project phases and the missions, and register the result in the hub.
That is a build procedure that says, in writing, that the unit of reuse is a 150 kilobyte file.
What went wrong
The copy became the architecture
Fifty-two courses times a hundred and fifty kilobytes each is 8.3 megabytes of HTML, against twenty-four kilobytes of shared JavaScript. Almost none of the code in this project exists once. It exists fifty-two times, with the contents swapped.
The cost is not disk space, which is free. The cost is that there is no such thing as a small change. Fixing one bug in the lesson renderer means fixing it fifty-two times, correctly, by hand, in files that have already drifted apart from each other.
I did not choose this design. I inherited it from the shape of my tools, and I did not notice that had happened until the bill came due.
The model could not learn, so I wrote its mistake into a file
The same syntax error kept coming back. It would put a backtick inside a JavaScript string, which is fatal, and I would tell it so, and it would agree, and then the next course it generated would do it again. It had no memory between files and no way to see the error I was describing.
My fix was to write the rule into a context document I pasted in every time: never use backticks in strings, use single quotes and concatenation. That line is still in the repository. It is a workaround for a model that could not retain a correction, preserved as though it were a house style.
The wider version: I had one hundred errors in a document and no way to point at them. An assistant that cannot see your workspace turns every correction into a description, and descriptions are slow and lossy.
I was locked to one model by my own template
I tried handing the course-writing work to a different model to go faster. It could not produce anything that fit, because fitting meant reproducing an undocumented 150 kilobyte file's exact internal shape. The template that made the project possible also made it non-portable.
So I wrote all fifty-two by hand, through the one chat window that knew the shape. A convention that only exists as a copied file cannot be handed to anyone, including another tool.
The cross-course features had to be scraped out of the pages
The hub and the daily review queue need to know what every course contains. In a normal design that is a database read. Here there was no database, because the content only exists inside the HTML.
So the tooling reads all fifty-two files, pulls the JavaScript arrays out with a regular expression, evaluates them, and writes a generated index the hub can load. It works. It is also the price of the copy-paste architecture, paid in a script that parses my own source code to find out what I built.
How it got finished
Once every course existed, I moved to a coding agent and asked it to audit the whole thing. The question that mattered was not "is this good" but "how do I make one change reach all fifty-two files". With something that could actually open the files, that became possible: shared behavior moved out into includes, the generated index replaced hand-maintained lists, and a fix could be applied everywhere at once.
That is the moment the project became finishable rather than merely large. It is also the moment I understood what I had been missing, which is why the next thing I built was built in numbered phases with written acceptance tests.
Along the way I had reached the right idea on my own, without the vocabulary for it: put the shared parts in one place, keep only what differs on each page, and let the hub own what is common. That is what shared components are. I got there from the pain rather than from a book, and I got there too late to restructure fifty-two files by hand.
What I would do differently
- Put the content in data and render it once. Fifty-two JSON files and one page would have been the same product with one copy of the logic, and every failure above disappears.
- Treat "copy the last one" as a warning, not a procedure. The moment I wrote that instruction down, I had documented the problem and mistaken it for a solution.
- Do not let the tool's limits become the design. The architecture here is a picture of what a chat window can do, not of what the product needed.
- Ship the thing anyway. It works, it is live, and I use it. A better architecture I never finished would have taught me nothing.
Five weeks later, I went back
I opened Cogito again five weeks after the last commit, expecting to spend an afternoon on the styling. What I found was that the fix in section 05 had covered half the problem, and I had never noticed, because the half it missed was invisible from the outside.
The shared JavaScript had moved into includes. The CSS never had. All fifty-two course pages still carried their own copy of the same stylesheet, and four days of copying had pulled those copies apart into thirty-two different versions. The two largest groups were eleven files each. They differed from one another by exactly one line.
I named my colors after what they looked like. So when I wanted a green course, I set --cyan to green.
That is the root cause, and it is worse than duplication. Every course has its own accent, and the variables were named --cyan, --magenta and --green. Making a green course meant lying to the variable. On one course --cyan and --green hold each other's values. On another, --magenta holds purple. A stylesheet whose names cannot be trusted cannot be read, so the only safe way to change anything was to copy a file that already looked right, which is how thirty-two versions happen.
The cost was not tidiness. It was a bug every reader could hit. The hub offers eight themes. The course pages defined four. Three of them existed only on the hub, and one of those three is the plain theme, which is free from the first level and exists to be easy to read. Choosing it gave you a readable hub and fifty-two unchanged course pages, which are the only pages with anything on them to read.
Two more findings, both worse than the styling. The site had no focus styles at all, so a keyboard user could not see where they were. And the eight navigation tabs on every course page are div elements with click handlers, so a keyboard could not reach them in the first place.
All of it is fixed and live. One stylesheet. Variables named for what they do instead of what they look like, which took one thousand seven hundred and nineteen replacements, because the names appear inside generated markup as well as in the CSS. Eight working themes. A visible focus ring, keyboard access to every control, and text that meets AA contrast.
The measurement I keep returning to is the smallest one. The two grays I used for secondary and tertiary text scored 4.68 and 4.69 against the background. They were the same color. I had shipped a type scale with a step in it that nobody could see, and the only reason I know is that I measured it instead of looking at it.
The pattern is the same one section 05 is about, one level down. Both times the problem was a convention that lived only as a copy. Both times I could not see it until something forced the copies into one place.
Cogito is live at learncogito.com and the source is public at github.com/ryanross949-svg/cogito, including the context document and the scraping script described above. Email me.