- Tailwind v4 builds silently dropped the entire design system.
inkwell-theme.csspulled its two dependencies with@import url(...), a form Tailwind v4's import resolver passes through instead of inlining. The unresolved imports landed below the generated rules — a spec-invalid position for@import— so Lightning CSS (Next.js/Turbopack) warned and dropped them: utilities likebg-accentcompiled but pointed at undefined variables, and.btn/.card/light-dark()never reached the output. All three entry files (inkwell-theme.css,inkwell.css,tokens.css) now use the quoted form (@import './file.css'), which v4 toolchains inline correctly and browsers treat identically — zero change for<link>consumers.
Changelog
Every change to the design system, in reverse chronological order. Following semver: minor bumps add components or tokens; patches refine existing ones; majors break the public API.
The architecture release. Zero visual change — every resolved color in every palette and mode is identical to 2.1.0 (verified by a 288-entry resolved-value parity diff, the 192-pair contrast gate, and a real-engine smoke test) — but the cascade, the entry files, and the browser floor all change shape. Spec: docs/specs/2026-06-12-v3-architecture-design.md.
- Cascade layering for the pure-CSS path.
inkwell.cssnow importsinkwell-components.cssinto@layer inkwell, so unlayered consumer CSS always overrides Inkwell components without specificity fights — the same ergonomics the Tailwind path has had since 2.0. - Derived tints. Every
*-tint/*-focus-ring/*-strong-border/*-tint-bordertoken now derives from its base viacolor-mix(in srgb, var(--base) P%, transparent). A palette that overrides--accentgets correct tints for free; tokens are still declared explicitly wherever the contrast gate forced a hand-tuned value (the derive-vs-declare rule, now documented inDESIGN_SYSTEM.md). - Release tags resumed:
v2.1.0marks the last 2.x for raw-URL pinning;v3.0.0tags this release.
- The Pattern B dark cascade is now
light-dark(). Every color token is a single:rootdeclaration; mode switching iscolor-schemeflipping (light darkauto,[data-theme]overrides, print forced light). The two byte-identical dark blocks ininkwell-tokens.cssand the duplicated dark blocks in every variant are gone — a brand token's dark value now lives in exactly one place instead of four across a canonical+variant pair. The dark.selectchevron override is the one surviving Pattern B residue (light-dark()only accepts colors; the chevron is abackground-imagedata URI). - Variant files shrank ~45% (97 → ~50 lines): single-declaration overrides; burgundy inherits every derived tint from canonical.
inkwell.cssis the canonical entry and imports the two source files directly. Install is three files (inkwell.css,inkwell-tokens.css,inkwell-components.css).- Scripts (
build-tokens-json.mjs,check-contrast.mjs) resolvelight-dark()/color-mix()to concrete per-mode values. The dark-block parity assertion is gone — the failure mode it guarded no longer exists.tokens.jsonschema and values are unchanged (two alphas normalize:0.10→0.1).
tokens.cssis now a one-line alias ofinkwell.css, kept so pre-3.0 consumers keep working. Removal slated for 4.0.
- Browser floor: Chrome/Edge 123, Firefox 120, Safari 17.5 (all mid-2024) —
light-dark()is the gating feature. If you must support older browsers, pin 2.1.0:https://raw.githubusercontent.com/vscarpenter/inkwell/v2.1.0/…. - If you load a second reset (normalize.css etc.) alongside Inkwell, it now wins over Inkwell's base/type rules — unlayered CSS beats
@layer inkwell. Drop the second reset (Inkwell ships its own) or import it into a lower layer:@import url('normalize.css') layer(reset);. Your intentional overrides need no change — they now always win, which is the point. - If you link
tokens.css, switch toinkwell.cssat your convenience; the alias keeps working until 4.0. - If you parse
inkwell-tokens.cssyourself, per-mode values now live insidelight-dark(light, dark)functions in:rootinstead of separate dark blocks;tokens.jsonis unchanged and remains the stable machine interface.
--accent-ink/--on-accenttokens. The accent now has two jobs with two tokens: ink (links, selected tabs, badge text, the global focus outline, the.dropcapfirst letter) and fill (button backgrounds, with--on-accentas the label color — the checkbox checkmark now draws in--on-accenttoo). Canonical indigo is dark enough for both, so it defaults--accent-ink: var(--accent)and--on-accent: var(--paper)— zero visual change. Variants whose accent is too light to read now override the ink: clay#A04E2C, sage#3A7456. Clay's light mode also flips--on-accentto slate and its hover to a lighter coral (--accent-d: #E08B6E) — white-on-coral measured 3.12:1.--olive-dark(#566740light /#9CB07Adark) — olive as text, mirroring the existing--warning-darkpattern..badge-success(was 3.10:1),.stat-delta.up(3.68:1) and.pill.resolved(3.68:1) now clear WCAG AA.--gray-400+--control-border. Checkbox/radio borders and the switch off-track were 1.56:1 — below WCAG 1.4.11's 3:1 for functional boundaries.--control-borderis a 1.5px border on the new--gray-400neutral step (3.2:1+ on both surfaces); decorative panel hairlines stay--gray-300.--info-tint/--info-strong-border..alert.is-infono longer borrows the accent tint;--infofinally has a job.- Button states:
:disabled,:active,.btn-sm, and anaria-busy="true"spinner (reduced-motion-safe). Pairaria-busy="true"withdisabledfrom JS — the CSS only blocks pointer events, not keyboard activation. .sr-onlyand.skip-linkaccessibility utilities..navbar/.navbar-inner/.field-row/.card-grid/.tbl-scrollpromoted into the component layer (every consumer was rebuilding the navbar fromindex.html).scripts/check-contrast.mjs— zero-dependency WCAG gate asserting 192 token-pair ratios across 4 palettes x 2 modes; wired into CI next to the tokens.json drift check.- Print stylesheet,
::selectionstyling (slate-on-oat — the accent tint was too faint to register as a selection),text-wrap: balance/prettyon headings and ledes.
.stat-card.is-primarydrops the 4px accent left stripe for a 1.5px full accent border.--warning-darklight value#A06A2A→#85561E(3.95:1 → 5.41:1 on its tint)..tbl thead thtext--gray-500→--gray-700(was 4.26:1 on the header fill).- Dark-mode hover now lifts. Dark
--accent-d#6273C0→#8B9ADB(button labels on the old hover read 3.90:1) and dark--accent-tintalpha 0.18 → 0.10 (badge-accent text read 4.07:1). Same treatment in the variants: sage dark--accent-d→#7FB294, burgundy dark--accent-d→#D57E7Ewith--accent-tintalpha → 0.10. Burgundy dark also sets an explicit--accent-ink: #D07878— the raw lifted accent can't clear 4.5:1 on the badge tint. - Variant grays: clay/sage/burgundy
--gray-500darkened to clear 4.5:1 on their surfaces (the BACKLOG "variant gray-500" item, option a). - The manual dark theme is screen-only. Both
[data-theme="dark"]blocks are wrapped in@media screen, so print always renders the light palette. - Theme toggle storage key unified on
inkwell-theme(wastheme-previewonindex.html/preview.html); pages still read the legacytheme-previewkey as a fallback, andexamples/demo.jsvalidates the stored value and migrates it to the new key on first load. The snippet is hand-copied per page and marked KEEP IN SYNC; the gallery (examples/index.html) gets the palette pre-paint too, but deliberately still no toggle widget. html { scroll-behavior: smooth }now respectsprefers-reduced-motion.
- Dark
.selectchevron now matches each variant's own gray (was canonical-only — BACKLOG item). - Pre-paint snippet eliminates flash. Saved theme and palette now apply via a
<head>pre-paint snippet — no flash of incorrect theme or palette on load.
- If you styled against
.stat-card.is-primary's left stripe, the marker is nowborder-color: var(--accent)on the full frame. .alert.is-infois now steel-blue (--info), not indigo. If you wanted the accent look, use.alertwith a custom tint.- The global focus outline follows
--accent-ink; in clay/sage it is now a darker, AA-passing shade. - Re-copy
variants/*.cssalongside the core files — 2.0 variant files define no--accent-ink/--gray-400, so against a 2.1 core they fall back to canonical values (failing raw-accent text in clay/sage, cool-gray control borders on warm palettes).
- Palette toggle on every example page. A
?palette=query string selects one of four palettes —indigo(default),clay,sage,burgundy— on every page inexamples/and onpreview.html. The widget sits next to the existing theme toggle: localStorage persistence underinkwell-palette,?palette=URL state viahistory.replaceState, click-through cycling. Resolution order on load is URL → localStorage →indigo.examples/index.htmland the rootindex.htmlare intentionally left without the toggle (gallery hub + starter template, respectively). scripts/build-tokens-json.mjs— zero-dependency Node script that regeneratestokens.jsonfrominkwell-tokens.css. Closes the audit-#20 drift risk: the JSON is no longer hand-maintained, and a newtokens-check.ymlworkflow runsnode scripts/build-tokens-json.mjs --checkon every PR and main push that touchesinkwell-tokens.cssortokens.json, failing the build with a line-level diff if the JSON is stale. The script also asserts the two dark-mode blocks (@media (prefers-color-scheme: dark)and[data-theme="dark"]) declare identical values — a parity invariant the manual JSON couldn't enforce.
- Variants are now override-only consumers of canonical.
variants/tokens-clay.css,tokens-sage.css,tokens-burgundy.css,tokens-indigo.cssare removed. Three new files —variants/clay.css,variants/sage.css,variants/burgundy.css— replace them. Each is ~70 lines, contains only brand-layer token overrides for:root+ the dark cascade, and is meant to load afterinkwell.css. The "two universes" naming rule (canonical uses--accent, variants use--clay, never mix) is retired. --claytoken namespace removed. Variants now use--accent,--accent-d,--accent-tint,--accent-focus-ring,--accent-strong-border— the same names canonical uses. Component CSS is no longer duplicated invariants/tokens-clay.css; components live once, ininkwell-components.css.variants/preview-clay.html,preview-sage.html,preview-burgundy.html,preview-indigo.htmlremoved.preview.html?palette=clay(etc.) replaces them.variants/compare.htmlnow embeds the canonicalpreview.htmlwith?palette=Xinstead of the per-palette pages — one source of truth for the comparison view.tokens.jsonformatting normalized. The generator emits a deterministic layout: 2-space indent, single-line objects up to 120 chars, multi-line otherwise;rgba()values now match the CSS source spacing (rgba(59, 74, 140, 0.14)) instead of the previous compact form. No token values changed — just the JSON formatting. Consumers parsing JSON structurally are unaffected; consumers diffing raw bytes will see a one-time noise commit.
- preview.html now listens for the parent-iframe
set-themepostMessage.variants/compare.htmlbroadcasts theme changes to its iframes so the maintainer can flip light/dark across all palettes at once. The legacy per-palettepreview-*.htmlpages (now removed) presumably listened for this; canonical preview.html did not. A smalladdEventListener("message")inside preview.html's theme IIFE restores the broadcast — transient (no localStorage write), per-session.
For consumers of Inkwell 1.x:
| Old | New |
|---|---|
<link href="variants/tokens-clay.css"> | <link href="variants/clay.css"> |
<link href="variants/tokens-sage.css"> | <link href="variants/sage.css"> |
<link href="variants/tokens-burgundy.css"> | <link href="variants/burgundy.css"> |
<link href="variants/tokens-indigo.css"> | (none — canonical inkwell.css is the indigo palette) |
var(--clay) | var(--accent) |
var(--clay-d) | var(--accent-d) |
var(--clay-tint) | var(--accent-tint) |
variants/preview-clay.html (or any other preview-*.html) | preview.html?palette=clay |
Variant CSS files no longer @import canonical. If you currently link only variants/tokens-clay.css, you must now link both inkwell.css and variants/clay.css (in that order). For Tailwind v4 consumers: load the variant CSS after the Tailwind build output so the cascade order works — see the new "Using a non-default palette with Tailwind" section in TAILWIND.md.
- Editorial primitives. New
.dropcap(4.2em first-letter accent ornament),.pullquote(serif italic with 1.5px accent left-rule),.byline(+.byline .authorfor the italic-serif author override), andfigure.figure(+ caption with rule-anchored italic-seriffigcaption). These are the smallest set that materially backs the system's "editorial interfaces" claim — seepreview.html§24 for a working sample. .t-ledetype style +--t-lede: 20pxtoken. Serif italic deck/intro paragraph that sits between a headline and the body in long-form prose. Closes the gap between.t-h3(19px) and.t-body(16px). Also exposed to Tailwind astext-lede..eyebrow-serif— italic-serif sibling to the existing mono.eyebrow. Mono stays the right call for product/dashboard contexts (eyebrow signals technical metadata);.eyebrow-serifis the magazine-kicker treatment for editorial contexts. Same accent rule, different voice..segmentedsegmented-control component. A pill-shaped group of mutually-exclusive options sharing one outer border — use for 2–4 short labels (theme toggle, view modes, density). Mark the active option witharia-pressed="true"or.is-active. The example pages had been hand-rolling this from three.btn-ghosts; now it's a first-class primitive.
- Headlines run weight 600.
.t-display/.t-h1/.t-h2/.t-h3bumped from 500 to 600, with tracking tightened a notch (display now -0.025em, h1 -0.018em, h2 -0.012em, h3 -0.01em; display line-height 1.05). Weight 500 inui-serif-fallback territory was reading as "slightly seriffed grotesque" at display sizes — 600 lets the serifs carry. Tailwind aliases ininkwell-theme.cssmirror the new values. --serifstack tightened. Droppedui-serif(which resolves to wildly variable fonts across platforms — New York on Apple, Noto Serif on Android, often DejaVu Serif on Linux — and can't be QA'd). New stack leads with Iowan Old Style (iOS/macOS), then Palatino (Windows), then Source Serif Pro (where installed), then Georgia (universal fallback). Every link in the chain is a serif we've verified at display sizes. No webfont — platform-stack rule preserved..alert-titlesimplified. Dropped the per-variant title color overrides (info/success/warning/danger titles previously rendered in the tinted hue, which made the info title indistinguishable from a link). Title now stays slate; the tinted background + 1.5px tinted border carry the semantic. Pair with an icon orrole="alert"if a third signal is needed..card.is-link:hovercalmed. Dropped the 3px translateY lift and the shadow swap; the hover now shifts only the border color (gray-300 → gray-500). The lift-and-glow gesture was a Material/Linear convention that fought the letterpress-quiet identity the 1.5px hairline establishes. Card transition list trimmed accordingly..stat-card.warnrenamed to.stat-card.is-primary. The old name said "warning" while the color was--accent— the meaning was neither warning nor primary depending on which signal you read. The class now means what it always did visually: this is the headline metric in the row. Markup must update:class="stat-card warn"→class="stat-card is-primary". Mirrored invariants/tokens-clay.cssand the fourvariants/preview-*.htmlpages.
class="stat-card warn"→class="stat-card is-primary"everywhere consumer markup uses it. No CSS-side fallback shipped; rename your markup.- If you were relying on
.alert-titletaking the tint color, that's gone. The title is nowvar(--slate)in every variant. Consumers who want a colored title can target.alert.is-X .alert-titlethemselves. - If you depended on
.card.is-link:hoverlifting and casting a shadow, both are gone. Hover now changes only the border color. Re-add the lift in your own CSS if you specifically need it for a marketing surface — but consider whether the system's calmer default fits better. - The
--seriftoken resolves to a different font on every OS now. If you previously eyeballed line-length aroundui-serif's OS-default rendering, re-check at display sizes — Iowan Old Style and Palatino set slightly tighter than New York / Noto Serif did.
tokens.jsondrift risk (audit #20). The JSON has been updated by hand for--t-ledeand the new serif stack to keep it in sync for this release, but the manual-mirror process remains. Tracked inBACKLOG.md.
*:focus-visibleno longer overrides element corner radius. Removed the strayborder-radius: var(--r-xs)line ininkwell-components.css— focused buttons (8px → 4px) and focusable cards (14px → 4px) were visibly snapping to a tighter corner during focus. Modern browsers already roundoutlinearound the element's ownborder-radiuswhenoutline-offsetis positive, so no replacement is needed..btnheight bumped 36px → 38px to match.input/.select. Form rows with a button next to an input now align exactly.- Six raw
rgba()literals replaced with tokens ininkwell-components.css(per the §4 anti-pattern: no hex literals in component CSS). Five of them were also a dark-mode correctness bug — the colors were locked to the light-mode hue. New tokens:--olive-strong-border,--warning-strong-border,--rust-focus-ring,--backdrop. Affects.alert.is-success,.alert.is-warning,.chip-dot.safe,.input.is-error:focus,.dialog::backdrop— borders and scrims now lift correctly in dark mode. .tldr codebackground no longer disappears in dark mode. The chip used a hardcodedrgba(255, 255, 255, 0.08)— white at 8% — which was invisible in dark mode (where.tldrflips to a light surface). Replaced with the new--tldr-code-tinttoken, which carries paired light/dark values so the tint inverts with the surface. This was also the lastrgba()literal ininkwell-components.css, closing out the §4 anti-pattern.- Documentation drift from the 1.3.0 split swept up. Updated
DESIGN_SYSTEM.md§1.2 / §3 / §6 / §7,README.mdcomponent and example counts,CONTRIBUTING.mdproject layout,AGENTS.mdcanonical-source claim, andagent-instructions.md§9 / §10 file lists to reference the post-split structure. No behavior change — just docs catching up to the CSS.
- Tailwind
text-*type-scale modifiers.inkwell-theme.cssnow bundlesline-height,letter-spacing, andfont-weightinto every--text-{display,h1,h2,h3,body,small,caption,eyebrow}via Tailwind v4's--text-{name}--{modifier}syntax. Result:class="font-serif text-h1"in a Tailwind template now produces the same rendered output asclass="t-h1"in pure-CSS markup — the two consumption paths converge. - New tokens (light + dark) in
inkwell-tokens.css:--olive-strong-border,--warning-strong-border,--rust-focus-ring,--backdrop,--tldr-code-tint. Mirrored intokens.jsonand the §1.1 token table inDESIGN_SYSTEM.md. None of these change existing light-mode appearance — they fix dark-mode drift and bring the system back in line with its own "no literals in components" rule. tokens.json_meta.canonical_sourcecorrected fromtokens.csstoinkwell-tokens.css(the 1.3.0 split made the latter authoritative).
- Tailwind v4 support. New
inkwell-theme.cssentry file makes Inkwell drop into any Tailwind v4 project when used alongsideinkwell-tokens.cssandinkwell-components.css— every token becomes a utility (bg-accent,text-slate,border-accent,font-serif,rounded-md,text-display,max-w-default), the 1.5px signature is exposed asborder-hair, and a@custom-variant darkblock teaches Tailwind to honor Inkwell's[data-theme]toggle. In Tailwind entry CSS, import only@import "tailwindcss"; @import "./inkwell-theme.css";. Notailwind.config.js, no JavaScript preset. TAILWIND.md— install guide, components-vs-utilities rule,border-hairconvention, cascade-order verification, two-universes warning.examples/tailwind.html— live integration demo that opens directly in a browser (uses the@tailwindcss/browserCDN for the demo; production builds useinkwell-theme.cssdirectly).- Updates to
agent-instructions.mdandREADME.mdcovering the new Tailwind path for AI coding agents and human developers.
- Split
tokens.cssintoinkwell-tokens.css+inkwell-components.css. The original file is now a two-line aggregator (@importshim) so existing consumers see no behavior change — linkingtokens.cssorinkwell.cssworks exactly as before. The split exists to supportinkwell-theme.css, which needs to wrap components in@layer componentswhile keeping:roottokens unlayered. - GitHub Pages workflow now syncs all five canonical CSS files into
examples/on deploy (previously two).
No migration needed for existing consumers. inkwell.css and tokens.css continue to work unchanged. If you maintain a fork that copies only tokens.css into your project, switch to copying tokens.css + inkwell-tokens.css + inkwell-components.css together — the new tokens.css is a shim that @imports the other two.
Tailwind v4 users: keep inkwell-tokens.css, inkwell-components.css, and inkwell-theme.css side by side, then drop @import "tailwindcss"; @import "./inkwell-theme.css"; into your entry CSS. See TAILWIND.md.
Tailwind v3. v3 requires a JS preset that conflicts with Inkwell's no-build pitch. v4 (October 2024 or later) is the supported integration; v3 users should upgrade or use Inkwell via inkwell.css directly without Tailwind utility coverage.
.tabs,.tab,.tab-panel— underline-style tab navigation. Botharia-selected="true"and.is-activeare styled, so consumers can use either pattern..tooltipwith[data-tooltip]— CSS-only bubble that reveals on:hoverand:focus-visible. Pair witharia-labelfor screen readers..breadcrumbs—<ol>-based list with/separators rendered via::before, andaria-current="page"styling for the leaf..pagination— numbered list with prev/next,aria-current="page"for the active page, and a.ellipsisspan for compressed ranges..skeleton(+.is-text/.is-title/.is-block/.is-circle) — shimmer placeholder using the gray-100/200 gradient so it stays warm in dark mode. Honorsprefers-reduced-motionby collapsing to a flat gray..empty-state(+.empty-state-icon) — centered no-data panel with icon slot, headline (serif), body, and action.tokens.json— machine-readable mirror oftokens.cssfor Figma plugins, Tailwind configs, and Style Dictionary.tokens.cssremains canonical; the JSON header documents this and notes it must be regenerated when CSS tokens change.- New example pages:
landing.html,search.html,changelog.html,not-found.html. CHANGELOG.md(this file).
examples/index.html— bumped to 14 example cards.DESIGN_SYSTEM.md— component table updated with the new entries.
.field,.field-label,.field-help,.field-error— vertical field-group wrapper for labelled form controls with helper or error text..input.is-errorand:disabledstates on form controls..textareaand.select— minimal styling matching.input, with a theme-aware SVG chevron on.select..radioand.switch— selection controls following the.checkboxpattern; switch is a pill-shaped track with a sliding knob..alertwith.is-info/.is-success/.is-warning/.is-danger— flat-tinted system message component, distinct from the inverted.tldrcallout..code-block— multi-line<pre><code>panel with optional.copybutton slot in the top-right..dialog— styling for native HTML<dialog>. Open viadialog.showModal()for free focus trap,Esc-to-close, scroll lock, and::backdropblur.kbd/.kbd— keyboard-shortcut chip in mono with a 2px bottom border line.- New example pages:
forms.html,docs.html. - New §5 Accessibility section in
DESIGN_SYSTEM.mddocumenting the deliberate sub-3:1 hairline border choice and the contrast guarantees for new tokens.
--gray-500:#85858A→#6F6F75in light mode (5.05:1 on--paper, 4.64:1 on--ivory) — clears WCAG AA 4.5:1 for body text. Affects.t-caption,.stat-label,.eyebrowcolor,.toc .n,.sec-head .count, and--input::placeholder. Dark mode unchanged at 6.4:1.
If you've forked stale documentation that hard-codes --gray-500: #85858A, replace with #6F6F75 to clear AA. No component classes were renamed.
- GitHub Actions workflow (
.github/workflows/pages.yml) deploysexamples/to GitHub Pages on push tomain. - The workflow syncs canonical
tokens.cssandinkwell.cssintoexamples/at deploy time so the live demo never drifts from source. - Live demo link in README pointing to inkwell.vinny.dev.
- Initial release with the Indigo & Cloud palette.
tokens.css— the canonical token layer plus all component CSS.inkwell.css— brand-named alias that re-exportstokens.css.- Pattern B dark mode (auto via
prefers-color-scheme: dark, manual override via[data-theme="light"|"dark"]on<html>). - Components:
.btn,.input,.checkbox,.badge,.card,.stat-card,.tbl,.tldr,.pill,.timeline,.chip-dot,.avatar,.toc,.sec-head,.eyebrow. DESIGN_SYSTEM.md— the canonical spec. Token tables, component list, dark-mode cascade, anti-patterns.preview.html— comprehensive component showcase.index.html— starter template with navbar, hero, and three-state theme toggle.variants/— three alternate palettes (clay, sage, burgundy) preserved for reference.
Made with hairlines and serifs by Vinny Carpenter. The 1.5px is on purpose.