<div align="center">

# Learning Platform

**One home for all self-paced learning courses — a shared hub, a shared asset layer, and a
scaffold so every new course starts from a template instead of a fork.**

![Static site](https://img.shields.io/badge/site-100%25%20static-4F46E5)
![No dependencies](https://img.shields.io/badge/dependencies-none-0E8C7E)
![Vanilla JS](https://img.shields.io/badge/stack-HTML%20%C2%B7%20CSS%20%C2%B7%20vanilla%20JS-7C3AED)
![Courses](https://img.shields.io/badge/active%20courses-5-DB2777)

</div>

This repo unifies the previously standalone course sites — **CNPE**, **Learning AI**,
**Learning Claude**, **Learning Cloud Migration**, and **Cloud Quest** — behind one hub
page, one registry, and one shared platform layer. Each existing course was lifted in
whole and still works exactly as before; new courses are scaffolded from `templates/`
and use the shared chrome in `shared/` instead of forking it.

The full engineering spec lives in **[CONTRACT.md](CONTRACT.md)** — read it before
touching `shared/`, `templates/`, or `scripts/`.

## Run it

```bash
cd learning-platform
python3 -m http.server 4700
```

Open http://localhost:4700 — the hub lists every course. No build step, no dependencies,
no third-party requests (one documented exception: Learning AI's opt-in AI News page).

## Layout

```
index.html          the hub (course grid regenerated by scripts/build.js)
courses.json        THE registry — add/edit courses here, never in index.html
shared/             platform layer: chrome engine, stylesheet, store, theme, fonts
courses/<slug>/     each course, self-contained
templates/          page skeleton + course scaffold source
scripts/build.js    validate + regenerate (sitemap, hub grid, courses-data)
scripts/new-course.js   scaffold a new course
```

## Add a new course

```bash
node scripts/new-course.js my-course "My Course Title" --accent "#7C3AED"
```

This scaffolds `courses/my-course/` from the template (welcome page + sample lesson +
manifest), registers it in `courses.json` as a **draft**, and regenerates the hub grid.
Then:

1. Author pages by copying `templates/page.html`; for each page add a row to the
   `topics` array in `courses/my-course/manifest.js` — sidebar, pager, and tags all
   follow that array automatically.
2. Keep all localStorage access behind `Store.create(COURSE.storagePrefix)`.
3. Run `node scripts/build.js` until green (it link-checks every topic row against disk).
4. Flip the course's `status` to `"active"` in `courses.json` and re-run the build.

The per-course authoring checklist ships in each scaffolded course's README.

## How the existing courses fit in

The five original courses were copied in **as-is** (lift-and-shift, Phase 0). They keep
their own `assets/site.js` + `assets/styles.css` and know nothing about `shared/`. That
is deliberate — see the phase plan below. The originals outside this repo were left
untouched; once you're happy with the platform, the standalone copies and their repos
can be archived.

Changes made during the lift (the only ones):

- Per-course `robots.txt` / `sitemap.xml` removed — replaced by the root pair
  (sitemap is generated).
- Two **dead** `assets/store.js` copies removed (CNPE, Cloud Migration). Both were
  unreferenced by any page and hardcoded Learning AI's `lai:` localStorage namespace —
  a real cross-course data collision waiting to happen on a shared origin.
- `migration-course-old.html` (gitignored personal backup in the old repo) not carried over.

## Phase roadmap

- **Phase 0 — unify entry** ✅ hub + registry + lift-and-shift + shared layer + templates
  + scaffolder.
- **Phase 1 — storage + fonts**: point each sibling course's `@font-face` at
  `shared/fonts/` (~1.6 MB dedup; the six files in `shared/fonts/` are already the
  canonical set); adopt `shared/js/store.js` where courses touch localStorage.
- **Phase 2 — shared chrome**: convert the four sibling courses to `manifest.js` +
  `shared/js/site.js`, deleting their forked site.js/styles.css copies.
- **Phase 3 — shared engines**: extract flashcard/quiz/checklist engines and inline data
  banks to `content/` JSON with stable ids; add dark-mode tokens to `platform.css`.

## Platform laws (short form — the long form is in CONTRACT.md)

1. 100% static; serve with any static file server.
2. Zero third-party runtime requests (AI News page is the sole, documented exception —
   therefore **no global CSP meta**).
3. Dev tooling is Node stdlib only; generated files are committed and stamped.
4. No service workers.
5. Bugfixes land in `shared/` — the course-local forks are frozen until Phase 2.
6. Every localStorage key is namespaced; prefixes are registered in `courses.json`.

## Known carried-over issues (tracked, not blockers)

- `courses/claude/assets/styles.css` has a `.lab` class collision (checklist
  styles leak onto flashcard faces) — fix lands with Phase 2.
- `courses/ai/study-program.html` bypasses its own Store wrapper with raw
  `prog_cfg_*` / `prog_done_*` keys, and its `privacy.html` claim ("everything under
  `lai:`") is therefore not quite true — normalize in Phase 1.
- CNPE's CNPA practice pages use four inconsistent localStorage key spellings
  (`lpe:` / `lpe.` variants) — normalize in Phase 1 via the Store migrations shim.
- Hardcoded `theme-color` #4F46E5 in every sibling page head will fight dark mode —
  templated-head fix in Phase 3.
- Users of the old per-course dev ports are on different origins; their localStorage
  progress cannot migrate automatically. If that matters, ship the small
  export/import-progress page described in CONTRACT's storage notes.
