This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Themes

Configuration of theme framworks for usage with the projectious.work brand system

This section contains guidance for configuring selected theming frameworks to use the projectious.work brand.

The Hugo guide combines the implementation contract, theme-specific guidance, and review fixtures in one page. It currently contains configurations for Docsy and Hextra.

The Terminal guide covers the sixteen-colour ANSI palette and the configuration that applies it to tmux, WezTerm, Kitty, iTerm2, and Zellij.

1 - Hugo

The projectious.work implementation contract and theme guidance for selected Hugo theme frameworks: Docsy and Hextra.

This page is the source of truth for rendering the projectious.work brand with Hugo. It answers which files supply the brand, where a theme may be changed, and how to prove the result is conformant.

Choose the Docsy or Hextra subsection after reading the shared contract. The examples use named values such as midnight-9 and accent-solid; resolve those names from the canonical token source when building a production theme.

Implementation Contract

This contract applies before choosing Docsy or Hextra. It gives a human implementer and an AI agent one unambiguous answer to the theme’s source of truth, customization boundary, and release evidence.

Inputs and ownership

NeedCanonical inputDo not do this
Colour valuesbrand/tokens/tokens.jsonCopy arbitrary hex values into templates
CSS-friendly tokensbrand/tokens/variables.cssCreate a second, competing token file
Logo and iconsbrand/logo/ supplied SVG or raster variantsRedraw, recolour, crop, or stretch a mark
Type rolesBrand typography guidanceSubstitute a decorative display face
Content hierarchyThis documentation content tree and front matterEncode navigation order in CSS
Theme behaviorThe selected theme’s current official documentationPatch vendored source as the first option

A value copied from a code example is an illustration, not a replacement for the source of truth.

File boundary

Keep the theme dependency and the branded site separate. This lets a theme upgrade remain a small, reviewable dependency change.

site/
├── hugo.yaml
├── assets/
│   ├── css/custom.css                    # Hextra adapter
│   └── scss/
│       ├── _variables_project.scss       # Docsy Bootstrap variables
│       ├── _variables_project_after_bs.scss
│       └── _styles_project.scss          # Docsy component overrides
├── layouts/_shortcodes/                  # Hextra: pj-callout only
├── static/
│   ├── images/                           # supplied logo variants only
│   └── favicons/                         # supplied favicon variants only
├── content/docs/                         # navigation and page content
└── themes/ or go.mod                     # pinned theme dependency

Do not place project branding under a vendored theme directory. Use only the documented extension point first; document any deliberate exception in the theme’s upgrade notes.

Non-negotiable visual rules

  1. Use Plus Jakarta Sans for headings, Source Sans 3 for reading copy, and IBM Plex Mono for code.
  2. Use accent-solid (#cc4528) for a single primary action per view. Its white label meets the normal-text contrast floor; identity orange does not automatically do so.
  3. Keep ordinary links blue. Accent colour is emphasis, not the default link colour.
  4. Keep code surfaces dark in both light and dark modes. Syntax tokens must be tested against that dark surface.
  5. Use quiet surfaces, token borders, and modest radii: 3px small, 6px normal, 9px large. Do not introduce gradients or oversized shadows.
  6. Support a user-controlled light/dark mode when the selected theme supports it. Test both modes independently; never make a dark mode by inverting a light screenshot.
  7. Make focus visible without depending on colour alone. The current-page marker, active navigation item, and alerts need a non-colour cue too.

The shared semantic layer

Both themes bind to one token set. A theme adapter may only map these names onto its own selectors; it may not redefine what a name means, and it may not introduce a colour that is not in this table. This is what makes a Docsy site and a Hextra site recognisably the same product, and it is the only reason the two dark modes can be reviewed against each other.

Every value below is a step from the canonical scales in brand/tokens/tokens.json. The ratio column is the measured contrast against the surface that role actually sits on.

TokenRoleLightDarkContrast (light / dark)
--pj-canvasApp background — page, navbar, sidebar, TOC#f8f9fb#0e1720step 1
--pj-surfaceCards, panels, inputs — elevated above the canvas#ffffff#131e2bstep 2 dark
--pj-surface-hoverHovered rows and menus#e2e9f2#1a2b3estep 3
--pj-textReading copy#142438#c5daf015.68 / 12.62
--pj-text-secondaryNavigation rest state, TOC, captions, metadata#5c6f82#97a8b85.18 / 7.41
--pj-headingH1–H6#1d3352#c5daf012.75 / 12.62
--pj-linkInline and navigation links#3a5a82#8aacc87.08 / 7.59
--pj-link-hoverLink emphasis only — accent as text, step 11#c04424#ea75584.87 / 6.18
--pj-nav-active-bgCurrent-page tint#d3deec#20354dstep 4
--pj-nav-active-textCurrent-page label#1d3352#c5daf010.42 / 8.74
--pj-borderDivider: cards, tables, navbar, section rules#cdd0d5#263f5a1.55 / 1.67
--pj-border-strongInputs and controls whose edge is the affordance#546a82#4d70985.58 / 3.52
--pj-actionPrimary action fill#cc4528#cc45284.72 with white
--pj-action-hoverPrimary action hover fill#b84228#b842285.46 with white
--pj-action-labelPrimary action label#ffffff#ffffff
--pj-focusKeyboard focus ring#cc4528#f098784.72 / 8.15
--pj-codeAlways-dark code surface#0e1720#0e1720
--pj-code-textCode foreground#c5daf0#c5daf012.62
--pj-code-chromeFilename bar and copy control on the code block#131e2b#131e2b
--pj-info-bg / --pj-info-borderInformational#e2e9f2 / #7490b2#1a2b3e / #4d70983.29 / 3.52 border
--pj-success-bg / --pj-success-borderCompleted#d1ebe0 / #2f7d65#18382d / #6cc0904.95 / 8.24 border
--pj-warning-bg / --pj-warning-borderNeeds attention#f5ecd0 / #8b6508#3d1e13 / #e0a92a5.30 / 8.50 border
--pj-danger-bg / --pj-danger-borderBlocked#f9e3e1 / #a8261c#3a1d20 / #f08b807.10 / 7.49 border

Four rules follow from the table and are not negotiable per theme:

Surfaces come off the ramp in step order, in both modes. The step roles are not decorative: step 1 is the app background, step 2 the subtle section band, steps 3–5 element fills, hover and active. Light mode used pure white as its canvas for a while, which reads fine on its own but leaves a card with nothing to lift off — a white panel on a white page separates by border alone, and the elevation scale has no room to work. Taking the canvas to step 1 puts white back where it is useful, as the raised surface.

The accent has a text step and a solid step, and they are different. Step 10 is “solid hover” — a surface role. Accent used as text, which in practice means a link hover, takes step 11, the scale’s low-emphasis text step. Solid accent fills keep step 10 with a white label. Using step 10 as text is the most common way this system gets bent: it passes on white at 4.72:1 and then fails the moment the surface underneath is anything but pure white.

There are two text tiers, not three. --pj-text and --pj-text-secondary are the only text colours. The scales define steps 11 and 12 as the text roles; a third “muted” tier has to come from step 9 or below, and no step below 11 clears 4.5:1 against the dark surfaces. If copy is not important enough for --pj-text-secondary, it is not important enough to ship.

The primary action is one colour in every mode and both themes. Fill --pj-action, hover --pj-action-hover, label always white. It does not lighten in dark mode and the label never changes colour, because every alternative either drops white below 4.5:1 or forces the label to invert on hover. Signal the dark-mode hover with the focus ring and elevation instead.

The warning border is gold, not the accent. #e05232 is the identity colour; using it as the warning edge makes every warning read as a brand flourish and makes the accent read as an error. Gold #8b6508 and #e0a92a carry the warning role in the two modes.

The header may be the one neutral surface. The default is a navbar that takes the canvas colour, so the reading surface runs unbroken from the top of the window. A site whose dark mode needs more separation may instead give the header the slate ramp — #20262c, 1.18:1 against the navy canvas — which leaves navy as the only saturated surface and lets the content read as the page rather than as another bar. The documentation site does this. Pick one and apply it in both themes; what is not allowed is a header in a third colour that is neither the canvas nor a documented surface step.

Dividers are quiet, and they are never the accent. A brand-coloured divider is a recurring request and the answer is no: the accent marks the one action a view is asking for, and a page whose every rule is orange has spent it. Orange is also poor as a line — 3.87:1 on white and 4.67:1 on midnight, loud enough to read as content and muddy at 1px. Where a divider needs more presence, raise its contrast and leave its weight at 1px; a 1px rule at 1.55:1 reads crisper than a 2px rule at 1.2:1 and stays out of the reading path. The accent may take one structural job per page — the documentation site uses a 40px accent lead-in on the section rule under each H2 — but never the rule itself.

--pj-border is a divider that groups content; it sits below 3:1 and that is correct, because the content it separates carries its own contrast. --pj-border-strong is for an input or control whose edge is the only thing announcing that it is interactive, and it clears 3:1 in both modes.

Required page states

Review these states with real content before approving the theme. A theme that only renders the home page is not complete.

StateWhat it proves
Long documentation pageReading measure, headings, TOC, tables, inline code
Page with a primary actionAccent contrast, hover, focus, disabled treatment
Code-heavy pageDark code surface, syntax, copy affordance, overflow
Nested navigationCurrent state, mobile menu, keyboard traversal
Search resultsQuery control, result hierarchy, empty state
Callouts and statusSemantic success, info, warning, danger states
Wide image and tableResponsive overflow and meaningful alternative text
Light and dark modePaired surfaces, logo/favicons, contrast, no flash

Release evidence

Attach or record the following for a theme change:

  • the pinned theme version and the official API/configuration pages consulted;
  • screenshots at 320px and 1280px in light and dark mode;
  • keyboard evidence for the navbar, sidebar, search, theme toggle, and a primary action;
  • automated build output and a manual check at 200% zoom;
  • a list of project override files, confirming that no vendored theme source was edited.

The Docsy and Hextra review fixtures below use comparable content states so visual regressions during a theme migration are easy to see.

Docsy

Open the Docsy example to inspect the documented patterns in a working site.

Docsy is Hugo’s Bootstrap-and-SCSS documentation theme. Its documented project SCSS files are the supported customization seam; this is where the brand belongs. Do not edit themes/docsy/.

What conformant looks like

Use a confident but quiet technical shell: a light navigation bar, a clear page title, white reading surfaces, and cards or callouts only where they help scanning. Reserve dark surfaces for code, the footer, and dark mode.

Heading hierarchy example

The page title Hugo contains H2 sections, such as Docsy, and each H2 can contain H3 subsections. H4 headings provide one further detail level. Hugo’s table of contents is configured to include headings through H4.

ElementRequired treatment
NavbarCanvas colour at 85% with blur and a hairline underline — no fill of its own
Sidebar and TOCCanvas colour, no panel fill; a hairline and whitespace do the separating
Footer--pj-footer, supplied logo, and readable light text
HeadingsPlus Jakarta Sans, 700–800; tight tracking above 20px
Body and navigationSource Sans 3, 16px / 1.65 for long-form content
CodeIBM Plex Mono on an always-dark surface
Primary actionOne accent-solid control, white label, 6px radius
Cards and callouts9px radius, token border, restrained shadow; semantic colours only
Light and darkdata-bs-theme palettes tested independently; never invert a screenshot

Install and configure the theme

Use the current Docsy installation method selected by the site (Hugo module, submodule, or checked-in theme). Keep the version pinned and upgrade it in its own pull request. The Docsy content guide documents the docs content type and its generated nested side navigation.

For a site rooted at src/, this is the relevant shape of hugo.yaml:

theme: [docsy]
enableGitInfo: true

markup:
  goldmark:
    renderer:
      unsafe: true # only when reviewed Hugo/HTML examples require it
  highlight:
    noClasses: false

params:
  ui:
    navbar_logo: true
    navbar_theme: light
    showLightDarkModeMenu: true
    sidebar_menu_compact: true
    sidebar_menu_foldable: true
    sidebar_search_disable: false
  copyright:
    authors: "Projectious"
    from_year: 2026

menu:
  main:
    - name: Documentation
      url: /docs/
      weight: 10

Keep pages under content/docs/. A section _index.md creates a sidebar group; descendant page weight values create a deliberate order. The following is enough to make a new group and pages appear as a nested Docsy sidebar menu:

content/docs/
└── platform/
    ├── _index.md       # title: Platform; weight: 40
    ├── deployment.md   # title: Deployment; weight: 10
    └── operations.md   # title: Operations; weight: 20

For a complete local kitchen-sink implementation, see the Docsy example. It is built manually with the scripts in examples/hugo-docsy/ and deployed beneath the GitHub Pages site.

Use title, linkTitle, description, and weight in front matter. A description also feeds document summaries and search metadata. Internal links use relref, so a changed base URL cannot silently break them.

Map the brand into Bootstrap and Docsy

Docsy reads project styles from assets/scss/ before theme styles. Place the following files in the site, not in the theme:

src/assets/scss/
├── _variables_project.scss
├── _variables_project_after_bs.scss
└── _styles_project.scss

Start with Bootstrap’s semantic roles, rather than assigning a brand colour to every individual selector.

// assets/scss/_variables_project.scss
$primary: #1d3352;   // midnight-9: headings, stable navigation
$secondary: #546a82; // slate-9: secondary UI
$success: #2f7d65;
$info: #3a5a82;
$warning: #8b6508;
$danger: #a8261c;

// slate-7 is a hairline, not a control edge: it measures 2.13:1 on white and
// leaves an input with no perceivable boundary. Use the strong border step.
$input-border-color: #546a82;

$font-family-sans-serif: "Source Sans 3", system-ui, sans-serif;
$font-family-monospace: "IBM Plex Mono", ui-monospace, monospace;
$headings-font-family: "Plus Jakarta Sans", system-ui, sans-serif;
$headings-font-weight: 700;
$body-line-height: 1.65;
$border-radius-sm: 3px;
$border-radius: 6px;
$border-radius-lg: 9px;
$enable-gradients: false;

Use the post-Bootstrap map file only when a named semantic role is genuinely missing:

// assets/scss/_variables_project_after_bs.scss
$projectious-colors: (
  "accent-solid": #cc4528,
  "surface": #f8f9fb,
);
$theme-colors: map-merge($theme-colors, $projectious-colors);

Then put type and shape rules in _styles_project.scss. Keep colour out of this part of the file — all of it belongs in the single colour layer below, so there is one place to look when a value is wrong.

// assets/scss/_styles_project.scss
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Source+Sans+3:wght@400;500;600&display=swap");

h1, h2, h3, h4, h5, h6 { letter-spacing: -0.3px; }
code, pre { font-family: "IBM Plex Mono", monospace; }
.btn {
  border-radius: 6px;
  font-family: "Plus Jakarta Sans", sans-serif;
  font-weight: 600;
}
pre, .highlight { border-radius: 9px; }

How Docsy binds to the shared layer

Docsy’s colour work happens in two places: Bootstrap’s semantic variables, which Docsy and Bootstrap components read on their own, and a small custom property layer for the surfaces Bootstrap has no variable for. Do not restate hex values in component rules — bind to the token and let the mode switch do the work.

Shared tokenDocsy / Bootstrap binding
--pj-canvas--bs-body-bg; also .td-navbar, .td-sidebar, .td-toc
--pj-surface--bs-secondary-bg; .card, form controls
--pj-surface-hover.td-sidebar-link:hover, .dropdown-item:hover, .table-hover
--pj-text--bs-body-color
--pj-text-secondary--bs-secondary-color; sidebar and TOC rest state
--pj-heading--bs-heading-color
--pj-link / --pj-link-hover--bs-link-color / --bs-link-hover-color
--pj-nav-active-bg / --pj-nav-active-text.td-sidebar-link.active, .td-toc a.active
--pj-border--bs-border-color, .td-navbar bottom edge
--pj-border-strong$input-border-color, .form-control
--pj-action / --pj-action-hover.btn-primary
--pj-info-*--pj-danger-*.alert-info.alert-danger

Bootstrap’s $warning is the gold from the shared layer, not the accent. Docsy’s [data-bs-theme] attribute is the mode switch; the same token names resolve differently under it, so no component rule needs a dark-mode variant.

Complete Docsy color layer

Add this to assets/scss/_styles_project.scss, after the Bootstrap variables in assets/scss/_variables_project.scss. It is the whole surface map: the custom properties, the Bootstrap variables they drive, and the component rules that Bootstrap does not cover.

// assets/scss/_styles_project.scss

:root,
[data-bs-theme="light"] {
  --pj-canvas: #f8f9fb;
  --pj-surface: #ffffff;
  --pj-surface-hover: #e2e9f2;
  --pj-text: #142438;
  --pj-text-secondary: #5c6f82;
  --pj-heading: #1d3352;
  --pj-link: #3a5a82;
  --pj-link-hover: #c04424;
  --pj-nav-active-bg: #d3deec;
  --pj-nav-active-text: #1d3352;
  --pj-border: #cdd0d5;
  --pj-border-strong: #546a82;
  --pj-focus: #cc4528;
  --pj-info-bg: #e2e9f2;      --pj-info-border: #7490b2;
  --pj-success-bg: #d1ebe0;   --pj-success-border: #2f7d65;
  --pj-warning-bg: #f5ecd0;   --pj-warning-border: #8b6508;
  --pj-danger-bg: #f9e3e1;    --pj-danger-border: #a8261c;
  --pj-navbar: rgba(248, 249, 251, 0.85);
  --pj-footer: #f0f3f8;
}

[data-bs-theme="dark"] {
  --pj-canvas: #0e1720;
  --pj-surface: #131e2b;
  --pj-surface-hover: #1a2b3e;
  --pj-text: #c5daf0;
  --pj-text-secondary: #97a8b8;
  --pj-heading: #c5daf0;
  --pj-link: #8aacc8;
  --pj-link-hover: #ea7558;
  --pj-nav-active-bg: #20354d;
  --pj-nav-active-text: #c5daf0;
  --pj-border: #263f5a;
  --pj-border-strong: #4d7098;
  --pj-focus: #f09878;
  --pj-info-bg: #1a2b3e;      --pj-info-border: #4d7098;
  --pj-success-bg: #18382d;   --pj-success-border: #6cc090;
  --pj-warning-bg: #3d1e13;   --pj-warning-border: #e0a92a;
  --pj-danger-bg: #3a1d20;    --pj-danger-border: #f08b80;
  --pj-navbar: rgba(14, 23, 32, 0.85);
  --pj-footer: #131e2b;
}

// Mode-independent. The code surface is dark in both modes by design.
:root {
  --pj-action: #cc4528;
  --pj-action-hover: #b84228;
  --pj-action-label: #ffffff;
  --pj-code: #0e1720;
  --pj-code-text: #c5daf0;
  --pj-code-chrome: #131e2b;
}

// Hand the tokens to Bootstrap so its own components follow without extra rules.
:root, [data-bs-theme="light"], [data-bs-theme="dark"] {
  --bs-body-bg: var(--pj-canvas);
  --bs-body-color: var(--pj-text);
  --bs-secondary-color: var(--pj-text-secondary);
  --bs-secondary-bg: var(--pj-surface);
  --bs-heading-color: var(--pj-heading);
  --bs-link-color: var(--pj-link);
  --bs-link-hover-color: var(--pj-link-hover);
  --bs-border-color: var(--pj-border);
  --bs-code-color: var(--pj-link); // Bootstrap's default is a pink outside the brand
}

// Bootstrap 5.3 composes most colours through `*-rgb` triplets rather than the
// hex variables — `a { color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity)) }`.
// Setting only `--bs-link-color` leaves every link on the theme's default. The
// triplets cannot reference a var(), so they are restated per mode.
:root, [data-bs-theme="light"] {
  --bs-body-color-rgb: 20, 36, 56;
  --bs-link-color-rgb: 58, 90, 130;
  --bs-link-hover-color-rgb: 204, 69, 40;
  --bs-emphasis-color-rgb: 20, 36, 56;
}
[data-bs-theme="dark"] {
  --bs-body-color-rgb: 197, 218, 240;
  --bs-link-color-rgb: 138, 172, 200;
  --bs-link-hover-color-rgb: 234, 117, 88;
  --bs-emphasis-color-rgb: 197, 218, 240;
}

// Docsy generates a per-block link colour for the landing blocks and applies it
// at `.td-box--x p > a`, which outranks a plain `.td-box--x a`. It also dims the
// active table-of-contents entry with a (0,3,1) selector. Match the shape of
// each rather than writing a weaker selector and reaching for !important.
.td-box--light p > a, .td-box--light span > a,
.td-box--white p > a, .td-box--white span > a { color: var(--pj-link); }
.td-toc #TableOfContents a.active { color: var(--pj-nav-active-text); font-weight: 600; }

// blocks/cover carries Docsy's `td-overlay--dark` scrim, which exists to darken
// a background photograph. With no cover image it only washes the surface grey.
.td-cover-block.td-overlay::after { background-color: transparent; }

// Shell. Navbar, sidebar and TOC share the canvas; borders and space separate
// them. No panel fills — the reading column is the only thing with weight.
.td-navbar {
  background: var(--pj-navbar);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--pj-border);
}
.td-sidebar,
.td-toc { background: var(--pj-canvas); border-color: var(--pj-border); }
.td-footer { background: var(--pj-footer); color: #c5daf0; }
.td-footer a { color: #8aacc8; }

// Navigation: quiet at rest, tinted pill when current, and a left marker so
// the current item is not signalled by colour alone.
.td-sidebar-link,
.td-toc a { color: var(--pj-text-secondary); }
.td-sidebar-link:hover,
.td-toc a:hover { background: var(--pj-surface-hover); color: var(--pj-heading); }
.td-sidebar-link.active,
.td-toc a.active {
  background: var(--pj-nav-active-bg);
  color: var(--pj-nav-active-text);
  font-weight: 600;
  border-radius: 6px;
  box-shadow: inset 2px 0 0 var(--pj-action);
}

.card { background: var(--pj-surface); border-color: var(--pj-border); border-radius: 9px; }
.table { --bs-table-border-color: var(--pj-border); }

.form-control, .form-select, .td-search__input {
  background: var(--pj-surface);
  border-color: var(--pj-border-strong);
  color: var(--pj-text);
}

.btn-primary {
  background: var(--pj-action);
  border-color: var(--pj-action);
  color: var(--pj-action-label);
}
.btn-primary:hover,
.btn-primary:focus-visible,
.btn-primary:active {
  background: var(--pj-action-hover);
  border-color: var(--pj-action-hover);
  color: var(--pj-action-label);
}

// The ring sits outside the control, so it never lands on the accent fill.
:focus-visible { outline: 2px solid var(--pj-focus); outline-offset: 2px; }

pre, .highlight, .td-content pre {
  background: var(--pj-code);
  color: var(--pj-code-text);
  border-radius: 9px;
}
.highlight .filename,
.td-click-to-copy { background: var(--pj-code-chrome); color: #97a8b8; }

.alert { border-left-width: 3px; color: var(--pj-text); }
.alert-info    { background: var(--pj-info-bg);    border-color: var(--pj-info-border); }
.alert-success { background: var(--pj-success-bg); border-color: var(--pj-success-border); }
.alert-warning { background: var(--pj-warning-bg); border-color: var(--pj-warning-border); }
.alert-danger  { background: var(--pj-danger-bg);  border-color: var(--pj-danger-border); }

Two details are easy to miss. The alert rule sets color explicitly: without it, a Bootstrap alert keeps its own tinted foreground and the dark-mode tints are unreadable. And the code block’s filename bar needs --pj-code-chrome, because the surface underneath it is dark in light mode too — a theme that styles only pre leaves dark text on a dark bar.

Docsy exposes a light/dark menu through params.ui.showLightDarkModeMenu. Do not disable it for a branded site: add the dark values above and test them.

Brand every visible theme element

AreaConfigureReview
Logostatic/ assets and params.ui.navbar_logoCorrect lockup, clear space, no CSS recolour
Navbarparams.ui.navbar_theme, project SCSSLight/translucent shell, compact height, accessible menu trigger
Sidebarcontent tree, weight, compact/foldable paramsStable order, current-page state, keyboard traversal
TOCheading hierarchy and tableOfContentsH2–H4 only; no skipped heading levels
SearchofflineSearch and sidebar search paramsSearch is discoverable and has readable results
Alertssemantic Bootstrap rolesWarning is gold, never the identity accent; alert text is --pj-text, not a tinted foreground
Tablesproject SCSS and Markdown tablesHorizontal overflow stays inside its container
Imagespage bundles and Hugo image processingAlt text, attribution, dark-mode treatment, no baked text
Footerparams.links, copyright, project partialsProject links, current year, dark-surface text contrast
CodeHugo Chroma plus project CSSAlways dark, copy affordance if enabled, AA token colours

Page-part example

Use ordinary Docsy content and one primary action. This produces a direct, task-oriented block without turning documentation into a marketing page.

---
title: Deploy a provider
linkTitle: Deploy
description: Validate a provider configuration and promote it safely.
weight: 10
---

<div class="alert alert-info" role="alert"><div class="h4 alert-heading" role="heading">Before you begin</div>


Run the validation command locally before requesting promotion.
</div>


## Validate the configuration

Use `brand/tokens/tokens.json` as the input. Do not duplicate token values.

<a class="btn btn-primary" href="#deploy">Deploy configuration</a>

Review specimen

This is the Docsy review fixture: a content-led page that exercises hierarchy, link behavior, callouts, cards, code, status, and one primary action. The visual direction takes modular discovery cues from Kubeflow without copying its identity, assets, or page composition.

Platform operations

Ship a reliable integration

Use this specimen to inspect the actual Docsy shell, type scale, hierarchy, link behavior, callouts, cards, code treatment, and primary action in one place. The page must remain readable before any interaction is available.

Review release check

What the reader needs first

AreaStateOwner
ConfigurationValidatedPlatform team
DocumentationReviewedTechnical writer
ReleaseWaiting for approvalRelease manager

The table should remain inside a scrollable container on narrow screens. Link text remains blue, such as the token reference, while the accent-filled control remains reserved for the next action.

A card-led decision surface

Validate

Check tokens, configuration, and internal links.

Read validation notes

Review

Confirm accessibility in light and dark modes.

Read review criteria

Promote

Create a release only from verified content.

Read release check

Cards are scanning aids, not decoration. Their links remain links rather than becoming three competing primary actions.

Code remains a dark working surface

params:
  ui:
    navbar_theme: light
    showLightDarkModeMenu: true
  offlineSearch: true

Inline code such as brand/tokens/tokens.json must stay distinct from its paragraph without looking like a second button. The block above stays dark in both theme modes, with readable syntax and a discoverable copy control.

Status language is semantic

Release check

At 320px, check the menu, table overflow, cards, action, code block, and TOC. At 1280px, check reading measure, fixed navigation, sidebar hierarchy, and the balance between the content column and its table of contents. Repeat each check in light and dark modes with keyboard navigation and 200% zoom.

Accessibility and release gate

In addition to the shared contract, test Docsy’s menu, sidebar, search, light / dark control, code copy affordance, and Bootstrap callouts at 320px and 1280px. Do not let a project SCSS override remove focus indicators or semantic headings.

Sources and upgrade boundary

When updating Docsy, diff the generated output and re-check every override in assets/scss/; theme-internal SCSS is not a stable public API.

Hextra

Open the Hextra example to inspect the documented patterns in a working site.

Hextra is a Hugo documentation theme with a compact shell, generated side navigation, and a supported custom-CSS entry point. Use its quiet, task-focused reading model while retaining projectious.work typography, colour, assets, and interaction rules.

What conformant looks like

The reading column should stay calm: clear heading hierarchy, an unobtrusive sidebar, a useful table of contents, and one action only when the task calls for it. Avoid a copied product-marketing treatment, excessive card shadows, or a logo that competes with the page title.

ElementRequired treatment
Shellwide page frame with a focused reading column; light, translucent navbar in light mode
TypographyPlus Jakarta Sans headings; Source Sans 3 content; IBM Plex Mono code
Accent #cc4528 only for the primary action or active emphasis; links remain blue
Surfaces White canvas, cards, sidebar and TOC; #f0f3f8 hover only; #0e1720 dark-mode canvas
NavigationContent-tree sidebar, current item distinct without colour alone
Dark modesystem default and a visible toggle; paired light/dark logo and favicon
Footer and searchQuiet supporting UI, local/project-controlled assets where required

Install and configure the theme

Pin the Hextra module or checked-in theme version. Keep the installation and theme upgrade separate from brand overrides. Hextra reads its site settings from hugo.yaml; its configuration guide documents navigation, sidebar, favicon, theme, search, and page settings.

module:
  imports:
    - path: github.com/imfing/hextra

enableGitInfo: true
markup:
  goldmark:
    renderer:
      unsafe: true # only for reviewed component examples
  highlight:
    noClasses: false

params:
  theme:
    default: system
    displayToggle: true
  navbar:
    displayTitle: true
    displayLogo: true
    width: wide
    logo:
      path: images/logo.svg
      dark: images/logo-dark.svg
      link: /
      width: 160
      height: 32
  page:
    width: wide
    displayPagination: true
  footer:
    width: wide
  search:
    enable: true
    type: flexsearch
    flexsearch:
      index: content
      tokenize: forward
  editURL:
    enable: true
    base: https://github.com/projectious-work/brand/edit/development/src/content

menu:
  main:
    - name: Documentation
      pageRef: /docs
      weight: 10
    - name: Search
      weight: 20
      params:
        type: search
    - name: Theme
      weight: 30
      params:
        type: theme-toggle
    - name: GitHub
      url: https://github.com/projectious-work/brand
      weight: 40
      params:
        icon: github

Hextra generates the main sidebar from the content directory. Create a folder with an _index.md to make a group, then set front-matter weights. Exclude utility pages with sidebar.exclude: true; hide a sidebar only when a page needs the additional reading width.

content/docs/
└── platform/
    ├── _index.md       # title: Platform; weight: 40
    ├── deployment.md   # title: Deployment; weight: 10
    └── operations.md   # title: Operations; weight: 20

For a complete local kitchen-sink implementation, see the Hextra example. It is built manually with the scripts in examples/hugo-hextra/ and deployed beneath the GitHub Pages site.

Load the brand through custom CSS

Hextra automatically loads assets/css/custom.css. Use it as the single brand adapter. The Hextra customization guide documents its primary HSL variables, layout variables, and public component classes. The guidance below targets Hextra v0.10.0; pin that version, and re-check the four class names it depends on after any upgrade.

Hextra derives a ten-step --color-primary-* ramp from three HSL variables, and uses that ramp for the active sidebar item, the code-block tint, and several focus states. Two things about it decide whether a brand adapter works:

Set the primary variables under html.dark as well as :root. Hextra’s own stylesheet re-declares all three under .dark. An adapter that sets them only on :root gets brand colours in light mode and stock Hextra blue (204deg 100% 50%) in dark mode — on the current sidebar item, which is the most visible navigation state on the page.

Do not let the derived ramp reach the code block. Hextra tints code with bg-primary-700/5, which is a light wash. The brand keeps code dark in both modes, so the adapter overrides the pre inside .hextra-code-block — not the wrapper, which sits behind that tint and has no visible effect.

/* assets/css/custom.css */
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Source+Sans+3:wght@400;500;600&display=swap");

/* midnight-9 #1d3352 expressed as HSL, so Hextra's derived ramp stays in the
   brand's hue rather than its default azure. */
:root {
  --primary-hue: 214deg;
  --primary-saturation: 47%;
  --primary-lightness: 22%;
  --hextra-max-page-width: 90rem;
  --hextra-max-navbar-width: 90rem;
  --hextra-max-footer-width: 90rem;
}

/* Required: Hextra re-declares these under .dark and would otherwise revert. */
html.dark {
  --primary-hue: 214deg;
  --primary-saturation: 47%;
  --primary-lightness: 62%;
}

@layer theme {
  :root {
    --hx-default-font-family: "Source Sans 3", system-ui, sans-serif;
    --hx-default-mono-font-family: "IBM Plex Mono", ui-monospace, monospace;
  }
}

The dark lightness is raised to 62% because Hextra reads primary-lightness as the mid of the ramp and derives dark-mode text roles above it. At the light value of 22% the dark sidebar’s active label resolves near-black on a dark tint.

How Hextra binds to the shared layer

Shared tokenHextra binding
--pj-canvasbody, .hextra-nav-container, .hextra-sidebar-container, .hextra-toc
--pj-surface.hextra-card, .hextra-feature-card, .search-input
--pj-surface-hoversidebar and TOC link hover, .hextra-card:hover
--pj-textbody, .content
--pj-text-secondarysidebar and TOC rest state, .hextra-toc headings
--pj-heading.content h1h4
--pj-link / --pj-link-hover.content a
--pj-nav-active-bg / --pj-nav-active-text.hextra-sidebar-active-item, .hextra-toc-active
--pj-border.hextra-nav-container bottom edge, cards, tables
--pj-border-strong.search-input, form controls
--pj-action / --pj-action-hover.pj-primary-action
--pj-code / --pj-code-chrome.hextra-code-block pre, .hextra-code-block .hextra-code-filename
--pj-info-*--pj-danger-*.pj-callout-* (project shortcode — see below)

Hextra marks the current sidebar entry with the hextra-sidebar-active-item class and the current TOC entry with hextra-toc-active. It does not emit aria-current, so an adapter written against a[aria-current="page"] silently styles nothing.

Complete Hextra color layer

Append this to the same assets/css/custom.css. The token block is byte-for-byte the Docsy one; only the selectors below it differ. Keep it that way — if the two adapters ever need different values, the shared layer is wrong, not the theme.

/* assets/css/custom.css — continued */

:root {
  --pj-canvas: #f8f9fb;
  --pj-surface: #ffffff;
  --pj-surface-hover: #e2e9f2;
  --pj-text: #142438;
  --pj-text-secondary: #5c6f82;
  --pj-heading: #1d3352;
  --pj-link: #3a5a82;
  --pj-link-hover: #c04424;
  --pj-nav-active-bg: #d3deec;
  --pj-nav-active-text: #1d3352;
  --pj-border: #cdd0d5;
  --pj-border-strong: #546a82;
  --pj-focus: #cc4528;
  --pj-info-bg: #e2e9f2;      --pj-info-border: #7490b2;
  --pj-success-bg: #d1ebe0;   --pj-success-border: #2f7d65;
  --pj-warning-bg: #f5ecd0;   --pj-warning-border: #8b6508;
  --pj-danger-bg: #f9e3e1;    --pj-danger-border: #a8261c;
  --pj-navbar: rgba(248, 249, 251, 0.85);
  --pj-footer: #f0f3f8;

  /* Mode-independent. */
  --pj-action: #cc4528;
  --pj-action-hover: #b84228;
  --pj-action-label: #ffffff;
  --pj-code: #0e1720;
  --pj-code-text: #c5daf0;
  --pj-code-chrome: #131e2b;
}

html.dark {
  --pj-canvas: #0e1720;
  --pj-surface: #131e2b;
  --pj-surface-hover: #1a2b3e;
  --pj-text: #c5daf0;
  --pj-text-secondary: #97a8b8;
  --pj-heading: #c5daf0;
  --pj-link: #8aacc8;
  --pj-link-hover: #ea7558;
  --pj-nav-active-bg: #20354d;
  --pj-nav-active-text: #c5daf0;
  --pj-border: #263f5a;
  --pj-border-strong: #4d7098;
  --pj-focus: #f09878;
  --pj-info-bg: #1a2b3e;      --pj-info-border: #4d7098;
  --pj-success-bg: #18382d;   --pj-success-border: #6cc090;
  --pj-warning-bg: #3d1e13;   --pj-warning-border: #e0a92a;
  --pj-danger-bg: #3a1d20;    --pj-danger-border: #f08b80;
  --pj-navbar: rgba(14, 23, 32, 0.85);
  --pj-footer: #131e2b;
}

/* Shell. Navbar, sidebar and TOC all sit on the canvas; a hairline and
   whitespace do the separating. */
body { background: var(--pj-canvas); color: var(--pj-text); }
.hextra-nav-container {
  background: var(--pj-navbar);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--pj-border);
}
.hextra-sidebar-container,
.hextra-toc { background: var(--pj-canvas); }
.hextra-footer { background: var(--pj-footer); color: #c5daf0; }
.hextra-footer a { color: #8aacc8; }

.content h1, .content h2, .content h3, .content h4 {
  color: var(--pj-heading);
  font-family: "Plus Jakarta Sans", system-ui, sans-serif;
  letter-spacing: -0.3px;
}
.content, .content p, .content li { color: var(--pj-text); }
.content a { color: var(--pj-link); }
.content a:hover, .content a:focus-visible { color: var(--pj-link-hover); }

/* Navigation. Quiet at rest, tinted pill when current, plus a left marker so
   the current item is not signalled by colour alone. */
.hextra-sidebar-container a,
.hextra-toc a { color: var(--pj-text-secondary); }
.hextra-sidebar-container a:hover,
.hextra-toc a:hover { background: var(--pj-surface-hover); color: var(--pj-heading); }
.hextra-sidebar-active-item,
.hextra-toc a.hextra-toc-active {
  background: var(--pj-nav-active-bg) !important;
  color: var(--pj-nav-active-text) !important;
  font-weight: 600;
  border-radius: 6px;
  box-shadow: inset 2px 0 0 var(--pj-action);
}

.hextra-card, .hextra-feature-card {
  background: var(--pj-surface);
  border-color: var(--pj-border);
  border-radius: 9px;
}
.hextra-card:hover { background: var(--pj-surface-hover); }

input, textarea, select, .search-input {
  background: var(--pj-surface);
  border-color: var(--pj-border-strong);
  color: var(--pj-text);
}

.pj-primary-action {
  display: inline-flex;
  align-items: center;
  min-height: 40px;
  padding: 0 16px;
  border-radius: 6px;
  background: var(--pj-action);
  border: 1px solid var(--pj-action);
  color: var(--pj-action-label);
  font-family: "Plus Jakarta Sans", sans-serif;
  font-weight: 600;
  text-decoration: none;
}
.pj-primary-action:hover,
.pj-primary-action:focus-visible {
  background: var(--pj-action-hover);
  border-color: var(--pj-action-hover);
  color: var(--pj-action-label);
}

:focus-visible { outline: 2px solid var(--pj-focus); outline-offset: 2px; }

/* Code stays dark in both modes. Target the `pre`, not the wrapper: Hextra
   tints the inner element and the wrapper's background never shows. */
.hextra-code-block pre,
.content pre {
  background: var(--pj-code) !important;
  color: var(--pj-code-text);
  border-radius: 9px;
}
.hextra-code-block .hextra-code-filename {
  background: var(--pj-code-chrome) !important;
  color: #97a8b8 !important;
}

/* See the note below: Hextra's own callout cannot be rebound from CSS. */
.pj-callout {
  margin: 24px 0;
  padding: 12px 16px;
  border-left: 3px solid;
  border-radius: 0 6px 6px 0;
  color: var(--pj-text);
}
.pj-callout-title {
  margin: 0 0 4px;
  font-family: "Plus Jakarta Sans", sans-serif;
  font-weight: 700;
  font-size: 0.9375rem;
}
.pj-callout-info    { background: var(--pj-info-bg);    border-color: var(--pj-info-border); }
.pj-callout-success { background: var(--pj-success-bg); border-color: var(--pj-success-border); }
.pj-callout-warning { background: var(--pj-warning-bg); border-color: var(--pj-warning-border); }
.pj-callout-danger  { background: var(--pj-danger-bg);  border-color: var(--pj-danger-border); }

Hextra’s built-in callout shortcode styles itself entirely with Tailwind utility classes and emits no stable class hook, so its semantic tints cannot be rebound from custom.css — a .hextra-callout-* rule matches nothing. The brand callout is therefore a small project shortcode in layouts/_shortcodes/pj-callout.html that emits the class names above; see examples/hugo-hextra/ for the implementation. Give each type a text label as well as a colour, so the state never depends on colour alone.

Two further theme behaviours have to be answered, or the result fails contrast in light mode specifically:

Pin the syntax palette to the code surface. Hextra ships separate light and dark Chroma stylesheets and selects between them by mode. The brand keeps code dark in both modes, so in light mode the theme paints its light palette — navy keywords, near-black identifiers — onto a midnight surface at roughly 1.1:1. The brand syntax theme has to be restated for both modes, and at sufficient specificity: Hextra’s rules are .highlight .chroma .xx, and its dark variant .dark .highlight .chroma .xx, so a two-class selector loses no matter what order the stylesheets load in.

Restate link colour on components that carry their own. Hextra colours every link inside .content, and .content a outranks a single component class. A primary action and any specimen containing links will otherwise have their labels repainted blue — including white-on-accent labels, which drops them to 1.5:1.

.content a.pj-primary-action,
.content a.pj-primary-action:hover { color: var(--pj-action-label); }

The !important declarations are deliberate and limited to three places where Hextra applies a Tailwind utility directly in its markup — the active navigation item, the code surface, and the filename bar. A utility class in the template always outranks a stylesheet selector, so this is the documented extension point behaving as designed, not an override of theme internals.

Brand every visible theme element

AreaConfigureReview
Logoparams.navbar.logo, files in static/images/Light/dark lockups, intrinsic dimensions, clear space
Faviconstatic/favicon.svg and static/favicon-dark.svgCorrect asset in each colour mode
Navbarparams.navbar, menu.main, custom CSSProduct title, search, theme control, mobile trigger
Sidebarcontent tree, front matter, menu.sidebarWeight order, current marker, no duplicate utility links
TOCheading structure, toc: false only when neededAccurate H2–H4 outline, useful at desktop width
Searchparams.searchClear input, keyboard use, relevant result excerpts
Cards and gridsHextra shortcodes and public CSS classes9px radius, restrained shadow, semantic content grouping
Code and copyChroma plus .hextra-code-blockAlways dark, readable syntax, visible copy result
Imagespage bundles, imageZoom only when helpfulDescriptive alt text, no text baked into screenshots
Footeri18n/en.yaml and optional footer partialCopyright, project links, dark text contrast
Scriptslocal assets/ paths where practicalNo surprise third-party runtime or unreviewed tracking

Page-part and review fixture

Use the same content states as the Docsy review specimen above so theme migrations compare like with like. Hextra expresses the callout, cards, and action through its documented shortcodes and one project CSS adapter:

---
title: Ship a reliable integration
description: A task-focused theme review fixture.
weight: 10
---

{{< callout type="info" >}}
Validate the configuration locally, then request promotion.
{{< /callout >}}

<a class="pj-primary-action" href="#release-check">Review release check</a>

## What the reader needs first

| Area | State | Owner |
|---|---|---|
| Configuration | Validated | Platform team |
| Documentation | Reviewed | Technical writer |
| Release | Waiting for approval | Release manager |

{{< cards >}}
  {{< card link="#validate" title="Validate"
      subtitle="Check tokens, configuration, and internal links" >}}
  {{< card link="#review" title="Review"
      subtitle="Confirm accessibility in light and dark modes" >}}
  {{< card link="#release-check" title="Promote"
      subtitle="Create a release from verified content" >}}
{{< /cards >}}

## Code remains a dark working surface

```yaml
params:
  theme:
    default: system
    displayToggle: true
```

## Release check {#release-check}

Check 320px and 1280px widths, keyboard focus, 200% zoom, and both modes.

Intended shell anatomy

┌─────────────────────────────────────────────────────────────────────┐
│ projectious.work      Documentation  Search  Theme                  │
├───────────────┬───────────────────────────────────────┬─────────────┤
│ Foundations   │ Platform operations                    │ On this page│
│ Themes        │ Ship a reliable integration            │ What reader │
│   Hugo        │ One concise explanation.               │ Cards       │
│               │ [Review release check]                 │ Code        │
│               │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ Status      │
│               │ │ Validate │ │ Review   │ │ Promote  │ │             │
│               │ └──────────┘ └──────────┘ └──────────┘ │             │
└───────────────┴───────────────────────────────────────┴─────────────┘

This is an anatomy diagram, not a prescribed pixel layout. Keep Hextra’s native responsive behavior. At smaller widths, the sidebar and TOC become available through the theme’s controls; the reading order stays header, title, explanation, action, and content.

Hextra acceptance cues

ComponentExpected branded result
NavbarCompact and dark-aware; supplied logo, search, and theme toggle
Reading columnCalm, readable Source Sans 3 body copy with Plus Jakarta Sans headings
ActionOne 40px-minimum accent-solid button with white text and visible focus
CardsQuiet border, 9px radius, subtle elevation, blue text links
CodeIBM Plex Mono on #0e1720 in both colour modes
Dark mode #0e1720 base, #131e2b raised surface, paired assets and contrast

Do not add a custom page template merely to achieve this fixture. Hextra’s content tree, documented cards and callout shortcodes, and one project CSS adapter should be sufficient. Use a layout override only when documented configuration and public component classes cannot meet an explicit product requirement; record the maintenance cost alongside the override.

Accessibility and release gate

In addition to the shared contract, test Hextra’s sidebar, TOC, search, theme toggle, cards, callouts, logo/favicons, code contrast, and custom action at 320px and 1280px. Do not let a custom CSS override remove focus indicators, semantic headings, or text selection.

Sources and upgrade boundary

When upgrading Hextra, compare the generated shell before and after the change. Limit overrides to documented settings and public classes; a copied theme template becomes the project’s maintenance burden.

2 - 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.