Skip to content

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-ui stack) — all prose, labels, headings.
  • Mono (--font-mono, ui-monospace stack) — reserved for data: coordinates, date-time groups, IDs, callsigns, counts. In web use the <Mono> primitive (adds tabular-nums); anywhere else use font-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. Padding sm/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 a text-xl font-semibold sans title + muted description. web: shared/ui/SectionHead.tsx · directory: components/section_head.edge

  • Mono — data values

    font-mono tabular-nums wrapper. Wrap every coordinate, DTG, ID, and count. web: shared/ui/Mono.tsx

  • Badge families — three, kept separate

    StatusBadge (lifecycle → badge-*), SeverityBadge (ok/warning/critical), and AffiliationDot/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 · SECRET warning · CONFIDENTIAL info · RESTRICTED primary · else success. Text text-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/40 tile with a cyan bold "P", next to the PETRA wordmark (font-semibold tracking-wide). See web/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 petra also appears in web as 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/ui primitives; don't hand-roll a card or 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-content opacity, borders with white/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