Same Blue, Two Meanings — Redrawing the Template Highlight
We marked template-inherited text with a blue background. Text selection is also a blue background. What looked like a color change turned into a tour of what a browser and two mobile engines will and won't let you paint.
Same Blue, Two Meanings — Redrawing the Template Highlight
Fecit has a mode called show and edit the template part. Open a task that was generated from a template, turn it on, and the text splits in two: the words the template handed down, and the words you wrote this time. It works because the lineage is engraved on individual characters — that story is in Lineage in the Letters.
Today’s story comes after that one. The lineage was being recorded exactly right. The way we showed it was wrong.
The report was one sentence
“The template marking is kind of confusing with the selection marking, isn’t it?”
Turn it on and you see it immediately. Template-owned characters get a soft brand-blue wash behind them. And when you drag across text with the mouse — you get a blue wash behind it. One screen, one visual, two meanings.
My first instinct was that the color was the problem. Looking again, color wasn’t the only thing that collided. The technique was identical. Both were “paint behind the glyphs,” and they happened to share a hue axis too. Breaking either one apart would have been enough.
I looked at underlines first. Selection is never drawn as an underline, so it’s unambiguous. But the underline format and links already use underlines, and more importantly, our mobile editors paint text backgrounds through a single attribute — switching to underlines would have meant hand-drawing a dotted underline on Android, which has no built-in one. Too expensive for what it bought.
So I split the hue axis instead. Selection is blue, so the template is amber: #EAB308 at 20% in light, #FDE047 at 22% in dark. It reads like a highlighter stroke. Wanting to use the brand color was the original sin here — the brand color lives in the same family as the system accent.
A little wider — but not with padding
“Can you make it a bit wider?”
Grow the painted area. It’s an inline element, so you give it horizontal padding and cancel it with an equal negative margin: the tint widens, the layout doesn’t move. A standard trick.
Not here. This rule carries box-decoration-break: clone, which makes a background that spans several lines draw as a complete shape on each line. It clones the margins too, not just the padding. Cancel with a negative margin and every wrapped line after the first shifts left.
I used box-shadow spread instead. Two pixels of spread bleeds the same color outside the box and touches layout not at all. Match the shadow color to the background and it looks exactly like padding.
Why the bullets stayed gray
“There are checklists and bullet lists, and the bullet doesn’t get the color.”
Of course it doesn’t. The lineage mark attaches to characters, and a list’s dot or number isn’t a character. It’s decoration the browser draws outside the paragraph. Nothing that lives outside the marked range can be reached by the background.
To paint it you have to address the marker directly, and that’s the second wall: ::marker doesn’t accept a background color. Color and font properties only. So markers went with a solid, deeper amber as text color rather than a wash. The body sits light and the dot lands dark, but side by side they read as one family.
Mobile was different again. iOS and Android draw bullets themselves as text attachments, and that color was one value for the whole view. Giving each line its own color meant asking, at the point where a line gets built, whether it contains any template-owned characters. The line data already carried that, so it came down to one predicate function written the same way on both platforms.
Why the inverted fill failed
The toolbar’s mode toggle needed to match the amber too. While I was at it, why not fill it solid and let it shout that the mode is on?
Built it, put it next to the body, and the two didn’t agree. The button was deep mustard; the body was pale cream. Same hex.
Which is obvious in hindsight. A body wash has to be pale, because text sits on top of it. Twenty percent alpha over white lands around #FBF1D5. The button was opaque #EAB308. And no amount of alpha tuning closes that gap — every step you darken the wash, the text on it gets harder to read. A translucent wash and an opaque fill can’t be the same color.
So the inversion came out and the button became a shrunken copy of the body highlight: pale amber background, deep amber icon. Which turned out to be the exact active-state grammar every other format toggle in that toolbar already used — same shape, different hue. I briefly ringed it with a border to give it more presence, then took that out too. Better to sit inside the convention than to be the one button in the toolbar speaking its own language.
The trip dark mode set
I reached for a palette token for the button, SECONDARY500, and assumed dark mode would follow.
It didn’t. Fecit’s dark theme defines the yellow ramp mirrored. In light, 50 is the palest and 900 the deepest; in dark it runs the other way. And 500, of all the steps, is its own mirror image — the same value in both themes.
A quiet trap: you used a token, so you assume you’re theme-aware, and you aren’t. The more annoying part came next. I needed a dark ink to sit on bright amber, and because the ramp is mirrored, there is no token that stays dark in both themes. It had to be a hardcoded value.
Which was, in the end, one more argument against the inverted fill. Once the button went back to a wash, the ink I needed became “deep in light, bright in dark” — and the mirrored ramp holds exactly that pair. The hardcoded value went away.
The checkbox meant drawing the box ourselves
Checklists were the deepest hole.
A desktop checkbox is a real browser-drawn control. The only thing CSS can touch is accent-color, and that changes the fill of the checked state only. The border of an empty box is drawn by the operating system, out of reach.
But a lineage marking has to be independent of checked state. This is provenance, not status. An item you haven’t finished should still show that it came from the template.
There was one way through: turn off the native control and draw the box ourselves. appearance: none erases the browser’s drawing, and border, fill, and checkmark get rebuilt in CSS. I matched the old control’s dimensions and colors so nothing looks different — but every checklist box on the desktop is now drawn by us rather than by the browser.
Which surfaced something I liked. Mobile had been drawing them by hand all along. The same concept was an OS control on one platform and our own code on the other. This time desktop is the one that caught up.
One footnote: the checkmark on an amber-filled box can’t be white. Amber is high-luminance and a white mark drowns in it, so it became a dark brown. White on the blue box clears 5:1; white on amber doesn’t reach 3:1.
The mode ends with the focus
The last item wasn’t about color at all. “Let’s have the toggle turn off when it loses focus.”
I opened the code and it already did — in the title editor, and on mobile. Only the desktop body editor was missing it. An old decision, “the mode lives as long as the editor,” had already been overturned everywhere else and survived in exactly one place. Three lines closed it.
Then one more. Clicking “apply to template” opens a dialog, and the mode stayed on. Focus never moved. Toolbar buttons deliberately don’t steal focus from the editor (they need to flush unsaved edits first), and the dialog didn’t take it either. There was no blur to hang the behavior on. That one had to be turned off explicitly.
What the day taught
One: what collides is often the technique, not the color. Half of why the blue highlight and the blue selection blurred together was the blue. The other half was “paint behind the glyphs.” Diagnose by color alone and you’ll collide again somewhere else.
Two: translucent and opaque aren’t siblings. Same hex, different alpha, different color. A wash under text and a fill inside a button can never converge, and forcing them together breaks one of the two.
Three: using a token isn’t the same as being theme-aware. On a ramp defined mirrored, the middle step doesn’t move between themes. The reassurance a token’s name gives you is worth nothing against its actual value.
Four: the same concept gets drawn by a different engine on every platform. List markers, checkboxes, text backgrounds — all three have different authors across the browser, iOS, and Android. “Put that color over here too” is almost never a one-liner, and how far to chase parity is a decision you have to price each time. Here we left the widened wash on desktop only: mobile paints the full line height already, so it has the room.
Five: more often than not, the grammar already exists and one place is missing it. The focus behavior was that. Not something to decide, just something to finish applying. Which is a good reason to go looking before writing a new rule.
Nothing got added today. The lineage was already being recorded exactly right yesterday. It’s just that now the person looking at it isn’t confused about what they’re looking at.