PETRA design system¶
PETRA is Bedrock Defence's tactical situational-awareness product. Its two
human-facing clients — web (the COP / decision-support
browser client) and directory (the identity
console) — share one design language, implemented as a single DaisyUI
v5 theme named waypoint.
This page is the source of truth for that language: the tokens, the components, and the
rules for using them. Read it before you build or restyle any PETRA UI, or write copy
that ships inside the product. If you are an agent adding a screen to web or directory,
these are your constraints — follow them and the screen will look native without design
review.
One theme, two apps
web/resources/css/app.css and directory/resources/css/app.css define the
identical waypoint theme. There is no tailwind.config.js — this is Tailwind
v4 CSS-first config (@plugin "daisyui/theme"). Change a token in one app and you
must change it in the other, or the clients drift. This page is where the shared
values are documented.
The one rule: three colour families never cross¶
PETRA runs three separate colour vocabularies. Mixing them is the single most common way to make a screen look wrong — and, worse, to make map data ambiguous. Keep them apart.
-
Brand / surface
Deep-navy surfaces + blue primary + cyan accent. The chrome and interactive skin of the app. Used everywhere for layout, buttons, links, focus.
-
Status — UI state only
info/success/warning/error. Toasts, validation, destructive-action confirms, lifecycle badges. Never describes an entity on the map. -
Affiliation — map data only
friend/hostile/neutral/unknown(APP-6 / MIL-STD-2525). Drives track dots and symbols. Never used for chrome, buttons, selection, or status.
Do not cross the streams
- A green badge means success (a UI state), not a neutral/friendly contact. Neutral is a separate teal token used only on the map.
- Cyan (
accent) means armed / active / selected — an interactive signal. It is never an affiliation and never a status. - Red
error(#ff6b81) is a UI failure colour. Hostile red (#ff5a4f) is a different token for map symbols. They look close on purpose-and must stay distinct in code so the map's meaning never leaks into the chrome.
Colour tokens¶
Dark-first. color-scheme: dark; base-100 is the lightest surface (cards, inputs),
base-300 the darkest (navbar, statusbar chrome). Reference tokens as DaisyUI classes
(bg-base-100, text-primary, border-accent/40) — never hardcode the hex.
Brand & surface¶
| Token | Swatch | Hex | Use |
|---|---|---|---|
base-100 |
#101a2e |
Lightest surface — cards, inputs, popups | |
base-200 |
#0e1626 |
Page body background | |
base-300 |
#0a0f1c |
Darkest chrome — navbar, statusbar | |
base-content |
#dbe4f3 |
Body text on navy | |
primary |
#5b93ff |
Bright blue — primary actions, links, active nav | |
secondary |
#3f5f8f |
Steel blue — secondary emphasis | |
accent |
#22d3ee |
Cyan — armed / active / selected signal, section eyebrows | |
neutral |
#16223a |
Raised navy — muted fills | |
border |
#223351 |
Hairline border (--color-border) |
Status — UI state only¶
Toasts, validation, destructive confirms, lifecycle badges. Do not use for map entities.
| Token | Swatch | Hex |
|---|---|---|
info |
#5b93ff |
|
success |
#4ad699 |
|
warning |
#f0ab3d |
|
error |
#ff6b81 |
Affiliation — map data only¶
APP-6 / MIL-STD-2525 symbology. Exposed as --color-aff-*, applied via inline style on
track dots/badges — never as chrome. See APP-6 symbology.
| Token | Swatch | Hex |
|---|---|---|
aff-friend |
#35c0f5 |
|
aff-hostile |
#ff5a4f |
|
aff-neutral |
#35d17d |
|
aff-unknown |
#f0c53a |
Text hierarchy by opacity¶
PETRA builds text hierarchy by fading base-content, not by switching colours. Borders
are hairlines at low opacity. Learn the ladder:
| Class | Role |
|---|---|
text-base-content/90 |
Strong — titles, primary values |
text-base-content/60 |
Body / description |
text-base-content/55 |
Inactive nav |
text-base-content/50 |
Field labels, eyebrows |
text-base-content/40 |
De-emphasised / hint |
border-white/10 |
Hairline divider / card border |
Typography¶
Two families, strictly divided:
- Sans (
--font-sans,system-uistack) — all prose, labels, headings. - Mono (
--font-mono,ui-monospacestack) — reserved for data: coordinates, date-time groups, IDs, callsigns, counts. Inwebuse the<Mono>primitive (addstabular-nums); anywhere else usefont-mono.
Mono is a signal, not a style
Monospace tells the operator "this is a precise value you can read off." Don't set prose in mono, and don't set coordinates or DTGs in sans. Eyebrows are the one deliberate exception — mono uppercase is the section-label convention (see SectionHead).
Type scale (size / line-height):
| Token | Size | Role |
|---|---|---|
text-xs |
12px / 1.1rem | Small labels, badges |
text-sm |
13px / 1.2rem | Secondary text, nav |
text-base |
15px / 1.55rem | Body |
text-lg |
18px / 1.5rem | Emphasis / h3 |
text-xl |
22px / 1.7rem | h2 / section title |
text-2xl |
28px / 2.1rem | Page title |
text-3xl |
36px / 2.5rem | Hero (rare) |
Weights are applied with Tailwind utilities, not tokenised: font-semibold (titles),
font-medium (nav), font-bold (logo glyph, hard emphasis).
Shape & sizing¶
Soft, not sharp — "tactical but not brutalist." Reference via the radius/size tokens, don't invent radii.
| Token | Value | Use |
|---|---|---|
--radius-box |
0.625rem |
Cards, panels, popups |
--radius-field / --radius-btn |
0.4rem |
Inputs, buttons |
--radius-selector |
0.375rem |
Toggles, checkboxes |
--size-field |
0.25rem |
40px default control height |
--border |
1px |
All borders are hairlines |
DaisyUI components¶
DaisyUI v5 (daisyui: ^5.5.x) on Tailwind v4. Compose UI from DaisyUI classes
first; drop to raw Tailwind only for layout and the opacity/hairline details above. The
components actually in use, most-used first:
| Component | Notes / common variants |
|---|---|
btn |
The workhorse. btn-primary (commit), btn-ghost (toolbar), btn-error (destructive), btn-xs/btn-sm for dense rows |
badge |
Status & counts. badge-sm default; pair with a status colour or badge-ghost/badge-outline |
input / select / textarea |
*-bordered, *-sm for dense forms |
modal / drawer |
Dialogs and side panels (web's comms panel is a drawer) |
join |
Segmented button/input groups |
table / table-sm |
Background is transparent (inherits card) |
alert |
alert-error/-success/-warning/-info — see tonal-alert pattern below |
card |
Use the Card primitive, not raw card (see below) |
loading loading-spinner |
Async state |
menu / dropdown |
menu menu-sm rounded-box |
toggle / checkbox / radio |
toggle-primary, checkbox-warning, etc. |
steps, tabs, tabs-border, divider, tooltip, progress, collapse |
Occasional |
Toasts: sonner, not daisyUI
web shows notifications with sonner (<Toaster position="top-center"
theme="dark" richColors>), not the DaisyUI toast region. Match that for app-level
notifications; reserve DaisyUI alert for inline, in-page messages.
Patterns & primitives¶
web ships a token-driven primitives layer at inertia/shared/ui/ (barrel index.ts);
directory mirrors the same shapes as Edge components in resources/views/components/.
Reach for these before hand-rolling — they encode the rules above.
-
Card — panel primitive
bg-base-100 border border-white/10 rounded-box shadow-sm. Optional left accent stripe (border-l-2 border-l-{primary|accent|warning|error|success}) to flag a row's state. Paddingsm/md; optional title + actions header.web: inertia/shared/ui/Card.tsx·directory: components/card.edge -
SectionHead + Eyebrow — page/section header
Mono uppercase cyan eyebrow (
font-mono text-xs tracking-[0.18em] uppercase text-accent) over atext-xl font-semiboldsans title + muted description.web: shared/ui/SectionHead.tsx·directory: components/section_head.edge -
Mono — data values
font-mono tabular-numswrapper. Wrap every coordinate, DTG, ID, and count.web: shared/ui/Mono.tsx -
Badge families — three, kept separate
StatusBadge(lifecycle →badge-*),SeverityBadge(ok/warning/critical), andAffiliationDot/AffiliationBadge(driven only by--color-aff-*). Do not collapse them into one.web: shared/ui/badges.tsx -
PageContainer — scroll shell
Width variants
form (max-w-xl)/list (max-w-3xl)/wide (max-w-5xl)/full,px-8 py-6 mx-auto.web: shared/ui/PageContainer.tsx -
Classification banner — always on
Full-width strip, colour by level: TOP SECRET
error· SECRETwarning· CONFIDENTIALinfo· RESTRICTEDprimary· elsesuccess. Texttext-xs font-bold tracking-[0.2em] uppercase. See Security model.web: components/classification_banner.edge·directory: components/classification.edge
Tonal alert pattern (both apps): text-{tone} bg-{tone}/10 border border-{tone}/20
rounded-md px-3 py-2 — a low-saturation wash rather than a solid fill.
App shell: <html data-theme="waypoint">, body bg-base-200 text-base-content, a
h-11 navbar on bg-base-300 border-b border-white/10 (active tab bg-primary/15
text-primary, inactive text-base-content/55 hover:bg-white/5, a cyan mono UTC clock),
main content, and a bottom statusbar.
Brand marks¶
- Wordmark
web/public/logo.svg— seismic / soundwave bars + "PETRA" lettering, white on transparent (works on dark chrome). - Favicon
web/public/favicon.svg— black rounded square (rx=100,#000) with the white seismic-bar glyph. - In-UI lockup — a
size-6 rounded-md bg-accent/15 border border-accent/40tile with a cyan bold "P", next to thePETRAwordmark (font-semibold tracking-wide). Seeweb/inertia/app/Navbar.tsx.
Naming: PETRA vs waypoint vs Bedrock
- PETRA — the product. All user-facing copy, titles, and marks say PETRA.
- Bedrock Defence — the company. The docs you are reading are Bedrock docs.
waypoint— the internal theme/package name (waypoint_web,data-theme="waypoint"). It appears in code and some legacy internal docs; it is not a brand name — never surface it to a user.- Lowercase
petraalso appears inwebas a comms concept (NetCarriage = 'petra' | 'radio', the in-band voice/chat channel). That is a domain term, not the brand.
Authoring checklist¶
For humans and agents building PETRA UI or writing in-product copy:
- Use DaisyUI classes and the
shared/uiprimitives; don't hand-roll acardor a section header. - Reference colour tokens (
bg-base-100,text-primary) — never a raw hex. - Keep the three colour families apart: status ≠ affiliation ≠ brand.
- Cyan (
accent) only for active / selected / armed state. - Coordinates, DTGs, IDs, callsigns, counts → mono (
<Mono>/font-mono). - Build text hierarchy with
base-contentopacity, borders withwhite/10. - Product name in copy is PETRA (uppercase); never "waypoint".
- Dark-first — design on navy; don't assume a white background.
- Respect the classification banner; never remove or restyle it ad hoc.
Where to read the code¶
| What | File |
|---|---|
| Theme tokens (source of truth) | web/resources/css/app.css, directory/resources/css/app.css |
| React primitives | web/inertia/shared/ui/ (Card, Mono, Eyebrow, SectionHead, PageContainer, badges) |
| Edge components | directory/resources/views/components/ (card, section_head, soft_alert, classification, field) |
| App shell | web/inertia/app/AppLayout.tsx, web/inertia/app/Navbar.tsx, directory/resources/views/components/layouts/main.edge |
| Brand marks | web/public/logo.svg, web/public/favicon.svg |