Components¶
"A cup, a table, a mat. Each one the same quality. Each one in its place."
Performance Principle¶
The site must feel nimble and responsive. No drag, no lag, no waiting. Every interaction responds instantly. Pages load fast. Scrolling is butter-smooth.
Performance Rules¶
- No
backdrop-filter— it's GPU-heavy and causes scroll jank on mobile - No heavy animations — transitions are 150-200ms max,
ease-outonly - No layout shifts — every element has explicit dimensions or aspect ratios
- Minimal JS — if CSS can do it, don't use JS. Tabs, accordions, toggles — CSS-first
- Self-hosted fonts only — no Google Fonts CDN round-trip. Inter loaded locally
- Images lazy-loaded — native
loading="lazy"on every<img>below the fold - No scroll-hijacking — native scroll everywhere. No smooth-scroll libraries
- Transitions on
transformandopacityonly — these are GPU-composited. Never animatewidth,height,padding,margin
Transition Standard¶
/* The ONE transition used everywhere */
--transition: 150ms ease-out;
/* Applied to interactive elements */
.interactive {
transition:
color var(--transition),
background-color var(--transition),
border-color var(--transition),
transform var(--transition),
opacity var(--transition),
box-shadow var(--transition);
}
Cards¶
The universal container for grouped content. All cards look identical regardless of what's inside.
.zen-card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: var(--space-6);
transition: border-color 150ms ease-out, transform 150ms ease-out;
}
.zen-card:hover {
border-color: var(--border-emphasis);
transform: translateY(-1px);
}
Card Variations¶
| Variation | Difference | Usage |
|---|---|---|
| Default | As above | Blog cards, tool cards, testimonials |
| Compact | padding: var(--space-4) |
Grid items, small cards |
| Interactive (link) | Adds cursor: pointer, hover lift |
Clickable navigation cards |
| Flat | No hover effect, no transform | Static information display |
What Cards DON'T Have¶
- ❌ Coloured borders (per-tool accent colours)
- ❌ Gradient backgrounds
- ❌ Watermark numbers
- ❌ Decorative icons at large sizes
- ❌ Shadow on rest state (shadow only on hover, and subtle)
Buttons¶
Three types. That's it.
Primary — The One CTA¶
.btn-primary {
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-6);
background: var(--accent);
color: #FFFFFF;
font-size: var(--text-sm);
font-weight: 600;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
transition: background-color 150ms ease-out, transform 150ms ease-out;
}
.btn-primary:hover {
background: var(--accent-hover);
transform: translateY(-1px);
}
Secondary — Supporting Actions¶
.btn-secondary {
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-6);
background: transparent;
color: var(--text-secondary);
font-size: var(--text-sm);
font-weight: 600;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
cursor: pointer;
transition: border-color 150ms ease-out, color 150ms ease-out;
}
.btn-secondary:hover {
border-color: var(--border-emphasis);
color: var(--text-primary);
}
Ghost — Tertiary / Inline¶
.btn-ghost {
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
background: transparent;
color: var(--accent);
font-size: var(--text-sm);
font-weight: 600;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
transition: background-color 150ms ease-out;
}
.btn-ghost:hover {
background: var(--accent-subtle);
}
Button Rules¶
- One primary button per view. If everything is important, nothing is.
- No pill-shaped buttons (
border-radius: 9999px) except theme toggle. Buttons use--radius-sm. - No gradient buttons. Solid colour only.
- No icon-only buttons without aria-label.
Navigation (Top Bar)¶
Clean, minimal, functional. No decoration.
.nav {
position: sticky;
top: 0;
z-index: 100;
background: var(--bg-surface);
border-bottom: 1px solid var(--border);
height: 56px;
display: flex;
align-items: center;
padding: 0 var(--space-6);
}
Nav Items¶
- Text only — no icons in nav links
- Active state:
color: var(--text-primary)+font-weight: 600 - Inactive:
color: var(--text-tertiary) - Hover:
color: var(--text-primary) - No underlines, no backgrounds, no borders on nav items
Theme Toggle¶
- Small icon button (sun/moon) at far right of nav
- Uses
--radius-full(pill shape — the ONE exception) - Toggles
data-themeon<html>
Sidebar (Reading Pages)¶
Stripe Docs pattern. Left sidebar for navigation, content on the right.
.sidebar {
position: sticky;
top: 56px; /* below nav */
width: var(--sidebar-width);
height: calc(100vh - 56px);
overflow-y: auto;
background: var(--bg-elevated);
border-right: 1px solid var(--border);
padding: var(--space-6) 0;
}
.sidebar-link {
display: block;
padding: var(--space-2) var(--space-6);
color: var(--text-tertiary);
font-size: var(--text-sm);
text-decoration: none;
transition: color 150ms ease-out, background-color 150ms ease-out;
}
.sidebar-link:hover {
color: var(--text-primary);
background: var(--accent-subtle);
}
.sidebar-link.active {
color: var(--accent);
font-weight: 600;
background: var(--accent-subtle);
border-right: 2px solid var(--accent);
}
Mobile Sidebar¶
On screens < 768px, sidebar collapses into a top hamburger menu or bottom sheet.
Mobile Strip Helpers (set 10 May 2026)¶
Added during Phase 14 zenification audit. Codifies Lessons #32, #33, #35 as global helpers in
style.cssso individual tools don't redo the work.
Opt-in classes — apply via class="…" attribute, no per-tool CSS needed:
| Class | Effect |
|---|---|
zt-mobile-hide |
display: none !important on viewports ≤768px |
zt-mobile-only |
display: none on desktop, restored to cascaded value on mobile |
zt-mobile-stack |
Forces a grid/flex container into single-column block layout on mobile |
zt-mobile-sidebar-below |
Sets order: 1 so this element renders after siblings on mobile |
zt-mobile-content-first |
Sets order: 0 — pair with zt-mobile-sidebar-below |
zt-mobile-details (on <details>) |
Forces collapsed on mobile even if [open] on desktop |
Global mobile rules already applied (no opt-in needed):
.zt-page— horizontal padding tightens to 12px on mobile (Lesson #32).zt-card— padding tightens to 10px on mobile.zt-card-tight— padding tightens to 8px on mobile
When to use which:
- Sidebar with stats/links the user doesn't need on phone →
zt-mobile-hide - "Pro tips" / decorative hero cards →
zt-mobile-hide - Mobile-only banner ("Tap to open menu") →
zt-mobile-only - 2-column comparison grid → add
zt-mobile-stackto the grid container - Results/recommendation panel that should appear below the interactive controls on phone →
zt-mobile-sidebar-belowon the panel +zt-mobile-content-firston the controls (both inside a flex container) - Long FAQ-style
<details>that's open by default → addzt-mobile-details
Example:
<div class="zt-page">
<h1>My Tool</h1>
<!-- Desktop: 2-col grid; mobile: stacked -->
<div class="grid two-col zt-mobile-stack">
<main class="zt-mobile-content-first">
<!-- interactive controls -->
</main>
<aside class="zt-mobile-sidebar-below">
<!-- results panel — sits below on mobile -->
</aside>
</div>
<!-- Decorative hero stats — hide on phone -->
<section class="hero-stats zt-mobile-hide">
…
</section>
<!-- Long FAQ — collapsed on mobile, open on desktop -->
<details class="zt-mobile-details" open>
<summary>FAQ</summary>
…
</details>
</div>
Tabs¶
For tool interfaces and content toggles. CSS-first approach.
.zen-tabs {
display: flex;
gap: var(--space-1);
border-bottom: 1px solid var(--border);
margin-bottom: var(--space-6);
}
.zen-tab {
padding: var(--space-3) var(--space-4);
color: var(--text-tertiary);
font-size: var(--text-sm);
font-weight: 500;
border: none;
background: none;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: color 150ms ease-out, border-color 150ms ease-out;
}
.zen-tab:hover {
color: var(--text-primary);
}
.zen-tab.active {
color: var(--accent);
border-bottom-color: var(--accent);
font-weight: 600;
}
Inputs¶
.zen-input {
width: 100%;
padding: var(--space-3) var(--space-4);
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-primary);
font-size: var(--text-body);
font-family: var(--font-sans);
outline: none;
transition: border-color 150ms ease-out;
}
.zen-input::placeholder {
color: var(--text-muted);
}
.zen-input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-subtle);
}
Tags / Badges¶
One style. No colour variations.
.zen-tag {
display: inline-flex;
align-items: center;
padding: var(--space-1) var(--space-2);
font-size: var(--text-caption);
font-weight: 500;
color: var(--text-tertiary);
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
}
No coloured tags. Tags differentiate by text content, not by colour. "Copilot", "AI Agents", "Guide" — all look identical. The text tells you what it is.
Brand Mark — Product Showcase (Not Decoration)¶
The hero section reserves space for a product showcase — a looping GIF or WebM showing actual tools, flashcards, practice exams, and study guides in action.
- Text on left, product showcase on right (like Linear/Notion)
- Hidden until the redesign is complete and screenshots are captured
- Container:
flex: 0 0 420px,border-radius: var(--radius-lg) - Must use
loading="lazy"for the media - Stacks vertically on mobile (< 1024px)
Rule: No decorative illustrations. The product IS the visual. Show, don't decorate.
Presentation Patterns (Not Everything Is a Card)¶
Cards are the default container, but overusing cards creates "card fatigue." Use these alternatives:
1. Quote Blocks (Testimonials, Reviews)¶
Left border accent line, no box. The content speaks for itself.
.quote-block {
padding: 0 0 0 var(--space-4);
border-left: 2px solid var(--border);
background: none;
}
.quote-block:hover {
border-left-color: var(--accent);
}
2. Inline Feature Rows (Feature Lists)¶
Icon + title + description in a horizontal flow. No borders, no background.
🛡️ Security Toolkit Compliance checks, phishing sims, threat analysis
⚙️ Admin Ops PowerShell builders, SLA calculators, incident comms
3. Stat Counters (Hero Stats, Social Proof)¶
Big number + label, no container. The number IS the visual element.
4. Tag Clouds (Vendor Bar, Category Labels)¶
Small inline tags — grouped, no card wrapper. Text differentiates, not colour.
5. Divider Sections¶
Content separated by subtle horizontal lines (1px solid var(--border)), not boxes.
When to use a card vs alternative: - Card: when the element is clickable and self-contained (links to another page) - Alternative: when the element is informational or supporting (doesn't need its own container)
Component Checklist¶
Before adding any new component, verify:
- [ ] Uses only design tokens (no hardcoded colours, sizes, or spacing)
- [ ] Works in both light and dark mode
- [ ] Has hover/focus states using
--transition - [ ] Only animates
transform,opacity,color,background-color,border-color,box-shadow - [ ] No
backdrop-filter - [ ] Responsive down to 375px
- [ ] Has appropriate
aria-attributes - [ ] Looks identical regardless of which page it's on
Reading Experience (Blog — Implemented ✅)¶
"Reading should feel like a warm book, not a cold screen."
The blog reading experience has three layers of reader comfort, all independent:
Layer 1: Theme (Light / Dark)¶
Site-wide toggle. Sets data-theme on <html>. Affects all pages.
Layer 2: Warm Tint (Cool / Warm)¶
Blog-only toggle. Adds .zt-reading--warm class on .zt-reading container. Adapts to whichever theme is active:
| Theme | Tint OFF (default) | Tint ON (warm) |
|---|---|---|
| Light | --bg-page: #FAFAFA |
--bg-page: #FFF8F0 (sepia) |
| Dark | --bg-page: #0A0A0A |
--bg-page: #1A1710 (warm dark) |
Scoping decision: Uses .zt-reading--warm class on the blog container (NOT data-reading="warm" on <html>) to prevent warm tint bleeding into tool pages when navigating.
The two toggles are independent — warm tint survives theme switches. Both saved to localStorage.
Layer 3: Font Size (14 / 16 / 18px)¶
Blog-only control. Uses --reading-font-size custom property on .zt-reading. Three steps:
| Size | Token | Chars/line at 720px |
|---|---|---|
| Small | 14px | ~82 |
| Default | 16px | ~72 |
| Large | 18px | ~63 |
Variable cascade lesson: The --reading-font-size variable MUST be defined on .zt-reading (the parent grid), NOT on .zt-reading-body. If defined on .zt-reading-body, the JS setProperty() on .zt-reading won't cascade because the child's own definition takes precedence.
Reading Layout Rules¶
- Prose text (p, ul, ol, blockquote, dl, details, figure, headings) max 720px — comfortable 65-80 chars/line
- Wide elements (tables, code, diagrams, images) use full column width — they need room
- H2 headings have left accent border —
border-left: 3px solid var(--accent); padding-left: var(--space-3) - Tables sit in JS-wrapped
.zt-table-wrapcontainers —border: 1px solid var(--border); border-radius: var(--radius-md); overflow-x: auto - Focus mode hides sidebars — content centres at 800px (wider than prose 720px to give code/tables room)
- Focus button hidden on mobile — sidebars already gone at ≤1024px
Reading Toolbar¶
Located in .zt-reading-actions below the meta line:
- Font group uses
.zt-toolbar-group(connected buttons with shared border) - Warm + Focus use
.zt-toolbar-btn(standalone buttons,.is-activestate) - All three save to separate localStorage keys:
zt-reading-font-size,zt-reading-warm,zt-reading-focus
Flash Prevention¶
Inline <script> in single.html runs BEFORE CSS paint to pre-apply saved prefs:
var r = document.querySelector('.zt-reading');
if (localStorage.getItem('zt-reading-warm') === '1') r.classList.add('zt-reading--warm');
if (localStorage.getItem('zt-reading-focus') === '1') r.classList.add('zt-reading--focused');
var fs = localStorage.getItem('zt-reading-font-size');
if (fs) r.style.setProperty('--reading-font-size', fs + 'px');
Files¶
| File | Purpose |
|---|---|
static/css/zt-reading.css |
All reading room CSS (layout, typography, toolbar, warm, focus, legacy overrides) |
static/js/zt-reading-enhance.js |
Table wrapping, font/warm/focus controls |
static/js/zt-toc.js |
TOC highlighting + progress bar (separate — has early returns) |
layouts/blog/single.html |
Template with toolbar buttons + flash prevention script |
Legacy Component Overrides (in zt-reading.css)¶
Blog posts use legacy HTML classes from pre-Zen era. These are overridden in the reading context:
| Legacy Class | Issue | Zen Override |
|---|---|---|
.living-doc-banner |
backdrop-filter, rgba bg, white text | var(--bg-elevated), var(--border), warning left-accent |
.prompt-cards blockquote |
dark gradient bg, light purple text | var(--bg-elevated), accent left-border |
.cowork-scenario blockquote |
dark gradient bg, light blue text | var(--bg-elevated), accent left-border |
.instruction-cards blockquote |
dark gradient bg, teal text | var(--bg-elevated), success left-border |
.prompt-example |
backdrop-filter, purple gradient | var(--bg-elevated), accent left-border |
.cowork-persona |
Per-color pills (6 variants) | Unified accent color |
.tool-counter |
rgba(255,255,255,0.06) bg, neon border |
var(--bg-elevated), var(--border) |
.tool-counter-num |
var(--hero-accent) → white in light mode |
var(--text-secondary) |
.kofi-btn |
Red rgba bg, white text | var(--bg-elevated), var(--text-tertiary) |
.blog-faq strong |
var(--neon-magenta) |
var(--accent) |
Diagram Sizing¶
Mermaid diagrams capped at max-height: 800px with SVG scaling: