On this page

Navigation
.site-nav · .nav-brand · .nav-links

Text utilities
.prose · .text-link · .tagline-secondary

Labels & metadata
.micro-label · .meta · .mono

Section rule
.section-label

Module
.module · .accent-left · .module-header

Status badges
.badge + 5 tones

Segment tags
.tag · .beachhead

Buttons
.portal-quiet-btn

Data table
.data-table

Index cards
.index-card · .card-grid

Progress
.progress-track · .progress-fill

Question list
.question-list

Code
pre.code · code.inline

Source disclosure
.portal-source-files

Page layout
.page · .page-header · .site-footer

Site nav .site-nav · .nav-brand · .nav-links #nav

One 2px ink bottom border, brand hard left, links pushed right with margin-left: auto. The current page takes .active — a 2px teal underline, never a fill. Under 640px the bar stacks and the links wrap rather than overflow.

<nav class="site-nav">
  <a class="nav-brand" href="/">Konstrukt</a>
  <ul class="nav-links">
    <li><a href="/" class="active">Overview</a></li>
    <li><a href="/components">Components</a></li>
  </ul>
</nav>

Text utilities

Prose & links .prose · .text-link · .tagline-secondary #text

.prose sets text-wrap: pretty for body copy (no orphaned last words). .text-link is a teal inline link that underlines only on hover. .tagline-secondary is the smaller second line under a page tagline. Base already applies pretty-wrapping to p, li, and headings — .prose is for containers whose direct children are not those elements.

Prose sets a softer rag and prevents a single word from being stranded on its own line at the end of a paragraph. Read a text link like this one — teal, no underline until hover.

Secondary tagline — one step quieter, one step smaller.

<p class="prose">Body copy … read <a class="text-link" href="/x">a link</a>.</p>
<p class="tagline-secondary">Secondary tagline</p>

Labels & metadata

Micro label .micro-label #labels

The uppercase kicker above a title or inside a module header. Display face, 700 weight, wide tracking. Coloured --text-muted by default; add .accent where it marks the primary path.

Module label Accent label
<p class="micro-label">Module label</p>
<p class="micro-label" style="color:var(--accent);">Accent label</p>
Meta & mono .meta · .mono #labels

.meta is muted supporting text. .mono switches to the mono face for anything that should read as data: timestamps, counts, IDs, code fragments. Put tabular figures on any number that sits in a column.

Meta text · supporting line 14:32 · 1,204 rows · v1.0.0 $1,240.00
<span class="meta">Meta text · supporting line</span>
<span class="mono">14:32 · 1,204 rows · v1.0.0</span>

Section rule

Section label .section-label #section-label

A label plus a 2px ink rule that runs to the edge of the container. Use it as a real <h2> so the document keeps an outline.

<h2 class="section-label">
  <span class="micro-label">01 · Dynamic</span>
</h2>

Module

Module .module #module

The primary content block: a flat surface with a 1px light border and a 2px ink top border. Add .accent-left for a teal rail that marks the lead panel of a section.

Standard module

Content sits inside the surface. Elevation is carried by the border weight, never a shadow.

Module · accent-left

The teal rail marks a lead panel. One per section, at most.

<section class="module accent-left">
  <div class="module-header">
    <p class="micro-label">Module · accent-left</p>
  </div>
  <p>The teal rail marks a lead panel.</p>
</section>

Status badges

Status badge .badge #badges

Five tones, each bound to a --status-*-bg / --status-* token pair. Badges carry state, not emphasis — never recolour one to make it louder.

Draft Review Complete In progress Launched
<span class="badge badge-draft">Draft</span>
<span class="badge badge-review">Review</span>
<span class="badge badge-complete">Complete</span>
<span class="badge badge-in-progress">In progress</span>
<span class="badge badge-launched">Launched</span>
ClassTokensUse for
.badge-draft--status-draftNot started, inactive
.badge-review--status-reviewAwaiting review, in QA
.badge-complete--status-completeDone, live, shipped
.badge-in-progress--status-in-progressActively being worked
.badge-launched--status-launchedPublic / announced

Segment tags

Segment tag .tag · .beachhead #tags

Audience segmentation, not status. Square, unrounded, 10px display type. The .beachhead variant flags a priority segment.

Builders Operators Learners Beachhead
<span class="tag tag-builders">Builders</span>
<span class="tag tag-operators">Operators</span>
<span class="tag tag-learners">Learners</span>
<span class="beachhead">Beachhead</span>

Buttons

Quiet button .portal-quiet-btn #buttons

Surface fill, 1px ink border. .is-selected / .is-active fills with --accent-soft and teal text. For the single primary action on a page, use .btn (ink fill, white text).

Primary action Secondary
<button class="portal-quiet-btn">Default</button>
<button class="portal-quiet-btn is-active">Active</button>
<button class="portal-quiet-btn is-selected">Selected</button>

<a class="btn" href="#">Primary action</a>
<a class="btn secondary" href="#">Secondary</a>

Data table

Data table .data-table #table

1px light row separators, a 2px ink rule under the header, no zebra striping. Wrap wide tables in .table-scroll so small viewports scroll the table rather than the page.

SurfaceStackStatus
PortalStatic HTML/JSLive
Marketing siteAstroReview
iOS appSwiftUIIn progress
<div class="table-scroll">
  <table class="data-table">
    <thead><tr><th>Surface</th><th>Status</th></tr></thead>
    <tbody>
      <tr>
        <td>Portal</td>
        <td><span class="badge badge-complete">Live</span></td>
      </tr>
    </tbody>
  </table>
</div>

Index cards

Index card .index-card · .card-grid #cards

A linked card for navigation grids. Hover adds a 4px teal left rail. Never nest an anchor inside an anchor — for a card with a secondary link, use <article class="index-card"> with an inner .index-card-primary link and a separate quick-action row.

Card label

Linked card

Use as an <a> when the whole card is one destination.

Card label

Article card

Use as an <article> when the card holds more than one link.

Quick action →

<div class="card-grid">
  <a class="index-card" href="/destination">
    <span class="card-label">Card label</span>
    <h3>Linked card</h3>
    <p class="card-detail">One destination.</p>
  </a>
</div>
Card states .card-meta · .coming-soon #cards

.card-meta is the footer strip inside a card — it sits at the bottom via margin-top: auto, so cards of unequal text length keep their footers aligned. .coming-soon dims a card, blocks pointer events, and appends "· coming soon" to its label. Add it to the card element, not a wrapper.

<a class="index-card" href="/x">
  <span class="card-label">Live card</span>
  <h3>Shipped</h3>
  <div class="card-meta"><span class="badge badge-complete">Live</span></div>
</a>

<a class="index-card coming-soon" href="/y"> … </a>

Progress

Progress track .progress-track · .progress-fill #progress

4px track on --border-light, teal fill. Set the value with an inline width (or a CSS custom property) — the transition is on the fill, so updates animate.

62% complete

<div class="progress-track">
  <div class="progress-fill" style="width:62%"></div>
</div>
<p class="meta">62% complete</p>

Question list

Question list .question-list #lists

Numbered list for open questions, scope boundaries, or decisions — the numbered marker is absolutely positioned so inline markup inside an item stays inline. Never build this as a grid with a number column.

  1. Does the marketing site consume generated tokens yet? Migration is in progress; the Astro repo carries its own copy.
  2. Which palette is canonical for client-facing work? Atelier is the default; hl is the signature.
  3. Is the Swift package versioned alongside the CSS output?
<ol class="question-list">
  <li><strong>Question one?</strong> Supporting detail.</li>
  <li>Question two?</li>
</ol>

Code

Code block pre.code · code.inline #code

Block code sits on --bg-alt with a 4px teal left rail and scrolls horizontally rather than wrapping. Wrap a block in .snippet to get the copy button.

npm run build   # tokens → dist/{css,swift,tokens.json}
npm run check   # parity gate

Inline: --accent is a token, not a colour.

<div class="snippet">
  <pre class="code">npm run build</pre>
</div>

Source disclosure

Source files disclosure .portal-source-files #sources

A native <details> collapsed to a single micro-label plus a chevron that rotates 180° when open. Use it to tuck provenance — source notes, backing files, data lineage — under a panel without spending vertical space on it by default.

Sources · 3 files ▼
  • tokens/semantic.json — surfaces, text, accent, status
  • tokens/themes/hl.light.json — palette override
  • src/base.css — portable components
<details class="portal-source-files">
  <summary class="portal-source-files-summary">
    <span class="micro-label">Sources · 3 files</span>
    <span class="portal-source-files-chevron">▼</span>
  </summary>
  <ul class="question-list"> … </ul>
</details>

Page layout

Page shell .page · .page-header · .site-footer #layout

.page caps content at 72rem (use .page-wide for 82rem showcase layout). .page-header is a stacked block — kicker, h1, tagline, meta row — closed by a 2px ink rule. It is not a two-column grid, so it does not clip on narrow viewports.

<main class="page">
  <header class="page-header">
    <p class="micro-label">Section kicker</p>
    <h1>Page title</h1>
    <p class="tagline">One sentence that says what this page is for.</p>
    <div class="meta-row">
      <span><strong>Source</strong> somewhere</span>
    </div>
  </header>
</main>