Design language

What the portal looks like and why. Every example on this page is the portal's own component, drawn from its own tokens.

Facility accent
Accent · on-accent

Start

Overview

The portal follows the conventions of GitHub settings, the Tailscale and Cloudflare consoles, and Grafana. A physicist or an operator should recognise every control without learning anything. The design's job is to make the state of someone's access obvious and to show the one thing to do next.

Four rules everything else follows from

  • Containers recede, content leads. A box, a rule or a header never competes with the data inside it. Borders are 1px and pale; fills are rare. The data is the darkest thing in a box.
  • Colour means something or it is absent. Neutrals are untinted greys. The accent marks the main action and the current location; it is ink unless a facility opts into its own colour. Status colours mark status and nothing else.
  • The accent is never a message. Banners and notes use the fixed status colours, so a facility whose colour is red or yellow cannot make a notice look like an error or a warning.
  • One loud thing per page. At most one page banner and at most one primary button. If nothing is wrong, the page is quiet.

Every table, token and name on this page is example data. This page is built from the portal's own tokens, Basecoat classes, af- extensions and Vue components, so an example cannot differ from what ships. Use the accent control above to try a facility colour; every example responds, and the theme control in the top bar switches light and dark.

Start

Built on Basecoat

The portal is built on Basecoat (basecoat-css): plain CSS classes that work the same in .astro pages and .vue components. The standing rule is Basecoat-class-first: if Basecoat ships a component, use its class and attributes and re-theme it through the token bridge; extend it with a separate af- class; never redefine a Basecoat class. Basecoat supplies structure, the portal's tokens supply identity.

Basecoat components in use

Basecoat components in use
Basecoat classAs used hereNotes
btnNo variant: the primary action. data-variant="outline": ordinary action. data-variant="ghost": low emphasis (Cancel, row links). data-size="sm" 32px in tables and banners; default 36px; data-size="lg" 40px; data-size="icon" 36px square.One primary per page. Danger is btn af-danger with data-variant="outline".
badgedata-variant="outline": a neutral pill label for a category (permission, credential type, read or write).20px, 12px text. Never status.
alertThe box for banners and notes, with af-banner or af-note.See Banner, note and hint.
table-container + tableWith af-box (frame); af-compact, af-num, af-act, af-sub on top.Three or more items with the same fields.
field, input, native-select, textarea, label, checkboxA label above the control, hint or error below. field with data-orientation="horizontal" for a checkbox. af-narrow caps a field at 360px.A placeholder never stands in for a label.
button-groupWith af-toggle: a single-choice switch (SegmentedControl). The selected button is ink, not the accent.A range or filter, not page navigation.
dialogNative <dialog> with af-modal (scrim, overlay shadow). ConfirmDialog wraps it; focus starts on Cancel.Decisions that cannot be undone, or a short form.
emptyCentred title, sentence and optional action.A list with nothing in it.
chartThrough BarChart.vue (Chart.js underneath, loaded only where a chart mounts): neutral --chart-N bars.See Chart.

Portal extensions on Basecoat

Portal extensions on Basecoat classes
ClassWhy it exists
af-dangerBasecoat's destructive variant is a tinted fill; the contract wants a red outline and label, never a fill, so it cannot be mistaken for a red facility accent.
af-banner / af-note (ok, warn, bad, info) on alertBasecoat's alert has no status treatments and expects an svg; these add the solid banner fills and tinted notes and lay a mark, text and action in one wrapping row. af-banner slim is the one-line shell bar.
af-box, af-compact, af-num, af-act, af-subThe table frame, 32px density, right-aligned numbers, the action cell and secondary cells.
af-toggleFills the selected button-group button with ink rather than the accent.
af-modalThe scrim and overlay shadow on dialog.
af-btn-xlThe one 52px call to action on the landing page.

Bespoke pieces

Pieces Basecoat has no equivalent for
Class or componentWhat it is forNot when
af-st + af-mark (StatusMark)A status word with its shape. Tones ok, warn, bad, info, busy, off.Labelling a category; the mark without a word.
PageMessageA page's banner slot: a problem or warning as af-banner, a hint as af-note info.More than one per page.
AsyncStateThe loading, session-expired, access-denied, error and empty ladder for fetched data.Hand-written loading or error markup.
af-kvA two-column list of label and value for one record.Comparing several records.
af-stats + af-statA label, a 24px value and an optional 12px line.A number needing a row of context.
af-copy (CopyField)A read-only mono value with a Copy button ("Copied" for two seconds).Values nobody copies.
Disclosure, ArmedConfirmButtonAn expanding region; a two-step inline confirm.Hiding the page's main content; anything needing an explanation of what is lost.
InfoTip / PopoverTooltipA tooltip on the native Popover API. A documented exception: Basecoat's data-tooltip is clipped by scrolling ancestors.Anything needed to finish the task; it is not reachable on touch.
af-nav, af-nav-group, af-nav-labelThe sidebar. A documented exception: the shell sidebar (fixed, grouped, count badges, drawer under 860px) is built in Base.astro, because Basecoat's sidebar targets an app-shell layout the portal does not use.In-page tabs.
af-page, af-page-head, af-sect, af-help, af-wrap, af-topbar, af-brand, skip-link, af-icon, EntitlementMarkPage structure, shell, icon sizing; the entitlements matrix cell.

The token bridge

Basecoat's own variables (--primary, --border, --ring, --chart-1 ...) are set from the portal's tokens in global.css, so a colour is changed once. These are their values on this page right now. Try an accent above: --primary follows it.

Basecoat variables and the values they resolve to
TokenValue nowUse
--primaryThe primary button, checked controls and links: the AF accent.
--primary-foregroundText on a primary fill: the AF on-accent colour.
--borderBasecoat container outlines, from the AF border.
--inputBasecoat control outlines, from the AF strong border.
--ringThe focus border and 3px halo of Basecoat controls.
--muted-foregroundBasecoat secondary text, chart axis labels.
--chart-1A chart bar: a neutral step.
--chart-2A chart bar on hover.

How it is wired is in docs/portal-styling.md.

Start

Design tokens

A token is a named design decision. Code refers to the name, never to the raw value, so a decision is changed in one place: portal/src/styles/global.css.
The three layers of tokens
LayerWhat it isExampleWho may use it
1. TokenA role, with its value. Colours, shadows, fonts, the type scale and radii are in @theme; spacing, control heights and motion in :root; the dark values in .dark.--color-af-border, defined once in global.cssEvery component and page, by name. Only global.css holds a literal colour.
2. BridgeBasecoat's own variables, set from the tokens so Basecoat follows the theme.--border: var(--color-af-border)Basecoat's stylesheet only.
3. ComponentA class that combines tokens: Basecoat's, re-themed through the bridge, plus af- extensions in components.css..btn[data-variant="outline"]Pages.
border: 1px solid var(--color-af-border);
padding: var(--space-16);
font-size: var(--text-14);

Douse a token for every colour, size, radius, shadow and duration.

border: 1px solid #1e2a3d;
padding: 14px;
font-size: 9px;

Don'twrite raw values in a component. Each one is a decision made again, by hand.

Enforcement: a contract test (designContract.test.ts) rejects hex colours, raw font sizes, off-scale radii and shadows, and hand-rolled replacements for Basecoat classes in portal/src. Without it the written system and the shipped portal drift apart.

Visual styles

Colour

Three groups with separate jobs. Neutrals build the page. The accent marks action and location: ink by default, or a colour the facility opts into. Status colours are fixed for every deployment and carry meaning.

Neutrals

Neutral colour tokens and their values now
TokenValue nowUse
--color-af-bgPage and container background.
--color-af-bg-subtleHover rows, read-only fields, selected nav item.
--color-af-bg-hoverHover on buttons and menu items.
--color-af-borderContainer outlines and row rules.
--color-af-border-strongAnything a person can type in or press.
--color-af-textAll primary text, including links.
--color-af-text-secondaryLabels, column headers, help text.
--color-af-text-disabledDisabled controls only, never readable text.

Facility accent

The default is a neutral monochrome theme: the accent is ink, near-black in light mode and near-white in dark. The portal does not give a facility an identity it has not chosen. A deployment may opt in by setting one colour (docs/portal-styling.md has the derivation); the portal derives the rest, so any colour is safe:

  • The accent appears only as a fill (primary button, checked control, skip link) or a 3px marker (current sidebar item). It is never a text colour, so a pale facility colour cannot produce unreadable links.
  • The on-accent colour is computed: white if it reaches 4.5:1 on the accent, otherwise near-black.
  • In dark mode a dark accent is lightened until it reaches 3:1 against the page, unless the deployment supplies its own dark-mode value.

The accent in use

Accent tokens and their values now
TokenValue nowUse
--color-af-accentA fill or the 3px nav marker. Never text, border or banner fill.
--color-af-on-accentText and icons on an accent fill: white if it reaches 4.5:1, otherwise near-black.

Status

Status meanings, marks, tokens and banner fills
MeaningMarkTokenBanner fillUse when
OKReachable--color-af-okWorking, linked, valid.
WarningExpires in 2 days--color-af-warnWorks for now, or someone must wait or ask.
ProblemBlocked--color-af-badNot working. Also destructive actions.
InformationMaintenance Friday--color-af-infoA neutral notice or hint. Nothing is wrong.
In progressMinting proxy--color-af-text-secondarynoneWaiting on the system.
OffNot linked--color-af-text-secondarynoneAbsent by choice, not an error. "Not linked" is this mark: it never raises a banner, at most a hint.

Chart colours are Basecoat's --chart-1 to --chart-5, bridged to neutral steps (secondary text, text, strong border, disabled text, border), never status colours. Banner fills and their --color-af-on-banner-* text are the same in both themes; --color-af-bad-text is the only status colour used for text.

Do and don't

Dokeep messages in status colours and actions in the accent. Try the yellow accent above: the warning still reads as a warning because of its shape, title and position.

Your grid proxy expires in 2 daysRenew it to keep Rucio and AMI working.

Don'tfill a message with the accent. Try maroon or yellow above: with maroon it reads as an error; with yellow, every notice looks like a warning.

LinkedBlocked

Dopair every status colour with a shape and a word. Colour alone fails for colour-blind users.

cern-ssorucio

Don'tcolour a name to show its state.

Used on every page. Status marks: Overview, Services, Identities, Tokens, Admin.

Visual styles

Typography

One sans-serif for everything a person reads. Monospace only for values a person copies or compares character by character. Hierarchy comes from size and weight, never from a change of typeface, from capitals or from letter-spacing.
Type scale: size tokens, current values, specimens and uses
SpecimenTokenValue nowUse
Sign in--text-3232 / 40px, 600. Landing page headline only.
Tokens--text-2424 / 32px, 600. Page title, one per page. Stat values.
No tokens yet--text-2020 / 28px, 600. Landing section headings; empty-state title; dialog title.
Linked accounts--text-1616 / 24px, 600. Section title, card name.
claude-desktop-laptop--text-1414 / 20px, 400 and 500. Default: body, cells, buttons, inputs.
Last used--text-1313 / 20px, 400 and 500. Column headers, help text, notes.
Updated 2 minutes ago--text-1212 / 16px, 400. Badges, nav group labels, chart axes. The minimum.
Font family tokens and their current values
TokenValue nowUse
--font-sansIBM Plex Sans: all text and headings.
--font-monoIBM Plex Mono: tokens, hostnames, tool names, commands, reference ids.

Typeface. IBM Plex Sans for headings and text; headings differ only in size and weight (600). IBM Plex Mono, from the same superfamily, for literal values, so a token string beside its label reads as one typeface in two modes.

  • Weights in use: 400 for text, 500 for labels and buttons, 600 for headings. No others.
  • Number columns are right-aligned with tabular numerals (af-num).
  • Capital I, lowercase l and 1 are drawn differently, which matters for hostnames, token names and dataset IDs.
  • Self-hosted and open-licensed (OFL). No letter-spacing is added to text; if a page feels tight, loosen row height and padding instead.
  • Help text is capped at 80 characters per line, empty-state text at 52.
ServiceCallsErrors
Rucio1,28411
AMI3170

Doset names in the text face and right-align numbers with tabular figures.

ServiceCallsErrors
Rucio1,28411
AMI3170

Don'tset whole tables in monospace or use tiny spaced capitals for headers. This is the terminal look the portal is leaving.

Used on every page.

Visual styles

Dimensions

Every gap, padding and control height comes from one scale based on 4px. Density is a named choice per page, not something each component decides.
Spacing tokens, current values and uses
TokenValue nowUse
--space-2Hairline offsets.
--space-4Icon to label inside a badge; between a title and its line.
--space-8Icon to label; between related controls.
--space-12Section title to content; between a label group and its control.
--space-16Cell padding; inside banners and notes.
--space-24Page header to content; compact page gap.
--space-32Between sections; page margin on wide screens.
--space-48Between landing regions.
--space-64Landing page rhythm; page bottom.
Control height tokens and their current values
TokenValue nowUse
--control-sSmall: Basecoat data-size="sm". Row actions inside tables, buttons in banners, segmented controls.
--control-mMedium: the default button, input and nav item.
--control-lLarge: data-size="lg"; copy fields.

There is no token for the 52px extra-large button (af-btn-xl): it is the landing page's Sign in, and nothing else.

Density

Comfortable and compact density
ModeTable rowCell textSection gapPages
Comfortable44px14px32pxOverview, Services, Identities, Tokens, Usage
Compact32px13px24pxEntitlements, Admin

Operators scan many rows at once, so their pages are compact. Physics users check a handful of items, so theirs are comfortable. A compact table may sit inside a comfortable page for a secondary checklist.

Visual styles

Layout grid

A fixed sidebar and one content column. Content is a single column of full-width sections stacked top to bottom. Side-by-side cards are the exception, not the default.
Layout by viewport width
ViewportSidebarContent marginContent max width
1136px and up220px, fixed32px1120px
860 to 1135px220px, fixed24pxfluid, up to 1120px
600 to 859pxA drawer opened from a 52px top bar24pxfluid
Under 600pxA drawer opened from a 52px top bar16pxfluid
Linked accounts
Grid certificate

Dostack full-width sections. Tables want width, and the reading order is unambiguous.

Don'tdefault to a row of three equal cards. It gives every item the same weight whether or not it matters.

Page content is 960px wide by default (--af-page-max on .af-page), left-aligned inside the column. A page whose main content is a wide table or chart adds af-wide to lift that cap: Services, Tokens, Usage and Admin do (Entitlements has no .af-page root, so it is already uncapped); Overview and Identities do not.

Wide tables scroll inside their own box, never the page. Used on every signed-in page. The landing page has no sidebar: the same column, text capped at 720px, regions 64px apart.

Visual styles

Border and corner radius

Borders separate; they do not decorate. Corners are soft enough that a container feels like a surface holding its content, and consistent enough that nobody notices them.
Corner radius tokens, current values and uses
TokenValue nowUse
--radius-4Popover tooltips, chart indicators.
--radius-8Buttons, inputs, nav items, copy fields (Basecoat's rounded-md, --radius minus 2px).
--radius-10Containers: table boxes, row lists, banners, notes, fields, empty states. Basecoat's dialog panel is 14px.
--radius-fullBadges (tags) and round icon buttons. Not status pills: status uses the status mark.
Border colour tokens and their current values
TokenValue nowUse
--color-af-border1px. Container outlines, row rules.
--color-af-border-strong1px. Anything a person can type in or press.
  • Table headers are unfilled. No coloured stripe down one side of a container.
  • The empty state (Basecoat's empty) is the only dashed border, and its content is centred, as Basecoat lays it out.
  • Focus: Basecoat's controls draw a border and 3px halo from --ring; the portal's own links, nav items and summaries draw a 2px outline in the text colour, offset 2px. No element shows both.
AccountStatus
CERN SSOLinked

Dolet the container recede: unfilled header, pale rules, the data is the darkest thing in the box.

AccountStatus
CERN SSOLinked

Don'tuse heavy outlines, filled headers or a coloured stripe down one side. The frame becomes louder than the content.

Visual styles

Elevation

Elevation says what is temporarily on top. Everything that belongs to the page is flat. Only things that appear over the page have a shadow.
Elevation levels
LevelTokenAllowed on
FlatnoneTables, sections, rows, banners, notes, sidebar, top bar, popover tooltips. Separated by a 1px border, never a shadow.
Raised--shadow-af-1The chart tooltip (through Basecoat's --shadow-xl), the open navigation drawer. No scrim.
Overlay--shadow-af-2 + --color-af-scrimDialogs. Scrim behind, focus trapped.
Shadow and scrim tokens and their current values
TokenValue nowUse
--shadow-af-1Raised: the chart tooltip (through Basecoat's --shadow-xl), the open navigation drawer.
--shadow-af-2Overlay: dialogs.
--color-af-scrimThe scrim behind a dialog.

Level 1: raised

A raised surface

Level 2: overlay

An overlay surface

The two swatches show the shadow tokens; the real thing is the dialog (see Dialog and tooltip) and the chart tooltip (hover a bar in Chart). No background blur anywhere. In dark mode a shadow is hard to see, so both also carry a 1px light outline. Basecoat's own small shadows (--shadow-xs, --shadow-sm) resolve to none and its backdrop blur to 0.

Expression

Motion

Motion explains a change: something appeared, moved or finished. Nothing moves for atmosphere on working pages.
Motion tokens and their current values
TokenValue nowUse
--time-fastHover colour.
--time-baseDisclosure chevron, the navigation drawer, a chart's first draw.
--time-slowThe sign-in spinner.
--easeEvery transition. Starts fast, settles.
  • Only hover colour, a turning chevron, the sliding drawer, a small spinner and a chart's first draw move. Page content does not animate on load.
  • With reduced motion requested, every transition and spinner stops and charts draw without animation.
  • Two named exceptions are kept on purpose: the particle-track canvas (beside the Overview header and in the landing hero; one still frame under reduced motion) and the gateway diagram on the landing page (runs only on screen, never under reduced motion). Neither sits behind text a person needs to read.

Expression

Icons

An icon repeats a label; it does not replace one. One set, Lucide, at one size.
  • Icons are inline SVG, so they inherit text colour and need no network request or icon font.
  • They come from the @lucide/vue and @lucide/astro packages, pinned in package.json; markup is never pasted by hand, so an icon can always be traced and updated.
  • 16px beside text, 20px in a large button. Lucide's default stroke; no per-icon weights or colours. currentColor, aria-hidden when decorative.
  • An icon-only button has an accessible name. Status uses the status mark, not an icon.

Lucide at 16px, and in a large button at 20px

PlusCopyExternalLinkXChevronRightKey

Dopair an icon with its word. An icon-only button needs an accessible name.

✓ Linked⚠ Expiring▸ Details→ Continue

Don'tuse text glyphs as icons. They change shape between fonts and platforms. An arrow is allowed only inside a sentence.

Content

Writing

Say what the state is and what to do about it, in the words a physicist would use to a colleague. The portal never asks a person to understand the broker to fix their access.
Writing rules with an example each
RuleWriteNot
Name the thing and the fixYour grid proxy has expired. Renew it to keep grid services working.Access denied (403).
Buttons are verb + objectLink certificateContinue
Sentence case everywhereNew tokenNEW TOKEN, New Token
Relative time, exact on hoverExpires in 3 days2026-11-13T09:14:00Z
No fillerToken createdYour token was successfully created!
Say what is lostRevoke token. Clients using it stop working immediately.Are you sure?
Empty states say what goes hereNo tokens yet. Create one for clients that can't sign in with a browser.Nothing to show.
Product words, not internalsGrid certificatex509 VOMS proxy credential provider

Internal terms (provider class names, config keys) appear only on the Admin page, where the reader is an operator.

Components

Status mark

A small filled shape followed by a plain word. The shape differs per meaning (circle, triangle, square, diamond, half ring, empty ring), so it works without colour. This is StatusMark.

The six tones

ReachableExpires in 2 daysBlockedMaintenanceMintingNot linked

Doput the fix next to a problem status.

ACTIVEBLOCKED

Don'tuse outlined pills for status. In a table they become a column of boxes louder than the names beside them. A tag or pill labels a category, never a state.

Used on: Overview, Services, Identities, Tokens, Admin.

Components

Button

Four kinds, ranked, in four sizes (32, 36, 40 and, for Sign in on the landing page, 52px). A page has at most one primary button; everything else is secondary or quiet.

Kinds

Sizes

Button kinds
KindLooks likeUse
Primary (btn)Accent fillThe page's main action. One per page, in the page header.
Secondary (data-variant="outline")OutlineOther actions: Refresh, Copy, row actions.
Quiet (data-variant="ghost")Text onlyCancel, dismiss, low-stakes actions in dense rows.
Danger (btn af-danger on the outline variant)Red outline and textRevoke, unlink, delete. Never filled, so it cannot be confused with a maroon or red facility accent.

Tokens

claude-desktop-laptop

Dogive the page one primary button and keep row actions small.

Don'tmake every action primary. Nothing leads when everything does.

Used on every page.

Components

Table

Where several things share the same fields, a table is the honest shape: tokens, usage per service, entitlements, admin data.
Tokens, example data
NameStatusCreatedLast usedCalls, 30 daysActions
claude-desktop-laptopActive12 Oct 20262 hours ago1,284
gemini-cli-workstationExpires in 5 days18 Aug 2026Yesterday317
old-notebookRevoked3 Mar 20264 months ago0

Compact, for Admin and Entitlements

Calls per user and service, example data
UserServiceCallsErrorsLast call
example-user-1Rucio4,102121 minute ago
example-user-2HTCondor873014 minutes ago
example-user-3AMI9632 hours ago
  • First column is the name, in the text face at normal weight and full ink. It is the darkest thing in the row.
  • Numbers are right-aligned with tabular figures (af-num). Dates and secondary facts are secondary text (af-sub).
  • Row actions (af-act) are small buttons at the right edge. More than two go into a menu.
  • Header is unfilled, 13px, secondary colour and sentence case. Rows are 44px, compact rows 32px.
  • Under 600px the table scrolls sideways inside its box; the page does not.

Used on: Tokens, Usage, Entitlements, Admin. Services and Identities are row lists (one bordered list, a rule between rows, a name and a status on each), not tables.

Components

Form field

Label above, help below, error in place of help. One column, capped at a readable width (af-narrow).

With help

Shown in your token list. Name the device or client.

With an error

Checkbox, and a note on what it does

Leave this unchecked for a token with your full account access.

Douse a visible label and a sensible default.

Don'tuse the placeholder as the label. It disappears as soon as the person types.

Used on: Tokens (new token), Identities (grid certificate passphrase, keytab upload), Admin (maintenance message).

Components

Copy field

A read-only value with a Copy button. The one place monospace is large and prominent, because the person is about to paste it somewhere exact.

CopyField: a value, and a token with its warning

https://mcp.example-facility.org/mcp
mcp_pat_EXAMPLE_8f2c41d7b0a94e6f9c3d5a1e7b2f0a91e

The buttons are live. After copying, the button reads "Copied" for two seconds. A multiline copy field holds a command or snippet. Used on: Overview (endpoint and client snippets), Tokens (new token dialog).

Components

Key-value list and stat

A key-value list shows the facts about one thing. A stat shows one number that matters. Neither needs a card around it.

Key-value list (af-kv)

Status
Valid
Subject
/DC=ch/DC=cern/OU=Users/CN=example
VO
atlas
Expires
In 11 hours

Stats (af-stats)

Calls1,601Last 30 days
Errors110.7%
Services used4of 6 available
Calls1,601
Errors11

Doset stats as plain numbers in a row, separated by space.

Calls1,601
Errors11

Don'tput each number in its own shadowed card with a coloured value.

Used on: Identities (certificate and account details), Usage and Overview (stats), Admin.

Components

Chart

Basecoat's chart component, which wraps Chart.js, through BarChart.vue. Used for the daily activity on the Usage page.

BarChart, example data. Hover a bar for the raised tooltip.

Calls per day, example data
Calls per day, example data
LabelCalls
3 Nov62
4 Nov70
5 Nov48
6 Nov75
7 Nov30
8 Nov0
9 Nov12
10 Nov55
11 Nov68
12 Nov80
13 Nov58
14 Nov44
15 Nov71
16 Nov39
  • Bars are --chart-1 (secondary text) and their hover is --chart-2 (text): neutral steps, legible in both themes, never status colours. The grid is --border; axis labels are 12px in secondary text in the page font, with flat bars and no gradient or shadow.
  • A canvas is opaque to assistive technology, so the chart is role="img" with a summary label and every value is repeated in a visually hidden table.
  • It draws without animation when reduced motion is requested, and redraws with the new colours when the theme changes. Chart.js and Basecoat's chart script load only on a page that draws a chart, and only when it mounts.

Used on: Usage.

Components

Dialog and tooltip

A dialog interrupts for a decision that cannot be undone or a short form. A tooltip names an icon or shows an exact value.

Dialog

This dialog is live: it opens a real native dialog on a scrim, traps focus, starts on Cancel and closes with Escape.

Revoke claude-desktop-laptop?

Clients using this token stop working immediately. This cannot be undone.

Two-step inline confirm

For a destructive action too small for a dialog.

Tooltip

Expires in 2 days13 November 2026, 09:14 UTC
  • Dialog buttons sit bottom right, with the confirming action last. The confirming button repeats the title's verb. The title is the 20px step.
  • A tooltip never holds information a person needs to complete the task. It is not reachable on touch.
  • Prefer inline content to a dialog. Do not open a dialog from a dialog.
  • A tooltip is flat: page background, strong border, 4px radius. It uses the native Popover API, so a scrolling ancestor cannot clip it.

Used on: Tokens (new, revoke), Identities (unlink, consent), Admin (maintenance mode). Tooltips: badges, dates, icon-only buttons.

Components

Empty state

When a list has nothing in it, say what belongs there and offer the action that fills it. Data fetched from the broker goes through one ladder of states (AsyncState): loading, session expired, access denied, error, empty, then the content.

Empty state

No tokens yet

Tokens let clients that can't sign in with a browser, such as Claude Desktop, reach the gateway as you.

AsyncState: loading

Loading usage…

AsyncState: session expired

AsyncState: access denied

AsyncState: error

AsyncState: empty

No tool calls in the last 30 days

Once your AI assistant calls tools through this platform, its estimated context usage shows up here.

This is Basecoat's empty, which centres its content under a dashed border; the portal keeps that layout. An empty state may carry the page's primary button, so the page header does not repeat it. The failure states are shown as notes here (inline); a page uses the banner form when the page's own data fails to load. Used on: Tokens, Identities, Usage (no calls in range).

Pages

Page pattern

Every signed-in page has the same four parts in the same order: title with the one primary action, the banner slot, then sections. A person who has seen one page knows where to look on all of them.

A signed-in page: Tokens, example data

Tokens

For clients that can't sign in with a browser.

Your tokens
Tokens, example data
NameStatusLast usedActions
claude-desktop-laptopActive2 hours ago
gemini-cli-workstationExpires in 5 daysYesterday
The parts of a page
PartRule
1. Maintenance barOnly while maintenance is on: the slim information bar above everything.
2. Page headerTitle (24px), one line saying what the page is for, and at most one primary button on the right.
3. Banner slotEmpty when nothing is wrong. Otherwise the single most urgent message for this page: a banner for a problem or warning, a hint (a quiet note) for information. Problem beats warning beats hint.
4. SectionsFull width, stacked, 32px apart (24px on a compact page). Each has a 16px title and one main component: a table, a key-value list, a row of stats or a copy field.
SidebarSame on every page. Accent marker on the current page.

The example is built from the real page classes (af-page, af-page-head, af-sect, af-nav) and components; the shell's sidebar and drawer themselves live in Base.astro. A data page also shows a problem banner when its own data fails to load.

Pages

Per-page specs

What each page is for, its one primary action, when its banner or hint appears and which component carries it.
Each page's job, action, banner and components
PageJobPrimary actionBanner slotMain componentsDensity
Landing /Say what the platform is; sign in.Sign in (af-btn-xl)Maintenance bar onlyHeadline, particle canvas, gateway diagram, benefitsSpacious
OverviewIs my access working? How do I connect a client?None; Copy on the endpoint In priority order: grid proxy expired (problem); a service misconfigured (problem) or unavailable (warning); a service rejected a linked credential (warning); proxy expiring (warning); a token expiring within 7 days (warning). Then hints: no account linked; N services not linked. Status rows, stats, copy field, particle canvas in the headerComfortable
ServicesWhich backends and methods can I reach?None Misconfigured (problem), unavailable (warning), a credential rejected (warning, with a re-link action when they share one identity). Hint: N services not linked, with an action when they share one account. Search, filter, service rows (not a table)Comfortable
IdentitiesLink accounts and keep the grid certificate valid.None in the header; each card has its ownProxy expired (problem) or expiring (warning); a link failed (problem); a link succeeded (ok, clears after 5 seconds). No hint: the rows show what is not linked.One card per credential, key-value listsComfortable
TokensCreate and revoke tokens for clients without a browser.New tokenA token expires within 7 days (warning); a revoke failed (problem)Table, dialog with copy fieldComfortable
UsageWhat have my clients been doing?None; range controlNoneStats, bar chart, table per serviceComfortable
EntitlementsWhich group grants which permission?NoneNoneCompact matrix tableCompact
AdminPlatform state across users; operators only.None; maintenance toggle in its sectionTool configuration issues (warning)Compact tables, status marksCompact

A data page also shows a problem banner when its own data fails to load.

State rules that hold on every page

  • Not linked is off, not wrong. The off mark, the words "Not linked", a quiet row. It never raises a warning or problem banner; at most a hint. A service that needs no credential (the gateway's own) is never counted as unlinked. The Identities page needs no hint: its rows show the state and the action.
  • Credential rejected is a warning, not "Not linked". The user did link, and the backend refuses what they set up. Warning mark, the words "Credential rejected", a row at full ink (never the quiet treatment), one sentence ("Your linked <identity> was rejected. Re-link it to use this service.") and a re-link action that names the identity and goes to its card. It raises a warning banner, ranked below misconfigured and unavailable and above a not-linked hint. It sorts with the things that need attention and is never counted as not linked. Its "Powered by" line shows no link state.
  • A grid proxy warns only when it cannot renew itself and has under 1 hour left. An auto-renewing proxy never shows as expiring.
  • Misconfigured and Unavailable differ. Misconfigured needs an operator: problem mark and a reference id to quote. Unavailable is temporary: warning mark, nothing to do but wait.
  • Access required needs an administrator: warning mark, inline detail, no banner. A row shows its identity's link state only where that is the actionable fact.
  • Maintenance is the slim information bar above the page, on every page including the landing page, not the page banner.