The system is disciplined, three-appearance, and quietly opinionated. It looks like
infrastructure: readable, repairable, and free of decorative noise. Colour
communicates meaning. Typography communicates role. Components remain ordinary
enough to understand without learning a private visual language.
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 · work
Bernhard 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 every appearance — 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 stays white in every appearance
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.
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.
Colour
Three 12-step scales, three appearances, semantic pairs, and the contrast rules that govern them.
The palette is three scales — midnight, orange, and slate — each
expressed as a 12-step ramp in light and dark variants. Semantic aliases map
those ramps into light, navy dark, and deep dark appearances. 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.
These are the named aliases most projects reach for first. They are shortcuts
into the scales, not a separate palette.
The light app background is midnight-1 (#f8f9fb), not white. White is the
raised surface. Links use midnight-11, never the accent. Colour carries
meaning: do not introduce bluish-purple gradients, rainbow categorisation, or
decorative colour families.
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 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 every
appearance.
The dark terminal is the default and every colour in it is measured against
midnight-dark-1. An optional light companion is available for explicitly
light code and terminal panels; it is a separate measured palette, never an
automatic colour-mode substitution. A terminal 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 each terminal palette is a companion to the three interface scales rather
than another scale: sixteen fixed ANSI slots plus chrome, derived from the ramps
and measured against its own background.
#
Name
Normal
On surface
Bright
On surface
Provenance
0
black
#0e1720
—
#2e4b68
2.00:1
midnight-dark-1 / midnight-dark-6 — Box drawing and rules, not text — deliberately below the floor.
1
red
#e55b5b
5.15:1
#f08b80
7.49:1
bright = danger-dark — Never the accent — an error and the brand must not look alike.
2
green
#3f9d74
5.41:1
#6cc090
8.24:1
bright = success-dark
3
yellow
#c08a1e
5.93:1
#e0a92a
8.50:1
bright = warning-dark — Gold, matching the warning role in the interface.
4
blue
#6289b3
4.95:1
#8aacc8
7.59:1
bright = midnight-dark-11
5
magenta
#bd6d96
4.98:1
#d491b4
7.32:1
terminal-only — The brand defines no magenta; this slot exists only here.
6
cyan
#3f97a3
5.31:1
#74c0c9
8.71:1
terminal-only — The brand defines no cyan; this slot exists only here.
7
white
#97a8b8
7.41:1
#c5daf0
12.62:1
slate-dark-11 / midnight-dark-12
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 the light danger solid,
#d92d20.
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.
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.
Role
Value
Measured
Background
#0e1720
the surface
Foreground
#c5daf0
12.62:1
Cursor
#e05232
4.67:1
Cursor text
#0e1720
4.67:1 on the cursor
Selection background
#20354d
—
Selection text
#c5daf0
8.74:1
Dim / comment
#72889d
4.93:1
Status bar surface
#131e2b
—
Status bar text
#c5daf0
11.74:1
Inactive tab and pane label
#7b8da3
4.95:1
Active tab fill
#e05232
4.67:1 with #0e1720 text
Active pane border
#e05232
4.67:1
Inactive pane border
#7b8da3
5.32:1
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.
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 identity accent is never body text. Light-mode accent text uses
orange-11 (#c04424); dark-mode accent text uses orange-dark-10
(accent-light, #ea7558). A solid control with a white label always uses
accent-solid.
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.
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:
Success
5.03:16.43:1
#17945f · #d1ebe0 · fg #17734c#6cc090 · #16302a
A completed, evidenced check
Warning
5.01:16.81:1
#ef8b0b · #fff1e0 · fg #b3520c#eda44a · #3a2611
A condition to assess. Gold, never the accent
Danger
5.32:16.40:1
#d92d20 · #fce8e8 · fg #b3261e#f08b80 · #3a1c19
A failure needing a next action
Info
4.73:16.04:1
#2563c9 · #dae2ec · fg #2f5fa8#8aacc8 · #1a2b3e
Neutral context, no judgement
Role
Light solid
Light tint foreground
Dark solid
Success
#17945f
#17734c on #d1ebe0
#6cc090
Warning
#ef8b0b
#b3520c on #fff1e0
#eda44a
Danger
#d92d20
#b3261e on #fce8e8
#f08b80
Info
#2563c9
#2f5fa8 on #dae2ec
#8aacc8
Use the matching -fg on a tint. Use --on-solid-* on a solid status fill:
white in light, dark ink on the pale solids used by dark appearances. A muted
page role is not automatically valid on a semantic tint.
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.
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
CloudAgentic AIAgile
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.
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.
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”.
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.
Light, navy dark, and deep dark are equally supported. See
Appearances for the derivative relationship,
surface ladders, selection, elevation, and default-dark code and terminal
surfaces.
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 the supplied preview cards. They follow their semantic token context —
switch appearance and they remain legible with the documentation.
Measurements listed here are normative: a 40px input is 40px.
Five variants, three sizes. Plus Jakarta Sans 500, 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
--color-danger
none
--on-solid-danger
Appearance-specific
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
uses opacity 0.88 rather than inventing a lighter or darker brand colour.
Hover is structural across components: primary surfaces move to opacity:.88
and card borders move to midnight-7. Pressed controls remain at their hover
state and never scale down. Do not lighten or darken a brand colour to create an
interaction state.
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.
Radius 9px, padding 16px 24px, 1px slate-5 border, flat at rest. shadow-1
is reserved for hover; nested
controls step down one radius.
Transparency is restricted. Use backdrop blur only for dark-on-dark modal
scrims and dark-panel inner cards, where the inner surface is
rgba(255,255,255,.04). Do not put frosted glass on a light surface. Tags and
badges use solid pale tints, never alpha overlays.
Header cells use the overline style. Striping uses the subtle background step,
never a border-only rule. Wide tables scroll inside their own container.
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.
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.
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
The code surface is dark by default. Use the complete light companion only for
an explicitly light panel; never switch it automatically with interface mode.
See Code.
Terminal
$ tofu apply -auto-approve Plan: 3 to add, 0 to change, 0 to destroy. ✓Policy check passed ●Deploying to staging…
Hugo
Apply the projectious.work brand through the supported Hugo theme, its public extension points, and site-owned content.
The supported Hugo implementation is
projectious-work/brand-theme-hugo-vanilla.
It is the reference shell for this documentation and the reusable starting
point for other projectious.work sites. Use the theme’s layouts, tokens,
components, and public extension points before adding site-local code.
This guide targets v0.3.2. Pin that release: a site should change its theme
only in an intentional, reviewable update.
Use section indexes and page weights for the documentation hierarchy. Keep the
header for global destinations; do not duplicate the entire documentation tree
there.
Write ordinary Markdown first. Add a shortcode only when the content has a
meaning Markdown cannot express, such as a labelled component specimen or a
semantic callout. The theme exposes public shortcodes for its standard content
patterns; see the theme’s
reference implementation
for current parameters and rendered examples.
Rules for site-local specimens:
Make the source understandable without relying on presentation alone.
Use semantic token names rather than copying colour literals into markup.
Show all appearances when colour or elevation changes meaningfully.
Keep keyboard order equal to DOM order.
Include the specimen’s intent and implementation rule in surrounding copy.
Prefer a local shortcode over enabling raw HTML throughout Markdown.
This documentation follows that model: colour scales, typography, controls,
code, terminal chrome, layouts, and collateral are live translations of the
design-system previews, not embedded screenshots of them.
Do not derive one appearance by inverting another. Bind components to semantic
roles and let each appearance provide its own values. Code and terminal
specimens may also have explicit light variants; a code block is not required
to stay dark merely because that was an older convention.
The initial inline appearance script must run before the first paint so the
page does not flash in the wrong scheme. A visible control must remain keyboard
operable, expose its current state, and persist the user’s choice.
Treat a theme update like a dependency change, not a cosmetic refresh.
console
hugo mod get github.com/projectious-work/brand-theme-hugo-vanilla@v0.3.2
hugo mod tidy
hugo mod graph
Then:
read the release notes and compare the module graph;
build from a clean destination;
review header, navigation, search, tables, callouts, code, and forms;
test light, navy, and deep appearances at narrow and wide widths;
test keyboard navigation, 200% zoom, reduced motion, and strong focus;
inspect print, Markdown, LLMS, and search-index outputs;
run link, contrast, and accessibility checks;
re-evaluate every site-local override.
Record the theme version in the site’s dependency and SBOM documentation. The
Maintenance and SBOM pages define the
repository-wide review and provenance expectations.
Everything representing the visual identity: logos, brand imagery, and the
supplied design-system documents.
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 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.
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 — documented under Tokens — are in
this category. The values are free to use; the marks are not.
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.
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.
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 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 16px. Below that the inner rings
collapse; use a wordmark or a purpose-built favicon instead. Any lockup with a
wordmark must be at least 24px high.
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.
Status treatments
Text-first maturity labels for truthful portfolio presentation.
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.
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.
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.
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.
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.
On a light marketing or documentation shell, the header uses sentence-case
navigation, the lowercase brand lockup, and one accent action on the right. The
footer places the brand mark left and page metadata right in the caption style.
Headers may stick; decorative elements and other page furniture may not.
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
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.
The supplied dashboard mockup is 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.
The supplied mobile mockup is its narrow counterpart: the same system with
the sidebar replaced by a tab bar and every region in one column.
A marketing page uses the same tokens at a calmer editorial density. The
designer’s UI kit composes seven regions: header, hero, practice pillars, code
showcase, convictions, call to action, and footer. This condensed live
translation keeps that sequence and hierarchy while allowing the documentation
theme to own the surrounding page.
projectious·work
Cloud · Agile · Agentic AI
Redesigning work.
Agent-first consulting for organisations adopting AI-native workflows.
We run what we recommend.
CloudComposable, provider-independent infrastructure.AgileLean delivery with continuous policy and audit.Agentic AIAgents integrated into the work, not the demo.
What we ship
Pipelines you can reason about.
Declarative configurations, auditable runs, and policies that fail closed.
01Do more with more02Specialized beats generic now03Provider independence04We run what we recommend
Ready to redesign how you work?30-minute introduction. No deck.
The hero makes one claim, not a carousel of claims. The accent appears at
decision points, the practice names remain joined by a middle dot, and the code
panel stays an inset technical surface. At narrow widths the navigation
disappears, pillars stack, and the showcase and call to action become one
column; the source order already matches the reading order.
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.
Appearances
Light, navy dark, and deep dark from one semantic token contract.
The system supports three appearances. They are not separate palettes and they
do not permit component-specific colour forks.
Appearance
Selector
Page
Raised
Subtle
Border
Light
data-theme="light"
#f8f9fb
#ffffff
#f0f3f8
#cdd0d5
Navy dark, default dark
data-theme="dark"
#132440
#1a2b3e
#20354d
#2e4b68
Deep dark
data-theme="dark" data-surface="deep"
#0e1720
#131e2b
#1a2b3e
#263f5a
With no explicit mode, the interface follows prefers-color-scheme. A dark
preference resolves to navy dark. Deep dark is an explicit choice.
Light
Raised surfaceCanvas · subtle · borderReady
Navy · default dark
Raised surfaceCanvas · subtle · borderReady
Deep · opt in
Raised surfaceCanvas · subtle · borderReady
The specimen translates the designer’s shared three-way preview control into a
simultaneous comparison: the component recipe stays identical while only its
semantic token context changes.
Deep dark begins at the bottom of the midnight ramp. Across a full interface,
panels have little room to separate from the page and the result reads heavy.
Navy begins higher. Raised surfaces remain visible, while a code panel can sit
below the page tone and read as inset rather than as a hole.
Navy is a derivative of deep dark. It changes only midnight steps 1–5,
surfaces, borders, and the neutral tag background. Orange, slate, text, status,
terminal, and syntax roles remain the same.
<htmldata-theme="dark"data-focus="strong"><!-- add data-surface="deep" only for the deep-dark appearance -->
Components consume semantic tokens; they do not branch on appearance.
orange-9 stays constant as the identity accent, but it is never body text.
Light application pages use midnight-1, not white. White is raised.
Elevation in dark uses an elevated surface step as well as a shadow.
Solid status fills use --on-solid-*; the dark solids are light tints and
cannot carry white text.
Code remains on #131e2b and terminal output on #0e1720 in every page
appearance.
An optional light code panel is not light mode
A light code or terminal specimen opts into the complete companion palette,
including syntax, ANSI slots, chrome, borders, and selection. It never switches
automatically with the page.
An element that paints a deliberate dark slab—such as a marketing hero,
terminal, modal, or footer—also establishes the appropriate foreground,
secondary text, border, selection, and control tokens. It must not depend on
the page appearance to make its contents readable.
Review every token-driven preview in light, navy dark, and deep dark. Then apply
high contrast, strong focus, 200% text, loose spacing, reduced motion, and
reduced transparency. A card that works in one screenshot has not passed.
Do
Use semantic surface and foreground roles, and pair --shadow-N with
--elevated-N on dark.
Don't
Create a fourth theme, invert colours, use white as the light app background, or
switch code to a light palette merely because the page is light.
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.
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.
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.
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.
One complete maturity label from the status vocabulary.
An optional short limitation, never a marketing claim.
Keep the status label intact and save a project-specific SVG beside its source.
Examples demonstrate the content structure; they are not evidence of current
maturity.
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.work name and marks remain subject to the trademark terms. Record
any third-party typeface, icon, logo, or screenshot in the provenance inventory
before use. The supplied templates contain no third-party logos or
screenshots.
Terminal
The default dark and optional light projectious.work terminal palettes, plus configuration for 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.
Real terminal and code chrome stays dark by default. The design system also
defines an optional light companion for deliberately light specimens. It is a
separate explicit choice—not a colour-mode response—and every value is measured
against its own surface.
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.
The default background is midnight-dark-1 (#0e1720). It does not follow
page colour mode. A light terminal must explicitly select the companion set.
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 is defined in the Colour foundations
and rendered here from the same source, so this page cannot state a value the
foundations do not.
#
Name
Normal
On surface
Bright
On surface
Provenance
0
black
#0e1720
—
#2e4b68
2.00:1
midnight-dark-1 / midnight-dark-6 — Box drawing and rules, not text — deliberately below the floor.
1
red
#e55b5b
5.15:1
#f08b80
7.49:1
bright = danger-dark — Never the accent — an error and the brand must not look alike.
2
green
#3f9d74
5.41:1
#6cc090
8.24:1
bright = success-dark
3
yellow
#c08a1e
5.93:1
#e0a92a
8.50:1
bright = warning-dark — Gold, matching the warning role in the interface.
4
blue
#6289b3
4.95:1
#8aacc8
7.59:1
bright = midnight-dark-11
5
magenta
#bd6d96
4.98:1
#d491b4
7.32:1
terminal-only — The brand defines no magenta; this slot exists only here.
6
cyan
#3f97a3
5.31:1
#74c0c9
8.71:1
terminal-only — The brand defines no cyan; this slot exists only here.
7
white
#97a8b8
7.41:1
#c5daf0
12.62:1
slate-dark-11 / midnight-dark-12
The bright ramp is the brand: where a hue already exists in the system, the
bright slot takes that step verbatim. The normal ramp is derived from it —
darkened until it reads a step back while still clearing the floor — and magenta
and cyan exist in neither half of the brand. A normal-ramp value is a terminal
value only: #e55b5b is the terminal’s red, not $danger (#a8261c).
Bright black is the one deliberate exception to the 4.5:1 floor: programs use it
for box drawing and rules, not for text. If a program uses it for prose that is
the program’s bug, and the fix is to configure the program — not to lighten the
palette until unreadable text becomes readable.
The brand accent has no ANSI slot, because it is not a semantic colour: it marks
where you are, which the chrome table covers.
The optional light terminal uses #f4f5f7 and a separately measured 16-slot
ANSI palette. It is appropriate for a light-surface specimen or an environment
whose operator has explicitly chosen a light terminal—not as an automatic
response to the surrounding page.
Slot
Normal
Slot
Bright
0 black
#1e2b38
8 bright black
#4a5e74
1 red
#b8352f
9 bright red
#a8261c
2 green
#276754
10 bright green
#1f7a52
3 yellow
#8b6508
11 bright yellow
#6f5106
4 blue
#3a5a82
12 bright blue
#1d3352
5 magenta
#8a3f6e
13 bright magenta
#752f5c
6 cyan
#1c6b6b
14 bright cyan
#125555
7 white
#546a82
15 bright white
#142438
Chrome uses foreground #142438, cursor #E05232, selection
#dae2ec / #142438, dim text #5c6f82, status-bar surface #e6e8eb, and
inactive pane border #adb2ba. The eight base tones clear 4.5:1; bright slots,
the cursor, and UI chrome clear 3:1. Nothing switches to it automatically.
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
#8aacc8
12 · bright blue
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
#e0a92a
11 · bright yellow
Comments
#7d90a3
— dedicated token
Strings and comments are the two 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 receive those values literally; one limited to sixteen colours should
use bright red for strings and bright black for comments, accepting that the
latter 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 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 — projectious.work# Status barset -g status-style "bg=#131e2b,fg=#c5daf0"set -g status-left-length 32set -g status-left "#[bg=#e05232,fg=#0e1720,bold] #S #[bg=#131e2b,fg=#e05232]"set -g status-right-length 80set -g status-right "#[fg=#7b8da3] %Y-%m-%d #[fg=#c5daf0]%H:%M "# Windows — the active one is the single accent element in the barsetw -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"# Panesset -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 promptset -g message-style "bg=#131e2b,fg=#c5daf0"set -g message-command-style "bg=#131e2b,fg=#e0a92a"# Copy mode — selection matches the emulator's selection pairsetw -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"# Clocksetw -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.
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:
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 is configured in Lua, so the palette can be a table the rest of the
config refers to.
lua
-- ~/.config/wezterm/wezterm.lualocalwezterm=require("wezterm")localpj={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 deliberatelycolors={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.
# ~/.config/kitty/kitty.conf — projectious.workfont_family IBM Plex Monofont_size 13.0disable_ligatures alwaysbackground #0e1720foreground #c5daf0cursor #e05232cursor_text_color #0e1720selection_background #20354dselection_foreground #c5daf0url_color #8aacc8# Normalcolor0 #0e1720color1 #e55b5bcolor2 #3f9d74color3 #c08a1ecolor4 #6289b3color5 #bd6d96color6 #3f97a3color7 #97a8b8# Brightcolor8 #2e4b68color9 #f08b80color10 #6cc090color11 #e0a92acolor12 #8aacc8color13 #d491b4color14 #74c0c9color15 #c5daf0# Tabs — the active tab is the single accent elementtab_bar_style powerlinetab_powerline_style slantedactive_tab_background #e05232active_tab_foreground #0e1720active_tab_font_style boldinactive_tab_background #131e2binactive_tab_foreground #7b8da3tab_bar_background #131e2b# Splitsactive_border_color #e05232inactive_border_color #7b8da3window_padding_width 6# Marks and bellsmark1_foreground #0e1720mark1_background #e0a92abell_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 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/config — projectious.worktheme=projectiousfont-family=IBM Plex Monofont-size=13font-feature=-ligafont-feature=-caltcursor-style=blockcursor-opacity=1# The accent marks the focused split, and nothing else in the chrome.split-divider-color=#7b8da3unfocused-split-fill=#0e1720window-padding-x=6window-padding-y=6window-padding-balance=truelink-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 that automatic pairing for the default configuration:
dark is the product default. If a deliberately light terminal is required,
configure the complete light companion palette as a separate named theme; do
not mix its slots with the dark set. 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
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:
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.
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.
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.
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.
Typography
Three families, three roles, a scalable reading-first ramp, and the font cuts required to preserve meaning.
Typography answers one question before it answers any aesthetic one: what kind
of information is this?
Plus Jakarta Sans is conviction. Use it for display, headings, navigation,
buttons, and the wordmark.
Source Sans 3 is clarity. Use it for running copy, interface labels,
descriptions, and captions.
IBM Plex Mono is precision. Use it for code, data, terminal output,
versions, and machine identifiers.
Never mix those roles. If the text guides attention, use Jakarta. If it is read,
use Source Sans. If it reports a system, use Plex Mono.
The mono cuts are semantic, not optional polish. Keywords use 700, types use
600, functions use 500, and comments and decorators use italic. Omitting those
files forces synthetic styles and removes the non-colour channel that keeps
syntax structure legible in greyscale.
Every row is rendered in the family, size, weight, and line height it documents.
It translates the supplied type-family, display, body, code, and scale preview
cards into one responsive reference.
Display · 48/800/1.1
Redesigning work
H1 · 36/700/1.15
Agent-first consulting
H2 · 28/700/1.2
Composable infrastructure
H3 · 24/600/1.25
How the system fits together
H4 · 20/600/1.3
Working in the open
H5 · 17/600/1.35
Supporting detail
Body L · 18/400/1.65
Augmenting people's strengths through composable Cloud · Agile · Agentic AI infrastructure.
Body · 16/400/1.6
Direct, technical, humanistic, and restrained.
Caption · 13/400/1.5
Figure 1 — pipeline stages and their policy gates.
Overline · 12/600/1.3
SECTION LABEL
Code · 14/400/1.6
createPipeline({ policy: "strict" })
Role
Size
Weight
Line height
Display
48 px
800
1.1
H1
36 px
700
1.15
H2
28 px
700
1.2
H3
24 px
600
1.25
H4
20 px
600
1.3
H5
17 px
600
1.35
Body large
18 px
400
1.65
Body
16 px
400
1.6
Caption
13 px
400
1.5
Overline
12 px
600
1.3
Code
14 px
400
1.6
Every size resolves through --font-scale. The accessibility settings raise
the entire ramp to 112.5%, 125%, 150%, or 200% without per-component overrides.
Four monochrome treatments are approved, for print, fax, embossing, and any
single-colour reproduction:
Monochrome variants
Midnight
Black
White reversed
Grey
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.
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.
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.
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.
Icon-only, on a midnight tile, with 12% padding. Corner radius scales with
size:
Favicon tiles — 12% padding, radius scales with size
48px / r8
32px / r4
16px / r2
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.
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.
The supplied email mockup is the visual reference. Its HTML is self-contained,
so it can be opened without a runtime or build step and translated into the
sending tool’s template format.
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:
html
<divstyle="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.
html
<!--[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]><!--><tablerole="presentation"cellpadding="0"cellspacing="0"border="0"><tr><tdbgcolor="#cc4528"style="border-radius:6px;"align="center"><ahref="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 6px radius-md 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.
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.
Code
The default dark code surface, optional light-panel companion, and measured syntax roles.
Code blocks stay dark regardless of appearance. 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 light, navy, and deep appearances. A deliberately light
specimen may opt into the companion palette below; colour mode never switches a
code block to it automatically.
The block you are reading is rendered by that rule:
For the default dark surface, every syntax value is read from the dark scale
and contrast is measured against #131e2b. The optional light panel uses the
separate light syntax set defined below.
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.
Role
On surface
LSP semantic token
TextMate scope
Plain and variables #c5daf0midnight-dark-12 The default. Anything the reader does not need to pick out.
Types and classes #6cc090terminal green (bright) Green, not cyan, because types are referenced on nearly every line of typed code and the most frequent role should hold the best-separated hue — 29.9 ΔE2000 from plain text, against cyan's 17.3.
7.67:1
type · class · struct · interface · enum · typeParameter · namespace
Functions and methods #8aacc8midnight-dark-11 / terminal blue (bright) Callables use the established informational blue, leaving yellow exclusively to numbers and constants.
7.06:1
function · method
entity.name.function · support.function .nf .fm
Decorators and macros #74c0c9terminal cyan (bright) Cyan sits closer to plain text than green does, which is affordable here because decorators are rare. Code that runs at a different time from the code around it — including C preprocessor directives and Rust attributes, which Chroma files under Comment.Preproc but which are macros, not commentary.
Operators and punctuation #97a8b8slate-dark-11 Present but recessive — structure you read past, not at.
6.90:1
operator
keyword.operator · punctuation .o .ow .p
Comments #7d90a3code-comment Italic. The only role with no scale step of its own. Documentation comments belong here even though Chroma files them under String.Doc — a docstring is documentation, not data.
Use a light code panel only when the surrounding artifact specifically needs a
light specimen. The surface is #f4f5f7; all ten roles are separately measured
for a 4.5:1 floor. These tokens are companions, not mode-swapped aliases:
Role
Token
Value
Plain
--syntax-plain-light
#0f1c2e
Comments
--syntax-comment-light
#5e7082
Operators
--syntax-operator-light
#54697f
Keywords
--syntax-keyword-light
#c2117f
Types and classes
--syntax-type-light
#08804e
Functions
--syntax-function-light
#1668d8
Strings
--syntax-string-light
#c94208
Numbers
--syntax-number-light
#94620a
Decorators and macros
--syntax-macro-light
#0d7d82
Invalid
--syntax-invalid-light
#d81420
The base stylesheet paints every <pre> dark. Styling only a wrapper therefore
leaves a dark rectangle over the intended light panel. The opt-in must
neutralise the nested element explicitly:
Ten modifiers times ten roles is a hundred combinations. Colour cannot carry that, and a theme that tries becomes unreadable.
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 (#7d90a3, 5.19: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.
Each example below renders the same real, compilable-shaped fragment in the
default dark palette and the optional light palette. The fragment is 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.
/* Ring buffer — fixed capacity, no allocation after init. */#include<stdint.h>#define RING_CAP 256 // macro: a decorator-role token
typedefenum{RING_OK=0,RING_FULL=1}ring_status_t;typedefstruct{uint8_tdata[RING_CAP];size_thead,tail;_Boolwrapped;}ring_t;staticinlinesize_tring_len(constring_t*r){return(r->head-r->tail)&(RING_CAP-1);}staticconstchar*RING_TAG="ring\n";// string literal
ring_status_tring_push(ring_t*restrictr,uint8_tbyte){// Reject when one slot short of capacity, so head never meets tail.
if(ring_len(r)==RING_CAP-1)returnRING_FULL;r->data[r->head++&(RING_CAP-1)]=byte;returnRING_OK;}
Light · optional
/* Ring buffer — fixed capacity, no allocation after init. */#include<stdint.h>#define RING_CAP 256 // macro: a decorator-role token
typedefenum{RING_OK=0,RING_FULL=1}ring_status_t;typedefstruct{uint8_tdata[RING_CAP];size_thead,tail;_Boolwrapped;}ring_t;staticinlinesize_tring_len(constring_t*r){return(r->head-r->tail)&(RING_CAP-1);}staticconstchar*RING_TAG="ring\n";// string literal
ring_status_tring_push(ring_t*restrictr,uint8_tbyte){// Reject when one slot short of capacity, so head never meets tail.
if(ring_len(r)==RING_CAP-1)returnRING_FULL;r->data[r->head++&(RING_CAP-1)]=byte;returnRING_OK;}
// Policy-based cache. Types, templates, and a lambda.
#include<string>#include<unordered_map>namespaceprojectious::cache{template<typenameKey,typenameValue>classLruCachefinal{public:explicitLruCache(std::size_tcapacity)noexcept:capacity_{capacity}{}[[nodiscard]]autoget(constKey&key)const->constValue*{constautoit=entries_.find(key);returnit==entries_.end()?nullptr:&it->second;}voidput(Keykey,Valuevalue){staticconstexprautokTag="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:voidevict()noexcept{/* … */}std::size_tcapacity_{0};std::unordered_map<Key,Value>entries_{};};}// namespace projectious::cache
Light · optional
// Policy-based cache. Types, templates, and a lambda.
#include<string>#include<unordered_map>namespaceprojectious::cache{template<typenameKey,typenameValue>classLruCachefinal{public:explicitLruCache(std::size_tcapacity)noexcept:capacity_{capacity}{}[[nodiscard]]autoget(constKey&key)const->constValue*{constautoit=entries_.find(key);returnit==entries_.end()?nullptr:&it->second;}voidput(Keykey,Valuevalue){staticconstexprautokTag="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:voidevict()noexcept{/* … */}std::size_tcapacity_{0};std::unordered_map<Key,Value>entries_{};};}// namespace projectious::cache
"""Pipeline stages and their policy gates."""from__future__importannotationsimportfunctoolsfromdataclassesimportdataclass,fieldfromtypingimportFinal,Iterable# Retry budget is a policy decision, not a tuning knob.MAX_RETRIES:Final[int]=3DEFAULT_POLICY="strict"@dataclass(frozen=True,slots=True)classStage:"""A single stage. Immutable once constructed."""name:strpolicy:str=DEFAULT_POLICYretries:int=0tags:list[str]=field(default_factory=list)@propertydefis_strict(self)->bool:returnself.policy=="strict"@staticmethoddefparse(raw:str)->"Stage":name,_,policy=raw.partition(":")returnStage(name=name.strip(),policy=policyorDEFAULT_POLICY)@functools.lru_cache(maxsize=None)defvalidate(stages:Iterable[Stage])->bool:forstageinstages:ifstage.retries>MAX_RETRIES:raiseValueError(f"{stage.name!r} exceeds {MAX_RETRIES} retries")returnTrue
Light · optional
"""Pipeline stages and their policy gates."""from__future__importannotationsimportfunctoolsfromdataclassesimportdataclass,fieldfromtypingimportFinal,Iterable# Retry budget is a policy decision, not a tuning knob.MAX_RETRIES:Final[int]=3DEFAULT_POLICY="strict"@dataclass(frozen=True,slots=True)classStage:"""A single stage. Immutable once constructed."""name:strpolicy:str=DEFAULT_POLICYretries:int=0tags:list[str]=field(default_factory=list)@propertydefis_strict(self)->bool:returnself.policy=="strict"@staticmethoddefparse(raw:str)->"Stage":name,_,policy=raw.partition(":")returnStage(name=name.strip(),policy=policyorDEFAULT_POLICY)@functools.lru_cache(maxsize=None)defvalidate(stages:Iterable[Stage])->bool:forstageinstages:ifstage.retries>MAX_RETRIES:raiseValueError(f"{stage.name!r} exceeds {MAX_RETRIES} retries")returnTrue
//! Policy evaluation for pipeline stages.
usestd::collections::HashMap;usestd::fmt::{self,Display};constMAX_RETRIES: u32=3;/// How strictly a stage is evaluated.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]pubenumPolicy{Strict,Advisory,}#[derive(Debug, Default)]pubstructStage<'a>{pubname: &'astr,pubpolicy: Option<Policy>,pubretries: u32,}impl<'a>Stage<'a>{// Strict by default: a gate that is not configured should fail closed.
pubfnnew(name: &'astr)-> Self{Self{name,policy: Some(Policy::Strict),retries: 0}}pubfnvalidate(&self)-> Result<(),String>{ifself.retries>MAX_RETRIES{returnErr(format!("{} exceeds {MAX_RETRIES} retries",self.name));}Ok(())}}implDisplayforPolicy{fnfmt(&self,f: &mutfmt::Formatter<'_>)-> fmt::Result{write!(f,"{}",matchself{Policy::Strict=>"strict",_=>"advisory"})}}
Light · optional
//! Policy evaluation for pipeline stages.
usestd::collections::HashMap;usestd::fmt::{self,Display};constMAX_RETRIES: u32=3;/// How strictly a stage is evaluated.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]pubenumPolicy{Strict,Advisory,}#[derive(Debug, Default)]pubstructStage<'a>{pubname: &'astr,pubpolicy: Option<Policy>,pubretries: u32,}impl<'a>Stage<'a>{// Strict by default: a gate that is not configured should fail closed.
pubfnnew(name: &'astr)-> Self{Self{name,policy: Some(Policy::Strict),retries: 0}}pubfnvalidate(&self)-> Result<(),String>{ifself.retries>MAX_RETRIES{returnErr(format!("{} exceeds {MAX_RETRIES} retries",self.name));}Ok(())}}implDisplayforPolicy{fnfmt(&self,f: &mutfmt::Formatter<'_>)-> fmt::Result{write!(f,"{}",matchself{Policy::Strict=>"strict",_=>"advisory"})}}
// Package pipeline evaluates stages against their policy gates.packagepipelineimport("errors""fmt")constMaxRetries=3// Policy is how strictly a stage is evaluated.typePolicyintconst(StrictPolicy=iotaAdvisory)varErrTooManyRetries=errors.New("stage exceeds retry budget")typeStagestruct{Namestring`json:"name"`PolicyPolicy`json:"policy"`Retriesint`json:"retries,omitempty"`}func(s*Stage)Validate()error{ifs.Retries>MaxRetries{returnfmt.Errorf("%q: %w",s.Name,ErrTooManyRetries)}returnnil}funcValidateAll(stages[]Stage)(okbool,errerror){fori:=rangestages{iferr=stages[i].Validate();err!=nil{returnfalse,err}}returntrue,nil}
Light · optional
// Package pipeline evaluates stages against their policy gates.packagepipelineimport("errors""fmt")constMaxRetries=3// Policy is how strictly a stage is evaluated.typePolicyintconst(StrictPolicy=iotaAdvisory)varErrTooManyRetries=errors.New("stage exceeds retry budget")typeStagestruct{Namestring`json:"name"`PolicyPolicy`json:"policy"`Retriesint`json:"retries,omitempty"`}func(s*Stage)Validate()error{ifs.Retries>MaxRetries{returnfmt.Errorf("%q: %w",s.Name,ErrTooManyRetries)}returnnil}funcValidateAll(stages[]Stage)(okbool,errerror){fori:=rangestages{iferr=stages[i].Validate();err!=nil{returnfalse,err}}returntrue,nil}
packagework.projectious.pipeline;importjava.util.List;importjava.util.Objects;/** A single pipeline stage and its policy gate. */publicfinalclassStageimplementsComparable<Stage>{publicstaticfinalintMAX_RETRIES=3;publicstaticfinallongTIMEOUT_MS=30_000L;publicstaticfinalintMASK=0xFF;privatefinalStringname;privatefinalPolicypolicy;privateintretries=0;privatedoublebudget=12.60;publicStage(Stringname,Policypolicy){this.name=Objects.requireNonNull(name,"name");this.policy=policy;}@OverridepublicintcompareTo(Stageother){returnthis.name.compareTo(other.name);}@Deprecated(since="2.0",forRemoval=true)publicbooleanisStrict(){returnpolicy==Policy.STRICT;}publicvoidvalidate(List<String>errors)throwsIllegalStateException{if(retries>MAX_RETRIES){thrownewIllegalStateException("%s exceeds %d retries".formatted(name,MAX_RETRIES));}}publicenumPolicy{STRICT,ADVISORY}}
Light · optional
packagework.projectious.pipeline;importjava.util.List;importjava.util.Objects;/** A single pipeline stage and its policy gate. */publicfinalclassStageimplementsComparable<Stage>{publicstaticfinalintMAX_RETRIES=3;publicstaticfinallongTIMEOUT_MS=30_000L;publicstaticfinalintMASK=0xFF;privatefinalStringname;privatefinalPolicypolicy;privateintretries=0;privatedoublebudget=12.60;publicStage(Stringname,Policypolicy){this.name=Objects.requireNonNull(name,"name");this.policy=policy;}@OverridepublicintcompareTo(Stageother){returnthis.name.compareTo(other.name);}@Deprecated(since="2.0",forRemoval=true)publicbooleanisStrict(){returnpolicy==Policy.STRICT;}publicvoidvalidate(List<String>errors)throwsIllegalStateException{if(retries>MAX_RETRIES){thrownewIllegalStateException("%s exceeds %d retries".formatted(name,MAX_RETRIES));}}publicenumPolicy{STRICT,ADVISORY}}
\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 \leq3$:
\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}
Light · optional
\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 \leq3$:
\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}
---title:Stage referenceweight: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. -->
Light · optional
---title:Stage referenceweight: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. -->
# Pipeline definition — one policy gate per stage.schema="https://projectious.work/schema/pipeline-2.json"[pipeline]name="validate-deploy"policy="strict"retries=3enabled=truebudget=12.60created=2026-08-02T09:00:00Ztags=["platform","eu-central"][[pipeline.stage]]name="validate"gate="strict"timeoutSeconds=120[[pipeline.stage]]name="deploy"gate="advisory"timeoutSeconds=600
Light · optional
# Pipeline definition — one policy gate per stage.schema="https://projectious.work/schema/pipeline-2.json"[pipeline]name="validate-deploy"policy="strict"retries=3enabled=truebudget=12.60created=2026-08-02T09:00:00Ztags=["platform","eu-central"][[pipeline.stage]]name="validate"gate="strict"timeoutSeconds=120[[pipeline.stage]]name="deploy"gate="advisory"timeoutSeconds=600
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 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 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.
console
$ hugo --gc --minify
Start building sites …
Total in 842 ms
Diagrams and demos
Evidence-first architecture, flow, screenshot, and demo framing.
“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
The supplied UI mockups and component previews show these rules in context,
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.
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.
Files
The canonical SVG, raster, and favicon assets in design system v2.1.1.
The package also contains three contextual SVG colourways — cool dot on white,
spot warm on dark, and warm on darker. They are supplied variants, not a licence
to recolour the canonical files. Use SVG wherever the medium accepts it; choose
128px or 256px PNG only when a raster format is required.
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.
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:
the projectious.work legal assessment supplied with the prior system.
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.
All three families are SIL Open Font License 1.1 and self-hosted as pinned
Fontsource 5.3.0 WOFF2 subsets. 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. The licence texts ship beside the files.
MIT License: free commercial use, modification, redistribution, and
sublicensing. The design system uses only Tabler’s outline set, overrides the
native stroke to 1.5 px, and keeps the licence notice with any vendored subset.
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.
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.
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.
Photography is not the default and is never full-bleed by default. Prefer a
diagram when the subject is a system, relationship, or process.
Use candid, real workspaces with a warm, desaturated grade. Avoid cool casts,
tropical saturation, and high-contrast black and white. If grain is useful,
keep it subtle at 4–6%.
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.
The illustration style is abstract system diagrams — nodes, flows, and
pipelines drawn in the brand palette.
Do not use hand-drawn illustration, texture, or decorative imagery. The one
approved decorative motif is a top-right orange circular glow at 6% opacity on
a dark hero panel.
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.
Social and OG images
Templates for LinkedIn, GitHub, blog headers, and Open Graph cards.
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.
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 under hover
--shadow-2
0 4px 12px rgba(0,0,0,0.08)
Popovers, dropdowns
--shadow-3
0 8px 24px rgba(0,0,0,0.12)
Modals
There are no inner, glow, or coloured shadows. Most surfaces remain level 0;
cards do not need a resting shadow when surface and border already establish
their edge.
In both dark appearances, elevation is expressed first by lightening the
surface. The ladder climbs midnight steps 1 → 2 → 4 → 6, adds a subtle top rim,
and deepens the shadow only to seat the panel. Adjacent steps are too close to
carry the hierarchy; a shadow alone is invisible against a dark page.
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.
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.
The rendered service-page mockups are the visual reference for hierarchy,
spacing, surface treatment, and the single-action close.
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.
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 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
Tabler 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”.
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.
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.
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.
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. The supplied article, documentation, and marketing mockups are the
visual reference for type hierarchy and fixed-width reading measures.
Longer, flowing documents — reports and papers — use the LaTeX and Typst
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.
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.
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.
Iconography
Tabler outline icons on a 24 px grid, with one stroke system and semantic colour.
The system uses Tabler Icons, outline set only. Tabler is MIT-licensed and
drawn on a 24 px grid. Do not mix its filled collection into the brand and do
not add another library to fill gaps.
16 px inline · 20 px in buttons · 24 px for navigation
A custom icon is permitted only where Tabler has no suitable concept. Draw it
on the same grid with the same stroke, caps, joins, and single-colour outline.
Marketing prototypes may request an individual outline SVG from the Tabler CDN.
React surfaces inline SVG rather than running a DOM-mutating icon script.
Distributable themes and offline products may vendor a versioned outline subset;
ship the MIT licence and record the pinned version in the SBOM.
The projectious.work mark is not an icon. It appears only as part of the
identity and must never stand in for home, AI, an agent, or a generic product.
Do
Pair unfamiliar icons with text and give every icon-only control an accessible
name and a 44 px target.
Don't
Use emoji, Unicode pictograms, filled Tabler variants, arbitrary icon colours,
or the brand mark as an interface glyph.
Presentations
Slide templates, animation, and the core deck structure.
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.
The supplied slide templates provide a full
client deck at 1280×720, 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.
Document templates for LaTeX and Typst are in
the document template set.
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:9, 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
Document 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.
Asset provenance
Per-asset source, licence, and attribution status for every third-party dependency.
The authoritative inventory is
the project provenance inventory
in the repository. Check it before adding an asset or shipping collateral.
Last reviewed 2026-08-13: the four declared font binaries and Font Awesome
are the only bundled third-party font assets. Their licence material ships with
the files.
Adding a bundled asset
If you commit a third-party binary — a font file, an icon SVG, an image — add a
row to the provenance inventory recording its individual source, licence, and
attribution requirement. Do not rely on the summary tables above.
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.
Four breakpoints, declared in
the synchronized design-system token sheet 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.
css
/* 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.5fr1fr;}}
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.
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.
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 mockup 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
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.
The supplied mobile mockup shows
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.
Forms
Input sizing, labels, focus, error, and disabled states.
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.
Error text sits below the field in the appearance’s danger foreground.
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.
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.
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.
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”.
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.
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.
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 token sheet changes nothing until an attribute is set on <html>. New work
should expose and persist the settings it supports; data-a11y="auto" follows
the operating system’s reduced-motion, reduced-transparency, and contrast
preferences.
Attribute
Value
Effect
data-a11y
auto
Follows supported reduced-motion, reduced-transparency, and contrast operating-system preferences
data-font-size
lg, xl, xxl, xxxl
112.5%, 125%, 150%, or 200% type through one shared scale
data-contrast
high
Stronger text and border roles in every appearance
data-focus
strong
Conforming 2px midnight ring with 2px offset
data-link-underline
on
Underlines links in running text; .link-plain opts out
data-motion
reduced
Near-zero transitions and animation
data-transparency
reduced
Removes backdrop blur and solidifies every scrim
data-text-spacing
loose
WCAG 1.4.12 diagnostic spacing, not the house style
data-theme
light, dark
Pins the colour mode
The navy appearance is data-theme="dark". Add data-surface="deep" for the
opt-in near-black appearance; it is a surface choice within dark mode, not a
third operating-system colour-scheme value.
Use .sr-only for an accessible name, .sr-only-focusable for content that
reveals on focus, .skip-link for the first control, .target for the 44px hit
floor, and .measure for the 65ch reading width.
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.
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.
html
<aclass="pj-skip"href="#main">Skip to content</a><aclass="pj-skip"href="#nav">Skip to navigation</a>
css
.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:topvar(--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.
Focus is always visible. The system never sets outline: none without
replacing it with something at least as loud.
The base focus ring is intentionally quiet and measures only about 1.2:1. It is
not conforming on its own. New work sets data-focus="strong", which draws a
2px midnight-9 (#1d3352) outline at 2px offset and clears 8.59:1. Draw
it with :focus-visible so a mouse click does not leave a ring behind while
keyboard focus still does.
The offset matters: at 0 the ring sits on the control’s own border and can
disappear against it.
The two deliberate WCAG 2.1 SC 1.4.3 exemptions are logotypes, where the
wordmark uses the identity accent, and inactive controls. Treat neither as a
general exception for copy or interactive content.
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.
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.
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.
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.
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.
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.
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.
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.
Tokens
Canonical design values, generated exports, appearance selectors, and consumption rules.
Tokens turn the visual rules into a portable contract. They are
MIT-licensed; the name, wordmark, and logo remain reserved marks. See
Licensing.
Do not edit a download. Change the structured source, regenerate all formats,
and run the drift check so consumers never receive three different systems.
console
uv run --script scripts/build_tokens.py
scripts/check-tokens.sh
Navy-default page (#132440) with raised #1a2b3e surfaces
[data-theme="dark"][data-surface="deep"]
Deep page (#0e1720) and #131e2b raised surfaces
Navy is a documented derivative. It changes the first five midnight steps and
the fourteen aliases that resolve from them; orange, slate, text, statuses,
syntax, and terminal values remain shared with deep dark.
Do not use step 9 as text: it is intentionally constant across appearances and
does not clear text contrast on dark surfaces. Use --color-text-*, --fg-*,
or steps 11–12. On a semantic tint, use the matching -fg; a solid status value
is not automatically the correct foreground for its tint.
Code and terminal surfaces are dark by default in all three page appearances.
The exports also carry complete light companions for deliberate light panels:
CSS custom properties can be used in declarations and JavaScript, but not as
the condition of a media query. Write @media (min-width: 768px) even though
--breakpoint-md is also exported.
Tailwind does not infer the selector strategy. It receives explicit
midnight, midnightNavy, and midnightDeep namespaces (and equivalent
semantic groups); the consuming project maps those values to its own variants.
The JSON file preserves the same light, navy, and deep hierarchy and
contains both syntax and terminal palettes. It is the preferred input for
generators that do not consume CSS.
Generated means verified
The documentation specimens and downloadable outputs use the same structured
values. The build, token drift check, and three-appearance contrast audit must
all pass before a token change ships.
AI consumer guide
A versioned map from brand use cases to their normative documentation and generated machine-readable material.
This page is the canonical guide for agents and other automated consumers of
the projectious.work brand contract. The current release is v3.0.2.
src/content/docs/ is the normative human-readable authority and
src/data/brand.yaml is the structured authority. Files under
/downloads/ are generated from those sources. Designer intake material is
upstream reference material and is not a public runtime contract.
The repository includes a local, read-only
Model Context Protocol server for agents
that need structured access to the released brand contract. It reads the same
generated manifest linked above; it does not maintain a second copy of the
tokens or documentation.
The server is a stdio process, not a hosted network endpoint. Clone the
repository and start it from the repository root:
sh
uv run --script mcp/brand_server.py
Python 3.10 or newer and uv are required. The script pins the compatible MCP
SDK range to mcp[cli]>=1.13,<2; uv creates the isolated environment on the
first run. No API key, credential, environment variable, or network service is
needed after the dependency is available.
The exact outer configuration key varies by client, but the command and
arguments are the same. Keep stdout reserved for MCP JSON-RPC; operational logs
go to stderr.
Return the exact value, semantic role, and appearance of a token.
explain_token_role(name, version?)
Explain where a token belongs without inventing a new value.
discover_assets(kind?, version?)
List normative, download, or example resources.
lookup_provenance(asset_id, version?)
Locate the provenance authority for a public asset.
validate_token_reference(reference, version?)
Check whether a supplied token name exists in the contract.
For example, ask the client to call lookup_token with
name: "--color-bg-navy", or call discover_assets with kind: "download"
before choosing a machine-readable format. The manifest covers appearance-
pinned scales and surfaces, semantic triples, syntax, terminal palettes, type,
spacing, radius, elevation, motion, and breakpoints. A validation request for
an unknown name returns valid: false; it does not search the filesystem or
the network.
The server serves exactly the version declared by the checked-out manifest.
Requests for any other version fail explicitly, which prevents an agent from
silently combining releases. To query an archived contract, check out that
release tag and run its server. For link-only retrieval, use the corresponding
versioned documentation site instead.
Every tool is annotated read-only, non-destructive, idempotent, and closed to
the outside world. The server accepts resource IDs and token names—not paths or
URLs. Its allowlist excludes internal process data, version-control metadata,
designer intake material, build caches, credentials, and maintainer
operations. It rejects unknown versions, resources, kinds, and tokens, and it
cannot modify the checkout.
The protocol smoke test initializes a real stdio session, discovers resources
and tools, reads a resource, exercises lookup and validation, and runs as part
of ./scripts/verify.sh.
The site root is the latest released contract. Archived releases live under
their exact /vX.Y.Z/ prefix and contain their own Markdown, discovery files,
downloads, and manifest. Never combine values from different versions.
Breaking or migration-relevant changes are recorded in the repository
changelog. Generated assets carry the same release identifier as the site that
serves them.
Use published documentation, Markdown outputs, token downloads, and manifest
entries. Do not treat repository process data, build caches, intake material,
or generated Pages history as brand guidance. Marks remain subject to the
trademark and brand-asset terms even when token values or code are MIT licensed.
Developer guide
Implement the projectious.work design system with semantic tokens, documented patterns, and reproducible checks.
Start with semantic roles such as --bg, --surface, --fg-1, --border,
--tag-bg, and the semantic status triples. Primitive scale steps support
those roles. A screenshot is verification evidence, not a source file.
Use data-theme="dark" for navy dark, the default dark appearance. Add
data-surface="deep" for deep dark. Review all three because elevation, borders,
and page/surface separation change even when typography and status roles do
not.
Self-hosted builds must ship all specified weights. IBM Plex Mono needs 400,
500, 600, and 700 in upright and italic because syntax meaning is carried by
weight and style as well as hue.
Use a 12-column desktop grid, 8 columns at medium widths, and 4 at the base.
Two-column layouts begin at 768 px; persistent sidebars begin at 1024 px. Keep
the reading measure at 65ch and every touch target at least 44 px.
Solid accent controls use --color-accent-solid with
--fixed-control-text. Semantic tints use their matching -fg. On dark
surfaces, pair --shadow-N with --elevated-N; the surface step carries the
visible elevation.
Code is dark by default on --code-panel-surface (#131e2b). A terminal is
deeper on --terminal-surface (#0e1720). A light code or terminal specimen is
an explicit opt-in and must use the complete light companion set, including
selection colours and chrome.
The verification contract includes generated-token parity, font and manifest
integrity, Hugo output, AI discovery, MCP protocol behavior, contrast across
appearances, and internal links. Add fit tests for any fixed-size card, slide,
or export under loose text spacing and 200% text.
Maintenance
Synchronize, audit, release, and recover the projectious.work design system without drift.
Human guidance, structured token data, generated downloads, theme assets, and
preview artifacts must not become competing authorities. Change the owning
source, regenerate its outputs, and verify the rendered documentation in the
same pull request.
The latest documentation is served from the site root. Released snapshots are
kept under versioned paths so external links remain stable. Prepare version
metadata and migration notes on a release branch, merge after review, then use
the repository release script to tag, archive, and publish.
If generated material and its source disagree, stop publishing. Restore parity
from the owning source, rerun verification, and document the affected releases.
Do not repair a public download by hand.
SBOM and dependencies
Supply-chain scope, licences, and maintenance obligations for the projectious.work design system.
The SBOM covers code and assets shipped to a browser, build-time tools needed to
reproduce the published site, generators used to create public downloads, and
third-party material embedded in templates. It does not classify the reserved
projectious.work marks as third-party dependencies.