# Template Design Neutralization — Design Spec

**Date:** 2026-06-03
**Workstream:** 1 of 2 (design neutralization). Workstream 2 — completing template starter features — is a separate spec.
**Status:** Approved (Approach A)

## Problem

Every starter template zip currently bakes in the **Substrate** design system: a shared warm‑neutral token base, a distinct per‑template accent, the Geist/Newsreader font `<link>`s, and a "Substrate design system" section in `KNOWLEDGE.md`. The `templates:sync-design` artisan command (`app/Console/Commands/SyncTemplateDesign.php`) regenerates this across all six zips.

This duplicates design identity in two places — the design‑system zips **and** the template zips — which causes:

- **Drift** between Substrate's canonical zip and the baked template copies, requiring the sync command to police.
- **A class of overlay bugs**: templates hardcode font `<link>`s *outside* the `<!-- design-system-fonts -->` marker, so a re‑theme leaves stale, unused fonts downloading (fixed defensively in the Go overlay, but the root coupling remains).
- **Conceptual leak**: the architecture is "template (structure) × design system (look)", yet the template carries a full look.

At build time, `executor.ApplyDesignOverlay` (Go builder) **fully overwrites `src/index.css`** with the resolved design system's tokens + accent at eager template‑init, and injects that system's fonts. So a template's baked CSS only ever surfaces (a) if a template is run/previewed standalone, or (b) as a fallback when no design system is applied — never on a normal build.

## Goal

Make each template zip carry **zero brand identity**. The template ships a neutral, valid baseline; the design‑system overlay is the single source of visual identity. Retire `templates:sync-design`.

## Decisions (locked)

| Decision | Choice |
| --- | --- |
| Fallback/standalone appearance | **Pure/minimal safety net** — neutral baseline only needs to compile and look acceptable; templates are not shown standalone in normal flow. |
| `SyncTemplateDesign` command | **Retire entirely** — bake the neutral baseline into the six zips once, maintain directly. |
| Scope | **Identity layer only** — `src/index.css`, `index.html` fonts, `KNOWLEDGE.md` design section. Leave functional status colors (green=in‑stock/success, red=error) alone; design systems don't define success/warning tokens. |
| Baseline source | **Approach A — static neutral baseline.** One hand‑authored neutral `index.css` used verbatim in all six zips. |

## Scope

### In scope (all six template zips: `default`, `ecommerce`, `dashboard`, `cms`, `landing`, `portfolio`)
1. Replace `src/index.css` with one canonical **neutral baseline** (below).
2. Edit `index.html`: remove the hardcoded Substrate font block; leave an empty `<!-- design-system-fonts -->` marker as the overlay's anchor.
3. Replace the `KNOWLEDGE.md` "Theme & styling — the Substrate design system" section with a system‑agnostic note.
4. Delete `app/Console/Commands/SyncTemplateDesign.php`.
5. Commit the regenerated six zips.

### Out of scope
- Tokenizing functional status colors (green/red/emerald) — separate concern; would need `--success`/`--warning` tokens added to every design‑system zip.
- Any change to design‑system zips (`substrate.zip` etc.) or the Go overlay (the existing stray‑font strip stays as a safety net).
- Completing template starter features (workstream 2).
- `src/custom.css` — already empty; untouched.

## The neutral baseline (`src/index.css`)

Keep the existing structural CSS verbatim — `@import "tailwindcss"`, `@custom-variant dark`, the entire `@theme inline` mapping (the `--color-* → hsl(var(--*))` block is value‑independent), the radius scale, the elevation scale, the accordion keyframes, and the `@layer`/base rules (`* { border-color }`, `body`, heading rhythm, transition rule). Change **only**: the comment header, the `:root` palette, the `.dark` palette, `--radius`, `--shadow-color`, and the `--font-sans`/`--font-serif` values. Remove the per‑template accent comment, the `--gradient-hero` vars + `.gradient-hero` helper (landing), and the serif‑heading override (cms) — verified unreferenced by any page/component, so removal is build‑safe.

Canonical neutral values (HSL `H S% L%` triples, matching the existing `hsl(var(--x))` usage):

```css
@import "tailwindcss";

@custom-variant dark (&:is(.dark *));

/* ============================================================
   Neutral baseline — no brand identity.
   The project's visual identity (color, typography, radius,
   elevation) is supplied by an installed DESIGN SYSTEM, applied
   at build time. This baseline only needs to be valid and render
   acceptably if a template is ever viewed without an overlay.
   ============================================================ */

:root {
  --background: 0 0% 100%;
  --foreground: 0 0% 9%;
  --card: 0 0% 100%;
  --card-foreground: 0 0% 9%;
  --popover: 0 0% 100%;
  --popover-foreground: 0 0% 9%;
  --secondary: 0 0% 96.1%;
  --secondary-foreground: 0 0% 9%;
  --muted: 0 0% 96.1%;
  --muted-foreground: 0 0% 45.1%;
  --accent: 0 0% 96.1%;
  --accent-foreground: 0 0% 9%;
  --border: 0 0% 89.8%;
  --input: 0 0% 89.8%;
  --destructive: 0 72% 51%;
  --destructive-foreground: 0 0% 98%;

  --primary: 0 0% 9%;
  --primary-foreground: 0 0% 98%;
  --ring: 0 0% 9%;

  --radius: 0.5rem;

  --chart-1: 0 0% 25%;
  --chart-2: 0 0% 40%;
  --chart-3: 0 0% 55%;
  --chart-4: 0 0% 70%;
  --chart-5: 0 0% 85%;

  --sidebar: 0 0% 98%;
  --sidebar-foreground: 0 0% 9%;
  --sidebar-primary: 0 0% 9%;
  --sidebar-primary-foreground: 0 0% 98%;
  --sidebar-accent: 0 0% 96.1%;
  --sidebar-accent-foreground: 0 0% 9%;
  --sidebar-border: 0 0% 89.8%;
  --sidebar-ring: 0 0% 9%;

  --shadow-color: 0 0% 0%;
}

.dark {
  --background: 0 0% 9%;
  --foreground: 0 0% 98%;
  --card: 0 0% 11%;
  --card-foreground: 0 0% 98%;
  --popover: 0 0% 11%;
  --popover-foreground: 0 0% 98%;
  --secondary: 0 0% 16%;
  --secondary-foreground: 0 0% 98%;
  --muted: 0 0% 16%;
  --muted-foreground: 0 0% 64%;
  --accent: 0 0% 18%;
  --accent-foreground: 0 0% 98%;
  --border: 0 0% 20%;
  --input: 0 0% 22%;
  --destructive: 0 62% 50%;
  --destructive-foreground: 0 0% 98%;

  --primary: 0 0% 98%;
  --primary-foreground: 0 0% 9%;
  --ring: 0 0% 83%;

  --chart-1: 0 0% 80%;
  --chart-2: 0 0% 65%;
  --chart-3: 0 0% 50%;
  --chart-4: 0 0% 38%;
  --chart-5: 0 0% 28%;

  --sidebar: 0 0% 11%;
  --sidebar-foreground: 0 0% 98%;
  --sidebar-primary: 0 0% 98%;
  --sidebar-primary-foreground: 0 0% 9%;
  --sidebar-accent: 0 0% 18%;
  --sidebar-accent-foreground: 0 0% 98%;
  --sidebar-border: 0 0% 20%;
  --sidebar-ring: 0 0% 83%;

  --shadow-color: 0 0% 0%;
}
```

`@theme inline { … }` block: unchanged **except** the typography lines become system stacks:

```css
  --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
```

Everything below `@theme inline` (the `*`, `body`, heading‑rhythm, and transition rules) is kept verbatim — it is structural, not brand‑specific.

## `index.html` change

Remove these four lines (the comment + three font `<link>`s):

```html
    <!-- Substrate type system: Geist (UI/body) + Newsreader (editorial) -->
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link href="https://fonts.googleapis.com/css2?family=Geist...&display=swap" rel="stylesheet" />
```

Replace with the empty marker block (gives `ApplyDesignOverlay` its anchor; with system fonts no `<link>` is needed):

```html
    <!-- design-system-fonts -->
    <!-- /design-system-fonts -->
```

## `KNOWLEDGE.md` change

Replace the "## Theme & styling — the Substrate design system" section (and its Substrate‑specific bullets) with:

```markdown
## Theme & styling
This template ships a **neutral baseline only**. The project's visual identity —
colors, typography, radius, and elevation — is provided by an installed
**design system**, applied automatically at build time.

- Build with the **semantic tokens**, never hardcoded values: surfaces use
  `bg-background` / `bg-card` / `bg-muted`, text uses `text-foreground` /
  `text-muted-foreground`, actions use `bg-primary` + `text-primary-foreground`,
  borders use `border-border`. Typography uses `font-sans` / `font-serif`.
- Never hardcode brand colors (e.g. `bg-indigo-600`) or font families — that
  bypasses the design system and will look wrong once a system is applied.
- Functional status colors (e.g. green for success/in‑stock, red for errors)
  are acceptable where they convey meaning, not brand.
```

## Build‑time interaction (unchanged, confirmed)

- On every real build, `ApplyDesignOverlay` overwrites `src/index.css` with the resolved system's tokens+accent and replaces the marker block with that system's fonts (stripping any stray font `<link>`s). The neutral baseline never reaches a built site that has a design system.
- The neutral baseline is the rendered result **only** when `ApplyDesignOverlay` is a no‑op (`design_system` payload nil/empty) — the true fallback. It is valid and renders as a clean grayscale, system‑font site.
- No Go builder changes in this workstream.

## One‑time bake mechanism

`templates:sync-design` is deleted, so the neutralization is a **one‑time transformation** done during implementation via a throwaway script (NOT committed — only its output is):

For each of the six zips under `storage/app/private/templates/`:
1. Extract to a temp dir.
2. Overwrite `src/index.css` with the canonical neutral baseline (one shared file).
3. Edit `index.html` (strip font block → empty marker).
4. Edit `KNOWLEDGE.md` (swap the theme section).
5. Re‑zip in place (preserve internal structure/paths), overwriting the committed zip.

Then delete the throwaway script. Commit the six regenerated zips + the command deletion.

## Testing / acceptance

1. **Standalone build per template** — extract each neutralized zip, `npm install && npm run build` succeeds (neutral CSS compiles; every shadcn `--color-*` var resolves).
2. **Overlay still wins** — run a builder build pinned to a **non‑Substrate** system (e.g. Carbon) on a neutralized template; assert built `index.html` contains only that system's fonts and `src/index.css` is that system's tokens (no neutral/Geist/Newsreader residue).
3. **Fallback renders** — render a neutralized template with no design system; confirm valid grayscale + system fonts, no console errors.
4. **No residue** — grep all six zips: no `Substrate`, `Geist`, `Newsreader`, `gradient-hero`, or `fonts.googleapis.com` strings remain; grep codebase: no `templates:sync-design` / `SyncTemplateDesign` references remain.
5. **Seeder unaffected** — `php artisan db:seed --class=TemplateSeeder` still runs; templates still register (zip contents changed, metadata unchanged).

## Risks & notes

- Removing landing's `.gradient-hero` and cms's serif‑heading override is build‑safe (verified: no page/component references them). If a future template's *pages* depend on a baked CSS helper, that page must be checked before unifying — none do today.
- `default-template.zip` ships on every install (always seeded), so its neutral baseline is the production fallback look — plain but valid, which is the intended behavior.
- This removes the only consumer of the per‑template accent concept; nothing else reads it.
- The existing Go overlay stray‑font strip becomes belt‑and‑suspenders rather than load‑bearing — keep it.
