A Tailwind card for a news article looks like this in a typical vibe-coded app:
<div class="bg-white rounded-lg shadow-md border border-gray-200 p-6
flex flex-col gap-4 hover:shadow-lg transition-all duration-200
cursor-pointer max-w-sm w-full min-h-[200px]">
<div class="flex items-center justify-between mb-2">
<span class="text-xs font-semibold uppercase tracking-wider text-indigo-600">
Category
</span>
<span class="text-xs text-gray-400">Mar 27</span>
</div>
<h3 class="text-lg font-bold text-gray-900 leading-tight line-clamp-2">
Title
</h3>
<p class="text-sm text-gray-600 leading-relaxed flex-1 line-clamp-3">
Description text goes here.
</p>
<a class="text-sm font-semibold text-indigo-600 hover:text-indigo-800 mt-auto">
Read more →
</a>
</div>
Count them. Twenty-seven class names. That is the conservative estimate. Add dark mode and you are at forty-three. Add a responsive breakpoint variant and you are past fifty.
Wire renders the same card with this:
:::cards
### Title
Description text goes here.
[Read more](/path/)
:::
The output is semantic HTML with two CSS classes: .feature-grid and .feature-card. The border color, shadow, and hover state come from --border, --shadow, and --primary in your wire.yml theme block. Change the brand color once. Every card updates. No grep. No regex-replace across 400 templates. No asking the AI to "find all the places we used text-indigo-600."
Tailwind card 27 utility classes per component. Dark mode doubles it. Every class is a decision frozen in HTML. Changing the brand requires a site-wide find-and-replace that AI will get 94% right.
Wire card
4 lines of markdown. Renders to .feature-card with two CSS classes. Brand change: one line in wire.yml. Build. Done.
The Antigravity Kit and Wire walk into a design system
There is a project called Antigravity Kit, an AI skill that gives coding assistants design recommendations via a BM25 search engine over CSV files. You ask it what kind of card to use for a SaaS landing page. It searches products.csv and styles.csv and returns suggestions. Glassmorphism for fintech. Minimalism for B2B. The recommendations are correct. The implementation is still up to you.
That is the honest category: a design oracle. It tells you what to build. Wire tells the build what to reject.
Both approaches exist because the same problem is real: AI coding assistants, left alone, produce technically valid HTML that looks fine on Monday and drifts by Thursday. The Antigravity approach catches it upstream with guidance. Wire catches it downstream with gates.
The question is what "downstream" means when the AI is writing 300 pages per month.
What it costs to read your own site
Here is the metric the Tailwind community does not discuss: the token cost of your component library when you hand your site to an AI agent.
A typical React card component with Tailwind utility classes runs to roughly 380 tokens of markup: class names, nested divs, breakpoint prefixes, hover states. Wire's :::cards shortcode encodes the same information in 28 tokens. One card. A site with 400 pages that each reference two cards: 304,000 tokens of Tailwind markup versus 22,400 tokens of Wire shortcodes.
Token cost at Claude Sonnet pricing is not the point. $0.07 is not a business problem. Signal-to-noise ratio is. An agent reading 304,000 tokens of bg-white rounded-lg shadow-md border border-gray-200 extracts the same semantic signal as an agent reading 22,400 tokens of .feature-card. The structure is identical. The waste is not. Every class name that carries zero semantic information is a place where the model could have been reading your content instead.
The escape hatch exists. Use it carefully.
Wire is not a prison. If a specific page genuinely needs a custom component that Wire does not ship, the mechanism is extra_css in your wire.yml for site-wide additions, or the extra_css frontmatter key for a single page. One line. Scoped. Auditable in git.
The key word is "genuinely." Wire ships seventeen shortcode components covering cards, stats, banners, tabs, FAQ accordions, pricing tables, testimonials, progress bars, steps, splits, alerts, and visual breaks. Before reaching for extra_css, the question to answer is: which of these seventeen does not solve my problem, and why?
"It looked better in Figma" is not an answer. "My brand has a specific gradient treatment on premium content cards that requires a ::before pseudo-element with a primary-to-accent linear gradient" is an answer. Wire does ship that: it is on every .visual component already. The difference between a customer who overrides correctly and one who overrides reflexively is whether they read the components guide first.
The extra_css frontmatter key scopes CSS to a single page. This is a surgical tool, not a default. If you are using it on more than two pages, you have a theme problem, not a page problem. The fix is wire.yml, not per-page frontmatter.
What happens when nothing refuses
There is a recognizable pattern in vibe-coded apps six months after launch. The initial sprint was clean. The agent had clear instructions, a fresh codebase, and short context windows. Then feature requests arrived. The agent added components. The CSS grew. Nobody drew a line.
By month six, a typical blog post page has:
<div class="page-wrapper">
<div class="content-container">
<div class="inner-wrapper max-w-4xl mx-auto">
<div class="article-body prose prose-lg">
<div class="content-block">
<p class="paragraph-text text-gray-700 leading-relaxed">
Your actual content.
</p>
</div>
</div>
</div>
</div>
</div>
Five nesting levels to reach a paragraph. Every level added by a different agent session that did not know what the previous session built. The agent is not wrong. The output is valid HTML. It renders. Nobody noticed because nothing refused.
Wire's build enforces 91 lint rules on every single build. Not after deployment. Before deployment. RULE-33 refuses broken internal links. RULE-35 refuses orphaned pages. RULE-08 refuses thin content. These are not warnings. They are refusals. The build does not complete. You fix the issue or the page does not ship.
The nested div soup accumulates precisely because nothing in the pipeline says stop. Wire says stop on every build. Not to be obstructionist. Because a real site shipped a real defect, and the rule exists so yours does not.
CSS specificity as a bug surface
Here is where it gets structural. Tailwind's utility model generates CSS specificity scores that conflict in non-obvious ways at scale.
A standard Tailwind component card at specificity level 0-0-1-27 (zero IDs, zero pseudo-classes, one element selector, twenty-seven class selectors) will beat any rule in your global stylesheet written at 0-0-0-1. This is correct browser behavior. It is also the reason that when an agent adds a hover:ring-2 ring-indigo-500 to fix an accessibility issue, your outline style defined in wire.css (specificity 0-0-0-1) disappears silently. No error. No warning. The browser resolves the tie by source order, and source order in a vibe-coded Tailwind app is whatever order the agent injected the classes.
Wire's component system avoids this class by design. The .feature-card selector has specificity 0-0-1-0. All theme variables are inherited from :root at specificity 0-0-0-0. The cascade is predictable because the specificity hierarchy is flat. There are no twenty-seven-class selectors to win a specificity race that you did not know was happening.
The lint rules are a type system
Go further. CSS has no types. Tailwind is untyped utility classes with no compiler. class="rounded-lg rounded-sm" is valid Tailwind. The border radius is undefined. Both values apply. The last one in the cascade wins. This is not a bug in Tailwind. It is the absence of a type system applied to presentation logic.
Wire's lint rules are the type checker that CSS refuses to be.
RULE-105 enforces: card links must be internal. That is type: href where context=card must satisfy predicate: is_internal_path. RULE-106 enforces all-or-nothing card link consistency: either every card in a :::cards block has a link or none do. That is a union type constraint on a data structure. RULE-68 enforces progress bar values between 0 and 100. That is type: progress_value ∈ ℤ, 0 ≤ x ≤ 100. RULE-108 refuses markdown links inside :::stats, :::banner, and :::progress blocks. Those shortcodes render plain text, and a link in plain-text context is undefined behavior.
These are not style preferences. These are invariants. The build enforces them at compile time. A Tailwind app has no compile time. It has a browser that tries its best.
The CSS custom property endgame
Wire ships --primary-light: color-mix(in srgb, var(--primary) 12%, var(--bg)). That is a pure function: given primary color and background color, derive a tint that is always readable against the background, in any theme, including dark themes where the background is not white. The color-mix(in srgb, ...) interpolation is perceptually uniform. Wire gets this right without SCSS mixins, without a design token pipeline, without a Figma-to-CSS synchronization step. The variable is computed by the browser at paint time from two values you set in wire.yml.
Tailwind's equivalent is bg-indigo-50, a hardcoded hex value that does not know what your background is, does not adapt to dark mode, and requires a human (or an AI that may be wrong 5% of the time) to pick the right shade for your specific primary color. When your brand color is #6200ea and the AI picks bg-purple-50 because it looks close enough, you have a mismatch that lives in your codebase until someone cares enough to fix it. Nobody will care enough to fix it. The AI will not notice.
--radius: 0px ships by default in Wire. That is not a missing feature. That is an editorial statement encoded as a CSS variable that the entire component system inherits. Rounded corners are a choice that has to be justified. Sharp corners are the default because Wire is a publishing system for content that should be read, not a SaaS dashboard that needs to feel approachable. The customer who needs rounded corners sets --radius: 4px in their extra CSS block. The customer who ships with the default is making a correct choice for their context. The build does not care either way. The variable propagates.
The vibe coding app has rounded-lg on some cards, rounded-md on others, rounded on the search input, and no rounded on the modal dialog because that component was added in a different session. Nobody remembers why. The AI will add more rounded classes when asked to "make it look more modern." The build will pass. The design will drift. The lint system does not exist to catch it.
Wire refuses the drift by refusing the mechanism that produces it. Two CSS classes, forty variables, and ninety-one rules that run before the diff exists.
That is the component tax. You pay it once, in structure. Or you pay it continuously, in entropy.