CSS Architecture

Every rune's visual presentation is defined in CSS. The identity transform produces semantic HTML with BEM classes and data attributes — your CSS targets these selectors to create the theme's look and feel.

BEM naming

All selectors follow the BEM convention with a configurable prefix (default: rf):

PatternExampleMeaning
.rf-{block}.rf-hintBlock — the rune's root element
.rf-{block}--{modifier}.rf-hint--warningModifier — a variant of the block
.rf-{block}__{element}.rf-hint__iconElement — a child within the block

The engine produces these classes automatically from the RuneConfig. You never write class names manually in content — the config drives the output, and your CSS matches it.

Block classes

Every rune gets its block class from the block field in the config:

/* Block selector — styles the rune's root element */
.rf-hint {
  border-left: 3px solid var(--hint-color);
  padding: 0.875rem 1.25rem;
  margin: 1.5rem 0;
  background: var(--hint-bg);
}

Modifier classes

Modifier classes come from three sources:

Dynamic modifiers — values read from meta tags:

/* hintType modifier with value "warning" → .rf-hint--warning */
.rf-hint--warning {
  --hint-color: var(--rf-color-warning);
  --hint-bg: var(--rf-color-warning-bg);
}

Context modifiers — applied when nested inside a parent rune:

/* Hint inside a Hero gets compact styling */
.rf-hint--in-hero {
  margin: 1rem 0 0;
  padding: 0.625rem 1rem;
  font-size: 0.875rem;
}

Static modifiers — always applied to certain rune variants:

/* FeaturedTier always has --featured */
.rf-tier--featured {
  border: 2px solid var(--rf-color-primary);
}

Element classes

Element classes are derived from data-name attributes on children. The engine reads data-name and adds the corresponding __element class:

/* Structural elements injected by the config */
.rf-hint__header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.rf-hint__icon {
  display: flex;
  color: var(--hint-color);
}
.rf-hint__title {
  font-weight: 600;
  text-transform: capitalize;
}

Data attribute styling

For variant-specific styling of child elements, use [data-*] attribute selectors instead of BEM modifier classes. The engine sets data attributes from resolved modifier values automatically.

This is the preferred pattern because the data attribute is set on the root element, and you can target any descendant based on it:

/* API method colors — data-method is set on the root .rf-api element */
[data-method="GET"] .rf-api__method {
  color: var(--rf-color-success);
  background: var(--rf-color-success-bg);
}
[data-method="POST"] .rf-api__method {
  color: var(--rf-color-info);
  background: var(--rf-color-info-bg);
}
[data-method="DELETE"] .rf-api__method {
  color: var(--rf-color-danger);
  background: var(--rf-color-danger-bg);
}
/* Recipe difficulty badge colors */
[data-difficulty="easy"] .rf-recipe__badge {
  color: var(--rf-color-success);
  background: var(--rf-color-success-bg);
}
[data-difficulty="medium"] .rf-recipe__badge {
  color: var(--rf-color-warning);
  background: var(--rf-color-warning-bg);
}
[data-difficulty="hard"] .rf-recipe__badge {
  color: var(--rf-color-danger);
  background: var(--rf-color-danger-bg);
}
note

Data attributes follow kebab-case naming. A modifier named hintType in the config becomes data-hint-type in the HTML. The engine handles the conversion automatically.

Dimension styling

Beyond per-rune data attributes, the identity transform emits universal dimension attributes that enable generic cross-rune styling. Instead of writing separate CSS for every rune's badges, sections, and containers, you write dimension rules once and they apply everywhere.

Ten dimensions cover metadata badges, structural anatomy, density, media treatment, interactivity, and more:

/* Metadata: style every status badge across every rune */
[data-meta-type="status"] {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  padding: 0.5em 1.0em;
  border: 1px solid var(--rf-color-border);
  border-radius: 999px;
}

/* Sentiment: color badges by meaning */
[data-meta-sentiment="positive"] { --meta-color: var(--rf-color-success); }
[data-meta-sentiment="negative"] { --meta-color: var(--rf-color-danger); }

/* Sections: consistent anatomy across all runes */
[data-section="header"] { display: flex; flex-wrap: wrap; gap: 0.5rem; }
[data-section="title"]  { font-size: 1.5rem; font-weight: 700; }
[data-section="footer"] { border-top: 1px solid var(--rf-color-border); }

/* Density: responsive detail levels */
[data-density="compact"] [data-section="description"] {
  -webkit-line-clamp: 2;
  overflow: hidden;
}
[data-density="minimal"] [data-section="body"] { display: none; }

Lumina's dimension CSS lives in styles/dimensions/ (8 files, ~54 rules total). These handle the generic baseline for all runes — per-rune CSS files only need to cover rune-specific styling that dimensions don't handle.

See Universal Theming Dimensions for the full system, all ten dimensions, and complete CSS patterns.

Context-aware styling

Context modifiers enable runes to adapt their appearance when nested inside other runes. The modifier class is added to the root element, so all CSS can be scoped under it:

/* CTA standalone — centered with generous padding */
.rf-cta {
  text-align: center;
  padding: 3.5rem 2rem 3rem;
}

/* CTA inside a Hero — less top padding */
.rf-cta--in-hero {
  padding-top: 2rem;
  padding-bottom: 0;
}

/* CTA inside Pricing — left-aligned, compact */
.rf-cta--in-pricing {
  text-align: left;
  padding: 1.5rem 0 0;
}

Common context modifier patterns in the base config:

RuneParentModifierPurpose
HintHeroin-heroCompact callout
HintFeaturein-featureNarrow callout
CTAHeroin-heroReduced padding
CTAPricingin-pricingLeft-aligned
FeatureHeroin-heroHero-specific layout
FeatureGridin-gridGrid-aware sizing

Design tokens

All visual values reference CSS custom properties (design tokens) rather than hard-coded values. This makes themes customizable and ensures dark mode works correctly.

Token categories

Tokens are defined in a CSS file (e.g., tokens/base.css) on the :root selector:

Typography:

:root {
  --rf-font-sans: 'Inter', system-ui, sans-serif;
  --rf-font-mono: 'JetBrains Mono', 'Fira Code', monospace;
}

Color palette:

:root {
  /* Primary scale */
  --rf-color-primary-50: #f0f9ff;
  --rf-color-primary-500: #0ea5e9;
  --rf-color-primary-900: #0c4a6e;

  /* Core palette */
  --rf-color-text: #1a1a2e;
  --rf-color-muted: #64748b;
  --rf-color-border: #e2e8f0;
  --rf-color-bg: #ffffff;
  --rf-color-primary: var(--rf-color-primary-500);
  --rf-color-primary-hover: var(--rf-color-primary-600);
}

Surfaces:

:root {
  --rf-color-surface: #f8fafc;
  --rf-color-surface-hover: #f1f5f9;
  --rf-color-surface-active: #e2e8f0;
  --rf-color-surface-raised: #ffffff;
}

Semantic intent:

:root {
  --rf-color-info: #3b82f6;
  --rf-color-info-bg: #eff6ff;
  --rf-color-info-border: #bfdbfe;
  --rf-color-warning: #f59e0b;
  --rf-color-warning-bg: #fffbeb;
  --rf-color-danger: #ef4444;
  --rf-color-danger-bg: #fef2f2;
  --rf-color-success: #10b981;
  --rf-color-success-bg: #ecfdf5;
}

Radii and shadows:

:root {
  --rf-radius-sm: 6px;
  --rf-radius-md: 10px;
  --rf-radius-lg: 16px;
  --rf-radius-full: 9999px;

  --rf-shadow-sm: 0 1px 3px rgba(0,0,0,0.06);
  --rf-shadow-md: 0 4px 12px rgba(0,0,0,0.07);
  --rf-shadow-lg: 0 8px 24px rgba(0,0,0,0.08);
}

Token naming convention

All tokens follow the pattern --{prefix}-{category}-{name}:

  • --rf-color-primary — color category, primary name
  • --rf-font-sans — font category, sans name
  • --rf-radius-md — radius category, medium name
  • --rf-shadow-lg — shadow category, large name

Semantic tokens reference the palette tokens, so changing the primary color scale automatically updates all UI that uses --rf-color-primary.

Using tokens in rune CSS

Always reference tokens instead of hard-coded values:

/* Good — uses tokens */
.rf-recipe {
  border: 1px solid var(--rf-color-border);
  border-radius: var(--rf-radius-lg);
  padding: 2rem;
}

/* Avoid — hard-coded values */
.rf-recipe {
  border: 1px solid #e2e8f0;
  border-radius: 16px;
  padding: 2rem;
}

Rune-scoped custom properties

For runes with multiple variant colors, define scoped custom properties and override them per variant:

.rf-hint {
  --hint-color: var(--rf-color-info);
  --hint-bg: var(--rf-color-info-bg);
  border-left: 3px solid var(--hint-color);
  background: var(--hint-bg);
}
.rf-hint--note {
  --hint-color: var(--rf-color-info);
  --hint-bg: var(--rf-color-info-bg);
}
.rf-hint--warning {
  --hint-color: var(--rf-color-warning);
  --hint-bg: var(--rf-color-warning-bg);
}
.rf-hint--caution {
  --hint-color: var(--rf-color-danger);
  --hint-bg: var(--rf-color-danger-bg);
}

This pattern keeps the base styles clean — you define the layout once using the scoped properties, then each variant only overrides the property values.

Dark mode

Dark mode is implemented via CSS custom properties, overridden in a dark mode context. Lumina supports both explicit attribute toggle and system preference:

/* Dark mode overrides — tokens/dark.css */
[data-theme="dark"] {
  --rf-color-text: #e2e8f0;
  --rf-color-muted: #94a3b8;
  --rf-color-border: #334155;
  --rf-color-bg: #0f172a;
  --rf-color-surface: #1e293b;
  /* ... all tokens overridden */
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --rf-color-text: #e2e8f0;
    /* same overrides */
  }
}

Because rune CSS references tokens, dark mode works automatically — you don't need per-rune dark mode styles. Only the token values change.

The theme's manifest.json declares dark mode support:

{
  "darkMode": {
    "attribute": "data-theme",
    "values": { "dark": "dark", "light": "light" },
    "systemPreference": true
  }
}

CSS custom property injection

The styles config field injects values as inline style attributes. This is used when CSS needs values that are truly dynamic per-instance:

/* Storyboard uses --sb-columns set by the engine */
.rf-storyboard {
  display: grid;
  grid-template-columns: repeat(var(--sb-columns, 3), 1fr);
}

The config styles: { columns: '--sb-columns' } tells the engine to set style="--sb-columns: 4" when columns=4 is specified. The CSS reads the custom property with a fallback default.

File organization

Dimension CSS

Generic cross-rune rules live in a dimensions/ directory, separate from per-rune files:

styles/dimensions/
├── metadata.css       # [data-meta-type], [data-meta-sentiment], [data-meta-rank]
├── density.css        # [data-density]
├── sections.css       # [data-section]
├── state.css          # [data-state]
├── media.css          # [data-media]
├── surfaces.css       # Surface type groupings (card, inline, banner, inset)
├── checklist.css      # [data-checked]
└── sequence.css       # [data-sequence]

These files are imported before per-rune CSS, providing the generic baseline that all runes share.

One file per rune block

Each BEM block gets its own CSS file for rune-specific styling:

styles/runes/
├── hint.css           # .rf-hint, .rf-hint--*, .rf-hint__*
├── recipe.css         # .rf-recipe, .rf-recipe__meta, etc.
├── api.css            # .rf-api, .rf-api__header, etc.
├── pricing.css        # .rf-pricing + .rf-tier (child rune)
└── ...

Child runes in parent files

Runes that are always children of another rune are styled in the parent's CSS file. For example, .rf-tier selectors live in pricing.css, not in a separate tier.css. If you're looking for a child rune's CSS (AccordionItem, Tab, Tier, etc.), check the parent rune's file instead.

Use refrakt inspect <child-rune> --audit to discover which selectors a child rune produces and whether they have CSS coverage.

All parent-child groupings:

Parent CSS fileContains selectors for
accordion.css.rf-accordion, .rf-accordion-item
annotate.css.rf-annotate, .rf-annotate-note
bento.css.rf-bento, .rf-bento-cell
breadcrumb.css.rf-breadcrumb, .rf-breadcrumb-item
cast.css.rf-cast, .rf-cast-member
changelog.css.rf-changelog, .rf-changelog-release
comparison.css.rf-comparison, .rf-comparison-column, .rf-comparison-row
conversation.css.rf-conversation, .rf-conversation-message
feature.css.rf-feature, .rf-feature-definition
form.css.rf-form, .rf-form-field
map.css.rf-map, .rf-map-pin
nav.css.rf-nav, .rf-nav-group, .rf-nav-item
pricing.css.rf-pricing, .rf-tier, .rf-tier--featured
recipe.css.rf-recipe, .rf-recipe-ingredient
reveal.css.rf-reveal, .rf-reveal-step
steps.css.rf-steps, .rf-step
storyboard.css.rf-storyboard, .rf-storyboard-panel
symbol.css.rf-symbol, .rf-symbol-group, .rf-symbol-member
tabs.css.rf-tabs, .rf-tab
timeline.css.rf-timeline, .rf-timeline-entry

Barrel import

A single index.css imports all token and rune CSS files:

/* index.css */
@import './tokens/base.css';
@import './tokens/dark.css';
@import './styles/global.css';
/* Dimension CSS — generic cross-rune rules */
@import './styles/dimensions/metadata.css';
@import './styles/dimensions/density.css';
@import './styles/dimensions/sections.css';
@import './styles/dimensions/state.css';
@import './styles/dimensions/media.css';
@import './styles/dimensions/surfaces.css';
@import './styles/dimensions/checklist.css';
@import './styles/dimensions/sequence.css';
/* Per-rune CSS — rune-specific overrides */
@import './styles/runes/hint.css';
@import './styles/runes/recipe.css';
/* ... all rune CSS files */

This is the theme's main CSS entry point, consumed by site builds. Dimension CSS comes before per-rune CSS so that rune-specific rules can override the generic baseline when needed.