Terminal

The projectious.work terminal palette and the configuration that applies it to tmux, WezTerm, Kitty, iTerm2, and Zellij.

This page is the source of truth for rendering the projectious.work brand in a terminal. It answers which sixteen colours the brand uses, which program owns each part of the result, and how to prove a configuration is conformant.

The terminal is where the brand’s rule that code surfaces stay dark in both modes stops being a stylistic choice and becomes the only option: a terminal has one surface, it is dark, and everything is measured against it.

Implementation contract

What owns what

A terminal’s appearance is produced by two programs that do not overlap, and almost every “my theme is wrong” report is a confusion between them.

LayerOwnsExamples
EmulatorThe sixteen ANSI colours, background, foreground, cursor, selection, its own tabs and splitsWezTerm, Kitty, iTerm2, Windows Terminal, GNOME Terminal
MultiplexerOnly its own chrome — status bar, pane borders, message line, copy mode, popupstmux, Zellij, screen

A multiplexer cannot fix a wrong ANSI palette, and an emulator cannot style a status bar. Configure the emulator first; a multiplexer theme applied over an unbranded emulator will look wrong no matter how carefully it is written.

Inputs and ownership

NeedCanonical inputDo not do this
The sixteen coloursThe ANSI table on this pageImport a third-party scheme and rename it
Surface, cursor, selectionThe chrome table on this pageLet the emulator keep its default background
Monospace faceIBM Plex MonoSubstitute a different mono face for the brand
Multiplexer chromeThe tmux / Zellij sections belowStyle a status bar in colours not on this page

Non-negotiable rules

  1. The background is midnight-dark-1 (#0e1720). The terminal has one surface and it does not follow a light mode.
  2. Every one of the fourteen non-background colours clears 4.5:1 against that surface. The measured floor for this palette is 4.95:1.
  3. orange-9 (#e05232) is the cursor and the active marker. It is not a text colour in the palette, and it is never ANSI red — an error and the brand accent must not look alike.
  4. Use IBM Plex Mono. Enable its ligatures only if the team has agreed to them; they change how operators read in diffs.
  5. Dim text uses the dedicated comment token (#72889d), not ANSI bright black. Bright black is a border value here and fails text contrast on purpose.
  6. Do not rely on colour alone for state. A red segment needs a word or glyph beside it.

The palette

Sixteen colours

The palette is defined in the Colour foundations and rendered here from the same source, so this page cannot state a value the foundations do not.

#NameNormalOn surfaceBrightOn surfaceProvenance
0black #0e1720 #2e4b682.00:1midnight-dark-1 / midnight-dark-6 — Box drawing and rules, not text — deliberately below the floor.
1red #e55b5b5.15:1 #f08b807.49:1bright = danger-dark — Never the accent — an error and the brand must not look alike.
2green #3f9d745.41:1 #6cc0908.24:1bright = success-dark
3yellow #c08a1e5.93:1 #e0a92a8.50:1bright = warning-dark — Gold, matching the warning role in the interface.
4blue #6289b34.95:1 #8aacc87.59:1bright = midnight-dark-11
5magenta #bd6d964.98:1 #d491b47.32:1terminal-only — The brand defines no magenta; this slot exists only here.
6cyan #3f97a35.31:1 #74c0c98.71:1terminal-only — The brand defines no cyan; this slot exists only here.
7white #97a8b87.41:1 #c5daf012.62:1slate-dark-11 / midnight-dark-12

The bright ramp is the brand: where a hue already exists in the system, the bright slot takes that step verbatim. The normal ramp is derived from it — darkened until it reads a step back while still clearing the floor — and magenta and cyan exist in neither half of the brand. A normal-ramp value is a terminal value only: #e55b5b is the terminal’s red, not $danger (#a8261c).

Bright black is the one deliberate exception to the 4.5:1 floor: programs use it for box drawing and rules, not for text. If a program uses it for prose that is the program’s bug, and the fix is to configure the program — not to lighten the palette until unreadable text becomes readable.

The brand accent has no ANSI slot, because it is not a semantic colour: it marks where you are, which the chrome table covers.

Chrome

RoleValueMeasured
Background #0e1720the surface
Foreground #c5daf012.62:1
Cursor #e052324.67:1
Cursor text #0e17204.67:1 on the cursor
Selection background #20354d
Selection text #c5daf08.74:1
Dim / comment #72889d4.93:1
Status bar surface #131e2b
Status bar text #c5daf011.74:1
Inactive tab and pane label #7b8da34.95:1
Active tab fill #e052324.67:1 with #0e1720 text
Active pane border #e052324.67:1
Inactive pane border #7b8da35.32:1

tmux

tmux styles its own chrome only. Everything inside a pane keeps the colours the emulator supplied, so configure the emulator first.

Required capability

tmux must be told the terminal supports true colour, or every hex value silently degrades to the nearest of 256 approximations — which is the usual cause of a status bar that is “nearly right”.

# ~/.tmux.conf
set -g default-terminal "tmux-256color"
set -ga terminal-overrides ",*256col*:Tc"
set -ga terminal-overrides ",xterm-256color:Tc"

Verify with tmux info | grep -i Tc, or print a truecolour ramp inside tmux and check for banding.

Theme

# ~/.tmux.conf — projectious.work

# Status bar
set -g status-style              "bg=#131e2b,fg=#c5daf0"
set -g status-left-length        32
set -g status-left               "#[bg=#e05232,fg=#0e1720,bold] #S #[bg=#131e2b,fg=#e05232]"
set -g status-right-length       80
set -g status-right              "#[fg=#7b8da3] %Y-%m-%d #[fg=#c5daf0]%H:%M "

# Windows — the active one is the single accent element in the bar
setw -g window-status-format         "#[fg=#7b8da3] #I #W "
setw -g window-status-current-format "#[bg=#e05232,fg=#0e1720,bold] #I #W "
setw -g window-status-activity-style "fg=#e0a92a"
setw -g window-status-bell-style     "fg=#e55b5b,bold"

# Panes
set -g pane-border-style        "fg=#7b8da3"
set -g pane-active-border-style "fg=#e05232"
set -g pane-border-status       top
set -g pane-border-format       " #{pane_index} #{pane_current_command} "

# Messages and the command prompt
set -g message-style       "bg=#131e2b,fg=#c5daf0"
set -g message-command-style "bg=#131e2b,fg=#e0a92a"

# Copy mode — selection matches the emulator's selection pair
setw -g mode-style "bg=#20354d,fg=#c5daf0"

# Popups (tmux 3.2+)
set -g popup-style        "bg=#131e2b,fg=#c5daf0"
set -g popup-border-style "fg=#e05232"

# Clock
setw -g clock-mode-colour "#8aacc8"

The active window is the only accent-filled element in the bar. Adding a second one — an accented session name and an accented window — removes the cue that tells you which window you are in.

Status-bar frameworks

Many tmux setups do not write set -g status-* lines directly. A status-bar framework — tmux-powerkit, catppuccin/tmux, dracula/tmux and others — takes over status-format and re-renders it on its own schedule, overwriting anything set before it loads.

That is fine, and it does not need working around. Supply the framework with brand values through whatever palette hook it documents, rather than fighting it with set -g lines it will discard. The shape differs per framework; the values do not. A framework that reads an associative array, for example:

declare -gA THEME_COLORS=(
    [background]="#0E1720"
    [statusbar-bg]="#131E2B"
    [statusbar-fg]="#C5DAF0"
    [session-bg]="#E05232"
    [session-fg]="#0E1720"
    [window-active-base]="#E05232"
    [window-inactive-base]="#7B8DA3"
    [pane-border-active]="#E05232"
    [pane-border-inactive]="#7B8DA3"
    [error-base]="#E55B5B"
    [warning-base]="#E0A92A"
    [info-base]="#8AACC8"
)

Every value maps to a row in the tables above. Where a framework offers a slot the brand has no name for, resolve it from the palette rather than inventing a colour: a “modified” or “prefix” state is a warning, a “copy mode” state is informational, and both already have values.

If a framework exposes no palette hook at all, load the set -g block above after it — in tmux, later wins — and record the ordering dependency in the config, because a framework upgrade can silently reintroduce its own colours.

WezTerm

WezTerm is configured in Lua, so the palette can be a table the rest of the config refers to.

-- ~/.config/wezterm/wezterm.lua
local wezterm = require("wezterm")

local pj = {
  bg        = "#0e1720",
  fg        = "#c5daf0",
  accent    = "#e05232",
  surface   = "#131e2b",
  selection = "#20354d",
  dim       = "#7b8da3",
}

return {
  font = wezterm.font_with_fallback({ "IBM Plex Mono", "Symbols Nerd Font Mono" }),
  font_size = 13.0,
  harfbuzz_features = { "calt=0", "liga=0" }, -- opt in to ligatures deliberately

  colors = {
    foreground    = pj.fg,
    background    = pj.bg,
    cursor_bg     = pj.accent,
    cursor_fg     = pj.bg,
    cursor_border = pj.accent,
    selection_bg  = pj.selection,
    selection_fg  = pj.fg,
    split         = pj.dim,

    ansi = {
      "#0e1720", "#e55b5b", "#3f9d74", "#c08a1e",
      "#6289b3", "#bd6d96", "#3f97a3", "#97a8b8",
    },
    brights = {
      "#2e4b68", "#f08b80", "#6cc090", "#e0a92a",
      "#8aacc8", "#d491b4", "#74c0c9", "#c5daf0",
    },

    tab_bar = {
      background = pj.surface,
      active_tab   = { bg_color = pj.accent,  fg_color = pj.bg,  intensity = "Bold" },
      inactive_tab = { bg_color = pj.surface, fg_color = pj.dim },
      inactive_tab_hover = { bg_color = pj.selection, fg_color = pj.fg },
      new_tab      = { bg_color = pj.surface, fg_color = pj.dim },
      new_tab_hover = { bg_color = pj.selection, fg_color = pj.fg },
    },
  },

  use_fancy_tab_bar = false,
  window_padding = { left = 12, right = 12, top = 8, bottom = 8 },
  inactive_pane_hsb = { saturation = 1.0, brightness = 1.0 },
}

Leave inactive_pane_hsb at 1.0. WezTerm’s default dims inactive panes, which silently pushes every measured value in them below the floor.

Kitty

# ~/.config/kitty/kitty.conf — projectious.work

font_family      IBM Plex Mono
font_size        13.0
disable_ligatures always

background       #0e1720
foreground       #c5daf0
cursor           #e05232
cursor_text_color #0e1720
selection_background #20354d
selection_foreground #c5daf0
url_color        #8aacc8

# Normal
color0  #0e1720
color1  #e55b5b
color2  #3f9d74
color3  #c08a1e
color4  #6289b3
color5  #bd6d96
color6  #3f97a3
color7  #97a8b8

# Bright
color8  #2e4b68
color9  #f08b80
color10 #6cc090
color11 #e0a92a
color12 #8aacc8
color13 #d491b4
color14 #74c0c9
color15 #c5daf0

# Tabs — the active tab is the single accent element
tab_bar_style          powerline
tab_powerline_style    slanted
active_tab_background  #e05232
active_tab_foreground  #0e1720
active_tab_font_style  bold
inactive_tab_background #131e2b
inactive_tab_foreground #7b8da3
tab_bar_background     #131e2b

# Splits
active_border_color    #e05232
inactive_border_color  #7b8da3
window_padding_width   6

# Marks and bells
mark1_foreground #0e1720
mark1_background #e0a92a
bell_border_color #e55b5b

Kitty applies background_opacity before contrast is measured. Any value below 1.0 puts whatever is behind the window into every ratio on this page, so the palette is no longer measurable. Keep it at 1.0 for work that has to meet the floor.

iTerm2

iTerm2 stores colours as a plist of floating-point components, so hand-editing is error-prone. Generate the scheme instead:

#!/usr/bin/env python3
"""Emit projectious.iterm2colors — import via Settings ▸ Profiles ▸ Colors ▸
Color Presets ▸ Import. Regenerate rather than hand-editing the plist."""

PALETTE = {
    "Ansi 0":  "#0e1720", "Ansi 8":  "#2e4b68",
    "Ansi 1":  "#e55b5b", "Ansi 9":  "#f08b80",
    "Ansi 2":  "#3f9d74", "Ansi 10": "#6cc090",
    "Ansi 3":  "#c08a1e", "Ansi 11": "#e0a92a",
    "Ansi 4":  "#6289b3", "Ansi 12": "#8aacc8",
    "Ansi 5":  "#bd6d96", "Ansi 13": "#d491b4",
    "Ansi 6":  "#3f97a3", "Ansi 14": "#74c0c9",
    "Ansi 7":  "#97a8b8", "Ansi 15": "#c5daf0",
    "Background": "#0e1720", "Foreground": "#c5daf0",
    "Bold":       "#c5daf0", "Cursor":     "#e05232",
    "Cursor Text": "#0e1720", "Link":      "#8aacc8",
    "Selection":  "#20354d", "Selected Text": "#c5daf0",
    "Badge":      "#e05232", "Tab":        "#131e2b",
}

def component(value):
    return f"<real>{int(value, 16) / 255:.10f}</real>"

rows = []
for name, hex_value in PALETTE.items():
    h = hex_value.lstrip("#")
    rows.append(
        f"\t<key>{name} Color</key>\n\t<dict>\n"
        f"\t\t<key>Color Space</key>\n\t\t<string>sRGB</string>\n"
        f"\t\t<key>Red Component</key>\n\t\t{component(h[0:2])}\n"
        f"\t\t<key>Green Component</key>\n\t\t{component(h[2:4])}\n"
        f"\t\t<key>Blue Component</key>\n\t\t{component(h[4:6])}\n"
        f"\t\t<key>Alpha Component</key>\n\t\t<real>1</real>\n\t</dict>"
    )

print('<?xml version="1.0" encoding="UTF-8"?>')
print('<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" '
      '"http://www.apple.com/DTDs/PropertyList-1.0.dtd">')
print('<plist version="1.0">\n<dict>')
print("\n".join(rows))
print("</dict>\n</plist>")
python3 make-iterm-scheme.py > projectious.itermcolors
open projectious.itermcolors     # registers it as a preset

Two iTerm2 defaults have to be turned off, both under Settings ▸ Profiles ▸ Colors:

SettingRequiredWhy
Minimum contrast0Non-zero silently rewrites foreground colours, so the measured palette is not what renders
Brighten bold textoffRepaints normal colours as brights and collapses the two halves of the palette
Smart cursor colouroffOverrides the accent cursor with a computed colour
Transparency / blur0Puts the desktop behind the window into every contrast ratio

Zellij

Zellij declares a named theme in KDL and paints its own UI from it.

// ~/.config/zellij/config.kdl
themes {
    projectious {
        fg      "#c5daf0"
        bg      "#0e1720"
        black   "#0e1720"
        red     "#e55b5b"
        green   "#3f9d74"
        yellow  "#c08a1e"
        blue    "#6289b3"
        magenta "#bd6d96"
        cyan    "#3f97a3"
        white   "#97a8b8"
        orange  "#e05232"
    }
}

theme "projectious"

pane_frames true
ui {
    pane_frames {
        rounded_corners true
        hide_session_name false
    }
}

Zellij’s orange slot is what it paints the active pane frame and the session name with, so it takes the brand accent. Its red/green/yellow slots drive mode indicators — locked, pane, resize — which is why they must be the semantic values and not decorative choices.

Zellij has no separate bright ramp: it derives emphasis from the ten names above. Programs running inside it still receive the emulator’s sixteen, so the bright half of the palette is not lost.

Verify

Palette

Print the sixteen slots and compare them against the table above:

for i in $(seq 0 15); do
  printf "\033[48;5;%dm  %3d  \033[0m" "$i" "$i"
  [ $(( (i + 1) % 8 )) -eq 0 ] && echo
done

Confirm true colour reaches the terminal — this must print a smooth ramp, not bands:

awk 'BEGIN{for(i=0;i<256;i++){printf "\033[48;2;%d;%d;%dm ", i, 90, 120}print "\033[0m"}'

States to review

A terminal theme that only renders a prompt is not complete.

StateWhat it proves
git diff with colourRed and green are distinguishable and neither is the accent
ls --color on a mixed directoryBlue, cyan and green separate at a glance
A failing command’s stderrRed reads as an error, not as brand emphasis
man or a pagerBold, underline, and dim all stay readable
Split panes, one activeThe active border is the only accent element
Copy / visual modeSelection contrast holds over both normal and coloured text
A full-screen TUI (htop, lazygit)Bright black is used for rules, not for text
256-colour and truecolour outputNo banding, no fallback to the nearest ANSI slot

Release evidence

  • the emulator and multiplexer versions, and the config files changed;
  • a screenshot of the sixteen-slot ramp beside the table on this page;
  • git diff, a failing command, and a split-pane view captured at the working font size;
  • confirmation that minimum-contrast, bold-brightening, and transparency settings are off;
  • for a multiplexer, evidence that true colour survives inside a session.

Sources and upgrade boundary

When a terminal is upgraded, re-print the sixteen-slot ramp before assuming the theme survived. Emulators change their default handling of bold, dim, and minimum contrast between releases more often than they change colour parsing, and each of those silently moves values off the measured palette.