Consumer Rules — src/pages/

You are building consumer-side code: pages, layouts, app-level screens that compose the shipped library. The 5 Laws (root CLAUDE.md) apply. The strict library audit protocol from src/CLAUDE.md does not apply here — page work should compose freely from existing primitives, not pre-flight every change against an analog.

This file is loaded for any session whose CWD is in src/pages/ or src/components/app/.


Exemplar-First

Before composing a new page in src/pages/ or src/components/app/, identify the matching exemplar in .claude/exemplars/ and view its wireframe + code as the canonical pattern. Composition recipes in COMPOSITION-RECIPES.md describe which primitives to compose; exemplars describe how to arrange them. The exemplar is an anchor, not a jail — deviate when the brief demands, using VE primitives correctly.

Discovery flow: .claude/rules/exemplars.md.


1. You are a consumer


2. AI build order (page / app composition)

When the task is page or app composition, read in this order:

  1. src/config/component-registry.json — what already exists
  2. .claude/rules/silent-failures.mdmandatory. Patterns that compile but silently break: dead numeric Tailwind utilities, custom-styled <button> missing btn-void, fieldset overrides, sticky-vs-nav math, conditional rendering jumps, chip-as-toggle anti-patterns. Read this BEFORE writing markup.
  3. .claude/rules/polish-phase.mdload when the brief uses “polish” / “improve” / “tighten” / “9/10” / “take it further”. Doctrine + anti-pattern catalog: polish is subtraction, not addition. Stops AI from reflexively adding presence clusters, view-mode tabs that don’t render, double-loudness active states, iOS-style overlay badges, and other training-distribution decoration.
  4. AI-PLAYBOOK.md — high-level working order
  5. COMPOSITION-RECIPES.md — page archetypes (dashboard, settings, list/detail, etc.)
  6. nearest local analog in src/pages/, src/layouts/, or src/components/app/
  7. relevant .claude/rules/*.md (auto-loaded on path match — page-composition.md, tailwind-registry.md, spacing-protocol.md, etc.)

Default to editing consumer files. Do not edit src/components/ui/, src/components/icons/, src/components/core/, src/styles/, src/types/, or src/config/design-tokens.ts unless the user explicitly asks for system-level work.


3. Page composition pattern

.astro files are route shells. src/layouts/ owns the shared app shell. src/components/app/*.svelte owns sections, form state, async actions. See .claude/rules/page-composition.md for the full scaffold and architecture rules.

---
import Layout from '../layouts/Layout.astro';
import AccountPage from '@components/app/AccountPage.svelte';
---

<Layout title="Account">
  <AccountPage client:load />
</Layout>

Layout imports use a relative path (../layouts/Layout.astro). There is no @layouts/* alias — every page in src/pages/ follows this convention, and nested routes (e.g. src/pages/atmospheres/[brand]/[atmosphere].astro) walk up the appropriate number of segments.

Spacing rhythm follows .claude/rules/spacing-protocol.md. Quick reference:

Surface variety — plain surface-raised is the right default for most blocks; reach beyond it only when the role genuinely differs. Restraint doctrine + full catalog in .claude/rules/component-usage.md §“Surface catalog” and .claude/rules/silent-failures.md §14. Quick-pick (use sparingly):


4. Reuse-first composites

Before reaching for raw <input> / <button> / <select>, check the registry for a composite. Common composites:

If a composite doesn’t support ...rest and you need to pass extra native attributes, extend the composite — don’t duplicate its template.


5. Icons

Static icons (Lucide)

<script lang="ts">
  import { Heart, TriangleAlert } from '@lucide/svelte';
</script>
<Heart class="icon" data-size="lg" />
<TriangleAlert class="icon text-error" />

Lucide icons (ISC, commercial-safe). Always class="icon". Sizing via data-size (sm | md | lg | xl | 2xl | 3xl | 4xl). State via data-* attributes only.

Interactive icons (custom)

Custom animated icons live in src/components/icons/. Use them like Lucide icons — class pattern is icon-[name] icon (component class first, base class second). Each interactive icon namespaces its own icon-[name] selector.

Color

Icons inherit color from their parent via currentColor. Color is decided at the usage site:

Stroke vs fill — declared on the SVG, not in CSS

Each custom icon’s stroke and fill mode is declared as an attribute on the <svg> root, not in .icon SCSS or Tailwind. This prevents cascade-layer fights (Tailwind utilities outranking SCSS) and lets each icon pick the mode that matches its shape language.

The three categories:

<!-- Stroke-only (line icons, like most Lucide) -->
<svg class="icon-arrow icon" stroke="currentColor" fill="none" viewBox="0 0 24 24">
  <path d="M5 12h14M13 5l7 7-7 7" />
</svg>

<!-- Mixed (line + accent fill) -->
<svg class="icon-pin icon" stroke="currentColor" fill="currentColor" viewBox="0 0 24 24">
  <path d="..." stroke-width="2" fill="none" />
  <circle cx="12" cy="10" r="3" />
</svg>

<!-- Fill-only (solid glyphs, logos) -->
<svg class="icon-spark icon" fill="currentColor" viewBox="0 0 24 24">
  <path d="..." />
</svg>

Lucide icons declare their own fill="none" internally — just use class="icon", don’t add fill-none.

Never set fill or stroke from CSS / Tailwind on .icon. Tailwind v4’s utilities layer outranks void-scss, and any SCSS rule like .icon { fill: var(--energy-primary); } will silently lose to a text-* utility on the same element. Color flows through currentColor and the parent’s text color — that’s why the SVG attributes use currentColor.

Logo icons

Non-square viewBox icons (e.g. LogoDGRS, LogoCoNexus) carry data-render="logo" on the <svg> root so SCSS can opt out of square data-size constraints.

Multiple instances on one page

SVG <defs> IDs (masks, filters, gradients) are auto-generated via $props.id() per instance. Don’t pass a manual id prop expecting to namespace internal defs — the runtime already does it. CSS animation hooks target classes (.mask-rect, .mask-x), not IDs.

Never


6. Empty states

Plain text, muted color, centered, generous padding. No italic — italic is reserved for prose semantics.

<p class="text-mute text-center p-lg">No items yet</p>

7. Premium-package integration

Premium packages (@void-energy/ambient-layers, @void-energy/kinetic-text) are consumed through their published exports. From consumer code:

import { ambientLayer } from '@void-energy/ambient-layers';
import { kinetic, narrative } from '@void-energy/kinetic-text';

Do not reach into packages/<name>/src/ via relative paths. Each premium package documents its API in its own AI-REFERENCE.md (see packages/ambient-layers/AI-REFERENCE.md and packages/kinetic-text/AI-REFERENCE.md).


8. State