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

Return to the regular view of this page.

Documentation

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

This documentation is rendered by the design system it describes. Every colour chip, type row, and code block on this site is produced from the same tokens the pages document.

Where to start

If you want to…Read
Use the palette correctly, including dark modeColour
Set text in the brand’s voiceTypography
Place the logo without breaking itLogo usage
Build an interfaceInterface
Produce a deck, signature, or social cardCollateral
Know what you are allowed to reuseGovernance
Pull the raw values into a projectTokens

How the system is organised

Foundations are the primitives — colour, type, spacing, shape, motion. Everything else composes from them.

Logo covers the mark, the four lockups, and the rules that keep them legible.

Interface applies the foundations to screens: components, code surfaces, dark mode, icons, and forms.

Collateral covers the artefacts that leave the product — decks, cards, signatures, social imagery.

Governance covers licensing, trademark, the pre-launch legal assessment, and asset provenance.

Source of truth

The canonical design documents live in brand/ in the repository — the HTML design-system documents, logo files, document templates, and the token exports. This site is the readable presentation of those sources, not a replacement for them.

1 - Foundations

The primitives — colour, typography, spacing, shape, and motion. Everything else composes from these.

Foundations are the values the rest of the system is built from. They are deliberately small in number and fixed: a component may choose which token to use, but it does not invent new ones.

1.1 - Colour

Three 12-step scales in two modes, their step roles, and the contrast rules that govern them.

The palette is three scales — midnight, orange, and slate — each expressed as a 12-step ramp in a light and a dark variant. The step numbering follows the Radix convention, which assigns every step a role. Using a step outside its role is the single most common way to break the system.

Core colours

These are the named aliases most projects reach for first. They are shortcuts into the scales, not a separate palette.

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

Step roles

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

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

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 both modes.

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

Do

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

Don't

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

Slate

Slate light · step 9 #546a82
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 three scales cover every surface that has a light mode. A terminal does not: it has one surface, it is dark, and every colour in it is measured against that one background. It also needs six hues where the interface needs three, because programs have been writing to sixteen ANSI slots since long before this system existed.

So the terminal palette is a fourth member of the system rather than a fifth scale — a fixed sixteen-slot palette plus its chrome, derived from the ramps and measured against midnight-dark-1.

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

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, iTerm2, and Zellij is on the Terminal theming page.

Contrast rules

  • Never use pure #000 or #fff as text. Use step 12 of the relevant scale: #142438 on light, #c5daf0 on dark.
  • Step 9 is constant across modes. The solid accent does not shift when the theme changes.
  • Body text targets 4.5:1, large text (≥24px, or ≥18.66px bold) targets 3:1.
  • Verify against the actual surface. A step that passes on the app background may fail on an elevated panel.

Where an identity colour cannot carry text

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

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

RoleLightDarkOn #0e1720
Success#2f7d65#6cc0903.65:1 → 8.24:1
Warning#8b6508#e0a92a3.41:1 → 8.50:1
Danger#a8261c#f08b802.55:1 → 7.49:1
Info#3a5a82#8aacc82.55:1 → 7.59:1

Dark mode

Both modes are equally supported. See Dark mode for the implementation rules — theme switching, persistence, image treatment, and the always-dark code surface.

1.2 - Typography

Three typefaces, three jobs, and a fixed type ramp.

The system uses three typefaces, each with one job. All three are licensed under the SIL Open Font License 1.1 and served from Google Fonts, so there are no bundled font binaries to license or redistribute.

The three families

FamilyWeightsRoleLicence
Plus Jakarta Sans400 · 500 · 600 · 700 · 800Headings, display, buttons, navigation, wordmarkSIL OFL 1.1
Source Sans 3400 · 500 · 600Body copy, UI labels, captionsSIL OFL 1.1
IBM Plex Mono400 · 500Code, terminal output, dataSIL OFL 1.1

Loading them:

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Source+Sans+3:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap">

The ramp

Every row below is set in the family, size, weight, and line-height it documents — this is the live specimen, not a picture of one.

Display · 48/800/1.1
Redesigning work
H1 · 36/700/1.15
Agent-first consulting
H2 · 28/700/1.2
Composable infrastructure
H3 · 22/600/1.25
How the system fits together
H4 · 18/600/1.3
Working in the open
H5 · 15/600/1.35
Supporting detail
Body L · 16/400/1.65
Augmenting people's strengths through composable Cloud, Agile, and Agentic AI infrastructure.
Caption · 12/400/1.5
Figure 1 — pipeline stages and their policy gates.
Overline · 11/600/1.3
SECTION LABEL
Code · 13/400/1.6
createPipeline({ policy: "strict" })

Rules

  • Letter-spacing tightens to −0.3px above 20px. Large type set at default tracking reads loose and unresolved.
  • Overline is a label, not a heading. 11px, 600, uppercase, 0.08em tracking. Use it above a heading, never as one.
  • Body copy is 16px / 1.65. This is the reading size for documentation and long-form prose. Do not set body text below 14px.
  • Never set body copy in Plus Jakarta Sans, and never set headings in Source Sans 3. The split is what makes the voice recognisable.
Do

Pair an overline with an H2 to label a section. Keep the ramp intact — skip sizes rather than inventing intermediate ones.

Don't

Introduce a fourth typeface, use a weight outside the listed set, or fake a weight with font-synthesis.

1.3 - Space, shape, and motion

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

Spacing

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

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 at rest
--shadow-20 4px 12px rgba(0,0,0,0.08)Hover, dropdowns
--shadow-30 8px 24px rgba(0,0,0,0.12)Modals, popovers

In dark mode, elevation is expressed by lightening the surface rather than deepening the shadow — a shadow on a near-black background is invisible.

Motion

Things slide into place. They do not bounce.

Durations

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
shadow-0
shadow-1
shadow-2
shadow-3
Motion — slides into place, never bounces
200ms · ease-out — entering
100ms · colour and opacity

Motion rules

  • All animation wraps in @media (prefers-reduced-motion: no-preference), or is disabled under prefers-reduced-motion: reduce.
  • CSS-only for HTML. For React, use Framer Motion with these same timing values.
  • Never animate text character-by-character.
  • Page transitions cap at 400ms.
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}
Do

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

Don't

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

2 - Logo

The mark, the four lockups, and the rules that keep them legible.

The mark is a stencil peony bud — concentric petal cuts resolving to an accent centre. It reads as a single form at 16px and holds its internal structure at poster scale.

2.1 - Lockups

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

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

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

The four lockups

Two-line (default) and one-line
projectious · work Two-line · headers, slides, cards · min 120px
projectious · work One-line · navbars, footers, signatures · min 140px
Dot-replace and stacked
projectious work Dot-replace · compact horizontal · min 160px
projectious · work Stacked · app icons, avatars, slide corners · min 60px
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 24px. The 16px rendering exists for favicons only, where the browser supplies the name.

Product line

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

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

Wordmark construction

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

Do

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

Don't

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

2.2 - Usage

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

Clear space

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

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

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

Minimum sizes

Minimum sizes — 48 / 32 / 24 / 16px
48px
32px
24px — minimum
16px — favicon only
VariantMinimum
Icon (standalone)24px — 16px for favicons only
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.

2.3 - Files

Delivered formats, when to use each, and where to download them.

Formats

FormatContentsUse for
SVGVector sourcePrimary for web and print
PNG @1x16 / 32 / 48 / 64 / 128 / 256 / 512pxNative-resolution raster
PNG @2x2× pixel dimensionsRetina — required for web
PNG @3x3× pixel dimensionsSuper-retina — mobile
ICOMulti-resolution (16 + 32 + 48)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

Each raster size ships in five variants:

  • 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

Downloads

The logo files are served from this site and are also in the repository under brand/logo/.

AssetLink
SVG — lighticon-light.svg
SVG — darkicon-dark.svg
SVG — mono blackicon-mono-black.svg
SVG — mono whiteicon-mono-white.svg
SVG — mono greyicon-mono-gray.svg

3 - Interface

The foundations applied to screens — components, code surfaces, dark mode, icons, and forms.

Interface guidance applies the foundations to working software. Where a component appears here, its measurements are normative — a 40px input is 40px, not “about 40px”.

3.1 - Components

The component set as live specimens — buttons, inputs, cards, tables, navigation, feedback, overlays, and data display.

Every specimen on this page is live markup styled by the brand tokens, ported from brand/html/projectious-kitchen-sink.html. They follow your colour mode — switch the theme and they change with the documentation.

Measurements listed here are normative: a 40px input is 40px.

Buttons

Five variants, three sizes. Plus Jakarta Sans 600, 6px radius, 200ms transitions.

Variants — medium (40px)
Sizes — sm 32px · md 40px · lg 48px
On a midnight surface
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-9Tertiary, toolbars
Danger#a8261cnonewhite7.10:1Destructive action
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 13px, slate-7 border, and a 2px focus ring at midnight 15% alpha.

Text, textarea, select
States — default, focus, error, disabled
Default slate-7 border
Focus midnight-9 + 2px ring
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 24px, 1px slate-4 border, shadow-1 at rest. Nested controls step down one radius.

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

Tables

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

Data table with sort indicator
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

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.

Navbar — midnight in both modes, 3px accent rule
projectious.work Documentation Pipelines Agents
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

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 always dark — see Code.

Terminal
$ tofu apply -auto-approve
Plan: 3 to add, 0 to change, 0 to destroy.
Policy check passed
Deploying to staging…

3.2 - Dark mode

Both modes are equally supported. The implementation rules that keep them in step.

Dark mode is not a variant of the brand — it is half of it. Both modes are designed, tested, and shipped together.

Implementation rules

  • Default follows prefers-color-scheme, with a manual override toggle persisted to localStorage.
  • CSS custom properties swap per theme. Components read tokens; they do not branch on the mode themselves.
  • Step 9 (solid accent) stays constant across modes. #E05232 is #E05232 everywhere.
  • No pure #000 or #fff as text. Use step 12 — #c5daf0 on dark, #142438 on light.
  • Images get a subtle overlay (black at low alpha) in dark mode, so a bright photograph does not punch a hole in a dark page.
  • Code blocks are always dark, regardless of mode. See Code.
The same card in both modes
Light mode
Body text is midnight-12; supporting copy is slate-11.
Deploy
Dark mode
Never pure white — midnight-12 from the dark scale.
Deploy

Surfaces

Elevation in dark mode is expressed by lightening the surface, not by deepening the shadow — a shadow against a near-black background is invisible.

LayerLightDark
App background#ffffffmidnight-dark-1 #0e1720
Subtle surfacemidnight-1midnight-dark-2 #131e2b
Raised surfacemidnight-2midnight-dark-3 #1a2b3e

Restating text on dark surfaces

The default heading colour is midnight-9, chosen so headings read correctly on light pages. That same value on a midnight surface is invisible — #1d3352 on #1d3352 is a contrast ratio of 1.00:1.

Any component that paints its own dark background must therefore restate its text colours. This applies to covers, dark boxes, footers, and overlays:

.surface--dark {
  --bs-body-color: #c5daf0;    // midnight-dark-12
  --bs-heading-color: #c5daf0;
  --bs-secondary-color: #97a8b8; // slate-dark-11, still AA
}
Do

Test every surface in both modes. Restate text colours on any component that sets its own dark background.

Don't

Ship a component that only works in one mode, or use pure black or white as a text colour in either.

3.3 - Code

The always-dark code surface and the syntax theme, with measured contrast for every token.

Code blocks are always dark

Code blocks stay dark regardless of colour mode. A code surface that flips with the theme forces the syntax palette to be designed twice and makes screenshots inconsistent between users. The surface is midnight-2 from the dark scale (#131e2b) in both light and dark mode.

The block you are reading is rendered by that rule:

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

Syntax theme

Because the surface is always dark, every syntax value is read from the dark scale. Contrast is measured against #131e2b.

Ten roles, not twenty-two tokens

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

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

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

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
#8aacc8 midnight-dark-11
Also structural keys — a YAML key is the keyword of its line.
7.06:1keyword · modifierkeyword.control · storage.modifier · storage.type
.k .kc .kd .kn .kp .kr .nt .na
Types and classes
#74c0c9 terminal cyan (bright)
The brand has no cyan; the terminal palette does, and this is the second place the system needed one.
8.11:1type · class · struct · interface · enum · typeParameter · namespaceentity.name.type · entity.name.class · support.class
.kt .nc .nn .ne .bp
Functions and methods
#e0a92a terminal yellow (bright)
Callables read as callable at a glance — the single most useful distinction beyond the original six.
7.91:1function · methodentity.name.function · support.function
.nf .fm
Decorators and macros
#d491b4 terminal magenta (bright)
Code that runs at a different time from the code around it.
6.82:1macro · decorator · evententity.name.tag · meta.decorator · support.macro
.nd .ni .nl
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 .sd .se .sh .si .sr .ss .sx .dl
Numbers and constants
#f09878 orange-dark-11
Literal values, including true/false/nil.
7.58: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
#72889d code-comment
Italic. The only role with no scale step of its own.
4.59:1commentcomment.line · comment.block · comment.block.documentation
.c .ch .cm .cp .c1 .cs
Invalid and deprecated
#f08b80 danger-dark
Deprecated is struck through as well as coloured — the state does not depend on hue.
6.98:1(modifier) deprecatedinvalid.illegal · invalid.deprecated
.err

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.

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 (#72889d, 4.59:1) exists as a dedicated syntax token — the dimmest value that clears AA while staying visibly below operators. It is not a scale step and should not be treated as one.

Do

Group scopes into roles, and let a language’s grammar map onto them. Keep the role count under ten, and check every value against the code surface.

Don't

Give each LSP token type its own hue, or use the accent as a syntax colour — it marks the primary action, and a code block is not one.

Inline code

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

Terminal output

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

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

3.4 - Icons

Lucide as the icon library, and how icons are sized and coloured.

Library

The system uses Lucide — an actively maintained fork of Feather with over 1,400 icons.

PropertyValue
LicenceISC (functionally equivalent to MIT)
Commercial usePermitted
AttributionNot required in product UI; retain the notice in source
ModificationPermitted

ISC is one of the most permissive open-source licences available, which is why it was chosen over icon sets with attribution or share-alike terms. See the legal assessment for the clearance detail.

Sizes on the 4px grid — 16 / 20 / 24 / 32px
16
20 — default
24
32
accent — primary action
Icon with a label, and an icon button
Policy enforced

Usage

  • Stroke width 2, matching Lucide’s default. Do not thin or thicken strokes to “match” a layout.
  • Size on the 4px grid: 16, 20, 24, 32px. 20px is the default for inline UI.
  • Colour follows text. An icon beside a label takes the label’s colour. Standalone icons carrying meaning take --color-secondary; an icon marking the primary action takes the accent.
  • Icons are not decoration. Every icon must carry meaning the label does not already carry, or be marked aria-hidden="true".
Do

Pair icons with text labels wherever the meaning is not universally understood. Give standalone icon buttons an accessible name.

Don't

Mix icon sets, recolour icons to arbitrary hues, or use an icon as the only indicator of state.

3.5 - 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 13px.

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

States

StateTreatment
Defaultslate-7 border
Focusmidnight-9 border + 2px focus ring, midnight at 15% alpha, 2px offset
Errororange-9 border + orange-1 background
Disabledslate-4 background, slate-8 text, 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.

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

Validation

  • Error text sits below the field, in orange-11 on light surfaces.
  • Never rely on border colour alone to signal an error — pair it with text.
  • Validate on blur, not on every keystroke; re-validate on submit.
Do

Keep labels visible at all times. Give every field an associated <label> and describe errors in words.

Don't

Use placeholder text as a label, signal errors with colour alone, or disable the submit button without explaining what is missing.

4 - Themes

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

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

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

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

4.1 - Hugo

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

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

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

Implementation Contract

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

Inputs and ownership

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

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

File boundary

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

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

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

Non-negotiable visual rules

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

The shared semantic layer

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

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

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

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

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

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

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

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

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

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

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

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

Required page states

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

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

Release evidence

Attach or record the following for a theme change:

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

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

Docsy

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

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

What conformant looks like

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

Heading hierarchy example

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

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

Install and configure the theme

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

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

theme: [docsy]
enableGitInfo: true

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

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

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

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

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

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

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

Map the brand into Bootstrap and Docsy

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

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

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

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

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

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

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

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

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

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

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

How Docsy binds to the shared layer

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

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

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

Complete Docsy color layer

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

// assets/scss/_styles_project.scss

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Brand every visible theme element

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

Page-part example

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

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

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


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


## Validate the configuration

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

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

Review specimen

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

Platform operations

Ship a reliable integration

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

Review release check

What the reader needs first

AreaStateOwner
ConfigurationValidatedPlatform team
DocumentationReviewedTechnical writer
ReleaseWaiting for approvalRelease manager

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

A card-led decision surface

Validate

Check tokens, configuration, and internal links.

Read validation notes

Review

Confirm accessibility in light and dark modes.

Read review criteria

Promote

Create a release only from verified content.

Read release check

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

Code remains a dark working surface

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

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

Status language is semantic

Release check

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

Accessibility and release gate

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

Sources and upgrade boundary

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

Hextra

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

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

What conformant looks like

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

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

Install and configure the theme

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

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

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

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

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

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

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

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

Load the brand through custom CSS

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

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

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

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

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

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

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

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

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

How Hextra binds to the shared layer

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

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

Complete Hextra color layer

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Brand every visible theme element

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

Page-part and review fixture

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

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

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

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

## What the reader needs first

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

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

## Code remains a dark working surface

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

## Release check {#release-check}

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

Intended shell anatomy

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

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

Hextra acceptance cues

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

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

Accessibility and release gate

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

Sources and upgrade boundary

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

4.2 - Terminal

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

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

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

Implementation contract

What owns what

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

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

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

Inputs and ownership

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

Non-negotiable rules

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

The palette

Sixteen colours

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

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

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

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

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

Chrome

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

tmux

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

Required capability

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

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

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

Theme

# ~/.tmux.conf — projectious.work

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

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

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

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

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

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

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

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

Status-bar frameworks

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

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

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

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

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

WezTerm

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

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

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

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

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

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

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

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

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

Kitty

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

font_family      IBM Plex Mono
font_size        13.0
disable_ligatures always

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

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

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

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

# Splits
active_border_color    #e05232
inactive_border_color  #7b8da3
window_padding_width   6

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

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

iTerm2

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

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

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

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

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

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

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

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

Zellij

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

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

theme "projectious"

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

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

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

Verify

Palette

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

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

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

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

States to review

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

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

Release evidence

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

Sources and upgrade boundary

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

5 - Portfolio presentation

Truthful status, social-preview, diagram, and demo patterns.

Portfolio presentation must make maturity and limitations easier to see, not make prototypes appear more complete than they are.

These patterns are Projectious organization assets. Bernhard Gerlach’s personal LinkedIn profile is a separate identity: use his name, portrait, and first-person voice only with his approval. Do not turn a personal profile into an organization-brand channel or imply personal endorsement.

5.1 - Status treatments

Text-first maturity labels for truthful portfolio presentation.

Required pattern

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

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.

5.2 - Social previews

Repository artwork that states category and maturity plainly.

Content model

Every 1280×640 preview contains only:

  1. Projectious organization 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.

The editable source is brand/portfolio/social-preview-template.svg. Replace bracketed text, keep the status label intact, and save a project-specific SVG beside the template. Examples for aibox, processkit, ai-market-research, and KubeClaw demonstrate the content structure; they are not evidence of current maturity.

Export and validation

Run:

npm run portfolio:validate
npm run portfolio:export

The export script uses resvg when available and otherwise leaves the SVG source as the canonical reusable asset. Commit generated PNGs only when a repository needs them.

Source and generated exports use this repository’s split license. The Projectious name and marks remain subject to TRADEMARK.md. Record any third-party typeface, icon, logo, or screenshot in brand/PROVENANCE.md before use. The supplied templates contain no third-party logos or screenshots.

5.3 - Diagrams and demos

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

Architecture and flow diagrams

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

Suggested caption:

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

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.

6 - Media

Motion, audio, video, photography, and presentation guidance.

Media guidance covers everything with a time dimension or a photographic surface — the places where the system meets material it does not fully control.

6.1 - Audio and video

Sonic identity and video production rules.

Audio identity

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

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.
  • Subtitles are always available.
  • Export at 1080p minimum, 4K when possible.
Do

Show the real thing at real speed. Caption everything.

Don't

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

6.2 - Photography and illustration

Sourcing rules, treatment, and the abstract illustration style.

Sources

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

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

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

Treatment

  • Photographs in dark mode receive a subtle black overlay so they do not punch a hole in the page.
  • The logo never sits directly on an unmodified photograph — see Logo usage.
  • Never place text over a busy region. Add a scrim or crop to a quiet area.

Illustration

The illustration style is abstract system diagrams — nodes, flows, and pipelines drawn in the brand palette.

  • Midnight for structure, slate for supporting lines, accent for the single element that matters.
  • Dashed strokes indicate inferred or optional relationships.
  • Labels use Source Sans 3; never letter-space diagram labels.
  • Diagrams are line-based, not filled — the system reads as engineering drawing, not infographic.
Do

Draw the actual architecture. Use accent once per diagram, on the element the reader should look at first.

Don't

Use isometric 3D illustration, gradient-filled shapes, or generic “tech” clip art.

6.3 - Presentations

Slide templates, animation, and the core deck structure.

Core slides

Six slide types cover most decks:

SlidePurpose
TitleDeck opening — display type on midnight
Section statementA single sentence marking a new section
Three-upThree parallel points
QuoteA single quotation, attributed
Code showcaseCode on the always-dark surface
CTA closerThe one thing you want the audience to do
Core slide types — 16:10
Redesigning work
Cloud · Agile · Agentic AI
projectious.work
Bad software is a decision, not a constraint.
The agent-first approach
Define Build Deploy
"We run what we recommend."
// pipeline
createPipeline({
  policy: "strict"
});
Let's talk
projectious.work

The full deck

Twelve slide types cover a complete talk. Each is 16:10 and uses the same tokens as the product surfaces — no separate “presentation theme”.

Opening — title, agenda, section statement
Redesigning work
Cloud · Agile · Agentic AI
Agenda
01 Why agent-first
02 Architecture
03 Operating model
04 What it costs
Bad software is a decision, not a constraint.
Content — three-up, split, comparison
The agent-first approach
Define Build Deploy
Before
Manual gates, tribal knowledge, slow feedback.
After
Policy as code, repeatable runs, fast feedback.
Where the time goes
Review42%
Build31%
Deploy27%
Evidence — metric, quote, code, diagram
1,284
Runs this week
"We run what we recommend."
// pipeline
createPipeline({
  policy: "strict"
});
Architecture
DefineBuildDeploy
Closing — CTA and contact
Let's talk
projectious.work
Thank you
info@projectious.work
SlidePurpose
TitleDeck opening — display type on midnight
AgendaNumbered outline; current item in midnight-11
Section statementA single sentence marking a new section
Three-upThree parallel points
SplitBefore/after or contrast, one half on midnight
ComparisonA small table where numbers are the point
MetricOne number, large; the label in overline style
QuoteA single quotation with the accent rule, attributed
Code showcaseCode on the always-dark surface
DiagramLine-based system diagram, accent on the focal node
CTA closerThe one thing you want the audience to do
ContactThank-you and a single contact route

Rules

  • 16:10 aspect ratio.
  • Title slides and section statements use midnight surfaces; content slides use white or midnight-1.
  • projectious.work is visible on the title and closing slides.
  • One idea per slide. If a slide needs a paragraph, it needs to be two slides.

Animation

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

Templates

Document templates for LaTeX and Typst are in brand/templates/. Both embed brand identity and fall under the brand-asset licence terms — see Licensing.

7 - Collateral

Artefacts that leave the product — cards, signatures, and social imagery.

Collateral is where the brand travels without its context. These artefacts are often seen once, at small size, next to other companies’ material — so they lean harder on the mark and the wordmark than in-product surfaces do.

7.1 - Business card

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

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

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

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.

If a printed card is required:

  • 85×55mm, matte stock.
  • Single-colour midnight on uncoated white, or reversed white on midnight.
  • Use the monochrome mark — no accent on press unless a spot colour is budgeted.
  • Respect the 1× clear-space rule; do not bleed the mark to the trim edge.

7.2 - Email signature

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

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

Two forms are supplied:

FormSourceUse for
Fullbrand/email/signature.htmlFirst contact, and any message leaving the organisation — it carries the legally required details
Shortbrand/email/signature-short.htmlReplies 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
Musterstraße 1, 10115 Berlin, Germany
Projectious GmbH · Registered seat Berlin
Amtsgericht Charlottenburg, HRB 000000 · VAT ID DE000000000
Managing director: Jane Doe
This message is intended only for the addressee and may contain confidential information. If you received it in error, please tell the sender and delete it.
Short form — replies and internal mail
Jane Doe
Principal Consultant · Projectious GmbH

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

One rule, and it is vertical

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

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

Do

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

Don't

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

Fields

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

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

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.

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

8 - Governance

Licensing, trademark, the pre-launch legal assessment, and per-asset provenance.

This section states what you may and may not do with the material in this repository. It is deliberately explicit: a design system that is source-available without clear terms is a liability for everyone who touches it.

8.1 - Licensing

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

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

Brand assets — proprietary, source-available

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

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 under brand/templates/ fall under these same terms: they embed brand identity (colours, fonts, tagline). You may use them as structural reference for your own templates, but not redistribute them with projectious.work branding intact.

Code, scripts, and tokens — MIT

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

The design tokensbrand/tokens/ — are in this category. The values are free to use; the marks are not.

What this means in practice

Do

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

Don't

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

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

8.2 - Trademark

Guidelines for referring to projectious.work and using its marks.

The names, logos, and icons associated with projectious.work are its trademarks. Full text: TRADEMARK.md.

Not permitted

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

Permitted

  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.

8.3 - Legal assessment

Pre-launch clearance review of the name, mark, fonts, icons, and imagery sources.

A pre-launch review of the elements that carry legal risk. Source: brand/html/projectious-legal-assessment.html.

Summary

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

Fonts

All three families are SIL Open Font License 1.1 and served from Google Fonts. OFL permits commercial use, embedding, and redistribution; it forbids selling the fonts on their own and requires that any derivative font use a different name. Since the fonts are linked rather than bundled, no font binaries are redistributed by this repository.

Icons — Lucide

ISC License, functionally equivalent to MIT: free commercial use, modification, redistribution, sublicensing. No attribution required in product UI — only the notice retained in source. Over 1,400 icons, actively maintained, a fork of Feather with a broader community.

Photography

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.

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

8.4 - Asset provenance

Per-asset source, licence, and attribution status for every third-party dependency.

The authoritative inventory is brand/PROVENANCE.md in the repository. Check it before adding an asset or shipping collateral.

Fonts

Loaded at runtime from the Google Fonts CDN — no font files are bundled.

FontLicenceAttributionRole
Plus Jakarta SansSIL OFL 1.1Not requiredHeadings, display
Source Sans 3SIL OFL 1.1Not requiredBody, UI
IBM Plex MonoSIL OFL 1.1Not requiredCode, data

Icons and photography

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

SourceLicenceAttributionCommercial
Lucide IconsISCNot 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-07-26: no unlicensed, sensitive, or third-party binary assets are committed to this repository. Every physically present file is original.

9 - Tokens

Machine-readable exports of the design values, and how to consume them.

The token files are the machine-readable form of the foundations. They are MIT-licensed — the values are free to use, the marks are not. See Licensing.

Downloads

FileFormatUse
variables.cssCSS custom propertiesAny web project
tokens.jsonJSONBuild pipelines, Style Dictionary
tailwind.config.jsJSTailwind CSS projects

Source: brand/tokens/.

CSS

<link rel="stylesheet" href="variables.css">
.button--primary {
  background: var(--color-primary);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  transition: background var(--duration-standard) var(--ease-out);
}

What the tokens cover

  • Colour — the seven named aliases. The full 12-step scales are documented on the Colour page; the token file exports the named entry points.
  • Typography — the three font stacks.
  • Spacing — the nine-step 4px scale.
  • Radius — five steps.
  • Elevation — four shadow levels.
  • Motion — four durations, two easing curves.

Consuming them in SCSS

This site maps the tokens onto Bootstrap and Docsy variables in src/assets/scss/_variables_project.scss. That file is a worked example of wiring the brand into an existing component framework — including the full 12-step scales in both modes, which the CSS export does not carry.