# Authoring guide — adding language pairs

This file lives in `es/` (the canonical pair) but applies to **every** pair folder: `es/`, `en/`, `de/`, `pt/`, and any new one you create. Every pair is a self-contained, file://-friendly folder with the same three shell files. The differences between pairs live in `meta.json` and in the lesson content.

## Folder layout (every pair)

```
<pair>/                            ← e.g. es/, en/, de/, pt/
├── index.html                     ← reader shell (same bytes across pairs)
├── flashcards.html                ← flashcards shell (same bytes across pairs)
├── sync-embedded-md.mjs           ← sync script (same bytes across pairs)
├── meta.json                      ← THIS is what makes the pair unique
├── curriculum.md                  ← the 100-day spine (in the explainer language)
├── lesson-template.md             ← copy this to start a new day
├── day-001.md                     ← target-language phrases + explainer narrative
├── day-002.md
├── day-NNN.md…
└── decks/
    ├── day-001.json
    ├── day-002.json
    └── day-NNN.json…
```

Anything pair-specific that the **shell** needs to know — UI labels, sidebar entries, the `html lang` attribute, font choices — lives in `meta.json`. The shells read it at runtime and recolor themselves.

## The pair convention

The folder is named after the **target language** (the language you're learning):

| Folder | Target | Explainer | Pair code (in `meta.json`) |
|---|---|---|---|
| `es/` | Spanish | English | `es-from-en` |
| `en/` | English | Spanish | `en-from-es` |
| `de/` | German | English | `de-from-en` |
| `pt/` | Brazilian Portuguese | English | `pt-from-en` |

If you ever want **two explainers for the same target** (say, Spanish-from-French as well as Spanish-from-English), rename to `es-from-en/`, `es-from-fr/`, and so on. Until then, target-code-only is enough.

## Add a new pair in five minutes

1. **Pick a folder name.** Use the target's two-letter code (`fr`, `it`, `ja`, …) or the full pair code if it would collide.
2. **Copy the shells from any existing pair.** All three shell files should be byte-identical across pairs:
   ```sh
   mkdir -p new-pair/decks
   cp es/index.html es/flashcards.html es/sync-embedded-md.mjs new-pair/
   ```
3. **Write `new-pair/meta.json`.** Copy an existing pair's `meta.json` and change:
   - `pair`, `target`, `explainer`, `htmlLang`
   - Every string in `ui.*` (these are what the user *sees*)
   - The `sidebar` entries (start with just one day, add more as you write them — but **do not list files in the sidebar before they exist**, the sync script will throw)
4. **Write content** in the explainer language:
   - `curriculum.md` — the spine for the target. Use any existing curriculum as the structural template.
   - `lesson-template.md` — the format for daily lessons (in the explainer language).
   - `day-001.md` — your first lesson (target phrases + explainer narrative).
   - `decks/day-001.json` — five flashcards, front = target phrase, back = explainer translation + concept note.
5. **Sync:**
   ```sh
   cd new-pair && node sync-embedded-md.mjs
   ```
   Open `new-pair/index.html` in any browser. Done.

## Adding more days to an existing pair

1. Copy `lesson-template.md` to `day-NNN.md` and write it.
2. Create `decks/day-NNN.json` (5 cards).
3. Open `meta.json` and append a new entry to the `dailyLessons` array with the matching flashcard deck nested under `children`:
   ```json
   {
     "id": "day-NNN",
     "path": "day-NNN.md",
     "title": "Day NNN — …",
     "children": [
       { "id": "fc-NNN", "path": "flashcards.html?deck=day-NNN", "title": "Flashcards" }
     ]
   }
   ```
4. `node sync-embedded-md.mjs`.

## Keeping the shells in sync across pairs

The three shell files (`index.html`, `flashcards.html`, `sync-embedded-md.mjs`) should stay identical across pairs. After you edit them in one pair (typically `es/`), propagate to the rest:

```sh
# Run from inside the idiomas/ folder:
for pair in en de pt; do
  cp es/index.html es/flashcards.html es/sync-embedded-md.mjs "$pair/"
  (cd "$pair" && node sync-embedded-md.mjs)
done
```

The `sync-embedded-md.mjs` step re-embeds each pair's own `meta.json`, lessons, and decks — so the shells become byte-identical *except* for the embedded JSON blocks, which are necessarily pair-specific.

## The `meta.json` contract

```json
{
  "pair": "<target>-from-<explainer>",
  "target":    { "code": "es", "name": "Spanish" },
  "explainer": { "code": "en", "name": "English" },
  "htmlLang":  "<BCP47 code of the explainer language>",
  "direction": "ltr | rtl",
  "phrasesPerDay": 5,
  "ui": {
    "siteTitle":          "<browser tab title for index.html>",
    "sidebarHeader":      "<sidebar H1>",
    "dailyPractice":      "<label for the top group (e.g. \"Today's review\")>",
    "startHere":          "<label for the 'curriculum / template' group>",
    "dailyLessons":       "<label for the 'days' group>",
    "appendix":           "<label for the 'concept appendix' group, optional>",
    "flashcards":         "<label for the 'flashcards' group>",
    "checkpoints":        "<label for the 'checkpoints' group at the bottom>",
    "flashcardsTitle":    "<browser tab title for flashcards.html>"
  },
  "sidebar": [
    {
      "label": "<KEY into ui.* above, e.g. \"startHere\">",
      "files": [
        { "id": "<unique slug>", "path": "<file or .html?deck=…>", "title": "<sidebar text>" }
      ]
    }
  ]
}
```

Rules:

- Every `label` in `sidebar` must match a key in `ui.*` (case-sensitive). If the lookup fails, the raw label string is shown.
- Every `path` ending in `.md` must point to a file that exists in the pair folder; the sync script will fail loudly if it doesn't.
- Paths starting with `flashcards.html?deck=NAME` need a matching `decks/NAME.json` file.
- Any sidebar entry can carry a `children` array of nested entries. The shell renders them as an indented sub-list under the parent. This is how each day's flashcard deck is grouped with the day instead of living in a separate top-level group.
- The reserved paths `flashcards.html?deck=due` and `flashcards.html?checkpoint=N` are *synthesized* inside the flashcards shell — they don't correspond to a JSON file. The first one shows due-today cards across every deck listed in `sidebar`; the second one quizzes you over days `(N-1)*15+1` to `N*15`.

## Flashcard modes (the shell handles these for free)

The `flashcards.html` shell selects its behavior from URL params:

| URL | What you get |
|---|---|
| `flashcards.html?deck=day-NNN` | **Drill mode** — that day's 5 cards, one at a time, with Got-it/Try-again grading that promotes/demotes the card through Leitner boxes 1→6. |
| `flashcards.html?deck=due`     | **Today's review** — every card across every linked deck whose `due` timestamp ≤ today. Shuffled. Same grading model. Empty? Banner explains. |
| `flashcards.html?checkpoint=N` | **Checkpoint quiz** — randomized cards from decks `day-001` through `day-(15N)`, capped at 20, back-side-first (explanation shown, target language is the answer). Final score is persisted to `localStorage` under `idiomas:checkpoints:<pair>`. |

Subsystems:

- **TTS** — every time the target-language face is shown, the shell speaks the phrase via the Web Speech API at rate 0.92. The voice is chosen by matching `meta.target.code` to the browser's available voices. The toolbar has a 🔊 replay and a 🔈/🔇 mute toggle (state in `localStorage` key `idiomas:audio:muted`, shared across pairs).
- **Leitner SRS** — grades schedule the card 1, 2, 4, 7, 14, 30 days out for boxes 1–6. Miss = back to box 1. State lives in `localStorage` keyed by pair (`idiomas:srs:<pair>`), so each pair has independent progress.
- **Keyboard** — `←/→` navigate, `Space/Enter` flips, `G/1` grades "got it", `M/2` grades "try again", `S` replays audio.

## When something breaks

- **"Could not load meta.json"** banner in the reader: you opened the page via `file://` but didn't run `node sync-embedded-md.mjs` first. The sync embeds a copy that file:// can read.
- **Lesson file listed in meta.sidebar is missing**: the sync script tells you exactly which path is missing from disk.
- **Embedded deck missing or empty**: you visited `flashcards.html?deck=X` but there's no `decks/X.json`. Either create it or remove the sidebar entry.
- **Shells drift between pairs**: re-copy them from `es/` (or whichever pair you've designated as the source of truth) and re-run sync everywhere.
