J3W1 / DESIGN SYSTEM
Foundations
True Black / Rose is the permanent default: black canvas, warm dark panels, the original rose foregrounds and dark-red actions. Exact roles and accessibility rules remain authoritative.
Identity
What the j3w1 theme is, where it came from, and the five rules that keep a port recognisable.
True-black canvas (#000000), near-black panels (#100c0c) and chrome (#090707), with dark-red raised layers, rose interface text (#e99499), near-white reading text (#f4eeee), dark-red selections (#531310), a bright red focus ring (#e53935), 1px square borders, zero radii, one monospace family for chrome, reading and code. Red marks interaction: focus, selection, active borders, primary actions. It is not a decoration and it is not applied to everything. Darkness is the canvas; readable content is the priority.
Five rules keep an implementation recognisable:
- Preserve the warm-black / rose / red identity. Never substitute a charcoal-and-blue dark theme because the target ships one.
- Use the role assigned to a token, never the token whose colour happens to look closest.
- Keep content, selection, keyboard focus, diagnostics and changed text distinguishable from one another. Selection is a fill; focus is a ring.
- Nothing is rounded, blurred, glowing or gradient-filled. Elevation is a 1px border and, for modals, a backdrop.
- Preserve the host application’s behaviour, keyboard handling and accessibility settings. Geometry and typography limits of a host are documented deviations, not reasons to rewrite it.
Where it came from
The palette began as a Manjaro i3 workstation preserved in j3w1/1w3j: a pywal
ramp pulled from a red wallpaper, then frozen and hand-tuned slot by slot in
config/Xresources while watching what ls, vim and the agnoster prompt
actually rendered. The i3 configuration supplied the rest of the idiom: 1px
pixel borders, 14/−2 gaps, tabbed layouts, a hidden top bar with Chinese
status labels, dunst toasts framed in #ae1914, dmenu selections in
#630f0d.
The contemporary reference is j3w1/j3w1.github.io, a working web
reinterpretation of that workstation. Its stylesheet named the roles this
specification adopts (--terminal, --surface-raised, --selection,
--focus, --prose, --muted …) and wrote the first contrast contract. Both
sources are pinned by revision in references/sources.json; after adoption
this repository is the authority and later changes in either source are
proposals, not automatic updates.
What “modern” means here
Clear organisation, responsive behaviour, strong keyboard and screen-reader support, precisely specified states, and controls that are pleasant to operate. It does not mean pill buttons, glass panels, gradients, neon glows, a Material-style redesign, or red on every surface. Sixteen concrete misreadings are rendered as labelled non-examples on the site.
The specification page’s generated literal-color previews have the narrow geometry, checkerboard and motion exception recorded in D-014; the component and consumer rules remain as written.
Profiles
default— the approved everyday composition. Monochrome red/rose for chrome, text, borders, interaction and actions; three bounded hues (amber, green, blue) for status, diagnostics and diffs (D-001); monochrome syntax.heritage-ansi— the sixteen Xresources slots and the historical assignments, exact, including the ones that fail the contrast floor. For terminals, archives and fidelity; never for new interface text.extended— a proposed overlay that colours syntax, a full sixteen-slot semantic terminal palette and chart series with the same three hues. Not approved; the site labels it.
Naming
Roles are color.<group>.<role> with a closed group list: surface, text,
border, interaction, action, status, code, diagnostic, diff,
terminal, chart, icon. Primitives are color.primitive.<family>.<step>
where the step rises with luminance. A CSS custom property is the path with
dots replaced by dashes: color.interaction.focus.ring →
--color-interaction-focus-ring. Roles alias primitives; primitives carry
provenance.
Foundations
Surfaces, text ladder, borders, interaction states, focus, typography, spacing, density, elevation, motion, icons and layout, with the rules that govern each role.
Values live in tokens/; this document says what each role means and where it
may be used. Every rule below is normative for the default profile.
Surfaces
surface.desktop and surface.canvas use true black (#000000). Readable
content sits on the canvas or default (#100c0c) panels and cards, raised (#160b0b) for sticky headers,
menus and popovers, overlay for dialogs and drawers, sunken (#000000)
for reading and detail panes, input for form-control fills, chrome and
chrome-alt for window frames and bars. Layering is subtle by design; it may
not erase the boundary of a control, which is why controls also carry
border.control.
Never tint photographs, video, document content or user-defined data colours to put more red on screen.
Text ladder
| Role | Value | Contrast on canvas / default / raised | Use |
|---|---|---|---|
text.prose | #f4eeee | 18.31 / 16.96 / 15.83 | long-form reading, on-fill text |
text.bright | #ffa2a7 | 10.98 / 10.17 / 9.49 | headings and emphasis |
text.default | #e99499 | 9.16 / 8.48 / 7.92 | interface and body text |
text.muted | #bd787d | 6.15 / 5.69 / 5.31 | secondary text; the darkest colour permitted for chrome text |
text.subtle | #ad7175 | 5.40 / 5.00 / 4.66 | metadata, comments, line numbers |
text.disabled | #8a5559 | 3.52 / 3.26 / 3.05 | disabled controls only (exempt under WCAG 1.4.3; at least 3:1 by policy) |
text.accent | #e53935 | 4.97 / 4.60 / 4.30 | accent text on canvas and default surfaces only |
text.accent-strong | #f73f35 | 5.72 / 5.29 / 4.94 | accent text safe on every surface |
Ordinary text must reach 4.5:1 against its actual background in every state.
#a3676b and #7d1310 are never text roles in default, even when a
particular darker background improves their ratio; they
remain in service as border.control, icon.decorative, status.neutral.fill
and border.disabled. Metadata and comments that a person needs to read are
not decorative and use text.subtle, not a graphic colour.
The specification page’s generated literal-color previews have the narrow geometry, checkerboard and motion exception recorded in D-014; the component and consumer rules remain as written.
Borders
Every border is 1px (border.width.default); 2px (border.width.emphasis) is
reserved for the selected indicator bar, the sticky-header rule and the invalid
border. The quote rule is the one wider edge, at twice the emphasis width,
because it marks a block rather than bounding a control. border.divider (#2b0e0d) separates rows and sections;
border.default (#531310) is decorative and may never be the only visible
edge of a control; border.control (#a3676b, 4.71:1) is the boundary of
every form control at rest; border.active (#e53935) marks the focused or
active control; border.overlay (#e53935) frames every floating layer.
Styles: solid by default; dashed only for focus, drop targets and read-only
fields; dotted for hints; double for conflicts. Radius is 0 everywhere,
including checkboxes, switches, chips, avatars, badges and tooltips.
Interaction states
Every interactive component declares which of these apply and demonstrates every applicable one, including the combinations. Precedence when several apply: disabled > loading > invalid > selected > pressed > hover; focus-visible is always drawn on top and never suppressed by another state.
| State | Visual | Non-colour channel |
|---|---|---|
| default | rest tokens | — |
hover (@media (hover: hover) only) | background → interaction.hover.bg; links use it too with text.link-hover, menu items → interaction.hover.bg-strong | link underline thickens 1px → 2px; cursor |
| active / pressed | background → interaction.pressed.bg; border → border.active | border change; no translation |
| focus-visible | the ring (below) | the ring itself; never a glow or a colour shift alone |
| selected | fill interaction.selection.bg with interaction.selection.text; tabs and navigation use a 2px border.selected-indicator | aria-selected / aria-current; a check glyph in lists |
| selected + focus-visible | the selection fill and a ring in the fill’s on-fill colour (interaction.focus.ring-container, 7.48:1 on the selection) | both visible at once |
| selected, container inactive | interaction.selection.inactive-bg with interaction.selection.inactive-text; no ring | lightness drop between the two fills |
| disabled | text.disabled, border.disabled, interaction.disabled.bg; no hover or pressed styling; still focusable when aria-disabled | disabled / aria-disabled; cursor: not-allowed; never opacity |
| invalid | border → status.danger.border at 2px; message in status.danger.text with the ✕ glyph; aria-invalid; aria-describedby | border width 1 → 2px; glyph; message |
| invalid + focus-visible | the 2px danger border and the ring in interaction.focus.ring-container at focus.offset-invalid | double boundary |
| read-only | text.default; 1px dotted border.divider bottom; no fill; focusable; selectable | readonly; dotted edge |
| loading | size unchanged; a static ⋯ glyph replaces the leading icon; aria-busy | glyph; no animation under reduced motion |
| busy / indeterminate | static 45° stripes of action.primary.bg on interaction.pressed.bg; aria-valuetext | stripes |
| checked / mixed | fill action.primary.bg, glyph ✓ or – in action.primary.text | glyph |
| expanded / collapsed | SVG chevron rotated 0 / 90° within the motion budget | aria-expanded |
| current | 2px border.selected-indicator and text.bright | indicator bar |
| drop-target / dragging | dashed interaction.drop-target border; interaction.marquee fill | dashed pattern |
Rules that apply everywhere:
- Selection is a fill; focus is a ring. Neither borrows the other’s form. A ring still has to be visible on whatever it sits over:
#e53935measures 2.52:1 onaction.primary.bgand 1.13:1 onstatus.danger.fill, which is why the ring recolours on those fills instead of relying on hue. It measures 3.38:1 on theinteraction.selection.bgrecessed by D-027, so a selected row keeps the ordinary control ring. - Focus ring (D-006; component mapping clarification D-015). Controls and rows:
outline: 1px dashed {color.interaction.focus.ring}; outline-offset: -2px. Focusable containers (panes, windows, dialogs, cards):outline: 2px solid {color.interaction.focus.ring-container}; outline-offset: -3px. On filled surfaces use the component’s explicit ring mapping: primary buttons and checked controls useinteraction.focus.ring-containerwhere declared; destructive buttons use their declared on-fill text role. The selection fill is the exception and takes the ordinary ring, on the measurement in rule 1. Keep the control or container geometry independently of the colour mapping.:focus:not(:focus-visible)draws nothing. Forced-colors mode usesHighlight. A host focus indicator is never removed without this replacement. - Never colour-only. Every state and every status carries a second channel: a glyph (
✕ ! ✓ i ·), a border width, an underline pattern, or an ARIA state. Charts carry pattern fills. - Hover changes fills and borders, never text colour alone, and only on hover-capable pointers.
- Disabled never uses opacity. Opacity leaks the background and breaks contrast accounting.
- Contrast floor. Text ≥ 4.5:1 and control boundaries, rings and meaningful graphics ≥ 3:1 against the actual background of the state, measured after compositing translucent colours.
- Motion. Durations are 80 / 120 / 150 ms at most, on opacity, transform and background only; under
prefers-reduced-motion: reduceevery duration is 0.01 ms, nothing slides or shimmers, and no behaviour waits ontransitionend.
Actions
Four button tones. primary is the only filled button at rest
(action.primary.bg #7d1310 with text.prose, 9.28:1); secondary is an
outline (border.control, text text.bright); tertiary is a ghost or icon
button (text.default, hover interaction.hover.bg); destructive is an
outline in action.destructive.text (#f73f35) that fills with
action.destructive.hover-bg (#dc282e) and #f9faf9 text (4.58:1) on
hover, and a filled variant for confirm dialogs. Destructive actions also
require a confirmation step or an undo affordance; the colour is not the
safeguard.
Status, diagnostics and diffs (D-001)
Five statuses: danger (#f73f35 text, #dc282e fill), warning
(#c9973f), success (#86a46f), info (#7e9ebb), neutral (#bd787d
text, #a3676b fill). Each has text, fill, on-fill, tint and border
roles, a mandatory glyph (✕ ! ✓ i ·) and a distinct underline or border
pattern (wavy, dashed, solid, dotted, double). The three extension hues are
desaturated and warm-shifted so they sit inside the identity, all between 6.5:1
and 7.6:1 on every surface, and are forbidden in surface, text,
border, interaction and action. Purple, cyan and magenta do not exist in
the theme.
Diagnostics: error = wavy #e53935 underline, #f73f35 text, #e53935
stripe, #2b0e0d line background; warning = wavy amber; info = dotted blue;
hint = dotted text.subtle; unused = text.muted with a dotted underline.
Diffs: added = #0f1a0e line background, green gutter mark and +,
word-level additions as a 2px green underline (no fill, so text contrast is
untouched); removed = #2b0e0d background, red gutter mark and −, word-level
removals underlined and struck through; modified = #1f1a0c background, amber
gutter mark and ~; conflicts = a double #f73f35 border. Textual markers and
existing status icons are always preserved.
Code and terminal
Editor surfaces use code.bg (#0c0909), code.current-line (#1c0a09),
code.line-number (text.subtle, replacing the site’s #7d1310), caret
#e99499, selection #420f0c (muted text stays 4.71:1 inside it), search
matches #630f0d with the current match inverted (#ffa2a7 on #0c0909),
bracket match as a 1px #e53935 border, indent guides #531310, rendered
whitespace #7d1310 (decorative). Syntax in default is monochrome:
keywords #f73f35, strings #bd787d, comments #ad7175 italic, numbers and
constants #d4868b, functions #ffa2a7, variables and operators #e99499,
types #b37175, properties and headings #e53935, tags #f73f35,
attributes and escapes #d4868b; invalid text takes a wavy underline,
deprecated text a line-through. Languages inherit these roles; a
language-specific override needs a documented reason.
Terminal roles carry the sixteen slots exactly in every profile. Slots
1, 4, 6, 8, 9, 10, 12, 13, 14 and 15 do not reach 4.5:1 on the terminal
background (slot 15, #a3676b, measures 4.45:1; slot 0 is the background
itself); the theme documents this rather than normalising it, because
programs, not the theme, choose which slot means what. Inside an editor
selection (code.selection-bg, #420f0c) muted text stays at 4.71:1 but the
keyword, type, property and comment roles fall below 4.5:1; a selection is a
transient state and the limitation is recorded, not hidden. The extended overlay
proposes a semantic sixteen-slot palette that all reaches 4.5:1.
Links use strong red text.link (#f73f35) and a persistent underline, distinct
from the rose body and heading ladder (D-024). Hover uses near-white
text.link-hover on interaction.hover.bg (D-027; the strong fill read louder
than the link itself and stays with menu items). Current-page and selected
navigation use their explicit indicator and on-fill roles.
Typography
One family: SauceCodePro NFM (Source Code Pro through the Nerd Fonts build),
falling back to Source Code Pro, Cascadia Mono, Consolas, Liberation Mono,
DejaVu Sans Mono and monospace. The site records size-adjust metrics for
the fallbacks; no font file is distributed. Weights 400 and 700 plus italic.
Scale (size/line-height in px): ui-sm 12/16, ui-md 13/18, ui-lg 14/20,
reading 15/24, code 13/19, terminal 13/19 with −0.5px letter-spacing,
h1 20/28 bold, h2 16/24 bold, h3 13/18 bold in text.accent, caption
11/16 only with text.muted or lighter. Hosts keep the user’s chosen size;
tiny UI text is a reference-implementation metric, not a rule.
Spacing and density (D-009)
Spacing steps: 0, 1, 2, 4, 8, 12, 16, 24, 32, 48 px. Two density modes applied
with data-density on any subtree: compact (control 24px, row 28px, ui-sm
type, 8px horizontal padding, 2px gaps) matches the workstation; comfortable
(control 32px, row 36px, ui-md type, 12px padding, 4px gaps) suits everyday
applications. Both meet the 24×24 CSS-pixel target minimum; compact requires
4px between adjacent targets. Table rows are exempt and always take the
comfortable row height: a data row is content rather than chrome, and
compacting it costs scannability where it is needed most. Controls inside a
row still follow the page density. The site’s 3px i3 gap, 14/−2 window gaps and
28px bar are window-manager geometry, not theme tokens.
Elevation and layering
There are no shadows except shadow.floating (0 6px 22px rgb(0 0 0 / 55%))
on the i3 floating-window specimen. Floating layers are a 1px border.overlay
on surface.raised; modals add surface.backdrop (rgb(0 0 0 / 65%)).
Stacking: canvas 0 < raised 1 < popover 10 < drawer 20 < dialog 30 < toast 40 <
skip link 1000.
A popup opened by hovering follows the pointer: it sits 8px from the cursor, flips to the other side of it rather than crossing a viewport edge, keeps an 8px margin from every edge, and is repositioned on pointer move, scroll and resize. A popup with nothing to operate takes no pointer events, so it cannot stand between the cursor and what opened it; one that carries a control or a link stays reachable and holds while the pointer is inside it. Either closes when the pointer leaves both the trigger and the popup.
The same popup opened from the keyboard anchors to its trigger instead — below it, or above when there is no room below — because focus has no pointer to follow. This is a placement rule only: what opens a popup, and whether it is a tooltip, a preview or an inspector, is the component’s own contract.
Icons
SVG line icons on a 16px grid, 1.5px stroke, stroke: currentColor,
fill: none, sizes 12 / 16 / 20. No icon font is required; Nerd Font glyphs
appear only in terminal and heritage specimens. Icon-only controls carry
aria-label and a tooltip. icon.decorative (#a3676b) is for glyphs that
convey nothing on their own.
Layout
Reading measure 72ch, content column 680px, application layout 1440px. Breakpoints 320 (reflow floor), 600, 900, 1280. Components use container queries; sidebars collapse to drawers below 900px; data tables scroll horizontally with a sticky first column below 600px; the page never scrolls horizontally at 320px. Right-to-left mirrors breadcrumb separators, pagination, sidebar and drawer sides, wizard progression, affixes and tree indentation.
Accessibility
Contrast targets, focus and keyboard contracts, reduced motion, forced colours, and what an automated pass does and does not prove.
What is claimed
WCAG 2.2 criteria are used as measurable design targets for the roles this theme defines. A green contrast report and a clean automated scan of the site are evidence that the specified colours and the reference markup meet those targets. They are not a conformance claim for any application that adopts the theme: keyboard operation, focus management, labels, reflow and manual review remain the consumer’s responsibility, and the theme’s own limitations are listed at the end of this document.
Contrast
- Ordinary text: at least 4.5:1 against the actual background of the state it is in, measured on resolved sRGB values after compositing translucent colours.
- Large text (18.66px bold or 24px regular and above): at least 3:1.
- Control boundaries, focus rings, meaningful icons and chart marks: at least 3:1 (WCAG 1.4.11).
- Disabled text is exempt under WCAG 1.4.3 but must still reach 3:1 by house
policy (
text.disabled#8a5559, 3.52:1 on the true-black canvas). - Never round a value into a pass. The report keeps the unrounded ratio and
displays two decimals;
4.4996reads 4.50 and fails. - Heritage values that fail (
#a3676bas text,#7d1310anywhere as text) are flagged$deprecatedin theheritage-ansiprofile and are never presented as ordinary passes.
exports/contrast.md lists every declared pair: the global pairs in
spec/contrast.json and every stateTokens and contrast[] entry of every
component. Waived pairs carry their reason; a waiver is only valid for
decorative graphics that are never the sole boundary of a control.
Focus
Focus is never lost and never invisible. Controls and rows show
focus.ring (1px dashed #e53935, offset −2px); focusable containers show
focus.ring-container (2px solid #ffa2a7, offset −3px). On fills, use the
component’s explicit ring role (D-015): primary and checked controls use
focus.ring-container where declared; destructive fills use their on-fill
text role. Colour mapping does not change ring geometry.
:focus without :focus-visible draws nothing,
which keeps pointer clicks quiet without hiding keyboard focus. A component
that removes a host’s default indicator must draw this one. State matrices on
the site render every declared combination including selected+focus-visible
and invalid+focus-visible, because those are where rings disappear in
practice.
Keyboard
Native controls keep native keyboard behaviour. Custom widgets follow the WAI
ARIA Authoring Practices pattern cited in their frontmatter (aria.apg):
roving tabindex in toolbars, tabs, trees and listboxes; aria-activedescendant
in comboboxes; focus trapping and Escape in dialogs and drawers; arrow keys
in radio groups and menus. Positive tabindex is banned. Every icon-only
control has an accessible name. The site’s own controls (search, filters,
inspector, copy) are reachable and operable with the keyboard, and the no-JS
build keeps every normative section readable.
Motion
Durations are at most 150ms and apply to opacity, transform and background
only. Under prefers-reduced-motion: reduce every duration is 0.01ms: toasts,
dialogs and drawers appear in place, indeterminate progress shows static
stripes, loading glyphs do not spin, skeletons do not shimmer, tab indicators
do not slide. No behaviour is sequenced on transitionend.
The specification page’s generated literal-color previews have the narrow geometry, checkerboard and motion exception recorded in D-014; the component and consumer rules remain as written.
Forced colours and high contrast
Under forced-colors: active rings use Highlight, selections use
Highlight / HighlightText, borders survive (they are real borders, not
box-shadows), and pattern fills keep charts and diffs legible.
Reflow, zoom and text
Components reflow at 320 CSS pixels without horizontal page scrolling, at 200%
browser zoom and at 200% text-only zoom. Rings and borders stay 1px / 2px CSS.
Targets are at least 24×24 CSS pixels with 4px spacing in compact density.
Long labels wrap or truncate with a visible affordance; the site’s stress
fixtures (English, Spanish, Chinese, Arabic with dir="rtl") exercise this.
Colour is never the only channel
Every status has a glyph; every state has a border, pattern or ARIA change;
diffs keep + − ~ markers; charts carry pattern fills and a table fallback.
The heritage-ansi terminal palette has reduced hue differentiation by
design, and the specification documents it as a limitation rather than fixing
it silently.
Limitations (recorded, not hidden)
- The theme specifies colours, states and reference markup. It cannot make a host application’s own focus management, labels or reading order correct.
- The automated scan on the site uses axe-core with the WCAG 2.x A and AA rule sets. It does not evaluate every success criterion, and manual checks (keyboard walks, screen-reader reading, zoom) require separate execution evidence. Missing protocols remain not run, not implied by the scan.
text.disabledat 3.52:1 on canvas is below the ordinary-text floor by design and relies on the WCAG exemption for inactive controls.- Heritage ANSI slots 1, 4, 6, 8, 9, 10, 12, 13, 14 and 15 fail 4.5:1 on the
terminal background (slot 0 is the background). Programs that render errors
or directories in those slots inherit that failure; the
extendedoverlay proposes a corrected set. - Syntax roles inside an editor selection: keyword, type, property and comment
drop below 4.5:1 on
code.selection-bg. Selection is transient; the limitation is recorded in the code-editor specification.
Verification evidence and freshness
The coverage ledger describes source facts: specified, demonstrated and test
implemented. exports/coverage.json schemaVersion 1 adds testImplemented and
retains tested as a compatibility alias with its original file-existence
meaning. Component JSON coverage and family totals use the same definitions.
No source coverage Boolean proves execution or blanket WCAG conformance.
The linked static verification report supplies actual per-test outcomes and per-component summaries. Evidence schemaVersion 1 records automated or manual method, category, component, explicitly covered states/variants, browser/version, OS, viewport, profile, density, JavaScript setting and a run or protocol reference. Empty state/variant lists mean unspecified, never all. The environment’s profile and density describe the initial configuration; tests that switch them say so in their scope. Browser records from one engine never imply another engine. Skipped, failed, not run and not applicable are separate outcomes; every non-pass has a reason. Manual screen-reader and keyboard claims require protocol records. The existing automated suite targets Chromium; other engines remain not run.
Freshness compares both the tested artifact digest and a conservative source fingerprint covering tracked contract sources, schemas, generators, site code, tests, agent guidance, CI configuration and package lockfile. All such changes invalidate the current claim; unrelated source changes may therefore invalidate more evidence than strictly necessary. A stale record retains its actual outcome and supporting reference but does not establish a current pass.
A build first publishes a static not-run report and a subject manifest. The
browser reporter checks that source and specimen assets still match that subject
before publishing execution evidence. Report generation writes only under
dist/verification/ and verifies all specimen bytes remain unchanged. Report
presentation gets a separate static/browser gate and never certifies itself.
Run timestamps and revisions live only in this post-test evidence, outside the
committed deterministic exports and their digests, avoiding circular hashes.
Local workflow: build, run the browser suite, npm run verification:report,
npm run verification:check, then npm run test:verification. A local run names
its local execution and digests; it is not represented as hosted CI evidence.
For a manual protocol, provide a schema-valid evidence document to
node scripts/verification-report.mjs <repo-relative-evidence.json> with its
actual tested digests, browser/environment and a protocol: reference containing
the protocol identifier and recorded observations. Retain the protocol with the
evidence; do not copy private artifacts into this public repository.