The markers are drawn, not typed
Gemtext has seven line types and each one is announced by a marker: an arrow for a link, one to three hashes for a heading, an asterisk for a list item, a greater-than sign for a quote. Open a .gmi file in a Gemini client and you see those characters because they are in the file.
This page contains none of them. It contains this instead:
a::before{ content:"=>"; content:"=>" / "" }
h1::before{ content:"#"; content:"#" / "" }
h2::before{ content:"##"; content:"##" / "" }
li::marker{ content:"*\a0"; content:"*\a0" / "" }
The second declaration in each pair is the entire trick. The slash gives the generated glyph an empty alternative text — it is seen and not heard. The bare declaration in front of it is the fallback for anything that has not learned the syntax yet.
The last line has a second fallback that the others do not need, and it is worth the detour. One engine — WebKit, and therefore every browser on an iPad, because they are all the same engine wearing different icons — never implemented the content property on a list marker at all. Not the syntax with the slash: the property. So both declarations are thrown away, the asterisk reverts to a round bullet, and the rule that hides the marker on a run of links is discarded with them, which leaves a stray bullet stranded on a line of its own. The list of markers you are reading was, on that one device, wrong in both directions at once.
The repair is to say it a second way, in a property that engine does implement:
ul{ list-style-type:"*\a0" }
Where the marker property is understood it wins, and brings its silent alternative text along. Where it is not, the string was already there. Neither line can be removed, which is an ordinary sort of tax on a page whose whole argument is that the markers are drawn rather than typed: drawing them is a claim about every renderer, and one of them was not listening.
Notice that the sentence you just read had to name that slash in words. Gemtext has no inline markup at all, so there is no way to set a fragment of a line in code type, or in bold, or in anything else. A phrase either stands on its own line in a block or it stands in prose as ordinary words. That constraint is why the page can be dressed as source in the first place: there is no inline formatting for the stylesheet to have to reconcile with.
What is actually underneath
Strip the stylesheet away and the heading above this paragraph is a heading and the link at the foot of this page is a link:
<h2 id="what-is-actually-underneath">What is actually underneath</h2>
<a href="gemtext.html">Back to the kitchen sink</a>
So a screen reader announces "heading, level two" and "link", not a row of punctuation. Generated content is not part of the document's text either, which means a copy takes the prose and leaves the decoration behind. The picture is for your eyes only, and everything else in the page knows it.
The same applies to the block cursor after the title, the scheme tags that follow an external link, and the rules that frame this code block. All drawn, none typed, none announced.
The constraint did most of the work
The surprise is how little had to be argued for. A format with no inline links, no bold, no italic and almost no colour has already made most of the hard choices, and several AAA criteria fall out of it by accident rather than by effort.
- A link is always its own line, because gemtext cannot put one inside a sentence. So every target clears 44 by 44 pixels with no inline exception, and every label has to make sense read aloud on its own — which is exactly what AAA asks of link text.
- There is no way to emphasise a word, so emphasis is carried by word order and by what you choose to say. Nothing depends on seeing a typeface change.
- Measure is capped at 66 characters, line spacing is 1.6, and the gap between paragraphs is 1.5 times that. Nothing is justified.
- Text sits at 10.6:1 in phosphor and 13.0:1 on paper; the dimmed amber used for the markers, captions and quotes holds 8.7:1. AAA asks for 7:1, and the build measures every pair on every run and refuses to stay quiet if a change drops one under.
Two effects had to be argued for, and both were argued in the same direction. The vignette is painted underneath the text, where it can only ever darken the background and so only ever raise contrast. The raster is painted over everything, and it lifts the dark rows rather than darkening the lit ones — which is both what a tube actually does and the only direction that keeps the dim amber above the floor. On a raster line the two text colours still read 9.8:1 and 8.0:1.
And when the reader has asked for more contrast, or their system is drawing in forced colours, the whole conceit is dropped: no raster, no vignette, no glow.
One file, pictures included
There is no stylesheet to fetch, no script to fetch, no font to fetch, and no image to fetch. All of it is inside the document you are reading.
The stylesheet is inlined and minified, which takes roughly seventeen kilobytes of heavily commented CSS down to about six; the only comment left is a banner pointing at the source those comments still live in. Pictures are inlined as data URIs, which is only honest because of what they are: one bit per pixel, two colours of which one is transparent, ordered 8 × 8 Bayer dither. The plate on the other page is 224 by 96 and weighs a few hundred bytes; the full-colour original it was made from is a quarter of a megabyte.
There is no greyscale to spend, so the tones have to be arranged rather than mixed.
The full-size original is copied alongside and linked, so it is still there for anyone who wants it — the picture on the other page is a link to its own original, and clicking it works whether or not the script is running.
Hovering is the whole speed story
That is where the single file stops being a boast and starts being the point.
A browser can be told to fetch a page before you ask for it. Normally that buys less than it sounds like: the prefetch brings back the HTML, and after you click you are still waiting on the stylesheet, the script and the images. Here there is nothing left to wait for. Fetching the document fetches the finished page — so an ordinary prefetch delivers what would otherwise need a full prerender, at a prefetch's cost.
The head of each page carries a small block naming the pages it links to:
{"prefetch":[{"source":"list","urls":["gemtext.html"],"eagerness":"moderate"}]}
It is not a script. It is a block of data the browser reads and never executes, and a browser that has not heard of it ignores it completely.
Moderate is the important word. Nothing is fetched because a page loaded. The browser waits until you hover a link for a fifth of a second, or press the mouse button, whichever comes first — and only then, and only for pages, never for pictures. Hover the link at the foot of this page and the next one is already in hand before you have finished clicking.
What it costs
Every page carries its own full copy of the stylesheet. Nothing is shared between them and nothing is cached across them, so the six kilobytes are paid for once per page rather than once per capsule.
That is the trade, and it is the right way round at this size: one request that arrives complete beats two requests where the second one decides whether the first was legible. It would be the wrong way round for a site of four hundred pages, where a shared stylesheet is fetched once and every page after the first arrives smaller. The honest thing is to say so rather than pretend the technique generalises.
Back to the kitchen sink