Plan
ID:SPEC-025Status:draft

Universal Theming Dimensions

Cross-rune semantic data attributes — surface, density, section anatomy, interactive state, media slots, checklist, and sequential items — so themes can style every rune generically with ~54 CSS rules instead of per-rune overrides. Builds on the metadata system (SPEC-024) to complete the ten-dimension universal theming model.


Problem

Theme development is expensive. A theme supporting 30+ runes needs hundreds of per-rune CSS rules. A recipe card, a character card, a work item card, and an event card all need the same container treatment (background, border, radius, padding) but the theme author writes it four times because each has a different BEM class. An accordion panel and a details block both need the same open/closed transition but are styled independently.

Every new rune — from official packages or the community — requires new theme CSS. A @refrakt-community/wine package releases a tasting rune. Every theme needs updating. If the theme author hasn’t written rules for .rune-wine-tasting, it renders unstyled.

The metadata system (SPEC-024) solved this for badges — three dimensions, ~18 rules, every badge styled. This specification extends the same principle to the rest of the rune: containers, anatomy, density, interactivity, and media.


Design Principles

Semantic data attributes. The identity transform emits data-* attributes that describe what something is, not how it should look. The theme maps semantics to visuals. The rune config declares semantics.

Generic rules, specific overrides. A theme’s generic rules (targeting data-* attributes) handle the baseline for every rune. Per-rune BEM rules override when a specific rune needs special treatment. The generic layer eliminates the need for most per-rune CSS.

Additive and incremental. The data attributes don’t change existing BEM classes. Themes that don’t use the generic system continue working. Runes can be migrated one at a time.

Community-proof. A community package rune that declares its dimensions gets themed automatically by any theme that implements the generic rules. No per-rune CSS contribution needed from theme authors.


Dimensions

Overview

DimensionAttributeValuesControlsDeclared by
Densitydata-densityfull, compact, minimalSpacing and detail levelRune config + context
Sectiondata-sectionheader, title, description, body, footerStructural anatomyRune config
Statedata-stateopen, closed, active, inactive, selected, disabledInteractive statesBehaviour script
Mediadata-mediaportrait, cover, thumbnail, heroImage treatmentRune config
Checklistdata-checkedchecked, unchecked, active, skippedCheckbox list itemsContent detection
Sequencedata-sequencenumbered, connected, plainOrdered item indicatorsRune config
Surface(class-based)card, inline, banner, insetContainer treatmentTheme only

Combined with the metadata system’s three dimensions (data-meta-type, data-meta-sentiment, data-meta-rank), the full set is ten dimensions covering every visual aspect of rune rendering.

Note on surface: Surface is deliberately excluded from the rune config. Which runes render as cards, banners, or inline elements is a visual design decision that belongs to the theme, not the rune. A minimal theme might render recipes inline. A dashboard theme might render everything as cards. The rune doesn’t know or care — it declares its structure (sections, media, metadata), and the theme decides the container treatment. See the Surface section for how themes assign surfaces.


Surface

Controls the container treatment — how the rune visually separates from its surroundings. Surface is owned entirely by the theme. The rune config does not declare a surface. The theme decides which runes are cards, which are inline, which are banners.

Values

ValueTreatmentTypical use
cardElevated container with background, border, and radiusRecipe, character, work item, testimonial, event
inlineNo visual boundary — flows with surrounding proseHint, details, sidenote, conversation message
bannerFull-width strip with backgroundHero, CTA, feature section
insetRecessed container with muted background, no borderCode block, blockquote, exercise prompt

Why Theme-Owned

A rune declaring surface: 'card' would impose a visual opinion that themes must fight against. A minimal theme that wants recipes to flow inline would have to override the card treatment. A magazine theme that wants characters as full-width banners would have to undo the card default.

The rune knows what it is — a recipe, a character, a work item. The theme knows how to present it. Surface is presentation.

Compare with the other dimensions: a recipe has a header section and a body section (structural fact). A recipe’s difficulty is a categorical metadata field (semantic fact). Whether the recipe renders as a card or inline is a design choice that varies between themes.

Theme Implementation

The theme defines surface styles once, then assigns runes to surfaces:

/* === Surface definitions (written once) === */

/* Card: elevated container */
.surface-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md, 0.5rem);
  padding: var(--rune-padding, var(--spacing-md));
}

/* Inline: no boundary */
.surface-inline {
  padding: var(--rune-padding, var(--spacing-sm)) 0;
}

/* Banner: full-width strip */
.surface-banner {
  background: var(--color-surface);
  padding: var(--rune-padding, var(--spacing-xl)) 0;
}

/* Inset: recessed area */
.surface-inset {
  background: var(--color-bg-muted, color-mix(in oklch, var(--color-bg) 95%, black));
  border-radius: var(--radius-md, 0.5rem);
  padding: var(--rune-padding, var(--spacing-md));
}

/* === Surface assignments (theme-specific) === */

/* Cards */
.rune-recipe,
.rune-character,
.rune-work,
.rune-bug,
.rune-decision,
.rune-testimonial,
.rune-event,
.rune-track {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md, 0.5rem);
  padding: var(--rune-padding, var(--spacing-md));
}

/* Inline */
.rune-hint,
.rune-details,
.rune-sidenote,
.rune-conversation {
  padding: var(--rune-padding, var(--spacing-sm)) 0;
}

/* Banners */
.rune-hero,
.rune-cta,
.rune-feature {
  background: var(--color-surface);
  padding: var(--rune-padding, var(--spacing-xl)) 0;
}

/* Inset */
.rune-exercise,
.rune-codegroup,
.rune-sandbox {
  background: var(--color-bg-muted);
  border-radius: var(--radius-md, 0.5rem);
  padding: var(--rune-padding, var(--spacing-md));
}

A different theme makes different assignments. A minimal theme might render everything inline. A dashboard theme might render everything as cards. The rune output is the same — only the theme CSS changes.

Surface and Tint Interaction

When a rune has a tint, the tint overrides the surface’s default background through the CSS cascade. The theme doesn’t need special interaction rules:

/* The tint bridge overrides --color-surface within the tinted container */
[data-tint] {
  --color-surface: var(--tint-background, var(--color-surface));
}

A tinted card picks up the tinted background. A tinted banner picks up the tinted background. The surface assignment and the tint compose naturally.

Surface and Density Interaction

Surface and density interact through the --rune-padding custom property. The density rules set --rune-padding, and the surface styles consume it:

/* Density sets the padding scale */
[data-density="full"] { --rune-padding: var(--spacing-lg); }
[data-density="compact"] { --rune-padding: var(--spacing-sm); }
[data-density="minimal"] { --rune-padding: var(--spacing-xs); }

/* Surface uses --rune-padding */
.rune-recipe { padding: var(--rune-padding, var(--spacing-md)); }

A compact card has tighter padding than a full card. The density and surface systems compose through the shared custom property without either knowing about the other.

Community Runes

A community rune that the theme hasn’t explicitly assigned a surface gets no container treatment — it renders as unstyled content. This is intentional. The theme author can add the community rune to their surface assignment list when they want to support it:

/* Adding support for a community rune */
.rune-recipe,
.rune-character,
.rune-wine-tasting {  /* ← add community rune here */
  /* card surface styles */
}

Alternatively, the theme can provide a catch-all for unassigned runes:

/* Fallback: any rune not explicitly assigned gets card treatment */
[class^="rune-"]:not(.rune-hint):not(.rune-hero):not(.rune-details) {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--rune-padding, var(--spacing-md));
}

This ensures community runes look reasonable even before the theme explicitly supports them. The catch-all excludes runes the theme has already assigned to non-card surfaces.


Density

Controls how much detail a rune shows and how tightly it’s spaced. A rune on a dedicated page shows full detail. The same rune in a grid card or list shows a condensed version.

Values

ValueTreatmentContext
fullAll sections visible, generous spacingDedicated page, expanded view
compactDescriptions truncated, secondary metadata hidden, tight spacingGrid cell, card grid, sidebar
minimalTitle and primary metadata only, very tightList view, backlog row, search results

Automatic Density

The identity transform sets density based on context. The rune config declares a default, and the rendering context overrides it:

ContextDensity
Rune on a dedicated pagefull
Rune inside a grid cellcompact
Rune inside a backlog or list viewminimal
Rune inside a split section’s media zonecompact
Author override via attributeWhatever the author specified

The author can override the automatic density:

{% recipe density="compact" %}
...
{% /recipe %}

Rune Config

Recipe: {
  block: 'recipe',
  surface: 'card',
  defaultDensity: 'full',
  // ...
}

Identity Transform Output

<!-- Full density on a page -->
<div class="rune-recipe" data-density="full">
  <div data-section="header">
    <span data-meta-type="temporal" data-meta-rank="primary">30 min</span>
    <span data-meta-type="category" data-meta-rank="primary" data-meta-sentiment="positive">Easy</span>
    <span data-meta-type="quantity" data-meta-rank="primary">4 servings</span>
  </div>
  <h2 data-section="title">Classic Sourdough</h2>
  <p data-section="description">A rustic loaf with an open crumb and crispy crust...</p>
  <div data-section="body"><!-- full recipe content --></div>
</div>

<!-- Compact density in a grid -->
<div class="rune-recipe" data-density="compact">
  <!-- same HTML, styled differently by theme -->
</div>

Theme CSS

/* === Full: all content visible, generous spacing === */
[data-density="full"] {
  --rune-padding: var(--spacing-lg);
}

/* === Compact: truncated, tight === */
[data-density="compact"] {
  --rune-padding: var(--spacing-sm);
}

[data-density="compact"] [data-section="description"] {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

[data-density="compact"] [data-meta-rank="secondary"] {
  display: none;
}

/* === Minimal: title and primary metadata only === */
[data-density="minimal"] {
  --rune-padding: var(--spacing-xs);
}

[data-density="minimal"] [data-section="description"] {
  display: none;
}

[data-density="minimal"] [data-section="body"] {
  display: none;
}

[data-density="minimal"] [data-section="footer"] {
  display: none;
}

[data-density="minimal"] [data-meta-rank="secondary"] {
  display: none;
}

At compact density, descriptions are clamped to two lines and secondary metadata is hidden. At minimal density, only the header (primary metadata) and title remain. These rules apply to every rune — a compact recipe card, a compact character card, and a compact work item card all truncate the same way.


Section Anatomy

Controls the structural parts of a rune. Most runes follow the same pattern: header with metadata, title, description, body content, footer with actions. The identity transform emits data-section attributes on each structural element, enabling universal anatomy styling.

Values

ValuePurposeContents
headerMetadata row above the titleBadges, status pills, category chips
titlePrimary headingThe rune’s name/headline
descriptionSecondary text below the titleSummary, blurb, subtitle
bodyMain content areaThe rune’s primary content
footerActions and links below the bodyButtons, links, related items
mediaVisual content areaImages, showcases, sandboxes (in split layouts)

Rune Config

The config maps each ref to a section role:

Recipe: {
  block: 'recipe',
  surface: 'card',
  sections: {
    header: 'header',     // ref name → section role
    title: 'title',
    description: 'description',
    content: 'body',
    tips: 'footer',
  },
  // ...
}

The identity transform reads the sections mapping and emits data-section alongside the BEM class:

<div class="rune-recipe__header" data-section="header">...</div>
<h2 class="rune-recipe__title" data-section="title">...</h2>

Identity Transform Output

Recipe:

<div class="rune-recipe" data-density="full">
  <div class="rune-recipe__header" data-section="header">
    <span data-meta-type="temporal" data-meta-rank="primary">30 min</span>
    <span data-meta-type="category" data-meta-rank="primary">Easy</span>
  </div>
  <h2 class="rune-recipe__title" data-section="title">Classic Sourdough</h2>
  <p class="rune-recipe__description" data-section="description">A rustic loaf...</p>
  <div class="rune-recipe__content" data-section="body">
    <!-- ingredients, steps -->
  </div>
  <div class="rune-recipe__tips" data-section="footer">
    <!-- chef tips -->
  </div>
</div>

Character:

<div class="rune-character" data-density="full">
  <div class="rune-character__badges" data-section="header">
    <span data-meta-type="category" data-meta-rank="primary">Antagonist</span>
    <span data-meta-type="status" data-meta-rank="primary" data-meta-sentiment="positive">Alive</span>
  </div>
  <img class="rune-character__portrait" data-section="media" data-media="portrait" src="...">
  <h2 class="rune-character__name" data-section="title">Veshra</h2>
  <div class="rune-character__content" data-section="body">
    <!-- sections: Appearance, Personality, Backstory -->
  </div>
</div>

Work Item:

<div class="rune-work" data-density="full">
  <div class="rune-work__header" data-section="header">
    <span data-meta-type="id" data-meta-rank="primary">RF-142</span>
    <span data-meta-type="status" data-meta-rank="primary" data-meta-sentiment="neutral">In Progress</span>
    <span data-meta-type="category" data-meta-rank="primary" data-meta-sentiment="caution">High</span>
  </div>
  <h2 class="rune-work__title" data-section="title">Implement tint dark mode</h2>
  <p class="rune-work__description" data-section="description">The tint rune needs dual...</p>
  <div class="rune-work__content" data-section="body">
    <!-- acceptance criteria, edge cases, approach -->
  </div>
  <div class="rune-work__refs" data-section="footer">
    <!-- references, links -->
  </div>
</div>

All three follow the same anatomy. The theme styles them uniformly.

Theme CSS

/* === Header: metadata row === */
[data-section="header"] {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: var(--spacing-sm);
}

/* === Title: primary heading === */
[data-section="title"] {
  font-size: var(--font-size-title, 1.5rem);
  font-weight: 700;
  line-height: 1.2;
  margin: 0;
}

/* Scale title with density */
[data-density="compact"] [data-section="title"] {
  font-size: var(--font-size-title-compact, 1.125rem);
}

[data-density="minimal"] [data-section="title"] {
  font-size: var(--font-size-title-minimal, 1rem);
}

/* === Description: secondary text === */
[data-section="description"] {
  color: var(--color-text-muted);
  font-size: var(--font-size-body, 1rem);
  line-height: 1.5;
  margin: var(--spacing-xs) 0 var(--spacing-md);
}

/* === Body: main content === */
[data-section="body"] {
  line-height: 1.6;
}

/* === Footer: actions and links === */
[data-section="footer"] {
  display: flex;
  flex-wrap: wrap;
  gap: var(--spacing-sm);
  margin-top: var(--spacing-md);
  padding-top: var(--spacing-sm);
  border-top: 1px solid var(--color-border);
}

/* === Media: visual content === */
[data-section="media"] {
  margin: var(--spacing-sm) 0;
}

Six rules for the anatomy. Combined with the density rules, titles scale down, descriptions truncate or hide, and footers disappear — all universally.


Interactive State

Controls the visual state of interactive rune elements — collapsible panels, tabbed sections, selectable items.

Values

ValueMeaningUsed by
openExpanded, visible contentAccordion panel, details content, reveal step
closedCollapsed, hidden contentAccordion panel, details content
activeCurrently selected/visibleActive tab, active accordion panel, current step
inactiveNot currently selectedInactive tab, other accordion panels
selectedUser-selected itemQuiz answer, datatable row
disabledNon-interactiveDisabled form field, locked content

Identity Transform Output

The identity transform sets the initial state. The behaviour script toggles it.

<!-- Accordion -->
<div class="rune-accordion__panel" data-state="open">
  <button class="rune-accordion__trigger">Section One</button>
  <div class="rune-accordion__content">...</div>
</div>
<div class="rune-accordion__panel" data-state="closed">
  <button class="rune-accordion__trigger">Section Two</button>
  <div class="rune-accordion__content">...</div>
</div>

<!-- Tabs -->
<button class="rune-tabs__tab" data-state="active">Tab One</button>
<button class="rune-tabs__tab" data-state="inactive">Tab Two</button>
<div class="rune-tabs__panel" data-state="active">Panel content...</div>
<div class="rune-tabs__panel" data-state="inactive">Panel content...</div>

Theme CSS

/* === Open/Closed: collapsible content === */
[data-state="open"] > [class*="__content"] {
  display: block;
}

[data-state="closed"] > [class*="__content"] {
  display: none;
}

/* Animated transition (theme opt-in) */
[data-state="open"] > [class*="__content"] {
  animation: rune-expand 0.2s ease-out;
}

@keyframes rune-expand {
  from { opacity: 0; transform: translateY(-0.5rem); }
  to { opacity: 1; transform: translateY(0); }
}

/* === Active/Inactive: selection state === */
[data-state="active"] {
  /* Tabs, accordion triggers */
}

[data-state="inactive"] {
  opacity: 0.7;
}

/* Active tab indicator */
button[data-state="active"] {
  border-bottom: 2px solid var(--color-accent);
  color: var(--color-accent);
}

button[data-state="inactive"] {
  border-bottom: 2px solid transparent;
  color: var(--color-text-muted);
}

/* === Selected: user selection === */
[data-state="selected"] {
  background: color-mix(in oklch, var(--color-accent) 10%, transparent);
  outline: 2px solid var(--color-accent);
}

/* === Disabled === */
[data-state="disabled"] {
  opacity: 0.4;
  pointer-events: none;
}

The behaviour script toggles data-state values. The theme animates the transitions. Every collapsible rune gets the same expand animation. Every tabbed rune gets the same active indicator. The behaviour script doesn’t need to know about styling — it just sets state.

Behaviour Script Integration

The existing @refrakt-md/behaviors script already toggles classes for interactive runes. Migrating to data-state attributes is a straightforward refactor:

// Before: class-based
panel.classList.toggle('rune-accordion__panel--open');

// After: state-based
panel.dataset.state = panel.dataset.state === 'open' ? 'closed' : 'open';

The behaviour script sets state. The theme reads state. Clean separation.


Media Slots

Controls the visual treatment of images and media elements within runes.

Values

ValueTreatmentUse cases
portraitCircular crop, 1:1 aspect ratioCharacter portrait, team member headshot, artist photo
coverFull-width, 16:9 aspect ratio, rounded top cornersRecipe photo, album cover, event image
thumbnailSmall fixed-size previewTrack artwork, search result preview, list item icon
heroLarge responsive image, may bleedHero background, feature section image
iconSmall square, no cropLogo, badge, small illustration

Rune Config

Recipe: {
  block: 'recipe',
  surface: 'card',
  mediaSlots: {
    image: 'cover',
  },
  // ...
}

Character: {
  block: 'character',
  surface: 'card',
  mediaSlots: {
    portrait: 'portrait',
  },
  // ...
}

Track: {
  block: 'track',
  mediaSlots: {
    artwork: 'thumbnail',
  },
  // ...
}

Identity Transform Output

<!-- Recipe cover image -->
<img class="rune-recipe__image" data-media="cover" src="/images/sourdough.jpg" alt="...">

<!-- Character portrait -->
<img class="rune-character__portrait" data-media="portrait" src="/images/veshra.jpg" alt="...">

<!-- Track thumbnail -->
<img class="rune-track__artwork" data-media="thumbnail" src="/images/album.jpg" alt="...">

Theme CSS

/* === Portrait: circular crop === */
[data-media="portrait"] {
  border-radius: 50%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  width: var(--media-portrait-size, 5rem);
  height: var(--media-portrait-size, 5rem);
}

/* === Cover: full-width banner image === */
[data-media="cover"] {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: var(--radius-md, 0.5rem) var(--radius-md, 0.5rem) 0 0;
}

/* Cover inside a card: negative margin to reach card edges */
[data-surface="card"] > [data-media="cover"]:first-child {
  margin: calc(-1 * var(--rune-padding, var(--spacing-md)));
  margin-bottom: var(--spacing-md);
  width: calc(100% + 2 * var(--rune-padding, var(--spacing-md)));
  border-radius: var(--radius-md, 0.5rem) var(--radius-md, 0.5rem) 0 0;
}

/* === Thumbnail: small fixed preview === */
[data-media="thumbnail"] {
  width: var(--media-thumbnail-size, 3rem);
  height: var(--media-thumbnail-size, 3rem);
  border-radius: var(--radius-sm, 0.25rem);
  object-fit: cover;
  flex-shrink: 0;
}

/* === Hero: large responsive image === */
[data-media="hero"] {
  width: 100%;
  object-fit: cover;
}

/* === Icon: small square, no crop === */
[data-media="icon"] {
  width: var(--media-icon-size, 2rem);
  height: var(--media-icon-size, 2rem);
  object-fit: contain;
  flex-shrink: 0;
}

Five rules. Every rune’s images are handled. A character portrait and a team member headshot both get the same circular crop. A recipe photo and an album cover both get the same full-width 16:9 treatment.

Media and Density Interaction

Media slots adapt to density:

/* Compact: smaller portraits, smaller thumbnails */
[data-density="compact"] [data-media="portrait"] {
  --media-portrait-size: 3rem;
}

[data-density="compact"] [data-media="cover"] {
  aspect-ratio: 3 / 1;
}

/* Minimal: no media */
[data-density="minimal"] [data-media] {
  display: none;
}

At compact density, portraits shrink and cover images become wider and shorter. At minimal density, all media is hidden — only text metadata remains.


Checklist

Controls the visual treatment of checkbox-style list items — the [x]/[ ] pattern common in acceptance criteria, progress tracking, and status lists. Today this pattern is styled independently in each rune that uses it (plot beats, comparison rows) or not styled at all (work/bug acceptance criteria). A universal checklist treatment eliminates the duplication and ensures every rune with checkbox items gets consistent styling for free.

Values

ValueMarkerMeaningVisual treatment
checked[x]Complete / done / includedFilled indicator (checkmark), muted text
unchecked[ ]Pending / todo / excludedEmpty indicator (hollow circle or empty box)
active[>]In progress / currentPrimary-coloured indicator with emphasis ring
skipped[-]Abandoned / excluded / N/AMuted indicator, strikethrough text

How It Works

The identity transform detects checkbox markers at the start of list item text content. When found, it:

  1. Strips the marker text ([x] , [ ] , [>] , [-] ) from the rendered output
  2. Sets data-checked on the <li> element with the resolved value (checked, unchecked, active, skipped)

This is a content-level pattern, not a rune-config-level dimension. Any list item in any rune's body content that starts with a checkbox marker gets the attribute automatically. Runes don't need to declare anything — the transform handles it generically.

Opt-in via Rune Config

Runes that want checkbox detection on specific structural lists (not just body content) can declare it in their config:

Work: {
  block: 'work',
  checklist: true,  // enable checkbox detection on all lists within this rune
  // ...
}

When checklist is not set, checkbox detection still applies to standard Markdown task list items (which Markdoc may already parse with a checked attribute on the AST node). The checklist: true flag extends detection to all lists, including those inside content model fields.

Identity Transform Output

Work item acceptance criteria:

<div class="rf-work__body" data-section="body">
  <section data-name="acceptance-criteria">
    <h2>Acceptance Criteria</h2>
    <ul>
      <li data-checked="checked">First criterion — done</li>
      <li data-checked="unchecked">Second criterion — pending</li>
      <li data-checked="unchecked">Third criterion — pending</li>
    </ul>
  </section>
</div>

Plot beats (rune-specific styling still applies via BEM):

<li class="rf-beat rf-beat--complete" data-checked="checked">
  <span data-field="label">Completed step</span>
</li>
<li class="rf-beat rf-beat--active" data-checked="active">
  <span data-field="label">Active step</span>
</li>

Plot beats get both the universal data-checked attribute and their rune-specific BEM modifier. The theme can style beats with the BEM classes for the dot/timeline treatment, while the universal data-checked rules provide the baseline text treatment (muted for checked, strikethrough for skipped). The two layers compose — specific overrides generic.

Theme CSS

/* === Checklist: universal checkbox item styling === */

/* All checklist items get left padding for the indicator */
[data-checked] {
  position: relative;
  padding-left: 1.75rem;
  list-style: none;
}

/* Indicator base — positioned left of text */
[data-checked]::before {
  content: '';
  position: absolute;
  left: 0.125rem;
  top: 0.5em;
  width: 1rem;
  height: 1rem;
  border-radius: var(--radius-sm, 0.25rem);
  border: 2px solid var(--color-border);
  background: transparent;
}

/* Checked — filled with checkmark */
[data-checked="checked"]::before {
  background: var(--color-success);
  border-color: var(--color-success);
  /* checkmark via CSS mask or content */
}

[data-checked="checked"] {
  color: var(--color-text-muted);
}

/* Active — primary colour with emphasis */
[data-checked="active"]::before {
  border-color: var(--color-primary);
  background: var(--color-primary);
  box-shadow: 0 0 0 3px color-mix(in oklch, var(--color-primary) 20%, transparent);
}

[data-checked="active"] {
  color: var(--color-primary);
  font-weight: 600;
}

/* Skipped — muted with strikethrough */
[data-checked="skipped"]::before {
  background: var(--color-text-muted);
  border-color: var(--color-text-muted);
}

[data-checked="skipped"] {
  text-decoration: line-through;
  color: var(--color-text-muted);
}

/* Unchecked — empty indicator (default styling from base rules) */

Six rules for the checklist. Every rune with checkbox-style list items gets consistent visual treatment. Plot beats can override with their dot/timeline treatment via BEM specificity. Work acceptance criteria, comparison feature lists, and any community rune with checklists all work automatically.

Checklist and Density Interaction

/* Compact: tighter spacing */
[data-density="compact"] [data-checked] {
  padding-left: 1.5rem;
}

[data-density="compact"] [data-checked]::before {
  width: 0.75rem;
  height: 0.75rem;
}

/* Minimal: indicators only, no text */
[data-density="minimal"] [data-checked] {
  font-size: 0;        /* hide text */
  padding-left: 0;
  display: inline-block;
  width: 1rem;
  height: 1rem;
}

Existing Rune Migration

PackageRuneCurrent approachMigration
storytellingPlot (beats)Marker regex → status modifier → BEM classes + custom dot CSSAdd data-checked alongside existing BEM. Dot styling stays via BEM; text treatment from universal rules
marketingComparisonMarker regex → row type → per-rune stylingAdd data-checked for check/cross rows. Row-specific layout stays via BEM
planWork/BugPipeline counts [x]/[ ] for progress badges; no visual styling on itemsAdd data-checked to list items. Acceptance criteria get checkbox indicators for free
planBacklogDisplays progress counts from pipelineNo change — still reads counts from entity data

Migration is additive. Existing BEM styling continues to work. The universal data-checked rules layer underneath.


Sequential Items

Controls the visual treatment of ordered, sequential items within runes — numbered step circles, vertical connector lines with dots, and horizontal connector lines. Today every rune that displays sequential items writes its own counter styling independently: steps, recipe, howto, track, timeline, itinerary, and plot (linear variant) all produce nearly identical CSS for numbered circles or connector dots with different BEM selectors. A universal sequential item treatment eliminates this duplication.

Values

ValueAttributeTreatment
numbereddata-sequenceNumbered circle indicator (counter) to the left of each item
connecteddata-sequenceVertical connector line between items with dots at each node
plaindata-sequenceNo visual indicator — ordered semantics only

An optional data-sequence-direction attribute controls orientation:

ValueTreatment
verticalItems stacked vertically (default), connector runs top→bottom
horizontalItems laid out horizontally, connector runs left→right

How It Works

The identity transform sets data-sequence on ordered item containers (<ol> elements or item wrappers) based on the rune config. Individual <li> elements within a data-sequence container inherit the sequential treatment automatically via CSS — no per-item attributes needed.

Rune Config

Runes declare their sequence style in config:

Steps: {
  block: 'steps',
  sequence: 'numbered',
  // ...
}

Timeline: {
  block: 'timeline',
  sequence: 'connected',
  // modifiers control direction
  // ...
}

Track: {
  block: 'track',
  sequence: 'numbered',
  // ...
}

Runes that don't declare sequence render ordered lists with default browser styling. The attribute is opt-in.

Identity Transform Output

Steps (numbered):

<ol data-sequence="numbered">
  <li class="rf-step">
    <!-- counter circle generated by CSS -->
    <span>Mix the dry ingredients</span>
  </li>
  <li class="rf-step">
    <span>Add wet ingredients slowly</span>
  </li>
</ol>

Timeline (connected, vertical):

<ol data-sequence="connected" data-sequence-direction="vertical">
  <li class="rf-timeline-entry">
    <time>2024-01-15</time>
    <span>Project kickoff</span>
  </li>
  <li class="rf-timeline-entry">
    <time>2024-03-01</time>
    <span>Beta release</span>
  </li>
</ol>

Timeline (connected, horizontal):

<ol data-sequence="connected" data-sequence-direction="horizontal">
  <li class="rf-timeline-entry">...</li>
  <li class="rf-timeline-entry">...</li>
</ol>

The rune-specific BEM classes remain for per-rune overrides. The universal data-sequence rules provide the baseline treatment — numbered circles, connector lines — that every sequential rune shares.

Theme CSS

/* === Sequential Items: universal ordered item styling === */

/* Numbered — counter circle to the left of each item */
[data-sequence="numbered"] {
  counter-reset: sequence;
  list-style: none;
  padding-left: 0;
}

[data-sequence="numbered"] > li {
  counter-increment: sequence;
  position: relative;
  padding-left: 2.25rem;
}

[data-sequence="numbered"] > li::before {
  content: counter(sequence);
  position: absolute;
  left: 0;
  top: 0.625rem;
  width: 1.5rem;
  height: 1.5rem;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.75rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--color-primary);
  background: var(--color-surface);
  border-radius: 50%;
}

[data-sequence="numbered"] > li + li {
  border-top: 1px solid var(--color-border);
}

/* Connected vertical — line with dots */
[data-sequence="connected"],
[data-sequence="connected"][data-sequence-direction="vertical"] {
  list-style: none;
  padding-left: 0;
}

[data-sequence="connected"] > li,
[data-sequence="connected"][data-sequence-direction="vertical"] > li {
  position: relative;
  padding-left: 2rem;
  padding-bottom: 2rem;
  border-left: 2px solid var(--color-border);
  margin-left: 0.375rem;
}

[data-sequence="connected"] > li:last-child,
[data-sequence="connected"][data-sequence-direction="vertical"] > li:last-child {
  border-left-color: transparent;
  padding-bottom: 0;
}

[data-sequence="connected"] > li::before,
[data-sequence="connected"][data-sequence-direction="vertical"] > li::before {
  content: '';
  position: absolute;
  left: -0.4375rem;
  top: 0.25rem;
  width: 0.75rem;
  height: 0.75rem;
  border-radius: 50%;
  background: var(--color-primary);
  border: 2px solid var(--color-bg);
  box-shadow: 0 0 0 2px var(--color-primary);
}

/* Connected horizontal — line with dots */
[data-sequence="connected"][data-sequence-direction="horizontal"] {
  display: flex;
  gap: 2rem;
  overflow-x: auto;
}

[data-sequence="connected"][data-sequence-direction="horizontal"] > li {
  position: relative;
  min-width: 12rem;
  padding-top: 1.5rem;
  padding-left: 0;
  padding-bottom: 0;
  border-left: none;
  border-top: 2px solid var(--color-border);
  margin-left: 0;
}

[data-sequence="connected"][data-sequence-direction="horizontal"] > li:last-child {
  border-top-color: 2px solid var(--color-border);
}

[data-sequence="connected"][data-sequence-direction="horizontal"] > li::before {
  left: 0.5rem;
  top: -0.4375rem;
}

/* Plain — no indicators, just ordered semantics */
[data-sequence="plain"] {
  list-style: none;
  padding-left: 0;
}

Approximately 8 rules (numbered base + item + indicator + separator, connected vertical + horizontal, plain). Every rune with sequential items gets consistent visual treatment. Per-rune BEM rules override where needed — plot beats keep their status-coloured dots, tracks keep their inline flex layout.

Sequential Items and Density Interaction

/* Compact: smaller circles, tighter spacing */
[data-density="compact"] [data-sequence="numbered"] > li {
  padding-left: 1.75rem;
}

[data-density="compact"] [data-sequence="numbered"] > li::before {
  width: 1.25rem;
  height: 1.25rem;
  font-size: 0.625rem;
}

[data-density="compact"] [data-sequence="connected"] > li {
  padding-bottom: 1rem;
}

/* Minimal: no indicators, collapsed list */
[data-density="minimal"] [data-sequence] > li::before {
  display: none;
}

[data-density="minimal"] [data-sequence="connected"] > li {
  border-left: none;
  padding-left: 0;
  margin-left: 0;
}

Existing Rune Migration

PackageRuneCurrent approachMigration
marketingStepscounter-reset: step on .rf-steps, counter circle on .rf-step::beforeAdd data-sequence="numbered" to <ol>. Remove ~15 lines of counter CSS. BEM classes stay for step-specific content styling
learningRecipecounter-reset: recipe-step on .rf-recipe__content ol, circle on li::beforeAdd data-sequence="numbered" to <ol>. Remove ~15 lines of counter CSS
learningHowTocounter-reset: howto-step on .rf-howto__content ol, circle on li::beforeAdd data-sequence="numbered" to <ol>. Remove ~15 lines of counter CSS
mediaTrackcounter-increment: track on .rf-track, counter on ::beforeAdd data-sequence="numbered" to track list. Track uses inline flex layout so BEM overrides the positioned circle with inline number styling
businessTimeline (vertical)border-left + dot on .rf-timeline--vertical .rf-timeline-entryAdd data-sequence="connected" data-sequence-direction="vertical". Remove ~12 lines of connector CSS
businessTimeline (horizontal)border-top + dot on .rf-timeline--horizontal .rf-timeline-entryAdd data-sequence="connected" data-sequence-direction="horizontal". Remove ~12 lines of connector CSS
placesItineraryborder-left + dot on .rf-itinerary-stopAdd data-sequence="connected". Remove ~12 lines of connector CSS
storytellingPlot (linear)border-left + status-coloured dot on .rf-plot--linear .rf-beatAdd data-sequence="connected". Status-coloured dots stay via .rf-beat--complete::before etc. which override the universal primary-colour dot

Migration is additive. Existing BEM classes remain. The universal rules handle the baseline; per-rune CSS can be reduced to only what's specific to that rune (content layout, status colours, media slots).


Complete Theme Baseline

A theme implementing all universal dimensions writes approximately this many rules:

DimensionRulesCoverageDeclared by
Meta types6Every metadata badge shapeRune config
Meta sentiments4Every badge colourRune config
Meta ranks2Every badge sizeRune config
Surfaces4 + assignmentsEvery container treatmentTheme
Densities3 (× section interactions)Every detail levelRune config + context
Sections6Every structural elementRune config
States6Every interactive stateBehaviour script
Media slots5Every image treatmentRune config
Checklist6Every checkbox-style list itemContent detection
Sequence8Every ordered item indicatorRune config
Total~54 + surface assignmentsEvery rune in the ecosystem

The surface assignments are the one per-rune cost — the theme lists which runes get which surface treatment. This is typically 4 selector groups (one per surface type) totalling maybe 10 additional lines. Everything else is universal.

A theme author’s workflow becomes:

  1. Style the 9 rune-declared dimensions (~50 rules) for universal coverage
  2. Assign surfaces to runes (~4 selector groups)
  3. Customise specific runes where the generic treatment isn’t sufficient
  4. The generic rules handle every rune they haven’t specifically customised — including community runes they’ve never seen

Rune Config Summary

The full config for a rune with all dimensions:

Recipe: {
  block: 'recipe',
  
  // Universal dimensions (rune-declared)
  defaultDensity: 'full',
  sections: {
    header: 'header',
    title: 'title',
    description: 'description',
    content: 'body',
    tips: 'footer',
  },
  mediaSlots: {
    image: 'cover',
  },
  
  // Metadata (from Metadata System Specification)
  refs: {
    prepTime: { metaType: 'temporal', metaRank: 'primary' },
    cookTime: { metaType: 'temporal', metaRank: 'primary' },
    difficulty: {
      metaType: 'category',
      metaRank: 'primary',
      sentimentMap: { easy: 'positive', medium: 'neutral', hard: 'caution' },
    },
    servings: { metaType: 'quantity', metaRank: 'primary' },
  },

  // Existing config (modifiers, contentWrapper, etc.)
  modifiers: { difficulty: { source: 'meta', default: 'medium' } },
  contentWrapper: { ref: 'content' },
  
  // Note: surface is NOT declared here — the theme owns it
}

All dimensions are optional. A rune that doesn’t declare sections renders without data-section attributes. A rune that doesn’t declare mediaSlots renders images without data-media attributes. Migration is per-field, per-rune, at whatever pace makes sense.


Dimension Map

The following tables map every rune across all packages to its proposed universal theming dimension values, derived from the actual rune configs (structure, contentWrapper, autoLabel, modifiers, and interactive behaviour).

Table 1: Section Anatomy Map

Maps each container-level rune's structural refs to the standard data-section roles. The cell value is the actual ref name used in config; "---" means the rune has no equivalent section.

PackageRuneheadertitledescriptionbodyfootermedia
coreHintheader (icon + title)------(content children)------
coreAccordionheader (eyebrow, headline, blurb via autoLabel)headlineblurb(panels)------
coreDetails---summary---(content children)------
coreCodeGrouptopbar (dots + title)title---(panels)------
coreGrid---------(cells)------
coreTabsheader (eyebrow, headline, blurb via autoLabel)headlineblurb(tab panels)------
coreDataTable---------table------
coreForm---------body------
coreRevealheader (eyebrow, headline, blurb via autoLabel)headlineblurb(steps)------
coreCompare---------(panels)------
coreConversation---------(messages)------
coreAnnotate---------body------
coreSidenote---------body------
coreFigure------caption(image content)------
coreGallery---------(items)------
corePullQuote---------body------
coreTextBlock---------body------
coreMediaText---------body---media
coreShowcase---------viewport------
coreEmbed---------fallback------
coreDiagram---title (figcaption)---container------
coreChart---title (figcaption)---containerlegend---
coreBlogheader (eyebrow, headline, blurb via autoLabel)headlineblurbcontent------
coreBudgetheader (title + meta)title---(categories)footer (totals)---
coreBreadcrumb---------items------
coreNav---------(groups/items)------
coreJuxtapose---------(panels)------
coreSandbox---------source------
coreDiff---------(lines)------
marketingHeroheader (eyebrow, headline, blurb via autoLabel)headlineblurb(actions)---media
marketingCallToActionheader (eyebrow, headline, blurb via autoLabel)headlineblurb(actions)------
marketingBentoheader (eyebrow, headline, blurb via autoLabel)headlineblurb(cells)------
marketingFeatureheader (eyebrow, headline, blurb via autoLabel)headlineblurb(definitions)---image
marketingStepsheader (eyebrow, headline, blurb via autoLabel)headlineblurb(step items)------
marketingPricingheader (eyebrow, headline, blurb via autoLabel)headlineblurb(tiers)------
marketingTestimonial---------content---avatar
marketingComparisonheader (eyebrow, headline, blurb via autoLabel)headlineblurbtable/cardsverdict---
docsApiheader (method + path + auth)------body------
docsSymbolheader (kind, lang, since, deprecated, source)headline---body------
docsChangelogheader (eyebrow, headline via autoLabel)headline---(releases)------
learningHowTometa (estimatedTime + difficulty)headlineblurbcontent------
learningRecipemeta (prep, cook, servings, difficulty)headlineblurb(ingredients + steps)---media
storytellingCharacterbadge (role + status)name---content---portrait
storytellingRealmbadge (type + scale)name---(sections)---scene
storytellingLorebadge (category)title---content------
storytellingFactionbadge (type + alignment + size)name---(sections)------
storytellingPlotbadge (type + structure)title---(beats)------
storytellingBond---------body------
storytellingStoryboard---------(panels)------
businessCastheader (eyebrow, headline, blurb via autoLabel)headlineblurb(members)------
businessOrganizationheader (eyebrow, headline, blurb via autoLabel)headlineblurbbody------
businessTimelineheader (eyebrow, headline, blurb via autoLabel)headlineblurb(entries)------
placesEventdetails (date, location, register)headlineblurbcontent------
placesItineraryheader (eyebrow, headline, blurb via autoLabel)headlineblurb(days)------
placesMap---------container------
mediaPlaylistheader (type-badge)title---(tracks)---media
mediaAudio------description(audio content)------
designSwatch---------chip------
designPalette---------grid/scale------
designTypography---title---specimens------
designSpacing---title---scale/radii/shadows------
designDesignContext---title---(sections)------
designPreview---------source------
designMockup---label---frame (viewport)------
planSpecheader (id, status, version, supersedes)------body------
planWorkheader (id, status, priority, complexity, assignee, milestone)------body------
planBugheader (id, status, severity, assignee, milestone)------body------
planDecisionheader (id, status, date, supersedes)------body------
planMilestoneheader (name, status, target)------body------
planBacklog---------(work items)------
planDecisionLog---------(decisions)------

Table 2: Media Slots Map

Runes that have image or media refs in their config, mapped to the proposed data-media slot type.

PackageRuneSlot refMedia type
coreMediaTextmediacover
coreFigure(image content)cover
marketingHeromediahero
marketingFeatureimagecover
marketingTestimonialavatarportrait
marketingStepmediacover
learningRecipemediacover
storytellingCharacterportraitportrait
storytellingRealmscenecover
storytellingStoryboard (panel)imagecover
mediaPlaylistmediacover
businessCast (member)(avatar via content)portrait
designMockupviewporthero
designPreview(rendered content)hero

Table 3: Interactive State Map

Runes that have interactive behaviour (toggling, selecting, expanding).

PackageRuneStates usedMechanism
coreAccordionopen / closed, active / inactive@refrakt-md/behaviors accordion script; panels toggle open/closed, triggers toggle active/inactive
coreDetailsopen / closedNative <details> element or behaviours script
coreTabsactive / inactive@refrakt-md/behaviors tabs script; tabs and panels toggle active/inactive
coreDataTableselected (rows)@refrakt-md/behaviors datatable script; sortable columns, searchable rows
coreFormdisabled (fields)@refrakt-md/behaviors form script; field validation states
coreRevealopen / closed, active / inactive@refrakt-md/behaviors reveal script; steps toggle through sequentially
coreJuxtaposeactive / inactive@refrakt-md/behaviors juxtapose script; slider or animation toggle
coreGalleryselected (lightbox)@refrakt-md/behaviors gallery lightbox; selected image in overlay
coreNavactive / inactiveWeb component <rf-nav>; active state tracks current page
coreCodeGroupactive / inactive@refrakt-md/behaviors tabs script (reused); panels toggle
coreSandboxactive / inactiveWeb component <rf-sandbox>; live/source toggle
coreDiagram---Web component <rf-diagram>; renders on client, no toggle state
placesMap---Web component <rf-map>; interactive map, no discrete states

Table 4: Default Density

Proposed default density for every container-level rune. Child/item runes are excluded (they inherit from their parent).

PackageRuneDefault density
coreHintcompact
coreAccordionfull
coreDetailscompact
coreCodeGroupcompact
coreGridfull
coreTabsfull
coreDataTablecompact
coreFormfull
coreRevealfull
coreComparefull
coreConversationcompact
coreAnnotatefull
coreSidenoteminimal
coreFigurecompact
coreGalleryfull
corePullQuotecompact
coreTextBlockfull
coreMediaTextfull
coreShowcasecompact
coreEmbedcompact
coreDiagramcompact
coreChartcompact
coreBlogfull
coreBudgetfull
coreBreadcrumbminimal
coreNavcompact
coreJuxtaposecompact
coreSandboxcompact
coreDiffcompact
marketingHerofull
marketingCallToActionfull
marketingBentofull
marketingFeaturefull
marketingStepsfull
marketingPricingfull
marketingTestimonialcompact
marketingComparisonfull
docsApifull
docsSymbolfull
docsChangelogfull
learningHowTofull
learningRecipefull
storytellingCharacterfull
storytellingRealmfull
storytellingLorefull
storytellingFactionfull
storytellingPlotfull
storytellingBondcompact
storytellingStoryboardfull
businessCastfull
businessOrganizationfull
businessTimelinefull
placesEventfull
placesItineraryfull
placesMapcompact
mediaPlaylistfull
mediaAudiocompact
designSwatchminimal
designPalettefull
designTypographyfull
designSpacingfull
designDesignContextfull
designPreviewcompact
designMockupcompact
planSpecfull
planWorkfull
planBugfull
planDecisionfull
planMilestonefull
planBacklogfull
planDecisionLogfull

Child and Item Runes

Child runes --- AccordionItem, Tab/TabPanel, BentoCell, ComparisonColumn/ComparisonRow, Step, Tier/FeaturedTier, RevealStep, JuxtaposePanel, ConversationMessage, AnnotateNote, BreadcrumbItem, NavGroup/NavItem, FormField, Definition, BudgetCategory/BudgetLineItem, Track, MapPin, ItineraryDay/ItineraryStop, CastMember, TimelineEntry, Beat, CharacterSection, RealmSection, FactionSection, SymbolGroup/SymbolMember, ChangelogRelease, StoryboardPanel, RecipeIngredient, etc. --- do not independently declare density, section anatomy, or media slot dimensions. They inherit their parent rune's density and render within the parent's structural context. The parent rune's config determines the overall anatomy; child runes contribute to the parent's body section content.

The one exception is media refs on child runes (e.g., StoryboardPanel has an image ref, CastMember may contain an avatar). These inherit the parent's density for sizing but declare their own data-media slot so the theme can apply the correct media treatment (portrait, cover, thumbnail) regardless of nesting depth.


Inspector Audit

$ refrakt inspect --audit-dimensions

  Surface assignments (theme-owned):
  card       18 runes assigned
  inline      6 runes assigned
  banner      4 runes assigned
  inset       3 runes assigned
  (unassigned) 2 runes no surface in theme (wine-tasting, stat-block)

  Density coverage:
  full       24 runes themed
  compact    24 runes themed
  minimal    24 runes themed

  Section anatomy:
  header     22 runes themed
  title      24 runes themed
  description 18 runes themed
  body       24 runes themed
  footer     12 runes themed
  media       8 runes themed

  Interactive state:
  open/closed  4 runes themed (accordion, details, reveal, exercise)
  active/inactive 2 runes themed (tabs, accordion)
  selected     2 runes themed (quiz, datatable)

  Media slots:
  portrait    3 runes themed
  cover       5 runes themed
  thumbnail   4 runes themed
  hero        2 runes themed
  icon        1 rune themed

  Community runes:
  @refrakt-community/wine wine-tasting:
 metadata, density, sections themed via dimensions
 no surface assigned add to theme surface selectors
  @refrakt-community/dnd-5e stat-block:
 metadata, density, sections themed via dimensions
 no surface assigned add to theme surface selectors

The inspector verifies that every dimension value in use has theme CSS, and flags runes — especially community runes — that could benefit from declaring dimensions they’re missing.

Relationships