DEV Community

Cover image for Taste can't be linted. The tells can.
Ian
Ian

Posted on

Taste can't be linted. The tells can.

My product's homepage has six top-level headings. Every one has a small pill or a line of capitals above it, and every one ends on a word set in italic serif.

I didn't notice until I pointed a design lint's rules at the page. Two of the pills say phrases that appear word for word in the lint's source code.

The lint ships inside Hallmark, and it changed how I think about the "AI slop" complaint. You can't write a test for taste. You can write one for the tells.

A design skill with a script behind it

Hallmark is an MIT-licensed skill for Claude Code, Codex, Cursor and OpenCode, made by Hassan El Mghari (Nutlope) at Together AI. Version 2 (2.0.0 in its package.json) was merged on October 22. It was #1 on GitHub trending on Tuesday with 2,830 stars that day, and the GitHub API showed 39,048 stars when I checked that evening.

Most design skills are a long prompt. This one is a prompt plus machinery. SKILL.md opens with a 16-rule Floor no build may break, then a flow: read the project, ask once for real material, deal three themes, build and review the hero alone, then sweep the page. There are 50 themes (I counted 50 files and 50 preview images). The rules live in references/gates.md, and the checker is scripts/sloplint.mjs: 4,422 lines, zero dependencies, Node 18 and up.

Install is one line:

npx skills add nutlope/hallmark -g
Enter fullscreen mode Exit fullscreen mode

The tells, written down as rules

gates.md is the file worth reading. It lists 90 numbered gates, each with a tier and a class. Floor gates never ship. Reflex gates are a model's defaults, and a build may waive one in a CSS comment with a real reason. Class M means the static script can settle it from CSS and markup; R needs a real browser; J is judgment at review time. I counted 45 M gates and 9 judged only.

The named tells read like things most of us have shipped: the eyebrow (a short line announcing the heading under it), the tracked-caps label, one coloured word in a headline, middle dots between meta items, a form that says "Thanks" while sending nothing. Four are "house rules" (the eyebrow, tracked caps, an 80-words-per-screen budget, template punctuation), which sit in the Floor with no waiver.

What sold me is how literal the script gets. This is how it decides a gradient is purple-to-blue:

const purple = stops.some((h) => hueIn(h, 265, 330));
const blue = stops.some((h) => hueIn(h, 200, 264));
// ...
if (purple && blue) return 'purple-to-blue';
Enter fullscreen mode Exit fullscreen mode

Each stop is converted to OKLCH first, and only counts with chroma above 0.04, so a grey wash passes. transition: all is one regex. A tracked-caps label is uppercase, small caps or letter-spacing past 0.03em on anything named like a label (badge, meta, nav, caption and about twenty more) or set at 15px or under. An eyebrow is a short line before a heading with a class like kicker or badge, two style signals, or one of a fixed list of announcing phrases. It reads Tailwind class strings in .jsx, .tsx, .vue, .svelte and .astro files too, including cn() and clsx() calls.

I ran five of the rules on my own homepage

I didn't run sloplint. It's downloaded code, and I don't execute that for a post. I read the functions, ported five checks to about 100 lines of Python, and ran them on the HTML and CSS of somiibo.com, my product's homepage. The CSS is one 217 KB bundle: Bootstrap 5.3.6, then the site's own layer. My scripts print one line per finding; here is the tally:

transition: all          16 rules (15 site layer, 1 inline)
animated height/width     2 (Bootstrap's own .collapsing)
purple-to-blue gradient   7 rules, 2 in use (hero, pills)
tracked capitals          0
middle-dot strings        0
eyebrows                  6 above 6 headings
accented word             6 of 6 headings
focus styles              present (passes)
overflow-x: clip          missing on html and body
Enter fullscreen mode Exit fullscreen mode

The headings stung. Each h1 and h2 wraps its last word or two in <span class="text-accent">, an italic serif: "Grow your social media on autopilot", "Unlock your growth potential". Each also has an eyebrow: capitals over the h1, a pill over every h2. Two pills say "How it works" and "Get started", both on the lint's list.

The gradient is less clear-cut. The hero uses an eight-stop rainbow with #a166ab (hue 322 by my conversion) and #5073b8 (263) in it, so the rule calls it purple-to-blue, though no person would. The textbook case, #5b47fb to #00d4ff, sits in the same stylesheet, unused on this page. The static tier reads rules, not usage.

The passes were real: no positive letter-spacing anywhere, no text joining items with two middle dots, and focus styles exist.

I tried ianwiedenman.com too. Above my name sits "Hello, I'm" in a <p class="text-uppercase tracking-widest">, an eyebrow asking for tracked caps. But tracking-widest is a Tailwind class on a Bootstrap site, and neither of its stylesheets defines it, so the browser draws plain capitals. The glow behind it is three radial gradients whose custom properties are named --grad-purple-x, --grad-cyan-x and --grad-pink-x.

Why a lint beats a paragraph of taste

Here's my claim. "Make it look premium" in a prompt is a wish. A model can agree with it and has no way to check its output against it. "No stop between hue 265 and 330 beside one between 200 and 264" is a test. It fails a build, names a line, and doesn't get tired.

There's evidence this matters. In September a user, ktlesr, posted a measurement in issue #74 on an earlier version: four landing-page briefs built with and without the skill, scored by pattern checks. Pure black or white appeared on 40 of 40 pages without it and 14 of 28 with it. transition: all didn't drop (14 of 40 against 16 of 28). Their reading: that rule lived in a reference file the model never opened.

Version 2 reads like the answer. transition: all is now rule 2 of the Floor, in the file that always loads, and rule 16 tells the agent to run sloplint, when Node is available, and fix every FAIL before handing back. A rule in prose can be skipped. A script with an exit code can't, as long as something runs it.

Where a lint stops

A lint on tells is a floor, not a design. Strip the eyebrows and italic words from my homepage and it avoids a dozen mistakes, but it still has no idea of its own. Builds pushed away from the same defaults can also converge on a new shared look. In a July HN thread, mstkllah said design skills like this all end up in the same place unless you are very explicit. Hallmark's answer sits outside the lint: themes dealt from fifty, a log of past builds the deal avoids, and a reviewer subagent that reads the page cold. That part is judgment, and I didn't test it.

Some rules are opinions. "Never an eyebrow" and "never tracked caps" are house rules, so no waiver. A magazine that sets section labels in spaced small caps on purpose is breaking the Floor.

One rough edge in the source: --tier isn't validated. Pass --tier Floor with a capital F and the filter matches nothing, so the run prints clean and exits 0. PR #93 reported it and was closed unmerged; the commit I read (6033bab) still does it. My own checks were static only. The fold and anything measured on the rendered page need the render tier, which needs Chrome and puppeteer-core.

Try it on a page you already have

After the install, /hallmark audit ./index.html reports without redesigning anything. From a clone, the bare lint is node skills/hallmark/scripts/sloplint.mjs index.html. I haven't run either; I read the source and reproduced a few checks by hand.

One detail I liked: the photo library is 755 openly licensed pictures on Wikimedia Commons, credited on the page. The README says it skips Unsplash, Pexels and Pixabay because their terms bar AI tools from picking their images.

Which of these tells is on your own homepage right now, and is it there on purpose?

Top comments (0)