# school-drills — platform contract

A family of self-contained, offline-capable learning apps for ages 9–15.
`conjugaison-app` is the reference implementation; read its code before writing a new app.

Future direction (NOT to be built now, but do not design against it): a unified
platform with accounts, cross-app leaderboards, and parental controls. That is why
the storage schema, the item model, and the design tokens below are fixed contracts.

---

## 1. Hard constraints (every app)

- **No build step. No bundler. No package.json. No node_modules.**
- **No ES modules, no `import`/`export`, no `fetch()` of local files.** Plain
  `<script src="...">` tags only, executed in order, sharing globals.
- Must run identically from `file://` (a USB stick) and from `https://` (S3+CloudFront).
- **No external runtime dependencies** except Google Fonts (`Fraunces` + `Inter`),
  loaded exactly as `conjugaison-app/index.html` does. Everything else is local.
- All state in `localStorage`, wrapped in try/catch so a blocked/full store never
  throws (copy the `loadXP`/`saveXP` pattern).
- Target browsers: modern evergreen desktop + mobile Safari/Chrome. Must be usable
  on a phone in portrait.

## 2. Required files

```
<app>/
  index.html          # app shell: header, xp bar, tab strip, layout, script tags
  styles.css          # copy conjugaison-app/styles.css, extend; DO NOT re-theme
  app.js              # UI, activities, boot — loaded LAST
  engine.js           # (optional) pure logic / rule generation, no DOM
  <subject>-data.js   # the data; split into several files if > ~200KB
  manifest.json       # PWA manifest
  service-worker.js   # copy conjugaison-app/service-worker.js, update ASSETS + CACHE_VERSION
  README.md           # what it teaches, data sources, how to add content
```

Icons: do **not** generate PNG icons. Reference `icon-192.png`, `icon-512.png`,
`apple-touch-icon.png`, `favicon-32.png` in the manifest/head, and note in README.md
that they are still to be produced. Leave them out of the service-worker `ASSETS`
list so the install does not fail on a missing file.

## 3. Design system — identical across all apps

Copy `conjugaison-app/styles.css` verbatim as your starting point. The palette is a
hard contract; the only per-app variation is the brand emoji + wordmark in `.brand`.

**Colour is themed, so never hardcode it.** `themes.css` + `themes.js` (copied into
every app so each stays standalone-deployable) define six schemes, applied via
`data-theme` on `<html>` and stored under the platform-wide key
`school-drills:theme` — the choice follows the child between apps.

Use `var(--token)` for every colour. A literal hex anywhere outside the `:root`
block will not follow the theme and will break the light schemes. The audit is:

```bash
awk '/^:root\{/,/^\}/{next}{print}' styles.css | grep -E "#[0-9a-fA-F]{3,8}"
```

That must come back empty. Available tokens beyond the palette:
`--knob` (switch knob), `--halo` (ring separating a marker from artwork beneath),
`--on-warm` / `--on-cool` (text on a saturated fill), `--masc` / `--fem`.

Adding a theme: one block in `themes.css` (it must define **every** token — a
missing one silently inherits and looks like a rendering bug) plus one entry in
`SD_THEMES`. Every theme must clear WCAG AA (4.5:1) on body, muted and emphasis
text over both page and card backgrounds.

Artwork is exempt where the colour carries meaning — the anatomical palette in
`human-body/diagrams.js` stays constant across themes, because a liver is
liver-coloured on a light page too. Keep such colours mid-toned so they read on
any background.

```css
:root{
  --navy:#0d1525;--navy2:#131d32;--card:#192038;--card2:#1f2944;
  --border:#253055;--gold:#c09330;--gold-l:#e0b545;--gold-bg:rgba(192,147,48,.12);
  --text:#d8e4ff;--text2:#7888b0;
  --ok:#3ecfb0;--ok-bg:rgba(62,207,176,.1);--ok-b:rgba(62,207,176,.3);
  --err:#f07878;--err-bg:rgba(240,120,120,.1);--err-b:rgba(240,120,120,.3);
  /* platform alias — future per-subject retheming changes only these 3 lines */
  --accent:var(--gold);--accent-l:var(--gold-l);--accent-bg:var(--gold-bg);
}
```

Use `var(--accent*)` in any **new** CSS you write. Leave existing gold usages alone.

Typography: `Fraunces` (serif, italic) for the "answer" — the thing being learned.
`Inter` for all chrome. Dark navy background, gold accent, teal = correct,
salmon = wrong. Rounded cards, 1px `--border` hairlines, generous padding.

## 4. Layout skeleton — same in every app

```
header    brand · [entity picker dropdown] · [audio toggle] [extra toggle]
xpbar     level badge · XP progress bar · ✓ / ✗ / 🔥 counters · reset button
tbar      the study-timer challenge bar
layout    aside (category checkboxes + filters)  |  content (tab strip + #activity)
```

The header's centre control is the **entity picker** — a searchable multi-select
dropdown. Adapt it to the subject (verbs → nouns / fact families / continents /
body systems) but keep the interaction and the markup classes (`.vs-*`).

## 5. Item model — hard contract

Every drillable thing is `(entity, category, index)`:

- **entity** — the thing being learned (a verb, a noun, a country, an organ, `7×8`)
- **category** — the facet being tested (a tense, a form, an attribute, an operation)
- **index** — position within that facet (person 0–5, or `0` when the facet is scalar)

```js
itemId(e,c,i) => e + '|' + c + '|' + i
```

Item records stored in progress are:

```js
{ e, c, i, ans, box, seen, correct, wrong, streak, due, last }
```

> NOTE: `conjugaison-app` uses the legacy field names `{v, t, i, form, …}`. New apps
> use `{e, c, i, ans, …}`. Conjugaison will be migrated later — do not copy its names.

**Pseudo-categories.** A drill that is not a facet of an entity — ordering the
steps of a process, labelling a diagram — is still stored as `(entity,
category, index)` so it shares the schedule, but its category is kept OUT of
`CATEGORIES`. It then never appears in the sidebar, and `liveItem()` gains a
branch for it. `tech-drills` does this with `Order` over sequence ids.

**Refuse to test what the data cannot answer.** `answerFor()` returns `null`
for facts that are disputed, unmeasured or inapplicable, and `poolItems()`
drops those items. This is how `periodic-table` never asks an f-block element
its group, and never quizzes a disputed element family.

**A facet that is only sometimes a real question must switch itself off.**
`tech-drills` asks "which area does this belong to?" only when two or more
areas are selected — with one selected the answer is always the same, which
teaches guessing. Filter the category out of the pool and say why in the UI.

**Validate cross-file references at load.** When one data file names things
defined in another — `ai-drills` scenarios whose options must be real concepts —
check every reference at startup and collect the failures in a `DATA_PROBLEMS`
array. A bad reference is invisible in normal use (the quiz just offers a choice
with nothing behind it), so it has to be caught loudly rather than by a child
mid-question.

**The distractor rule.** Wrong answers for multiple-choice must be *siblings* — other
values from the same entity or the same category — never random. Sibling distractors
are what make the quiz feel like it was written by a teacher. Every app must define a
`distractorsFor(item, n)` that returns plausible near-misses, and document its
strategy in a comment.

## 6. Required subsystems — port these from `conjugaison-app/app.js`

| Subsystem | Source | Notes |
|---|---|---|
| XP + levels + level-up toast | `LVLS`, `addXP`, `updateXPBar` | keep the 6 tiers and thresholds; translate names |
| Toast | `toast()` | unchanged |
| Leitner SRS | `SRS_INTERVALS`, `recordAnswer`, `dueItems`, `weakItems` | unchanged logic, renamed fields |
| Revision tab | `renderREV`/`startREV` | "Today's review" + "My mistakes"; typed recall |
| Progress dashboard | `renderPROG` | mastered / accuracy / due, mastery bar per category, weak-entity chips |
| Daily activity caps | `LIMITS`, `bumpUsage`, `renderLocked` | ration passive games, keep active recall plentiful, never cap review/reference |
| Study-timer challenge + certificate | `initTimer`…`downloadCert` | keep it — it is the seed of parental controls. Retheme the canvas certificate to the subject |
| Text-to-speech | `speak`, `spkBtn` | French app: `fr-FR`. English apps: `en-GB`, and only where reading aloud actually helps |
| PWA offline | `service-worker.js` | update `ASSETS` + `CACHE_VERSION` |

## 7. localStorage keys — namespaced

```
<slug>:xp        {total}
<slug>:prog      {items:{...}}
<slug>:usage     {date, counts:{}}
<slug>:best      per-game high scores, e.g. {speed: 42}
```

Slugs: `conjugaison` (legacy keys `cjxp`/`cjprog`/`cjusage`), `frnoms`, `math`, `geo`, `body`, `chem`, `tech`, `ai`.

Also expose, for the future platform to discover:

```js
const SUBJECT = { slug:'geo', title:'…', brand:'🌍 …', lang:'en', tabs:[…] };
```

## 8. Content standards

- Audience: **ages 9–15**. Wholesome, classroom-safe, no pop culture, no brands,
  no violence, nothing that dates.
- Factual accuracy is non-negotiable. Where a fact is disputed or has changed
  recently, prefer the stable/most-taught answer and note it in README.md.
- Prefer **generating** content from rules over storing it; store only what rules
  cannot derive. Keep hand-authored data in its own file, easy to append to.
- Every wrong answer must teach: always show the correct answer, and where it is
  cheap to do so, show *why* (the rule, the pattern, a one-line note).

## 9. Definition of done

- Opens from `file://` with no console errors and no network requests except fonts.
- Every tab renders, plays through, and completes; empty states handled.
- Progress and XP survive a reload; "reset" actually clears.
- Works at 380px wide.
- README.md written.
