Version: 1.0.0 Last Updated: February 2026 Last Reviewed: February 21, 2026 Purpose: Document the BEM architecture, file structure, import system, and styling rules
This project uses a strict Semantic BEM (Block Element Modifier) CSS architecture. Tailwind utility classes are forbidden. All styles are defined in dedicated CSS files within /styles/.
/styles/
globals.css # Root: design tokens, reset, base styles, utility helpers
animations.css # Centralised keyframe animations (26 keyframes)
blocks/ # One CSS file per component or page
header.css
footer.css
blog-page.css
portfolio-card.css
...
components/
typeform-embed.css # Third-party component overrides
/styles/globals.css is the root stylesheet loaded by the application. It:
- Imports
animations.cssat the top:@import './animations.css'; - Defines the CSS reset (replacing Tailwind Preflight)
- Defines all
:rootdesign tokens (WordPress preset architecture) - Defines
.darkmode overrides - Defines base element styles (
body, headings,p) - Defines global utility helpers (
.text-center,.sr-only,.container-wide, etc.) - Defines global animation utilities (
.animate-neon-pulse-*,.bg-aurora-mesh) - Defines accessibility rules (
:focus-visible,prefers-reduced-motion)
Each component/page imports its own block CSS file directly in the TSX:
// In /components/pages/blog/BlogPage.tsx
import "@/styles/blocks/blog-page.css";The @/ alias resolves to the project root. This means every block CSS file is independently loaded by the component that uses it.
- One CSS file per component/page - e.g.,
blog-page.cssforBlogPage.tsx - No circular imports - block files do NOT import
globals.css(it's already loaded) - No
@apply- Tailwind's@applydirective is forbidden - No
@importin block files - all tokens are available globally fromglobals.css
.block # Standalone component
.block__element # Child element of the block
.block--modifier # Variant of the block
.block__element--modifier # Variant of an element
/* Block */
.blog-card { }
/* Elements */
.blog-card__image { }
.blog-card__title { }
.blog-card__footer { }
/* Modifiers */
.blog-card--featured { }
.blog-card__title--large { }- Block names: lowercase, hyphen-separated:
.portfolio-card,.hero-layout - Elements: double underscore:
.portfolio-card__image - Modifiers: double hyphen:
.portfolio-card--featured - No nesting beyond 2 levels:
.block__element--modifieris the max depth - No standalone modifiers: always pair with the block:
.blog-card.blog-card--featured
All dark mode styles use the .dark class selector on the <html> element:
/* Light mode (default) */
.blog-card {
background-color: var(--wp--preset--color--base);
color: var(--wp--preset--color--contrast);
}
/* Dark mode override */
.dark .blog-card {
background-color: var(--wp--preset--color--atomic-black);
color: var(--wp--preset--color--neutral-200);
}- Every visible component must have a
.darkvariant - Use
.dark .blockselector (space between.darkand the block) - Text colours use
-textsuffix tokens:--neon-purple-text(accessible in light),--neon-purple(bright in dark) - The
.darkclass is toggled on<html>byThemeToggle.tsx
| Property | Token Pattern | Example |
|---|---|---|
| Colour | --wp--preset--color--* |
var(--wp--preset--color--neon-pink) |
| Font family | --wp--preset--font-family--* |
var(--wp--preset--font-family--brand-heading) |
| Font size | --wp--preset--font-size--* |
var(--wp--preset--font-size--300) |
| Spacing | --wp--preset--spacing--* |
var(--wp--preset--spacing--fluid-md) |
| Border radius | --wp--preset--border-radius--* |
var(--wp--preset--border-radius--lg) |
| Shadow | --wp--preset--shadow--* |
var(--wp--preset--shadow--card) |
| Gradient | --wp--preset--gradient--* |
var(--wp--preset--gradient--cyberpunk) |
| Z-index | --wp--preset--z-index--* |
var(--wp--preset--z-index--header) |
/* BAD - hardcoded values */
.card { background: #f3f4f6; border-radius: 1rem; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
/* GOOD - token references */
.card { background: var(--wp--preset--color--neutral-100); border-radius: var(--wp--preset--border-radius--lg); box-shadow: var(--wp--preset--shadow--card); }Rule: Use gap on flex/grid containers instead of margin on children.
/* BAD - margin on children */
.card-list .card { margin-bottom: 1rem; }
/* GOOD - gap on parent */
.card-list { display: flex; flex-direction: column; gap: var(--wp--preset--spacing--block-gap); }The standard inter-element gap is --wp--preset--spacing--block-gap which maps to fluid-md (clamp 1rem-2rem).
margin: 0 autofor centering containers- Legacy utility helpers (
.mb-fluid-md) for compatibility (prefer gap)
Mobile-first with standard breakpoints:
/* Base: mobile */
.grid { grid-template-columns: 1fr; }
/* Tablet portrait */
@media (min-width: 768px) {
.grid { grid-template-columns: repeat(2, 1fr); }
}
/* Desktop */
@media (min-width: 1024px) {
.grid { grid-template-columns: repeat(3, 1fr); }
}
/* Wide desktop */
@media (min-width: 1440px) {
.grid { grid-template-columns: repeat(4, 1fr); }
}| Pattern | Reason |
|---|---|
Tailwind utilities (flex, p-4, text-center) |
Strict BEM architecture |
Inline styles (style={{ ... }}) |
Except dynamic data-driven values |
!important |
Architecture issue - fix specificity instead |
@apply |
Tailwind directive - not available |
| Raw hex colours | Use CSS custom properties |
| Hardcoded px spacing | Use fluid spacing tokens |
margin for element spacing |
Use gap on parent |
Nested selectors beyond .dark .block__element |
Keep specificity flat |
Defined in :root of globals.css:
| Token | Value | Use Case |
|---|---|---|
--sm |
Subtle 1px | Inputs, small elements |
--md |
Medium 4px | Cards at rest |
--lg |
Elevated 10px | Dropdowns, popovers |
--xl |
High 20px | Modals, overlays |
--2xl |
Maximum 25px | Hero elements |
--card |
Card default | Portfolio/blog cards |
--card-hover |
Card hover | Lift on hover |
--neon-pink |
Pink glow | Neon accents |
--neon-purple |
Purple glow | Primary neon |
--neon-blue |
Blue glow | CTA neon |
Last Updated: February 2026