stratum

A minimal CSS framework designed to embed into Go projects. This page is the spec — every primitive lives below. If you can't build a new page out of these blocks, add a primitive. Don't introduce a page-specific class.

Layout

The app shell: top bar with the brand and profile menu; a content frame underneath; an optional aside on the right of long-form pages.

Top bar — body > header with .brand and a profile menu (here without a signed-in user).

your-app.example (profile menu when signed in)

Page content goes here.

Full-window demos — open in a tab and resize across the 720px breakpoint to see the rails collapse:

Typography

Six heading levels. Plain by default — opt into a Wikipedia / MDN baseline rule with .h-rule, or apply globally inside an <article> for rendered prose.

Heading 1

Heading 2

Heading 3

Heading 4

Heading 5
Heading 6

With .h-rule on each heading.

Heading 1

Heading 2

A regular paragraph carries the body color. The quick brown fox jumps over the lazy dog.

A .muted paragraph uses the secondary foreground color.

A .fine-print paragraph: small, muted, with underlined inline links — for disclaimers below a CTA. Read our Terms and Privacy Policy.

An .empty paragraph: italic, muted — placeholder when a section has no content.

Inline code with backticks; a regular link in flowing text.

Multi-line fenced code block — copy.js injects a copy button on hover.

func main() {
    fmt.Println("hello, world")
}

Single-line code block — no copy button (single-line snippets read as inline).

go run ./cmd/foo

Images

An image inside an <article> gets a thin border and rounded corners, so a screenshot whose background matches the page doesn't blend into it.

Sample UI screenshot

Buttons

One base class, stackable variants. Use <a class="button"> for navigation, <button> for actions. Modifiers: .button-primary, .button-secondary, .button-ghost, .button-danger, .button-sm.

Compact size — .button-sm.

With an icon — <svg class="icon"> alongside the label.

Disabled state.

Forms

One .row per labelled control. Wrap a stack of rows in <form class="stack"> to space them; cap with a button.

Stacked form — text, password, email, select, submit.

Input with prefix. .input-group wraps an .input-prefix and a regular <input>. Use when a portion of the value is fixed by the system (a URL prefix, a protocol, a currency) and only the tail is user-editable.

wikilayer.org/1-maxim/

Input with trailing action. Same .input-group wrapper, but the right-hand member is a <button> tagged .input-action instead of a static prefix. Use for forms where typing and submitting belong to one visual unit — search boxes, signup flows, slug-edit rows. The button keeps the .button baseline plus a left divider that fuses it with the input.

Invalid state. Set aria-invalid="true" on the control; the red border survives once focus leaves and the .form-error message below the field carries the wording.

wikilayer.org/1-maxim/

Только латинские строчные буквы, цифры, точки и дефисы.

Минимум 2 символа.

Read-only fact list — .row-inline-list.

Email:
alex@example.com
ID:
42
Account created:
4 January 2026

Tabs

Pure-CSS, radio-driven. Convention: radio ids tab-1, tab-2, …; matching panels tab-panel-1, etc. Up to four out of the box.

Panel one. Click a tab to switch.

Panel two.

Panel three.

Alerts

Inline message blocks. Same shape, color role per variant. Use .alert-success as a one-shot success banner after a redirect — same primitive, no separate "flash" component.

Wrong username or password.

Settings saved.

Callouts

GitHub-flavoured callouts rendered from > [!TYPE] blockquotes in markdown. Five variants share the shape and differ by color role.

Note

Background context the reader can skip.

Tip

A recommendation that helps the reader.

Important

Something the reader should not miss.

Warning

Something that often goes wrong.

Caution

Destructive consequence if ignored.

Map

Embedded location iframe rendered from > [!MAP] blockquotes in markdown. The body's first line is lat, lng; remaining lines join into an optional caption.

Royal Observatory, Greenwich

Avatars

Round chip with user initials (or a Gravatar via img.avatar). Two sizes — default 30px header chip, .avatar-lg 160px for profile pages.

JM AB XY ?

Coloured initials (.avatar-cN, N = userID % 12). Same hue index across themes — only the chroma/lightness shift between light and dark.

A B C D E F G H I J K L

Large size for the profile page.

JM AB XY

.avatar-frame wraps an avatar with a corner overflow menu — upload, replace, clear. Toggle the data-busy attribute on the wrapper to swap the trigger icon (default .toggle-default) for a spinner (.toggle-busy, pair with .spin).

JM
JM

Tags

Small coloured label. .tag sets the shape, .tag-cN (N = 0..7) picks a palette hue. The hue is the ink for text and border; the fill is a themed tint, so the same tag reads in light and dark.

for-agent checklist gotcha deprecated canonical draft example rule

As a superscript trailing a heading (<sup class="tag-row">), one example per level. The pill scales with the heading it trails.

Heading 1for-agent

Heading 2checklist

Heading 3gotchadraft

Heading 4canonical

Heading 5example
Heading 6rule

Row list with per-row actions

A .row-list where the trailing element on each row is either a static .row-list-item-meta badge (read-only, e.g. owner row) or a <details> menu whose <summary> shows the current value plus a chevron. Clicking the role text itself opens the menu — bigger hit target than an overflow icon, and it makes "this is changeable" visually obvious. <details name="..."> makes opening one row auto-close the others. The avatar + name are wrapped in .row-list-item-link — an <a> that points at the subject's profile (inherits text colour, underlines on hover, doesn't fight the trailing menu for clicks).

Entry

One row in a vertical list of titled excerpts — search hit, blog index, inbox item. Title is a heading (h3 or h4) carrying a link, body is a short paragraph, .entry-meta is the muted timestamp / kind / author line below. Pair with <ol class="stack"> or <ul class="stack"> for the surrounding list (the .stack on a list strips bullets and the user-agent left padding).

  1. Markdown support

    Markdown is a widely-supported plain-text format that reads cleanly in any editor.

  2. Agent rules / No # headings inside markdown bodies

    diff. The tree is the structure, not the markdown text.

Cards

Bordered container for grouped content.

Card title

Card body. Quick brown fox jumps over the lazy dog.

Open

Copy block

Inline <code> with a one-click Copy button. Class is .url-pill for historical reasons — the body is generic, anything you need the user to paste somewhere works (URLs, tokens, prompts).

https://your-app.example/mcp

Rail section

Collapsible sidebar block: <details class="rail-section"> wrapping a <summary class="rail-section-summary"> with an .eyebrow heading and chevron. Works in either page rail (left nav or right aside). Pair with data-rail-section="NAME" + rail.js to remember per-section open/closed state in a cookie. The server reads the cookie and stamps the open attribute at render time so there's no flash.

Icons

From static/icons.txt. Run make icons-sync to fetch from Lucide / Simple Icons. Re-paste the resulting <symbol>s into the inline sprite at the top of this file when the manifest changes.

lock
globe
info
lightbulb
triangle-alert
octagon-alert
circle-alert
copy
history
settings
archive
eye
sun
moon
monitor
github