Article card
Use as an <article> when the card holds more than one link.
Reference
Every portable building block from base.css, with the live render,
the markup, and the variants. Nothing here uses a hard-coded colour — the components bind to
tokens, so they re-theme with the palette and mode you select above.
On this page
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>
.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>
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.
<p class="micro-label">Module label</p> <p class="micro-label" style="color:var(--accent);">Accent label</p>
.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.
<span class="meta">Meta text · supporting line</span> <span class="mono">14:32 · 1,204 rows · v1.0.0</span>
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>
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>
Five tones, each bound to a --status-*-bg / --status-*
token pair. Badges carry state, not emphasis — never recolour one to make it
louder.
<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>
| Class | Tokens | Use for |
|---|---|---|
.badge-draft | --status-draft | Not started, inactive |
.badge-review | --status-review | Awaiting review, in QA |
.badge-complete | --status-complete | Done, live, shipped |
.badge-in-progress | --status-in-progress | Actively being worked |
.badge-launched | --status-launched | Public / announced |
Audience segmentation, not status. Square, unrounded, 10px display type. The
.beachhead variant flags a priority segment.
<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>
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).
<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>
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.
| Surface | Stack | Status |
|---|---|---|
| Portal | Static HTML/JS | Live |
| Marketing site | Astro | Review |
| iOS app | SwiftUI | In 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>
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.
Use as an <a> when the whole card is one destination.
Use as an <article> when the card holds more than one link.
<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-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>
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.
<div class="progress-track"> <div class="progress-fill" style="width:62%"></div> </div> <p class="meta">62% complete</p>
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.
<ol class="question-list"> <li><strong>Question one?</strong> Supporting detail.</li> <li>Question two?</li> </ol>
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 gateInline: --accent is a token, not a colour.
<div class="snippet"> <pre class="code">npm run build</pre> </div>
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.
tokens/semantic.json — surfaces, text, accent, statustokens/themes/hl.light.json — palette overridesrc/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 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.
Section kicker
Page title
One sentence that says what this page is for.
<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>