Skip to content

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

  1. No backdrop-filter — it's GPU-heavy and causes scroll jank on mobile
  2. No heavy animations — transitions are 150-200ms max, ease-out only
  3. No layout shifts — every element has explicit dimensions or aspect ratios
  4. Minimal JS — if CSS can do it, don't use JS. Tabs, accordions, toggles — CSS-first
  5. Self-hosted fonts only — no Google Fonts CDN round-trip. Inter loaded locally
  6. Images lazy-loaded — native loading="lazy" on every <img> below the fold
  7. No scroll-hijacking — native scroll everywhere. No smooth-scroll libraries
  8. Transitions on transform and opacity only — these are GPU-composited. Never animate width, 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

  1. One primary button per view. If everything is important, nothing is.
  2. No pill-shaped buttons (border-radius: 9999px) except theme toggle. Buttons use --radius-sm.
  3. No gradient buttons. Solid colour only.
  4. No icon-only buttons without aria-label.

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);
}
  • 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-theme on <html>

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.css so 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-stack to the grid container
  • Results/recommendation panel that should appear below the interactive controls on phone → zt-mobile-sidebar-below on the panel + zt-mobile-content-first on the controls (both inside a flex container)
  • Long FAQ-style <details> that's open by default → add zt-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

  1. Prose text (p, ul, ol, blockquote, dl, details, figure, headings) max 720px — comfortable 65-80 chars/line
  2. Wide elements (tables, code, diagrams, images) use full column width — they need room
  3. H2 headings have left accent borderborder-left: 3px solid var(--accent); padding-left: var(--space-3)
  4. Tables sit in JS-wrapped .zt-table-wrap containersborder: 1px solid var(--border); border-radius: var(--radius-md); overflow-x: auto
  5. Focus mode hides sidebars — content centres at 800px (wider than prose 720px to give code/tables room)
  6. Focus button hidden on mobile — sidebars already gone at ≤1024px

Reading Toolbar

Located in .zt-reading-actions below the meta line:

[🔗 Share] [A− | A | A+] [☀️ Warm] [👁 Focus]
  • Font group uses .zt-toolbar-group (connected buttons with shared border)
  • Warm + Focus use .zt-toolbar-btn (standalone buttons, .is-active state)
  • 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:

.zt-reading-body .mermaid { max-height: 800px; overflow: hidden; }
.zt-reading-body .mermaid svg { max-height: 740px; width: auto; height: auto; }
Users can ⛶ fullscreen for detail (button from Z17).