projectious.work brand · Documentation v3.0.2
projectious.work brand · v3.0.2

Documentation

The projectious.work brand and design system — foundations, logo, interface patterns, collateral, and usage terms.

Printed August 17, 2026 · 39 pages

Contents

  1. Features
  2. Business card
  3. Colour
  4. Components
  5. Hugo
  6. Licensing
  7. Lockups
  8. Status treatments
  9. Patterns
  10. Appearances
  11. Audio & video
  12. Email signature
  13. Social previews
  14. Terminal
  15. Trademark
  16. Typography
  17. Usage
  18. Email
  19. Code
  20. Diagrams and demos
  21. Files
  22. Legal assessment
  23. Photography & illustration
  24. Social & OG
  25. Space, shape & motion
  26. One-pager
  27. Documents
  28. Icons
  29. Presentations
  30. Provenance
  31. Responsive
  32. Forms
  33. States
  34. Accessibility
  35. Tokens
  36. AI consumption
  37. Developer guide
  38. Maintenance
  39. SBOM and dependencies

Features

What the projectious.work design system provides to writers, designers, developers, and agents.

One contract, several consumers#

Identity and voice

Mission, convictions, sentence-case writing, stock taglines, and reserved marks.

Three appearances

Light, navy dark, and deep dark from one semantic token layer.

Accessible by construction

Strong focus, scalable type, underlined prose links, contrast, motion, transparency, and text-spacing controls.

Implementation tokens

Colour, type, space, shape, elevation, motion, overlays, print, terminal, and syntax roles.

Live artifacts

Theme-idiomatic specimens for components, layouts, marks, code, terminals, slides, and collateral.

Machine consumption

Clean Markdown, llms.txt, a versioned manifest, and a read-only brand MCP server.

Design stance#

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.

Included artifacts#

  • Marketing header, hero, practice-area pillars, code showcase, convictions, closing action, and footer.
  • Agent-console sidebar, top bar, pipeline list, status pills, run detail, terminal output, and system status bar.
  • Six slide types at 1280 × 720.
  • Logo variants, lockups, minimum-size and clear-space specimens.
  • Core colours, complete ramps, semantics, type ramp, space, radii, elevation, breakpoints, icons, buttons, inputs, cards, tags, alerts, and accessibility.
  • Dark and optional light syntax examples for Python, Rust, YAML, TOML, and LaTeX, plus terminal palette and status treatment.

Business card

The digital-first vCard, shared by QR code or link.

The primary format is digital — a vCard shared via QR code or direct link, not a printed card.

Digital vCard
projectious · work
Bernhard Gerlach
Cloud · Agile · Agentic AI
info@projectious.work
projectious.work

Paper stock#

The same card on white. This is the form to hand a printer when a physical card is required, and the one to reach for when the card will sit on a light surface it does not control — a conference badge holder, a scanned page, a slide.

Paper card — white stock, accent on the leading edge
projectious · work
Bernhard Gerlach
Cloud · Agile · Agentic AI
info@projectious.work
projectious.work

Three things change, and nothing else does:

DigitalPaper
Surfacemidnight-dark-1White in every appearance — paper has no colour mode
AccentA 12% wash bleeding off the cornerA 3px rule down the leading edge
MarkLight-on-dark colourwaymidnight-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.

Layout#

  • Midnight surface, mark top-left, name in Plus Jakarta Sans 700.
  • Role in Source Sans 3 400, in slate-dark-11.
  • Contact rows in IBM Plex Mono at 11px — addresses and handles are data, and monospace makes them scannable and unambiguous.
  • projectious.work always present.
  • Accent used once, on the QR frame or the primary contact method.

Print fallback#

If a printed card is required:

  • 85×55mm, matte stock.
  • Single-colour midnight on uncoated white, or reversed white on midnight.
  • Use the monochrome mark — no accent on press unless a spot colour is budgeted.
  • Reserve clear space equal to half the mark’s height on every side; do not bleed the mark to the trim edge.

Handing colour to a printer#

The brand is defined in sRGB hex. Press is not sRGB, so a printed card needs the values converted — and the conversion is the vendor’s job, not a lookup table’s.

Give the printer the Lab values. They are device-independent: they describe the colour itself rather than one recipe for making it, so the vendor can hit them on whatever stock and profile the job actually runs on.

ColourHexLab (D50)CMYK — unmanaged
Midnight#1d3352L 20.7, a −0.5, b −21.965 / 38 / 0 / 68
Orange#E05232L 54.9, a 55.2, b 48.50 / 63 / 78 / 12
Accent solid#cc4528L 49.3, a 53.3, b 46.80 / 66 / 80 / 20
Slate#546a82L 43.7, a −3.7, b −16.235 / 18 / 0 / 49
Midnight dark#132440L 13.9, a 1.0, b −20.370 / 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.

Core colours#

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.

Primary
#1d3352
Text, headings, primary surfaces
Accent
#e05232
CTAs, highlights, active states
Secondary
#546a82
Supporting text, borders
Identity variants
Primary Light
#2b4d78
Hover states, dark-mode primary
Primary Dark
#132440
Dark backgrounds, navbar, code blocks
Accent Light
#ea7558
Accent hover on dark, syntax strings
Accent Dark
#b84228
Accent pressed state
Accent Solid
#cc4528
Fill for solid controls with white text (4.72:1)

Step roles#

Every scale uses the same twelve roles in the same order:

StepsRoleUsed for
1–2App and subtle backgroundsPage and section surfaces
3–5Element backgroundsComponent fills, hover, active
6–8BordersSubtle, default, and strong borders
9–10SolidSolid fills and their hover state
11–12TextLow-emphasis and high-emphasis text
Only 11 and 12 are text steps

Steps 8, 9, and 10 are border and solid-surface roles. They are not held to text contrast thresholds and must not be used for body text. If you need dimmer text than step 11, define a dedicated token and verify its contrast — see the code-comment token for a worked example.

Midnight#

Midnight light · step 9 #1d3352
1 App bg
2 Subtle bg
3 Elem bg
4 Hover bg
5 Active bg
6 Subtle brd
7 Border
8 Strong brd
9 Solid bg
10 Solid hvr
11 Lo text
12 Hi text
Midnight dark · step 9 #1d3352
1 App bg
2 Subtle bg
3 Elem bg
4 Hover bg
5 Active bg
6 Subtle brd
7 Border
8 Strong brd
9 Solid bg
10 Solid hvr
11 Lo text
12 Hi text

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.

Orange#

Orange light · step 9 #e05232
1 App bg
2 Subtle bg
3 Elem bg
4 Hover bg
5 Active bg
6 Subtle brd
7 Border
8 Strong brd
9 Solid bg
10 Solid hvr
11 Lo text
12 Hi text
Orange dark · step 9 #e05232
1 App bg
2 Subtle bg
3 Elem bg
4 Hover bg
5 Active bg
6 Subtle brd
7 Border
8 Strong brd
9 Solid bg
10 Solid hvr
11 Lo text
12 Hi text

Orange is the accent. It marks the primary action, the active state, and little else. Step 9 (#e05232) is constant across appearances.

Do

Use orange for the single most important action in a view, active navigation state, and focus emphasis.

Don't

Use orange for large background fills, body text, or more than one competing call to action on the same screen.

Slate#

Slate light · step 9 #546a82
1 App bg
2 Subtle bg
3 Elem bg
4 Hover bg
5 Active bg
6 Subtle brd
7 Border
8 Strong brd
9 Solid bg
10 Solid hvr
11 Lo text
12 Hi text
Slate dark · step 9 #546a82
1 App bg
2 Subtle bg
3 Elem bg
4 Hover bg
5 Active bg
6 Subtle brd
7 Border
8 Strong brd
9 Solid bg
10 Solid hvr
11 Lo text
12 Hi text

Slate is the secondary — supporting text, borders, and neutral surfaces. Step 9 (#546a82) is the brand secondary and is constant across modes.

Terminal#

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.

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

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.

Terminal chrome#

The accent gets no ANSI slot, because it is not semantic — it marks where you are. That, and the surfaces around the sixteen, live here.

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

Every non-background value clears 4.5:1 against the surface; the measured floor is 4.95:1. ANSI 0 bright is the one deliberate exception — programs use it for box drawing and rules, not for text.

Configuration for tmux, WezTerm, Kitty, Ghostty, iTerm2, and Zellij is on the Terminal theming page.

Contrast rules#

  • Use semantic foreground roles for text. The fixed white token is valid on measured solid controls; screen copy uses the appearance’s foreground role.
  • Step 9 is constant across modes. The solid accent does not shift when the theme changes.
  • Body text targets 4.5:1, large text (≥24px, or ≥18.66px bold) targets 3:1.
  • Verify against the actual surface. A step that passes on the app background may fail on an elevated panel.

Where an identity colour cannot carry text#

Being the brand colour does not make a value a legible background. White on orange-9 (#e05232) measures 3.87:1 — fine as a mark or a border, but below the floor for button labels. Rather than dilute the accent, the system adds a separate fill for that job:

TokenHexWith white text
--color-accent#e052323.87:1 — identity only, not for text
--color-accent-solid#cc45284.72:1 — solid controls
--color-accent-dark#b842285.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.

Semantic colours are mode-specific#

The callout hues are tuned for dark text on tinted light backgrounds. Used as foregrounds on the dark app surface they fall below AA, so dark mode has its own set:

Success
5.03:1 6.43:1
#17945f · #d1ebe0 · fg #17734c #6cc090 · #16302a
A completed, evidenced check
Warning
5.01:1 6.81:1
#ef8b0b · #fff1e0 · fg #b3520c #eda44a · #3a2611
A condition to assess. Gold, never the accent
Danger
5.32:1 6.40:1
#d92d20 · #fce8e8 · fg #b3261e #f08b80 · #3a1c19
A failure needing a next action
Info
4.73:1 6.04:1
#2563c9 · #dae2ec · fg #2f5fa8 #8aacc8 · #1a2b3e
Neutral context, no judgement
RoleLight solidLight tint foregroundDark 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.

Data visualisation#

A chart palette introduces no new colours. It is a set of rules for which existing steps may sit beside each other in a plot, and — more usefully — for when colour stops being the right tool.

Categorical: three series#

SeriesTokenHexOn white
1--midnight-9#1d335212.75:1
2--orange-9#e052323.87:1
3--slate-9#546a825.58:1

One step-9 solid per family, in that order. All three clear the 3:1 non-text contrast floor against a white plot area, so a bar or a line is visible without a border.

Assign them in order and keep the assignment stable across every chart in a deck or a dashboard: if midnight is “cloud” on slide four, it is “cloud” on slide nine. A series that changes colour between charts costs the reader more than a fourth series would have gained them.

Three-series grouped bars — the whole categorical palette
Cloud Agentic AI Agile
Orange and slate differ in hue, not in value

orange-9 against slate-9 measures 1.44:1. On screen they are easy to tell apart — orange against blue-grey is also one of the safest pairs for the common colour-vision deficiencies. Printed in greyscale, or on a projector with the colour turned down, they merge.

So when exactly two series are being compared, use midnight-9 and orange-9 (3.29:1) and leave slate for the third. And direct-label every series — the legend is the fallback, not the mechanism.

There is no fourth series#

The palette stops at three, and extending it is the wrong fix. The step-6 tier is not an option: midnight-6 and slate-6 measure 1.03:1 against each other — the same colour, for practical purposes — and all three step-6 values sit at 1.8–2.0:1 against white, below the 3:1 floor for a mark you have to see.

When a chart has more than three categories, one of these is the answer:

  • Group the tail. Rank the categories and collapse everything past the third into “Other”. If the fourth is genuinely interesting, it is the subject of its own chart.
  • Small multiples. One chart per category, same axes, same scale. Reading eight small charts is faster than decoding an eight-colour legend.
  • Direct labelling with one highlight. Draw every series in slate-7, draw the one being discussed in orange-9, and label it in place. This is the house style for a line chart in a deck — one idea per slide holds for charts too.
  • Stop using colour. A ranked bar chart in a single colour, sorted by value, answers “which is biggest” better than any palette does.

Sequential and ordinal scales#

A magnitude scale uses one family, steps 3 through 8:

--midnight-3 → --midnight-4 → --midnight-5 → --midnight-6 → --midnight-7 → --midnight-8

Six levels, stepping evenly in luminance (each 1.06–1.40× its neighbour) — which is what makes the ramp readable as an ordered scale rather than as six colours.

Step 9 is not the top of that ramp. It is 3.87× darker than step 8, which is a jump the eye reads as a category boundary rather than one more level. Use it deliberately for exactly that: a four-bucket choropleth of 3 · 5 · 7 · 9, where the top bucket is meant to separate itself. Do not append it to a six-level heatmap.

Steps 3–7 are all below 3:1 against a white plot area, so a sequential fill needs an edge: give the plot a 1px slate-4 cell grid, or the reader loses the boundary between a light cell and the page.

For a diverging scale — where the middle is neutral and both ends are extreme — run midnight-8 → midnight-3 → orange-3 → orange-8, with --midnight-1 at the midpoint. Never build a diverging scale from success and danger: those hues carry a judgement, and “below average” is not “wrong”.

Chart furniture#

ElementValue
Axis line, ticks--slate-5
Grid lines--slate-3, horizontal only
Axis labels, legend--slate-11, 12px
Value labels--midnight-12, 12px, IBM Plex Mono
Plot backgroundnone — the page surface
Annotation, callout rule--orange-9

Numbers are set in IBM Plex Mono, right-aligned, for the same reason table numerics are: digits have to line up to be compared.

Do

Keep categorical charts to three series and label them directly. Hold a series' colour constant across a deck. Use one family’s steps 3–8 for magnitude.

Don't

Invent a fourth categorical colour from the step-6 tier, rely on a legend as the only way to identify a series, append step 9 to a sequential ramp, or build a diverging scale from the success and danger hues.

Three appearances#

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.

Buttons#

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
VariantFillBorderTextContrastUse
Primarymidnight-9nonewhite12.75:1Default action
Accentaccent-solid #cc4528nonewhite4.72:1The single most important action
Outlinetransparent1.5px orange-9orange-115.13:1Secondary action
Ghosttransparent1px borderslate-9—Tertiary, toolbars
Danger--color-dangernone--on-solid-dangerAppearance-specificDestructive 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.

Inputs#

Default height 40px (sm 32, lg 48). Source Sans 3 at 14px, a 1.5px semantic border, and the accent at 12% as the 3px focus halo.

Text, textarea, select
States — default, focus, error, disabled
Default semantic border
Focus accent border + 3px tint
Error Agent identifier is incomplete.
Disabled slate-4 bg, cursor not-allowed
Checkbox, radio, toggle, slider
On Off

Full rules — labels, validation, focus — are on Forms.

Cards#

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.

Basic, with image, with actions
Validate & deploy
Runs policy checks before promoting to staging.
Image / diagram
Architecture
Nodes, flows, and policy gates.
Agent: auditor
Idle for 2 hours.

Tables#

Header cells use the overline style. Striping uses the subtle background step, never a border-only rule. Wide tables scroll inside their own container.

Data table with sort indicator
PipelinePolicyAgentsStatus
validate-deploystrict2Healthy
nightly-auditadvisory1Degraded
release-trainstrict4Blocked

Table with search and filters#

A toolbar above the table carries the search field and filter chips. Active filters are shown as removable chips so the current view is always legible — never leave a filter applied with no visible indication.

Search, filter chips, pagination footer
PipelinePolicyOwnerStatus
release-trainstrictplatformBlocked
nightly-auditstrictsecurityFailed
2 of 48 pipelines · 2 filters applied

Multi-level headers#

Grouped columns use a two-row header. The group row is centred over its span and separated by a vertical rule — the only place the system uses one.

Two-row header with column groups
PipelineThis weekLast week
RunsPassp95RunsPassp95
validate-deploy41298.1%4m12s38897.4%4m40s
nightly-audit771.4%18m03s785.7%17m22s
release-train1492.9%9m51s1291.7%10m08s

Grouped rows and totals#

Row groups act as sub-headings inside the table. A totals row is separated by a 2px rule — heavier than the body rules, so it reads as a summary rather than another record.

Row groups, indented children, totals
WorkloadRunsCost
Platform
validate-deploy412€48.10
release-train14€12.60
Security
nightly-audit7€21.40
secret-scan96€9.05
Total529€91.15

Wide tables#

A wide table scrolls inside its own container, never the page, and pins its first column so the row identity stays visible while scrolling.

Horizontal scroll with a sticky first column
PipelinePolicyOwnerRunsPassp50p95RegionStatus
validate-deploystrictplatform41298.1%2m01s4m12seu-centralHealthy
nightly-auditstrictsecurity771.4%11m40s18m03seu-westDegraded
Do

Right-align numeric columns and set them in IBM Plex Mono so digits line up. Show the active filter state and the result count together.

Don't

Add vertical rules between ordinary columns, let a wide table scroll the whole page, or apply a filter without a visible chip saying so.

Navigation#

The navbar follows the colour mode: midnight-1 at 88% alpha in light, midnight-dark-1 at 88% in dark, both over a 12px backdrop blur so content scrolling underneath stays legible. It is separated by a 1px border, not a fill — the header is chrome, and a solid midnight band at the top of every page spends the brand’s darkest surface on navigation.

The active link carries a 2px accent-solid underline plus the high-emphasis text step. The underline is accent-solid rather than orange-9 because it sits directly against 13px text and is read as part of it.

Navbar — follows the colour mode; 1px border, 2px accent underline on the active link
projectious.work Documentation Pipelines Agents

This specimen mirrors .td-navbar — the header at the top of this page. Switch the theme and both change together.

Breadcrumbs, tabs, pagination, sidebar
DocsInterfaceComponents
13px, secondary
OverviewRunsSettings
accent underline on active
current in midnight-9
Foundations
Logo
Interface
active: 600 weight

Alerts and feedback#

Four semantic colours as a 4px left border on a tinted background. The hues are mode-specific — see Colour.

Alerts
Info
Pipeline requires at least one validation agent.
Success
All checks passed. Deployment ready.
Warning
Agent "monitor" has been idle for 2 hours.
Danger
Policy violation — deployment blocked.
Badges, progress, spinner
Default Urgent v0.1.0 Breaking
Semantic colour is not decoration

Success, warning, and danger carry meaning. Do not use them to add visual variety to neutral content.

Modals and overlays#

Modal radius 13px with shadow-3; scrim is midnight at 40% alpha. Focus is trapped while open and restored to the trigger on close; Esc always closes.

Dialog, dropdown, tooltip
Delete pipeline?
This removes the pipeline and its run history. This cannot be undone.
Re-run
Duplicate
Export logs
Last run 4 minutes ago12px, 3px radius

Data display#

Avatar, stat card, tags
JD AS
1,284
Runs this week
▲ 12% vs last week
Composable Agent-first Humanistic
List and timeline
validate-deploy4m ago
nightly-audit2h ago
release-train1d ago
Policy check passed
09:14
Deploying to staging
09:16
Production
pending
Status indicators — always a dot plus a label
Healthy Degraded Failed Idle
Do

Pair every status dot with a text label, so meaning does not depend on colour perception.

Don't

Use a bare coloured dot, or rely on red/green alone to distinguish states.

Code and terminal#

The code surface is 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.

What the theme owns#

The theme implements the reusable documentation product rather than one particular site’s content.

Theme responsibilitySite responsibility
Header, navigation, footer, search, breadcrumbs, and table of contentsInformation architecture, page titles, weights, and copy
Brand token namespaces and responsive shellSite-specific token additions through public hooks
Standard Markdown rendering and public shortcodesDomain specimens and examples composed from those primitives
HTML, Markdown, print, LLMS, and search-index outputsCorrect output configuration and content metadata
Public layout blocks and partial extension pointsNarrow overrides with a documented reason

Do not copy internal theme partials into a site. A copied internal file stops receiving fixes and silently becomes a fork.

Install and pin#

Use Hugo Modules and record the exact release in go.mod.

go
module github.com/projectious-work/example-site

go 1.25.0

require github.com/projectious-work/brand-theme-hugo-vanilla v0.3.2
yaml
module:
  imports:
    - path: github.com/projectious-work/brand-theme-hugo-vanilla

Fetch the module, then commit both module files.

console
hugo mod get github.com/projectious-work/brand-theme-hugo-vanilla@v0.3.2
hugo mod tidy

Run the development server from the Hugo source directory.

console
hugo server --disableFastRender

Configure the site#

The theme builds navigation from Hugo menus. Keep the public header short and stable; this site uses exactly five destinations.

yaml
menus:
  main:
    - name: Getting started
      pageRef: /getting-started
      weight: 10
    - name: Documentation
      pageRef: /docs
      weight: 20
    - name: Contributing
      pageRef: /contributing
      weight: 30
    - name: Downloads
      pageRef: /downloads
      weight: 40
    - name: Legal
      pageRef: /legal
      weight: 50

Use section indexes and page weights for the documentation hierarchy. Keep the header for global destinations; do not duplicate the entire documentation tree there.

Use the public content vocabulary#

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:

  1. Make the source understandable without relying on presentation alone.
  2. Use semantic token names rather than copying colour literals into markup.
  3. Show all appearances when colour or elevation changes meaningfully.
  4. Keep keyboard order equal to DOM order.
  5. Include the specimen’s intent and implementation rule in surrounding copy.
  6. 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.

Extend through supported hooks#

Version 0.3.2 provides public end-of-bundle hooks for site-owned assets.

text
layouts/
└── partials/
    └── hooks/
        ├── scripts-end.html
        └── styles-end.html

Compile a site stylesheet from the style hook:

go-html-template
{{- with resources.Get "scss/specimens.scss" -}}
  {{- $css := . | css.Sass | minify | fingerprint -}}
  <link rel="stylesheet" href="{{ $css.RelPermalink }}"
    integrity="{{ $css.Data.Integrity }}">
{{- end -}}

Load narrowly scoped behaviour from the script hook:

go-html-template
{{- with resources.Get "js/color-swatches.js" -}}
  {{- $js := . | minify | fingerprint -}}
  <script defer src="{{ $js.RelPermalink }}"
    integrity="{{ $js.Data.Integrity }}"></script>
{{- end -}}

Keep these hooks thin. They are integration points, not a place to recreate the theme pipeline.

Override deliberately#

The theme exposes a named main layout block and public partials for supported composition. Before overriding anything:

  1. confirm Markdown, configuration, a shortcode, or a public hook cannot solve the requirement;
  2. identify whether the target is documented as public;
  3. copy the smallest possible unit;
  4. add a comment naming the upstream version and reason;
  5. re-audit the override on every theme update.

If several sites need the same override, propose it upstream. Repetition is evidence that the capability belongs in the theme.

Honour the appearance contract#

The brand has three appearances:

AppearanceSelectorPurpose
Lightdata-theme="light"Bright canvas and paper-like surfaces
Navydata-theme="dark"Default dark product appearance
Deepdata-theme="dark" data-surface="deep"Opt-in near-black focused workspace

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.

Support every output#

The theme can render more than the browser page. Treat these as product surfaces, not build by-products.

OutputReview obligation
HTMLResponsive shell, navigation, search, focus, and all appearances
PrintNo navigation clutter; URLs, tables, and code remain legible
MarkdownMeaning and hierarchy survive without site chrome
LLMSUseful machine-oriented index and page discovery
Search indexTitles, descriptions, and canonical routes are accurate

Only enable outputs the site publishes, but test every enabled format in the release build.

Upgrade the theme#

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:

  1. read the release notes and compare the module graph;
  2. build from a clean destination;
  3. review header, navigation, search, tables, callouts, code, and forms;
  4. test light, navy, and deep appearances at narrow and wide widths;
  5. test keyboard navigation, 200% zoom, reduced motion, and strong focus;
  6. inspect print, Markdown, LLMS, and search-index outputs;
  7. run link, contrast, and accessibility checks;
  8. 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.

Acceptance checklist#

  • Theme module is pinned to v0.3.2 and both module files are committed.
  • Header contains Getting started, Documentation, Contributing, Downloads, and Legal in that order.
  • Site additions use public hooks, blocks, partials, and shortcodes only.
  • Light, navy, and deep appearances use semantic roles rather than inversion.
  • Code and terminal examples include their defined light treatments.
  • Tabler icons remain outline, 1.5px stroke, and use the approved size set.
  • All enabled human and machine outputs build successfully.
  • Narrow/wide, keyboard, zoom, motion, focus, contrast, and print checks pass.
  • Local overrides have an owner, reason, and upstream-version review note.

Licensing

A split licence — brand assets are proprietary, code and tokens are MIT.

The repository carries two categories of intellectual property under different terms. The full text is in LICENSE.md; this page summarises it.

Brand assets — proprietary, source-available#

Everything representing the visual identity: logos, brand imagery, and the supplied design-system documents.

PermittedDownload and view for personal reference, editorial reporting, or authorised partner use
ProhibitedModification, derivative works, sale, or commercial use — including merchandise or software featuring these assets — without prior written consent
No impersonationUse 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.

Code, scripts, and tokens — MIT#

Source code, automation scripts, and configuration files (.py, .js, .sh, .json, .yaml) are MIT-licensed: use, copy, modify, merge, publish, distribute, sublicense, and sell, provided the copyright and permission notice travel with the software.

The design tokens — documented under Tokens — are in this category. The values are free to use; the marks are not.

What this means in practice#

Do

Use the token files, the spacing scale, and the component measurements in your own work. Link to this documentation. Write about the brand editorially.

Don't

Ship the logo in your product, sell anything carrying the marks, or present modified brand assets as projectious.work’s.

For commercial inquiries or brand consent: info@projectious.work

Lockups

Four approved lockups, rendered live, and where each one belongs.

Four lockups are approved. Each has a defined context; using one outside its context is what makes a layout feel off-brand even when the colours are right.

All four are built with HTML and CSS rather than flattened artwork, so spacing comes from natural text flow and never needs re-kerning.

The four lockups#

Two-line (default) and one-line
projectious · work Two-line · headers, slides, cards · min 120px
projectious · work One-line · navbars, footers, signatures · min 140px
Dot-replace and stacked
projectious work Dot-replace · compact horizontal · min 160px
projectious · work Stacked · app icons, avatars, slide corners · min 60px
LockupCompositionUse forMinimum width
Two-lineMark + “projectious” over “· work”Headers, slides, cards — the default120px
One-lineMark + “projectious · work” inlineNavbars, footers, signatures140px
Dot-replaceMark substitutes the dot in “projectious.work”Compact horizontal contexts160px
StackedMark above the wordmark, centredApp icons, avatars, slide corners60px

On light and dark#

Every lockup has a light-surface and a dark-surface rendering. The mark’s inner cuts pick up the surface colour, so it never needs a plate or a knockout box.

The same lockup on both surfaces
projectious · work
projectious · work

The mark alone#

The mark may stand alone only where the wordmark is already established nearby — an app icon on a page that names the product, an avatar in a signed-in context, a favicon.

Standalone mark — 96 · 64 · 48 · 32 · 24 · 16px

Minimum for the standalone mark is 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.

Product line#

Product names extend the wordmark with the same dot construction and flow.

Product-line extensions
projectious · guard projectious · forge projectious · flow

Wordmark construction#

The separator between “projectious” and “work” is a dot in the mark’s own geometry, not a typed period. It takes slate on light surfaces and midnight-11 on dark. Spacing is handled by flex layout so the wordmark reflows naturally — do not hand-kern it or convert it to a static image.

The wordmark keeps the accent

“work” is set in orange-9 (#E05232). As body text that value would be too low-contrast, but WCAG 2.1 SC 1.4.3 exempts logotypes — “text that is part of a logo or brand name has no minimum contrast requirement”. The wordmark is the one place the identity accent is used as letterforms.

Do

Use the two-line lockup unless the space is explicitly horizontal. Let the flex layout handle spacing.

Don't

Rebuild a lockup by hand, change the order of mark and wordmark, or set the wordmark in a different typeface.

Status treatments

Text-first maturity labels for truthful portfolio presentation.

Required pattern#

Every project presentation must show a status as text. Color, icons, position, and motion may reinforce the label but must never replace it.

StatusUse whenSupporting cue
Usable projectDocumented users can complete the stated core task.Solid dot
Working prototypeA real implementation runs, with known limits.Half-filled dot
Applied researchEvidence or experiments are the primary output.Diamond
Supporting assetThe repository enables another project.Square
Idea / implementation startingScope exists; implementation is absent or partial.Ring
ArchivedWork is preserved but no longer maintained.Horizontal bar

Do not substitute broad claims such as “platform”, “production-ready”, or “enterprise” unless the owning repository publishes evidence for that claim. Prefer narrow artifact categories: CLI, process layer, applied research, prototype, library, or supporting asset.

Accessibility#

  • Keep the full status label visible at all viewport sizes.
  • Pair every color with the status-specific shape in the table.
  • Use normal text contrast of at least 4.5:1 and large-text/UI contrast of at least 3:1.
  • Test light and dark variants independently.
  • Do not encode a status change with animation alone.

The source templates use high-contrast neutral text and reserve accent color for a supporting rule. Maintainers must rerun local validation after changing colors.

Project examples#

Examples are prompts for maintainers, not permanent factual claims. Confirm each status and limitation with the owning repository before publishing.

ProjectNarrow categoryExample status lineRequired limitation
aiboxCLIUsable projectState supported hosts and providers.
processkitProcess layerWorking prototypeState current API/version constraints.
ai-market-researchApplied researchApplied researchState dataset date and methodology limits.
KubeClawPrototypeIdea / implementation startingState 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.

The page shell#

Three regions, always in this order in the DOM:

RegionWidthContents
Sidebar224px, fixedBrand lockup, pj-sidebar items, account block pinned to the bottom
Header60px, fixed heightPage title, one line of context, search, and one accent action
Contentfills, scrollsThe 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.

The KPI row#

Directly under the header: four stat cards in a repeat(4, 1fr) grid, --space-4 gap.

Four, not three and not six. Three leaves a hole in a 12-column grid; six turns the row into a wall of numbers nobody reads. If there are five things worth measuring, the fifth one is not a KPI.

Each card is a pj-stat — the number first in Plus Jakarta Sans 800, its label under it in the overline style, then a delta line. The pj-stat carries its own border and radius, so it is not nested inside a pj-card; that would draw the box twice.

A delta states its direction in text or an arrow, not in colour. The default __delta is the success hue; a delta that is not good news takes an explicit colour, and a delta that is merely neutral takes the muted foreground rather than borrowing a semantic one.

KPI row — value, overline label, delta with an explicit direction
6
Active engagements
▲ 1 this month
412
Agent hours
▲ 18% vs last week
3.2h
Time to audit
▼ 0.6h faster
5
Open findings
2 need review

Primary content and secondary panel#

Under the KPI row, a 1.5fr / 1fr split: the thing the page is about on the left, the thing that gives it context on the right.

  • Primary is the record set — a table in a pj-table-shell, with its toolbar, filter chips, and pagination footer.
  • Secondary is a pj-card holding a feed: a pj-timeline or pj-list of recent events, each with a status dot and a text label.

align-items: start, so the two panels are independent — the feed does not stretch to match a long table, and the table does not gain whitespace to match a short feed.

The split is 1.5fr / 1fr because the primary panel holds tabular data with four or more columns and the secondary holds one column of prose. An even 1fr / 1fr starves the table and pads the feed.

At md the ratio flattens to 1fr / 1fr; below md the secondary panel moves below the primary, in source order.

Reading the whole thing at once#

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.

Overview
Engagements
Agents
Reports
Settings
Engagements overview
Cloud · Agile · Agentic AI
6
Active engagements
▲ 1 this month
412
Agent hours
▲ 18% vs last week
3.2h
Time to audit
▼ 0.6h faster
5
Open findings
2 need review
ClientPracticeStatusProgress
Nordic Freight AGCloudOn track68%
Halvorsen BankAgentic AIAt risk41%
Meridian RetailAgileOn track82%
Ferra InsuranceAgentic AIBlocked23%
Agent activity
Compliance agent flagged a missing audit trail
4 min ago
Migration plan v3 approved for Nordic Freight
32 min ago
Spec-from-issue agent opened 3 draft PRs
1h ago
Nightly audit completed with no findings
5h ago

Marketing page#

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
Practice areasHow we 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.

# Agent-validated deploy
policy: strict
agents: [auditor, deployer]
✓ 12 checks passed · 1.2s
01Do more with more 02Specialized beats generic now 03Provider independence 04We run what we recommend
Ready to redesign how you work?30-minute introduction. No deck.
projectious·work Cloud · Agile · Agentic AI · 2026

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.

Density#

One pattern, two densities. The measurements on the components page are the comfortable density and are the default. A compact density exists for screens whose job is scanning many rows at once — an audit log, a run history:

ComfortableCompact
Table row padding12px8px
Control height40px32px
Card padding24px16px
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.

AppearanceSelectorPageRaisedSubtleBorder
Lightdata-theme="light"#f8f9fb#ffffff#f0f3f8#cdd0d5
Navy dark, default darkdata-theme="dark"#132440#1a2b3e#20354d#2e4b68
Deep darkdata-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 surface Canvas · subtle · border Ready
Navy · default dark
Raised surface Canvas · subtle · border Ready
Deep · opt in
Raised surface Canvas · subtle · border Ready

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.

Why navy is the default dark#

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.

Implementation#

html
<html data-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.

Surface ownership#

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 contract#

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.

Audio and video

Sonic identity and video production rules.

Audio identity#

Four defined cues. All are short, tonal, and mid-register — the audio equivalent of “slides into place, doesn’t bounce”.

CueSoundLength
NotificationShort tonal chime, mid-register200ms
SuccessAscending C→E, warm sine wave—
ErrorSingle low A2300ms
Podcast introAmbient pad, tension→warmth resolve3s

Error is a single low note, not a buzz or a descending pair — it marks the event without scolding.

Video rules#

  • Demos run at real time. No fast-forward, no speed ramps. If a step is slow, that is information the viewer needs.
  • Transitions are a cut or a 200ms fade. No swoops, wipes, or 3D moves.
  • All video uses midnight surfaces.
  • Video never autoplays. Playback begins with a deliberate user action.
  • Subtitles are always available.
  • Export at 1080p minimum, 4K when possible.
Do

Show the real thing at real speed. Caption everything.

Don't

Speed up a demo to hide latency, or use a transition that draws attention to itself.

Email signature

A signature that survives every mail client, carries the details business mail is required to carry, and stays quiet on a white message.

Email is not the web. Every value below is a literal, every layout is a table, and nothing depends on a stylesheet the client is free to discard.

Two forms are supplied:

FormUse for
FullFirst contact, and any message leaving the organisation — it carries the legally required details
ShortReplies and internal mail, where the full block repeated down a thread becomes noise

The signature#

Full form — a white message is the design constraint
Jane Doe
Principal Consultant
Projectious GmbH
+49 30 000 000 00  ·  jane@projectious.work
projectious.work
Musterstraße 1, 10115 Berlin, Germany
Projectious GmbH · Registered seat Berlin
Amtsgericht Charlottenburg, HRB 000000 · VAT ID DE000000000
Managing director: Jane Doe
This message is intended only for the addressee and may contain confidential information. If you received it in error, please tell the sender and delete it.
Short form — replies and internal mail
Jane Doe
Principal Consultant · Projectious GmbH
jane@projectious.work  ·  projectious.work

Both specimens are rendered on white, because that is where a signature actually lives. The surrounding page has a colour mode; a mail message generally does not.

One rule, and it is vertical#

A signature accumulates dividers as it accumulates fields — one under the name, another above the legal block, another before the disclaimer — and each reads as a section break in a message that is not sectioned. On a white background the result is a small form stapled to the bottom of a letter.

So there are no horizontal rules at all. One vertical accent rule runs down the left of the whole block: it marks the signature as a unit, ties it to the brand, and costs one line instead of four. Everything else is separated by space and by type size.

Do

Group the fields — identity, contact, address, legal — and separate the groups with about 9px of space. Let the type sizes do the ranking.

Don't

Add a rule between groups, a coloured band behind the name, social icon rows, a quotation, or a “please consider the environment” line.

Fields#

Replace every [PLACEHOLDER]. Delete any row that does not apply — the block is built so that removing a row leaves no gap behind it.

FieldRequiredNotes
Full nameAlways15px, 700, midnight-9
Job titleAlwaysIts own line — a title merged into the company line reads as part of the company
Company legal nameAlwaysThe registered name, not the trading shorthand, wherever the legal block appears
PhoneRecommendedWith country code; a signature is often read from another country
Email addressAlwaysEven though it is in the header — messages get forwarded
WebsiteAlways
Postal addressRegionally requiredStreet, postcode, city, country
Register detailsRegionally requiredLegal form, registered seat, register court and number, VAT ID, directors
Confidentiality noticeOptionalTwo lines at most; delete it unless your organisation requires it

What the law asks for#

A starting point, not legal advice — confirm against your own jurisdiction before shipping a signature.

JurisdictionBusiness email must carry
GermanyLegal form, registered seat, register court and number, and every managing director (§ 35a GmbHG, § 37a HGB)
EU / UKCompany registration number, registered office address, and VAT number where registered
United StatesNo statutory signature requirement; confidentiality notices are convention rather than law
CanadaCASL requires sender identification and a postal address on commercial messages
AustraliaACN or ABN on business correspondence

Construction#

RuleWhy
Table layout, role="presentation"Flexbox and grid do not survive Outlook’s Word renderer; the role stops screen readers announcing a data table
White declared on table, row, and cellA client repainting for dark mode decides what to invert per element. White on the cell alone leaves the table around it unpainted — see below
Each background written twice, as bgcolor and background-colorThe background shorthand alone is not enough: several clients strip shorthands and keep longhands, and Outlook’s renderer prefers the attribute
Inline styles onlyMost clients strip <style>, and none support custom properties — so no var(--pj-*) anywhere
Web-safe font stackBrand faces will not load in a mail client; the stack degrades to a system sans without changing the layout
Maximum width 520pxBelow the 600px that forces horizontal scrolling in narrow reading panes
Literal hex valuesThe brand steps, written out — see the table below
No imagesA hosted logo is blocked by default in many clients and leaves a broken frame. The accent rule carries the brand with nothing to load
No background imagesIf a fill is ever needed, use the bgcolor attribute

Declaring white three times is not belt-and-braces#

It is the difference between a signature that survives dark mode and one that does not, and it is the most common way this block breaks.

html
<table … bgcolor="#ffffff" style="…background:#ffffff;background-color:#ffffff;…">
  <tr bgcolor="#ffffff" style="background-color:#ffffff">
    <td bgcolor="#ffffff" style="background:#ffffff;background-color:#ffffff;…">

A client that repaints a message for dark mode works element by element: it looks at each one, decides whether it has a background it should keep, and inverts the rest. Declare white only on the <td> and the <table> around it has no background to keep — so the cell stays white while its own container is repainted dark. The result is a white island with a dark halo, or worse, the dark text inverted to light and then drawn on the white cell that was kept.

Every enclosing element that has a painted area needs the declaration, and each one needs it in both forms: the bgcolor attribute, which Outlook’s renderer prefers, and background-color, because several clients strip the background shorthand while keeping longhands.

Dark mode will recolour this, and that is fine

Several clients — Outlook mobile and some webmail among them — invert or shift signature colours in dark mode, and none can be reliably prevented from doing so. The signature is therefore built to survive recolouring rather than to fight it: the hierarchy is carried by size and weight as much as by hue, so it still reads when the palette is altered.

Colours#

Three families, which is what a signature should have.

RoleValueUse
Heading #1d3352The name
Primary text #142438Title, phone
Secondary #546a82Company, address
Link #3a5a82Email, website
Legal and notice #546a82Register details, confidentiality notice
Accent #E05232The vertical rule, and nothing else

Before shipping a change#

  • Send to Outlook (Windows), Gmail web, Apple Mail, and one mobile client.
  • Reply to that message twice and check how the signature looks quoted inside a thread.
  • Turn on the client’s dark mode and confirm the hierarchy still reads.
  • Forward it, to confirm nothing depends on the original message’s styles.

Social previews

Repository artwork that states category and maturity plainly.

Content model#

Every 1280×640 preview contains only:

  1. projectious.work organisation mark and name.
  2. Project name.
  3. One narrow artifact category.
  4. One complete maturity label from the status vocabulary.
  5. 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.

Export and validation#

Run:

sh
bash scripts/validate-portfolio-assets.sh
bash scripts/export-portfolio-assets.sh

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.

Implementation contract#

What owns what#

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

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

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

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.

Inputs and ownership#

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

Non-negotiable rules#

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

The palette#

Sixteen colours#

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

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

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

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

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

Chrome#

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

Optional light companion#

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.

SlotNormalSlotBright
0 black#1e2b388 bright black#4a5e74
1 red#b8352f9 bright red#a8261c
2 green#27675410 bright green#1f7a52
3 yellow#8b650811 bright yellow#6f5106
4 blue#3a5a8212 bright blue#1d3352
5 magenta#8a3f6e13 bright magenta#752f5c
6 cyan#1c6b6b14 bright cyan#125555
7 white#546a8215 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.

Syntax in the terminal#

An editor running inside a terminal paints code from the sixteen ANSI slots, not from a stylesheet. Since the syntax roles were reassigned by measured perceptual distance, seven of the nine now resolve to an ANSI slot exactly — so a file open in Helix, Neovim or Vim under this palette looks like the same file on the documentation site.

Syntax roleValueANSI slot
Plain and variables #c5daf015 · bright white
Keywords and modifiers #d491b413 · bright magenta
Types and classes #6cc09010 · bright green
Functions and methods #8aacc812 · bright blue
Decorators and macros #74c0c914 · bright cyan
Operators and punctuation #97a8b87 · white
Invalid and deprecated #e55b5b1 · red
Strings #ea7558— brand orange-dark-10
Numbers and constants #e0a92a11 · 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#

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

Required capability#

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

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

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

Theme#

bash
# ~/.tmux.conf — projectious.work

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

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

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

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

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

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

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

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

Status-bar frameworks#

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

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

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

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

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

WezTerm#

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

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

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

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

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

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

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

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

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

Kitty#

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

font_family      IBM Plex Mono
font_size        13.0
disable_ligatures always

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

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

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

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

# Splits
active_border_color    #e05232
inactive_border_color  #7b8da3
window_padding_width   6

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

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

Ghostty#

Ghostty reads a plain key = value file and supports named themes, so the palette is a separate file that the config selects. That is the idiomatic split: the theme file carries only colour, and the config carries everything else.

ini
# ~/.config/ghostty/themes/projectious — colour only, nothing else
palette = 0=#0e1720
palette = 1=#e55b5b
palette = 2=#3f9d74
palette = 3=#c08a1e
palette = 4=#6289b3
palette = 5=#bd6d96
palette = 6=#3f97a3
palette = 7=#97a8b8
palette = 8=#2e4b68
palette = 9=#f08b80
palette = 10=#6cc090
palette = 11=#e0a92a
palette = 12=#8aacc8
palette = 13=#d491b4
palette = 14=#74c0c9
palette = 15=#c5daf0

background = #0e1720
foreground = #c5daf0

cursor-color = #e05232
cursor-text = #0e1720

selection-background = #20354d
selection-foreground = #c5daf0
ini
# ~/.config/ghostty/config — projectious.work

theme = projectious

font-family = IBM Plex Mono
font-size = 13
font-feature = -liga
font-feature = -calt

cursor-style = block
cursor-opacity = 1

# The accent marks the focused split, and nothing else in the chrome.
split-divider-color = #7b8da3
unfocused-split-fill = #0e1720

window-padding-x = 6
window-padding-y = 6
window-padding-balance = true

link-url = true

theme also takes an absolute path, which is the better form for a config checked into a dotfiles repository: theme = /Users/you/dotfiles/ghostty/projectious.

Ghostty’s theme accepts a light:…,dark:… pair that follows the system appearance. Do not use 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:

SettingRequiredWhy
minimum-contrast1Anything 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-opacity1Below 1 puts whatever is behind the window into every ratio on this page
background-blurfalseOnly applies under transparency, and blurring the desktop behind the text does not make the ratio measurable again
unfocused-split-opacity1Ghostty fades unfocused splits by default. The faded text is still text, and at the default it no longer clears the floor
ini
minimum-contrast = 1
background-opacity = 1
background-blur = false
unfocused-split-opacity = 1

Leaving unfocused-split-opacity at its default is the most common way this theme fails review: the split you are not looking at is the one you are reading a stack trace in.

Ghostty has no bold-brightening setting to disable — it renders bold as a bold face and leaves the colour alone, which is the behaviour the other emulators on this page need to be told to adopt.

Check the parsed result rather than the file, since an unknown key is skipped rather than reported:

sh
ghostty +show-config | grep -E 'palette|background|foreground|contrast|opacity'
ghostty +validate-config

iTerm2#

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

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

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

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

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

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

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

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

Zellij#

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

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

theme "projectious"

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

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

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

Verify#

Palette#

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

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

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

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

States to review#

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

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

Release evidence#

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

Sources and upgrade boundary#

  • tmux manual
  • WezTerm colour configuration
  • Kitty configuration
  • Ghostty configuration reference
  • iTerm2 documentation
  • Zellij theme configuration

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.

Not permitted#

  • Using the logo as your own, or as part of your own logo.
  • Using the brand name in a way that suggests partnership or official status without a signed agreement.
  • Registering domain names, social handles, or app names that include the trademarks in a confusingly similar way.

Permitted#

  1. Linking to the official website.
  2. Press articles or blog posts about the project.
  3. Technical documentation that refers to the tools.

These are nominative uses — referring to the thing by its name. They do not require permission, and they do not grant any broader licence.

When in doubt, ask

Unauthorised use of the trademarks may result in a request for removal. Contact info@projectious.work before shipping anything you are unsure about — asking first is always cheaper than a takedown.

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.

Families and required cuts#

FamilyRequired cutsRoleLicence
Plus Jakarta Sans400 · 500 · 600 · 700 · 800 uprightDisplay, headings, controlsSIL OFL 1.1
Source Sans 3400 · 500 · 600 uprightBody, UI, descriptionsSIL OFL 1.1
IBM Plex Mono400 · 500 · 600 · 700 upright and italicCode, data, terminalSIL OFL 1.1

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.

The ramp#

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" })
RoleSizeWeightLine height
Display48 px8001.1
H136 px7001.15
H228 px7001.2
H324 px6001.25
H420 px6001.3
H517 px6001.35
Body large18 px4001.65
Body16 px4001.6
Caption13 px4001.5
Overline12 px6001.3
Code14 px4001.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.

Rules#

  • Tighten display tracking by -0.5px. Keep body tracking normal.
  • Use positive tracking only for overline labels: 12 px, 600, uppercase, 0.08em.
  • Use body large for standfirsts and body for sustained reading. Do not shrink prose to make a fixed box fit.
  • Use at most two sizes on one slide.
  • Keep system fallbacks, but never introduce a fourth brand family.
  • Do not disable font-synthesis as a substitute for shipping the required files; ship the cuts the semantics require.
Do

Pair an overline with a heading, keep the ramp intact, and prove every fixed-size artifact at 200% text and with loose text spacing.

Don't

Set body copy in Plus Jakarta Sans, headings in Source Sans 3, or code in a generic monospace when the brand font is available.

Usage

Clear space, minimum sizes, monochrome variants, and placing the mark on colour and photography.

Clear space#

Exclusion zone — 1x the icon height on every side
Nothing enters the dashed zone

Minimum clear space is 1× the icon height on all sides. Nothing — text, rules, image edges, other logos — enters that exclusion zone.

At a 40px mark, that is 40px of clear space in every direction. The zone scales with the mark, so it never needs recalculating per context.

Minimum sizes#

Minimum sizes — 48 / 32 / 24 / 16px
48px
32px
16px — minimum
16px — favicon only
VariantMinimum
Mark (standalone)16px
Any lockup with wordmark24px
Stacked lockup60px
Two-line lockup120px
One-line lockup140px
Dot-replace lockup160px

Below these sizes the petal cuts collapse into a smudge. If the space is smaller than the minimum, use a smaller variant rather than scaling one down.

Monochrome#

Four monochrome treatments are approved, for print, fax, embossing, and any single-colour reproduction:

Monochrome variants
Midnight
Black
White reversed
Grey
VariantMarkInner linesSurface
Midnight#1d3352WhiteLight
Black#000000WhiteLight
White reversed#ffffffSlateMidnight
Grey#9299a4WhiteLight

The white-reversed variant uses slate for its inner lines rather than the background colour, so the petal structure stays visible. All petal structure must remain legible in every monochrome variant — a solid silhouette is not an approved treatment.

On colour#

Placing the mark on colour
Lighter than slate-5 -> light mark
Darker -> dark mark
On accent → white ring
On photography -> midnight backing
BackgroundTreatment
Lighter than slate-5Light-surface mark
Darker than slate-5Dark-surface mark
Accent (#E05232)Mark with a white ring border
PhotographySemi-transparent midnight backing behind the mark

The slate-5 threshold is the decision rule: measure the background, pick the side it falls on. On the accent, neither variant has enough separation on its own, which is why the ring exists.

On photography#

Never place the mark directly on an unmodified photograph. Apply a semi-transparent midnight backing — a shape, a scrim, or a gradient — so the mark sits on a controlled surface. The backing is part of the logo treatment, not an optional enhancement.

Watermark#

Watermark — uniform wash, no accent

For background use, the mark may run as a watermark at 8–15% opacity. It renders as a uniform wash: no accent colour in watermark mode, since a single coloured element at low opacity reads as a printing error.

Favicons#

Icon-only, on a midnight tile, with 12% padding. Corner radius scales with size:

Favicon tiles — 12% padding, radius scales with size
48px / r8
32px / r4
16px / r2
SizeRadius
16px2px
32px4px
48px8px
180px36px
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.

Structure#

600px, centred on a neutral surround, in this order:

BandFillContents
PreheaderhiddenOne sentence, ~90 characters — see below
Mastheadmidnight-9The wordmark. Nothing else
LedewhiteIssue overline, headline, and a standfirst that says what is inside
SectionswhiteNumbered overline, heading, two or three sentences
Call to actionwhiteOne bulletproof button
Footermidnight-9Identity, 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 preheader#

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
<div style="display:none;max-height:0;overflow:hidden;opacity:0;">
  Cloud · Agile · Agentic AI — this month: three agents worth stealing, and
  why generic SaaS is losing its edge.
</div>

Leave it out and the client fills the space with whatever text comes first, which is usually the wordmark followed by “View in browser”.

The bulletproof button#

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]><!-->
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
  <tr><td bgcolor="#cc4528" style="border-radius:6px;" align="center">
    <a href="https://…" target="_blank" style="display:block;padding:13px 26px;
       font-family:Arial,Helvetica,sans-serif;font-size:14px;font-weight:bold;
       color:#ffffff;text-decoration:none;mso-line-height-rule:exactly;">Read the case study</a>
  </td></tr>
</table>
<!--<![endif]-->

Three things are load-bearing:

  • The fill is #cc4528, not #E05232. White on the identity accent is 3.87:1; on accent-solid it is 4.72:1. The same rule as solid controls in the interface, and email is where it matters most, because a mail client will not offer a hover state to compensate.
  • 44px tall. A mail is read on a phone more often than not, so the touch floor is the binding constraint.
  • arcsize="9%" approximates the 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.

Colour in a message#

The signature’s colour table holds, with two additions a whole message needs:

RoleValueUse
Body text #142438Paragraphs — midnight-12, never #333 and never #000
Section overline #c04424The numbered section labels — orange-11, 5.13:1 on white
Button fill #cc4528With white text, 4.72:1
Footer text #c5daf0On the midnight-9 footer, 8.90:1
Footer link #f0a48cUnsubscribe, on midnight — 6.31:1
Hairline #e5e3deSection dividers, as a 1px table row
Page surround #f8f9fbmidnight-1, behind the 600px white sheet so the raised surface retains an edge

The accent appears as text in the overlines and as a fill on the one button — and #E05232 itself appears nowhere carrying text, in either role.

The footer is not decoration#

Commercial mail has to say who sent it, why it arrived, and how to stop it. Those three facts are the footer’s whole job:

  • Who — the legal entity and its postal address.
  • Why — one sentence: “Sent to you because you subscribed to Field notes.”
  • How to stop — an unsubscribe link that works in one click and does not require signing in.

An unsubscribe that leads to a login screen is, in most of the jurisdictions on the signature page, not an unsubscribe.

Before sending#

  • Send to Outlook (Windows), Gmail web, Apple Mail, and one mobile client. The VML path only exists for the first of those, so it only gets tested there.
  • Check the preheader in the inbox list, not just in the opened message.
  • Turn images off. The message must still make complete sense — which is why there are none in the layout to begin with.
  • Turn dark mode on. Clients recolour aggressively; as with the signature, the hierarchy is carried by size and weight so it survives.
  • Click the unsubscribe link from a logged-out browser.
Do

Write the preheader. Keep the message at one call to action. Fill solid buttons with accent-solid. State why the reader is receiving it.

Don't

Ship a styled <a> as the only button, use #E05232 behind white text, rely on an image to carry the message, or hide the unsubscribe behind a login.

Code

The default dark code surface, optional light-panel companion, and measured syntax roles.

Code blocks are dark by default#

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:

js
// Agent pipeline definition
const pipeline = createPipeline({
  name: "validate-deploy",
  policy: "strict",
  agents: ["auditor", "deployer"],
});

Syntax theme#

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.

Ten roles, not twenty-two tokens#

Editors do not describe code with six token types. The Language Server Protocol defines 22 semantic token types and 10 modifiers, and TextMate grammars — the model behind VS Code, Sublime Text, and most highlighters — define 11 root scopes with a deep sub-scope tree under each.

A theme should not answer that with twenty-two colours. Past roughly nine, hue stops being a signal: everything is coloured, so nothing is marked. The two scope vocabularies are therefore grouped into ten roles, and the modifiers are carried by weight and slant rather than by more hue.

Every role is an existing brand or terminal value. The expansion introduced no new colour — the terminal palette had already added the two hues, cyan and magenta, that a syntax theme needs and the three interface scales do not have.

RoleOn surfaceLSP semantic tokenTextMate scope
Plain and variables
#c5daf0 midnight-dark-12
The default. Anything the reader does not need to pick out.
11.74:1variable · parameter · property · enumMembervariable · variable.parameter · variable.other
.n .nv .nx .py .vc .vg .vi
Keywords and modifiers
#d491b4 terminal magenta (bright)
Also structural keys — a YAML key is the keyword of its line.
6.82:1keyword · modifierkeyword.control · storage.modifier · storage.type
.k .kc .kd .kn .kp .kr .nt .na
Types and classes
#6cc090 terminal 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:1type · class · struct · interface · enum · typeParameter · namespaceentity.name.type · entity.name.class · support.class
.kt .nc .nn .ne .bp
Functions and methods
#8aacc8 midnight-dark-11 / terminal blue (bright)
Callables use the established informational blue, leaving yellow exclusively to numbers and constants.
7.06:1function · methodentity.name.function · support.function
.nf .fm
Decorators and macros
#74c0c9 terminal 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.
8.11:1macro · decorator · evententity.name.tag · meta.decorator · support.macro
.nd .ni .nl .cp .cpf
Strings
#ea7558 orange-dark-10
Interpolation delimiters take the operator colour, so the expression inside stays readable as code.
5.76:1stringstring.quoted · string.interpolated · string.regexp
.s .s1 .s2 .sa .sb .sc .se .sh .si .sr .ss .sx .dl
Numbers and constants
#e0a92a warning-dark
Literal values, including true/false/nil.
8.50:1number · regexpconstant.numeric · constant.language · constant.character
.m .mb .mf .mh .mi .mo .il .no
Operators and punctuation
#97a8b8 slate-dark-11
Present but recessive — structure you read past, not at.
6.90:1operatorkeyword.operator · punctuation
.o .ow .p
Comments
#7d90a3 code-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.
5.19:1commentcomment.line · comment.block · comment.block.documentation
.c .ch .cm .c1 .cs .sd
Invalid and deprecated
#e55b5b terminal red (normal)
Deprecated is struck through as well as coloured — the state does not depend on hue.
4.79:1(modifier) deprecatedinvalid.illegal · invalid.deprecated
.err

Optional light-panel companion#

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:

RoleTokenValue
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:

css
.code--light {
  background: var(--terminal-light-surface);
  color: var(--syntax-plain-light);
}

.code--light pre {
  background: transparent;
  color: var(--syntax-plain-light);
  padding: 0;
}

Do not reuse the dark syntax values on this surface and do not make the light set the automatic counterpart of dark mode.

Modifiers are not colours#

LSP modifiers combine with any token type: ten modifiers against ten roles is a hundred states. Hue cannot carry that, so it does not try.

LSP modifierTreatmentWhy
declaration · definitionWeight 500Where a name is introduced, distinguished from where it is used.
deprecatedLine-throughA state, not a category — it must survive greyscale.
documentationItalic, comment colourDoc comments are comments; they are not a separate hue.
readonly · static · abstract · async · defaultLibraryNo distinct colourTen 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.

Why comments have a dedicated token#

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.

Worked examples#

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.

C#

Dark · default
/* Ring buffer — fixed capacity, no allocation after init. */
#include <stdint.h>
#define RING_CAP 256          // macro: a decorator-role token

typedef enum { RING_OK = 0, RING_FULL = 1 } ring_status_t;

typedef struct {
    uint8_t  data[RING_CAP];
    size_t   head, tail;
    _Bool    wrapped;
} ring_t;

static inline size_t ring_len(const ring_t *r) {
    return (r->head - r->tail) & (RING_CAP - 1);
}

static const char *RING_TAG = "ring\n";   // string literal

ring_status_t ring_push(ring_t *restrict r, uint8_t byte) {
    // Reject when one slot short of capacity, so head never meets tail.
    if (ring_len(r) == RING_CAP - 1) return RING_FULL;
    r->data[r->head++ & (RING_CAP - 1)] = byte;
    return RING_OK;
}
Light · optional
/* Ring buffer — fixed capacity, no allocation after init. */
#include <stdint.h>
#define RING_CAP 256          // macro: a decorator-role token

typedef enum { RING_OK = 0, RING_FULL = 1 } ring_status_t;

typedef struct {
    uint8_t  data[RING_CAP];
    size_t   head, tail;
    _Bool    wrapped;
} ring_t;

static inline size_t ring_len(const ring_t *r) {
    return (r->head - r->tail) & (RING_CAP - 1);
}

static const char *RING_TAG = "ring\n";   // string literal

ring_status_t ring_push(ring_t *restrict r, uint8_t byte) {
    // Reject when one slot short of capacity, so head never meets tail.
    if (ring_len(r) == RING_CAP - 1) return RING_FULL;
    r->data[r->head++ & (RING_CAP - 1)] = byte;
    return RING_OK;
}

C++#

Dark · default
// Policy-based cache. Types, templates, and a lambda.
#include <string>
#include <unordered_map>

namespace projectious::cache {

template <typename Key, typename Value>
class LruCache final {
public:
    explicit LruCache(std::size_t capacity) noexcept : capacity_{capacity} {}

    [[nodiscard]] auto get(const Key& key) const -> const Value* {
        const auto it = entries_.find(key);
        return it == entries_.end() ? nullptr : &it->second;
    }

    void put(Key key, Value value) {
        static constexpr auto kTag = "lru";  // string literal
        // Evict before insert so size never exceeds the capacity.
        if (entries_.size() >= capacity_) evict();
        entries_.emplace(std::move(key), std::move(value));
    }

private:
    void evict() noexcept { /* … */ }

    std::size_t capacity_{0};
    std::unordered_map<Key, Value> entries_{};
};

}  // namespace projectious::cache
Light · optional
// Policy-based cache. Types, templates, and a lambda.
#include <string>
#include <unordered_map>

namespace projectious::cache {

template <typename Key, typename Value>
class LruCache final {
public:
    explicit LruCache(std::size_t capacity) noexcept : capacity_{capacity} {}

    [[nodiscard]] auto get(const Key& key) const -> const Value* {
        const auto it = entries_.find(key);
        return it == entries_.end() ? nullptr : &it->second;
    }

    void put(Key key, Value value) {
        static constexpr auto kTag = "lru";  // string literal
        // Evict before insert so size never exceeds the capacity.
        if (entries_.size() >= capacity_) evict();
        entries_.emplace(std::move(key), std::move(value));
    }

private:
    void evict() noexcept { /* … */ }

    std::size_t capacity_{0};
    std::unordered_map<Key, Value> entries_{};
};

}  // namespace projectious::cache

Python#

Dark · default
"""Pipeline stages and their policy gates."""

from __future__ import annotations

import functools
from dataclasses import dataclass, field
from typing import Final, Iterable

# Retry budget is a policy decision, not a tuning knob.
MAX_RETRIES: Final[int] = 3
DEFAULT_POLICY = "strict"


@dataclass(frozen=True, slots=True)
class Stage:
    """A single stage. Immutable once constructed."""

    name: str
    policy: str = DEFAULT_POLICY
    retries: int = 0
    tags: list[str] = field(default_factory=list)

    @property
    def is_strict(self) -> bool:
        return self.policy == "strict"

    @staticmethod
    def parse(raw: str) -> "Stage":
        name, _, policy = raw.partition(":")
        return Stage(name=name.strip(), policy=policy or DEFAULT_POLICY)


@functools.lru_cache(maxsize=None)
def validate(stages: Iterable[Stage]) -> bool:
    for stage in stages:
        if stage.retries > MAX_RETRIES:
            raise ValueError(f"{stage.name!r} exceeds {MAX_RETRIES} retries")
    return True
Light · optional
"""Pipeline stages and their policy gates."""

from __future__ import annotations

import functools
from dataclasses import dataclass, field
from typing import Final, Iterable

# Retry budget is a policy decision, not a tuning knob.
MAX_RETRIES: Final[int] = 3
DEFAULT_POLICY = "strict"


@dataclass(frozen=True, slots=True)
class Stage:
    """A single stage. Immutable once constructed."""

    name: str
    policy: str = DEFAULT_POLICY
    retries: int = 0
    tags: list[str] = field(default_factory=list)

    @property
    def is_strict(self) -> bool:
        return self.policy == "strict"

    @staticmethod
    def parse(raw: str) -> "Stage":
        name, _, policy = raw.partition(":")
        return Stage(name=name.strip(), policy=policy or DEFAULT_POLICY)


@functools.lru_cache(maxsize=None)
def validate(stages: Iterable[Stage]) -> bool:
    for stage in stages:
        if stage.retries > MAX_RETRIES:
            raise ValueError(f"{stage.name!r} exceeds {MAX_RETRIES} retries")
    return True

Rust#

Dark · default
//! Policy evaluation for pipeline stages.

use std::collections::HashMap;
use std::fmt::{self, Display};

const MAX_RETRIES: u32 = 3;

/// How strictly a stage is evaluated.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Policy {
    Strict,
    Advisory,
}

#[derive(Debug, Default)]
pub struct Stage<'a> {
    pub name: &'a str,
    pub policy: Option<Policy>,
    pub retries: u32,
}

impl<'a> Stage<'a> {
    // Strict by default: a gate that is not configured should fail closed.
    pub fn new(name: &'a str) -> Self {
        Self { name, policy: Some(Policy::Strict), retries: 0 }
    }

    pub fn validate(&self) -> Result<(), String> {
        if self.retries > MAX_RETRIES {
            return Err(format!("{} exceeds {MAX_RETRIES} retries", self.name));
        }
        Ok(())
    }
}

impl Display for Policy {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", match self { Policy::Strict => "strict", _ => "advisory" })
    }
}
Light · optional
//! Policy evaluation for pipeline stages.

use std::collections::HashMap;
use std::fmt::{self, Display};

const MAX_RETRIES: u32 = 3;

/// How strictly a stage is evaluated.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Policy {
    Strict,
    Advisory,
}

#[derive(Debug, Default)]
pub struct Stage<'a> {
    pub name: &'a str,
    pub policy: Option<Policy>,
    pub retries: u32,
}

impl<'a> Stage<'a> {
    // Strict by default: a gate that is not configured should fail closed.
    pub fn new(name: &'a str) -> Self {
        Self { name, policy: Some(Policy::Strict), retries: 0 }
    }

    pub fn validate(&self) -> Result<(), String> {
        if self.retries > MAX_RETRIES {
            return Err(format!("{} exceeds {MAX_RETRIES} retries", self.name));
        }
        Ok(())
    }
}

impl Display for Policy {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", match self { Policy::Strict => "strict", _ => "advisory" })
    }
}

Go#

Dark · default
// Package pipeline evaluates stages against their policy gates.
package pipeline

import (
	"errors"
	"fmt"
)

const MaxRetries = 3

// Policy is how strictly a stage is evaluated.
type Policy int

const (
	Strict Policy = iota
	Advisory
)

var ErrTooManyRetries = errors.New("stage exceeds retry budget")

type Stage struct {
	Name    string `json:"name"`
	Policy  Policy `json:"policy"`
	Retries int    `json:"retries,omitempty"`
}

func (s *Stage) Validate() error {
	if s.Retries > MaxRetries {
		return fmt.Errorf("%q: %w", s.Name, ErrTooManyRetries)
	}
	return nil
}

func ValidateAll(stages []Stage) (ok bool, err error) {
	for i := range stages {
		if err = stages[i].Validate(); err != nil {
			return false, err
		}
	}
	return true, nil
}
Light · optional
// Package pipeline evaluates stages against their policy gates.
package pipeline

import (
	"errors"
	"fmt"
)

const MaxRetries = 3

// Policy is how strictly a stage is evaluated.
type Policy int

const (
	Strict Policy = iota
	Advisory
)

var ErrTooManyRetries = errors.New("stage exceeds retry budget")

type Stage struct {
	Name    string `json:"name"`
	Policy  Policy `json:"policy"`
	Retries int    `json:"retries,omitempty"`
}

func (s *Stage) Validate() error {
	if s.Retries > MaxRetries {
		return fmt.Errorf("%q: %w", s.Name, ErrTooManyRetries)
	}
	return nil
}

func ValidateAll(stages []Stage) (ok bool, err error) {
	for i := range stages {
		if err = stages[i].Validate(); err != nil {
			return false, err
		}
	}
	return true, nil
}

Java#

Dark · default
package work.projectious.pipeline;

import java.util.List;
import java.util.Objects;

/** A single pipeline stage and its policy gate. */
public final class Stage implements Comparable<Stage> {

    public static final int MAX_RETRIES = 3;
    public static final long TIMEOUT_MS = 30_000L;
    public static final int MASK = 0xFF;

    private final String name;
    private final Policy policy;
    private int retries = 0;
    private double budget = 12.60;

    public Stage(String name, Policy policy) {
        this.name = Objects.requireNonNull(name, "name");
        this.policy = policy;
    }

    @Override
    public int compareTo(Stage other) {
        return this.name.compareTo(other.name);
    }

    @Deprecated(since = "2.0", forRemoval = true)
    public boolean isStrict() {
        return policy == Policy.STRICT;
    }

    public void validate(List<String> errors) throws IllegalStateException {
        if (retries > MAX_RETRIES) {
            throw new IllegalStateException("%s exceeds %d retries".formatted(name, MAX_RETRIES));
        }
    }

    public enum Policy { STRICT, ADVISORY }
}
Light · optional
package work.projectious.pipeline;

import java.util.List;
import java.util.Objects;

/** A single pipeline stage and its policy gate. */
public final class Stage implements Comparable<Stage> {

    public static final int MAX_RETRIES = 3;
    public static final long TIMEOUT_MS = 30_000L;
    public static final int MASK = 0xFF;

    private final String name;
    private final Policy policy;
    private int retries = 0;
    private double budget = 12.60;

    public Stage(String name, Policy policy) {
        this.name = Objects.requireNonNull(name, "name");
        this.policy = policy;
    }

    @Override
    public int compareTo(Stage other) {
        return this.name.compareTo(other.name);
    }

    @Deprecated(since = "2.0", forRemoval = true)
    public boolean isStrict() {
        return policy == Policy.STRICT;
    }

    public void validate(List<String> errors) throws IllegalStateException {
        if (retries > MAX_RETRIES) {
            throw new IllegalStateException("%s exceeds %d retries".formatted(name, MAX_RETRIES));
        }
    }

    public enum Policy { STRICT, ADVISORY }
}

Assembly (NASM)#

Dark · default
; Sum a byte array. rdi = pointer, rsi = length, returns in rax.
        section .data
msg:    db  "sum: ", 0
LEN     equ 5

        section .text
        global  sum_bytes

sum_bytes:
        xor     rax, rax            ; accumulator
        test    rsi, rsi
        jz      .done               ; empty input

.loop:
        movzx   rdx, byte [rdi]
        add     rax, rdx
        inc     rdi
        dec     rsi
        jnz     .loop

.done:
        ret
Light · optional
; Sum a byte array. rdi = pointer, rsi = length, returns in rax.
        section .data
msg:    db  "sum: ", 0
LEN     equ 5

        section .text
        global  sum_bytes

sum_bytes:
        xor     rax, rax            ; accumulator
        test    rsi, rsi
        jz      .done               ; empty input

.loop:
        movzx   rdx, byte [rdi]
        add     rax, rdx
        inc     rdi
        dec     rsi
        jnz     .loop

.done:
        ret

Bash#

Dark · default
#!/usr/bin/env bash
# Validate a pipeline definition and promote it when the gates pass.
set -euo pipefail

readonly MAX_RETRIES=3
readonly POLICY="${PIPELINE_POLICY:-strict}"
declare -A GATE_STATUS=()

log() { printf '%s  %s\n' "$(date -u +%FT%TZ)" "$*" >&2; }

validate_stage() {
    local -r name="$1" retries="${2:-0}"
    if (( retries > MAX_RETRIES )); then
        log "ERROR ${name} exceeds ${MAX_RETRIES} retries"
        return 1
    fi
    GATE_STATUS["$name"]="ok"
}

main() {
    local -a stages=("validate" "deploy")
    for stage in "${stages[@]}"; do
        validate_stage "$stage" 0 || exit 1
    done
    log "policy=${POLICY} stages=${#stages[@]}"
}

main "$@"
Light · optional
#!/usr/bin/env bash
# Validate a pipeline definition and promote it when the gates pass.
set -euo pipefail

readonly MAX_RETRIES=3
readonly POLICY="${PIPELINE_POLICY:-strict}"
declare -A GATE_STATUS=()

log() { printf '%s  %s\n' "$(date -u +%FT%TZ)" "$*" >&2; }

validate_stage() {
    local -r name="$1" retries="${2:-0}"
    if (( retries > MAX_RETRIES )); then
        log "ERROR ${name} exceeds ${MAX_RETRIES} retries"
        return 1
    fi
    GATE_STATUS["$name"]="ok"
}

main() {
    local -a stages=("validate" "deploy")
    for stage in "${stages[@]}"; do
        validate_stage "$stage" 0 || exit 1
    done
    log "policy=${POLICY} stages=${#stages[@]}"
}

main "$@"

LaTeX#

Dark · default
\documentclass[11pt,a4paper]{article}
\usepackage[utf8]{inputenc}
\usepackage{amsmath}

% Pipeline notation used throughout the paper.
\newcommand{\stage}[2]{\ensuremath{#1 \xrightarrow{#2}}}

\title{Policy Gates in Composable Pipelines}
\author{Jane Doe}

\begin{document}
\maketitle

\section{Definitions}
A stage $s_i$ passes when its retry count $r_i \leq 3$:
\begin{equation}
    \forall s_i \in S : r_i \leq R_{\max}, \quad R_{\max} = 3
\end{equation}

\begin{itemize}
    \item \textbf{Strict} — the gate fails closed.
    \item \emph{Advisory} — the gate records and continues.
\end{itemize}

\end{document}
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 \leq 3$:
\begin{equation}
    \forall s_i \in S : r_i \leq R_{\max}, \quad R_{\max} = 3
\end{equation}

\begin{itemize}
    \item \textbf{Strict} — the gate fails closed.
    \item \emph{Advisory} — the gate records and continues.
\end{itemize}

\end{document}

Markdown#

Dark · default
---
title: Stage reference
weight: 10
---

# Stage reference

A stage passes when its retry count stays at or below **three**. See the
[policy guide](../policy/) for the full rules.

## Fields

| Field | Type | Default |
|---|---|---|
| `name` | string | — |
| `policy` | enum | `strict` |

> Advisory gates record a failure and continue. Strict gates fail closed.

1. Validate the configuration
2. Request promotion
3. Deploy

```sh
pipeline validate --policy strict
```

<!-- Deprecated: `--legacy-gate` is removed in 2.0. -->
Light · optional
---
title: Stage reference
weight: 10
---

# Stage reference

A stage passes when its retry count stays at or below **three**. See the
[policy guide](../policy/) for the full rules.

## Fields

| Field | Type | Default |
|---|---|---|
| `name` | string | — |
| `policy` | enum | `strict` |

> Advisory gates record a failure and continue. Strict gates fail closed.

1. Validate the configuration
2. Request promotion
3. Deploy

```sh
pipeline validate --policy strict
```

<!-- Deprecated: `--legacy-gate` is removed in 2.0. -->

JSON#

Dark · default
{
  "$schema": "https://projectious.work/schema/pipeline-2.json",
  "name": "validate-deploy",
  "policy": "strict",
  "retries": 3,
  "enabled": true,
  "owner": null,
  "budget": 12.6,
  "stages": [
    { "name": "validate", "gate": "strict", "timeoutSeconds": 120 },
    { "name": "deploy", "gate": "advisory", "timeoutSeconds": 600 }
  ],
  "tags": ["platform", "eu-central"]
}
Light · optional
{
  "$schema": "https://projectious.work/schema/pipeline-2.json",
  "name": "validate-deploy",
  "policy": "strict",
  "retries": 3,
  "enabled": true,
  "owner": null,
  "budget": 12.6,
  "stages": [
    { "name": "validate", "gate": "strict", "timeoutSeconds": 120 },
    { "name": "deploy", "gate": "advisory", "timeoutSeconds": 600 }
  ],
  "tags": ["platform", "eu-central"]
}

YAML#

Dark · default
# Pipeline definition — one policy gate per stage.
apiVersion: projectious.work/v2
kind: Pipeline
metadata:
  name: validate-deploy
  labels: { team: platform, region: eu-central }

defaults: &defaults
  policy: strict
  retries: 3
  enabled: true

spec:
  <<: *defaults
  budget: 12.60
  owner: ~
  stages:
    - name: validate
      timeoutSeconds: 120
    - name: deploy
      policy: advisory
      timeoutSeconds: 600
  owner: "platform@projectious.work"
  schema: 'https://projectious.work/schema/pipeline-2.json'
  notes: |
    Advisory gates record and continue.
    Strict gates fail closed.
Light · optional
# Pipeline definition — one policy gate per stage.
apiVersion: projectious.work/v2
kind: Pipeline
metadata:
  name: validate-deploy
  labels: { team: platform, region: eu-central }

defaults: &defaults
  policy: strict
  retries: 3
  enabled: true

spec:
  <<: *defaults
  budget: 12.60
  owner: ~
  stages:
    - name: validate
      timeoutSeconds: 120
    - name: deploy
      policy: advisory
      timeoutSeconds: 600
  owner: "platform@projectious.work"
  schema: 'https://projectious.work/schema/pipeline-2.json'
  notes: |
    Advisory gates record and continue.
    Strict gates fail closed.

TOML#

Dark · default
# Pipeline definition — one policy gate per stage.
schema = "https://projectious.work/schema/pipeline-2.json"

[pipeline]
name    = "validate-deploy"
policy  = "strict"
retries = 3
enabled = true
budget  = 12.60
created = 2026-08-02T09:00:00Z
tags    = ["platform", "eu-central"]

[[pipeline.stage]]
name           = "validate"
gate           = "strict"
timeoutSeconds = 120

[[pipeline.stage]]
name           = "deploy"
gate           = "advisory"
timeoutSeconds = 600
Light · optional
# Pipeline definition — one policy gate per stage.
schema = "https://projectious.work/schema/pipeline-2.json"

[pipeline]
name    = "validate-deploy"
policy  = "strict"
retries = 3
enabled = true
budget  = 12.60
created = 2026-08-02T09:00:00Z
tags    = ["platform", "eu-central"]

[[pipeline.stage]]
name           = "validate"
gate           = "strict"
timeoutSeconds = 120

[[pipeline.stage]]
name           = "deploy"
gate           = "advisory"
timeoutSeconds = 600

What each language reaches#

The table is measured from the rendered page, not asserted: every block above is parsed and its emitted token classes are mapped back to the roles. ● means the role appears in that example.

LanguagePlainKeywordTypeFunctionMacroStringNumberOperatorCommentReached
C●●●●●●●●●9/9
C++●●●●●●●●●9/9
Python●●●●●●●●●9/9
Rust●●●●●●●●●9/9
Go●●●●·●●●●8/9
Java●●●●●●·●●8/9
Bash●●··●●●●●7/9
Assembly (NASM)●●●●●●●●●9/9
LaTeX●●···●●·●5/9
Markdown·●●··●●●●6/9
JSON·●···●●●·4/9
YAML·●··●●●●●6/9
TOML●●···●●●●6/9

Nine roles rather than ten, because invalid only appears when a grammar actually fails to parse — a correct example cannot demonstrate it.

Where a language falls short, the reason is the language or the lexer:

  • Bash — no type system, and Chroma’s shell lexer does not mark function definitions.
  • Go — Go has no macro or annotation construct; its struct tags are strings.
  • Java — Chroma’s Java lexer emits a plain name for every numeric literal, so numbers cannot be separated. A lexer limitation, not a palette one.
  • LaTeX — No type, callable or operator concept in the grammar — commands are keywords.
  • Markdown — Prose, not code: there is nothing to name, call, or annotate.
  • JSON — By design: no comments, no identifiers, no callables. Keys take the keyword role.
  • YAML — Anchors and merge keys take the macro role; there are no callables or types.
  • TOML — Table headers take the plain role; there are no callables or types.
Two findings worth carrying into any theme

Chroma files C preprocessor directives and Rust attributes under Comment.Preproc, which would colour #define and #[derive(…)] as commentary. They are macros — the LSP says so — and are coloured as macros here.

It also files documentation comments under String.Doc, which would colour a Rust /// line and a Python docstring as data. Both are documentation, and take the comment role.

Inline code#

Inline code does not take the dark block treatment — it follows the surrounding surface. On light surfaces it sits on midnight-2 (light scale) with orange-11 text; in dark mode both values shift to their dark-scale counterparts. It uses IBM Plex Mono at 13px with a 3px radius.

Terminal output#

Terminal blocks use the same dark surface. Prompts take the comment colour, output takes the operator colour, so a transcript stays readable without becoming a second syntax theme.

console
$ hugo --gc --minify
Start building sites …
Total in 842 ms

Diagrams and demos

Evidence-first architecture, flow, screenshot, and demo framing.

Architecture and flow diagrams#

  • Prefer left-to-right flow, explicit boundaries, and named protocols.
  • Distinguish implemented components from planned components with text: implemented, external, or planned.
  • Add a caption with repository, source path, commit or release, and capture date.
  • Never infer architecture from a social preview or marketing page.
  • Keep editable source beside the export and document the export command.

Suggested caption:

Source: owner/repository, docs/architecture.svg, release vX.Y.Z, captured YYYY-MM-DD. Planned elements are labelled.

The three diagram types#

“Prefer diagrams over photographs” is only useful if the diagrams exist. Consulting deliverables need three, and they answer different questions:

TypeAnswersShape
SequenceWhen — what happens, in what order, between whomActors across the top, lifelines down, labelled messages between them
ArchitectureWhere — what runs, and on whose infrastructureA control plane above, deployment targets below, connected by plain rules
Org chartWho — who owns the work, and who they answer toOne 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.

Screenshots and demos#

  • Show real output from a named build, commit, or release.
  • Frame the output without modifying product state or hiding errors.
  • State sample/synthetic data clearly.
  • Do not fabricate dashboards, terminal output, customer logos, or product UI.
  • Crop personal data, tokens, account identifiers, and unrelated windows.
  • Record capture command, viewport, theme, and date in provenance.

When there is no real output yet, use a text-first project card labelled Idea / implementation starting. A truthful absence is better than a fictional interface.

Files

The canonical SVG, raster, and favicon assets in design system v2.1.1.

Formats#

FormatContentsUse for
SVGVector sourcePrimary for web and print
PNG128px and 256px colour variantsRaster-only contexts
favicon PNG32×32Browser favicon
apple-touch180×180 PNGiOS home screen

SVG is the source. Raster exports exist for contexts that cannot take vector — prefer SVG wherever the medium allows it.

Variants#

The canonical mark variants are:

  • icon-light — for light surfaces
  • icon-dark — for dark surfaces
  • icon-mono-black — single-colour black
  • icon-mono-white — single-colour white, reversed
  • icon-mono-gray — single-colour grey

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.

Summary#

#AreaVerdict
1Company name “projectious”Low risk
2Logo similarity — stencil peony budLow risk
3Font licensingLow risk
4Icon library — TablerLow risk
5Photography and imagery sourcesLow risk
6Other design elementsLow risk

Fonts#

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.

Icons — Tabler#

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.

Photography#

SourceLicenceCostAttributionCommercialRestriction
UnsplashUnsplash LicenseFreeNot requiredAllowedNo reselling as-is; no competing stock service
PexelsPexels LicenseFreeNot requiredAllowedNo reselling; no implying endorsement

Both permit free commercial use without attribution for website imagery, marketing, social posts, presentations, and blog posts. The binding restriction is that you may not compile the images into a competing stock service.

Colour#

Colours cannot be trademarked in the abstract — only in connection with specific goods and services in specific contexts (Tiffany blue for jewellery being the canonical example). The palette (#1d3352 midnight, #E05232 orange, #546a82 slate) uses common design colours and does not infringe any known colour trademark in the IT consulting space.

Recommended actions before launch#

  • Run a registered-trademark search in the operating jurisdictions.
  • Keep the asset provenance inventory current as assets are added.
  • Retain licence notices in source for all third-party dependencies.

Photography and illustration

Sourcing rules, treatment, and the abstract illustration style.

Sources#

Preferred: Unsplash and Pexels — royalty-free, no attribution required for commercial use. Commission custom photography where possible, from local European photographers.

Never use: watermarked Shutterstock or Getty imagery, or AI-generated imagery.

Every photograph must pass the test: “would a developer trust this?” Stock imagery of handshakes in glass lobbies fails it. A real desk, real hardware, or real people working passes it.

Treatment#

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

Illustration#

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.

Priority#

LinkedIn and GitHub first, blog second, Twitter/X as backup. The templates are sized for those surfaces in that order.

Template rules#

  • Backgrounds are midnight or midnight-dark, or white with an accent top bar.
  • projectious.work is always visible.
  • Never place text over a busy background.
  • An accent glow circle at subtle opacity provides visual interest without competing with the text.
  • Typography hierarchy is unchanged: Plus Jakarta Sans for headlines, Source Sans 3 for body.
Open Graph card — 1200x630
Bad software is a decision,
not a constraint.
projectious.work · Cloud · Agile · Agentic AI

Open Graph#

  • 1200×630px.
  • Headline at Display weight, capped at roughly 60 characters — longer headlines are truncated by most platforms anyway.
  • The mark sits bottom-left or top-left, never centred.
  • Test the rendering at 320px wide: most impressions are in a mobile timeline.
Do

Keep one message per card. Check contrast against the actual background, not the design canvas.

Don't

Fill the card with a screenshot, use more than one accent element, or rely on text smaller than 24px at 1200×630.

Space, shape, and motion

The 4px spacing base, the radius and elevation ladders, and the motion tokens.

Spacing#

A 4px base with a nine-step scale. Every margin, padding, and gap resolves to one of these values.

TokenValueTypical use
--space-14pxIcon-to-label gaps
--space-28pxTight component padding
--space-312pxControl padding
--space-416pxDefault element spacing
--space-524pxCard padding, paragraph rhythm
--space-632pxComponent separation
--space-748pxSub-section separation
--space-864pxSection separation
--space-996pxPage-level bands

Content sits inside a 1100px measure. Wider viewports gain margin, not line length.

Radius#

TokenValueApplied to
--radius-sm3pxTags, chips, small indicators
--radius-md6pxButtons, inputs, code blocks
--radius-lg9pxCards, panels
--radius-xl13pxLarge panels, modals
--radius-full9999pxPills, avatars

Radius is a signal of scale: the larger the surface, the larger the radius. Mixing radii on nested surfaces reads as a mistake — a 6px control inside a 9px card is correct; a 13px control inside a 6px card is not.

Elevation#

Four levels. Shadows are soft and low-contrast; the system leans on borders and surface tint before it reaches for shadow.

TokenValueApplied to
--shadow-0noneFlat surfaces, default
--shadow-10 1px 3px rgba(0,0,0,0.06)Cards under hover
--shadow-20 4px 12px rgba(0,0,0,0.08)Popovers, dropdowns
--shadow-30 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.

Motion#

Things slide into place. They do not bounce.

Durations#

TokenValueUse
--duration-micro100msColour and opacity changes
--duration-standard200msHover, focus, small moves
--duration-expand300msHeight and width changes, accordions
--duration-page400msRoute and view transitions

Easing#

TokenValueUse
--ease-outcubic-bezier(0.33, 1, 0.68, 1)Entering — arriving on screen
--ease-incubic-bezier(0.32, 0, 0.67, 0)Exiting — leaving screen
Radius ladder — 3 / 6 / 9 / 13 / full
sm 3px
md 6px
lg 9px
xl 13px
full
Elevation — shadow-0 through shadow-3
level 0 · flat
level 1 · card
level 2 · popover
level 3 · modal
Motion — slides into place, never bounces
200ms · ease-out — entering
100ms · colour and opacity

Motion rules#

  • All animation wraps in @media (prefers-reduced-motion: no-preference), or is disabled under prefers-reduced-motion: reduce.
  • CSS-only for HTML. For React, use Framer Motion with these same timing values.
  • Never animate text character-by-character.
  • Page transitions cap at 400ms.
  • Pressed controls do not scale down. Their colour remains at the hover state.
  • Slide transitions are a cut or 200ms fade. Sequential elements may fade up with 40–80ms staggered delays.
  • Do not use overshoots, parallax, or autoplay video.
css
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}
Do

Use --ease-out for anything appearing and --ease-in for anything leaving. Keep durations on the ladder.

Don't

Add spring or bounce easing, animate layout-shifting properties, or exceed 400ms for any single transition.

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.

One service per sheet#

A one-pager describes one offer. A sheet listing four services is a capabilities brochure, and it is read by nobody, because a reader who wants one of the four has to find it first.

If there are four services, there are four one-pagers. They share the structure below and differ only in their content — which is what makes the structure worth specifying.

The bands#

Four horizontal bands, top to bottom. The sheet is 1000px wide in the HTML source and prints to a single A4 or Letter page.

BandFillJob
Headermidnight-9Wordmark, an overline naming the artefact, the service name at display size, and one sentence of promise
BenefitswhiteThree columns: icon in a tinted tile, a short heading, two lines of body
ProcesswhiteA numbered horizontal sequence — the steps, with a duration under each
Closemidnight-12The one-line commitment, the contact route, and one accent button

Two midnight bands, at the top and the bottom, with white between them. The sheet opens and closes on the brand and leaves the middle to the content — the same shape as a title-and-closer deck, for the same reason.

The header carries a single decorative element: an orange-9 circle at 6% opacity bleeding off the top-right corner. That is the entire ornament budget for the page.

Three benefits, not five#

Three columns fit a 12-column grid at four columns each, three items are readable at a glance, and three is roughly how many distinct reasons a person retains from a page they were handed.

Each benefit is an icon tile, a heading of no more than five words, and body text of no more than two lines. The icon is 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”.

The process band#

A numbered sequence with a connecting rule, each step carrying a duration. The durations are the point: a one-pager that says “we migrate your cloud” competes on adjectives, and one that says “week 3: stand up self-hosted core” competes on a plan.

The numbers are set in IBM Plex Mono inside midnight-9 discs, because they are data. The connector is a 1px slate-5 rule, not an arrow — the numbers already carry the direction.

Do not put a duration on the sheet that the engagement cannot hold. The portfolio principle — make maturity and limitations easier to see, not harder — applies to sales collateral without modification.

One call to action#

The close band gets one accent element. It is a button shape carrying white text, so it fills with accent-solid (#cc4528) and not the identity accent — the same 4.72:1 rule as the interface and the email.

Beside it, exactly one contact route, in mono. Two routes make the reader choose before they act.

Printing it#

The example is HTML, and the export path is the browser’s print dialogue — the same route as the resume template.

  • Colour is not guaranteed. A one-pager is frequently reprinted on an office mono laser. The layout must survive greyscale: the bands separate by value, the numbers stay legible, and no meaning is carried by the accent alone.
  • Turn on background graphics in the print dialogue, or the midnight bands print as white and the reversed text disappears.
  • Check the fold. If the sheet will be folded, nothing important sits within 10mm of the fold line.
  • For volume print, hand the vendor the CMYK values rather than the hex.
Do

Keep one service per sheet, three benefits, and one call to action. Put real durations on the process steps. Check it in greyscale before it is printed.

Don't

List a service catalogue, use stock photography of people in meetings, add a second contact route, or promise a timeline the engagement cannot hold.

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.

Decide the pagination first#

Two kinds of document, and the choice is made before any layout happens:

FlowingExplicitly paginated
ContentOne continuous text flowA fixed set of designed pages
PagesHowever many the text needsA number you chose
PaperReflows onto the reader’s paperDesigned to a fixed page box
ExamplesReport, memo, letter, paperCV, proposal, brief, certificate
RouteLaTeX / Typst templatesThe pattern on this page

Getting this wrong is expensive in one direction: a flowing document forced into fixed pages breaks the moment a paragraph grows, while a designed page that is allowed to reflow loses the arrangement that was its whole purpose.

Never pin a paper size on a flowing document. Letter and A4 differ, the reader’s printer knows which one it has, and the text should reflow onto it. A paginated document does pin a page box, and each page is designed to fill Letter and A4 alike without overlap.

The one-page document#

Header, summary, body sections, and a footer region — with the whole thing constrained to fit a single page rather than allowed to run onto a second.

RegionTreatment
HeaderName at display size in Plus Jakarta Sans 800; role and contact beside it; a 2px midnight-9 rule under the whole band
SummaryOne paragraph at 14.5px. Three sentences at most
SectionsAn accent overline as the section marker, then the content. No boxes, no rules between entries
Two-up regionWhere two short sections sit side by side — skills and education, or scope and budget
Footer regionThe evidence: selected engagements, references, links

The section markers are the only accent on the page, and they are text. A document has no primary action, so there is nothing for a solid accent to mark — an accent-filled band on a CV is decoration claiming to be emphasis.

Entries are separated by space, not by rules. A one-page document accumulates horizontal rules faster than anything else in the system, and each one costs a line of content while adding a section break to a document with one section per heading — the same argument the signature makes, on a bigger sheet.

Dates and other data are set in IBM Plex Mono and right-aligned against the entry title, so a reader scanning chronology has a column to scan.

Fitting one page#

The constraint that makes this pattern hard is that the content has to end where the page does.

  • Cut content, not type. 14.5px body and 13.5px entries are the floor. A document set at 11px to fit is a document that will not be read.
  • Cut the oldest entry first. A CV is not an archive; a proposal is not a contract.
  • The two-up region is the pressure valve. Two short sections side by side recover roughly a third of a page against stacking them.
  • Check it at 100%. A browser zoomed to 90% will happily show you a page that overflows in print.

Exporting#

The example is HTML on the paged-document shell; the export route is the browser’s print dialogue, to PDF.

  • Print backgrounds on, margins default, headers and footers off — the browser’s own header would print a URL across the top of a CV.
  • Send the PDF, not the HTML. The HTML depends on a font load and a runtime; the PDF depends on nothing.
  • Check the PDF at 100% and printed on plain paper before it goes anywhere.

Reusing the pattern#

The resume is the worked example, but the pattern is not resume-specific. A one-page proposal, a project brief, and a decision memo are the same document with different section names:

DocumentHeaderSectionsFooter region
CVName and contactExperience · Skills · EducationSelected engagements
ProposalClient and dateScope · Approach · Timeline · PriceAssumptions and exclusions
MemoSubject and dateContext · Options · RecommendationDecision 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.

Library#

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.

Tabler outline · 1.5 px stroke · 24 px grid
device-desktop
pencil
search
versions
tag
Size and state
16 · inline
20 · button
24 · navigation
danger · meaningful

Geometry#

PropertyRule
Native grid24 × 24 px
Stroke1.5 px, including icons supplied at 2 px
Caps and joinsRound
FillNone
Sizes16 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.

Colour and state#

  • Default icons use the secondary foreground (slate-11).
  • Active or selected icons use the primary foreground (midnight-9 on light).
  • Danger uses the danger token only when danger is the icon’s meaning.
  • Colour never decorates an otherwise neutral list of categories.
  • A status icon is always paired with a label, shape, or other non-colour cue.

Delivery#

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.

Core slides#

Six of the twelve slide types cover most decks. Build with these first and reach for the other six only when the content actually calls for one — the full table is below.

Core slide types — 1280×720 · 16:9
Redesigning work
Cloud · Agile · Agentic AI
projectious.work
Bad software is a decision, not a constraint.
The agent-first approach
Define Build Deploy
"We run what we recommend."
// pipeline
createPipeline({
  policy: "strict"
});
Let's talk
projectious.work

The full deck#

Twelve slide types cover a complete talk. Each is 1280×720 (16:9) and uses the same tokens as the product surfaces — no separate “presentation theme”.

Opening — title, agenda, section statement
Redesigning work
Cloud · Agile · Agentic AI
projectious.work
Agenda
01 Why agent-first
02 Architecture
03 Operating model
04 What it costs
Bad software is a decision, not a constraint.
Content — three-up, split, comparison
The agent-first approach
Define Build Deploy
One idea per slide.
Before
Manual gates, tribal knowledge, slow feedback.
After
Policy as code, repeatable runs, fast feedback.
Where the time goes
Review42%
Build31%
Deploy27%
Evidence — metric, quote, code, diagram
1,284
Runs this week
"We run what we recommend."
— projectious.work
// pipeline
createPipeline({
  policy: "strict"
});
Architecture
DefineBuildDeploy
Closing — CTA and contact
Let's talk
projectious.work
Thank you
info@projectious.work

The twelve types#

Core marks the six that cover most decks. Example marks the types the worked deck below actually demonstrates.

SlideCoreExamplePurpose
Title●●Deck opening — display type on midnight
Agenda●Numbered outline; current item in midnight-11
Section statement●●A single sentence marking a new section
Three-up●●Three parallel points
SplitBefore/after or contrast, one half on midnight
Comparison●A small table where numbers are the point
Metric●One number, large; the label in overline style
Quote●●A single quotation with the accent rule, attributed
Code showcase●●Code on the default-dark surface
DiagramLine-based system diagram, accent on the focal node
CTA closer●●The one thing you want the audience to do
ContactThank-you and a single contact route

A real deck#

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.

Rules#

  • 1280×720, 16:9 aspect ratio.
  • Title slides and section statements use midnight surfaces; content slides use white or midnight-1.
  • projectious.work is visible on the title and closing slides.
  • One idea per slide. If a slide needs a paragraph, it needs to be two slides.
  • Use no more than two type sizes on one slide.

Animation#

  • Slide transitions: cut or 200ms fade.
  • Build animations use --ease-out at --duration-standard.
  • Never animate text character-by-character.
  • Motion is for revealing structure, not for holding attention.

Templates and export targets#

Document templates for LaTeX and Typst are in 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.

RouteUseNotes
HTML deck → presentTalks you give yourselfThe example deck. Keyboard navigation and speaker notes included
HTML deck → print to PDFSending a deck to be readLandscape, background graphics on, margins none
Slides / PowerPoint / KeynoteDecks the client will editBuild the twelve types as masters; do not rebuild them per deck
LaTeX / TypstDocuments, not decksDocument 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.

Fonts#

Self-hosted from Fontsource release 5.3.0. Each file is a Latin WOFF2 subset, used without modification, and ships with its SIL OFL 1.1 licence.

FileSHA-256Source package
plus-jakarta-sans-latin-variable.woff2153fc85b70298beeb1d61a5f723331649e7f23bb77302a66e61cb3e2fbdb5e79@fontsource-variable/plus-jakarta-sans@5.3.0
source-sans-3-latin-variable.woff27a19a7027e125257d310c6dbd78ae3a30b5ea1e3794d60b12bb28227a003bfda@fontsource-variable/source-sans-3@5.3.0
ibm-plex-mono-latin-400.woff208949f728dc52d528e69b1667d15c89a5686a4ee9a296ff90983985f99c380f7@fontsource/ibm-plex-mono@5.3.0
ibm-plex-mono-latin-500.woff201d285447409c8a588692162439a038b8cbd7871309ee20267b0d2d91c6e8e22@fontsource/ibm-plex-mono@5.3.0

Icons and photography#

Neither is bundled in this repository. Both are cleared for future use:

SourceLicenceAttributionCommercial
Tabler IconsMITNot required in UIAllowed
UnsplashUnsplash LicenseNot requiredAllowed
PexelsPexels LicenseNot requiredAllowed

Original work#

All logo files, design tokens, document templates, and design-system documents are original work, © projectious.work, under the brand-asset terms in Licensing.

Review status#

Last reviewed 2026-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.

Breakpoints#

Four breakpoints, declared in the synchronized design-system token sheet and normative here:

NameMin widthThe layout it describes
sm640pxLarge phone, landscape phone
md768pxSmall tablet — the first two-column layout
lg1024pxTablet landscape, small laptop — sidebars appear
xl1280pxDesktop — 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.5fr 1fr; }
}
Breakpoint ruler — base · 640 · 768 · 1024 · 1280
base
1 col
640
1 col
768
2 col
1024
+ sidebar
1280
margined

How the grid collapses#

The grid is 12 columns inside the 1100px measure. It does not stay twelve columns all the way down — it resolves to four column counts:

FromColumnsGutterPage padding
base416px16px
md 768px816px24px
lg 1024px1224px32px
xl 1280px1224pxauto — the measure caps at 1100px

Below md, every multi-column region becomes one column. There is no two-column layout on a phone: two 160px columns are two unreadable columns. Regions stack in source order, so the DOM must already be in reading order — do not rely on order or grid-area to fix a sequence that is wrong in the markup, because that breaks the focus and reading order.

Spacing steps down one rung when the grid collapses: a --space-6 (32px) section gap on desktop becomes --space-5 (24px) below md. Radius, type scale, and border weights do not change — a card is a 9px card at every width.

The 1100px measure is a maximum, not a target

Wider viewports gain margin, not line length. At xl the content stays 1100px wide and centres; it does not grow to fill a 1920px display. This is the same rule the spacing page states, restated here because it is the most common thing a responsive rewrite breaks.

Touch targets#

44×44px minimum for anything tappable, at every viewport — the floor is a finger, not a breakpoint.

The default control heights are 32 / 40 / 48px, so the sm and md sizes are below the floor on their own. Two ways to resolve it, in order of preference:

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

css
/* Preferred: 40px control, 44px target. */
.pj-btn--md {
  min-height: 40px;
  position: relative;
}
.pj-btn--md::after {
  content: "";
  position: absolute;
  inset: -2px 0;   /* 40 + 2 + 2 = 44 */
}

Mobile navigation#

Use a bottom tab bar for application surfaces. Use a top drawer for documentation and marketing surfaces. The choice follows the shape of the navigation, not the shape of the device:

  • An application has a small, flat set of destinations — the dashboard 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
screen content
Overview Agents Alerts Account

Tables, code, and diagrams#

Three content types cannot reflow, and each has one answer:

  • Tables scroll horizontally inside their own container with the first column pinned — never the page. See wide tables.
  • Code blocks scroll horizontally. Do not soft-wrap code: a wrapped line changes what the code appears to say.
  • Diagrams get a scrollable container or a re-drawn narrow variant. Do not scale a diagram down until its labels are unreadable — an 8px label is not a responsive diagram, it is a broken one.

Worked example#

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.

Sizing#

SizeHeight
sm32px
md40px — the default
lg48px

All inputs are set in Source Sans 3 at 14px.

Sizes — sm 32 · md 40 · lg 48
Small
Medium
Large

States#

StateTreatment
Default1.5px --border on --surface
FocusAccent border + 3px --tint-accent-active halo
ErrorDanger border, danger tint, and matching danger foreground
Disabled--surface-2, muted text, semantic border, cursor: not-allowed
Every state
Default
Focus
ErrorDescribe the problem in words.
Disabled

Labels#

Labels use the overline style and sit above the field. Floating labels are not used anywhere in the system — they hide the label at exactly the moment the user is filling the field, and they break at long label lengths.

css
.field__label {
  font-size: 12px;
  font-weight: 600;
  line-height: 1.3;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-secondary);
}

Validation#

  • Error text sits below the field in 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.

Empty states#

Three kinds, and they are not interchangeable:

KindWhenThe action
First runThe feature works; nothing has been created yetThe accent button that creates the first one
Filtered to nothingThere is data; the current filter excludes all of itClear the filter — and show what the filter is
Nothing to reportEmpty is the good outcome — no open findingsNo action. Say so plainly

The first-run empty state is the only one that gets an accent button — it is the only one where creating something is what the user came to do. “Filtered to nothing” gets a ghost button, because the fix is to undo, not to create.

First run, filtered to nothing, nothing to report
No pipelines yet
A pipeline runs your policy checks before a deployment is promoted.
No pipelines match
48 pipelines exist. None are both strict and failing.
No open findings
The last audit completed 4 minutes ago and found nothing.
Healthy

An empty state is body text, not an illustration. The system’s answer to a blank screen is a diagram or nothing, never a mascot or a spot illustration of a person holding a magnifying glass.

Never show an empty state you are not sure about

“No results” while a request is still in flight is a lie the user acts on. A region is in the loading state until the response arrives, and only then resolves to empty, populated, or error. Empty is a result, not a default.

Loading states#

Two mechanisms, chosen by what you know about the shape of what is coming:

UseWhy
SkeletonYou know the layout — a table of rows, a card grid, a KPI rowThe layout does not jump when content arrives
SpinnerYou know nothing — an action in flight, an indeterminate waitThere is no shape to promise

Prefer the skeleton. A spinner in the middle of a region that is about to become a table tells the user nothing and then reflows the page underneath them.

A skeleton is midnight-2 blocks at the real dimensions of the content, with the real gaps and the real number of rows where that is known. It carries a 100ms shimmer at --duration-standard, and it does not shimmer at all under prefers-reduced-motion: reduce — it simply sits there as static blocks.

Skeleton — real dimensions, real row count
Spinner — indeterminate work, with its label
Applying changes…

Timing rules, so the loading state does not become its own flicker:

  • Under 200ms — show nothing. A skeleton that appears and vanishes is worse than a brief pause.
  • 200ms to 10s — skeleton or spinner.
  • Over 10s — a determinate pj-progress bar with a count (“3 of 12 checks”), or the state moves to a background job with its own row in the list.

Every loading region carries role="status" and an accessible label, or a screen-reader user gets silence where a sighted user gets motion. Never animate a skeleton in a way that hides that the operation has stalled — a shimmer that runs for two minutes says “working” when the truth is “stuck”.

Error pages#

Full-page errors — 404 and 500 — are the brand’s worst-case first impression, so they use the same page shell and the same voice as everything else. No apology paragraph, no cartoon, no error code as a headline.

404500
Headline“That page does not exist”“Something failed on our side”
BodyWhat might have happened, in one sentenceWhat we know, and whether it is being worked on
ActionBack to a real destination + searchRetry, and a route to a status page or support
BlameThe link, not the readerUs, explicitly

The code (404, 500) appears as an overline above the headline, in the muted foreground — findable when someone is reporting the problem, never the loudest thing on the page.

404 and 500 — same shell, different obligation
Error 404
That page does not exist
The link may be out of date, or the pipeline may have been deleted.
Error 500
Something failed on our side
The request did not complete. Nothing was deployed. Reference req_8f2c14.

A 500 says what did not happen. “Nothing was deployed” is the sentence the reader needs; “an unexpected error occurred” is the sentence they cannot act on. If the outcome is genuinely unknown, say that instead — it is a different fact and it changes what they do next.

Inline errors#

Not every failure takes the page. A failure scoped to one region stays in that region, so the rest of the screen remains usable:

  • A field fails with pj-input--error and a message below it — never a tooltip, never colour on the border alone.
  • A panel fails with a pj-alert--danger inside the panel and a retry control, while the surrounding page keeps working.
  • A background job fails into a row in the activity feed with a pj-status --err dot and the word “Failed”.
Panel-scoped failure — the page keeps working
Agent activity
Could not load activity
The activity service did not respond. Everything else on this page is current.
Do

Say what is true, why, and the one thing to do. Use a skeleton wherever the layout is known. Label every loading region for screen readers. Tell a reader what did not happen when something failed.

Don't

Show “no results” before the response arrives, put an accent button on a “filtered to nothing” state, headline an error with its status code, or let a region-scoped failure take the whole page.

Accessibility

Keyboard order, skip links, focus management across composed layouts, and the ARIA the component set requires.

Accessibility appears throughout this system as measurements — contrast ratios on Colour, the 44px touch floor on Responsive, the focus trap on modals. This page covers what those cannot: order, announcement, and focus — the properties that only exist once components are composed into a screen.

The target is WCAG 2.2 Level AA.

Opt-in accessibility settings#

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.

AttributeValueEffect
data-a11yautoFollows supported reduced-motion, reduced-transparency, and contrast operating-system preferences
data-font-sizelg, xl, xxl, xxxl112.5%, 125%, 150%, or 200% type through one shared scale
data-contrasthighStronger text and border roles in every appearance
data-focusstrongConforming 2px midnight ring with 2px offset
data-link-underlineonUnderlines links in running text; .link-plain opts out
data-motionreducedNear-zero transitions and animation
data-transparencyreducedRemoves backdrop blur and solidifies every scrim
data-text-spacinglooseWCAG 1.4.12 diagnostic spacing, not the house style
data-themelight, darkPins 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.

Reading and focus order#

There is one order, and the DOM defines it. Keyboard order, screen-reader order, and visual order must agree.

That makes source order a layout constraint, not a detail: write regions in the order they should be read, and let CSS place them. The page shell is therefore authored as skip link → header → sidebar → main → complementary, and Grid puts the sidebar on the left.

Never use order, row-reverse, or grid-area to change the sequence a reader encounters. It moves the pixels and leaves the keyboard behind, which is exactly the bug that a stacked mobile layout exposes: if the DOM is already in reading order, the collapse to one column is free.

Never set a positive tabindex. tabindex="0" puts an element in the natural order and tabindex="-1" makes it programmatically focusable; anything above zero creates a second, competing order.

Skip links#

Every page begins with a skip link — the first focusable element in the DOM, visually hidden until focused, then rendered as a normal control against the midnight header.

One skip link per major landmark the keyboard would otherwise have to traverse. For the page shell that is two: Skip to content and Skip to navigation. A sidebar of thirty items in front of the content is thirty tab stops on every single page.

html
<a class="pj-skip" href="#main">Skip to content</a>
<a class="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: top var(--duration-standard) var(--ease-out);
}
.pj-skip:focus { top: var(--space-4); }

The target must be able to receive focus: <main id="main" tabindex="-1">. Without it, some browsers move the viewport but leave focus at the top of the document, and the next Tab lands back in the navigation.

Landmarks#

The page shell maps onto landmarks one-to-one. Use the elements, not role attributes on divs:

RegionElementNotes
Sidebar<nav aria-label="Sections">The label distinguishes it from any other nav
Header<header>The page title inside it is the <h1>
Content<main id="main" tabindex="-1">Exactly one per page
Secondary panel<aside aria-labelledby="…">Points at the panel’s own heading
Footer<footer>

Two nav elements on a page need two different aria-labels. An unlabelled pair is announced as “navigation, navigation”.

Headings are a real hierarchy, not a type ramp: one <h1> per page, no skipped levels. If a heading is the wrong size, restyle it — do not renumber it.

Focus visibility#

Focus is always visible. The system never sets outline: none without replacing it with something at least as loud.

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

Strong focus ring — 2px midnight, 2px offset
Tab to see the real thing
css
:focus-visible {
  outline: 2px solid var(--midnight-9);
  outline-offset: 2px;
}

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.

Moving focus#

Focus moves only in response to something the user did, and it always ends somewhere they can see.

EventFocus goes to
Modal opensThe modal’s first focusable element, or its heading
Modal closesThe control that opened it
Drawer or menu opensIts first item
A route changesThe new page’s <h1>, or <main>
A validation error appears on submitThe first invalid field
A row is deletedThe next row, or the region’s heading if it was the last

Modals and drawers trap focus while open and close on Esc. Everything else must not trap: a Tab inside a table, a card, or a tab strip leaves it again.

Deleting a row without moving focus leaves it on a detached node, which drops the keyboard user back at the top of the document — a silent, common, and entirely avoidable regression.

ARIA for the composed patterns#

The components page shows tabs, navigation, status, and overlays as markup. This is what each owes assistive technology once it is real.

Tabs#

pj-tabs is a tab strip, so it needs the full pattern: role="tablist" on the container, role="tab" with aria-selected on each tab, role="tabpanel" with aria-labelledby on each panel. Arrow keys move between tabs; Tab leaves the strip entirely — only the selected tab is in the tab order.

If arrow-key navigation is not implemented, do not use the tab roles. A tablist that does not respond to arrow keys is worse than a list of links, because it has promised behaviour it does not have.

Navigation#

The current page in a pj-sidebar or pj-navbar is marked aria-current="page". The 600-weight and the accent underline are the visual half of that statement; aria-current is the other half, and neither substitutes for the other.

Status and semantic colour#

Every pj-status dot is paired with its text label. That rule appears on the components page as a visual-design rule; it is also the accessibility rule, and it is why the system has no bare-dot variant to reach for.

Live regions announce changes that happen without a user action:

RegionAttribute
Loading regionrole="status" + aria-label
Toast, non-urgent updatearia-live="polite"
Validation summary, failurearia-live="assertive"
Progress barrole="progressbar" with aria-valuenow / min / max

Reserve assertive for things that interrupt correctly. An assertive region that fires on every keystroke makes the page unusable with a screen reader.

Icons#

An icon that repeats its label is aria-hidden="true". An icon that is the control carries the accessible name: <button aria-label="Security settings">. See Icons.

Tables#

A pj-table uses <th scope="col">, and scope="row" on the identifying column. Sortable headers carry aria-sort="ascending" | "descending" | "none", updated when the sort changes — the arrow glyph alone is not announced. Multi-level headers need scope="colgroup" on the group row.

Motion and preference#

The motion rules are an accessibility requirement, not a stylistic one. Everything animated is wrapped in prefers-reduced-motion: no-preference, or neutralised under reduce.

Skeletons stop shimmering under reduce but stay visible — the loading state is information, and removing it would remove the information along with the motion.

What to check before shipping a screen#

  1. Tab from the address bar to the end. Focus is visible at every stop and the order matches the visual order.
  2. The skip link is the first stop, and using it lands focus in <main>.
  3. Nothing is reachable only by mouse; nothing is reachable only by keyboard.
  4. Every image, icon button, and form control has an accessible name.
  5. Zoom to 200% and to 400%. Nothing is clipped; nothing scrolls in two directions at once.
  6. Turn colour off — greyscale the screen. Every state is still distinguishable.
  7. 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.

Source and generated files#

src/data/brand.yaml is the structured source used by this documentation. The generator produces three public formats from it:

  • CSS custom properties
  • Design-token JSON
  • Tailwind configuration

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

Contract coverage#

GroupExported contract
IdentityMidnight, orange, and slate shortcuts; accent-solid for white-labelled controls
ScalesThree twelve-step ramps, pinned separately for light, navy, and deep
SemanticsSuccess, warning, danger, and info as solid, tint, and on-tint foreground
SurfacesPage, raised surface, subtle surface, border, and three text levels per appearance
TypeThree font roles, eleven scalable type roles, weights, line heights, and tracking
LayoutNine spacing steps, four breakpoints, 1100px container, 65ch measure, and 44px touch floor
Shape and depthFive radii, four shadows, dark-surface elevation steps, and rims
MotionFour durations and two easing curves
SyntaxTen dark-panel roles plus ten separately measured light-panel roles
TerminalDark-default and optional-light sixteen-slot ANSI palettes plus chrome
AccessibilityFont scale, strong focus, contrast, link, motion, transparency, and spacing hooks

Select an appearance#

Light is the root contract. With no explicit choice, the operating-system dark preference resolves to navy. An explicit selector always wins.

html
<html data-theme="light">
<html data-theme="dark">
<html data-theme="dark" data-surface="deep">
SelectorResult
[data-theme="light"]Light canvas (#f8f9fb) and white raised surfaces
[data-theme="dark"]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.

Use semantic roles#

css
.card {
  background: var(--color-surface);
  color: var(--color-text-primary);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-4) var(--space-5);
}

.button--accent {
  background: var(--color-accent-solid);
  color: var(--fixed-control-text, #fff);
}

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.

Light code and terminal companions#

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
.code--light {
  background: var(--syntax-light-surface);
  color: var(--syntax-plain-light);
}

.terminal--light {
  background: var(--terminal-light-surface);
  color: var(--terminal-light-foreground);
}

These values never activate automatically. A component opts in to the entire companion palette; mixing light and dark syntax roles is not supported.

Typography scales as one system#

Every exported type size is a calculation over --font-scale. This makes the 200% accessibility setting apply without rewriting component selectors.

css
html[data-font-size="xxxl"] { --font-scale: 2; }

h1 {
  font-size: var(--type-h1-size);
  font-weight: var(--type-h1-weight);
  line-height: var(--type-h1-lh);
}

Framework notes#

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.

Use-case map#

NeedNormative sourceMachine-readable material
Identity, voice, and statusDocumentationllms.txt
Colours, type, spacing, shape, and motionFoundationsJSON tokens
CSS integrationTokensCSS variables
Logo selection and placementLogoManifest
Components and product surfacesInterfaceClean Markdown
Templates and collateralCollateralManifest
Licence and trademark limitsGovernancellms.txt
ProvenanceAsset provenanceManifest

Brand MCP server#

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.

Client configuration#

Point an MCP client at the script with an absolute repository path. A typical configuration is:

json
{
  "mcpServers": {
    "projectious-work-brand": {
      "command": "uv",
      "args": [
        "run",
        "--script",
        "/absolute/path/to/brand/mcp/brand_server.py"
      ]
    }
  }
}

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.

Resources#

Resources use this template:

text
brand://VERSION/RESOURCE_ID

For the current checkout, examples include:

URIContent class
brand://v3.0.2/identityNormative identity and voice guidance
brand://v3.0.2/foundationsNormative colour, type, spacing, shape, and motion index
brand://v3.0.2/tokens-guideNormative token usage guidance
brand://v3.0.2/tokens-jsonGenerated JSON token download
brand://v3.0.2/tokens-cssGenerated CSS custom properties
brand://v3.0.2/logoNormative logo guidance
brand://v3.0.2/componentsNormative component guidance
brand://v3.0.2/rendered-specimensRendered code specimens used as examples
brand://v3.0.2/licensingLicence contract
brand://v3.0.2/trademarkTrademark limits
brand://v3.0.2/provenanceThird-party asset provenance
brand://v3.0.2/changelogRelease history and migrations

Use MCP resource discovery rather than assuming this list is exhaustive. The public brand manifest is the allowlist behind discovery and reads.

Tools#

ToolPurpose
lookup_token(name, version?)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.

Version behavior#

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.

Security boundary#

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.

Compatibility and versions#

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.

Retrieval boundaries#

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.

Build from roles, not screenshots#

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.

Select the appearance explicitly#

html
<html data-theme="light" data-a11y="auto" data-focus="strong">

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.

Use the three type roles#

css
.heading { font-family: var(--font-heading); }
.copy { font-family: var(--font-body); }
.system-output { font-family: var(--font-code); }

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.

Compose interfaces#

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 and terminal surfaces#

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.

Verify before release#

sh
./scripts/verify.sh

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.

Sources and ownership#

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.

Routine maintenance#

  1. Review upstream design-system inputs and their sync notes.
  2. Diff named tokens, appearance overrides, assets, and normative prose.
  3. Update structured sources before generated CSS or JSON.
  4. Rebuild every download and machine-readable manifest.
  5. Exercise light, navy dark, deep dark, and accessibility settings.
  6. Run the full repository verification and inspect the browser output.
  7. Record breaking changes and migration steps in the changelog.

Audit invariants#

  • Only steps 11 and 12 are used for text.
  • Step 9 is never body text.
  • Every semantic tint carries its matching foreground.
  • Every status has a non-colour channel.
  • Component CSS contains no undocumented colour or scrim literals.
  • Code and terminal surfaces remain distinct.
  • Print uses fixed light-context tokens rather than the active screen theme.
  • Preview cards resolve from semantic tokens in all three appearances.

Version and release chain#

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.

Recovery#

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.

Direct dependency inventory#

DependencyPurposeLicenceDelivery
brand-theme-hugo-vanilla v0.3.1Documentation rendererMITPinned Hugo module
Hugo extendedStatic-site buildApache-2.0Build-time tool
Plus Jakarta SansDisplay and headingsOFL-1.1Self-hosted by the theme
Source Sans 3Body and UI copyOFL-1.1Self-hosted by the theme
IBM Plex MonoCode, data, and terminalOFL-1.1Self-hosted by the theme
Tabler IconsInterface iconographyMITVersioned theme subset
FlexSearchClient-side searchApache-2.0Vendored theme runtime
PyYAMLToken and manifest generationMITEphemeral uv dependency
PlaywrightBrowser verificationApache-2.0Ephemeral uv dependency

Scope#

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.

Maintenance rules#

  • Pin released modules and vendored assets to a reviewable version.
  • Ship licence text beside self-hosted fonts and icon subsets.
  • Do not mix a second icon library into the Tabler set.
  • Record substitutions explicitly, including why the canonical delivery path was unsuitable.
  • Rebuild and review the SBOM whenever the module graph, fonts, icons, search runtime, or generation tools change.

The per-asset evidence remains in Provenance.