# Astrowise product data schema

Product data lives in `data/sources/*.json`. Each source file is a JSON **array** of product
objects for one category. The file name prefix decides the category:

| Prefix          | Category  | Example file                     |
| --------------- | --------- | -------------------------------- |
| `telescopes-`   | telescope | `telescopes-sky-watcher.json`    |
| `mounts-`       | mount     | `mounts-premium.json`            |
| `tripods-`      | tripod    | `tripods-all.json`               |
| `cameras-`      | camera    | `cameras-all.json`               |
| `guiding-`      | guide     | `guiding-all.json`               |
| `filters-`      | filter    | `filters-all.json`               |
| `focusers-`     | focuser   | `focusers-all.json`              |
| `rotators-`     | rotator   | `rotators-all.json`              |
| `eyepieces-`    | eyepiece  | `eyepieces-all.json`             |

`scripts/build-data.js` validates every source file and writes the merged catalogs the site
loads (`data/telescopes.json`, `data/mounts.json`, `data/tripods.json`).

Check a single file while you work:

```
deno run --allow-read scripts/build-data.js --check data/sources/telescopes-sky-watcher.json
```

Rebuild everything:

```
deno run --allow-read --allow-write scripts/build-data.js
```

---

## Rules that apply to every category

- **Real products only.** Every entry must be a product that really exists (or existed) under
  that exact name. If you are not confident a product exists, or you do not know its core
  specs (aperture and focal length for telescopes, payload for mounts, capacity for tripods),
  leave it out. Fewer accurate entries beat more invented ones.
- Units: millimetres for optics and leg diameters, centimetres for tripod heights,
  kilograms for weights and payloads, **US dollars** for prices.
- `price` is the typical *new street price in USD in 2025–2026*, rounded (nearest $5 under
  $200, nearest $10 under $2,000, nearest $50 above). If a product is discontinued, use its
  last typical new price.
- Unknown optional numbers are `null`, never `0`. Unknown optional arrays are `[]`.
- Weights: use the manufacturer figure when known; otherwise give a careful estimate rounded
  to 0.5 kg (never leave a required weight empty).
- Write `summary` and `highlights` in plain, neutral British-or-American English. No hype words
  ("amazing", "incredible", "best ever"), no emoji, no prices in the text.

### Common fields

| Field        | Type                 | Notes |
| ------------ | -------------------- | ----- |
| `id`         | string, required     | Lowercase kebab-case slug of brand + model, ASCII only: `sky-watcher-skyliner-200p`. Unique. |
| `brand`      | string, required     | Official brand spelling: `Sky-Watcher`, `Celestron`, `Explore Scientific`, `William Optics`, `Tele Vue`, `TS-Optics`, `iOptron`, `ZWO` … |
| `model`      | string, required     | Model name without the brand: `Skyliner 200P`, `EdgeHD 8 OTA`, `AM5`. |
| `aka`        | string[], optional   | Regional or alternative names, e.g. `["Classic 200P"]`. |
| `price`      | number, required     | USD, see rounding above. |
| `year`       | integer or null      | Year introduced, if known. |
| `status`     | enum, required       | `current` (sold new in 2025–2026) or `discontinued`. |
| `includes`   | string[], required   | Up to 5 notable included items ("25 mm and 10 mm eyepieces", "8×50 finder", "Counterweight 5 kg"). `[]` if unknown. |
| `highlights` | string[], required   | 2–3 short selling points, each ≤ 48 characters, no trailing full stop. |
| `summary`    | string, required     | One factual sentence, ≤ 180 characters. |
| `popularity` | integer 1–10         | 10 = iconic best-seller recommended everywhere, 7 = well known and popular, 4 = niche, 1 = obscure. |
| `visual`     | object, required     | Appearance hints used to draw the product illustration (see below). |

### `visual`

| Field    | Type                 | Notes |
| -------- | -------------------- | ----- |
| `color`  | `#rrggbb`, required  | Dominant colour of the tube / body / legs. |
| `accent` | `#rrggbb`, required  | Secondary colour: dew shield, cells, rings, knobs, head plate. |
| `base`   | `#rrggbb` or null    | Telescopes: Dobsonian rocker box, or the included mount/tripod colour. Mounts: included tripod colour. Tripods: `null`. |
| `finish` | enum, required       | `gloss`, `matte`, `metallic` or `carbon`. |

Typical colours: white tube `#f1f2f4`, black `#16181c`, Celestron classic orange `#e8742a`,
dark blue `#1f3f8f`, anodised red `#b3202a`, gold/champagne `#c9a65a`, silver `#b9bec6`,
natural wood `#b07a45`, carbon fibre `#2a2c30` with finish `carbon`.

---

## Telescopes (`telescopes-*.json`)

| Field         | Type                 | Notes |
| ------------- | -------------------- | ----- |
| `design`      | enum, required       | `refractor`, `newtonian`, `dobsonian`, `sct`, `maksutov`, `rc`, `rasa`, `smart` |
| `subtype`     | enum, required       | Depends on `design`, see the table below. |
| `aperture`    | number, required     | Clear aperture in mm. |
| `focalLength` | number, required     | Native focal length in mm (without reducers or flatteners). |
| `weight`      | number, required     | Total weight **as sold** in kg (includes mount and tripod if they come with it). |
| `otaWeight`   | number or null       | Optical tube alone, kg. |
| `mount`       | enum, required       | `none` (tube only / OTA), `dobsonian`, `altaz`, `eq`, `integrated` (smart telescopes). |
| `control`     | enum, required       | `none` (OTA only), `manual`, `pushto` (app- or encoder-guided pointing, e.g. StarSense Explorer), `goto` (motorised GoTo). |
| `mountName`   | string or null       | Included mount, e.g. `"EQ-320 equatorial"`, `"NexStar SE single fork arm"`. `null` for OTAs. |
| `use`         | array, required      | Any of `visual`, `imaging`. What the scope is genuinely suited to. |
| `targets`     | array, required      | Any of `planets` (Moon, planets, double stars), `deepsky` (galaxies, nebulae, clusters), `widefield` (large nebulae, Milky Way fields). Its real strengths, not everything. |
| `level`       | enum, required       | `beginner`, `intermediate`, `advanced`. |

### `design` and `subtype`

| `design`    | Allowed `subtype` values | Use for |
| ----------- | ------------------------ | ------- |
| `refractor` | `achromat`, `ed-doublet`, `apo-triplet`, `petzval` | Lens telescopes. `petzval` = 4- or 5-element flat-field imaging designs (RedCat, Askar FRA/SQA, FSQ, Z61…). |
| `newtonian` | `standard`, `astrograph` | Newtonian reflectors on an EQ/alt-az mount or sold as a tube. `astrograph` = fast imaging Newtonians (Quattro, HNT, Epsilon, Boren-Simon, ONTC…). |
| `dobsonian` | `solid`, `collapsible`, `truss`, `tabletop` | Newtonians on a Dobsonian rocker base. `tabletop` = small scopes on a tabletop base (Heritage 130P, StarBlast). Always `mount: "dobsonian"`. |
| `sct`       | `classic`, `aplanatic` | Schmidt–Cassegrains. `aplanatic` = EdgeHD, ACF and similar coma-free designs. |
| `maksutov`  | `classic` | Maksutov–Cassegrains. |
| `rc`        | `rc`, `cdk`, `cassegrain` | Ritchey–Chrétien, corrected Dall–Kirkham, classical/Dall–Kirkham Cassegrains. |
| `rasa`      | `rasa` | Rowe–Ackermann / prime-focus Schmidt astrographs. |
| `smart`     | `arm`, `tower`, `compact` | App-controlled all-in-one imaging telescopes. `arm` = single-arm body (Seestar), `tower` = tube carried in a fork or body (Vespera, Stellina, eVscope, Odyssey), `compact` = box-shaped (Dwarf). Always `mount: "integrated"`, `control: "goto"`. |

### Telescope example

```json
{
  "id": "example-optics-ed80",
  "brand": "Example Optics",
  "model": "ED80 OTA",
  "aka": [],
  "design": "refractor",
  "subtype": "ed-doublet",
  "aperture": 80,
  "focalLength": 600,
  "weight": 2.6,
  "otaWeight": 2.6,
  "mount": "none",
  "control": "none",
  "mountName": null,
  "use": ["visual", "imaging"],
  "targets": ["deepsky", "widefield", "planets"],
  "level": "intermediate",
  "price": 520,
  "year": 2015,
  "status": "current",
  "includes": ["Tube rings", "Vixen-style dovetail", "2-inch dual-speed focuser"],
  "highlights": ["Light, travel-friendly ED doublet", "Good first imaging scope"],
  "summary": "An 80 mm f/7.5 ED doublet refractor that works as a grab-and-go visual scope and a first astrophotography tube.",
  "popularity": 7,
  "visual": { "color": "#f1f2f4", "accent": "#16181c", "base": null, "finish": "gloss" }
}
```

---

## Mounts (`mounts-*.json`)

| Field            | Type               | Notes |
| ---------------- | ------------------ | ----- |
| `type`           | enum, required     | `gem` (German equatorial with worm gears, incl. centre-balanced CEM designs), `strain-wave` (harmonic drive), `altaz` (single-arm or yoke alt-azimuth), `fork`, `tracker` (camera star tracker). |
| `modes`          | array, required    | Any of `eq`, `altaz`: the modes it can run in (AZ-EQ6 and AM5 have both). |
| `goto`           | boolean, required  | Motorised GoTo pointing. |
| `payload`        | number, required   | Manufacturer-rated instrument payload in kg (for strain-wave mounts: *without* counterweight). |
| `payloadWithCW`  | number or null     | Rated payload with an optional counterweight, if the maker quotes one. |
| `weight`         | number, required   | Mount head only, kg (no tripod, no counterweights). |
| `tripodIncluded` | boolean, required  | Whether the standard package includes a tripod or pier. |
| `control`        | array, required    | Any of `hand-controller`, `app`, `wifi`, `bluetooth`, `usb`, `ascom` (ASCOM/INDI driver support). `[]` for manual mounts. |
| `autoguide`      | boolean, required  | ST-4 port or software autoguiding support. |
| `battery`        | boolean, required  | Built-in rechargeable battery. |
| `use`            | array, required    | Any of `visual`, `imaging`. |
| `level`          | enum, required     | `beginner`, `intermediate`, `advanced`. |

### Mount example

```json
{
  "id": "example-astro-hd12",
  "brand": "Example Astro",
  "model": "HD12",
  "aka": [],
  "type": "strain-wave",
  "modes": ["eq", "altaz"],
  "goto": true,
  "payload": 12,
  "payloadWithCW": 18,
  "weight": 5,
  "tripodIncluded": false,
  "control": ["hand-controller", "app", "wifi", "usb", "ascom"],
  "autoguide": true,
  "battery": false,
  "use": ["imaging", "visual"],
  "level": "intermediate",
  "price": 1990,
  "year": 2022,
  "status": "current",
  "includes": ["Hand controller", "Carry case"],
  "highlights": ["No counterweight needed up to 12 kg", "Runs in EQ or alt-az mode"],
  "summary": "A compact strain-wave GoTo mount that carries 12 kg without a counterweight.",
  "popularity": 8,
  "visual": { "color": "#b3202a", "accent": "#16181c", "base": null, "finish": "metallic" }
}
```

---

## Tripods and piers (`tripods-*.json`)

| Field           | Type               | Notes |
| --------------- | ------------------ | ----- |
| `type`          | enum, required     | `tripod`, `pier` (free-standing column on feet or a fixed base), `half-pier` (pier extension / half-pier column on short legs). |
| `material`      | enum, required     | `aluminium`, `steel`, `carbon`, `wood`. |
| `capacity`      | number, required   | Rated load in kg. |
| `weight`        | number, required   | kg. |
| `minHeight`     | number or null     | Minimum working height in cm. |
| `maxHeight`     | number or null     | Maximum working height in cm. |
| `legDiameter`   | number or null     | Upper-leg tube diameter (or column diameter for piers) in mm. |
| `compatibility` | array, required    | Mount families it fits directly or with a standard adapter: `sw-small` (Sky-Watcher EQ3/EQ5/AZ-GTi/AZ5/Star Adventurer class), `sw-large` (Sky-Watcher HEQ5/EQ6-R/AZ-EQ5/AZ-EQ6 class), `celestron` (AVX/CGEM/CGX class), `ioptron`, `zwo` (AM3/AM5/AM7), `losmandy`, `universal` (flat plate, 3/8″ or M10 bolt, adapters for most mounts). |
| `tray`          | boolean, required  | Accessory tray or leg spreader included. |

### Tripod example

```json
{
  "id": "example-astro-steel-2",
  "brand": "Example Astro",
  "model": "Steel Tripod 2\"",
  "aka": [],
  "type": "tripod",
  "material": "steel",
  "capacity": 30,
  "weight": 8,
  "minHeight": 85,
  "maxHeight": 140,
  "legDiameter": 50,
  "compatibility": ["sw-large"],
  "tray": true,
  "price": 260,
  "year": null,
  "status": "current",
  "includes": ["Accessory tray"],
  "highlights": ["2-inch stainless steel legs", "Fits HEQ5 and EQ6-class heads"],
  "summary": "A heavy 2-inch steel tripod for HEQ5 and EQ6-class equatorial mounts.",
  "popularity": 6,
  "visual": { "color": "#c9ccd2", "accent": "#16181c", "base": null, "finish": "metallic" }
}
```

---

## Astro cameras (`cameras-*.json`)

Dedicated astronomy cameras (cooled deep-sky, planetary, guide cameras). No DSLRs.

| Field          | Type               | Notes |
| -------------- | ------------------ | ----- |
| `uses`         | array, required    | Any of `deep-sky`, `planetary`, `guiding`: what the camera is genuinely good for. |
| `color`        | enum, required     | `color` (one-shot colour) or `mono`. |
| `cooled`       | boolean, required  | Has regulated TEC cooling. |
| `sensor`       | string, required   | Sensor name, e.g. `"Sony IMX571"`. |
| `sensorWidth`  | number, required   | Active sensor width in mm. |
| `sensorHeight` | number, required   | Active sensor height in mm. |
| `megapixels`   | number, required   | Effective resolution in megapixels. |
| `pixelSize`    | number, required   | Pixel size in µm. |
| `readNoise`    | number or null     | Lowest read noise in e⁻. |
| `fullWell`     | number or null     | Full-well capacity in ke⁻. |
| `adc`          | integer or null    | ADC bit depth (12, 14, 16). |
| `maxFps`       | number or null     | Frame rate at full resolution. |
| `weight`       | number, required   | Camera body in kg (e.g. `0.7`). |
| `backFocus`    | number or null     | Sensor back focus in mm (commonly 6.5, 12.5 or 17.5). |

`visual.color` is the body colour, `visual.accent` the front plate / fins / trim colour.

## Guide scopes and off-axis guiders (`guiding-*.json`)

| Field         | Type               | Notes |
| ------------- | ------------------ | ----- |
| `type`        | enum, required     | `guide-scope` or `oag` (off-axis guider). |
| `aperture`    | number or null     | mm. Required for guide scopes, `null` for OAGs. |
| `focalLength` | number or null     | mm. Required for guide scopes, `null` for OAGs. |
| `prism`       | number or null     | OAG pick-off prism size in mm (e.g. `8` for 8×8 mm). `null` for guide scopes. |
| `thickness`   | number or null     | OAG optical length (back focus it uses) in mm. `null` for guide scopes. |
| `weight`      | number, required   | kg. |
| `focuser`     | enum or null       | Guide scope focusing: `helical`, `push-pull`, `crayford`, `rack-and-pinion`. `null` for OAGs. |
| `ed`          | boolean, required  | ED / apochromatic glass. `false` for OAGs. |
| `attach`      | array, required    | How it attaches: any of `finder-shoe`, `rings`, `dovetail`, `inline` (OAGs sit in the imaging train). |
| `cameraFit`   | string or null     | Guide camera connection, e.g. `"1.25-inch"`, `"M42"`, `"M48"`. |

## Filters (`filters-*.json`)

One entry per filter model (not per size). `price` is for the size named in `priceSize`.

| Field        | Type               | Notes |
| ------------ | ------------------ | ----- |
| `type`       | enum, required     | `light-pollution` (broadband LP), `dual-band` (Hα + OIII for colour cameras), `multi-band` (tri/quad-band for colour cameras), `narrowband` (single line for mono cameras), `lrgb` (LRGB/RGB imaging filters or sets), `visual-nebula` (UHC, OIII, Hβ visual filters), `planetary` (colour, neutral density, IR-pass, moon filters), `uv-ir-cut` (luminance / UV-IR block). |
| `lines`      | array, required    | Emission lines passed: any of `ha`, `oiii`, `sii`, `hb`, `nii`. `[]` for broadband, LRGB, planetary and UV/IR filters. |
| `bandwidth`  | number or null     | FWHM in nm for line filters (e.g. `7`, `3`). `null` otherwise. |
| `sizes`      | array, required    | Available sizes: any of `1.25in`, `2in`, `31mm`, `36mm`, `50mm`, `clip-in`. |
| `priceSize`  | enum, required     | The size `price` refers to (prefer `2in`, then `1.25in`, then `36mm`). Must appear in `sizes`. |
| `camera`     | enum, required     | `color` (designed for one-shot colour), `mono` (mono cameras), `any` (works with both, or visual). |
| `use`        | array, required    | Any of `visual`, `imaging`. |
| `set`        | boolean, required  | Sold as a set of several filters (e.g. LRGB, SHO). |

`visual.color` is the colour the coating reflects at an angle (e.g. purple-blue for dual-band, red for Hα, green-cyan for OIII); `visual.accent` is the cell colour.

## Focusers (`focusers-*.json`)

| Field        | Type               | Notes |
| ------------ | ------------------ | ----- |
| `type`       | enum, required     | `electronic` (motorised / autofocus) or `manual`. |
| `design`     | enum, required     | `crayford`, `rack-and-pinion`, `helical`, `coupler` (a motor that attaches to an existing focuser knob, like ZWO EAF), `sct` (SCT rear-cell focus motor). |
| `drawtube`   | number or null     | Drawtube size in inches for manual focusers (`2`, `2.5`, `3`). `null` for couplers. |
| `travel`     | number or null     | Drawtube travel in mm. |
| `maxLoad`    | number or null     | Rated load in kg. |
| `control`    | array, required    | Any of `usb`, `wifi`, `bluetooth`, `hand-controller`, `ascom`, `asiair`. `[]` for manual focusers. |
| `tempSensor` | boolean, required  | Temperature probe for focus compensation. |
| `fits`       | array, required    | Any of `refractor`, `newtonian`, `sct`, `universal`. |
| `weight`     | number, required   | kg. |

## Rotators (`rotators-*.json`)

| Field              | Type               | Notes |
| ------------------ | ------------------ | ----- |
| `type`             | enum, required     | `electronic` or `manual`. |
| `clearAperture`    | number or null     | Clear aperture in mm. |
| `thickness`        | number or null     | Optical length (back focus it uses) in mm. |
| `maxLoad`          | number or null     | Rated load in kg. |
| `control`          | array, required    | Any of `usb`, `wifi`, `bluetooth`, `hand-controller`, `ascom`, `asiair`. `[]` for manual. |
| `integratedFocuser`| boolean, required  | Also a focuser (e.g. Moonlite NiteCrawler). |
| `weight`           | number, required   | kg. |

## Eyepieces (`eyepieces-*.json`)

One entry per eyepiece **line** (e.g. Tele Vue Delos), listing its focal lengths. `price` is the typical price of ONE eyepiece in the line.

| Field          | Type               | Notes |
| -------------- | ------------------ | ----- |
| `focalLengths` | number[], required | Focal lengths sold, in mm, ascending (e.g. `[3.5, 4.5, 6, 8, 10]`). For zooms: `[min, max]`. |
| `zoom`         | boolean, required  | Zoom eyepiece. |
| `afov`         | number, required   | Apparent field in degrees (for zooms, at the widest setting). |
| `eyeRelief`    | number or null     | Typical eye relief in mm across the line (use the minimum if it varies a lot). |
| `barrel`       | array, required    | Any of `1.25in`, `2in` (both for dual-barrel or lines that have both). |
| `waterproof`   | boolean, required  | Nitrogen-purged / waterproof. |
| `weight`       | number or null     | Typical single eyepiece in kg. |

`visual.color` is the body colour, `visual.accent` the trim ring / grip colour.

## Sky objects (`objects.json`)

The catalogue behind **Targets** and the field-of-view tool: every Messier object plus showpiece
NGC/IC objects for both hemispheres, and famous double and coloured stars. It holds reference facts
only; which objects are recommended on a given night is computed in the browser from these
positions, your place, the date, the Moon, your sky and your equipment.
`scripts/build-objects.js` validates it and writes `data/objects.json` with decimal coordinates.

| Field    | Type               | Notes |
| -------- | ------------------ | ----- |
| `id`     | string, required   | Lowercase letters and digits (`m42`, `ngc7000`, `albireo`). Used in URLs (`#/tools/fov?target=m42`). |
| `cat`    | string, required   | Designation shown to people (`M42`, `NGC 7000`, `β Cygni`). |
| `name`   | string or omitted  | Common name, if it has one. |
| `alt`    | string or omitted  | Another designation (`NGC 1976`). |
| `type`   | enum, required     | `galaxy`, `galaxygroup`, `emission`, `reflection`, `planetary`, `snr`, `dark`, `open`, `globular`, `starcloud`, `asterism`, `double`, `star`. |
| `type2`  | enum or omitted    | A second type that also applies (the Pleiades are an open cluster with reflection nebulosity). |
| `form`   | `"E"` or omitted   | Elliptical galaxy (drawn without spiral arms). |
| `ra`     | `"hh mm.m"`        | Right ascension, J2000. Output: degrees. |
| `dec`    | `"±dd mm"`         | Declination, J2000. Output: degrees. |
| `mag`    | number or null     | Visual magnitude (integrated). Null for dark nebulae. |
| `size`   | [major, minor]     | Arcminutes, major first. Omitted for doubles and single stars (derived from `sep`). |
| `pa`     | number or omitted  | Position angle of the major axis, degrees east of north (0–179). |
| `con`    | string, required   | IAU constellation abbreviation (`Ori`). |
| `mag2`, `sep` | numbers       | Doubles only: companion magnitude and separation in arcseconds. |
| `colors` | string[] or omitted | Star colours as `#rrggbb` (doubles: primary, companion). |
| `note`   | string or omitted  | One sentence in our own words, under 160 characters. |

Output adds `sb`, the mean surface brightness in mag/arcsec² (`mag + 2.5 log10(area in arcsec²)`).

## Sky map data (`data/sky/`)

Built by `scripts/build-sky.js` from the catalogues in `data/sources/sky/`; sources and
licences are in `data/sky/CREDITS.md`.

| File | Format |
| --- | --- |
| `stars-a.bin`, `stars-b.bin` | 8 bytes per star, brightest first. Bytes 0–2: RA as a little-endian 24-bit fraction of 360°. Bytes 3–5: (Dec + 90°) / 180° the same way. Byte 6: (V + 2) × 16. Byte 7: (B−V + 0.5) × 80. J2000 frame, proper motion applied to 2026.0. `stars-a` holds stars to magnitude 7 and `stars-b` the rest; a star's index counts across both files. |
| `sky.json` | `epoch`; `stars` `{ count, a }` (stars in each file); `names` `[index, IAU name, Bayer or Flamsteed designation, Flamsteed number when both exist]`; `constellations` `[abbreviation, name, genitive, label RA, label Dec, rank 1–3]`; `lines` `{ abbreviation: [[ra, dec, ra, dec, …], …] }` (degrees). |
| `stars-info.json` | `[index, HIP, HR, HD, spectral type, parallax (mas)]` for named and naked-eye stars. |
| `bounds.json` | IAU constellation boundaries as polylines `[ra, dec, ra, dec, …]` (J2000 degrees). |
| `milkyway.png` | Milky Way brightness, 8-bit greyscale, 2048 × 1024, equirectangular: x = RA 0–360°, y = Dec +90° (top) to −90°. |
| `tyc/index.json` | Deeper Tycho-2 stars for zoomed-in views: `{ order: 2, magLimit: 10.5, epoch: 2026, count, tiles }`. `tiles[n]` is the number of stars in nested HEALPix cell n (192 cells, numbered as in `assets/js/healpix.js`). |
| `tyc/2-<n>.bin` | The stars in cell n, in the `stars-a.bin` layout, brightest first. All have V ≤ 10.5 and none is a Hipparcos star. Empty cells get no file. Stars are filed by their position as decoded, so each lies inside its cell. Pick cells with `cellsInCone(2, centre, radius)`. |

## Deep-sky catalogue (`deep.json`)

About 12,000 OpenNGC objects (CC BY-SA 4.0) for the sky map and search, built by
`scripts/build-deep.js`; anything in `objects.json` is left out. It has the same fields and types
as `objects.json` (`note` is always null), plus `type2`, `magB` (mag is a blue magnitude), `m`
(Messier number) and `noSize` (size is a 1′ placeholder). Ids look like `ngc1234`, `ic434`,
`ngc5194a`, and `alt` holds other names and duplicate designations. `mag` may be fainter than 15,
and galaxy `sb` is OpenNGC's B-band value. Objects are sorted brightest first. `deep-lists.json`
maps Caldwell numbers to ids and gives the Herschel 400 ids in League order. Sources are in
`data/CREDITS.md`.

## Catalogues for observers

Each file is an array, one record per line; `?` marks a field present only when known.

| File | Order | Fields |
| --- | --- | --- |
| `doubles.json` | by RA | `id` (WDS name and components), `disc`, `comp` (or null), `ra`/`dec` (J2000 degrees, proper motion applied to 2026), `m1`, `m2`, `pa` (°), `sep` (″), `year` of the last measure, `sp`?, `orbit`? { `P` years, `T` decimal Julian year of periastron, `e`, `a` ″, `i`, `node`, `omega` degrees, `grade` 1–5 } |
| `zc.json` | by ZC number | `zc`, `xz` (or null), `ra`/`dec` (ICRS degrees at 2026), `v`, `sp`, `sao`, `hip` |
| `eclipsing.json` | brightest first | `name`, `ra`/`dec` (degrees at 2026), `epoch` (HJD of primary minimum), `period` (days), `max`, `min`, `min2`?, `type` (GCVS), `band`? (missing means V) |
| `variables.json` | brightest first | `name`, `oid` (VSX id), `hip` (or null), `type`, `ra`/`dec` (Hipparcos position at 2026 when matched, else VSX J2000), `max`, `min`, `period`, `epoch` (HJD), `sp`, `band`? (missing means V) |
| `moon-features.json`, `mars-features.json` | largest first | `name`, `type` (singular descriptor such as crater or mare; `satellite` means a lettered crater), `lat`, `lon` (planetocentric degrees, east positive), `diam` (km or null) |

## Learn (`data/learn/`)

Plain JSON arrays, fetched by `assets/js/learn-page.js` (and the search) when first wanted. Every
item has an `id` and, where it states checkable facts, `sources` (URLs).

| File | Fields |
| --- | --- |
| `glossary.json` | `term`, `short` (one sentence, ≤ 18 words: the hover and tap explanation), `more` (≤ 70 words), `aliases`, `see` (ids of related terms) |
| `explainers.json` | `title`, `minutes`, `intro`, `sections` [{ `heading`, `text` }], `tryTonight`, `visual` (which tool or view shows it) |
| `guides.json` | `title`, `forGear` (reflector, refractor, binoculars or any), `minutes`, `needs`, `steps`, `mistakes` |
| `quiz.json` | `level` (kids, beginner or keen), `topic`, `q`, `options` (4), `answer` (index), `explain` |
| `lessons.json` | `title`, `level`, `season`, `minutes`, `steps` [{ `type`: read, find (with `target`), question or outside, … }] |
| `stories.json` | `title`, `hook` (one sentence, for the home page), `story` (about 120 words), `distanceLy`, `distanceNote`, `discovered`, `seeIt`, `lightLeft` (what was happening on Earth when the light set off) |
| `history.json` | `month`, `day`, `year`, `text`, `topic` (discovery, mission, eclipse…): "on this night in history" (no `id`) |
| `bingo.json` | `season`, `title`, `squares` [{ `id`, `label`, `hint`, `difficulty` (1–3), `needs` (eyes, binoculars or telescope) }] |
| `teachers.json` | `keyStage` (2 or 3), `year`, `title`, `curriculum` (the National Curriculum statements it covers), `objectives`, `minutes`, `activities`, `worksheet`, `safety` |
| `asterisms.json` | `name`, `stars` [{ `name`, `bayer`, `hip` }], `lines` (pairs of indexes into `stars`), `season`, `howToFind` |
| `moon-notes.json` | `name` (matching `moon-features.json` where it's a named feature), `note`, `best` (when in the Moon's month), `difficulty` (1–3), `lunar100` (its number in Sky & Telescope's Lunar 100, if any), `lat`/`lon` (where it is, east positive) and `diam` (km, or null): added by scripts/build-moon-notes.js, none for Earthshine: no `id` |
