Documentation
The projectious.work brand and design system — foundations, logo, interface patterns, collateral, and usage terms.
This documentation is rendered by the design system it describes. Every colour
chip, type row, and code block on this site is produced from the same tokens the
pages document.
Where to start
| If you want to… | Read |
|---|
| Use the palette correctly, including dark mode | Colour |
| Set text in the brand’s voice | Typography |
| Place the logo without breaking it | Logo usage |
| Build an interface | Interface |
| Produce a deck, signature, or social card | Collateral |
| Know what you are allowed to reuse | Governance |
| Pull the raw values into a project | Tokens |
How the system is organised
Foundations are the primitives — colour, type, spacing, shape, motion.
Everything else composes from them.
Logo covers the mark, the four lockups, and the rules that keep them legible.
Interface applies the foundations to screens: components, code surfaces,
dark mode, icons, and forms.
Collateral covers the artefacts that leave the product — decks, cards,
signatures, social imagery.
Governance covers licensing, trademark, the pre-launch legal assessment, and
asset provenance.
Source of truth
The canonical design documents live in brand/
in the repository — the HTML design-system documents, logo files, document
templates, and the token exports. This site is the readable presentation of
those sources, not a replacement for them.
1 - Foundations
The primitives — colour, typography, spacing, shape, and motion. Everything else composes from these.
Foundations are the values the rest of the system is built from. They are
deliberately small in number and fixed: a component may choose which token to
use, but it does not invent new ones.
1.1 - Colour
Three 12-step scales in two modes, their step roles, and the contrast rules that govern them.
The palette is three scales — midnight, orange, and slate — each
expressed as a 12-step ramp in a light and a dark variant. The step numbering
follows the Radix convention, which assigns every step a role. Using a step
outside its role is the single most common way to break the system.
Core colours
These are the named aliases most projects reach for first. They are shortcuts
into the scales, not a separate palette.
Primary
#1d3352
Text, headings, primary surfaces
Primary Light
#2b4d78
Hover states, dark-mode primary
Primary Dark
#132440
Dark backgrounds, navbar, code blocks
Accent
#E05232
CTAs, highlights, active states
Accent Light
#ea7558
Accent hover on dark, syntax strings
Accent Dark
#b84228
Accent pressed state
Accent Solid
#cc4528
Fill for solid controls with white text (4.72:1)
Secondary
#546a82
Supporting text, borders
Step roles
Every scale uses the same twelve roles in the same order:
| Steps | Role | Used for |
|---|
| 1–2 | App and subtle backgrounds | Page and section surfaces |
| 3–5 | Element backgrounds | Component fills, hover, active |
| 6–8 | Borders | Subtle, default, and strong borders |
| 9–10 | Solid | Solid fills and their hover state |
| 11–12 | Text | Low-emphasis and high-emphasis text |
Only 11 and 12 are text steps
Steps 8, 9, and 10 are border and solid-surface roles. They are not held to text
contrast thresholds and must not be used for body text. If you need dimmer text
than step 11, define a dedicated token and verify its contrast — see
the code-comment token for a worked
example.
Midnight
Midnight
light · step 9 #1d3352
Midnight
dark · step 9 #1d3352
Midnight is the primary. It carries structure, text, and the calm end of the
system. Step 9 (#1d3352) is the brand primary and is identical in both modes.
Orange
Orange
light · step 9 #E05232
Orange
dark · step 9 #E05232
Orange is the accent. It marks the primary action, the active state, and little
else. Step 9 (#E05232) is constant across modes.
Do
Use orange for the single most important action in a view, active navigation
state, and focus emphasis.
Don't
Use orange for large background fills, body text, or more than one competing
call to action on the same screen.
Slate
Slate
light · step 9 #546a82
Slate
dark · step 9 #546a82
Slate is the secondary — supporting text, borders, and neutral surfaces. Step 9
(#546a82) is the brand secondary and is constant across modes.
Terminal
The three scales cover every surface that has a light mode. A terminal does not:
it has one surface, it is dark, and every colour in it is measured against that
one background. It also needs six hues where the interface needs three, because
programs have been writing to sixteen ANSI slots since long before this system
existed.
So the terminal palette is a fourth member of the system rather than a fifth
scale — a fixed sixteen-slot palette plus its chrome, derived from the ramps and
measured against midnight-dark-1.
Read the provenance column carefully. The bright ramp is the brand: where a
hue already exists in the system, the bright slot takes that step verbatim. The
normal ramp has no brand equivalent — the scales define one value per
semantic role, not a dim and a bright — so each normal slot is its bright
counterpart darkened until it reads a step back while still clearing the floor.
Magenta and cyan exist in neither half of the brand. They are here because a
terminal requires them, and nowhere else.
A terminal value is not a brand value
#e55b5b is the terminal’s red. It is not $danger, which is #a8261c.
The normal ramp exists to fill ANSI slots and is measured only against the
terminal surface; using one of its values in the interface puts an unmeasured
colour on an unrelated background.
Terminal chrome
The accent gets no ANSI slot, because it is not semantic — it marks where you
are. That, and the surfaces around the sixteen, live here.
Every non-background value clears 4.5:1 against the surface; the measured floor
is 4.95:1. ANSI 0 bright is the one deliberate exception — programs use it for
box drawing and rules, not for text.
Configuration for tmux, WezTerm, Kitty, Ghostty, iTerm2, and Zellij is on the
Terminal theming page.
Contrast rules
- Never use pure
#000 or #fff as text. Use step 12 of the relevant
scale: #142438 on light, #c5daf0 on dark. - Step 9 is constant across modes. The solid accent does not shift when the
theme changes.
- Body text targets 4.5:1, large text (≥24px, or ≥18.66px bold) targets 3:1.
- Verify against the actual surface. A step that passes on the app
background may fail on an elevated panel.
Where an identity colour cannot carry text
Being the brand colour does not make a value a legible background. White on
orange-9 (#E05232) measures 3.87:1 — fine as a mark or a border, but
below the floor for button labels. Rather than dilute the accent, the system
adds a separate fill for that job:
| Token | Hex | With white text |
|---|
--color-accent | #E05232 | 3.87:1 — identity only, not for text |
--color-accent-solid | #cc4528 | 4.72:1 — solid controls |
--color-accent-dark | #b84228 | 5.46:1 — hover and pressed |
The same principle produced the
code-comment token: when no existing
step can do the job accessibly, name a new one rather than misuse a step.
Semantic colours are mode-specific
The callout hues are tuned for dark text on tinted light backgrounds. Used
as foregrounds on the dark app surface they fall below AA, so dark mode has its
own set:
| Role | Light | Dark | On #0e1720 |
|---|
| Success | #2f7d65 | #6cc090 | 3.65:1 → 8.24:1 |
| Warning | #8b6508 | #e0a92a | 3.41:1 → 8.50:1 |
| Danger | #a8261c | #f08b80 | 2.55:1 → 7.49:1 |
| Info | #3a5a82 | #8aacc8 | 2.55:1 → 7.59:1 |
Data visualisation
A chart palette introduces no new colours. It is a set of rules for which
existing steps may sit beside each other in a plot, and — more usefully — for
when colour stops being the right tool.
Categorical: three series
| Series | Token | Hex | On white |
|---|
| 1 | --midnight-9 | #1d3352 | 12.75:1 |
| 2 | --orange-9 | #E05232 | 3.87:1 |
| 3 | --slate-9 | #546a82 | 5.58:1 |
One step-9 solid per family, in that order. All three clear the 3:1 non-text
contrast floor against a white plot area, so a bar or a line is visible
without a border.
Assign them in order and keep the assignment stable across every chart in a
deck or a dashboard: if midnight is “cloud” on slide four, it is “cloud” on
slide nine. A series that changes colour between charts costs the reader more
than a fourth series would have gained them.
Three-series grouped bars — the whole categorical palette
Orange and slate differ in hue, not in value
orange-9 against slate-9 measures 1.44:1. On screen they are easy to
tell apart — orange against blue-grey is also one of the safest pairs for the
common colour-vision deficiencies. Printed in greyscale, or on a projector with
the colour turned down, they merge.
So when exactly two series are being compared, use midnight-9 and orange-9
(3.29:1) and leave slate for the third. And direct-label every series — the
legend is the fallback, not the mechanism.
There is no fourth series
The palette stops at three, and extending it is the wrong fix. The step-6 tier
is not an option: midnight-6 and slate-6 measure 1.03:1 against each
other — the same colour, for practical purposes — and all three step-6 values
sit at 1.8–2.0:1 against white, below the 3:1 floor for a mark you have to see.
When a chart has more than three categories, one of these is the answer:
- Group the tail. Rank the categories and collapse everything past the third
into “Other”. If the fourth is genuinely interesting, it is the subject of its
own chart.
- Small multiples. One chart per category, same axes, same scale. Reading
eight small charts is faster than decoding an eight-colour legend.
- Direct labelling with one highlight. Draw every series in
slate-7, draw
the one being discussed in orange-9, and label it in place. This is the
house style for a line chart in a
deck — one idea per slide holds
for charts too. - Stop using colour. A ranked bar chart in a single colour, sorted by value,
answers “which is biggest” better than any palette does.
Sequential and ordinal scales
A magnitude scale uses one family, steps 3 through 8:
--midnight-3 → --midnight-4 → --midnight-5 → --midnight-6 →
--midnight-7 → --midnight-8
Six levels, stepping evenly in luminance (each 1.06–1.40× its neighbour) — which
is what makes the ramp readable as an ordered scale rather than as six colours.
Step 9 is not the top of that ramp. It is 3.87× darker than step 8, which is
a jump the eye reads as a category boundary rather than one more level. Use it
deliberately for exactly that: a four-bucket choropleth of 3 · 5 · 7 · 9,
where the top bucket is meant to separate itself. Do not append it to a
six-level heatmap.
Steps 3–7 are all below 3:1 against a white plot area, so a sequential fill
needs an edge: give the plot a 1px slate-4 cell grid, or the reader loses the
boundary between a light cell and the page.
For a diverging scale — where the middle is neutral and both ends are extreme —
run midnight-8 → midnight-3 → orange-3 → orange-8, with --midnight-1 at the
midpoint. Never build a diverging scale from success and danger: those hues
carry a judgement, and “below average” is not “wrong”.
Chart furniture
| Element | Value |
|---|
| Axis line, ticks | --slate-5 |
| Grid lines | --slate-3, horizontal only |
| Axis labels, legend | --slate-11, 12px |
| Value labels | --midnight-12, 12px, IBM Plex Mono |
| Plot background | none — the page surface |
| Annotation, callout rule | --orange-9 |
Numbers are set in IBM Plex Mono, right-aligned, for the same reason
table numerics are: digits have
to line up to be compared.
Do
Keep categorical charts to three series and label them directly. Hold a series'
colour constant across a deck. Use one family’s steps 3–8 for magnitude.
Don't
Invent a fourth categorical colour from the step-6 tier, rely on a legend as the
only way to identify a series, append step 9 to a sequential ramp, or build a
diverging scale from the success and danger hues.
Dark mode
Both modes are equally supported. See
Dark mode for the implementation
rules — theme switching, persistence, image treatment, and the always-dark code
surface.
1.2 - Typography
Three typefaces, three jobs, and a fixed type ramp.
The system uses three typefaces, each with one job. All three are licensed under
the SIL Open Font License 1.1 and served from Google Fonts, so there are no
bundled font binaries to license or redistribute.
The three families
| Family | Weights | Role | Licence |
|---|
| Plus Jakarta Sans | 400 · 500 · 600 · 700 · 800 | Headings, display, buttons, navigation, wordmark | SIL OFL 1.1 |
| Source Sans 3 | 400 · 500 · 600 | Body copy, UI labels, captions | SIL OFL 1.1 |
| IBM Plex Mono | 400 · 500 | Code, terminal output, data | SIL OFL 1.1 |
Loading them:
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Source+Sans+3:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap">
The ramp
Every row below is set in the family, size, weight, and line-height it
documents — this is the live specimen, not a picture of one.
Display · 48/800/1.1
Redesigning work
H1 · 36/700/1.15
Agent-first consulting
H2 · 28/700/1.2
Composable infrastructure
H3 · 22/600/1.25
How the system fits together
H4 · 18/600/1.3
Working in the open
H5 · 15/600/1.35
Supporting detail
Body L · 16/400/1.65
Augmenting people's strengths through composable Cloud, Agile, and Agentic AI infrastructure.
Caption · 12/400/1.5
Figure 1 — pipeline stages and their policy gates.
Overline · 11/600/1.3
SECTION LABEL
Code · 13/400/1.6
createPipeline({ policy: "strict" })
Rules
- Letter-spacing tightens to −0.3px above 20px. Large type set at default
tracking reads loose and unresolved.
- Overline is a label, not a heading. 11px, 600, uppercase, 0.08em tracking.
Use it above a heading, never as one.
- Body copy is 16px / 1.65. This is the reading size for documentation and
long-form prose. Do not set body text below 14px.
- Never set body copy in Plus Jakarta Sans, and never set headings in
Source Sans 3. The split is what makes the voice recognisable.
Do
Pair an overline with an H2 to label a section. Keep the ramp intact — skip
sizes rather than inventing intermediate ones.
Don't
Introduce a fourth typeface, use a weight outside the listed set, or fake a
weight with font-synthesis.
1.3 - Space, shape, and motion
The 4px spacing base, the radius and elevation ladders, and the motion tokens.
Spacing
A 4px base with a nine-step scale. Every margin, padding, and gap resolves to
one of these values.
| Token | Value | Typical use |
|---|
--space-1 | 4px | Icon-to-label gaps |
--space-2 | 8px | Tight component padding |
--space-3 | 12px | Control padding |
--space-4 | 16px | Default element spacing |
--space-5 | 24px | Card padding, paragraph rhythm |
--space-6 | 32px | Component separation |
--space-7 | 48px | Sub-section separation |
--space-8 | 64px | Section separation |
--space-9 | 96px | Page-level bands |
Content sits inside a 1100px measure. Wider viewports gain margin, not
line length.
Radius
| Token | Value | Applied to |
|---|
--radius-sm | 3px | Tags, chips, small indicators |
--radius-md | 6px | Buttons, inputs, code blocks |
--radius-lg | 9px | Cards, panels |
--radius-xl | 13px | Large panels, modals |
--radius-full | 9999px | Pills, avatars |
Radius is a signal of scale: the larger the surface, the larger the radius.
Mixing radii on nested surfaces reads as a mistake — a 6px control inside a 9px
card is correct; a 13px control inside a 6px card is not.
Elevation
Four levels. Shadows are soft and low-contrast; the system leans on borders and
surface tint before it reaches for shadow.
| Token | Value | Applied to |
|---|
--shadow-0 | none | Flat surfaces, default |
--shadow-1 | 0 1px 3px rgba(0,0,0,0.06) | Cards at rest |
--shadow-2 | 0 4px 12px rgba(0,0,0,0.08) | Hover, dropdowns |
--shadow-3 | 0 8px 24px rgba(0,0,0,0.12) | Modals, popovers |
In dark mode, elevation is expressed by lightening the surface rather than
deepening the shadow — a shadow on a near-black background is invisible.
Motion
Things slide into place. They do not bounce.
Durations
| Token | Value | Use |
|---|
--duration-micro | 100ms | Colour and opacity changes |
--duration-standard | 200ms | Hover, focus, small moves |
--duration-expand | 300ms | Height and width changes, accordions |
--duration-page | 400ms | Route and view transitions |
Easing
| Token | Value | Use |
|---|
--ease-out | cubic-bezier(0.33, 1, 0.68, 1) | Entering — arriving on screen |
--ease-in | cubic-bezier(0.32, 0, 0.67, 0) | Exiting — leaving screen |
Radius ladder — 3 / 6 / 9 / 13 / full
sm 3px
md 6px
lg 9px
xl 13px
full
Elevation — shadow-0 through shadow-3
shadow-0
shadow-1
shadow-2
shadow-3
Motion — slides into place, never bounces
200ms · ease-out — entering
100ms · colour and opacity
Motion rules
- All animation wraps in
@media (prefers-reduced-motion: no-preference), or is
disabled under prefers-reduced-motion: reduce. - CSS-only for HTML. For React, use Framer Motion with these same timing values.
- Never animate text character-by-character.
- Page transitions cap at 400ms.
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
Do
Use --ease-out for anything appearing and --ease-in for anything leaving.
Keep durations on the ladder.
Don't
Add spring or bounce easing, animate layout-shifting properties, or exceed 400ms
for any single transition.
1.4 - Responsive
The four breakpoints, how the 12-column grid collapses, touch-target sizes, and the mobile navigation pattern.
The rest of the foundations describe values that do not change with viewport
width. This page describes the four points at which the layout does.
Breakpoints
Four breakpoints, declared in
brand/tokens/variables.css and
normative here:
| Name | Min width | The layout it describes |
|---|
sm | 640px | Large phone, landscape phone |
md | 768px | Small tablet — the first two-column layout |
lg | 1024px | Tablet landscape, small laptop — sidebars appear |
xl | 1280px | Desktop — the 1100px measure is fully margined |
Below sm is not a breakpoint; it is the base. Write the narrow layout
first and add width with min-width queries, so an unstyled or unsupported
viewport gets the single-column layout rather than a clipped desktop one.
/* Base — single column, 360px and up. */
.panel-row { display: grid; gap: var(--space-4); }
@media (min-width: 768px) {
.panel-row { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
.panel-row { grid-template-columns: 1.5fr 1fr; }
}
Breakpoint ruler — base · 640 · 768 · 1024 · 1280
base
1 col
640
1 col
768
2 col
1024
+ sidebar
1280
margined
How the grid collapses
The grid is 12 columns inside the
1100px measure. It does
not stay twelve columns all the way down — it resolves to four column counts:
| From | Columns | Gutter | Page padding |
|---|
| base | 4 | 16px | 16px |
md 768px | 8 | 16px | 24px |
lg 1024px | 12 | 24px | 32px |
xl 1280px | 12 | 24px | auto — the measure caps at 1100px |
Below md, every multi-column region becomes one column. There is no
two-column layout on a phone: two 160px columns are two unreadable columns.
Regions stack in source order, so the DOM must already be in reading order
— do not rely on order or grid-area to fix a sequence that is wrong in the
markup, because that breaks the focus and reading
order.
Spacing steps down one rung when the grid collapses: a --space-6 (32px)
section gap on desktop becomes --space-5 (24px) below md. Radius, type
scale, and border weights do not change — a card is a 9px card at every
width.
The 1100px measure is a maximum, not a target
Wider viewports gain margin, not line length. At xl the content stays 1100px
wide and centres; it does not grow to fill a 1920px display. This is the same
rule the spacing page states, restated here because it is the most common thing
a responsive rewrite breaks.
Touch targets
44×44px minimum for anything tappable, at every viewport — the floor is a
finger, not a breakpoint.
The default control heights are 32 / 40 / 48px, so the sm and md sizes are
below the floor on their own. Two ways to resolve it, in order of
preference:
- Grow the hit area, not the control. Keep the 40px visual button and give
it a transparent 2px vertical extension, or wrap it in a 44px-tall row. The
measurement stays normative; the target clears the floor.
- Use
lg (48px) on touch-primary surfaces. Correct for the primary action
on a phone, where a 40px button next to a 48px one reads as demoted.
Never shrink to sm (32px) for a touch target. Spacing between adjacent targets
is at least --space-2 (8px), so a mis-tap does not fire the neighbour.
/* Preferred: 40px control, 44px target. */
.pj-btn--md {
min-height: 40px;
position: relative;
}
.pj-btn--md::after {
content: "";
position: absolute;
inset: -2px 0; /* 40 + 2 + 2 = 44 */
}
Mobile navigation
Use a bottom tab bar for application surfaces. Use a top drawer for
documentation and marketing surfaces. The choice follows the shape of the
navigation, not the shape of the device:
- An application has a small, flat set of destinations — the
dashboard example has five.
Five or fewer flat destinations fit a tab bar, which keeps the current
location permanently visible, sits in the thumb arc, and costs no taps to
reach. That is worth the 56px of permanent screen it occupies.
- Documentation has a deep, nested tree. A tab bar cannot express it, and
flattening the tree to fit one is worse than a drawer. A drawer behind a
labelled disclosure control costs one tap and can show the whole hierarchy.
The failure mode is a tab bar with six or more items, or one that hides the
primary destination behind “More”. If the destinations do not fit, the surface
is a drawer surface.
Both patterns carry the same obligations: the current destination is marked with
an accent underline or fill plus the label, never colour alone; the drawer
traps focus while open, closes on Esc, and returns focus to its trigger — the
same contract as a modal.
Bottom tab bar — 5 destinations, current marked with fill and label
screen content
Overview
Agents
Alerts
Account
Tables, code, and diagrams
Three content types cannot reflow, and each has one answer:
- Tables scroll horizontally inside their own container with the first
column pinned — never the page. See wide
tables.
- Code blocks scroll horizontally. Do not soft-wrap code: a wrapped line
changes what the code appears to say.
- Diagrams get a scrollable container or a re-drawn narrow variant. Do not
scale a diagram down until its labels are unreadable — an 8px label is not a
responsive diagram, it is a broken one.
Worked example
Mobile onboarding —
three screens in a device frame: an onboarding panel on midnight, an engagement
list of stacked cards, and a finding-review screen with two side-by-side
actions. It is the dashboard
below md: same tokens, same components, one column, tab bar instead of
sidebar.
Do
Write the base layout first and add width with min-width queries. Put regions
in the DOM in reading order. Give every tappable thing a 44px target.
Don't
Keep two columns below 768px, reorder regions with CSS to fix source order,
scale a diagram until its labels are illegible, or let a wide table scroll the
whole page.
2 - Logo
The mark, the four lockups, and the rules that keep them legible.
The mark is a stencil peony bud — concentric petal cuts resolving to an accent
centre. It reads as a single form at 16px and holds its internal structure at
poster scale.
2.1 - Lockups
Four approved lockups, rendered live, and where each one belongs.
Four lockups are approved. Each has a defined context; using one outside its
context is what makes a layout feel off-brand even when the colours are right.
All four are built with HTML and CSS rather than flattened artwork, so spacing
comes from natural text flow and never needs re-kerning.
The four lockups
Two-line (default) and one-line
projectious
· work
Two-line · headers, slides, cards · min 120px
projectious · work
One-line · navbars, footers, signatures · min 140px Dot-replace and stacked
projectious
work
Dot-replace · compact horizontal · min 160px
projectious · work
Stacked · app icons, avatars, slide corners · min 60px | Lockup | Composition | Use for | Minimum width |
|---|
| Two-line | Mark + “projectious” over “· work” | Headers, slides, cards — the default | 120px |
| One-line | Mark + “projectious · work” inline | Navbars, footers, signatures | 140px |
| Dot-replace | Mark substitutes the dot in “projectious.work” | Compact horizontal contexts | 160px |
| Stacked | Mark above the wordmark, centred | App icons, avatars, slide corners | 60px |
On light and dark
Every lockup has a light-surface and a dark-surface rendering. The mark’s inner
cuts pick up the surface colour, so it never needs a plate or a knockout box.
The same lockup on both surfaces
The mark alone
The mark may stand alone only where the wordmark is already established
nearby — an app icon on a page that names the product, an avatar in a
signed-in context, a favicon.
Standalone mark — 96 · 64 · 48 · 32 · 24 · 16px
Minimum for the standalone mark is 24px. The 16px rendering exists for
favicons only, where the browser supplies the name.
Product line
Product names extend the wordmark with the same dot construction and flow.
Product-line extensions
projectious · guard
projectious · forge
projectious · flow Wordmark construction
The separator between “projectious” and “work” is a dot in the mark’s own
geometry, not a typed period. It takes slate on light surfaces and midnight-11
on dark. Spacing is handled by flex layout so the wordmark reflows naturally —
do not hand-kern it or convert it to a static image.
The wordmark keeps the accent
“work” is set in orange-9 (#E05232). As body text that value would be too
low-contrast, but WCAG 2.1 SC 1.4.3 exempts logotypes — “text that is part of a
logo or brand name has no minimum contrast requirement”. The wordmark is the one
place the identity accent is used as letterforms.
Do
Use the two-line lockup unless the space is explicitly horizontal. Let the flex
layout handle spacing.
Don't
Rebuild a lockup by hand, change the order of mark and wordmark, or set the
wordmark in a different typeface.
2.2 - Usage
Clear space, minimum sizes, monochrome variants, and placing the mark on colour and photography.
Clear space
Exclusion zone — 1x the icon height on every side
Nothing enters the dashed zone Minimum clear space is 1× the icon height on all sides. Nothing — text,
rules, image edges, other logos — enters that exclusion zone.
At a 40px mark, that is 40px of clear space in every direction. The zone scales
with the mark, so it never needs recalculating per context.
Minimum sizes
Minimum sizes — 48 / 32 / 24 / 16px
| Variant | Minimum |
|---|
| Icon (standalone) | 24px — 16px for favicons only |
| Stacked lockup | 60px |
| Two-line lockup | 120px |
| One-line lockup | 140px |
| Dot-replace lockup | 160px |
Below these sizes the petal cuts collapse into a smudge. If the space is smaller
than the minimum, use a smaller variant rather than scaling one down.
Monochrome
Four monochrome treatments are approved, for print, fax, embossing, and any
single-colour reproduction:
| Variant | Mark | Inner lines | Surface |
|---|
| Midnight | #1d3352 | White | Light |
| Black | #000000 | White | Light |
| White reversed | #ffffff | Slate | Midnight |
| Grey | #9299a4 | White | Light |
The white-reversed variant uses slate for its inner lines rather than the
background colour, so the petal structure stays visible. All petal structure
must remain legible in every monochrome variant — a solid silhouette is not an
approved treatment.
On colour
Placing the mark on colour
Lighter than slate-5 -> light mark On photography -> midnight backing | Background | Treatment |
|---|
| Lighter than slate-5 | Light-surface mark |
| Darker than slate-5 | Dark-surface mark |
Accent (#E05232) | Mark with a white ring border |
| Photography | Semi-transparent midnight backing behind the mark |
The slate-5 threshold is the decision rule: measure the background, pick the
side it falls on. On the accent, neither variant has enough separation on its
own, which is why the ring exists.
On photography
Never place the mark directly on an unmodified photograph. Apply a
semi-transparent midnight backing — a shape, a scrim, or a gradient — so the
mark sits on a controlled surface. The backing is part of the logo treatment,
not an optional enhancement.
Watermark
Watermark — uniform wash, no accent
For background use, the mark may run as a watermark at 8–15% opacity. It
renders as a uniform wash: no accent colour in watermark mode, since a
single coloured element at low opacity reads as a printing error.
Favicons
Icon-only, on a midnight tile, with 12% padding. Corner radius scales with
size:
Favicon tiles — 12% padding, radius scales with size
| Size | Radius |
|---|
| 16px | 2px |
| 32px | 4px |
| 48px | 8px |
| 180px | 36px |
Do
Measure the background against slate-5 before choosing a variant. Keep the full
clear-space zone even when layout is tight.
Don't
Recolour the mark outside the approved variants, add effects (shadow, glow,
outline, gradient), rotate or skew it, or place it on a busy photograph without
a backing.
2.3 - Files
Delivered formats, when to use each, and where to download them.
| Format | Contents | Use for |
|---|
| SVG | Vector source | Primary for web and print |
| PNG @1x | 16 / 32 / 48 / 64 / 128 / 256 / 512px | Native-resolution raster |
| PNG @2x | 2× pixel dimensions | Retina — required for web |
| PNG @3x | 3× pixel dimensions | Super-retina — mobile |
| ICO | Multi-resolution (16 + 32 + 48) | Favicon |
| apple-touch | 180×180 PNG | iOS home screen |
SVG is the source. Raster exports exist for contexts that cannot take vector —
prefer SVG wherever the medium allows it.
Variants
Each raster size ships in five variants:
icon-light — for light surfacesicon-dark — for dark surfacesicon-mono-black — single-colour blackicon-mono-white — single-colour white, reversedicon-mono-gray — single-colour grey
Downloads
The logo files are served from this site and are also in the repository under
brand/logo/.
Licence
Logo files are brand assets, not MIT-licensed code. You may reference and
display them under the terms in
Licensing; you may not modify
them or use them to imply endorsement. See also the
Trademark guidelines.
3 - Interface
The foundations applied to screens — components, code surfaces, dark mode, icons, and forms.
Interface guidance applies the foundations
to working software. Where a component appears here, its measurements are
normative — a 40px input is 40px, not “about 40px”.
3.1 - Components
The component set as live specimens — buttons, inputs, cards, tables, navigation, feedback, overlays, and data display.
Every specimen on this page is live markup styled by the brand tokens, ported
from brand/html/projectious-kitchen-sink.html. They follow your colour mode —
switch the theme and they change with the documentation.
Measurements listed here are normative: a 40px input is 40px.
Five variants, three sizes. Plus Jakarta Sans 600, 6px radius, 200ms transitions.
Variants — medium (40px)
Sizes — sm 32px · md 40px · lg 48px
On a midnight surface
| Variant | Fill | Border | Text | Contrast | Use |
|---|
| Primary | midnight-9 | none | white | 12.75:1 | Default action |
| Accent | accent-solid #cc4528 | none | white | 4.72:1 | The single most important action |
| Outline | transparent | 1.5px orange-9 | orange-11 | 5.13:1 | Secondary action |
| Ghost | transparent | 1px border | slate-9 | — | Tertiary, toolbars |
| Danger | #a8261c | none | white | 7.10:1 | Destructive action |
Accent buttons fill with accent-solid, not step 9
orange-9 (#E05232) is the identity accent and is unchanged as a mark,
border, active state, or syntax colour. But white on it measures 3.87:1 —
below the 4.5:1 floor for 13–14px labels. Solid accent controls therefore fill
with --color-accent-solid (#cc4528), white at 4.72:1; hover
continues to accent-dark (#b84228, 5.46:1).
Do
Use exactly one accent button per view. Give every button a verb — “Deploy”,
“Save changes”.
Don't
Place two accent buttons side by side, or use a danger button for a reversible
action.
Default height 40px (sm 32, lg 48). Source Sans 3 at 13px, slate-7 border,
and a 2px focus ring at midnight 15% alpha.
Text, textarea, select
States — default, focus, error, disabled
Full rules — labels, validation, focus — are on
Forms.
Cards
Radius 9px, padding 24px, 1px slate-4 border, shadow-1 at rest. Nested
controls step down one radius.
Basic, with image, with actions
Validate & deploy
Runs policy checks before promoting to staging.
Image / diagram
Architecture
Nodes, flows, and policy gates.
Agent: auditor
Idle for 2 hours.
Tables
Header cells use the overline style. Striping uses the subtle background step,
never a border-only rule. Wide tables scroll inside their own container.
Data table with sort indicator
| Pipeline | Policy | Agents | Status |
|---|
| validate-deploy | strict | 2 | Healthy |
| nightly-audit | advisory | 1 | Degraded |
| release-train | strict | 4 | Blocked |
Table with search and filters
A toolbar above the table carries the search field and filter chips. Active
filters are shown as removable chips so the current view is always legible —
never leave a filter applied with no visible indication.
Search, filter chips, pagination footer
Grouped columns use a two-row header. The group row is centred over its span and
separated by a vertical rule — the only place the system uses one.
Two-row header with column groups
Grouped rows and totals
Row groups act as sub-headings inside the table. A totals row is separated by a
2px rule — heavier than the body rules, so it reads as a summary rather than
another record.
Row groups, indented children, totals
| Workload | Runs | Cost |
|---|
| Platform |
| validate-deploy | 412 | €48.10 |
| release-train | 14 | €12.60 |
| Security |
| nightly-audit | 7 | €21.40 |
| secret-scan | 96 | €9.05 |
| Total | 529 | €91.15 |
Wide tables
A wide table scrolls inside its own container, never the page, and pins its
first column so the row identity stays visible while scrolling.
Horizontal scroll with a sticky first column
Do
Right-align numeric columns and set them in IBM Plex Mono so digits line up.
Show the active filter state and the result count together.
Don't
Add vertical rules between ordinary columns, let a wide table scroll the whole
page, or apply a filter without a visible chip saying so.
Navigation
The navbar follows the colour mode: midnight-1 at 88% alpha in light,
midnight-dark-1 at 88% in dark, both over a 12px backdrop blur so content
scrolling underneath stays legible. It is separated by a 1px border, not a
fill — the header is chrome, and a solid midnight band at the top of every page
spends the brand’s darkest surface on navigation.
The active link carries a 2px accent-solid underline plus the
high-emphasis text step. The underline is accent-solid rather than orange-9
because it sits directly against 13px text and is read as part of it.
Navbar — follows the colour mode; 1px border, 2px accent underline on the active link
This specimen mirrors .td-navbar — the header at the top of this page. Switch
the theme and both change together.
Breadcrumbs, tabs, pagination, sidebar
OverviewRunsSettings
accent underline on activecurrent in midnight-9
active: 600 weight
Alerts and feedback
Four semantic colours as a 4px left border on a tinted background. The hues are
mode-specific — see Colour.
Alerts
Info
Pipeline requires at least one validation agent.
Success
All checks passed. Deployment ready.
Warning
Agent "monitor" has been idle for 2 hours.
Danger
Policy violation — deployment blocked.
Badges, progress, spinner
Default
Urgent
v0.1.0
Breaking
Semantic colour is not decoration
Success, warning, and danger carry meaning. Do not use them to add visual
variety to neutral content.
Modals and overlays
Modal radius 13px with shadow-3; scrim is midnight at 40% alpha. Focus is
trapped while open and restored to the trigger on close; Esc always closes.
Dialog, dropdown, tooltip
Delete pipeline?
This removes the pipeline and its run history. This cannot be undone.
Re-run
Duplicate
Export logs
Last run 4 minutes ago12px, 3px radius
Data display
Avatar, stat card, tags
JD
AS1,284
Runs this week
▲ 12% vs last week
Composable
Agent-first
Humanistic List and timeline
validate-deploy4m ago
nightly-audit2h ago
release-train1d ago
Deploying to staging
09:16Status indicators — always a dot plus a label
Healthy
Degraded
Failed
Idle
Do
Pair every status dot with a text label, so meaning does not depend on colour
perception.
Don't
Use a bare coloured dot, or rely on red/green alone to distinguish states.
Code and terminal
The code surface is always dark — see Code.
Terminal
$ tofu apply -auto-approve
Plan: 3 to add, 0 to change, 0 to destroy.
✓ Policy check passed
● Deploying to staging…
3.2 - Composed patterns
How the components combine into whole screens — the page shell, the KPI row, and the primary/secondary split.
Components answers “what does a
card look like”. This page answers the question after it: what does a whole
screen look like when it is made only of those parts.
Nothing here is a new component. Every element below already exists on the
components page; what is normative here is the arrangement — which regions a
screen has, in what order, at what widths.
The page shell
Three regions, always in this order in the DOM:
| Region | Width | Contents |
|---|
| Sidebar | 224px, fixed | Brand lockup, pj-sidebar items, account block pinned to the bottom |
| Header | 60px, fixed height | Page title, one line of context, search, and one accent action |
| Content | fills, scrolls | The page body — everything below |
The sidebar is the only large midnight fill on the screen. It is the
application’s frame, so it stays constant while the content region changes; a
sidebar that re-renders per route reads as a page reload.
The header carries exactly one accent button. That is the whole quota for
the screen — one accent per view
— which is why the pattern places it here rather than leaving it to the content
region to spend.
Below lg (1024px) the sidebar leaves and becomes the mobile navigation
pattern; see Responsive.
The KPI row
Directly under the header: four stat cards in a repeat(4, 1fr) grid,
--space-4 gap.
Four, not three and not six. Three leaves a hole in a 12-column grid; six turns
the row into a wall of numbers nobody reads. If there are five things worth
measuring, the fifth one is not a KPI.
Each card is a pj-stat — the
number first in Plus Jakarta Sans 800, its label under it in the overline style,
then a delta line. The pj-stat carries its own border and radius, so it is
not nested inside a pj-card; that would draw the box twice.
A delta states its direction in text or an arrow, not in colour. The default
__delta is the success hue; a delta that is not good news takes an explicit
colour, and a delta that is merely neutral takes the muted foreground rather
than borrowing a semantic one.
KPI row — value, overline label, delta with an explicit direction
6
Active engagements
▲ 1 this month
412
Agent hours
▲ 18% vs last week
3.2h
Time to audit
▼ 0.6h faster
5
Open findings
2 need review
Primary content and secondary panel
Under the KPI row, a 1.5fr / 1fr split: the thing the page is about on the
left, the thing that gives it context on the right.
- Primary is the record set — a table
in a
pj-table-shell, with its toolbar, filter chips, and pagination footer. - Secondary is a
pj-card holding a feed: a pj-timeline or pj-list of
recent events, each with a status dot and a text label.
align-items: start, so the two panels are independent — the feed does not
stretch to match a long table, and the table does not gain whitespace to match a
short feed.
The split is 1.5fr / 1fr because the primary panel holds tabular data with four
or more columns and the secondary holds one column of prose. An even 1fr / 1fr
starves the table and pads the feed.
At md the ratio flattens to 1fr / 1fr; below md the secondary panel moves
below the primary, in source order.
Reading the whole thing at once
Dashboard mockup — a client
engagement dashboard built entirely from the parts above: page shell, KPI row,
engagement table, agent-activity feed. Open it beside the components page and
every element in it should be findable there.
Its narrow counterpart is
Mobile onboarding —
the same system with the sidebar replaced by a tab bar and every region in one
column.
Density
One pattern, two densities. The measurements on the components page are the
comfortable density and are the default. A compact density exists for
screens whose job is scanning many rows at once — an audit log, a run history:
| Comfortable | Compact |
|---|
| Table row padding | 12px | 8px |
| Control height | 40px | 32px |
| Card padding | 24px | 16px |
| Section gap | --space-6 32px | --space-5 24px |
Density changes padding and control height. It does not change type size,
radius, or border weight — a compact table is the same table with less air, not
a smaller one. Compact is never used on touch-primary surfaces, where the
44px floor applies regardless.
Do
Compose screens from the documented components. Spend the one accent action in
the header. Keep the KPI row at four. Let the secondary panel fall below the
primary on narrow viewports.
Don't
Introduce a screen-specific component when an arrangement of existing ones will
do, put a second accent button in the content region, or use compact density on
a touch surface.
3.3 - Dark mode
Both modes are equally supported. The implementation rules that keep them in step.
Dark mode is not a variant of the brand — it is half of it. Both modes are
designed, tested, and shipped together.
Implementation rules
- Default follows
prefers-color-scheme, with a manual override toggle
persisted to localStorage. - CSS custom properties swap per theme. Components read tokens; they do not
branch on the mode themselves.
- Step 9 (solid accent) stays constant across modes.
#E05232 is #E05232
everywhere. - No pure
#000 or #fff as text. Use step 12 — #c5daf0 on dark,
#142438 on light. - Images get a subtle overlay (black at low alpha) in dark mode, so a bright
photograph does not punch a hole in a dark page.
- Code blocks are always dark, regardless of mode. See
Code.
The same card in both modes
Light mode
Body text is midnight-12; supporting copy is slate-11.
Deploy
Dark mode
Never pure white — midnight-12 from the dark scale.
Deploy
Surfaces
Elevation in dark mode is expressed by lightening the surface, not by
deepening the shadow — a shadow against a near-black background is invisible.
| Layer | Light | Dark |
|---|
| App background | #ffffff | midnight-dark-1 #0e1720 |
| Subtle surface | midnight-1 | midnight-dark-2 #131e2b |
| Raised surface | midnight-2 | midnight-dark-3 #1a2b3e |
Restating text on dark surfaces
The default heading colour is midnight-9, chosen so headings read correctly on
light pages. That same value on a midnight surface is invisible — #1d3352 on
#1d3352 is a contrast ratio of 1.00:1.
Any component that paints its own dark background must therefore restate its
text colours. This applies to covers, dark boxes, footers, and overlays:
.surface--dark {
--bs-body-color: #c5daf0; // midnight-dark-12
--bs-heading-color: #c5daf0;
--bs-secondary-color: #97a8b8; // slate-dark-11, still AA
}
Do
Test every surface in both modes. Restate text colours on any component that
sets its own dark background.
Don't
Ship a component that only works in one mode, or use pure black or white as a
text colour in either.
3.4 - Code
The always-dark code surface and the syntax theme, with measured contrast for every token.
Code blocks are always dark
Code blocks stay dark regardless of colour mode. A code surface that flips
with the theme forces the syntax palette to be designed twice and makes
screenshots inconsistent between users. The surface is midnight-2 from the
dark scale (#131e2b) in both light and dark mode.
The block you are reading is rendered by that rule:
// Agent pipeline definition
const pipeline = createPipeline({
name: "validate-deploy",
policy: "strict",
agents: ["auditor", "deployer"],
});
Syntax theme
Because the surface is always dark, every syntax value is read from the dark
scale. Contrast is measured against #131e2b.
Ten roles, not twenty-two tokens
Editors do not describe code with six token types. The
Language Server Protocol
defines 22 semantic token types and 10 modifiers, and
TextMate grammars — the
model behind VS Code, Sublime Text, and most highlighters — define 11 root
scopes with a deep sub-scope tree under each.
A theme should not answer that with twenty-two colours. Past roughly nine, hue
stops being a signal: everything is coloured, so nothing is marked. The two
scope vocabularies are therefore grouped into ten roles, and the modifiers
are carried by weight and slant rather than by more hue.
Every role is an existing brand or terminal value. The expansion introduced no
new colour — the terminal palette had already added the two hues, cyan and
magenta, that a syntax theme needs and the three interface scales do not have.
Modifiers are not colours
LSP modifiers combine with any token type: ten modifiers against ten roles is a
hundred states. Hue cannot carry that, so it does not try.
Deprecated must survive greyscale
deprecated is a state, not a category. It is struck through as well as
recoloured, so a reader who cannot separate the red from the plain text still
sees that the symbol should not be used.
Comments are the one syntax role with no scale step available to it. Steps 8–10
are border and solid-surface roles and are not held to text thresholds; step 11
is already spoken for by operators.
So code-comment (#72889d, 4.59:1) exists as a dedicated syntax token —
the dimmest value that clears AA while staying visibly below operators. It is
not a scale step and should not be treated as one.
Do
Group scopes into roles, and let a language’s grammar map onto them. Keep the
role count under ten, and check every value against the code surface.
Don't
Give each LSP token type its own hue, or use the accent as a syntax colour — it
marks the primary action, and a code block is not one.
Worked examples
Each block below is a real, compilable-shaped fragment chosen to exercise as
many of the ten roles as its language has. The coverage table after them records
which roles each language actually reaches — several cannot reach all ten, and
that is a property of the language, not a gap in the theme.
C
/* Ring buffer — fixed capacity, no allocation after init. */
#include <stdint.h>
#define RING_CAP 256 // macro: a decorator-role token
typedef enum { RING_OK = 0, RING_FULL = 1 } ring_status_t;
typedef struct {
uint8_t data[RING_CAP];
size_t head, tail;
_Bool wrapped;
} ring_t;
static inline size_t ring_len(const ring_t *r) {
return (r->head - r->tail) & (RING_CAP - 1);
}
static const char *RING_TAG = "ring\n"; // string literal
ring_status_t ring_push(ring_t *restrict r, uint8_t byte) {
// Reject when one slot short of capacity, so head never meets tail.
if (ring_len(r) == RING_CAP - 1) return RING_FULL;
r->data[r->head++ & (RING_CAP - 1)] = byte;
return RING_OK;
}
C++
// Policy-based cache. Types, templates, and a lambda.
#include <string>
#include <unordered_map>
namespace projectious::cache {
template <typename Key, typename Value>
class LruCache final {
public:
explicit LruCache(std::size_t capacity) noexcept : capacity_{capacity} {}
[[nodiscard]] auto get(const Key& key) const -> const Value* {
const auto it = entries_.find(key);
return it == entries_.end() ? nullptr : &it->second;
}
void put(Key key, Value value) {
static constexpr auto kTag = "lru"; // string literal
// Evict before insert so size never exceeds the capacity.
if (entries_.size() >= capacity_) evict();
entries_.emplace(std::move(key), std::move(value));
}
private:
void evict() noexcept { /* … */ }
std::size_t capacity_{0};
std::unordered_map<Key, Value> entries_{};
};
} // namespace projectious::cache
Python
"""Pipeline stages and their policy gates."""
from __future__ import annotations
import functools
from dataclasses import dataclass, field
from typing import Final, Iterable
# Retry budget is a policy decision, not a tuning knob.
MAX_RETRIES: Final[int] = 3
DEFAULT_POLICY = "strict"
@dataclass(frozen=True, slots=True)
class Stage:
"""A single stage. Immutable once constructed."""
name: str
policy: str = DEFAULT_POLICY
retries: int = 0
tags: list[str] = field(default_factory=list)
@property
def is_strict(self) -> bool:
return self.policy == "strict"
@staticmethod
def parse(raw: str) -> "Stage":
name, _, policy = raw.partition(":")
return Stage(name=name.strip(), policy=policy or DEFAULT_POLICY)
@functools.lru_cache(maxsize=None)
def validate(stages: Iterable[Stage]) -> bool:
for stage in stages:
if stage.retries > MAX_RETRIES:
raise ValueError(f"{stage.name!r} exceeds {MAX_RETRIES} retries")
return True
Rust
//! Policy evaluation for pipeline stages.
use std::collections::HashMap;
use std::fmt::{self, Display};
const MAX_RETRIES: u32 = 3;
/// How strictly a stage is evaluated.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Policy {
Strict,
Advisory,
}
#[derive(Debug, Default)]
pub struct Stage<'a> {
pub name: &'a str,
pub policy: Option<Policy>,
pub retries: u32,
}
impl<'a> Stage<'a> {
// Strict by default: a gate that is not configured should fail closed.
pub fn new(name: &'a str) -> Self {
Self { name, policy: Some(Policy::Strict), retries: 0 }
}
pub fn validate(&self) -> Result<(), String> {
if self.retries > MAX_RETRIES {
return Err(format!("{} exceeds {MAX_RETRIES} retries", self.name));
}
Ok(())
}
}
impl Display for Policy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", match self { Policy::Strict => "strict", _ => "advisory" })
}
}
Go
// Package pipeline evaluates stages against their policy gates.
package pipeline
import (
"errors"
"fmt"
)
const MaxRetries = 3
// Policy is how strictly a stage is evaluated.
type Policy int
const (
Strict Policy = iota
Advisory
)
var ErrTooManyRetries = errors.New("stage exceeds retry budget")
type Stage struct {
Name string `json:"name"`
Policy Policy `json:"policy"`
Retries int `json:"retries,omitempty"`
}
func (s *Stage) Validate() error {
if s.Retries > MaxRetries {
return fmt.Errorf("%q: %w", s.Name, ErrTooManyRetries)
}
return nil
}
func ValidateAll(stages []Stage) (ok bool, err error) {
for i := range stages {
if err = stages[i].Validate(); err != nil {
return false, err
}
}
return true, nil
}
Java
package work.projectious.pipeline;
import java.util.List;
import java.util.Objects;
/** A single pipeline stage and its policy gate. */
public final class Stage implements Comparable<Stage> {
public static final int MAX_RETRIES = 3;
public static final long TIMEOUT_MS = 30_000L;
public static final int MASK = 0xFF;
private final String name;
private final Policy policy;
private int retries = 0;
private double budget = 12.60;
public Stage(String name, Policy policy) {
this.name = Objects.requireNonNull(name, "name");
this.policy = policy;
}
@Override
public int compareTo(Stage other) {
return this.name.compareTo(other.name);
}
@Deprecated(since = "2.0", forRemoval = true)
public boolean isStrict() {
return policy == Policy.STRICT;
}
public void validate(List<String> errors) throws IllegalStateException {
if (retries > MAX_RETRIES) {
throw new IllegalStateException("%s exceeds %d retries".formatted(name, MAX_RETRIES));
}
}
public enum Policy { STRICT, ADVISORY }
}
Assembly (NASM)
; Sum a byte array. rdi = pointer, rsi = length, returns in rax.
section .data
msg: db "sum: ", 0
LEN equ 5
section .text
global sum_bytes
sum_bytes:
xor rax, rax ; accumulator
test rsi, rsi
jz .done ; empty input
.loop:
movzx rdx, byte [rdi]
add rax, rdx
inc rdi
dec rsi
jnz .loop
.done:
ret
Bash
#!/usr/bin/env bash
# Validate a pipeline definition and promote it when the gates pass.
set -euo pipefail
readonly MAX_RETRIES=3
readonly POLICY="${PIPELINE_POLICY:-strict}"
declare -A GATE_STATUS=()
log() { printf '%s %s\n' "$(date -u +%FT%TZ)" "$*" >&2; }
validate_stage() {
local -r name="$1" retries="${2:-0}"
if (( retries > MAX_RETRIES )); then
log "ERROR ${name} exceeds ${MAX_RETRIES} retries"
return 1
fi
GATE_STATUS["$name"]="ok"
}
main() {
local -a stages=("validate" "deploy")
for stage in "${stages[@]}"; do
validate_stage "$stage" 0 || exit 1
done
log "policy=${POLICY} stages=${#stages[@]}"
}
main "$@"
LaTeX
\documentclass[11pt,a4paper]{article}
\usepackage[utf8]{inputenc}
\usepackage{amsmath}
% Pipeline notation used throughout the paper.
\newcommand{\stage}[2]{\ensuremath{#1 \xrightarrow{#2}}}
\title{Policy Gates in Composable Pipelines}
\author{Jane Doe}
\begin{document}
\maketitle
\section{Definitions}
A stage $s_i$ passes when its retry count $r_i \leq 3$:
\begin{equation}
\forall s_i \in S : r_i \leq R_{\max}, \quad R_{\max} = 3
\end{equation}
\begin{itemize}
\item \textbf{Strict} — the gate fails closed.
\item \emph{Advisory} — the gate records and continues.
\end{itemize}
\end{document}
Markdown
---
title: Stage reference
weight: 10
---
# Stage reference
A stage passes when its retry count stays at or below **three**. See the
[policy guide](../policy/) for the full rules.
## Fields
| Field | Type | Default |
|---|---|---|
| `name` | string | — |
| `policy` | enum | `strict` |
> Advisory gates record a failure and continue. Strict gates fail closed.
1. Validate the configuration
2. Request promotion
3. Deploy
```sh
pipeline validate --policy strict
```
<!-- Deprecated: `--legacy-gate` is removed in 2.0. -->
JSON
{
"$schema": "https://projectious.work/schema/pipeline-2.json",
"name": "validate-deploy",
"policy": "strict",
"retries": 3,
"enabled": true,
"owner": null,
"budget": 12.6,
"stages": [
{ "name": "validate", "gate": "strict", "timeoutSeconds": 120 },
{ "name": "deploy", "gate": "advisory", "timeoutSeconds": 600 }
],
"tags": ["platform", "eu-central"]
}
YAML
# Pipeline definition — one policy gate per stage.
apiVersion: projectious.work/v2
kind: Pipeline
metadata:
name: validate-deploy
labels: { team: platform, region: eu-central }
defaults: &defaults
policy: strict
retries: 3
enabled: true
spec:
<<: *defaults
budget: 12.60
owner: ~
stages:
- name: validate
timeoutSeconds: 120
- name: deploy
policy: advisory
timeoutSeconds: 600
owner: "platform@projectious.work"
schema: 'https://projectious.work/schema/pipeline-2.json'
notes: |
Advisory gates record and continue.
Strict gates fail closed.
TOML
# Pipeline definition — one policy gate per stage.
schema = "https://projectious.work/schema/pipeline-2.json"
[pipeline]
name = "validate-deploy"
policy = "strict"
retries = 3
enabled = true
budget = 12.60
created = 2026-08-02T09:00:00Z
tags = ["platform", "eu-central"]
[[pipeline.stage]]
name = "validate"
gate = "strict"
timeoutSeconds = 120
[[pipeline.stage]]
name = "deploy"
gate = "advisory"
timeoutSeconds = 600
What each language reaches
The table is measured from the rendered page, not asserted: every block above is
parsed and its emitted token classes are mapped back to the roles. ● means the
role appears in that example.
| Language | Plain | Keyword | Type | Function | Macro | String | Number | Operator | Comment | Reached |
|---|
| C | ● | ● | ● | ● | ● | ● | ● | ● | ● | 9/9 |
| C++ | ● | ● | ● | ● | ● | ● | ● | ● | ● | 9/9 |
| Python | ● | ● | ● | ● | ● | ● | ● | ● | ● | 9/9 |
| Rust | ● | ● | ● | ● | ● | ● | ● | ● | ● | 9/9 |
| Go | ● | ● | ● | ● | · | ● | ● | ● | ● | 8/9 |
| Java | ● | ● | ● | ● | ● | ● | · | ● | ● | 8/9 |
| Bash | ● | ● | · | · | ● | ● | ● | ● | ● | 7/9 |
| Assembly (NASM) | ● | ● | ● | ● | ● | ● | ● | ● | ● | 9/9 |
| LaTeX | ● | ● | · | · | · | ● | ● | · | ● | 5/9 |
| Markdown | · | ● | ● | · | · | ● | ● | ● | ● | 6/9 |
| JSON | · | ● | · | · | · | ● | ● | ● | · | 4/9 |
| YAML | · | ● | · | · | ● | ● | ● | ● | ● | 6/9 |
| TOML | ● | ● | · | · | · | ● | ● | ● | ● | 6/9 |
Nine roles rather than ten, because invalid only appears when a grammar
actually fails to parse — a correct example cannot demonstrate it.
Where a language falls short, the reason is the language or the lexer:
- Bash — no type system, and Chroma’s shell lexer does not mark function definitions.
- Go — Go has no macro or annotation construct; its struct tags are strings.
- Java — Chroma’s Java lexer emits a plain name for every numeric literal, so numbers cannot be separated. A lexer limitation, not a palette one.
- LaTeX — No type, callable or operator concept in the grammar — commands are keywords.
- Markdown — Prose, not code: there is nothing to name, call, or annotate.
- JSON — By design: no comments, no identifiers, no callables. Keys take the keyword role.
- YAML — Anchors and merge keys take the macro role; there are no callables or types.
- TOML — Table headers take the plain role; there are no callables or types.
Two findings worth carrying into any theme
Chroma files C preprocessor directives and Rust attributes under
Comment.Preproc, which would colour #define and #[derive(…)] as
commentary. They are macros — the LSP says so — and are coloured as macros here.
It also files documentation comments under String.Doc, which would colour a
Rust /// line and a Python docstring as data. Both are documentation, and take
the comment role.
Inline code
Inline code does not take the dark block treatment — it follows the
surrounding surface. On light surfaces it sits on midnight-2 (light scale)
with orange-11 text; in dark mode both values shift to their dark-scale
counterparts. It uses IBM Plex Mono at 13px with a 3px radius.
Terminal output
Terminal blocks use the same dark surface. Prompts take the comment colour,
output takes the operator colour, so a transcript stays readable without
becoming a second syntax theme.
$ hugo --gc --minify
Start building sites …
Total in 842 ms
3.5 - Icons
Lucide as the icon library, and how icons are sized and coloured.
Library
The system uses Lucide — an actively maintained fork
of Feather with over 1,400 icons.
| Property | Value |
|---|
| Licence | ISC (functionally equivalent to MIT) |
| Commercial use | Permitted |
| Attribution | Not required in product UI; retain the notice in source |
| Modification | Permitted |
ISC is one of the most permissive open-source licences available, which is why
it was chosen over icon sets with attribution or share-alike terms. See the
legal assessment for the
clearance detail.
Getting the actual icon
Every icon in this system is a verbatim Lucide path. The specimens on this
page are lucide/shield, copied from the library rather than approximated —
including the 1 1 0 0 1 arc segments that give Lucide its corner radius, which
is exactly the character a redrawn path loses.
Take the file, do not trace it:
curl -O https://unpkg.com/lucide-static@0.544.0/icons/shield.svg
Pin the version. lucide-static@latest resolves to whatever shipped this
morning, which lets an upstream release silently change a committed document.
The copies used by the worked examples are
vendored at brand/examples/lucide/ at a pinned version, with the upstream ISC
notice beside them.
Inline the <path> and set width, height, and stroke at the use site;
keep viewBox="0 0 24 24", fill="none", stroke-width="2", and both
stroke-linecap and stroke-linejoin at round. Changing any of those five is
what makes an icon stop looking like the rest of the set.
Sizes on the 4px grid — 16 / 20 / 24 / 32px
Icon with a label, and an icon button
Usage
- Stroke width 2, matching Lucide’s default. Do not thin or thicken strokes
to “match” a layout.
- Size on the 4px grid: 16, 20, 24, 32px. 20px is the default for inline UI.
- Colour follows text. An icon beside a label takes the label’s colour.
Standalone icons carrying meaning take
--color-secondary; an icon marking
the primary action takes the accent. - Icons are not decoration. Every icon must carry meaning the label does not
already carry, or be marked
aria-hidden="true".
Do
Pair icons with text labels wherever the meaning is not universally understood.
Give standalone icon buttons an accessible name.
Don't
Mix icon sets, recolour icons to arbitrary hues, or use an icon as the only
indicator of state.
3.6 - Forms
Input sizing, labels, focus, error, and disabled states.
Sizing
| Size | Height |
|---|
| sm | 32px |
| md | 40px — the default |
| lg | 48px |
All inputs are set in Source Sans 3 at 13px.
Sizes — sm 32 · md 40 · lg 48
States
| State | Treatment |
|---|
| Default | slate-7 border |
| Focus | midnight-9 border + 2px focus ring, midnight at 15% alpha, 2px offset |
| Error | orange-9 border + orange-1 background |
| Disabled | slate-4 background, slate-8 text, cursor: not-allowed |
Labels
Labels use the overline style and sit above the field. Floating labels
are not used anywhere in the system — they hide the label at exactly the moment
the user is filling the field, and they break at long label lengths.
.field__label {
font-size: 11px;
font-weight: 600;
line-height: 1.3;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--color-secondary);
}
Validation
- Error text sits below the field, in
orange-11 on light surfaces. - Never rely on border colour alone to signal an error — pair it with text.
- Validate on blur, not on every keystroke; re-validate on submit.
Do
Keep labels visible at all times. Give every field an associated <label> and
describe errors in words.
Don't
Use placeholder text as a label, signal errors with colour alone, or disable the
submit button without explaining what is missing.
3.7 - States
Empty, loading, and error states — the screens a component set never shows you.
A component set shows every part working. A product spends a lot of its life in
the three states where nothing is working yet: there is no data, the data
has not arrived, and the data will not arrive. Those three screens are
where a design system is actually tested, so they are specified here rather than
left to whoever hits them first.
Every state on this page follows the same shape:
What is true → why → the one thing to do about it.
If a state cannot name the one thing to do, it is an error state with an
optimistic title.
Empty states
Three kinds, and they are not interchangeable:
| Kind | When | The action |
|---|
| First run | The feature works; nothing has been created yet | The accent button that creates the first one |
| Filtered to nothing | There is data; the current filter excludes all of it | Clear the filter — and show what the filter is |
| Nothing to report | Empty is the good outcome — no open findings | No action. Say so plainly |
The first-run empty state is the only one that gets an accent button — it is the
only one where creating something is what the user came to do. “Filtered to
nothing” gets a ghost button, because the fix is to undo, not to create.
First run, filtered to nothing, nothing to report
No pipelines yet
A pipeline runs your policy checks before a deployment is promoted.
No pipelines match
48 pipelines exist. None are both strict and failing.
No open findings
The last audit completed 4 minutes ago and found nothing.
Healthy
An empty state is body text, not an illustration. The system’s answer to a
blank screen is a diagram or nothing,
never a mascot or a spot illustration of a person holding a magnifying glass.
Never show an empty state you are not sure about
“No results” while a request is still in flight is a lie the user acts on. A
region is in the loading state until the response arrives, and only then
resolves to empty, populated, or error. Empty is a result, not a default.
Loading states
Two mechanisms, chosen by what you know about the shape of what is coming:
| Use | Why |
|---|
| Skeleton | You know the layout — a table of rows, a card grid, a KPI row | The layout does not jump when content arrives |
| Spinner | You know nothing — an action in flight, an indeterminate wait | There is no shape to promise |
Prefer the skeleton. A spinner in the middle of a region that is about to become
a table tells the user nothing and then reflows the page underneath them.
A skeleton is midnight-2 blocks at the real dimensions of the content,
with the real gaps and the real number of rows where that is known. It carries a
100ms shimmer at --duration-standard, and it does not shimmer at all under
prefers-reduced-motion: reduce — it simply sits there as static blocks.
Skeleton — real dimensions, real row count
Spinner — indeterminate work, with its label
Applying changes…
Timing rules, so the loading state does not become its own flicker:
- Under 200ms — show nothing. A skeleton that appears and vanishes is worse
than a brief pause.
- 200ms to 10s — skeleton or spinner.
- Over 10s — a determinate
pj-progress bar with a count (“3 of 12 checks”),
or the state moves to a background job with its own row in the list.
Every loading region carries role="status" and an accessible label, or a
screen-reader user gets silence where a sighted user gets motion. Never animate
a skeleton in a way that hides that the operation has stalled — a shimmer that
runs for two minutes says “working” when the truth is “stuck”.
Error pages
Full-page errors — 404 and 500 — are the brand’s worst-case first impression, so
they use the same page shell and the same voice as everything else. No apology
paragraph, no cartoon, no error code as a headline.
| 404 | 500 |
|---|
| Headline | “That page does not exist” | “Something failed on our side” |
| Body | What might have happened, in one sentence | What we know, and whether it is being worked on |
| Action | Back to a real destination + search | Retry, and a route to a status page or support |
| Blame | The link, not the reader | Us, explicitly |
The code (404, 500) appears as an overline above the headline, in the muted
foreground — findable when someone is reporting the problem, never the loudest
thing on the page.
404 and 500 — same shell, different obligation
Error 404
That page does not exist
The link may be out of date, or the pipeline may have been deleted.
Error 500
Something failed on our side
The request did not complete. Nothing was deployed. Reference req_8f2c14.
A 500 says what did not happen. “Nothing was deployed” is the sentence the
reader needs; “an unexpected error occurred” is the sentence they cannot act on.
If the outcome is genuinely unknown, say that instead — it is a different fact
and it changes what they do next.
Inline errors
Not every failure takes the page. A failure scoped to one region stays in that
region, so the rest of the screen remains usable:
- A field fails with
pj-input--error and a message below
it — never a tooltip, never colour on
the border alone. - A panel fails with a
pj-alert--danger inside the panel and a retry
control, while the surrounding page keeps working. - A background job fails into a row in the activity feed with a
pj-status --err dot and the word “Failed”.
Panel-scoped failure — the page keeps working
Agent activity
Could not load activity
The activity service did not respond. Everything else on this page is current.
Do
Say what is true, why, and the one thing to do. Use a skeleton wherever the
layout is known. Label every loading region for screen readers. Tell a reader
what did not happen when something failed.
Don't
Show “no results” before the response arrives, put an accent button on a
“filtered to nothing” state, headline an error with its status code, or let a
region-scoped failure take the whole page.
3.8 - Accessibility
Keyboard order, skip links, focus management across composed layouts, and the ARIA the component set requires.
Accessibility appears throughout this system as measurements — contrast ratios
on Colour, the 44px touch floor on
Responsive, the focus trap on
modals. This page covers what those cannot: order, announcement, and focus
— the properties that only exist once components are composed into a screen.
The target is WCAG 2.2 Level AA.
Reading and focus order
There is one order, and the DOM defines it. Keyboard order, screen-reader order,
and visual order must agree.
That makes source order a layout constraint, not a detail: write regions in
the order they should be read, and let CSS place them. The
page shell is therefore authored as
skip link → header → sidebar → main → complementary, and Grid puts the sidebar
on the left.
Never use order, row-reverse, or grid-area to change the sequence a
reader encounters. It moves the pixels and leaves the keyboard behind, which
is exactly the bug that a
stacked mobile layout exposes:
if the DOM is already in reading order, the collapse to one column is free.
Never set a positive tabindex. tabindex="0" puts an element in the
natural order and tabindex="-1" makes it programmatically focusable; anything
above zero creates a second, competing order.
Skip links
Every page begins with a skip link — the first focusable element in the DOM,
visually hidden until focused, then rendered as a normal control against the
midnight header.
One skip link per major landmark the keyboard would otherwise have to traverse.
For the page shell that is two: Skip to content and Skip to navigation.
A sidebar of thirty items in front of the content is thirty tab stops on every
single page.
<a class="pj-skip" href="#main">Skip to content</a>
<a class="pj-skip" href="#nav">Skip to navigation</a>
.pj-skip {
position: absolute;
left: var(--space-4);
top: calc(-1 * var(--space-9)); /* off-screen, still focusable */
z-index: 100;
padding: var(--space-2) var(--space-4);
min-height: 44px;
background: var(--color-primary);
color: #fff;
border-radius: var(--radius-md);
transition: top var(--duration-standard) var(--ease-out);
}
.pj-skip:focus { top: var(--space-4); }
The target must be able to receive focus: <main id="main" tabindex="-1">.
Without it, some browsers move the viewport but leave focus at the top of the
document, and the next Tab lands back in the navigation.
Landmarks
The page shell maps onto landmarks one-to-one. Use the elements, not role
attributes on divs:
| Region | Element | Notes |
|---|
| Sidebar | <nav aria-label="Sections"> | The label distinguishes it from any other nav |
| Header | <header> | The page title inside it is the <h1> |
| Content | <main id="main" tabindex="-1"> | Exactly one per page |
| Secondary panel | <aside aria-labelledby="…"> | Points at the panel’s own heading |
| Footer | <footer> | |
Two nav elements on a page need two different aria-labels. An unlabelled
pair is announced as “navigation, navigation”.
Headings are a real hierarchy, not a type ramp: one <h1> per page, no skipped
levels. If a heading is the wrong size, restyle it — do not renumber it.
Focus visibility
Focus is always visible. The system never sets outline: none without
replacing it with something at least as loud.
The focus ring is a 2px orange-10 (#cc4528) outline at 2px offset, drawn
with :focus-visible so a mouse click on a button does not leave a ring behind
while keyboard focus still does.
Focus ring — 2px accent, 2px offset, on both surfaces
Tab to see the real thing
:focus-visible {
outline: 2px solid var(--color-accent-solid);
outline-offset: 2px;
}
The offset matters: at 0 the ring sits on the control’s own border and
disappears against an outline button, whose border is already the accent.
Moving focus
Focus moves only in response to something the user did, and it always ends
somewhere they can see.
| Event | Focus goes to |
|---|
| Modal opens | The modal’s first focusable element, or its heading |
| Modal closes | The control that opened it |
| Drawer or menu opens | Its first item |
| A route changes | The new page’s <h1>, or <main> |
| A validation error appears on submit | The first invalid field |
| A row is deleted | The next row, or the region’s heading if it was the last |
Modals and drawers trap focus while open and close on Esc. Everything else
must not trap: a Tab inside a table, a card, or a tab strip leaves it again.
Deleting a row without moving focus leaves it on a detached node, which drops
the keyboard user back at the top of the document — a silent, common, and
entirely avoidable regression.
ARIA for the composed patterns
The components page shows tabs, navigation, status, and overlays as markup. This
is what each owes assistive technology once it is real.
Tabs
pj-tabs is a tab strip, so it needs the full pattern: role="tablist" on the
container, role="tab" with aria-selected on each tab, role="tabpanel" with
aria-labelledby on each panel. Arrow keys move between tabs; Tab leaves the
strip entirely — only the selected tab is in the tab order.
If arrow-key navigation is not implemented, do not use the tab roles. A
tablist that does not respond to arrow keys is worse than a list of links,
because it has promised behaviour it does not have.
Navigation
The current page in a pj-sidebar or pj-navbar is marked
aria-current="page". The 600-weight and the accent underline are the visual
half of that statement; aria-current is the other half, and neither substitutes
for the other.
Status and semantic colour
Every pj-status dot is paired with its text label. That rule appears on the
components page as a visual-design rule; it is also the accessibility rule, and
it is why the system has no bare-dot variant to reach for.
Live regions announce changes that happen without a user action:
| Region | Attribute |
|---|
| Loading region | role="status" + aria-label |
| Toast, non-urgent update | aria-live="polite" |
| Validation summary, failure | aria-live="assertive" |
| Progress bar | role="progressbar" with aria-valuenow / min / max |
Reserve assertive for things that interrupt correctly. An assertive region
that fires on every keystroke makes the page unusable with a screen reader.
Icons
An icon that repeats its label is aria-hidden="true". An icon that is the
control carries the accessible name:
<button aria-label="Security settings">. See
Icons.
Tables
A pj-table uses <th scope="col">, and scope="row" on the identifying
column. Sortable headers carry aria-sort="ascending" | "descending" | "none",
updated when the sort changes — the arrow glyph alone is not announced.
Multi-level headers need
scope="colgroup" on the group row.
Motion and preference
The motion rules are an
accessibility requirement, not a stylistic one. Everything animated is wrapped in
prefers-reduced-motion: no-preference, or neutralised under reduce.
Skeletons stop shimmering under reduce but stay visible — the loading state is
information, and removing it would remove the information along with the motion.
What to check before shipping a screen
- Tab from the address bar to the end. Focus is visible at every stop and the
order matches the visual order.
- The skip link is the first stop, and using it lands focus in
<main>. - Nothing is reachable only by mouse; nothing is reachable only by keyboard.
- Every image, icon button, and form control has an accessible name.
- Zoom to 200% and to 400%. Nothing is clipped; nothing scrolls in two
directions at once.
- Turn colour off — greyscale the screen. Every state is still distinguishable.
- Contrast every new pairing against its actual surface, not against white.
Do
Write regions in reading order and place them with CSS. Return focus to the
trigger when an overlay closes. Pair every colour signal with text. Label both
nav landmarks.
Don't
Use a positive tabindex, reorder content with order or grid-area, remove a
focus outline without replacing it, or apply tab roles to a strip that does not
handle arrow keys.
4 - 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, Ghostty, iTerm2,
and Zellij.
4.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.
| Need | Canonical input | Do not do this |
|---|
| Colour values | brand/tokens/tokens.json | Copy arbitrary hex values into templates |
| CSS-friendly tokens | brand/tokens/variables.css | Create a second, competing token file |
| Logo and icons | brand/logo/ supplied SVG or raster variants | Redraw, recolour, crop, or stretch a mark |
| Type roles | Brand typography guidance | Substitute a decorative display face |
| Content hierarchy | This documentation content tree and front matter | Encode navigation order in CSS |
| Theme behavior | The selected theme’s current official documentation | Patch 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
- Use Plus Jakarta Sans for headings, Source Sans 3 for reading copy, and
IBM Plex Mono for code.
- 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. - Keep ordinary links blue. Accent colour is emphasis, not the default link
colour.
- Keep code surfaces dark in both light and dark modes. Syntax tokens must be
tested against that dark surface.
- Use quiet surfaces, token borders, and modest radii: 3px small, 6px normal,
9px large. Do not introduce gradients or oversized shadows.
- 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.
- 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.
| Token | Role | Light | Dark | Contrast (light / dark) |
|---|
--pj-canvas | App background — page, navbar, sidebar, TOC | #f8f9fb | #0e1720 | step 1 |
--pj-surface | Cards, panels, inputs — elevated above the canvas | #ffffff | #131e2b | step 2 dark |
--pj-surface-hover | Hovered rows and menus | #e2e9f2 | #1a2b3e | step 3 |
--pj-text | Reading copy | #142438 | #c5daf0 | 15.68 / 12.62 |
--pj-text-secondary | Navigation rest state, TOC, captions, metadata | #5c6f82 | #97a8b8 | 5.18 / 7.41 |
--pj-heading | H1–H6 | #1d3352 | #c5daf0 | 12.75 / 12.62 |
--pj-link | Inline and navigation links | #3a5a82 | #8aacc8 | 7.08 / 7.59 |
--pj-link-hover | Link emphasis only — accent as text, step 11 | #c04424 | #ea7558 | 4.87 / 6.18 |
--pj-nav-active-bg | Current-page tint | #d3deec | #20354d | step 4 |
--pj-nav-active-text | Current-page label | #1d3352 | #c5daf0 | 10.42 / 8.74 |
--pj-border | Divider: cards, tables, navbar, section rules | #cdd0d5 | #263f5a | 1.55 / 1.67 |
--pj-border-strong | Inputs and controls whose edge is the affordance | #546a82 | #4d7098 | 5.58 / 3.52 |
--pj-action | Primary action fill | #cc4528 | #cc4528 | 4.72 with white |
--pj-action-hover | Primary action hover fill | #b84228 | #b84228 | 5.46 with white |
--pj-action-label | Primary action label | #ffffff | #ffffff | — |
--pj-focus | Keyboard focus ring | #cc4528 | #f09878 | 4.72 / 8.15 |
--pj-code | Always-dark code surface | #0e1720 | #0e1720 | — |
--pj-code-text | Code foreground | #c5daf0 | #c5daf0 | 12.62 |
--pj-code-chrome | Filename bar and copy control on the code block | #131e2b | #131e2b | — |
--pj-info-bg / --pj-info-border | Informational | #e2e9f2 / #7490b2 | #1a2b3e / #4d7098 | 3.29 / 3.52 border |
--pj-success-bg / --pj-success-border | Completed | #d1ebe0 / #2f7d65 | #18382d / #6cc090 | 4.95 / 8.24 border |
--pj-warning-bg / --pj-warning-border | Needs attention | #f5ecd0 / #8b6508 | #3d1e13 / #e0a92a | 5.30 / 8.50 border |
--pj-danger-bg / --pj-danger-border | Blocked | #f9e3e1 / #a8261c | #3a1d20 / #f08b80 | 7.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.
| State | What it proves |
|---|
| Long documentation page | Reading measure, headings, TOC, tables, inline code |
| Page with a primary action | Accent contrast, hover, focus, disabled treatment |
| Code-heavy page | Dark code surface, syntax, copy affordance, overflow |
| Nested navigation | Current state, mobile menu, keyboard traversal |
| Search results | Query control, result hierarchy, empty state |
| Callouts and status | Semantic success, info, warning, danger states |
| Wide image and table | Responsive overflow and meaningful alternative text |
| Light and dark mode | Paired 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/.
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.
| Element | Required treatment |
|---|
| Navbar | Canvas colour at 85% with blur and a hairline underline — no fill of its own |
| Sidebar and TOC | Canvas colour, no panel fill; a hairline and whitespace do the separating |
| Footer | --pj-footer, supplied logo, and readable light text |
| Headings | Plus Jakarta Sans, 700–800; tight tracking above 20px |
| Body and navigation | Source Sans 3, 16px / 1.65 for long-form content |
| Code | IBM Plex Mono on an always-dark surface |
| Primary action | One accent-solid control, white label, 6px radius |
| Cards and callouts | 9px radius, token border, restrained shadow; semantic colours only |
| Light and dark | data-bs-theme palettes tested independently; never invert a screenshot |
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 token | Docsy / 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
| Area | Configure | Review |
|---|
| Logo | static/ assets and params.ui.navbar_logo | Correct lockup, clear space, no CSS recolour |
| Navbar | params.ui.navbar_theme, project SCSS | Light/translucent shell, compact height, accessible menu trigger |
| Sidebar | content tree, weight, compact/foldable params | Stable order, current-page state, keyboard traversal |
| TOC | heading hierarchy and tableOfContents | H2–H4 only; no skipped heading levels |
| Search | offlineSearch and sidebar search params | Search is discoverable and has readable results |
| Alerts | semantic Bootstrap roles | Warning is gold, never the identity accent; alert text is --pj-text, not a tinted foreground |
| Tables | project SCSS and Markdown tables | Horizontal overflow stays inside its container |
| Images | page bundles and Hugo image processing | Alt text, attribution, dark-mode treatment, no baked text |
| Footer | params.links, copyright, project partials | Project links, current year, dark-surface text contrast |
| Code | Hugo Chroma plus project CSS | Always 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
Release boundary
Validate the configuration locally, then request promotion. The action above
is the only primary action in this view.
What the reader needs first
| Area | State | Owner |
|---|
| Configuration | Validated | Platform team |
| Documentation | Reviewed | Technical writer |
| Release | Waiting for approval | Release 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
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
Validated
Use success only for a completed, evidenced check.
Needs attention
Warnings describe a condition to assess; do not use the brand accent as a
generic warning colour.
Blocked
Errors need a clear next action and must not rely on colour as their only cue.
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.
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.
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.
| Element | Required treatment |
|---|
| Shell | wide page frame with a focused reading column; light, translucent navbar in light mode |
| Typography | Plus 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 |
| Navigation | Content-tree sidebar, current item distinct without colour alone |
| Dark mode | system default and a visible toggle; paired light/dark logo and favicon |
| Footer and search | Quiet supporting UI, local/project-controlled assets where required |
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.
| Shared token | Hextra binding |
|---|
--pj-canvas | body, .hextra-nav-container, .hextra-sidebar-container, .hextra-toc |
--pj-surface | .hextra-card, .hextra-feature-card, .search-input |
--pj-surface-hover | sidebar and TOC link hover, .hextra-card:hover |
--pj-text | body, .content |
--pj-text-secondary | sidebar and TOC rest state, .hextra-toc headings |
--pj-heading | .content h1–h4 |
--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.
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
| Area | Configure | Review |
|---|
| Logo | params.navbar.logo, files in static/images/ | Light/dark lockups, intrinsic dimensions, clear space |
| Favicon | static/favicon.svg and static/favicon-dark.svg | Correct asset in each colour mode |
| Navbar | params.navbar, menu.main, custom CSS | Product title, search, theme control, mobile trigger |
| Sidebar | content tree, front matter, menu.sidebar | Weight order, current marker, no duplicate utility links |
| TOC | heading structure, toc: false only when needed | Accurate H2–H4 outline, useful at desktop width |
| Search | params.search | Clear input, keyboard use, relevant result excerpts |
| Cards and grids | Hextra shortcodes and public CSS classes | 9px radius, restrained shadow, semantic content grouping |
| Code and copy | Chroma plus .hextra-code-block | Always dark, readable syntax, visible copy result |
| Images | page bundles, imageZoom only when helpful | Descriptive alt text, no text baked into screenshots |
| Footer | i18n/en.yaml and optional footer partial | Copyright, project links, dark text contrast |
| Scripts | local assets/ paths where practical | No 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.
| Component | Expected branded result |
|---|
| Navbar | Compact and dark-aware; supplied logo, search, and theme toggle |
| Reading column | Calm, readable Source Sans 3 body copy with Plus Jakarta Sans headings |
| Action | One 40px-minimum accent-solid button with white text and visible focus |
| Cards | Quiet border, 9px radius, subtle elevation, blue text links |
| Code | IBM 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.
4.2 - Terminal
The projectious.work terminal palette and the configuration that applies it to tmux, WezTerm, Kitty, Ghostty, 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.
| Layer | Owns | Examples |
|---|
| Emulator | The sixteen ANSI colours, background, foreground, cursor, selection, its own tabs and splits | WezTerm, Kitty, Ghostty, iTerm2, Windows Terminal |
| Multiplexer | Only its own chrome — status bar, pane borders, message line, copy mode, popups | tmux, 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.
Zellij is the exception worth knowing
Zellij is a multiplexer, but its theme definition also declares the sixteen
colour names it paints its own UI with. It still does not change the ANSI
palette programs receive — that remains the emulator’s. Set both.
| Need | Canonical input | Do not do this |
|---|
| The sixteen colours | The ANSI table on this page | Import a third-party scheme and rename it |
| Surface, cursor, selection | The chrome table on this page | Let the emulator keep its default background |
| Monospace face | IBM Plex Mono | Substitute a different mono face for the brand |
| Multiplexer chrome | The tmux / Zellij sections below | Style a status bar in colours not on this page |
Non-negotiable rules
- The background is
midnight-dark-1 (#0e1720). The terminal has one
surface and it does not follow a light mode. - 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.
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.- Use IBM Plex Mono. Enable its ligatures only if the team has agreed to them;
they change how operators read in diffs.
- 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. - 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.
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
Syntax in the terminal
An editor running inside a terminal paints code from the sixteen ANSI slots, not
from a stylesheet. Since the syntax roles were
reassigned by measured perceptual distance, seven of the nine now resolve to an
ANSI slot exactly — so a file open in Helix, Neovim or Vim under this palette
looks like the same file on the documentation site.
| Syntax role | Value | ANSI slot |
|---|
| Plain and variables | #c5daf0 | 15 · bright white |
| Keywords and modifiers | #d491b4 | 13 · bright magenta |
| Types and classes | #6cc090 | 10 · bright green |
| Functions and methods | #e0a92a | 11 · bright yellow |
| Decorators and macros | #74c0c9 | 14 · bright cyan |
| Operators and punctuation | #97a8b8 | 7 · white |
| Invalid and deprecated | #e55b5b | 1 · red |
| Strings | #ea7558 | — brand orange-dark-10 |
| Numbers and constants | #f09878 | — brand orange-dark-11 |
| Comments | #72889d | — dedicated token |
Strings, numbers and comments are the three that do not map. ANSI has no orange
slot — orange is the brand’s accent family — and comments need a value dimmer
than any slot provides while still clearing 4.5:1. An editor that supports
truecolour should be given those three literally; one limited to sixteen colours
should use bright red for strings, red for numbers, and bright black for
comments, accepting that the last of those falls below the text floor.
This convergence was not designed for; it fell out
The syntax roles were reassigned to fix a legibility problem — keywords and
operators measured ΔE2000 5.2 apart, which is the same colour for reading
purposes. Because the only hues available were the ones the terminal palette had
already added to the system, the fix pulled the web theme onto the ANSI slots.
Worth noticing: a constraint that looked like a limitation produced the
coherence.
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.
Ghostty
Ghostty reads a plain key = value file and supports named themes, so the
palette is a separate file that the config selects. That is the idiomatic split:
the theme file carries only colour, and the config carries everything else.
# ~/.config/ghostty/themes/projectious — colour only, nothing else
palette = 0=#0e1720
palette = 1=#e55b5b
palette = 2=#3f9d74
palette = 3=#c08a1e
palette = 4=#6289b3
palette = 5=#bd6d96
palette = 6=#3f97a3
palette = 7=#97a8b8
palette = 8=#2e4b68
palette = 9=#f08b80
palette = 10=#6cc090
palette = 11=#e0a92a
palette = 12=#8aacc8
palette = 13=#d491b4
palette = 14=#74c0c9
palette = 15=#c5daf0
background = #0e1720
foreground = #c5daf0
cursor-color = #e05232
cursor-text = #0e1720
selection-background = #20354d
selection-foreground = #c5daf0
# ~/.config/ghostty/config — projectious.work
theme = projectious
font-family = IBM Plex Mono
font-size = 13
font-feature = -liga
font-feature = -calt
cursor-style = block
cursor-opacity = 1
# The accent marks the focused split, and nothing else in the chrome.
split-divider-color = #7b8da3
unfocused-split-fill = #0e1720
window-padding-x = 6
window-padding-y = 6
window-padding-balance = true
link-url = true
theme also takes an absolute path, which is the better form for a config
checked into a dotfiles repository: theme = /Users/you/dotfiles/ghostty/projectious.
Ghostty’s theme accepts a light:…,dark:… pair that follows the system
appearance. Do not use it here. This palette has one surface and every ratio
on this page is measured against it; a light variant would be a different
palette, not a mode of this one — see
Colour.
Four defaults have to be set explicitly, because each one moves rendered colour
off the measured palette:
| Setting | Required | Why |
|---|
minimum-contrast | 1 | Anything above 1 lets Ghostty rewrite a foreground to reach a ratio it computes itself. The palette already clears 4.95:1; this would replace measured values with generated ones |
background-opacity | 1 | Below 1 puts whatever is behind the window into every ratio on this page |
background-blur | false | Only applies under transparency, and blurring the desktop behind the text does not make the ratio measurable again |
unfocused-split-opacity | 1 | Ghostty fades unfocused splits by default. The faded text is still text, and at the default it no longer clears the floor |
minimum-contrast = 1
background-opacity = 1
background-blur = false
unfocused-split-opacity = 1
Leaving unfocused-split-opacity at its default is the most common way this
theme fails review: the split you are not looking at is the one you are
reading a stack trace in.
Ghostty has no bold-brightening setting to disable — it renders bold as a bold
face and leaves the colour alone, which is the behaviour the other emulators on
this page need to be told to adopt.
Check the parsed result rather than the file, since an unknown key is skipped
rather than reported:
ghostty +show-config | grep -E 'palette|background|foreground|contrast|opacity'
ghostty +validate-config
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:
| Setting | Required | Why |
|---|
| Minimum contrast | 0 | Non-zero silently rewrites foreground colours, so the measured palette is not what renders |
| Brighten bold text | off | Repaints normal colours as brights and collapses the two halves of the palette |
| Smart cursor colour | off | Overrides the accent cursor with a computed colour |
| Transparency / blur | 0 | Puts 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.
| State | What it proves |
|---|
git diff with colour | Red and green are distinguishable and neither is the accent |
ls --color on a mixed directory | Blue, cyan and green separate at a glance |
| A failing command’s stderr | Red reads as an error, not as brand emphasis |
man or a pager | Bold, underline, and dim all stay readable |
| Split panes, one active | The active border is the only accent element |
| Copy / visual mode | Selection 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 output | No 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.
5 - Portfolio presentation
Truthful status, social-preview, diagram, and demo patterns.
Portfolio presentation must make maturity and limitations easier to see, not
make prototypes appear more complete than they are.
These patterns are Projectious organization assets. Bernhard Gerlach’s
personal LinkedIn profile is a separate identity: use his name, portrait, and
first-person voice only with his approval. Do not turn a personal profile into
an organization-brand channel or imply personal endorsement.
5.1 - Status treatments
Text-first maturity labels for truthful portfolio presentation.
Required pattern
Every project presentation must show a status as text. Color, icons, position,
and motion may reinforce the label but must never replace it.
| Status | Use when | Supporting cue |
|---|
| Usable project | Documented users can complete the stated core task. | Solid dot |
| Working prototype | A real implementation runs, with known limits. | Half-filled dot |
| Applied research | Evidence or experiments are the primary output. | Diamond |
| Supporting asset | The repository enables another project. | Square |
| Idea / implementation starting | Scope exists; implementation is absent or partial. | Ring |
| Archived | Work is preserved but no longer maintained. | Horizontal bar |
Do not substitute broad claims such as “platform”, “production-ready”, or
“enterprise” unless the owning repository publishes evidence for that claim.
Prefer narrow artifact categories: CLI, process layer,
applied research, prototype, library, or supporting asset.
Accessibility
- Keep the full status label visible at all viewport sizes.
- Pair every color with the status-specific shape in the table.
- Use normal text contrast of at least 4.5:1 and large-text/UI contrast of at
least 3:1.
- Test light and dark variants independently.
- Do not encode a status change with animation alone.
The source templates use high-contrast neutral text and reserve accent color
for a supporting rule. Maintainers must rerun local validation after changing
colors.
Project examples
Examples are prompts for maintainers, not permanent factual claims. Confirm
each status and limitation with the owning repository before publishing.
| Project | Narrow category | Example status line | Required limitation |
|---|
| aibox | CLI | Usable project | State supported hosts and providers. |
| processkit | Process layer | Working prototype | State current API/version constraints. |
| ai-market-research | Applied research | Applied research | State dataset date and methodology limits. |
| KubeClaw | Prototype | Idea / implementation starting | State which flows are implemented. |
5.2 - Social previews
Repository artwork that states category and maturity plainly.
Content model
Every 1280×640 preview contains only:
- Projectious organization mark and name.
- Project name.
- One narrow artifact category.
- One complete maturity label from the status vocabulary.
- An optional short limitation, never a marketing claim.
The editable source is
brand/portfolio/social-preview-template.svg. Replace bracketed text, keep
the status label intact, and save a project-specific SVG beside the template.
Examples for aibox, processkit, ai-market-research, and KubeClaw demonstrate
the content structure; they are not evidence of current maturity.
Export and validation
Run:
npm run portfolio:validate
npm run portfolio:export
The export script uses resvg when available and otherwise leaves the SVG
source as the canonical reusable asset. Commit generated PNGs only when a
repository needs them.
Source and generated exports use this repository’s split license. The
Projectious name and marks remain subject to TRADEMARK.md. Record any
third-party typeface, icon, logo, or screenshot in brand/PROVENANCE.md
before use. The supplied templates contain no third-party logos or
screenshots.
5.3 - Diagrams and demos
Evidence-first architecture, flow, screenshot, and demo framing.
Architecture and flow diagrams
- Prefer left-to-right flow, explicit boundaries, and named protocols.
- Distinguish implemented components from planned components with text:
implemented, external, or planned.
- Add a caption with repository, source path, commit or release, and capture
date.
- Never infer architecture from a social preview or marketing page.
- Keep editable source beside the export and document the export command.
Suggested caption:
Source: owner/repository, docs/architecture.svg, release vX.Y.Z,
captured YYYY-MM-DD. Planned elements are labelled.
The three diagram types
“Prefer diagrams over photographs” is only useful if the diagrams exist.
Consulting deliverables need three, and they answer different questions:
| Type | Answers | Shape |
|---|
| Sequence | When — what happens, in what order, between whom | Actors across the top, lifelines down, labelled messages between them |
| Architecture | Where — what runs, and on whose infrastructure | A control plane above, deployment targets below, connected by plain rules |
| Org chart | Who — who owns the work, and who they answer to | One root, a rule, direct reports beneath |
Diagram gallery — all
three, built from the brand tokens.
The drawing rules are the same in all three, and they are deliberately spare:
- Lines, not arrows, unless direction is the point. A sequence diagram’s
messages are directional and get an arrowhead. An architecture diagram’s
connections usually are not — a 1px
slate-5 rule says “connected” without
claiming a flow that may not exist. - One accent per diagram, at most. The accent marks the focal node — the
thing the surrounding paragraph is about. A diagram where three nodes are
orange has no focus.
- Boxes carry a fill or a border, never both plus a shadow. Solid
midnight-9 for the emphasised node, surface-2 with a slate-5 border for
the rest. - Labels are 12–13px and horizontal. No rotated text. A diagram whose labels
need a head-tilt is a diagram that needed a different layout.
- Monospace for anything that is an identifier — a service name, a region, a
hostname — and the body face for anything that is prose.
An org chart names roles, and names people only with their agreement. The
portfolio identity rules apply: a chart is a
public artefact, and a person’s name in one is a disclosure about them.
Screenshots and demos
- Show real output from a named build, commit, or release.
- Frame the output without modifying product state or hiding errors.
- State sample/synthetic data clearly.
- Do not fabricate dashboards, terminal output, customer logos, or product UI.
- Crop personal data, tokens, account identifiers, and unrelated windows.
- Record capture command, viewport, theme, and date in provenance.
When there is no real output yet, use a text-first project card labelled
Idea / implementation starting. A truthful absence is better than a
fictional interface.
5.4 - Documents
The one-page document pattern — CV, proposal, memo — and how it paginates.
A document is not a screen that happens to be printed. It has a fixed page box,
no hover state, no dark mode, and one shot at being legible on a printer nobody
tested. This page defines the pattern the system uses for short, laid-out
documents, and links the worked example.
Longer, flowing documents — reports and papers — use the LaTeX and Typst
templates in
brand/templates/
instead. The split is about pagination, not length; see below.
Two kinds of document, and the choice is made before any layout happens:
| Flowing | Explicitly paginated |
|---|
| Content | One continuous text flow | A fixed set of designed pages |
| Pages | However many the text needs | A number you chose |
| Paper | Reflows onto the reader’s paper | Designed to a fixed page box |
| Examples | Report, memo, letter, paper | CV, proposal, brief, certificate |
| Route | LaTeX / Typst templates | The pattern on this page |
Getting this wrong is expensive in one direction: a flowing document forced into
fixed pages breaks the moment a paragraph grows, while a designed page that is
allowed to reflow loses the arrangement that was its whole purpose.
Never pin a paper size on a flowing document. Letter and A4 differ, the
reader’s printer knows which one it has, and the text should reflow onto it. A
paginated document does pin a page box, and each page is designed to fill Letter
and A4 alike without overlap.
The one-page document
Header, summary, body sections, and a footer region — with the whole thing
constrained to fit a single page rather than allowed to run onto a second.
| Region | Treatment |
|---|
| Header | Name at display size in Plus Jakarta Sans 800; role and contact beside it; a 2px midnight-9 rule under the whole band |
| Summary | One paragraph at 14.5px. Three sentences at most |
| Sections | An accent overline as the section marker, then the content. No boxes, no rules between entries |
| Two-up region | Where two short sections sit side by side — skills and education, or scope and budget |
| Footer region | The evidence: selected engagements, references, links |
The section markers are the only accent on the page, and they are text. A
document has no primary action, so there is nothing for a solid accent to mark —
an accent-filled band on a CV is decoration claiming to be emphasis.
Entries are separated by space, not by rules. A one-page document accumulates
horizontal rules faster than anything else in the system, and each one costs a
line of content while adding a section break to a document with one section per
heading — the same argument the
signature makes, on a bigger
sheet.
Dates and other data are set in IBM Plex Mono and right-aligned against the
entry title, so a reader scanning chronology has a column to scan.
Fitting one page
The constraint that makes this pattern hard is that the content has to end
where the page does.
- Cut content, not type. 14.5px body and 13.5px entries are the floor. A
document set at 11px to fit is a document that will not be read.
- Cut the oldest entry first. A CV is not an archive; a proposal is not a
contract.
- The two-up region is the pressure valve. Two short sections side by side
recover roughly a third of a page against stacking them.
- Check it at 100%. A browser zoomed to 90% will happily show you a page
that overflows in print.
Exporting
The example is HTML on the paged-document shell; the export route is the
browser’s print dialogue, to PDF.
- Print backgrounds on, margins default, headers and footers off —
the browser’s own header would print a URL across the top of a CV.
- Send the PDF, not the HTML. The HTML depends on a font load and a runtime; the
PDF depends on nothing.
- Check the PDF at 100% and printed on plain paper before it goes anywhere.
Reusing the pattern
The resume is the worked example, but the pattern is not resume-specific. A
one-page proposal, a project brief, and a decision memo are the same document
with different section names:
| Document | Header | Sections | Footer region |
|---|
| CV | Name and contact | Experience · Skills · Education | Selected engagements |
| Proposal | Client and date | Scope · Approach · Timeline · Price | Assumptions and exclusions |
| Memo | Subject and date | Context · Options · Recommendation | Decision and owner |
Where the document makes a claim about a project’s maturity, the
status vocabulary applies — a document
is not exempt from it because it is prose.
Do
Decide flowing or paginated before laying anything out. Keep the accent to the
section markers. Separate entries with space. Export to PDF and check it
printed.
Don't
Shrink type to make content fit, pin a paper size on a flowing document, add
rules between entries, or send the HTML in place of the PDF.
6 - Media
Motion, audio, video, photography, and presentation guidance.
Media guidance covers everything with a time dimension or a photographic
surface — the places where the system meets material it does not fully control.
6.1 - Audio and video
Sonic identity and video production rules.
Audio identity
Four defined cues. All are short, tonal, and mid-register — the audio equivalent
of “slides into place, doesn’t bounce”.
| Cue | Sound | Length |
|---|
| Notification | Short tonal chime, mid-register | 200ms |
| Success | Ascending C→E, warm sine wave | — |
| Error | Single low A2 | 300ms |
| Podcast intro | Ambient pad, tension→warmth resolve | 3s |
Error is a single low note, not a buzz or a descending pair — it marks the event
without scolding.
Video rules
- Demos run at real time. No fast-forward, no speed ramps. If a step is
slow, that is information the viewer needs.
- Transitions are a cut or a 200ms fade. No swoops, wipes, or 3D moves.
- All video uses midnight surfaces.
- Subtitles are always available.
- Export at 1080p minimum, 4K when possible.
Do
Show the real thing at real speed. Caption everything.
Don't
Speed up a demo to hide latency, or use a transition that draws attention to
itself.
6.2 - Photography and illustration
Sourcing rules, treatment, and the abstract illustration style.
Sources
Preferred: Unsplash and Pexels — royalty-free, no attribution required for
commercial use. Commission custom photography where possible, from local
European photographers.
Never use: watermarked Shutterstock or Getty imagery, or AI-generated
imagery.
Every photograph must pass the test: “would a developer trust this?” Stock
imagery of handshakes in glass lobbies fails it. A real desk, real hardware, or
real people working passes it.
Treatment
- Photographs in dark mode receive a subtle black overlay so they do not punch a
hole in the page.
- The logo never sits directly on an unmodified photograph — see
Logo usage.
- Never place text over a busy region. Add a scrim or crop to a quiet area.
Illustration
The illustration style is abstract system diagrams — nodes, flows, and
pipelines drawn in the brand palette.
- Midnight for structure, slate for supporting lines, accent for the single
element that matters.
- Dashed strokes indicate inferred or optional relationships.
- Labels use Source Sans 3; never letter-space diagram labels.
- Diagrams are line-based, not filled — the system reads as engineering
drawing, not infographic.
Do
Draw the actual architecture. Use accent once per diagram, on the element the
reader should look at first.
Don't
Use isometric 3D illustration, gradient-filled shapes, or generic “tech” clip
art.
6.3 - Presentations
Slide templates, animation, and the core deck structure.
Core slides
Six of the twelve slide types cover most decks. Build with these first and reach
for the other six only when the content actually calls for one — the full table
is below.
Core slide types — 16:10
Redesigning work
Cloud · Agile · Agentic AI
projectious.work
Bad software is a decision, not a constraint.
The agent-first approach
Define
Build
Deploy
"We run what we recommend."
// pipeline
createPipeline({
policy: "strict"
});
Let's talk
projectious.work
The full deck
Twelve slide types cover a complete talk. Each is 16:10 and uses the same
tokens as the product surfaces — no separate “presentation theme”.
Opening — title, agenda, section statement
Redesigning work
Cloud · Agile · Agentic AI
Agenda
01 Why agent-first
02 Architecture
03 Operating model
04 What it costs
Bad software is a decision, not a constraint.
Content — three-up, split, comparison
The agent-first approach
Define
Build
Deploy
Before
Manual gates, tribal knowledge, slow feedback.
After
Policy as code, repeatable runs, fast feedback.
Where the time goes
| Review | 42% |
| Build | 31% |
| Deploy | 27% |
Evidence — metric, quote, code, diagram
"We run what we recommend."
// pipeline
createPipeline({
policy: "strict"
});
Architecture
Closing — CTA and contact
Let's talk
projectious.work
Thank you
info@projectious.work
The twelve types
Core marks the six that cover most decks. Example marks the types the
worked deck below actually demonstrates.
| Slide | Core | Example | Purpose |
|---|
| Title | ● | ● | Deck opening — display type on midnight |
| Agenda | | ● | Numbered outline; current item in midnight-11 |
| Section statement | ● | ● | A single sentence marking a new section |
| Three-up | ● | ● | Three parallel points |
| Split | | | Before/after or contrast, one half on midnight |
| Comparison | | ● | A small table where numbers are the point |
| Metric | | ● | One number, large; the label in overline style |
| Quote | ● | ● | A single quotation with the accent rule, attributed |
| Code showcase | ● | ● | Code on the always-dark surface |
| Diagram | | | Line-based system diagram, accent on the focal node |
| CTA closer | ● | ● | The one thing you want the audience to do |
| Contact | | | Thank-you and a single contact route |
A real deck
Slide deck example — a full
client deck at 1920×1080, populated with a realistic consulting narrative rather
than lorem text. Arrow keys, PgUp/PgDn, and Space navigate; each slide
carries its speaker notes in the source.
The specimens above are scaled-down divs that show the shape of each type.
The example is the thing a reader can open, present from, and export.
It runs eleven slides and demonstrates nine of the twelve types, plus two
compositions that are worth naming because they look like new types and are
not:
- Roadmap — a horizontal timeline of the engagement’s weeks. It is a
Three-up stretched to four steps along a rule: same parallel-points structure,
same tokens, one axis added.
- Metric row — four stat cards across the slide. That is the
KPI row from the interface, used
on a slide. The single-number Metric type is for the one number a section
is about; the row is for the four that summarise an engagement.
Neither is a thirteenth slide type. If a deck needs an arrangement that is not in
the table, compose it from the parts that are. A composition earns a place in the
table by turning up in several decks and being written down — not by being used
once.
Split, Diagram, and Contact are not in the example deck; their specimens are
above.
Rules
- 16:10 aspect ratio.
- Title slides and section statements use midnight surfaces; content slides use
white or
midnight-1. projectious.work is visible on the title and closing slides.- One idea per slide. If a slide needs a paragraph, it needs to be two slides.
Animation
- Slide transitions: cut or 200ms fade.
- Build animations use
--ease-out at --duration-standard. - Never animate text character-by-character.
- Motion is for revealing structure, not for holding attention.
Templates and export targets
Document templates for LaTeX and Typst are in
brand/templates/.
Both embed brand identity and fall under the brand-asset licence terms — see
Licensing.
Those two are for documents. A deck usually has to arrive somewhere a client
can open and edit it, so Google Slides, PowerPoint, and Keynote are
first-class targets, not a downgrade from the HTML deck. The system survives
the trip because it asks very little of the format: three fonts, three colour
families, 16:10, and no effects. Set up a client-facing deck once and the slide
types above are all reproducible as masters.
| Route | Use | Notes |
|---|
| HTML deck → present | Talks you give yourself | The example deck. Keyboard navigation and speaker notes included |
| HTML deck → print to PDF | Sending a deck to be read | Landscape, background graphics on, margins none |
| Slides / PowerPoint / Keynote | Decks the client will edit | Build the twelve types as masters; do not rebuild them per deck |
| LaTeX / Typst | Documents, not decks | brand/templates/ |
Two things to fix in every native deck tool, because none of them default to the
system: replace the theme fonts with Plus Jakarta Sans, Source Sans 3, and IBM
Plex Mono (embed them, or the deck reflows on the client’s machine), and
replace the default palette with the brand hex values rather than leaving the
tool’s approximations of them in the theme.
Everything else is subtraction: no shadow presets, no gradient fills, no
transition other than a cut or a 200ms fade, no bulleted list where the type
table above offers a slide.
7 - Collateral
Artefacts that leave the product — cards, signatures, and social imagery.
Collateral is where the brand travels without its context. These artefacts are
often seen once, at small size, next to other companies’ material — so they lean
harder on the mark and the wordmark than in-product surfaces do.
7.1 - Business card
The digital-first vCard, shared by QR code or link.
The primary format is digital — a vCard shared via QR code or direct link,
not a printed card.
Digital vCard
projectious · workBernhard Gerlach
Cloud · Agile · Agentic AI
info@projectious.work
projectious.work
Paper stock
The same card on white. This is the form to hand a printer when a physical card
is required, and the one to reach for when the card will sit on a light surface
it does not control — a conference badge holder, a scanned page, a slide.
Paper card — white stock, accent on the leading edge
projectious · workBernhard Gerlach
Cloud · Agile · Agentic AI
info@projectious.work
projectious.work
Three things change, and nothing else does:
| Digital | Paper |
|---|
| Surface | midnight-dark-1 | White, in both modes — paper has no colour mode |
| Accent | A 12% wash bleeding off the corner | A 3px rule down the leading edge |
| Mark | Light-on-dark colourway | midnight-9 shell, white cut, orange-9 bud |
The wash does not survive the change of surface. At 12% on midnight it reads as
a soft light source; at 12% on white it reads as a printing fault — an uneven
patch of ink that a press will faithfully reproduce. So the accent moves to a
rule down the leading edge, which is one line, prints cleanly at any size, and
is the same device the
signature uses.
The name, role, and contact rows keep their type sizes and weights exactly. Only
their colours change, to the light-mode text steps: midnight-9 for the name,
slate-9 for the role at 5.58:1, and slate-11 for the mono rows at 5.18:1.
This specimen is white in both modes, deliberately
Switch the site’s theme and the digital card follows it; this one does not. A
card printed on white stock is white under every lighting condition there is,
and a specimen that goes dark when the reader’s browser does would be showing
something that cannot be printed.
Layout
- Midnight surface, mark top-left, name in Plus Jakarta Sans 700.
- Role in Source Sans 3 400, in
slate-dark-11. - Contact rows in IBM Plex Mono at 11px — addresses and handles are data, and
monospace makes them scannable and unambiguous.
projectious.work always present.- Accent used once, on the QR frame or the primary contact method.
Print fallback
If a printed card is required:
- 85×55mm, matte stock.
- Single-colour midnight on uncoated white, or reversed white on midnight.
- Use the monochrome mark — no accent on
press unless a spot colour is budgeted.
- Respect the 1× clear-space rule; do not bleed the mark to the trim edge.
Handing colour to a printer
The brand is defined in sRGB hex. Press is not sRGB, so a printed card needs the
values converted — and the conversion is the vendor’s job, not a lookup table’s.
Give the printer the Lab values. They are device-independent: they describe
the colour itself rather than one recipe for making it, so the vendor can hit
them on whatever stock and profile the job actually runs on.
| Colour | Hex | Lab (D50) | CMYK — unmanaged |
|---|
| Midnight | #1d3352 | L 20.7, a −0.5, b −21.9 | 65 / 38 / 0 / 68 |
| Orange | #E05232 | L 54.9, a 55.2, b 48.5 | 0 / 63 / 78 / 12 |
| Accent solid | #cc4528 | L 49.3, a 53.3, b 46.8 | 0 / 66 / 80 / 20 |
| Slate | #546a82 | L 43.7, a −3.7, b −16.2 | 35 / 18 / 0 / 49 |
| Midnight dark | #132440 | L 13.9, a 1.0, b −20.3 | 70 / 44 / 0 / 75 |
The CMYK column is a starting point, not a specification
Those numbers are the naive device conversion — the arithmetic one, with no
colour management. They are here so a proof is not blocked on a phone call, and
they are wrong on any specific press.
Convert through the vendor’s own ICC profile before production — FOGRA51 for
coated stock in Europe, GRACoL 2013 in North America — and sign off on a
physical proof on the actual stock. Matte and uncoated paper absorb ink and will
run darker and flatter than the screen, most visibly on Orange.
Do not guess a Pantone equivalent. If the job budgets a spot colour, give the
vendor the Lab value and let them specify the closest ink; a Pantone number
picked from a screen swatch is a different colour with an authoritative name on
it.
7.2 - Email signature
A signature that survives every mail client, carries the details business mail is required to carry, and stays quiet on a white message.
Email is not the web. Every value below is a literal, every layout is a table,
and nothing depends on a stylesheet the client is free to discard.
Two forms are supplied:
The signature
Full form — a white message is the design constraint
Jane Doe Principal Consultant Projectious GmbH Musterstraße 1, 10115 Berlin, Germany Projectious GmbH · Registered seat Berlin Amtsgericht Charlottenburg, HRB 000000 · VAT ID DE000000000 Managing director: Jane Doe This message is intended only for the addressee and may contain confidential
information. If you received it in error, please tell the sender and delete it. |
Short form — replies and internal mail
Jane Doe Principal Consultant · Projectious GmbH |
Both specimens are rendered on white, because that is where a signature
actually lives. The surrounding page has a colour mode; a mail message
generally does not.
One rule, and it is vertical
A signature accumulates dividers as it accumulates fields — one under the name,
another above the legal block, another before the disclaimer — and each reads as
a section break in a message that is not sectioned. On a white background the
result is a small form stapled to the bottom of a letter.
So there are no horizontal rules at all. One vertical accent rule runs down
the left of the whole block: it marks the signature as a unit, ties it to the
brand, and costs one line instead of four. Everything else is separated by space
and by type size.
Do
Group the fields — identity, contact, address, legal — and separate the groups
with about 9px of space. Let the type sizes do the ranking.
Don't
Add a rule between groups, a coloured band behind the name, social icon rows, a
quotation, or a “please consider the environment” line.
Fields
Replace every [PLACEHOLDER]. Delete any row that does not apply — the block is
built so that removing a row leaves no gap behind it.
| Field | Required | Notes |
|---|
| Full name | Always | 15px, 700, midnight-9 |
| Job title | Always | Its own line — a title merged into the company line reads as part of the company |
| Company legal name | Always | The registered name, not the trading shorthand, wherever the legal block appears |
| Phone | Recommended | With country code; a signature is often read from another country |
| Email address | Always | Even though it is in the header — messages get forwarded |
| Website | Always | |
| Postal address | Regionally required | Street, postcode, city, country |
| Register details | Regionally required | Legal form, registered seat, register court and number, VAT ID, directors |
| Confidentiality notice | Optional | Two lines at most; delete it unless your organisation requires it |
What the law asks for
A starting point, not legal advice — confirm against your own jurisdiction
before shipping a signature.
| Jurisdiction | Business email must carry |
|---|
| Germany | Legal form, registered seat, register court and number, and every managing director (§ 35a GmbHG, § 37a HGB) |
| EU / UK | Company registration number, registered office address, and VAT number where registered |
| United States | No statutory signature requirement; confidentiality notices are convention rather than law |
| Canada | CASL requires sender identification and a postal address on commercial messages |
| Australia | ACN or ABN on business correspondence |
Construction
| Rule | Why |
|---|
Table layout, role="presentation" | Flexbox and grid do not survive Outlook’s Word renderer; the role stops screen readers announcing a data table |
| White declared on table, row, and cell | A client repainting for dark mode decides what to invert per element. White on the cell alone leaves the table around it unpainted — see below |
Each background written twice, as bgcolor and background-color | The background shorthand alone is not enough: several clients strip shorthands and keep longhands, and Outlook’s renderer prefers the attribute |
| Inline styles only | Most clients strip <style>, and none support custom properties — so no var(--pj-*) anywhere |
| Web-safe font stack | Brand faces will not load in a mail client; the stack degrades to a system sans without changing the layout |
| Maximum width 520px | Below the 600px that forces horizontal scrolling in narrow reading panes |
| Literal hex values | The brand steps, written out — see the table below |
| No images | A hosted logo is blocked by default in many clients and leaves a broken frame. The accent rule carries the brand with nothing to load |
| No background images | If a fill is ever needed, use the bgcolor attribute |
Declaring white three times is not belt-and-braces
It is the difference between a signature that survives dark mode and one that
does not, and it is the most common way this block breaks.
<table … bgcolor="#ffffff" style="…background:#ffffff;background-color:#ffffff;…">
<tr bgcolor="#ffffff" style="background-color:#ffffff">
<td bgcolor="#ffffff" style="background:#ffffff;background-color:#ffffff;…">
A client that repaints a message for dark mode works element by element: it
looks at each one, decides whether it has a background it should keep, and
inverts the rest. Declare white only on the <td> and the <table> around it
has no background to keep — so the cell stays white while its own container is
repainted dark. The result is a white island with a dark halo, or worse, the
dark text inverted to light and then drawn on the white cell that was kept.
Every enclosing element that has a painted area needs the declaration, and each
one needs it in both forms: the bgcolor attribute, which Outlook’s renderer
prefers, and background-color, because several clients strip the background
shorthand while keeping longhands.
Dark mode will recolour this, and that is fine
Several clients — Outlook mobile and some webmail among them — invert or shift
signature colours in dark mode, and none can be reliably prevented from doing
so. The signature is therefore built to survive recolouring rather than to fight
it: the hierarchy is carried by size and weight as much as by hue, so it still
reads when the palette is altered.
Colours
Three families, which is what a signature should have.
| Role | Value | Use |
|---|
| Heading | #1d3352 | The name |
| Primary text | #142438 | Title, phone |
| Secondary | #546a82 | Company, address |
| Link | #3a5a82 | Email, website |
| Legal and notice | #546a82 | Register details, confidentiality notice |
| Accent | #E05232 | The vertical rule, and nothing else |
Before shipping a change
- Send to Outlook (Windows), Gmail web, Apple Mail, and one mobile client.
- Reply to that message twice and check how the signature looks quoted inside a
thread.
- Turn on the client’s dark mode and confirm the hierarchy still reads.
- Forward it, to confirm nothing depends on the original message’s styles.
7.3 - Email
A sendable message — the newsletter layout, its bulletproof button, and the constraints a signature does not have to solve.
The signature covers the
block at the bottom of someone else’s message. This page covers the message
itself: a full-width, sendable email with a header, sections, a call to action,
and a footer.
Everything the signature page says about construction — tables, inline styles,
web-safe stacks, literal hex, role="presentation" — applies here unchanged and
is not repeated. What follows is what a whole message adds.
It opens straight from the filesystem — no runtime, no build step — so it can be
pasted into a sending tool and previewed as-is.
Structure
600px, centred on a neutral surround, in this order:
| Band | Fill | Contents |
|---|
| Preheader | hidden | One sentence, ~90 characters — see below |
| Masthead | midnight-9 | The wordmark. Nothing else |
| Lede | white | Issue overline, headline, and a standfirst that says what is inside |
| Sections | white | Numbered overline, heading, two or three sentences |
| Call to action | white | One bulletproof button |
| Footer | midnight-9 | Identity, why they are receiving it, unsubscribe, postal address |
600px is the width every mail client can be relied on to render without
horizontal scrolling, and it is the reason the message is not laid out on the
1100px measure. Below
that, max-width:100% on the outer table lets the content reflow in a phone
client.
Sections are numbered — 01 · agent pattern — in the accent, in the overline
style. The number is the only ornament in the body: it gives a scanning reader a
structure to skip through without a rule, an icon, or a background tint per
section.
The first line of hidden text is what the inbox shows next to the subject line.
It is not optional and it is not a repeat of the headline:
<div style="display:none;max-height:0;overflow:hidden;opacity:0;">
Cloud · Agile · Agentic AI — this month: three agents worth stealing, and
why generic SaaS is losing its edge.
</div>
Leave it out and the client fills the space with whatever text comes first,
which is usually the wordmark followed by “View in browser”.
A styled <a> is not a button in Outlook’s Word renderer: padding collapses and
the fill shrinks to the text. So the call to action is built twice — a VML
rectangle inside a mso conditional comment, and a table cell with bgcolor
for everyone else.
<!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" href="https://…"
style="height:44px;v-text-anchor:middle;width:220px;"
arcsize="9%" fillcolor="#cc4528" strokecolor="#cc4528">
<w:anchorlock/>
<center style="color:#ffffff;font-family:Arial,sans-serif;font-size:14px;font-weight:bold;">
Read the case study
</center>
</v:roundrect>
<![endif]-->
<!--[if !mso]><!-->
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
<tr><td bgcolor="#cc4528" style="border-radius:4px;" align="center">
<a href="https://…" target="_blank" style="display:block;padding:13px 26px;
font-family:Arial,Helvetica,sans-serif;font-size:14px;font-weight:bold;
color:#ffffff;text-decoration:none;mso-line-height-rule:exactly;">Read the case study</a>
</td></tr>
</table>
<!--<![endif]-->
Three things are load-bearing:
- The fill is
#cc4528, not #E05232. White on the identity accent is
3.87:1; on accent-solid it is 4.72:1. The same rule as
solid controls in the interface,
and email is where it matters most, because a mail client will not offer a
hover state to compensate. - 44px tall. A mail is read on a phone more often than not, so the
touch floor is the binding
constraint.
arcsize="9%" approximates the 4px radius the HTML path sets. VML has no
pixel radius; it takes a percentage of the shorter side.
One button per message. A second call to action does not add a second chance —
it splits the first one.
Colour in a message
The signature’s colour table
holds, with two additions a whole message needs:
| Role | Value | Use |
|---|
| Body text | #142438 | Paragraphs — midnight-12, never #333 and never #000 |
| Section overline | #c04424 | The numbered section labels — orange-11, 5.13:1 on white |
| Button fill | #cc4528 | With white text, 4.72:1 |
| Footer text | #c5daf0 | On the midnight-9 footer, 8.90:1 |
| Footer link | #f0a48c | Unsubscribe, on midnight — 6.31:1 |
| Hairline | #e5e3de | Section dividers, as a 1px table row |
| Page surround | #f5f4f2 | Behind the 600px sheet. Not a system token — it is the neutral the standalone brand documents use, and it exists so the white sheet has an edge in clients that show one |
The accent appears as text in the overlines and as a fill on the one
button — and #E05232 itself appears nowhere carrying text, in either role.
Commercial mail has to say who sent it, why it arrived, and how to stop it.
Those three facts are the footer’s whole job:
- Who — the legal entity and its postal address.
- Why — one sentence: “Sent to you because you subscribed to Field notes.”
- How to stop — an unsubscribe link that works in one click and does not
require signing in.
An unsubscribe that leads to a login screen is, in most of the jurisdictions on
the signature page, not an
unsubscribe.
Before sending
- Send to Outlook (Windows), Gmail web, Apple Mail, and one mobile client. The
VML path only exists for the first of those, so it only gets tested there.
- Check the preheader in the inbox list, not just in the opened message.
- Turn images off. The message must still make complete sense — which is why
there are none in the layout to begin with.
- Turn dark mode on. Clients recolour aggressively; as with the signature, the
hierarchy is carried by size and weight so it survives.
- Click the unsubscribe link from a logged-out browser.
Do
Write the preheader. Keep the message at one call to action. Fill solid buttons
with accent-solid. State why the reader is receiving it.
Don't
Ship a styled <a> as the only button, use #E05232 behind white text, rely on
an image to carry the message, or hide the unsubscribe behind a login.
7.4 - Social and OG images
Templates for LinkedIn, GitHub, blog headers, and Open Graph cards.
Priority
LinkedIn and GitHub first, blog second, Twitter/X as backup. The templates
are sized for those surfaces in that order.
Template rules
- Backgrounds are midnight or midnight-dark, or white with an accent top
bar.
projectious.work is always visible.- Never place text over a busy background.
- An accent glow circle at subtle opacity provides visual interest without
competing with the text.
- Typography hierarchy is unchanged: Plus Jakarta Sans for headlines, Source
Sans 3 for body.
Open Graph card — 1200x630
Bad software is a decision,
not a constraint.
projectious.work · Cloud · Agile · Agentic AI
Open Graph
- 1200×630px.
- Headline at Display weight, capped at roughly 60 characters — longer headlines
are truncated by most platforms anyway.
- The mark sits bottom-left or top-left, never centred.
- Test the rendering at 320px wide: most impressions are in a mobile timeline.
Do
Keep one message per card. Check contrast against the actual background, not the
design canvas.
Don't
Fill the card with a screenshot, use more than one accent element, or rely on
text smaller than 24px at 1200×630.
7.5 - Service one-pager
The sales flier — one service, one page, one call to action.
The card, the signature, and the social artwork all carry the brand in a few
square centimetres. The one-pager is the opposite problem: a whole page,
handed over or attached, that has to explain a single service to someone who has
not asked yet.
One service per sheet
A one-pager describes one offer. A sheet listing four services is a
capabilities brochure, and it is read by nobody, because a reader who wants one
of the four has to find it first.
If there are four services, there are four one-pagers. They share the structure
below and differ only in their content — which is what makes the structure worth
specifying.
The bands
Four horizontal bands, top to bottom. The sheet is 1000px wide in the HTML
source and prints to a single A4 or Letter page.
| Band | Fill | Job |
|---|
| Header | midnight-9 | Wordmark, an overline naming the artefact, the service name at display size, and one sentence of promise |
| Benefits | white | Three columns: icon in a tinted tile, a short heading, two lines of body |
| Process | white | A numbered horizontal sequence — the steps, with a duration under each |
| Close | midnight-12 | The one-line commitment, the contact route, and one accent button |
Two midnight bands, at the top and the bottom, with white between them. The
sheet opens and closes on the brand and leaves the middle to the content — the
same shape as a title-and-closer
deck, for the same reason.
The header carries a single decorative element: an orange-9 circle at 6%
opacity bleeding off the top-right corner. That is the entire ornament budget for
the page.
Three benefits, not five
Three columns fit a 12-column grid at four columns each, three items are
readable at a glance, and three is roughly how many distinct reasons a person
retains from a page they were handed.
Each benefit is an icon tile, a heading of no more than five words, and body text
of no more than two lines. The icon is
Lucide at 20px inside a 40px tile
filled with the tag background — it is a marker, not an illustration.
The heading states the benefit, not the feature: “Self-hosted where it matters”,
not “Self-hosting support”.
The process band
A numbered sequence with a connecting rule, each step carrying a duration. The
durations are the point: a one-pager that says “we migrate your cloud” competes
on adjectives, and one that says “week 3: stand up self-hosted core” competes on
a plan.
The numbers are set in IBM Plex Mono inside midnight-9 discs, because they are
data. The connector is a 1px slate-5 rule, not an arrow — the numbers already
carry the direction.
Do not put a duration on the sheet that the engagement cannot hold. The
portfolio principle — make maturity
and limitations easier to see, not harder — applies to sales collateral without
modification.
One call to action
The close band gets one accent element. It is a button shape carrying white text,
so it fills with accent-solid (#cc4528) and not the identity accent — the
same 4.72:1 rule as the interface
and the email.
Beside it, exactly one contact route, in mono. Two routes make the reader choose
before they act.
Printing it
The example is HTML, and the export path is the browser’s print dialogue — the
same route as the resume
template.
- Colour is not guaranteed. A one-pager is frequently reprinted on an office
mono laser. The layout must survive greyscale: the bands separate by value,
the numbers stay legible, and no meaning is carried by the accent alone.
- Turn on background graphics in the print dialogue, or the midnight bands
print as white and the reversed text disappears.
- Check the fold. If the sheet will be folded, nothing important sits within
10mm of the fold line.
- For volume print, hand the vendor the
CMYK values rather than the
hex.
Do
Keep one service per sheet, three benefits, and one call to action. Put real
durations on the process steps. Check it in greyscale before it is printed.
Don't
List a service catalogue, use stock photography of people in meetings, add a
second contact route, or promise a timeline the engagement cannot hold.
8 - Governance
Licensing, trademark, the pre-launch legal assessment, and per-asset provenance.
This section states what you may and may not do with the material in this
repository. It is deliberately explicit: a design system that is
source-available without clear terms is a liability for everyone who touches it.
8.1 - Licensing
A split licence — brand assets are proprietary, code and tokens are MIT.
The repository carries two categories of intellectual property under
different terms. The full text is in
LICENSE.md;
this page summarises it.
Brand assets — proprietary, source-available
Everything representing the visual identity: logos, brand imagery, and the
design-system documents under brand/.
| |
|---|
| Permitted | Download and view for personal reference, editorial reporting, or authorised partner use |
| Prohibited | Modification, derivative works, sale, or commercial use — including merchandise or software featuring these assets — without prior written consent |
| No impersonation | Use must not imply endorsement, affiliation, or ownership by anyone other than projectious.work |
Document templates under brand/templates/ fall under these same terms: they
embed brand identity (colours, fonts, tagline). You may use them as structural
reference for your own templates, but not redistribute them with projectious.work
branding intact.
Code, scripts, and tokens — MIT
Source code, automation scripts, and configuration files (.py, .js, .sh,
.json, .yaml) are MIT-licensed: use, copy, modify, merge, publish,
distribute, sublicense, and sell, provided the copyright and permission notice
travel with the software.
The design tokens — brand/tokens/ — are in
this category. The values are free to use; the marks are not.
What this means in practice
Do
Use the token files, the spacing scale, and the component measurements in your
own work. Link to this documentation. Write about the brand editorially.
Don't
Ship the logo in your product, sell anything carrying the marks, or present
modified brand assets as projectious.work’s.
For commercial inquiries or brand consent: info@projectious.work
8.2 - Trademark
Guidelines for referring to projectious.work and using its marks.
The names, logos, and icons associated with projectious.work are its
trademarks. Full text:
TRADEMARK.md.
Not permitted
- Using the logo as your own, or as part of your own logo.
- Using the brand name in a way that suggests partnership or official status
without a signed agreement.
- Registering domain names, social handles, or app names that include the
trademarks in a confusingly similar way.
Permitted
- Linking to the official website.
- Press articles or blog posts about the project.
- Technical documentation that refers to the tools.
These are nominative uses — referring to the thing by its name. They do not
require permission, and they do not grant any broader licence.
When in doubt, ask
Unauthorised use of the trademarks may result in a request for removal.
Contact info@projectious.work before shipping anything you are unsure about
— asking first is always cheaper than a takedown.
8.3 - Legal assessment
Pre-launch clearance review of the name, mark, fonts, icons, and imagery sources.
A pre-launch review of the elements that carry legal risk. Source:
brand/html/projectious-legal-assessment.html.
Not legal advice
This is a design-side risk assessment, not a legal opinion, and it is not a
registered-trademark search. Obtain professional advice before relying on it for
a commercial launch.
Summary
| # | Area | Verdict |
|---|
| 1 | Company name “projectious” | Low risk |
| 2 | Logo similarity — stencil peony bud | Low risk |
| 3 | Font licensing | Low risk |
| 4 | Icon library — Lucide | Low risk |
| 5 | Photography and imagery sources | Low risk |
| 6 | Other design elements | Low risk |
Fonts
All three families are SIL Open Font License 1.1 and served from Google
Fonts. OFL permits commercial use, embedding, and redistribution; it forbids
selling the fonts on their own and requires that any derivative font use a
different name. Since the fonts are linked rather than bundled, no font binaries
are redistributed by this repository.
Icons — Lucide
ISC License, functionally equivalent to MIT: free commercial use,
modification, redistribution, sublicensing. No attribution required in product
UI — only the notice retained in source. Over 1,400 icons, actively maintained,
a fork of Feather with a broader community.
Photography
| Source | Licence | Cost | Attribution | Commercial | Restriction |
|---|
| Unsplash | Unsplash License | Free | Not required | Allowed | No reselling as-is; no competing stock service |
| Pexels | Pexels License | Free | Not required | Allowed | No reselling; no implying endorsement |
Both permit free commercial use without attribution for website imagery,
marketing, social posts, presentations, and blog posts. The binding restriction
is that you may not compile the images into a competing stock service.
Colour
Colours cannot be trademarked in the abstract — only in connection with specific
goods and services in specific contexts (Tiffany blue for jewellery being the
canonical example). The palette (#1d3352 midnight, #E05232 orange, #546a82
slate) uses common design colours and does not infringe any known colour
trademark in the IT consulting space.
Recommended actions before launch
- Run a registered-trademark search in the operating jurisdictions.
- Keep the asset provenance inventory
current as assets are added.
- Retain licence notices in source for all third-party dependencies.
8.4 - Asset provenance
Per-asset source, licence, and attribution status for every third-party dependency.
The authoritative inventory is
brand/PROVENANCE.md
in the repository. Check it before adding an asset or shipping collateral.
Fonts
Loaded at runtime from the Google Fonts CDN — no font files are bundled.
| Font | Licence | Attribution | Role |
|---|
| Plus Jakarta Sans | SIL OFL 1.1 | Not required | Headings, display |
| Source Sans 3 | SIL OFL 1.1 | Not required | Body, UI |
| IBM Plex Mono | SIL OFL 1.1 | Not required | Code, data |
Icons and photography
Neither is bundled in this repository. Both are cleared for future use:
| Source | Licence | Attribution | Commercial |
|---|
| Lucide Icons | ISC | Not required in UI | Allowed |
| Unsplash | Unsplash License | Not required | Allowed |
| Pexels | Pexels License | Not required | Allowed |
Original work
All logo files, design tokens, document templates, and design-system documents
are original work, © projectious.work, under the brand-asset terms in
Licensing.
Review status
Last reviewed 2026-07-26: no unlicensed, sensitive, or third-party binary
assets are committed to this repository. Every physically present file is
original.
Adding a bundled asset
If you commit a third-party binary — a font file, an icon SVG, an image — add a
row to brand/PROVENANCE.md recording its individual source, licence, and
attribution requirement. Do not rely on the summary tables above.
9 - Tokens
Machine-readable exports of the design values, and how to consume them.
The token files are the machine-readable form of the
foundations. They are MIT-licensed —
the values are free to use, the marks are not. See
Licensing.
Downloads
Source: brand/tokens/.
All three are generated from
src/data/brand.yaml
by scripts/build-tokens.mjs — the same file this documentation renders from,
so a page and a download cannot disagree about a value. scripts/check-tokens.sh
fails the build if the committed files drift from a fresh generation, or if
brand.yaml drifts from the SCSS that actually styles the site.
CSS
<link rel="stylesheet" href="variables.css">
.button--primary {
background: var(--color-primary);
border-radius: var(--radius-md);
padding: var(--space-3) var(--space-4);
transition: background var(--duration-standard) var(--ease-out);
}
What the tokens cover
Everything the foundations define, in all three files:
| Covers |
|---|
| Colour — named | The eight aliases: primary, accent (with accent-solid), secondary, and their light and dark variants |
| Colour — scales | All three 12-step scales, in both modes — 72 values |
| Colour — semantic | Success, warning, danger, info: the solid value, its tint, and the foreground to use on that tint, per mode |
| Surfaces | Page canvas, elevated surface, three text levels, border — per mode |
| Typography | Three font stacks, and the eleven-style type scale with size, weight, and line height |
| Spacing | The nine-step 4px scale |
| Radius | Five steps |
| Elevation | Four shadow levels |
| Motion | Four durations, two easing curves |
| Breakpoints | sm 640, md 768, lg 1024, xl 1280 |
| Terminal | The sixteen ANSI slots plus thirteen chrome values |
Two things the exports cannot do for you
Breakpoints are exported as values, not as queries. A custom property cannot
be used in a @media condition, so --breakpoint-md is available to calc()
and to JavaScript, but the query itself still has to be written out.
Tailwind has no notion of a colour mode, so the dark scales are exported as
their own midnightDark / orangeDark / slateDark keys rather than swapped in
behind dark:. Wire them to whatever dark strategy your project already uses.
Modes
variables.css declares the dark values twice: once under
@media (prefers-color-scheme: dark), and once under [data-theme="dark"]. A
theme toggle that only works when the operating system already agrees is not a
toggle, so both routes are present and the explicit attribute wins.
Consuming them in SCSS
This site maps the tokens onto Bootstrap and Docsy variables in
src/assets/scss/_variables_project.scss,
which is a worked example of wiring the brand into an existing component
framework.
The site is the test
This documentation is styled by those same SCSS files. If a token is wrong, this
page renders wrong — which is the point. There is no separate brand-site theme
that can drift away from the system it documents.