diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 11a76df..582a705 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -47,7 +47,8 @@
"Bash(mv freighter-science1.png freighter12.png)",
"Bash(sort -t'\\(' -k2 -n)",
"Bash(grep -o 'scale\\(3.50\\).\\\\{0,200\\\\}')",
- "Bash(python3 -c ':*)"
+ "Bash(python3 -c ':*)",
+ "Bash(grep -r \"margin.*-\\\\|negative\" tasks/*/CLAUDE.md)"
]
}
}
diff --git a/assets/items/asteroid-shapes/asteroid1.png b/assets/items/asteroid-shapes/asteroid1.png
new file mode 100644
index 0000000..422e67e
Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid1.png differ
diff --git a/assets/items/asteroid-shapes/asteroid10.png b/assets/items/asteroid-shapes/asteroid10.png
new file mode 100644
index 0000000..61363e0
Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid10.png differ
diff --git a/assets/items/asteroid-shapes/asteroid11.png b/assets/items/asteroid-shapes/asteroid11.png
new file mode 100644
index 0000000..90c643c
Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid11.png differ
diff --git a/assets/items/asteroid-shapes/asteroid12.png b/assets/items/asteroid-shapes/asteroid12.png
new file mode 100644
index 0000000..f47083c
Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid12.png differ
diff --git a/assets/items/asteroid-shapes/asteroid13.png b/assets/items/asteroid-shapes/asteroid13.png
new file mode 100644
index 0000000..e12d869
Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid13.png differ
diff --git a/assets/items/asteroid-shapes/asteroid14.png b/assets/items/asteroid-shapes/asteroid14.png
new file mode 100644
index 0000000..0f944d4
Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid14.png differ
diff --git a/assets/items/asteroid-shapes/asteroid15.png b/assets/items/asteroid-shapes/asteroid15.png
new file mode 100644
index 0000000..bc0db6f
Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid15.png differ
diff --git a/assets/items/asteroid-shapes/asteroid16.png b/assets/items/asteroid-shapes/asteroid16.png
new file mode 100644
index 0000000..3e68e48
Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid16.png differ
diff --git a/assets/items/asteroid-shapes/asteroid2.png b/assets/items/asteroid-shapes/asteroid2.png
new file mode 100644
index 0000000..5aa4077
Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid2.png differ
diff --git a/assets/items/asteroid-shapes/asteroid3.png b/assets/items/asteroid-shapes/asteroid3.png
new file mode 100644
index 0000000..601d454
Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid3.png differ
diff --git a/assets/items/asteroid-shapes/asteroid4.png b/assets/items/asteroid-shapes/asteroid4.png
new file mode 100644
index 0000000..0fd861f
Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid4.png differ
diff --git a/assets/items/asteroid-shapes/asteroid5.png b/assets/items/asteroid-shapes/asteroid5.png
new file mode 100644
index 0000000..fd74186
Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid5.png differ
diff --git a/assets/items/asteroid-shapes/asteroid6.png b/assets/items/asteroid-shapes/asteroid6.png
new file mode 100644
index 0000000..8da4c7e
Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid6.png differ
diff --git a/assets/items/asteroid-shapes/asteroid7.png b/assets/items/asteroid-shapes/asteroid7.png
new file mode 100644
index 0000000..5beaaec
Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid7.png differ
diff --git a/assets/items/asteroid-shapes/asteroid8.png b/assets/items/asteroid-shapes/asteroid8.png
new file mode 100644
index 0000000..2cd4573
Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid8.png differ
diff --git a/assets/items/asteroid-shapes/asteroid9.png b/assets/items/asteroid-shapes/asteroid9.png
new file mode 100644
index 0000000..a0fa24d
Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid9.png differ
diff --git a/package.json b/package.json
index fa40578..c660371 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,8 @@
"pdf": "node src/scripts/generate-pdf.mjs",
"dev": "concurrently \"pnpm build:css:watch\" \"pnpm preview\"",
"split-sprites": "node src/scripts/split-sprites.mjs",
- "remove-bg": "node src/scripts/remove-bg.mjs"
+ "remove-bg": "node src/scripts/remove-bg.mjs",
+ "create-shapes": "node src/scripts/create-asteroid-shapes.mjs"
},
"keywords": [],
"author": "",
diff --git a/src/scripts/create-asteroid-shapes.mjs b/src/scripts/create-asteroid-shapes.mjs
new file mode 100644
index 0000000..5f12193
--- /dev/null
+++ b/src/scripts/create-asteroid-shapes.mjs
@@ -0,0 +1,159 @@
+import sharp from 'sharp';
+import { resolve, basename, join } from 'path';
+import { existsSync, mkdirSync, readdirSync } from 'fs';
+import { fileURLToPath } from 'url';
+
+const PROJECT_ROOT = resolve(fileURLToPath(import.meta.url), '../../..');
+const DEFAULT_INPUT = resolve(PROJECT_ROOT, 'assets/icons/pack3-asteroids');
+const DEFAULT_OUTPUT = resolve(PROJECT_ROOT, 'assets/items/asteroid-shapes');
+
+// Tunable constants
+const OUTLINE_WIDTH = 5; // px thickness of boundary outline
+const OUTLINE_COLOR = 40; // darkness of outline (0=black, 255=white)
+const EDGE_THRESHOLD = 30; // min gradient magnitude to count as an edge
+const EDGE_DARKNESS = 180; // how dark internal edge lines are (0=black, 255=white)
+const ALPHA_THRESHOLD = 128; // min alpha to consider a pixel "opaque"
+
+async function createAsteroidShape(filePath, outputDir) {
+ // Step 1: Read source raw RGBA
+ const { data: srcData, info } = await sharp(filePath)
+ .ensureAlpha()
+ .raw()
+ .toBuffer({ resolveWithObject: true });
+ const { width, height } = info;
+
+ // Step 2: Gradient-based edge detection on greyscale
+ const greyResult = await sharp(filePath)
+ .greyscale()
+ .blur(1.0)
+ .raw()
+ .toBuffer({ resolveWithObject: true });
+ const grey = greyResult.data;
+
+ // Compute Sobel gradient magnitude per pixel
+ const gradients = new Float32Array(width * height);
+ for (let y = 1; y < height - 1; y++) {
+ for (let x = 1; x < width - 1; x++) {
+ const idx = y * width + x;
+ // Only compute for opaque pixels
+ if (srcData[idx * 4 + 3] < ALPHA_THRESHOLD) continue;
+ // Sobel X
+ const gx =
+ -grey[(y - 1) * width + (x - 1)] + grey[(y - 1) * width + (x + 1)]
+ - 2 * grey[y * width + (x - 1)] + 2 * grey[y * width + (x + 1)]
+ - grey[(y + 1) * width + (x - 1)] + grey[(y + 1) * width + (x + 1)];
+ // Sobel Y
+ const gy =
+ -grey[(y - 1) * width + (x - 1)] - 2 * grey[(y - 1) * width + x] - grey[(y - 1) * width + (x + 1)]
+ + grey[(y + 1) * width + (x - 1)] + 2 * grey[(y + 1) * width + x] + grey[(y + 1) * width + (x + 1)];
+ gradients[idx] = Math.sqrt(gx * gx + gy * gy);
+ }
+ }
+
+ // Build base layer: white with faint gray edge lines where gradient is strong
+ const baseLayer = Buffer.alloc(width * height * 4);
+ for (let i = 0; i < width * height; i++) {
+ const alpha = srcData[i * 4 + 3];
+ if (alpha < ALPHA_THRESHOLD) {
+ // Transparent pixel — keep transparent
+ baseLayer[i * 4 + 3] = 0;
+ continue;
+ }
+ const grad = gradients[i];
+ let v = 255; // default white
+ if (grad > EDGE_THRESHOLD) {
+ // Map gradient above threshold to gray line darkness
+ const intensity = Math.min(1, (grad - EDGE_THRESHOLD) / 200);
+ v = 255 - Math.round(intensity * (255 - EDGE_DARKNESS));
+ }
+ baseLayer[i * 4 + 0] = v;
+ baseLayer[i * 4 + 1] = v;
+ baseLayer[i * 4 + 2] = v;
+ baseLayer[i * 4 + 3] = alpha;
+ }
+
+ // Step 3: Outline layer — boundary detection via neighbor check
+ const outlineLayer = Buffer.alloc(width * height * 4, 0);
+
+ for (let y = 0; y < height; y++) {
+ for (let x = 0; x < width; x++) {
+ const idx = y * width + x;
+ if (srcData[idx * 4 + 3] < ALPHA_THRESHOLD) continue;
+
+ let isBorder = false;
+ outer:
+ for (let dy = -OUTLINE_WIDTH; dy <= OUTLINE_WIDTH; dy++) {
+ for (let dx = -OUTLINE_WIDTH; dx <= OUTLINE_WIDTH; dx++) {
+ if (dx === 0 && dy === 0) continue;
+ const nx = x + dx;
+ const ny = y + dy;
+ if (nx < 0 || nx >= width || ny < 0 || ny >= height) {
+ isBorder = true;
+ break outer;
+ }
+ const nIdx = ny * width + nx;
+ if (srcData[nIdx * 4 + 3] < ALPHA_THRESHOLD) {
+ isBorder = true;
+ break outer;
+ }
+ }
+ }
+
+ if (isBorder) {
+ outlineLayer[idx * 4 + 0] = OUTLINE_COLOR;
+ outlineLayer[idx * 4 + 1] = OUTLINE_COLOR;
+ outlineLayer[idx * 4 + 2] = OUTLINE_COLOR;
+ outlineLayer[idx * 4 + 3] = 255;
+ }
+ }
+ }
+
+ // Step 4: Composite — base (white + faint edges) + outline on top
+ const raw = { width, height, channels: 4 };
+
+ await sharp(baseLayer, { raw })
+ .composite([
+ {
+ input: outlineLayer,
+ raw,
+ blend: 'over',
+ },
+ ])
+ .png()
+ .toFile(join(outputDir, basename(filePath)));
+}
+
+async function main() {
+ const inputDir = resolve(process.argv[2] || DEFAULT_INPUT);
+ const outputDir = resolve(process.argv[3] || DEFAULT_OUTPUT);
+
+ if (!existsSync(inputDir)) {
+ console.error(`Not found: ${inputDir}`);
+ process.exit(1);
+ }
+
+ mkdirSync(outputDir, { recursive: true });
+
+ const files = readdirSync(inputDir)
+ .filter(f => /\.png$/i.test(f))
+ .sort()
+ .map(f => join(inputDir, f));
+
+ if (files.length === 0) {
+ console.error(`No PNG files found in: ${inputDir}`);
+ process.exit(1);
+ }
+
+ console.log(`Creating ${files.length} asteroid shape(s)...`);
+
+ await Promise.all(
+ files.map(async (filePath) => {
+ await createAsteroidShape(filePath, outputDir);
+ console.log(` ${basename(filePath)} -> done`);
+ })
+ );
+
+ console.log(`Done. ${files.length} shapes saved to ${outputDir}`);
+}
+
+main();
diff --git a/tasks/cargo-filling/CLAUDE.md b/tasks/cargo-filling/CLAUDE.md
new file mode 100644
index 0000000..12b01f6
--- /dev/null
+++ b/tasks/cargo-filling/CLAUDE.md
@@ -0,0 +1,124 @@
+# Cargo Filling Task Type
+
+Split an asteroid to fill a spaceship cargo bay to exactly 10. Combines asteroid splitting with cargo bay loading. Teaches addition crossing the tens boundary.
+
+## Math Concept
+
+Given:
+- Cargo bay capacity: always **10**
+- Asteroid A already in cargo (1-9)
+- Asteroid B floating in space, B > (10 - A), so it doesn't fit whole
+- Child splits B = N + M where A + N = 10 (N fills remaining space)
+- Full expression: `A + B = (A + N) + M = C`
+- Under `(A + N)` a curly brace with "10"
+
+Answer is always: N = 10 - A, M = B - N, C = A + B.
+
+## Layout
+
+- **Page:** A4 (210mm x 297mm), white background
+- **Header:** Hero splitter image + title + subtitle
+- **Footer:** Cabin interior (28mm, `object-cover`)
+- **Content:** 6 task cards per page in 2 columns x 3 rows grid
+- **Dividers:** Dark solid lines between cards (`border-bottom: 1.5px solid #334155`, odd cards have `border-right`)
+- **Card overflow:** `hidden` — ships get cropped at card boundary
+
+## Card Layout (tuned values)
+
+Each card has two zones:
+- **Visual area** (flex: 1, relative) — asteroid, ship, arrows, remnant
+- **Formula area** (flex-shrink: 0, bottom) — label + formula + brace
+
+### Element positions & sizes (CSS)
+
+| Element | Position | Size |
+|---------|----------|------|
+| `.space-asteroid` | `left: -7.5mm; top: 18.5mm` | `28mm × 28mm` |
+| `.cargo-ship` | `right: -12.5mm; top: -5mm` | `90mm × 42mm` |
+| `.badge-10` | `left: 68%; top: 39%` (of ship) | `22px × 22px` |
+| `.inner-ast` | `left: 86%; top: 73%` (of ship) | `14mm × 14mm` |
+| `.remnant-shape` | `left: 61%; bottom: 3mm` | `16mm × 16mm` shape |
+
+### SVG arrows (viewBox `0 0 100 55`)
+
+Both arrows start from asteroid center and curve outward:
+- **Arrow to ship:** `M7,30 Q30,15 52,25`
+- **Arrow to remnant:** `M7,30 Q24,57 61,47`
+
+Stroke: `#6366f1`, width `0.5`, marker-end arrowhead.
+
+### Visual elements
+
+- **Big asteroid:** `pack3-asteroids/asteroidN.png` — colored, with white number B overlay (1.8rem)
+- **Ship:** `pack4-cargobay/cargo-bayN.png` — flipped with `scaleX(-1)` so nose points right
+- **Inner asteroid:** `items/asteroid-shapes/asteroidN.png` — line-art outline, white number A (1.1rem)
+- **Remnant:** `items/asteroid-shapes/asteroidN.png` — line-art outline (opacity 0.5), blank underline, "?" label
+- **Badge "10":** indigo circle (`#4f46e5`) with white "10"
+- **Asteroid z-index: 6** (above arrows z-index: 4)
+
+### Formula
+
+```
+запиши формулу для пилота
+A + B = (A + __) + __ = __
+ ⏟
+ 10
+```
+
+- Label: uppercase, `#818cf8`, 0.45rem
+- Formula: single line, 0.85rem bold `#1e1b4b`
+- Brace: SVG path (width 90% of brace-group), margin-top 1.5mm
+- "10": `#6366f1`, 0.6rem bold
+- Brace gap from formula: 2mm (configurable via `braceGapMm`)
+
+## Asset Conventions
+
+- **Cargo bays:** `assets/icons/pack4-cargobay/cargo-bay{1-9}.png` — 6 unique per page
+- **Asteroids (colored):** `assets/icons/pack3-asteroids/asteroid{1-16}.png` — for big asteroid in space
+- **Asteroids (outline):** `assets/items/asteroid-shapes/asteroid{1-16}.png` — for inner asteroid & remnant
+- **Hero images:** `assets/hero-images/splitters/splitter{1-9}.png`
+- **Footer images:** `assets/footers/cabin{1-9}.jpeg`
+
+## Problem Range
+
+- A: 2-9 (value already in cargo)
+- B: must be > (10-A) and ≤ 9
+- Practical combinations:
+ - A=8,9: B from 3-9 (easiest — small split)
+ - A=5,6,7: B from 4-9 (medium)
+ - A=2,3,4: B from 7-9 (hardest — large split)
+
+## Difficulty Progression
+
+- Page 1: A=8,9 with smaller B values (easy)
+- Page 2: A=5,6,7 (medium)
+- Page 3: A=2,3,4 with larger B (hard)
+
+## Color Palette
+
+Same as project-wide indigo theme:
+- Title: `text-indigo-950`
+- Subtitle: `text-indigo-400`
+- Number overlay (big asteroid): white with text-shadow
+- Number overlay (inner asteroid): white with text-shadow
+- Cargo "10" badge: `bg-indigo-600` circle with white text
+- Formula text: `text-indigo-950`
+- Input blanks: `border-indigo-300` underlines
+- Brace + "10" label: `#6366f1`
+- Arrows: `#6366f1`
+
+## Title & Subtitle
+
+- **Title:** "Заполни Трюм"
+- **Subtitle:** "На какие части нужно расщепить астероид, чтобы заполнить свободное место в трюме? Какой кусочек останется в космосе? Каков общий объём?"
+
+## Scripts
+
+- `scripts/generate.mjs` — template + data → output pipeline (standard, no transforms yet)
+
+## Editor
+
+- `editor-temp.html` — temporary drag-and-drop tuner for card element positions/sizes
+- Loads/saves state from `docs/cargo-filling-1.data.json`
+- Export button outputs JSON with CSS values and SVG arrow paths
+- Convention: edit only card 1, then apply values to all cards in template
diff --git a/tasks/cargo-filling/docs/cargo-filling-1.data.json b/tasks/cargo-filling/docs/cargo-filling-1.data.json
new file mode 100644
index 0000000..11570a7
--- /dev/null
+++ b/tasks/cargo-filling/docs/cargo-filling-1.data.json
@@ -0,0 +1,31 @@
+{
+ "pages": [
+ {
+ "page": 1,
+ "cards": [
+ {
+ "index": 0,
+ "asteroidScale": 0.85,
+ "ship": {
+ "top": -7
+ },
+ "innerAst": {
+ "left": 57,
+ "top": 70
+ }
+ },
+ {
+ "index": 1,
+ "ship": {
+ "right": -26.5,
+ "top": -7
+ },
+ "innerAst": {
+ "left": 40,
+ "top": 68
+ }
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tasks/cargo-filling/docs/cargo-filling-1.md b/tasks/cargo-filling/docs/cargo-filling-1.md
new file mode 100644
index 0000000..844009a
--- /dev/null
+++ b/tasks/cargo-filling/docs/cargo-filling-1.md
@@ -0,0 +1,55 @@
+# Cargo Filling — Document 1
+
+Addition crossing the tens boundary. 3 pages, 6 cards per page, 18 problems total.
+Difficulty progression: easy → medium → hard.
+
+## Page 1 — Easy (A=8,9)
+
+Hero: splitter3 (right), Footer: cabin1
+
+| # | A | B | N=10-A | M=B-N | C=A+B | Asteroid img | Cargo bay img |
+|---|---|---|--------|-------|-------|--------------|---------------|
+| 1 | 8 | 3 | 2 | 1 | 11 | asteroid1 | cargo-bay1 |
+| 2 | 9 | 4 | 1 | 3 | 13 | asteroid2 | cargo-bay2 |
+| 3 | 8 | 5 | 2 | 3 | 13 | asteroid3 | cargo-bay3 |
+| 4 | 9 | 6 | 1 | 5 | 15 | asteroid4 | cargo-bay1 |
+| 5 | 8 | 7 | 2 | 5 | 15 | asteroid5 | cargo-bay2 |
+| 6 | 9 | 8 | 1 | 7 | 17 | asteroid6 | cargo-bay3 |
+
+## Page 2 — Medium (A=5,6,7)
+
+Hero: splitter6 (left), Footer: cabin3
+
+| # | A | B | N=10-A | M=B-N | C=A+B | Asteroid img | Cargo bay img |
+|---|---|---|--------|-------|-------|--------------|---------------|
+| 1 | 7 | 4 | 3 | 1 | 11 | asteroid7 | cargo-bay4 |
+| 2 | 6 | 5 | 4 | 1 | 11 | asteroid8 | cargo-bay5 |
+| 3 | 7 | 6 | 3 | 3 | 13 | asteroid9 | cargo-bay6 |
+| 4 | 5 | 7 | 5 | 2 | 12 | asteroid10 | cargo-bay4 |
+| 5 | 6 | 8 | 4 | 4 | 14 | asteroid11 | cargo-bay5 |
+| 6 | 7 | 9 | 3 | 6 | 16 | asteroid12 | cargo-bay6 |
+
+## Page 3 — Hard (A=2,3,4)
+
+Hero: splitter9 (right), Footer: cabin5
+
+| # | A | B | N=10-A | M=B-N | C=A+B | Asteroid img | Cargo bay img |
+|---|---|---|--------|-------|-------|--------------|---------------|
+| 1 | 4 | 7 | 6 | 1 | 11 | asteroid13 | cargo-bay7 |
+| 2 | 3 | 8 | 7 | 1 | 11 | asteroid14 | cargo-bay8 |
+| 3 | 4 | 8 | 6 | 2 | 12 | asteroid15 | cargo-bay9 |
+| 4 | 3 | 9 | 7 | 2 | 12 | asteroid16 | cargo-bay7 |
+| 5 | 4 | 9 | 6 | 3 | 13 | asteroid1 | cargo-bay8 |
+| 6 | 2 | 9 | 8 | 1 | 11 | asteroid2 | cargo-bay9 |
+
+## Hero Orientation
+
+- Page 1: splitter3, right side (`flex-row-reverse`)
+- Page 2: splitter6, left side (default `flex-row`)
+- Page 3: splitter9, right side (`flex-row-reverse`)
+
+## Footer Images
+
+- Page 1: cabin1.jpeg
+- Page 2: cabin3.jpeg
+- Page 3: cabin5.jpeg
diff --git a/tasks/cargo-filling/docs/cargo-filling-1.template.html b/tasks/cargo-filling/docs/cargo-filling-1.template.html
new file mode 100644
index 0000000..14fbff4
--- /dev/null
+++ b/tasks/cargo-filling/docs/cargo-filling-1.template.html
@@ -0,0 +1,1179 @@
+
+
+
+
+
+
+
+
+
+ Заполни Трюм
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
На какие части нужно расщепить астероид, чтобы заполнить свободное место в трюме? Какой кусочек останется в космосе? Каков общий объём?
+
+
+
+
+
+
+
+
+
+
+
3
+
+
+
+
10
+
+
+
8
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
4
+
+
+
+
10
+
+
+
9
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
5
+
+
+
+
10
+
+
+
8
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
6
+
+
+
+
10
+
+
+
9
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
7
+
+
+
+
10
+
+
+
8
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
8
+
+
+
+
10
+
+
+
9
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
На какие части нужно расщепить астероид, чтобы заполнить свободное место в трюме? Какой кусочек останется в космосе? Каков общий объём?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
На какие части нужно расщепить астероид, чтобы заполнить свободное место в трюме? Какой кусочек останется в космосе? Каков общий объём?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
На какие части нужно расщепить астероид, чтобы заполнить свободное место в трюме? Какой кусочек останется в космосе? Каков общий объём?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
На какие части нужно расщепить астероид, чтобы заполнить свободное место в трюме? Какой кусочек останется в космосе? Каков общий объём?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
На какие части нужно расщепить астероид, чтобы заполнить свободное место в трюме? Какой кусочек останется в космосе? Каков общий объём?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
На какие части нужно расщепить астероид, чтобы заполнить свободное место в трюме? Какой кусочек останется в космосе? Каков общий объём?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
На какие части нужно расщепить астероид, чтобы заполнить свободное место в трюме? Какой кусочек останется в космосе? Каков общий объём?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
На какие части нужно расщепить астероид, чтобы заполнить свободное место в трюме? Какой кусочек останется в космосе? Каков общий объём?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tasks/cargo-filling/editor.html b/tasks/cargo-filling/editor.html
new file mode 100644
index 0000000..6765f63
--- /dev/null
+++ b/tasks/cargo-filling/editor.html
@@ -0,0 +1,560 @@
+
+
+
+
+
+ Cargo Filling Editor
+
+
+
+
+
+
+ Keys: +/- scale/resize • arrows move • 0 reset • Esc deselect
+
+
+
+
+
+
+
+
diff --git a/tasks/cargo-filling/index.html b/tasks/cargo-filling/index.html
new file mode 100644
index 0000000..1bd1b1b
--- /dev/null
+++ b/tasks/cargo-filling/index.html
@@ -0,0 +1,182 @@
+
+
+
+
+
+ Fill the Cargo Bay — Space Math Adventures
+
+
+
+
+
+
+
+
+
← All Categories
+
+
+
+
+
+
+
+
+
+
Cargo Filling 1
+
+ 3 pages
+ Easy → Hard
+ 6 cards per page • Split asteroid to fill cargo bay to 10 • Addition through 10
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tasks/cargo-filling/scripts/generate.mjs b/tasks/cargo-filling/scripts/generate.mjs
new file mode 100644
index 0000000..6f331e0
--- /dev/null
+++ b/tasks/cargo-filling/scripts/generate.mjs
@@ -0,0 +1,192 @@
+#!/usr/bin/env node
+
+/**
+ * Generate output HTML from template + data for cargo-filling documents.
+ *
+ * Usage: node generate.mjs
+ * Example: node generate.mjs cargo-filling-1
+ *
+ * Reads: docs/.template.html
+ * Reads: docs/.data.json (optional)
+ * Writes: docs/.output.html
+ *
+ * data.json format:
+ * {
+ * pages: [{
+ * page: 1,
+ * cards: [{
+ * index: 0,
+ * asteroidScale: 1.15,
+ * ship: { right: -10, top: -3, width: 85, height: 40 },
+ * innerAst: { left: 60, top: 70 }
+ * }]
+ * }]
+ * }
+ *
+ * Transforms:
+ * - asteroidScale: scale on .space-asteroid img, .remnant-shape .shape-container, .inner-ast
+ * - ship: inline right/top/width/height on .cargo-ship
+ * - innerAst: inline left/top on .inner-ast (composed with translate and optional scale)
+ */
+
+import { readFileSync, writeFileSync, existsSync } from 'fs';
+import { join, dirname } from 'path';
+import { fileURLToPath } from 'url';
+import { postGenerate } from '../../../src/scripts/post-generate.mjs';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const docsDir = join(__dirname, '..', 'docs');
+
+const docId = process.argv[2];
+if (!docId) {
+ console.error('Usage: node generate.mjs ');
+ process.exit(1);
+}
+
+const templatePath = join(docsDir, `${docId}.template.html`);
+const dataPath = join(docsDir, `${docId}.data.json`);
+const outputPath = join(docsDir, `${docId}.output.html`);
+
+if (!existsSync(templatePath)) {
+ console.error(`Template not found: ${templatePath}`);
+ process.exit(1);
+}
+
+let html = readFileSync(templatePath, 'utf-8');
+
+if (existsSync(dataPath)) {
+ const data = JSON.parse(readFileSync(dataPath, 'utf-8'));
+ html = applyData(html, data);
+ console.log(`Applied data from ${data.pages?.length || 0} pages`);
+}
+
+writeFileSync(outputPath, html);
+console.log(`Generated: ${outputPath}`);
+await postGenerate(outputPath);
+
+function applyData(html, data) {
+ if (!data.pages) return html;
+
+ // Split HTML into pages by w-[210mm] h-[297mm] divs
+ const pageRegex = /
Fill the Cargo Bay
+Split an asteroid to fill a ship’s cargo bay! The bay holds 10, but there’s already cargo inside. Break the asteroid so one piece fills the gap. Practices addition crossing 10.
+