Whoever loads last wins
Two real sites wear a look this engine never shipped. sh.cynicky.blog is dressed as Ghost — a full-width lead card, a dotted frame, tiles instead of a plain list. arch-linux.cz is dressed as a Hugo theme, Blowfish specifically — a compact bar, a profile block, cards laid out like someone else's homepage entirely. Neither forked the engine. Neither edited a template. Both still take a plain git pull.
One stylesheet, loaded last
site.extra_css in config/site.yml names a path, or a list of them, loaded after the engine's own colors.css and site.css. That's the entire mechanism — a skin is one file of yours that repaints what's already on the page, and the engine never writes to it, so an update can never conflict with it.
Local paths only: every page carries style-src 'self', and a stylesheet on another host would just be dropped by the browser with no error anyone would ever see — the build refuses a remote path out loud instead, before that silence has a chance to happen.
Whether that file belongs in git is a separate decision — commit it and a second machine already has your look, exclude it and a public repository never carries a design that wasn't meant to be public. The engine's own .gitignore doesn't decide this for you either way.
The rule everything else follows from
Loading last means your rules win at equal specificity — including the rules the engine wrote for narrow screens, which is not the part anyone expects to be winning against.
sh.cynicky.blog's skin floats a table of contents beside the article and gave it a percentage width to match its own measure. On a desktop that looked right. Below 700px, the engine has its own reflow: the contents drop above the text and the width rule becomes auto, because there's no room beside anything at that size any more. The skin's percentage didn't know that and kept applying anyway — loading last means winning always, not just when it's convenient to. The article came out 155 pixels wide on a 390-pixel phone screen.
The fix wraps the override in the range where it's actually meant to apply:
@media (min-width: 861px) {
.toc { width: 350px; }
}
Below that width, the skin says nothing, and the engine's own reflow is left to do the one job it was already doing correctly.
What the engine leans on without saying so
Two smaller traps came out of the other skin, arch-linux.cz's, and both share the same shape: a rule that looks purely decorative turns out to be load-bearing somewhere else.
Unsticking the engine's nav bar is a reasonable thing for a skin to want. On arch-linux.cz it also made the theme-switch button stop responding to clicks — not because the button broke, but because the bar sat in a CSS grid, where z-index takes effect without needing position at all, and the unstuck bar kept its higher layer sitting invisibly on top of the button. The site's own admin found it by actually clicking the switch; nothing in a screenshot would ever have shown it.
A second, smaller one: max-width on a flex item with flex-basis: 100% shrinks the item's hypothetical width rather than capping its contents — which is how an article body once ended up sitting beside its own meta line instead of below it. The cap belongs on the children, not the box around them.
One page of a listing, not the whole feed
A stylesheet can't read an address, so the front page and /page/2/ used to be indistinguishable to CSS — which mattered the moment either skin wanted a lead card on the first page only. Since 1.3.2 every listing's <body> says which kind it is:
.page-first .post-list-item:first-child { /* the lead card */ }
.page-cont .archive-note { /* only on the continuations */ }
Two classes rather than one, on purpose — a single mark on the continuations would mean writing the first page's look unconditionally and then undoing it property by property, and a rule that undoes another rule is the kind a later edit quietly stops undoing.

That's sh.cynicky.blog above — full-width lead card, dotted frame, tiles instead of a plain list, and not one edited engine file behind any of it. Its search stays a field in the engine's own bar rather than the modal Ghost itself would give it, because rebuilding it as one would mean owning behaviour the engine never promised. Neither skin, this one or arch-linux.cz's, ever tried for a pixel-perfect copy of what it imitates. Fidelity was never the goal. Belonging to the family was.
A skin stops being the cheap tool the moment it needs markup the engine doesn't build — at that point editing the template is the honest choice, not a stylesheet fighting to fake one. Short of that line, though, none of this needed a theme system: no hooks, no registry, no hierarchy of overrides to keep straight. One fact carries the whole feature. Your file loads last.
—
P.S. — sh.cynicky.blog isn't where cynicky.blog itself lives. The real site is still on Ghost, updated most days; sh.cynicky.blog is the same look, kept as a working installation of its own. Its author looked at a real migration and chose to stay where he already was — worth saying plainly, rather than letting a domain name imply otherwise.
