specview already solved its visual layer — a complete newspaper design system with a 926-line playground that renders every component, every state, every token. oll.am has six products to consolidate and a stack of HTML artifacts that should look like one publication. The cheapest way to get there is not to design again; it is to lift specview's system verbatim. This page is the bridge: the tokens and component CSS pulled out as a portable library, with one line each on where it slots into oll.am.
The shared palette is eight values. Everything is built from ink-on-cream with a single slate-blue accent and one editorial red. Swatches render live — toggle dark mode in the masthead to see the paired values.
The raw custom properties, verbatim from web-ng/src/styles.css (light) — the dark overrides live under [data-theme="dark"] in the same file and at the top of this page:
:root {
--bg: #FFFEF9;
--ink: #121212;
--ink-light: #5A5A5A;
--ink-muted: #999999;
--border: #DFDFDF;
--border-dark: #121212;
--accent: #567B95;
--red: #C41E3A;
--code-bg: #F5F5F5;
/* status (four-state gen bar) */
--status-idle: #1a6b30;
--status-active: #7a5800;
--status-success: #1a6b30;
--status-failure: #C41E3A;
--status-running: #22A66A;
--status-success-bg: #2E7D32;
/* surfaces */
--surface: #FAFAF7;
--surface-raised: #F5F5F0;
/* section identity (header titles only — never on cards) */
--color-active: #22A66A;
--color-specced: #567B95;
--color-ready: #7B6BAE;
--color-braindump: #A08060;
}
--red shifts to #E05A72 (and the playground's app-only block lists --status-failure: #8B0000 dark). Those are the values in the source; the swatches above show light-mode hexes with the live boxes re-tinting on toggle.Each typeface has exactly one role and a hard rule. Playfair never appears at weight 400 in a headline; Source Sans never carries multi-sentence reading text. JetBrains Mono is this artifact's code face (specview uses 'SF Mono', Consolas).
| Family | Var | Role & rule | Weights |
|---|---|---|---|
| Playfair Display | --serif | Headlines, titles, pull-quotes, large numbers. Always 700 for headlines — never 400. | 400 / 700 / 700i |
| Source Serif 4 | --body | Body copy, paragraphs, reading text. Multi-sentence content only. | 400 / 600 |
| Source Sans 3 | --sans | Labels, metadata, UI chrome, uppercase tags. Never for reading text. | 400 / 600 |
| px | Family | Where it appears (verbatim) |
|---|---|---|
| 64 / 56 | Playfair 700 | .masthead-title (app 64, demo 56), letter-spacing:-0.02em |
| 36 | Playfair 700 | .expanded-title, line-height:1.15 |
| 26 | Playfair 700 | .markdown-content h1 |
| 22 | Playfair 700 | .modal-title |
| 20 | Playfair 700 | .markdown-content h2 |
| 18 | Playfair 700 | .context-card__label, .file-item.featured title |
| 15 | Playfair 600 / Serif | .file-item-title, .col-title (700); markdown body |
| 13–14 | Source Serif 4 | .file-item-teaser, modal textarea body |
| 11 | Source Sans 600 | nav links, .op-chip, meta — uppercase, letter-spacing:0.06–0.1em |
| 9–10 | Source Sans 600 | .overline (9), .badge (9), .col-badge (10) |
--serif: 'Playfair Display', Georgia, serif;
--body: 'Source Serif 4', Georgia, serif;
--sans: 'Source Sans 3', system-ui, sans-serif;
.masthead-title { font: 700 64px var(--serif);
letter-spacing: -0.02em; }
.file-item-title { font: 600 15px var(--serif);
line-height: 1.3; }
.overline { font: 600 9px var(--sans);
letter-spacing: 0.12em;
text-transform: uppercase; }
Honesty note: specview has no named spacing or radius custom properties. The rhythm is a convention applied by hand across the CSS. These are the recurring values, quoted from the source — adopt them as the de-facto scale.
| Scale | Recurring values (px) | Where |
|---|---|---|
| Spacing / rhythm | 4 · 6 · 8 · 12 · 16 · 20 · 24 · 32 · 40 · 48 · 52 | gaps, padding, margins — e.g. .page{padding:20px 40px}, .file-column{padding:0 24px}, .markdown-content{column-gap:52px} |
| Border radius | 2px standard · 3px occasional · 20px pill · 50% dots | .col-badge/.op-chip/.badge = 2; .style-chip = 20 (the one pill); status dots = 50% |
| Container | max-width 1400 (app) · 920–1080 (artifacts) | .page{max-width:1400px;margin:0 auto} |
2px for every chip, badge and input; 20px only for the style-preset pill; 50% only for status dots. There is no large-radius "card" rounding anywhere, because there are no cards.The entire visual hierarchy is borders — eight distinct, semantic uses. Weight encodes importance: 3px ink = major structural break, 2px ink = section-label underline, 1px border = content divider. This is the heart of the language.
| Weight | Semantic role | Rendered |
|---|---|---|
| 1px solid --border | Masthead bottom, column dividers, between sections | |
| 2px solid --ink | Column/widget title underline, editor toolbar bottom | |
| 3px solid --ink | Major structural break (section-bar top, expanded panel top, footer top) | |
| 3px solid --border-dark | <hr> thick rule | |
| border-left 3px --border-dark | Code-block accent (markdown / pre) | code |
| border-left 2px --ink | Sidebar active file | active |
| border 1px dashed --accent | Generate button (the "AI action" affordance) | |
| 3px double --border-dark | Masthead / footer rule (the artifact convention) |
Shadow is almost banned. It appears in exactly three places, each deliberate — anything that floats above the page earns a shadow; nothing on the page does. Z-index is a short, flat ladder.
/* 1. Modal — floats over a scrim */ .modal { box-shadow: 0 8px 48px rgba(0,0,0,0.18); } /* 2. Floating editor toolbar — docked above */ .editor-toolbar--floating { box-shadow: 0 -4px 20px rgba(0,0,0,0.08); } /* 3. Context-card hover — the one on-page lift */ .context-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
9 editor toolbar / gen-status-bar (sticky) 10 .section-nav (sticky) 100 .editor-toolbar--floating (docked) 1000 .modal-backdrop 3000 .modal-backdrop (playground variant)
Honesty note: these are inline values, not a named token scale. The rule is the takeaway — sticky chrome ≈ 9–10, overlays ≈ 1000+.
One standard transition (0.15s) for all hovers; a handful of named keyframes for state. Motion communicates state change, never decoration. Three render live below.
| Name | Timing | Job |
|---|---|---|
| rise | 0.2s ease · one-shot | Panel / row entering — opacity + 6px translateY |
| gen-shimmer | 1.6s linear · infinite | Generation in-progress track |
| poll-pulse | 0.7s ease-out · infinite alt | Live polling indicator (expanding ring) |
| dot-pulse | 1s ease-in-out · infinite | Per-file running dot |
| thinking-pulse | 1.2s ease-in-out · infinite | AI "thinking" three-dot row |
| count-pulse | 200ms ease-out · one-shot | A count badge ticking up |
| status-success-flash | 2s ease-out · one-shot | Status bar flashing on success |
| (transition) | 0.15s | The universal hover/focus duration |
@keyframes rise {
from { opacity:0; transform:translateY(6px); }
to { opacity:1; transform:translateY(0); }
}
@keyframes gen-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
@keyframes poll-pulse {
0% { box-shadow:0 0 0 0 rgba(34,166,106,.55); }
100% { box-shadow:0 0 0 5px rgba(34,166,106,0); }
}
Each component: what it is and when to use it, the real thing rendered (it inherits the captured CSS at the top of this file), and the source to copy. Nothing here is redrawn.
.masthead-top { display:grid;
grid-template-columns: 150px 1fr 150px;
align-items: flex-end; gap: 20px; } /* center=editorial; app uses align-items:center */
.masthead-title { font: 700 64px var(--serif); letter-spacing: -0.02em; }
.masthead-tagline { font: italic 13px var(--body); color: var(--ink-light); }
editor-apply/modal-generate for the dominant action, upgrade-btn for checkout (filled accent so money reads as primary), outline variants for secondary..editor-apply-btn { background: var(--ink); color: var(--bg);
border: 1px solid var(--ink); font: 700 12px var(--sans);
text-transform: uppercase; letter-spacing: .06em; padding: 6px 22px; }
.upgrade-btn { background: var(--accent); color: #fff; /* money = filled accent */
border: 1px solid var(--accent); padding: 5px 12px; }
.new-project-btn { background: transparent; color: var(--accent);
border: 1px solid var(--accent); } /* hover → fills accent */
.logout-btn { background: transparent; color: var(--ink);
border: 1px solid var(--border-dark); } /* hover → fills ink */
Honesty note: the landing's .btn-primary / .btn-secondary live in landing/style.css (not read here); per the playground states table their hovers are opacity:0.85 (primary) and border-color:--ink (secondary).
.op-chip { font: 11px var(--sans); text-transform: uppercase;
letter-spacing: .06em; border: 1px solid var(--border);
color: var(--ink-muted); padding: 4px 14px; border-radius: 2px; }
.op-chip.active { background: var(--ink); color: var(--bg); border-color: var(--ink); }
.op-chip--accent { border-color: var(--accent); color: var(--accent); }
.style-chip { border-radius: 20px; } /* the one pill in the system */
gap:0; borders do the spacing), each with a 2px-ink underlined header and stacked items. When: any list of grouped documents/products. .featured bumps the first item bigger..file-grid { display: grid; grid-template-columns: repeat(3,1fr);
gap: 0; } /* borders do the work */
.file-column { padding: 0 24px; border-right: 1px solid var(--border); }
.file-header { border-bottom: 2px solid var(--ink);
margin-bottom: 16px; padding-bottom: 8px; }
.file-item { border-bottom: 1px solid var(--border); transition: background .15s; }
.file-item.featured .file-item-title { font-size: 18px; font-weight: 700; }
.context-card { border: 1px solid var(--border); padding: 20px;
transition: border-color .15s, box-shadow .15s; }
.context-card:hover { border-color: var(--ink);
box-shadow: 0 2px 8px rgba(0,0,0,.06); } /* the one on-page shadow */
.context-card__label { font: 700 18px var(--serif); }
.diff-block-remove { background: rgba(196,30,58,.05);
border-left: 3px solid var(--red);
text-decoration: line-through; opacity: .65; }
.diff-block-add { background: rgba(46,125,50,.06);
border-left: 3px solid var(--status-success-bg); }
.badge { font: 600 9px var(--sans); text-transform: uppercase;
letter-spacing: .05em; padding: 2px 6px; border-radius: 2px;
background: var(--border); color: var(--ink-light); }
.badge--new { background: var(--red); color: #fff; }
.badge--complete { background: var(--status-success-bg); color: #fff; }
.overline { font: 600 9px var(--sans); letter-spacing: .12em;
text-transform: uppercase; color: var(--ink-muted); }
.gen-status-bar--idle { background: var(--status-idle); }
.gen-status-bar--active { background: var(--status-active); }
.gen-status-bar--success { background: var(--status-success); }
.gen-status-bar--failure { background: var(--status-failure); }
.gen-status-track { height: 2px; background-size: 200% 100%;
background: linear-gradient(90deg, transparent, var(--accent), #fff, var(--accent), transparent);
animation: gen-shimmer 1.6s linear infinite; }
.modal { background: var(--bg); border: 1px solid var(--border-dark);
max-width: 680px; box-shadow: 0 8px 48px rgba(0,0,0,.18); }
.modal-header { border-bottom: 2px solid var(--border-dark); }
.modal-input:focus { border-color: var(--ink); } /* focus = ink border */
The chain adapter is the only AI call boundary. Feature modules import from chain/adapter.py — never providers directly.
Three functions cover all uses: generate(), stream(), rewrite().
The chain layer is a boundary, not an abstraction.
.markdown-content { column-count: 2; column-gap: 52px;
column-rule: 1px solid var(--border); line-height: 1.85; }
.markdown-content h1 { font: 700 26px var(--serif); column-span: all;
border-bottom: 1px solid var(--border); }
@media (max-width: 900px) { .markdown-content { column-count: 1; } }
.search-bar input { border: none; border-bottom: 1px solid var(--border);
background: none; } .search-bar input:focus { border-bottom-color: var(--ink); }
.update-banner { background: var(--ink); color: var(--bg); text-align: center; }
.update-banner button { border: 1px solid var(--bg); background: none; color: var(--bg); }
The system isn't arbitrary taste — it's a small set of principles, each tracing to one of Rams' ten rules of good design, and each provable in the real specview CSS above.
A masthead, columns, section headers, pull-quotes, a body in a serif reading face. The interface borrows a 400-year-old form everyone already knows how to read, so structure needs no explanation — your eye walks it like a front page.
Why it maps: Rams' "good design makes a product understandable" — the form clarifies structure and is, ideally, self-explanatory. The newspaper is a pre-loaded mental model; the UI inherits its legibility for free.
Hierarchy is carried entirely by border weight (3px / 2px / 1px) and whitespace. Almost no fills, almost no shadows (three, all for things that float), almost no radius (2px). gap:0 with borders doing the spacing.
Why it maps: "less, but better — back to purity, back to simplicity." Every removed box is one less thing competing for attention. The restraint is the design.
The palette is ink-on-cream plus a single slate-blue --accent and one editorial --red. Colour is rationed: accent marks the AI/primary action and money CTA; red marks destructive/error; saturated greens/ambers appear only inside the status bar.
Why it maps: "products are tools, not decoration — neutral and restrained, to leave room for the user." When colour is scarce, the little there is becomes meaningful and directs the eye.
The four-state generation bar, the shimmer-while-working track, the polling pulse, the diff add/remove blocks — the UI shows exactly what the system is doing, including failure (red bar + Retry). Nothing is faked or hidden behind a spinner.
Why it maps: "good design is honest — it does not make a product more innovative, powerful or valuable than it really is." Real progress, real latency badges, real errors. The interface doesn't oversell.
Every value is a custom property defined once in :root and paired under [data-theme="dark"] — components never hardcode a colour. The playground exists at all because the system is documented down to each chip's radius and each animation's easing.
Why it maps: "nothing must be arbitrary or left to chance — care and accuracy in the design process show respect for the user." A single source of truth is thoroughness made mechanical.
Portable plain CSS, drops into any static <style> (no Angular, no SCSS). Adopt in this order; each enriches the existing oll.am HTMLs without touching a word of copy.
| # | Style | Where it slots into oll.am |
|---|---|---|
| 1 | The :root token block (light + dark) | The foundation — paste both blocks once; every artifact already uses these var names, so this aligns colour/type/dark-mode instantly. |
| 2 | The 3 font families | Swap the Google Fonts link to Playfair + Source Serif 4 + Source Sans 3; gives every page the editorial voice. |
| 3 | Border hierarchy (3/2/1px ink/border) | Section breaks → 3px ink; heading underlines → 2px ink; dividers → 1px border. Replaces ad-hoc rules across the build-log pages. |
| 4 | .masthead + .masthead-title | Standard nameplate at the top of every oll.am artifact — unifies the publication. |
| 5 | .file-grid / .file-column / .file-item | The artifact index / timeline page — list products or docs as a newspaper front page. |
| 6 | .context-card (+ hover) | The product chooser / "pick a service" grids; the only sanctioned card + shadow. |
| 7 | op-chip / style-chip / badge / overline | Filters, tags, kickers and state markers throughout — the small-label vocabulary. |
| 8 | Button set (apply / upgrade / outline) | CTAs — use the filled-accent upgrade-btn for every Stripe checkout so money reads as primary. |
| 9 | .diff-block-add / -remove | Before/after sections in decision write-ups and migration docs. |
| 10 | .gen-status-bar (4 states) + keyframes | Any live-process page (deploy status, generation) — honest progress + failure. |
| 11 | .markdown-content (2-col) | Long-form generated prose blocks inside artifacts. |
| 12 | The 7 @keyframes + 0.15s transition | All motion — paste once, reference by name. |
<style> head contains items 1, 2 and 4–12 pasted verbatim, and every rendered example above is the real component. To reuse, lift that block into oll.am/site as a shared tokens.css + components.css and link it from each artifact.specview's design system is a finished, frozen-worthy asset, exactly like oll-core is on the backend. The move is the same: prove it where it lives, then lift it verbatim. These tokens and component styles get pulled into the oll.am HTMLs as a shared library — the publication gets specview's polish without anyone redrawing a button. The cheapest good design is the good design you already shipped.