michael

a grayscale theme for grayscale-forced displays — terminal first

Install Try the inspector Browse the gallery

The inspector highlights any public GitHub file in michael — no install needed.

Most themes assume color. On a screen forced to grayscale — desktop monitor or phone — color themes collapse: their token distinctions become whatever lightness accidents the original hues had. michael is authored grayscale from the start. Meaning is carried by lightness × weight × style × overlay, never by hue.

Every value is derived from measurable acceptance gates, and the theme is vision-evaluated against Solarized and Flexoki converted to grayscale the same way your display converts everything else.

The ramp

Ten perceptual steps in OKLCH lightness, off-polar endpoints (never pure black/white), five foreground levels per polarity, tuned so adjacent levels stay ≥ 0.10 OKLCH apart — distinguishable even after aggressive quantization.

#070707
#161616
strong
fg
muted
faint
strong
select
bg-alt
paper

The grammar

With no hue, each channel carries exactly one kind of meaning — so any token maps by answering three binary questions instead of "pick a gray":

channelvaluesmeaning
levelink / strong / fg / muted / faintimportance
weightregular / boldbinding — definitions, keywords
stylenone / italickind — comments, parameters, metadata
underlinesolid / wavy / dotteddiagnostics only: error / warning / info
overlayfill / outline / inverseselection, brace match, current search

In the terminal, bold-is-bright doubles the weight channel as a brightness escape hatch across all 16 ANSI slots.

Live code — not an image

The renders below are PNGs from our eval pipeline. This section is actual highlighted text using our generated highlight.js theme — the same grammar, the same values, live in your browser. Toggle the variant:

The generated CSS ships as site/assets/michael-hljs.css and out/highlightjs/michael-{light,dark}.css — drop-in themes for any highlight.js-based blog or docs site.

Showcase

michael dark — Python
dark — keywords bold ink, types ink, definitions bold-strong, strings muted, comments faint italic, punctuation faint
michael light — Python
light — same grammar, polarity-tuned values
michael dark — terminal session
terminal session — git-status trio (modified/deleted/staged) on three distinct grays, dirs and executables pop via bold-is-bright, grep matches hit ink
michael light — terminal session
terminal session, light

Measured, not vibes

Every hex value must pass acceptance gates before it ships:

gatethreshold
body text contrast≥ 7:1 (WCAG AAA)
strong / ink levels≥ 10:1
muted level≥ 4.5:1
de-emphasized level≥ 3:1
adjacent level spacing≥ 0.10 OKLCH ΔL
text on selection≥ 4.5:1
16-gray quantizationall states survive (e-ink floor)

Against the baselines

Solarized and Flexoki are luminance-preserving grayscaled — exactly what a grayscale-forced display does to them — then graded blind by a vision model on rendered sessions (median of repeated grades, per-class diagnostics).

themeterminal separation (0–5)readability (0–5)
michael dark3.04.0
flexoki dark2.04.0
solarized dark2.03.0
michael light2.02.0
flexoki light1.02.0
solarized light2.03.0

n = 6 images per theme with median-of-3 grading. Full methodology, per-class diagnostics and the experiment log live in the repository (corpus/eval-report.md, notes/eval-experiments.org).

What gets lost when a display forces grayscale

Solarized's keyword blue and type yellow, its function cyan and constant orange — on a grayscale-forced screen those hues are destroyed, and every token lands on whatever gray its lightness happens to be. Flip the toggle on the Solarized panel to watch it happen. michael (right) never had a color story to lose: its hierarchy is the gray story, by construction.

Solarized dark
Solarized dark — original colorset (7 hues carrying the semantics)
 
michael dark at 16 grays
michael dark — authored gray: five levels plus weight/style, gate-checked

Install

Installing needs nothing but Python 3 — the generators are stdlib-only. With no arguments it installs every target at once (GNOME Terminal, Doom Emacs, VS Code, Ghostty); pass the targets you want to install a subset:

git clone https://github.com/charly-vibes/michael.git
cd michael
./install.sh
targetactivate
GNOME TerminalPreferences → Profiles → michael light/dark
Doom Emacs(custom-set-variables '(doom-theme 'doom-michael-dark))
VS CodePreferences: Color Theme → michael light/dark
Ghosttytheme = light:michael-light,dark:michael-dark
Windows Terminalmerge michael-schemes.json, set colorScheme

Pick targets with ./install.sh gnome ghostty windows. The eval pipeline (renders, gates, vision eval) additionally uses uv — optional unless you want to hack on the theme.

Gallery

All renders — six languages plus a console session, light and dark, full fidelity and 4-bit, across michael and both baselines — are browsable in the gallery — or paste any public GitHub blob URL into the inspector to see it highlighted live. Filter by language and theme, flip quantization to see the e-ink floor test, and switch render: color originals to see the authentic Solarized and Flexoki colorsets your display destroys.

Links

Repository · Gallery (every render) · Inspector (live-highlight any GitHub file) · ramp.json · tokens.json · eval report