- You already know how to count and work with whole numbers from 0 to
- 10. On a number line, those values lie on or to the right of zero.
+ You can already add, subtract, multiply, and divide whole numbers using written
+ calculation (columns, carry, and borrow). On a number line, those values lie on or to the
+ right of zero.
Numbers can also lie to the left of zero — we call them negative.
Temperatures below freezing, bank overdrafts, and positions below sea level are everyday examples
of directed numbers.
diff --git a/content/math/properties-of-operations/_index.md b/content/math/properties-of-operations/_index.md
new file mode 100644
index 0000000..45455d3
--- /dev/null
+++ b/content/math/properties-of-operations/_index.md
@@ -0,0 +1,271 @@
++++
+title = "Properties of operations"
+description = "See why order, grouping, and distribution can change how you write a sum or product without changing the result — with interactive area and dot models."
+template = "chapter.html"
+weight = 2
+[extra]
+subject = "math"
+chapter_id = "properties-of-operations"
+strand = "Number & Algebra"
++++
+
+
+
+ You can already read number sentences from basic math notation and combine
+ numbers from counting and numbers. This chapter names three patterns that keep
+ showing up: rules about order, grouping, and
+ splitting a sum before you multiply. They do not replace careful arithmetic —
+ they explain why different written forms can still mean the same amount.
+
+
+
+
Three properties
+
+ - Commutative — swap the order (for + and ×).
+ - Associative — regroup with brackets (for + and ×).
+ - Distributive — multiply each part of a sum (× over +).
+
+
+
+
Same story, three rules
+
+ Imagine three friends. Friend A gets \$2 each day for 3 days and
+ \$2 each day for 5 more days. You can count the money in
+ different ways:
+
+
+ Distribute
+ \(2(3 + 5) = (2 \times 3) + (2 \times 5) = 6 + 10 = 16\)
+
+
+ Commute
+ \(3 + 5 = 5 + 3 = 8\) days in either order before you multiply by \$2 per day in a lump sum.
+
+
+ Associate
+ \((2 + 3) + 5 = 2 + (3 + 5) = 10\) when you group which daily amounts you add first.
+
+
+ Distribute is about multiplying through a bracket.
+ Commute is about order.
+ Associate is about which pair you combine first when there are three or more
+ numbers.
+
+
+
Commutative property
+
+ Commute means to move or swap. For addition and
+ multiplication of whole numbers, changing the order does not change the result.
+
+
+ \[ a + b = b + a \qquad ab = ba \]
+
+
+ Example: \(3 + 5 = 5 + 3 = 8\). Three dots then five dots is the same total as five dots then
+ three dots.
+
+
+ Subtraction and division are not commutative. \(5 - 3 \neq 3 - 5\) and
+ \(6 \div 2 \neq 2 \div 6\). Order matters there.
+
+
+
+
Associative property
+
+ When you add or multiply three or more numbers, brackets tell you which pair to
+ do first. The associative property says different groupings give the same answer
+ (for + and × only).
+
+
+ \[ (a + b) + c = a + (b + c) \qquad (ab)c = a(bc) \]
+
+
+ Example: \((2 + 3) + 5 = 5 + 5 = 10\) and \(2 + (3 + 5) = 2 + 8 = 10\). You may combine
+ \(2 + 3\) first or \(3 + 5\) first.
+
+
+ Again, subtraction and division are not associative. \((10 - 4) - 2 \neq
+ 10 - (4 - 2)\).
+
+
+
+
Distributive property
+
+ The distributive property links multiplication to addition. Multiplying a sum
+ is the same as multiplying each addend and then adding — “distribute” the multiply across the
+ bracket.
+
+
+ \[ a(b + c) = ab + ac \]
+
+
+ Example: \(2(3 + 5) = 2 \times 8 = 16\). Split the width into \(3\) and \(5\): \(2 \times 3 = 6\)
+ and \(2 \times 5 = 10\), then \(6 + 10 = 16\). The area model below shows one tall rectangle
+ split into two parts.
+
+
+ Later, in written calculation, partial products in multiplication use this idea:
+ you multiply by each digit of the bottom number separately, then add the rows.
+
+
+
+
Which property is it?
+
+
+
+
+ | You change… |
+ Property |
+ Example |
+
+
+
+
+ | Order of two numbers |
+ Commutative |
+ \(4 + 7 = 7 + 4\) |
+
+
+ | Brackets with three numbers |
+ Associative |
+ \((1 + 2) + 3 = 1 + (2 + 3)\) |
+
+
+ | Multiply through a sum |
+ Distributive |
+ \(3(2 + 4) = 3 \times 2 + 3 \times 4\) |
+
+
+
+
+
+
What comes next
+
+ In written calculation you will stack digits and use carry and borrow. Knowing
+ these properties helps you check work (e.g. swap order to verify an addition) and understand why
+ multiplication splits into several rows before you add them.
+
+
+
+
+
History
+
+ Long before algebra used letters, merchants and accountants rearranged piles and rows of goods.
+ Indian and Islamic mathematicians in the Middle Ages wrote rules for how sums and products
+ behave. The words commutative, associative, and
+ distributive became standard in European algebra books in the 1800s, when
+ school math settled on one notation for all.
+
+
+ The distributive rule is ancient geometry in disguise: a large rectangle split into two smaller
+ ones has total area equal to the sum of the parts. That picture is still one of the clearest
+ explanations today.
+
+
+
Checkpoints
+
+ -
+ Which property lets you write \(7 + 4\) as \(4 + 7\) without changing the total?
+
+ -
+ Is \((5 \times 2) \times 3\) the same as \(5 \times (2 \times 3)\)? Which property says so?
+
+ -
+ Rewrite \(4(6 + 1)\) as a sum of two products.
+
+ -
+ Why is \(8 - 3\) not commutative? Give two different answers for \(3 - 8\) and \(8 - 3\).
+
+ -
+ A rectangle is 5 units tall and \((2 + 3)\) units wide. How does the distributive property
+ describe its area?
+
+
+
+
Applications
+
Mental math
+
+ Distribute to make friendly numbers: \(7 \times 98 = 7 \times (100 - 2) = 700 - 14 = 686\).
+ Commute to add easier first: \(6 + 47 + 4 = (6 + 4) + 47 = 57\).
+
+
+
+ Buying 3 packs of \((2 + 5)\) items is the same as \(3 \times 2\) items plus \(3 \times 5\)
+ items. Swapping the order you scan barcodes does not change how many items you have (commute on
+ addition when you merge carts).
+
+
Later algebra
+
+ Expanding \(a(b + c)\) in polynomials and factorising common factors both rely on the
+ distributive property. You will meet them again in polynomials: expansion &
+ factorisation.
+
+
+
Question bank
+
+
Easy
+
+
+
+ Fill in the blank using a property: \(9 + 6 = 6 + \underline{\hspace{1.5em}}\).
+
+
+
Answer: \(9\) — commutative property of addition.
+
+
+
+
+ Compute: \(2(4 + 3)\).
+
+
+ Answer: \(14\). Either \(2 \times 7 = 14\), or \(2 \times 4 + 2 \times 3 = 8 +
+ 6 = 14\) (distributive).
+
+
+
+
+
+
+
+
+ Evaluate both sides and compare: \((3 + 4) + 5\) and \(3 + (4 + 5)\).
+
+
+
Answer: Both equal \(12\) — associative property of addition.
+
+
+
+
+ Use distribution to find \(5 \times 102\).
+
+
Answer: \(510\). Steps: \(5 \times (100 + 2) = 500 + 10 = 510\).
+
+
+
+
Difficult
+
+
+
+ Explain why \(12 \div (3 \div 2)\) is not the same as \((12 \div 3) \div 2\).
+
+
+
+ Answer: Left: \(12 \div 1.5 = 8\). Right: \(4 \div 2 = 2\). Division is not
+ associative; grouping changes the meaning.
+
+
+
+
+
+
+ A garden bed is 4 m wide and \((2 + 3)\) m long. Write two ways to find its area and show they
+ match.
+
+
+
+ Answer: \(20\) m². One rectangle: \(4 \times 5 = 20\). Split length:
+ \(4 \times 2 + 4 \times 3 = 8 + 12 = 20\) (distributive).
+
+
+
+
diff --git a/content/math/properties-of-operations/chapter.scss b/content/math/properties-of-operations/chapter.scss
new file mode 100644
index 0000000..2ea54a1
--- /dev/null
+++ b/content/math/properties-of-operations/chapter.scss
@@ -0,0 +1,469 @@
+// Wide interactive demos (match written-calculation breakout)
+.book-prose .math-widget-mount:has(.prop-dist),
+.book-prose .math-widget-mount:has(.prop-comm),
+.book-prose .math-widget-mount:has(.prop-assoc) {
+ max-width: none;
+}
+
+.prop-dist,
+.prop-comm,
+.prop-assoc {
+ width: min(1100px, 92vw);
+ max-width: none;
+}
+
+// --- Distributive area model ---
+.prop-dist__header {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 12px 20px;
+ margin-bottom: 0.5rem;
+}
+
+.prop-dist__title {
+ margin: 0;
+}
+
+.prop-dist__toggle {
+ font: inherit;
+ font-size: 0.8125rem;
+ font-weight: 600;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ padding: 8px 14px;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ background: var(--paper);
+ color: var(--soft-ink);
+ cursor: pointer;
+}
+
+.prop-dist__toggle:hover,
+.prop-dist__toggle:focus-visible {
+ border-color: var(--accent);
+ color: var(--accent);
+}
+
+.prop-dist__formula {
+ margin-bottom: 1.25rem;
+ font-size: 1.05rem;
+}
+
+.prop-dist__formula .katex {
+ font-size: 1.1em;
+}
+
+.prop-dist__var-a {
+ color: #db2777;
+}
+
+.prop-dist__var-b {
+ color: #2563eb;
+}
+
+.prop-dist__var-c {
+ color: #16a34a;
+}
+
+.prop-dist__figure {
+ margin: 0 0 1.5rem;
+}
+
+.prop-dist__model {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: 8px 10px;
+ max-width: 520px;
+ margin: 0 auto;
+ align-items: stretch;
+}
+
+.prop-dist__height {
+ display: flex;
+ align-items: center;
+ grid-row: 1;
+ grid-column: 1;
+}
+
+.prop-dist__dim {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.95rem;
+}
+
+.prop-dist__dim--a {
+ width: 2rem;
+ padding-right: 8px;
+ color: #db2777;
+ border-right: 3px solid #db2777;
+}
+
+.prop-dist__rects {
+ display: flex;
+ grid-row: 1;
+ grid-column: 2;
+ min-height: 100px;
+ border-left: 3px solid #db2777;
+ border-right: 3px solid #db2777;
+}
+
+.prop-dist__rect {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 3rem;
+ padding: 12px 8px;
+ font-size: 0.9rem;
+}
+
+.prop-dist__rect--b {
+ border-top: 3px solid #2563eb;
+ border-bottom: 3px solid #2563eb;
+ background: color-mix(in srgb, #2563eb 10%, var(--card-bg));
+}
+
+.prop-dist__rect--c {
+ border-top: 3px solid #16a34a;
+ border-bottom: 3px solid #16a34a;
+ background: color-mix(in srgb, #16a34a 10%, var(--card-bg));
+}
+
+.prop-dist__area .katex {
+ font-size: 0.95em;
+}
+
+.prop-dist__widths {
+ display: flex;
+ gap: 0;
+ grid-row: 2;
+ grid-column: 2;
+}
+
+.prop-dist__dim--b,
+.prop-dist__dim--c {
+ text-align: center;
+ padding-top: 6px;
+ border-top: 2px solid currentColor;
+}
+
+.prop-dist__dim--b {
+ color: #2563eb;
+}
+
+.prop-dist__dim--c {
+ color: #16a34a;
+}
+
+.prop-dist__label-a {
+ color: #db2777;
+}
+
+.prop-dist__label-b {
+ color: #2563eb;
+}
+
+.prop-dist__label-c {
+ color: #16a34a;
+}
+
+.prop-dist__slider-a {
+ accent-color: #db2777;
+}
+
+.prop-dist__slider-b {
+ accent-color: #2563eb;
+}
+
+.prop-dist__slider-c {
+ accent-color: #16a34a;
+}
+
+.prop-dist__controls {
+ display: grid;
+ gap: 1rem;
+}
+
+@media (min-width: 640px) {
+ .prop-dist__controls {
+ grid-template-columns: repeat(3, 1fr);
+ }
+}
+
+// --- Commutative ---
+.prop-comm__mode,
+.prop-assoc__mode {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-bottom: 1rem;
+}
+
+.prop-comm__mode-btn,
+.prop-assoc__mode-btn {
+ font: inherit;
+ font-size: 0.875rem;
+ font-weight: 600;
+ padding: 8px 16px;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ background: var(--paper);
+ color: var(--soft-ink);
+ cursor: pointer;
+}
+
+.prop-comm__mode-btn--active,
+.prop-assoc__mode-btn--active {
+ border-color: var(--accent);
+ background: color-mix(in srgb, var(--accent) 12%, var(--paper));
+ color: var(--ink);
+}
+
+.prop-comm__compare {
+ margin: 0 0 1rem;
+ font-size: 0.95rem;
+ color: var(--soft-ink);
+}
+
+.prop-comm__compare .katex {
+ font-size: 1em;
+}
+
+.prop-comm__stage {
+ display: flex;
+ justify-content: center;
+ margin: 1rem 0;
+ min-height: 100px;
+}
+
+.prop-comm__add-piles {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: center;
+ gap: 12px 16px;
+}
+
+.prop-comm__pile {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 6px;
+}
+
+.prop-comm__pile-label {
+ font-family: var(--mono);
+ font-size: 0.75rem;
+ font-weight: 700;
+ color: var(--muted);
+}
+
+.prop-comm__row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 5px;
+ justify-content: center;
+ max-width: 120px;
+}
+
+.prop-comm__dot {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: #0f766e;
+}
+
+.prop-comm__pile-dots--a .prop-comm__dot {
+ background: #2563eb;
+}
+
+.prop-comm__pile-dots--b .prop-comm__dot {
+ background: #c2410c;
+}
+
+.prop-comm__op {
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--muted);
+}
+
+.prop-comm__merged {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ justify-content: center;
+ max-width: 200px;
+}
+
+.prop-comm__grid {
+ display: grid;
+ gap: 5px;
+ padding: 12px;
+ border-radius: 10px;
+ border: 1px solid var(--line);
+ background: color-mix(in srgb, var(--accent) 6%, var(--card-bg));
+}
+
+.prop-comm__grid-dot {
+ width: 14px;
+ height: 14px;
+ border-radius: 4px;
+ background: var(--accent);
+}
+
+.prop-comm__swap,
+.prop-assoc__toggle {
+ display: block;
+ margin: 0 auto;
+ font: inherit;
+ font-size: 0.875rem;
+ font-weight: 600;
+ padding: 10px 18px;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ background: var(--paper);
+ color: var(--ink);
+ cursor: pointer;
+}
+
+.prop-comm__swap:hover,
+.prop-comm__swap:focus-visible,
+.prop-assoc__toggle:hover,
+.prop-assoc__toggle:focus-visible {
+ border-color: var(--accent);
+ color: var(--accent);
+}
+
+// --- Associative ---
+.prop-assoc__formulas {
+ margin-bottom: 1rem;
+}
+
+.prop-assoc__formulas .math-widget__formula {
+ margin: 0 0 0.35rem;
+}
+
+.prop-assoc__alt {
+ margin: 0;
+ font-size: 0.9rem;
+ color: var(--muted);
+}
+
+.prop-assoc__stage {
+ display: flex;
+ justify-content: center;
+ margin: 1.25rem 0;
+ overflow-x: auto;
+}
+
+.prop-assoc__group {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: center;
+ gap: 10px 12px;
+ padding: 16px;
+ border-radius: 12px;
+ border: 1px dashed var(--line);
+ background: color-mix(in srgb, var(--paper) 50%, var(--card-bg));
+}
+
+.prop-assoc__bracket {
+ font-size: 2rem;
+ font-weight: 700;
+ line-height: 1;
+ color: var(--accent);
+}
+
+.prop-assoc__op,
+.prop-assoc__eq {
+ font-size: 1.35rem;
+ font-weight: 700;
+ color: var(--muted);
+}
+
+.prop-assoc__block {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ padding: 8px 10px;
+ border-radius: 8px;
+ max-width: 100px;
+}
+
+.prop-assoc__block--x {
+ background: color-mix(in srgb, #2563eb 14%, transparent);
+ border: 1px solid color-mix(in srgb, #2563eb 35%, transparent);
+}
+
+.prop-assoc__block--y {
+ background: color-mix(in srgb, #c2410c 14%, transparent);
+ border: 1px solid color-mix(in srgb, #c2410c 35%, transparent);
+}
+
+.prop-assoc__block--z {
+ background: color-mix(in srgb, #16a34a 14%, transparent);
+ border: 1px solid color-mix(in srgb, #16a34a 35%, transparent);
+}
+
+.prop-assoc__dot {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ background: currentColor;
+}
+
+.prop-assoc__block--x .prop-assoc__dot {
+ color: #2563eb;
+ background: #2563eb;
+}
+
+.prop-assoc__block--y .prop-assoc__dot {
+ background: #c2410c;
+}
+
+.prop-assoc__block--z .prop-assoc__dot {
+ background: #16a34a;
+}
+
+.prop-assoc__result {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ max-width: 140px;
+ padding: 8px;
+ border-radius: 8px;
+ border: 1px solid var(--line);
+}
+
+.prop-assoc__dot--result {
+ background: var(--accent);
+}
+
+.prop-assoc__many {
+ font-size: 0.75rem;
+ color: var(--muted);
+ align-self: center;
+}
+
+.prop-example {
+ margin: 1rem 0;
+ padding: 14px 18px;
+ border-left: 3px solid var(--accent);
+ background: color-mix(in srgb, var(--accent) 8%, var(--card-bg));
+ border-radius: 0 10px 10px 0;
+ font-size: 0.95rem;
+}
+
+.prop-example__label {
+ display: block;
+ font-family: var(--mono);
+ font-size: 0.68rem;
+ font-weight: 700;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ color: var(--accent);
+ margin-bottom: 6px;
+}
diff --git a/content/math/properties-of-operations/core.html b/content/math/properties-of-operations/core.html
new file mode 100644
index 0000000..c28a325
--- /dev/null
+++ b/content/math/properties-of-operations/core.html
@@ -0,0 +1,137 @@
+
+
+ You can already read number sentences from basic math notation and combine
+ numbers from counting and numbers. This chapter names three patterns that keep
+ showing up: rules about order, grouping, and
+ splitting a sum before you multiply. They do not replace careful arithmetic —
+ they explain why different written forms can still mean the same amount.
+
+
+
+
Three properties
+
+ - Commutative — swap the order (for + and ×).
+ - Associative — regroup with brackets (for + and ×).
+ - Distributive — multiply each part of a sum (× over +).
+
+
+
+
Same story, three rules
+
+ Imagine three friends. Friend A gets \$2 each day for 3 days and
+ \$2 each day for 5 more days. You can count the money in
+ different ways:
+
+
+ Distribute
+ \(2(3 + 5) = (2 \times 3) + (2 \times 5) = 6 + 10 = 16\)
+
+
+ Commute
+ \(3 + 5 = 5 + 3 = 8\) days in either order before you multiply by \$2 per day in a lump sum.
+
+
+ Associate
+ \((2 + 3) + 5 = 2 + (3 + 5) = 10\) when you group which daily amounts you add first.
+
+
+ Distribute is about multiplying through a bracket.
+ Commute is about order.
+ Associate is about which pair you combine first when there are three or more
+ numbers.
+
+
+
Commutative property
+
+ Commute means to move or swap. For addition and
+ multiplication of whole numbers, changing the order does not change the result.
+
+
+ \[ a + b = b + a \qquad ab = ba \]
+
+
+ Example: \(3 + 5 = 5 + 3 = 8\). Three dots then five dots is the same total as five dots then
+ three dots.
+
+
+ Subtraction and division are not commutative. \(5 - 3 \neq 3 - 5\) and
+ \(6 \div 2 \neq 2 \div 6\). Order matters there.
+
+
+
+
Associative property
+
+ When you add or multiply three or more numbers, brackets tell you which pair to
+ do first. The associative property says different groupings give the same answer
+ (for + and × only).
+
+
+ \[ (a + b) + c = a + (b + c) \qquad (ab)c = a(bc) \]
+
+
+ Example: \((2 + 3) + 5 = 5 + 5 = 10\) and \(2 + (3 + 5) = 2 + 8 = 10\). You may combine
+ \(2 + 3\) first or \(3 + 5\) first.
+
+
+ Again, subtraction and division are not associative. \((10 - 4) - 2 \neq
+ 10 - (4 - 2)\).
+
+
+
+
Distributive property
+
+ The distributive property links multiplication to addition. Multiplying a sum
+ is the same as multiplying each addend and then adding — “distribute” the multiply across the
+ bracket.
+
+
+ \[ a(b + c) = ab + ac \]
+
+
+ Example: \(2(3 + 5) = 2 \times 8 = 16\). Split the width into \(3\) and \(5\): \(2 \times 3 = 6\)
+ and \(2 \times 5 = 10\), then \(6 + 10 = 16\). The area model below shows one tall rectangle
+ split into two parts.
+
+
+ Later, in written calculation, partial products in multiplication use this idea:
+ you multiply by each digit of the bottom number separately, then add the rows.
+
+
+
+
Which property is it?
+
+
+
+
+ | You change… |
+ Property |
+ Example |
+
+
+
+
+ | Order of two numbers |
+ Commutative |
+ \(4 + 7 = 7 + 4\) |
+
+
+ | Brackets with three numbers |
+ Associative |
+ \((1 + 2) + 3 = 1 + (2 + 3)\) |
+
+
+ | Multiply through a sum |
+ Distributive |
+ \(3(2 + 4) = 3 \times 2 + 3 \times 4\) |
+
+
+
+
+
+
What comes next
+
+ In written calculation you will stack digits and use carry and borrow. Knowing
+ these properties helps you check work (e.g. swap order to verify an addition) and understand why
+ multiplication splits into several rows before you add them.
+
+
diff --git a/content/math/properties-of-operations/supplement.html b/content/math/properties-of-operations/supplement.html
new file mode 100644
index 0000000..04a49d1
--- /dev/null
+++ b/content/math/properties-of-operations/supplement.html
@@ -0,0 +1,122 @@
+
+
History
+
+ Long before algebra used letters, merchants and accountants rearranged piles and rows of goods.
+ Indian and Islamic mathematicians in the Middle Ages wrote rules for how sums and products
+ behave. The words commutative, associative, and
+ distributive became standard in European algebra books in the 1800s, when
+ school math settled on one notation for all.
+
+
+ The distributive rule is ancient geometry in disguise: a large rectangle split into two smaller
+ ones has total area equal to the sum of the parts. That picture is still one of the clearest
+ explanations today.
+
+
+
Checkpoints
+
+ -
+ Which property lets you write \(7 + 4\) as \(4 + 7\) without changing the total?
+
+ -
+ Is \((5 \times 2) \times 3\) the same as \(5 \times (2 \times 3)\)? Which property says so?
+
+ -
+ Rewrite \(4(6 + 1)\) as a sum of two products.
+
+ -
+ Why is \(8 - 3\) not commutative? Give two different answers for \(3 - 8\) and \(8 - 3\).
+
+ -
+ A rectangle is 5 units tall and \((2 + 3)\) units wide. How does the distributive property
+ describe its area?
+
+
+
+
Applications
+
Mental math
+
+ Distribute to make friendly numbers: \(7 \times 98 = 7 \times (100 - 2) = 700 - 14 = 686\).
+ Commute to add easier first: \(6 + 47 + 4 = (6 + 4) + 47 = 57\).
+
+
+
+ Buying 3 packs of \((2 + 5)\) items is the same as \(3 \times 2\) items plus \(3 \times 5\)
+ items. Swapping the order you scan barcodes does not change how many items you have (commute on
+ addition when you merge carts).
+
+
Later algebra
+
+ Expanding \(a(b + c)\) in polynomials and factorising common factors both rely on the
+ distributive property. You will meet them again in polynomials: expansion &
+ factorisation.
+
+
+
Question bank
+
+
Easy
+
+
+
+ Fill in the blank using a property: \(9 + 6 = 6 + \underline{\hspace{1.5em}}\).
+
+
+
Answer: \(9\) — commutative property of addition.
+
+
+
+
+ Compute: \(2(4 + 3)\).
+
+
+ Answer: \(14\). Either \(2 \times 7 = 14\), or \(2 \times 4 + 2 \times 3 = 8 +
+ 6 = 14\) (distributive).
+
+
+
+
+
+
+
+
+ Evaluate both sides and compare: \((3 + 4) + 5\) and \(3 + (4 + 5)\).
+
+
+
Answer: Both equal \(12\) — associative property of addition.
+
+
+
+
+ Use distribution to find \(5 \times 102\).
+
+
Answer: \(510\). Steps: \(5 \times (100 + 2) = 500 + 10 = 510\).
+
+
+
+
Difficult
+
+
+
+ Explain why \(12 \div (3 \div 2)\) is not the same as \((12 \div 3) \div 2\).
+
+
+
+ Answer: Left: \(12 \div 1.5 = 8\). Right: \(4 \div 2 = 2\). Division is not
+ associative; grouping changes the meaning.
+
+
+
+
+
+
+ A garden bed is 4 m wide and \((2 + 3)\) m long. Write two ways to find its area and show they
+ match.
+
+
+
+ Answer: \(20\) m². One rectangle: \(4 \times 5 = 20\). Split length:
+ \(4 \times 2 + 4 \times 3 = 8 + 12 = 20\) (distributive).
+
+
+
+
diff --git a/content/math/properties-of-operations/widgets/associative-demo.tsx b/content/math/properties-of-operations/widgets/associative-demo.tsx
new file mode 100644
index 0000000..b63454d
--- /dev/null
+++ b/content/math/properties-of-operations/widgets/associative-demo.tsx
@@ -0,0 +1,205 @@
+import katex from "katex";
+import { createMemo, createSignal, For } from "solid-js";
+
+function renderLatex(latex: string) {
+ return katex.renderToString(latex, { throwOnError: false, strict: "ignore" });
+}
+
+function DotBlock(props: { count: number; tone: "x" | "y" | "z" }) {
+ return (
+
+
+ {() => }
+
+
+ );
+}
+
+function AssociativeDemo() {
+ const [mode, setMode] = createSignal<"add" | "mul">("add");
+ const [x, setX] = createSignal(2);
+ const [y, setY] = createSignal(3);
+ const [z, setZ] = createSignal(5);
+ const [groupFirst, setGroupFirst] = createSignal(true);
+
+ const xy = createMemo(() => (mode() === "add" ? x() + y() : x() * y()));
+ const total = createMemo(() => (mode() === "add" ? xy() + z() : xy() * z()));
+
+ const leftGroupHtml = createMemo(() => {
+ if (mode() === "add") {
+ return groupFirst()
+ ? renderLatex(`(${x()}+${y()})+${z()} = ${xy()}+${z()} = ${total()}`)
+ : renderLatex(`${x()}+(${y()}+${z()}) = ${x()}+${y() + z()} = ${total()}`);
+ }
+ return groupFirst()
+ ? renderLatex(`(${x()}\\times${y()})\\times${z()} = ${xy()}\\times${z()} = ${total()}`)
+ : renderLatex(
+ `${x()}\\times(${y()}\\times${z()}) = ${x()}\\times${y() * z()} = ${total()}`
+ );
+ });
+
+ const otherGroupHtml = createMemo(() => {
+ if (mode() === "add") {
+ return groupFirst()
+ ? renderLatex(`${x()}+(${y()}+${z()}) = ${total()}`)
+ : renderLatex(`(${x()}+${y()})+${z()} = ${total()}`);
+ }
+ return groupFirst()
+ ? renderLatex(`${x()}\\times(${y()}\\times${z()}) = ${total()}`)
+ : renderLatex(`(${x()}\\times${y()})\\times${z()} = ${total()}`);
+ });
+
+ return (
+
+ );
+}
+
+export default AssociativeDemo;
diff --git a/content/math/properties-of-operations/widgets/commutative-demo.tsx b/content/math/properties-of-operations/widgets/commutative-demo.tsx
new file mode 100644
index 0000000..711b1e9
--- /dev/null
+++ b/content/math/properties-of-operations/widgets/commutative-demo.tsx
@@ -0,0 +1,170 @@
+import katex from "katex";
+import { createMemo, createSignal, For } from "solid-js";
+
+function renderLatex(latex: string) {
+ return katex.renderToString(latex, { throwOnError: false, strict: "ignore" });
+}
+
+function DotRow(props: { count: number; class?: string }) {
+ return (
+
+
+ {() => }
+
+
+ );
+}
+
+function CommutativeDemo() {
+ const [mode, setMode] = createSignal<"add" | "mul">("add");
+ const [left, setLeft] = createSignal(3);
+ const [right, setRight] = createSignal(5);
+ const [swapped, setSwapped] = createSignal(false);
+
+ const first = createMemo(() => (swapped() ? right() : left()));
+ const second = createMemo(() => (swapped() ? left() : right()));
+ const product = createMemo(() => left() * right());
+
+ const equationHtml = createMemo(() => {
+ if (mode() === "add") {
+ return renderLatex(`${first()} + ${second()} = ${first() + second()}`);
+ }
+ return renderLatex(`${first()} \\times ${second()} = ${product()}`);
+ });
+
+ const compareHtml = createMemo(() => {
+ if (mode() === "add") {
+ return renderLatex(`${left()} + ${right()} = ${right()} + ${left()} = ${left() + right()}`);
+ }
+ return renderLatex(`${left()} \\times ${right()} = ${right()} \\times ${left()} = ${product()}`);
+ });
+
+ return (
+
+ );
+}
+
+export default CommutativeDemo;
diff --git a/content/math/properties-of-operations/widgets/distributive-area-demo.tsx b/content/math/properties-of-operations/widgets/distributive-area-demo.tsx
new file mode 100644
index 0000000..1b0faed
--- /dev/null
+++ b/content/math/properties-of-operations/widgets/distributive-area-demo.tsx
@@ -0,0 +1,158 @@
+import katex from "katex";
+import { createMemo, createSignal } from "solid-js";
+
+function renderLatex(latex: string) {
+ return katex.renderToString(latex, { throwOnError: false, strict: "ignore" });
+}
+
+function DistributiveAreaDemo() {
+ const [a, setA] = createSignal(2);
+ const [b, setB] = createSignal(3);
+ const [c, setC] = createSignal(5);
+ const [showNumbers, setShowNumbers] = createSignal(true);
+
+ const leftArea = createMemo(() => a() * b());
+ const rightArea = createMemo(() => a() * c());
+ const total = createMemo(() => leftArea() + rightArea());
+
+ const formulaHtml = createMemo(() => {
+ if (showNumbers()) {
+ return renderLatex(
+ `${a()}\\times(${b()}+${c()}) = ${a()}\\times${b()} + ${a()}\\times${c()} = ${leftArea()} + ${rightArea()} = ${total()}`
+ );
+ }
+ return renderLatex("a(b+c) = a \\times b + a \\times c");
+ });
+
+ const leftLabel = createMemo(() =>
+ showNumbers() ? `${a()} \\times ${b()} = ${leftArea()}` : "a \\times b"
+ );
+ const rightLabel = createMemo(() =>
+ showNumbers() ? `${a()} \\times ${c()} = ${rightArea()}` : "a \\times c"
+ );
+
+ return (
+
+ );
+}
+
+export default DistributiveAreaDemo;
diff --git a/content/math/written-calculation/_index.md b/content/math/written-calculation/_index.md
new file mode 100644
index 0000000..010bb60
--- /dev/null
+++ b/content/math/written-calculation/_index.md
@@ -0,0 +1,293 @@
++++
+title = "Written calculation"
+description = "Add, subtract, multiply, and divide whole numbers using vertical written form: align columns, regroup, and read the result."
+template = "chapter.html"
+weight = 2
+[extra]
+subject = "math"
+chapter_id = "written-calculation"
+strand = "Number & Algebra"
++++
+
+
+
+ You can read number sentences from basic math notation and use the
+ properties of operations to see why reordering or splitting sums can still give
+ the same answer. This chapter teaches
+ written calculation — how to add, subtract, multiply, and divide on paper by
+ stacking digits in vertical form: one number above another, digit under digit.
+ The ideas work for any size number; the visualizer below works best with about eight digits or
+ fewer per line.
+
+
+
+
Vertical form
+
+ Line up the same places in columns, work from right to left, and write the answer under a horizontal
+ line. Carry and borrow keep each column within a single digit.
+
+
+
+
Why columns?
+
+ In counting and numbers you learned place value: the ones column,
+ tens column, hundreds column. In vertical form, digits that share a place must sit in the same
+ column. If you shift a digit left or right, you are changing its value.
+
+
+ Always line up the ones under the ones, then tens under tens, and so on. Draw a
+ horizontal line under the stack before you write the answer.
+
+
+
Carry and borrow
+
+ In the four operations, a column sometimes gives a digit from 0 to 9 and sometimes
+ gives 10 or more (or too small to subtract). Then you adjust the next column to the
+ left.
+
+
+ -
+ Carry — in addition, when a column sums to 10 or more, you write the ones
+ digit of that sum in the column and pass the extra ten to the next column on the left.
+
+ -
+ Borrow — in subtraction, when the top digit is smaller than the bottom
+ digit, you take one ten from the column on the left, add 10 to the current column, then subtract.
+
+
+
Work from the right (ones) toward the left, one column at a time.
+
+
Carry: 27 + 59 = 86
+
¹
+ 27
++ 59
+────
+ 86
+
+ In the ones column: \(7 + 9 = 16\). Write 6 in the ones place. Carry
+ 1 to the tens column (written small above the tens). In the tens column:
+ \(2 + 5 + 1 = 8\).
+
+
+
Borrow: 47 − 19 = 28
+
−1
+ 47
+− 19
+────
+ 28
+
+ In the ones column: \(7 - 9\) does not work. Borrow 10 from the tens: the 4 becomes
+ 3, and the ones digit becomes 17. Then \(17 - 9 = 8\). In the tens
+ column: \(3 - 1 = 2\).
+
+
+
Addition
+
+ Write the numbers one above the other, plus sign to the left of the bottom number, line underneath,
+ sum below. Add each column from right to left; carry when needed.
+
+
+
Subtraction
+
+ Put the larger number on top. Subtract column by column. When the top digit is too
+ small, borrow from the left (decrease that digit by 1, add 10 to the current column). Example with a
+ zero in the middle: \(503 - 278 = 225\) needs borrowing across the tens column.
+
+
+
Multiplication
+
+ Write the larger number on top (often). Multiply the top number by each digit of the bottom number,
+ one partial product per digit, shifted so ones line up with ones. Add the partial
+ products for the final answer.
+
+
+ 23
+ × 14
+ ────
+ 92
+ 230
+ ────
+ 322
+
Here \(23 \times 4 = 92\) and \(23 \times 10 = 230\); \(92 + 230 = 322\).
+
+
Division
+
+ Long division writes the divisor outside a bracket, the dividend inside, and the
+ quotient on top. At each step: how many times does the divisor fit into the current part of the
+ dividend? Write that digit in the quotient, multiply, subtract, then bring down the next digit. If
+ the division is not exact, the amount left is the remainder.
+
+
+
Names of the parts
+
+
+
+
+ | Operation |
+ Parts |
+ Answer |
+
+
+
+
+ | Addition |
+ addend + addend |
+ sum |
+
+
+ | Subtraction |
+ minuend − subtrahend |
+ difference |
+
+
+ | Multiplication |
+ factor × factor |
+ product |
+
+
+ | Division |
+ dividend ÷ divisor |
+ quotient (and remainder if any) |
+
+
+
+
+
+
Try it yourself
+
+ Use the visualizer below: enter two or more whole numbers (one per line), choose an operation, and
+ click Generate visualization. Step through with Play or the arrow
+ buttons to see each carry, borrow, and column update.
+
+
+
+
+
+
History
+
+ People calculated with tokens, rods, and abacuses long before everyone used pencil and paper. In many
+ places, including China, India, and the Arab world, scribes lined up digits in columns so addition
+ and subtraction could be done digit by digit.
+
+
+ Mechanical calculators (1600s–1900s) had to solve the same puzzle: when one wheel
+ moved from 9 to 0, it had to add 1 to the wheel for the next place. Early machines often did addition
+ first, then ran a separate carry cycle to update higher digits. Later designs passed
+ the carry instantly along the whole machine. That idea appears again inside modern chips: a
+ carry flag records whether the last step overflowed a column.
+
+
+
Derivation
+
Why column addition works
+
+ Split each number by place value. For example,
+ \(27 + 59 = (20 + 7) + (50 + 9) = (20 + 50) + (7 + 9) = 70 + 16 = 86\). The ones column adds
+ \(7 + 9\); the tens column adds \(2 + 5\) plus any carry. Each column only mixes the same place —
+ that is why alignment matters.
+
+
Subtraction and addition
+
+ Subtraction is the inverse of addition: \(c = a - b\) means the same as \(c + b = a\). Borrowing
+ moves one ten from the left so the subtraction in the current column matches that story.
+
+
+
Checkpoints
+
+
Checkpoint
+
When you add 7 and 9 in the ones column, what do you write in the ones place? What do you carry?
+
+
+
Checkpoint
+
In \(47 - 19\), why does the 4 in the tens column become 3 before you subtract the ones?
+
+
+
Checkpoint
+
In \(23 \times 14\), why are there two rows under the line before the final sum?
+
+
+
Real life
+
+ - Shopping — adding prices on a receipt column by column.
+ - Scores — finding the difference between two team totals.
+ - Sharing — dividing a total equally (e.g. \(157\) stickers among \(12\) children: quotient 13, remainder 1).
+
+
+
Question bank — Easy
+
+ Work out \(34 + 25\).
+
+
34
++ 25
+────
+ 59
+
Answer: 59
+
+
+
+ Work out \(61 - 28\).
+
+
Answer: 33 (borrow from tens in the ones column).
+
+
+
+
+
+ Work out \(27 + 59\).
+
+
¹
+ 27
++ 59
+────
+ 86
+
Answer: 86
+
+
+
+ Work out \(503 - 278\).
+
+
Answer: 225
+
Steps: Borrow across the tens (0) from hundreds; ones become 13−8=5; tens 9−7=2; hundreds 4−2=2.
+
+
+
+ Work out \(23 \times 14\).
+
+
+
+
Difficult
+
+ Work out \(156 \div 12\).
+
+
+
+ Work out \(157 \div 12\) and state the remainder.
+
+
Answer: Quotient 13, remainder 1.
+
+
+
+
Hardcore
+
+ A book has 864 pages. You read 379 pages. How many pages are left?
+
+
Answer: \(864 - 379 = 485\) pages.
+
+
+
+ A classroom orders 24 packs of 15 pencils. How many pencils is that?
+
+
Answer: \(24 \times 15 = 360\) pencils.
+
+
+
+
References
+
+ - Wikipedia — Carry (arithmetic) and Subtraction.
+ - Cajori, Florian — A History of Mathematical Notations.
+ - Other school methods (Austrian/additions, partial differences) exist; this chapter uses column borrow as in most HK primary texts.
+
+
diff --git a/content/math/written-calculation/chapter.scss b/content/math/written-calculation/chapter.scss
new file mode 100644
index 0000000..291cf7d
--- /dev/null
+++ b/content/math/written-calculation/chapter.scss
@@ -0,0 +1,371 @@
+// Break out of prose max-width (68ch) to the same width as .container
+.book-prose .math-widget-mount:has(.va-workbench) {
+ max-width: none;
+}
+
+.va-workbench {
+ --va-text: var(--ink);
+ --va-operator: var(--muted);
+ --va-carry: #dc2626;
+ --va-borrow: var(--accent);
+ --va-result: var(--success);
+ --va-line: var(--line);
+ --va-highlight: color-mix(in srgb, var(--accent) 14%, transparent);
+ --va-highlight-border: color-mix(in srgb, var(--accent) 38%, transparent);
+ --va-error: #dc2626;
+ --va-canvas-dot: color-mix(in srgb, var(--line) 90%, transparent);
+ --va-panel-bg: var(--card-bg);
+ --va-panel-border: var(--card-border);
+ --va-stage-bg: var(--paper);
+
+ width: min(1100px, 92vw);
+ max-width: none;
+ margin-top: 1.25rem;
+}
+
+[data-theme="dark"] .va-workbench,
+:root:not([data-theme="light"]) .va-workbench {
+ --va-carry: #f87171;
+ --va-error: #f87171;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root:not([data-theme="light"]) .va-workbench {
+ --va-carry: #f87171;
+ --va-error: #f87171;
+ }
+}
+
+.va-workbench__shell {
+ display: flex;
+ flex-direction: column;
+ min-height: 28rem;
+ border: 1px solid var(--va-panel-border);
+ border-radius: 14px;
+ overflow: hidden;
+ background: var(--va-panel-bg);
+ box-shadow: 0 10px 32px var(--shadow-soft);
+}
+
+@media (min-width: 768px) {
+ .va-workbench__shell {
+ flex-direction: row;
+ min-height: 32rem;
+ }
+}
+
+.va-workbench__sidebar {
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ padding: 1.25rem 1.5rem;
+ border-bottom: 1px solid var(--line);
+ background: var(--va-panel-bg);
+ overflow-y: auto;
+}
+
+@media (min-width: 768px) {
+ .va-workbench__sidebar {
+ width: 18rem;
+ border-bottom: none;
+ border-right: 1px solid var(--line);
+ }
+}
+
+.va-workbench__brand {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ margin-bottom: 0.25rem;
+}
+
+.va-workbench__brand-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 2.5rem;
+ height: 2.5rem;
+ border-radius: 0.75rem;
+ background: var(--accent);
+ color: var(--paper);
+ font-size: 1.25rem;
+ font-weight: 700;
+ box-shadow: 0 4px 12px var(--shadow-soft);
+}
+
+.va-workbench__brand-text {
+ font-size: 1rem;
+ line-height: 1.3;
+ color: var(--ink);
+}
+
+.va-workbench__brand-text strong {
+ color: var(--accent);
+ font-weight: 600;
+}
+
+.va-workbench__field {
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+}
+
+.va-workbench__label {
+ font-size: 0.8125rem;
+ font-weight: 600;
+ color: var(--soft-ink);
+}
+
+.va-workbench__op-grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 0.5rem;
+}
+
+.va-workbench__op-btn {
+ font: inherit;
+ font-size: 1.25rem;
+ font-weight: 700;
+ padding: 0.5rem;
+ border: 1px solid var(--line);
+ border-radius: 0.5rem;
+ background: color-mix(in srgb, var(--paper) 60%, var(--card-bg));
+ color: var(--soft-ink);
+ cursor: pointer;
+ transition:
+ background 0.12s ease,
+ border-color 0.12s ease,
+ color 0.12s ease;
+}
+
+.va-workbench__op-btn:hover {
+ border-color: var(--accent);
+ color: var(--ink);
+}
+
+.va-workbench__op-btn--active {
+ border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
+ background: color-mix(in srgb, var(--accent) 12%, var(--card-bg));
+ color: var(--accent);
+}
+
+.va-workbench__textarea {
+ width: 100%;
+ padding: 0.75rem 1rem;
+ font-family: var(--mono);
+ font-size: 1.0625rem;
+ line-height: 1.4;
+ color: var(--ink);
+ background: color-mix(in srgb, var(--paper) 50%, var(--card-bg));
+ border: 1px solid var(--line);
+ border-radius: 0.5rem;
+ resize: vertical;
+ min-height: 5rem;
+}
+
+.va-workbench__textarea:focus {
+ outline: 2px solid var(--accent);
+ outline-offset: 1px;
+}
+
+.va-workbench__hint,
+.va-workbench__footer-note {
+ font-size: 0.75rem;
+ line-height: 1.45;
+ color: var(--muted);
+ margin: 0;
+}
+
+.va-workbench__generate {
+ font: inherit;
+ font-weight: 600;
+ width: 100%;
+ padding: 0.75rem 1rem;
+ border: none;
+ border-radius: 0.5rem;
+ background: var(--accent);
+ color: var(--paper);
+ cursor: pointer;
+ box-shadow: 0 4px 14px var(--shadow-soft);
+ transition:
+ filter 0.12s ease,
+ transform 0.08s ease;
+}
+
+.va-workbench__generate:hover {
+ filter: brightness(1.06);
+}
+
+.va-workbench__generate:active {
+ transform: scale(0.98);
+}
+
+.va-workbench__anim {
+ padding-top: 1rem;
+ border-top: 1px solid var(--line);
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.va-workbench__transport {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.5rem;
+ padding: 0.35rem;
+ border: 1px solid var(--line);
+ border-radius: 0.5rem;
+ background: color-mix(in srgb, var(--paper) 40%, var(--card-bg));
+}
+
+.va-workbench__transport-btn {
+ font: inherit;
+ font-size: 1.5rem;
+ line-height: 1;
+ padding: 0.35rem 0.65rem;
+ border: none;
+ border-radius: 0.35rem;
+ background: transparent;
+ color: var(--soft-ink);
+ cursor: pointer;
+}
+
+.va-workbench__transport-btn:hover {
+ color: var(--accent);
+ background: var(--card-bg);
+}
+
+.va-workbench__play {
+ font: inherit;
+ font-weight: 600;
+ flex: 1;
+ padding: 0.45rem 1rem;
+ border: none;
+ border-radius: 0.35rem;
+ background: var(--accent);
+ color: var(--paper);
+ cursor: pointer;
+}
+
+.va-workbench__play:hover {
+ filter: brightness(1.06);
+}
+
+.va-workbench__speed {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: 0.75rem;
+ color: var(--muted);
+}
+
+.va-workbench__speed-slider {
+ flex: 1;
+ accent-color: var(--accent);
+}
+
+.va-workbench__footer-note {
+ margin-top: auto;
+ text-align: center;
+}
+
+.va-workbench__stage {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 16rem;
+ background: var(--va-stage-bg);
+ position: relative;
+}
+
+.va-workbench__message-box {
+ position: absolute;
+ top: 1rem;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 2;
+ width: min(92%, 32rem);
+ padding: 0.85rem 1.1rem;
+ text-align: center;
+ border: 1px solid var(--line);
+ border-radius: 0.75rem;
+ background: color-mix(in srgb, var(--card-bg) 92%, transparent);
+ backdrop-filter: blur(8px);
+ box-shadow: 0 8px 24px var(--shadow-soft);
+}
+
+.va-workbench__message-box--error .va-workbench__message {
+ color: var(--va-error);
+}
+
+.va-workbench__message {
+ font-size: 1rem;
+ font-weight: 600;
+ color: var(--ink);
+ margin: 0 0 0.5rem;
+ line-height: 1.4;
+}
+
+.va-workbench__progress-track {
+ width: 100%;
+ height: 0.35rem;
+ border-radius: 999px;
+ background: var(--line);
+ overflow: hidden;
+}
+
+.va-workbench__progress-fill {
+ height: 100%;
+ background: var(--accent);
+ transition: width 0.25s ease;
+}
+
+.va-workbench__step-counter {
+ font-size: 0.75rem;
+ color: var(--muted);
+ margin: 0.35rem 0 0;
+}
+
+.va-workbench__canvas-wrap {
+ flex: 1;
+ position: relative;
+ min-height: 14rem;
+ background-color: var(--va-stage-bg);
+ background-image: radial-gradient(var(--va-canvas-dot) 1px, transparent 1px);
+ background-size: 20px 20px;
+}
+
+.va-workbench__canvas {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ display: block;
+}
+
+.va-static {
+ font-family: var(--mono);
+ font-size: 1.25rem;
+ line-height: 1.35;
+ letter-spacing: 0.08em;
+ margin: 1rem 0;
+ padding: 1rem 1.25rem;
+ color: var(--ink);
+ background: color-mix(in srgb, var(--paper) 50%, var(--card-bg));
+ border: 1px solid var(--line);
+ border-radius: 0.5rem;
+ overflow-x: auto;
+}
+
+.va-static__carry,
+.va-static__borrow {
+ font-size: 0.85em;
+ vertical-align: super;
+}
+
+.book-prose .written-terms-table td:first-child {
+ font-weight: 600;
+ white-space: nowrap;
+}
diff --git a/content/math/written-calculation/core.html b/content/math/written-calculation/core.html
new file mode 100644
index 0000000..55f0704
--- /dev/null
+++ b/content/math/written-calculation/core.html
@@ -0,0 +1,152 @@
+
+
+ You can read number sentences from basic math notation and use the
+ properties of operations to see why reordering or splitting sums can still give
+ the same answer. This chapter teaches
+ written calculation — how to add, subtract, multiply, and divide on paper by
+ stacking digits in vertical form: one number above another, digit under digit.
+ The ideas work for any size number; the visualizer below works best with about eight digits or
+ fewer per line.
+
+
+
+
Vertical form
+
+ Line up the same places in columns, work from right to left, and write the answer under a horizontal
+ line. Carry and borrow keep each column within a single digit.
+
+
+
+
Why columns?
+
+ In counting and numbers you learned place value: the ones column,
+ tens column, hundreds column. In vertical form, digits that share a place must sit in the same
+ column. If you shift a digit left or right, you are changing its value.
+
+
+ Always line up the ones under the ones, then tens under tens, and so on. Draw a
+ horizontal line under the stack before you write the answer.
+
+
+
Carry and borrow
+
+ In the four operations, a column sometimes gives a digit from 0 to 9 and sometimes
+ gives 10 or more (or too small to subtract). Then you adjust the next column to the
+ left.
+
+
+ -
+ Carry — in addition, when a column sums to 10 or more, you write the ones
+ digit of that sum in the column and pass the extra ten to the next column on the left.
+
+ -
+ Borrow — in subtraction, when the top digit is smaller than the bottom
+ digit, you take one ten from the column on the left, add 10 to the current column, then subtract.
+
+
+
Work from the right (ones) toward the left, one column at a time.
+
+
Carry: 27 + 59 = 86
+
¹
+ 27
++ 59
+────
+ 86
+
+ In the ones column: \(7 + 9 = 16\). Write 6 in the ones place. Carry
+ 1 to the tens column (written small above the tens). In the tens column:
+ \(2 + 5 + 1 = 8\).
+
+
+
Borrow: 47 − 19 = 28
+
−1
+ 47
+− 19
+────
+ 28
+
+ In the ones column: \(7 - 9\) does not work. Borrow 10 from the tens: the 4 becomes
+ 3, and the ones digit becomes 17. Then \(17 - 9 = 8\). In the tens
+ column: \(3 - 1 = 2\).
+
+
+
Addition
+
+ Write the numbers one above the other, plus sign to the left of the bottom number, line underneath,
+ sum below. Add each column from right to left; carry when needed.
+
+
+
Subtraction
+
+ Put the larger number on top. Subtract column by column. When the top digit is too
+ small, borrow from the left (decrease that digit by 1, add 10 to the current column). Example with a
+ zero in the middle: \(503 - 278 = 225\) needs borrowing across the tens column.
+
+
+
Multiplication
+
+ Write the larger number on top (often). Multiply the top number by each digit of the bottom number,
+ one partial product per digit, shifted so ones line up with ones. Add the partial
+ products for the final answer.
+
+
+ 23
+ × 14
+ ────
+ 92
+ 230
+ ────
+ 322
+
Here \(23 \times 4 = 92\) and \(23 \times 10 = 230\); \(92 + 230 = 322\).
+
+
Division
+
+ Long division writes the divisor outside a bracket, the dividend inside, and the
+ quotient on top. At each step: how many times does the divisor fit into the current part of the
+ dividend? Write that digit in the quotient, multiply, subtract, then bring down the next digit. If
+ the division is not exact, the amount left is the remainder.
+
+
+
Names of the parts
+
+
+
+
+ | Operation |
+ Parts |
+ Answer |
+
+
+
+
+ | Addition |
+ addend + addend |
+ sum |
+
+
+ | Subtraction |
+ minuend − subtrahend |
+ difference |
+
+
+ | Multiplication |
+ factor × factor |
+ product |
+
+
+ | Division |
+ dividend ÷ divisor |
+ quotient (and remainder if any) |
+
+
+
+
+
+
Try it yourself
+
+ Use the visualizer below: enter two or more whole numbers (one per line), choose an operation, and
+ click Generate visualization. Step through with Play or the arrow
+ buttons to see each carry, borrow, and column update.
+
+
+
diff --git a/content/math/written-calculation/supplement.html b/content/math/written-calculation/supplement.html
new file mode 100644
index 0000000..96656ba
--- /dev/null
+++ b/content/math/written-calculation/supplement.html
@@ -0,0 +1,129 @@
+
+
History
+
+ People calculated with tokens, rods, and abacuses long before everyone used pencil and paper. In many
+ places, including China, India, and the Arab world, scribes lined up digits in columns so addition
+ and subtraction could be done digit by digit.
+
+
+ Mechanical calculators (1600s–1900s) had to solve the same puzzle: when one wheel
+ moved from 9 to 0, it had to add 1 to the wheel for the next place. Early machines often did addition
+ first, then ran a separate carry cycle to update higher digits. Later designs passed
+ the carry instantly along the whole machine. That idea appears again inside modern chips: a
+ carry flag records whether the last step overflowed a column.
+
+
+
Derivation
+
Why column addition works
+
+ Split each number by place value. For example,
+ \(27 + 59 = (20 + 7) + (50 + 9) = (20 + 50) + (7 + 9) = 70 + 16 = 86\). The ones column adds
+ \(7 + 9\); the tens column adds \(2 + 5\) plus any carry. Each column only mixes the same place —
+ that is why alignment matters.
+
+
Subtraction and addition
+
+ Subtraction is the inverse of addition: \(c = a - b\) means the same as \(c + b = a\). Borrowing
+ moves one ten from the left so the subtraction in the current column matches that story.
+
+
+
Checkpoints
+
+
Checkpoint
+
When you add 7 and 9 in the ones column, what do you write in the ones place? What do you carry?
+
+
+
Checkpoint
+
In \(47 - 19\), why does the 4 in the tens column become 3 before you subtract the ones?
+
+
+
Checkpoint
+
In \(23 \times 14\), why are there two rows under the line before the final sum?
+
+
+
Real life
+
+ - Shopping — adding prices on a receipt column by column.
+ - Scores — finding the difference between two team totals.
+ - Sharing — dividing a total equally (e.g. \(157\) stickers among \(12\) children: quotient 13, remainder 1).
+
+
+
Question bank — Easy
+
+ Work out \(34 + 25\).
+
+
34
++ 25
+────
+ 59
+
Answer: 59
+
+
+
+ Work out \(61 - 28\).
+
+
Answer: 33 (borrow from tens in the ones column).
+
+
+
+
+
+ Work out \(27 + 59\).
+
+
¹
+ 27
++ 59
+────
+ 86
+
Answer: 86
+
+
+
+ Work out \(503 - 278\).
+
+
Answer: 225
+
Steps: Borrow across the tens (0) from hundreds; ones become 13−8=5; tens 9−7=2; hundreds 4−2=2.
+
+
+
+ Work out \(23 \times 14\).
+
+
+
+
Difficult
+
+ Work out \(156 \div 12\).
+
+
+
+ Work out \(157 \div 12\) and state the remainder.
+
+
Answer: Quotient 13, remainder 1.
+
+
+
+
Hardcore
+
+ A book has 864 pages. You read 379 pages. How many pages are left?
+
+
Answer: \(864 - 379 = 485\) pages.
+
+
+
+ A classroom orders 24 packs of 15 pencils. How many pencils is that?
+
+
Answer: \(24 \times 15 = 360\) pencils.
+
+
+
+
References
+
+ - Wikipedia — Carry (arithmetic) and Subtraction.
+ - Cajori, Florian — A History of Mathematical Notations.
+ - Other school methods (Austrian/additions, partial differences) exist; this chapter uses column borrow as in most HK primary texts.
+
+
diff --git a/content/math/written-calculation/widgets/lib/math-visualizer.ts b/content/math/written-calculation/widgets/lib/math-visualizer.ts
new file mode 100644
index 0000000..fe40572
--- /dev/null
+++ b/content/math/written-calculation/widgets/lib/math-visualizer.ts
@@ -0,0 +1,841 @@
+export type VisualizerColors = {
+ text: string;
+ operator: string;
+ carry: string;
+ borrow: string;
+ result: string;
+ line: string;
+ highlight: string;
+ highlightBorder: string;
+ error: string;
+};
+
+type CellData = {
+ text?: string;
+ color?: string;
+ crossedOut?: boolean;
+ smallText?: string;
+ smallColor?: string;
+};
+
+type Frame = {
+ cells: Record
;
+ lines: Array<{ y: number; startX: number; endX: number }>;
+ highlights: Array<{ x: number; y: number; w: number; h: number }>;
+ message: string;
+};
+
+export class MathVisualizer {
+ private canvas: HTMLCanvasElement;
+ private ctx: CanvasRenderingContext2D;
+ private wrapper: HTMLElement;
+ private themeRoot: HTMLElement;
+
+ frames: Frame[] = [];
+ currentFrame = 0;
+ isPlaying = false;
+ private playInterval: ReturnType | null = null;
+ speed = 800;
+ operation: "+" | "-" | "*" | "/" = "+";
+
+ private colors: VisualizerColors = {
+ text: "#1e293b",
+ operator: "#64748b",
+ carry: "#ef4444",
+ borrow: "#9a6b2e",
+ result: "#16a34a",
+ line: "#cbd5e1",
+ highlight: "rgba(59, 130, 246, 0.15)",
+ highlightBorder: "rgba(59, 130, 246, 0.4)",
+ error: "#ef4444",
+ };
+
+ onMessage?: (msg: string, isError?: boolean) => void;
+ onStep?: (current: number, total: number) => void;
+ onPlayState?: (playing: boolean) => void;
+
+ constructor(opts: {
+ canvas: HTMLCanvasElement;
+ wrapper: HTMLElement;
+ themeRoot: HTMLElement;
+ }) {
+ this.canvas = opts.canvas;
+ const ctx = opts.canvas.getContext("2d");
+ if (!ctx) throw new Error("Canvas 2D not available");
+ this.ctx = ctx;
+ this.wrapper = opts.wrapper;
+ this.themeRoot = opts.themeRoot;
+ this.refreshColors();
+ }
+
+ refreshColors() {
+ const s = getComputedStyle(this.themeRoot);
+ const get = (name: string, fallback: string) =>
+ s.getPropertyValue(name).trim() || fallback;
+ this.colors = {
+ text: get("--va-text", "#1a1a1a"),
+ operator: get("--va-operator", "#767676"),
+ carry: get("--va-carry", "#dc2626"),
+ borrow: get("--va-borrow", "#9a6b2e"),
+ result: get("--va-result", "#2a7d52"),
+ line: get("--va-line", "#e8e8e8"),
+ highlight: get("--va-highlight", "rgba(154, 107, 46, 0.15)"),
+ highlightBorder: get("--va-highlight-border", "rgba(154, 107, 46, 0.4)"),
+ error: get("--va-error", "#dc2626"),
+ };
+ }
+
+ resizeCanvas() {
+ const rect = this.wrapper.getBoundingClientRect();
+ const dpr = window.devicePixelRatio || 1;
+ this.canvas.width = rect.width * dpr;
+ this.canvas.height = rect.height * dpr;
+ this.ctx.setTransform(1, 0, 0, 1, 0, 0);
+ this.ctx.scale(dpr, dpr);
+ this.canvas.style.width = `${rect.width}px`;
+ this.canvas.style.height = `${rect.height}px`;
+ this.render();
+ }
+
+ setSpeed(ms: number) {
+ this.speed = ms;
+ if (this.isPlaying) {
+ this.pause();
+ this.play();
+ }
+ }
+
+ setOperation(op: "+" | "-" | "*" | "/") {
+ this.operation = op;
+ }
+
+ generateFromInput(raw: string) {
+ const lines = raw
+ .split("\n")
+ .map((s) => s.trim())
+ .filter((s) => /^\d+$/.test(s));
+
+ if (lines.length < 2) {
+ this.showMessage("Please enter at least two numbers.", true);
+ return;
+ }
+
+ this.frames = [];
+ this.currentFrame = 0;
+ this.pause();
+
+ try {
+ if (this.operation === "+") {
+ this.generateAddition(lines);
+ } else {
+ let current = lines[0];
+ const finalStepFrames: Frame[] = [];
+
+ for (let i = 1; i < lines.length; i++) {
+ const next = lines[i];
+
+ if (i > 1) {
+ const transFrame = this.createEmptyFrame();
+ transFrame.message = `Intermediate result: ${current}. Preparing next operation...`;
+ for (let k = 0; k < current.length; k++) {
+ this.setCell(transFrame, k, 1, {
+ text: current[k],
+ color: this.colors.result,
+ });
+ }
+ this.frames.push(transFrame);
+ }
+
+ if (this.operation === "-") {
+ this.generateSubtraction(current, next);
+ const diff = BigInt(current) - BigInt(next);
+ if (diff < 0n) {
+ throw new Error(
+ "Result dropped below zero. Negative visualization not supported."
+ );
+ }
+ current = diff.toString();
+ } else if (this.operation === "*") {
+ this.generateMultiplication(current, next);
+ current = (BigInt(current) * BigInt(next)).toString();
+ } else if (this.operation === "/") {
+ if (BigInt(next) === 0n) throw new Error("Cannot divide by zero.");
+ this.generateDivision(current, next);
+ current = (BigInt(current) / BigInt(next)).toString();
+ }
+
+ finalStepFrames.push(this.cloneGrid(this.frames[this.frames.length - 1]));
+ }
+
+ if (finalStepFrames.length > 1) {
+ this.frames.push(this.mergeFramesToSummary(finalStepFrames));
+ }
+ }
+
+ if (this.frames.length > 0) {
+ this.render();
+ this.play();
+ }
+ } catch (e) {
+ const msg = e instanceof Error ? e.message : String(e);
+ this.showMessage(`Error: ${msg}`, true);
+ }
+ }
+
+ private showMessage(msg: string, isError = false) {
+ this.onMessage?.(msg, isError);
+ }
+
+ private cloneGrid(grid: Frame): Frame {
+ return JSON.parse(JSON.stringify(grid)) as Frame;
+ }
+
+ private createEmptyFrame(): Frame {
+ return { cells: {}, lines: [], highlights: [], message: "" };
+ }
+
+ private setCell(frame: Frame, x: number, y: number, data: CellData) {
+ frame.cells[`${x},${y}`] = { ...frame.cells[`${x},${y}`], ...data };
+ }
+
+ private getCell(frame: Frame, x: number, y: number): CellData | null {
+ return frame.cells[`${x},${y}`] ?? null;
+ }
+
+ private mergeFramesToSummary(finalFrames: Frame[]): Frame {
+ const summaryFrame = this.createEmptyFrame();
+ summaryFrame.message = "Final Summary of All Steps";
+ let currentOffsetX = 0;
+
+ for (let i = 0; i < finalFrames.length; i++) {
+ const f = finalFrames[i];
+ let minX = Infinity;
+ let maxX = -Infinity;
+ let hasCells = false;
+ Object.keys(f.cells).forEach((key) => {
+ const [x] = key.split(",").map(Number);
+ minX = Math.min(minX, x);
+ maxX = Math.max(maxX, x);
+ hasCells = true;
+ });
+ if (!hasCells) continue;
+
+ const shiftX = currentOffsetX - minX;
+
+ Object.entries(f.cells).forEach(([key, cell]) => {
+ const [x, y] = key.split(",").map(Number);
+ summaryFrame.cells[`${x + shiftX},${y}`] = { ...cell };
+ });
+
+ f.lines.forEach((line) => {
+ summaryFrame.lines.push({
+ y: line.y,
+ startX: line.startX + shiftX,
+ endX: line.endX + shiftX,
+ });
+ });
+
+ if (i > 0) {
+ summaryFrame.cells[`${currentOffsetX - 2},2`] = {
+ text: "→",
+ color: this.colors.operator,
+ };
+ }
+
+ currentOffsetX += maxX - minX + 1 + 3;
+ }
+
+ return summaryFrame;
+ }
+
+ private generateAddition(numbers: string[]) {
+ const maxLen = Math.max(...numbers.map((s) => s.length));
+ const padded = numbers.map((s) => s.padStart(maxLen, "0"));
+ const frame = this.createEmptyFrame();
+ frame.message = `Setting up addition for ${numbers.join(" + ")}`;
+ const numCount = numbers.length;
+ const width = maxLen + 2;
+
+ this.setCell(frame, 0, numCount, { text: "+", color: this.colors.operator });
+
+ for (let r = 0; r < numCount; r++) {
+ const pStr = padded[r];
+ for (let i = 0; i < maxLen; i++) {
+ if (i >= maxLen - numbers[r].length) {
+ this.setCell(frame, i + 2, r + 1, { text: pStr[i], color: this.colors.text });
+ }
+ }
+ }
+
+ const lineY = numCount + 1;
+ frame.lines.push({ y: lineY, startX: 0, endX: width });
+ this.frames.push(this.cloneGrid(frame));
+
+ let carry = 0;
+
+ for (let i = maxLen - 1; i >= 0; i--) {
+ let sum = carry;
+ const columnDigits: number[] = [];
+ for (let r = 0; r < numCount; r++) {
+ const d = parseInt(padded[r][i], 10);
+ sum += d;
+ columnDigits.push(d);
+ }
+
+ const nextCarry = Math.floor(sum / 10);
+ const digit = sum % 10;
+ const col = i + 2;
+
+ const stepFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
+ stepFrame.highlights.push({
+ x: col,
+ y: carry > 0 ? 0 : 1,
+ w: 1,
+ h: carry > 0 ? numCount + 1 : numCount,
+ });
+
+ let msg = `Add column: ${columnDigits.join(" + ")}`;
+ if (carry > 0) msg += ` + ${carry} (carry) = ${sum}.`;
+ else msg += ` = ${sum}.`;
+ if (nextCarry > 0) msg += ` Write ${digit}, carry ${nextCarry}.`;
+ else msg += ` Write ${digit}.`;
+ stepFrame.message = msg;
+ this.frames.push(stepFrame);
+
+ const resultFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
+ resultFrame.highlights = [];
+ this.setCell(resultFrame, col, lineY + 1, {
+ text: digit.toString(),
+ color: this.colors.result,
+ });
+ if (nextCarry > 0) {
+ this.setCell(resultFrame, col - 1, 0, {
+ text: nextCarry.toString(),
+ color: this.colors.carry,
+ });
+ }
+ resultFrame.message = msg;
+ this.frames.push(resultFrame);
+
+ carry = nextCarry;
+ }
+
+ if (carry > 0) {
+ const finalFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
+ const carryStr = carry.toString();
+ for (let c = 0; c < carryStr.length; c++) {
+ this.setCell(finalFrame, 1 - (carryStr.length - 1 - c), lineY + 1, {
+ text: carryStr[c],
+ color: this.colors.result,
+ });
+ }
+ finalFrame.message = "Write final carry down.";
+ this.frames.push(finalFrame);
+ }
+ }
+
+ private generateSubtraction(s1: string, s2: string) {
+ const n1 = BigInt(s1);
+ const n2 = BigInt(s2);
+ if (n1 < n2) {
+ throw new Error(
+ "First number must be greater than or equal to second number for visualizer."
+ );
+ }
+
+ const maxLen = Math.max(s1.length, s2.length);
+ const p1 = s1.padStart(maxLen, "0").split("").map(Number);
+ const p2 = s2.padStart(maxLen, "0").split("").map(Number);
+
+ const frame = this.createEmptyFrame();
+ frame.message = `Setting up subtraction for ${s1} - ${s2}`;
+ const width = maxLen + 2;
+
+ this.setCell(frame, 0, 2, { text: "−", color: this.colors.operator });
+ for (let i = 0; i < maxLen; i++) {
+ if (i >= maxLen - s1.length) {
+ this.setCell(frame, i + 2, 1, { text: p1[i].toString(), color: this.colors.text });
+ }
+ if (i >= maxLen - s2.length) {
+ this.setCell(frame, i + 2, 2, { text: p2[i].toString(), color: this.colors.text });
+ }
+ }
+ frame.lines.push({ y: 3, startX: 0, endX: width });
+ this.frames.push(this.cloneGrid(frame));
+
+ for (let i = maxLen - 1; i >= 0; i--) {
+ let d1 = p1[i];
+ const d2 = p2[i];
+ const col = i + 2;
+
+ if (d1 < d2) {
+ let borrowIdx = i - 1;
+ while (borrowIdx >= 0 && p1[borrowIdx] === 0) borrowIdx--;
+
+ for (let j = borrowIdx; j < i; j++) {
+ const bFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
+ const cellToCross = this.getCell(bFrame, j + 2, 1);
+ this.setCell(bFrame, j + 2, 1, { crossedOut: true });
+ const newVal = p1[j] === 0 ? 9 : p1[j] - 1;
+ p1[j] = newVal;
+ this.setCell(bFrame, j + 2, 0, {
+ text: newVal.toString(),
+ color: this.colors.borrow,
+ });
+ if (j < i - 1) {
+ bFrame.message = "Borrowing across zero. Cross out 0, make it 9.";
+ } else {
+ bFrame.message = `Borrow 1 from ${cellToCross?.text ?? ""}, leaving ${newVal}.`;
+ }
+ bFrame.highlights = [{ x: j + 2, y: 0, w: 1, h: 2 }];
+ this.frames.push(bFrame);
+ }
+
+ p1[i] += 10;
+ d1 = p1[i];
+ const addFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
+ this.setCell(addFrame, col, 1, { crossedOut: true });
+ this.setCell(addFrame, col, 0, { text: d1.toString(), color: this.colors.borrow });
+ addFrame.message = `Add 10 to current column, making it ${d1}.`;
+ addFrame.highlights = [{ x: col, y: 0, w: 1, h: 2 }];
+ this.frames.push(addFrame);
+ }
+
+ const subFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
+ const origDigit = parseInt(s1.padStart(maxLen, "0")[i], 10);
+ subFrame.highlights = [
+ {
+ x: col,
+ y: p1[i] > origDigit ? 0 : 1,
+ w: 1,
+ h: p1[i] > origDigit ? 3 : 2,
+ },
+ ];
+ subFrame.message = `Subtract: ${d1} - ${d2} = ${d1 - d2}.`;
+ this.frames.push(subFrame);
+
+ const resultFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
+ resultFrame.highlights = [];
+ const res = d1 - d2;
+ const currentP1 = BigInt(p1.slice(0, i + 1).join(""));
+ const currentP2 = BigInt(p2.slice(0, i + 1).join(""));
+ if (res > 0 || i === maxLen - 1 || currentP1 > currentP2) {
+ this.setCell(resultFrame, col, 4, {
+ text: res.toString(),
+ color: this.colors.result,
+ });
+ }
+ resultFrame.message = `Write ${res}.`;
+ this.frames.push(resultFrame);
+ }
+ }
+
+ private generateMultiplication(s1: string, s2: string) {
+ const topOffset = 2;
+ const stackWidth = Math.max(s1.length, s2.length);
+ const rightOffset = stackWidth;
+ const opCol = rightOffset - s2.length;
+ const width = rightOffset + 1;
+
+ const frame = this.createEmptyFrame();
+ frame.message = `Setting up multiplication: ${s1} × ${s2}`;
+
+ this.setCell(frame, opCol, topOffset + 1, {
+ text: "×",
+ color: this.colors.operator,
+ });
+ for (let i = 0; i < s1.length; i++) {
+ this.setCell(frame, rightOffset - s1.length + 1 + i, topOffset, {
+ text: s1[i],
+ color: this.colors.text,
+ });
+ }
+ for (let i = 0; i < s2.length; i++) {
+ this.setCell(frame, rightOffset - s2.length + 1 + i, topOffset + 1, {
+ text: s2[i],
+ color: this.colors.text,
+ });
+ }
+ frame.lines.push({ y: topOffset + 2, startX: opCol, endX: rightOffset });
+ this.frames.push(this.cloneGrid(frame));
+
+ const partialProducts: string[] = [];
+ let rowOffset = topOffset + 2;
+
+ for (let j = s2.length - 1; j >= 0; j--) {
+ const d2 = parseInt(s2[j], 10);
+ let carry = 0;
+ let partial = "";
+ const shift = s2.length - 1 - j;
+
+ const startFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
+ Object.keys(startFrame.cells).forEach((k) => {
+ if (k.endsWith(",0") || k.endsWith(",1")) delete startFrame.cells[k];
+ });
+
+ for (let i = s1.length - 1; i >= 0; i--) {
+ const d1 = parseInt(s1[i], 10);
+ const prod = d1 * d2 + carry;
+ const nextCarry = Math.floor(prod / 10);
+ const digit = prod % 10;
+ const highlightCol = rightOffset - s1.length + 1 + i;
+ const bottomCol = rightOffset - s2.length + 1 + j;
+
+ const stepFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
+ stepFrame.highlights = [
+ { x: highlightCol, y: topOffset, w: 1, h: 1 },
+ { x: bottomCol, y: topOffset + 1, w: 1, h: 1 },
+ ];
+ let msg = `${d1} × ${d2}`;
+ if (carry > 0) msg += ` + ${carry} (carry) = ${prod}.`;
+ else msg += ` = ${prod}.`;
+ if (nextCarry > 0) msg += ` Write ${digit}, carry ${nextCarry}.`;
+ else msg += ` Write ${digit}.`;
+ stepFrame.message = msg;
+ if (carry > 0) {
+ this.setCell(stepFrame, highlightCol, topOffset - 1, {
+ text: carry.toString(),
+ color: this.colors.carry,
+ });
+ }
+ this.frames.push(stepFrame);
+
+ const writeFrame = this.cloneGrid(stepFrame);
+ writeFrame.highlights = [];
+ const writeCol = rightOffset - shift - (s1.length - 1 - i);
+ this.setCell(writeFrame, writeCol, rowOffset, {
+ text: digit.toString(),
+ color: this.colors.result,
+ });
+ if (nextCarry > 0) {
+ this.setCell(writeFrame, highlightCol - 1, topOffset - 1, {
+ text: nextCarry.toString(),
+ color: this.colors.carry,
+ });
+ }
+ this.frames.push(writeFrame);
+
+ carry = nextCarry;
+ partial = digit.toString() + partial;
+ }
+
+ if (carry > 0) {
+ const finalFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
+ const writeCol = rightOffset - shift - s1.length;
+ this.setCell(finalFrame, writeCol, rowOffset, {
+ text: carry.toString(),
+ color: this.colors.result,
+ });
+ finalFrame.message = "Write final carry down.";
+ this.frames.push(finalFrame);
+ partial = carry.toString() + partial;
+ }
+
+ if (shift > 0) {
+ const padFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
+ for (let k = 0; k < shift; k++) {
+ this.setCell(padFrame, rightOffset - k, rowOffset, {
+ text: "0",
+ color: this.colors.operator,
+ });
+ }
+ padFrame.message = "Add placeholder zero(s).";
+ this.frames.push(padFrame);
+ partial = partial + "0".repeat(shift);
+ }
+
+ partialProducts.push(partial);
+ rowOffset++;
+ }
+
+ if (partialProducts.length > 1) {
+ const addSetupFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
+ addSetupFrame.lines.push({ y: rowOffset, startX: opCol, endX: rightOffset });
+ this.setCell(addSetupFrame, opCol, rowOffset - 1, {
+ text: "+",
+ color: this.colors.operator,
+ });
+ addSetupFrame.message = "Add the partial products together.";
+ Object.keys(addSetupFrame.cells).forEach((k) => {
+ if (k.endsWith(`,${topOffset - 1}`)) delete addSetupFrame.cells[k];
+ });
+ this.frames.push(addSetupFrame);
+
+ let carry = 0;
+ const maxP = Math.max(...partialProducts.map((p) => p.length));
+
+ for (let i = 0; i < maxP; i++) {
+ let sum = carry;
+ for (let j = 0; j < partialProducts.length; j++) {
+ const pStr = partialProducts[j];
+ if (i < pStr.length) sum += parseInt(pStr[pStr.length - 1 - i], 10);
+ }
+ const digit = sum % 10;
+ const nextCarry = Math.floor(sum / 10);
+ const col = rightOffset - i;
+ const resFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
+ this.setCell(resFrame, col, rowOffset, {
+ text: digit.toString(),
+ color: this.colors.result,
+ });
+ let msg = `Sum column = ${sum}. Write ${digit}.`;
+ if (nextCarry > 0) {
+ this.setCell(resFrame, col - 1, topOffset + 1, {
+ text: nextCarry.toString(),
+ color: this.colors.carry,
+ });
+ msg += ` Carry ${nextCarry}.`;
+ }
+ resFrame.message = msg;
+ this.frames.push(resFrame);
+ carry = nextCarry;
+ }
+
+ if (carry > 0) {
+ const finalFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
+ this.setCell(finalFrame, rightOffset - maxP, rowOffset, {
+ text: carry.toString(),
+ color: this.colors.result,
+ });
+ finalFrame.message = "Write final addition carry.";
+ this.frames.push(finalFrame);
+ }
+ }
+ }
+
+ private generateDivision(s1: string, s2: string) {
+ if (s2.length > 15) throw new Error("Divisor too large for visualizer (max 15 digits).");
+ const div = parseInt(s2, 10);
+ if (div === 0) throw new Error("Cannot divide by zero");
+ if (BigInt(s1) === 0n) throw new Error("0 divided by anything is 0");
+
+ const frame = this.createEmptyFrame();
+ const divLen = s2.length;
+ const divOffset = 0;
+ const divStart = divLen + 1;
+ frame.message = `Setup long division: ${s1} ÷ ${s2}`;
+
+ for (let i = 0; i < divLen; i++) {
+ this.setCell(frame, divOffset + i, 2, { text: s2[i], color: this.colors.operator });
+ }
+ this.setCell(frame, divLen, 2, { text: ")", color: this.colors.text });
+ for (let i = 0; i < s1.length; i++) {
+ this.setCell(frame, divStart + i, 2, { text: s1[i], color: this.colors.text });
+ }
+ frame.lines.push({ y: 1.5, startX: divStart - 0.2, endX: divStart + s1.length });
+ this.frames.push(this.cloneGrid(frame));
+
+ const dividendArr = s1.split("");
+ let currentVal = 0;
+ let row = 3;
+ let quotientStr = "";
+
+ for (let i = 0; i < dividendArr.length; i++) {
+ const nextDigit = parseInt(dividendArr[i], 10);
+ currentVal = currentVal * 10 + nextDigit;
+
+ const bringFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
+ if (row > 3) {
+ this.setCell(bringFrame, divStart + i, row - 1, {
+ text: nextDigit.toString(),
+ color: this.colors.text,
+ });
+ bringFrame.message = `Bring down ${nextDigit}. Current value is ${currentVal}.`;
+ bringFrame.highlights = [{ x: divStart + i, y: 2, w: 1, h: row - 2 }];
+ } else {
+ bringFrame.message = `Consider ${currentVal}.`;
+ bringFrame.highlights = [{ x: divStart, y: 2, w: i + 1, h: 1 }];
+ }
+ this.frames.push(bringFrame);
+
+ if (currentVal >= div || i === dividendArr.length - 1 || quotientStr.length > 0) {
+ const qDigit = Math.floor(currentVal / div);
+ quotientStr += qDigit.toString();
+
+ const qFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
+ qFrame.highlights = [];
+ this.setCell(qFrame, divStart + i, 0, {
+ text: qDigit.toString(),
+ color: this.colors.result,
+ });
+ qFrame.message = `${div} goes into ${currentVal} exactly ${qDigit} times.`;
+ this.frames.push(qFrame);
+
+ if (qDigit > 0 || i === dividendArr.length - 1) {
+ const subVal = qDigit * div;
+ const subStr = subVal.toString();
+ const subFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
+ this.setCell(subFrame, divStart + i - subStr.length, row, {
+ text: "-",
+ color: this.colors.operator,
+ });
+ for (let k = 0; k < subStr.length; k++) {
+ this.setCell(subFrame, divStart + i - subStr.length + 1 + k, row, {
+ text: subStr[k],
+ color: this.colors.borrow,
+ });
+ }
+ subFrame.lines.push({
+ y: row + 1,
+ startX: divStart + i - subStr.length,
+ endX: divStart + i + 1,
+ });
+ subFrame.message = `Multiply ${qDigit} × ${div} = ${subVal}, then subtract.`;
+ this.frames.push(subFrame);
+
+ currentVal = currentVal - subVal;
+ row += 2;
+ const resFrame = this.cloneGrid(this.frames[this.frames.length - 1]);
+ const currStr = currentVal.toString();
+ for (let k = 0; k < currStr.length; k++) {
+ this.setCell(resFrame, divStart + i - currStr.length + 1 + k, row - 1, {
+ text: currStr[k],
+ color: this.colors.text,
+ });
+ }
+ resFrame.message = `Remainder is ${currentVal}.`;
+ this.frames.push(resFrame);
+ }
+ }
+ }
+ }
+
+ render() {
+ this.refreshColors();
+ if (this.frames.length === 0) {
+ const dpr = window.devicePixelRatio || 1;
+ this.ctx.clearRect(0, 0, this.canvas.width / dpr, this.canvas.height / dpr);
+ return;
+ }
+
+ const frame = this.frames[this.currentFrame];
+ const ctx = this.ctx;
+ const dpr = window.devicePixelRatio || 1;
+ ctx.clearRect(0, 0, this.canvas.width / dpr, this.canvas.height / dpr);
+
+ let minX = Infinity;
+ let maxX = -Infinity;
+ let minY = Infinity;
+ let maxY = -Infinity;
+
+ Object.keys(frame.cells).forEach((key) => {
+ const [x, y] = key.split(",").map(Number);
+ minX = Math.min(minX, x);
+ maxX = Math.max(maxX, x);
+ minY = Math.min(minY, y);
+ maxY = Math.max(maxY, y);
+ });
+
+ if (!Number.isFinite(minX)) {
+ this.updateUI();
+ return;
+ }
+
+ minX -= 1;
+ maxX += 1;
+ minY -= 1;
+ maxY += 1;
+
+ const cols = maxX - minX + 1;
+ const rows = maxY - minY + 1;
+ const w = this.canvas.width / dpr;
+ const h = this.canvas.height / dpr;
+ const cellSize = Math.min(w / cols, h / rows, 60);
+ const offsetX = (w - cols * cellSize) / 2 - minX * cellSize;
+ const offsetY = (h - rows * cellSize) / 2 - minY * cellSize;
+
+ ctx.textBaseline = "middle";
+ ctx.textAlign = "center";
+
+ frame.highlights.forEach((hl) => {
+ ctx.fillStyle = this.colors.highlight;
+ ctx.fillRect(
+ offsetX + hl.x * cellSize,
+ offsetY + hl.y * cellSize,
+ hl.w * cellSize,
+ hl.h * cellSize
+ );
+ ctx.strokeStyle = this.colors.highlightBorder;
+ ctx.lineWidth = 2;
+ ctx.strokeRect(
+ offsetX + hl.x * cellSize,
+ offsetY + hl.y * cellSize,
+ hl.w * cellSize,
+ hl.h * cellSize
+ );
+ });
+
+ ctx.strokeStyle = this.colors.line;
+ ctx.lineWidth = 3;
+ frame.lines.forEach((line) => {
+ ctx.beginPath();
+ ctx.moveTo(offsetX + line.startX * cellSize, offsetY + line.y * cellSize);
+ ctx.lineTo(offsetX + line.endX * cellSize, offsetY + line.y * cellSize);
+ ctx.stroke();
+ });
+
+ const mono = getComputedStyle(this.themeRoot).getPropertyValue("--mono").trim() || "monospace";
+ ctx.font = `bold ${cellSize * 0.6}px ${mono}`;
+
+ Object.entries(frame.cells).forEach(([key, cell]) => {
+ const [x, y] = key.split(",").map(Number);
+ const cx = offsetX + x * cellSize + cellSize / 2;
+ const cy = offsetY + y * cellSize + cellSize / 2;
+ ctx.fillStyle = cell.color || this.colors.text;
+ if (cell.text) ctx.fillText(cell.text, cx, cy);
+ if (cell.crossedOut) {
+ ctx.strokeStyle = this.colors.carry;
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ ctx.moveTo(cx - cellSize * 0.3, cy + cellSize * 0.3);
+ ctx.lineTo(cx + cellSize * 0.3, cy - cellSize * 0.3);
+ ctx.stroke();
+ }
+ });
+
+ this.updateUI();
+ }
+
+ private updateUI() {
+ if (this.frames.length > 0) {
+ const msg = this.frames[this.currentFrame].message;
+ this.onMessage?.(msg, false);
+ this.onStep?.(this.currentFrame + 1, this.frames.length);
+ }
+ }
+
+ play() {
+ if (this.frames.length === 0) return;
+ if (this.currentFrame >= this.frames.length - 1) this.currentFrame = 0;
+ this.isPlaying = true;
+ this.onPlayState?.(true);
+ this.playInterval = setInterval(() => {
+ if (this.currentFrame < this.frames.length - 1) this.step(1);
+ else this.pause();
+ }, this.speed);
+ }
+
+ pause() {
+ this.isPlaying = false;
+ this.onPlayState?.(false);
+ if (this.playInterval) clearInterval(this.playInterval);
+ this.playInterval = null;
+ }
+
+ togglePlay() {
+ if (this.isPlaying) this.pause();
+ else this.play();
+ }
+
+ step(dir: number) {
+ const newFrame = this.currentFrame + dir;
+ if (newFrame >= 0 && newFrame < this.frames.length) {
+ this.currentFrame = newFrame;
+ this.render();
+ }
+ }
+
+ destroy() {
+ this.pause();
+ }
+}
diff --git a/content/math/written-calculation/widgets/vertical-arithmetic-workbench.tsx b/content/math/written-calculation/widgets/vertical-arithmetic-workbench.tsx
new file mode 100644
index 0000000..482c148
--- /dev/null
+++ b/content/math/written-calculation/widgets/vertical-arithmetic-workbench.tsx
@@ -0,0 +1,231 @@
+import { createEffect, createSignal, onCleanup, onMount } from "solid-js";
+import { MathVisualizer } from "./lib/math-visualizer";
+
+type Op = "+" | "-" | "*" | "/";
+
+const DEFAULT_NUMBERS = "27\n59";
+
+function VerticalArithmeticWorkbench() {
+ let rootRef!: HTMLElement;
+ let canvasRef!: HTMLCanvasElement;
+ let canvasWrapRef!: HTMLDivElement;
+ let visualizer: MathVisualizer | undefined;
+
+ const [operation, setOperation] = createSignal("+");
+ const [numbersInput, setNumbersInput] = createSignal(DEFAULT_NUMBERS);
+ const [message, setMessage] = createSignal("Enter numbers and click Generate");
+ const [isError, setIsError] = createSignal(false);
+ const [stepCurrent, setStepCurrent] = createSignal(0);
+ const [stepTotal, setStepTotal] = createSignal(0);
+ const [playing, setPlaying] = createSignal(false);
+ const [speed, setSpeed] = createSignal(800);
+
+ const progress = () =>
+ stepTotal() > 0 ? (stepCurrent() / stepTotal()) * 100 : 0;
+
+ onMount(() => {
+ visualizer = new MathVisualizer({
+ canvas: canvasRef,
+ wrapper: canvasWrapRef,
+ themeRoot: rootRef,
+ });
+ visualizer.onMessage = (msg, err) => {
+ setMessage(msg);
+ setIsError(!!err);
+ };
+ visualizer.onStep = (current, total) => {
+ setStepCurrent(current);
+ setStepTotal(total);
+ };
+ visualizer.onPlayState = setPlaying;
+
+ const resize = () => visualizer?.resizeCanvas();
+ resize();
+ const ro = new ResizeObserver(resize);
+ ro.observe(canvasWrapRef);
+ window.addEventListener("resize", resize);
+
+ const themeObserver = new MutationObserver(() => {
+ visualizer?.refreshColors();
+ visualizer?.render();
+ });
+ themeObserver.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ["data-theme"],
+ });
+
+ onCleanup(() => {
+ ro.disconnect();
+ window.removeEventListener("resize", resize);
+ themeObserver.disconnect();
+ visualizer?.destroy();
+ });
+
+ setTimeout(() => generate(), 100);
+ });
+
+ createEffect(() => {
+ visualizer?.setOperation(operation());
+ visualizer?.setSpeed(speed());
+ });
+
+ const generate = () => {
+ visualizer?.setOperation(operation());
+ visualizer?.generateFromInput(numbersInput());
+ };
+
+ return (
+
+ );
+}
+
+export default VerticalArithmeticWorkbench;
diff --git a/data/catalog.json b/data/catalog.json
index 40653e6..980935a 100644
--- a/data/catalog.json
+++ b/data/catalog.json
@@ -3,7 +3,7 @@
"subtitle": "Browse chapters by subject and curriculum",
"curriculums": ["Foundation", "DSE", "IB", "A-Level", "AP", "IGCSE"],
"subjects": [
- { "id": "math", "name": "Mathematics" },
+ { "id": "math", "name": "Mathematics", "banner": "/catalog/banners/math.webp" },
{ "id": "science", "name": "Science" },
{ "id": "humanities", "name": "Humanities" },
{ "id": "computing", "name": "Computing" }
diff --git a/data/catalog.types.ts b/data/catalog.types.ts
index b7f37c1..336f3c3 100644
--- a/data/catalog.types.ts
+++ b/data/catalog.types.ts
@@ -29,6 +29,8 @@ export type CatalogGraph = {
export type CatalogSubject = {
id: string;
name: string;
+ /** Root-relative URL, e.g. /catalog/banners/math.webp */
+ banner?: string;
strands: CatalogStrand[];
graph?: CatalogGraph;
};
diff --git a/data/faq.json b/data/faq.json
new file mode 100644
index 0000000..6e33ba1
--- /dev/null
+++ b/data/faq.json
@@ -0,0 +1,142 @@
+{
+ "intro": "Quick answers about Open Free Books — free textbooks for learners worldwide, how we work with different syllabi, and how to use the catalog.",
+ "items": [
+ {
+ "id": "what-is-ofb",
+ "type": "standard",
+ "question": "What is Open Free Books?",
+ "answer": [
+ "Open Free Books aims to be more than a learning resource: free textbooks for everyone worldwide, from elementary school through university — on the web, at no cost.",
+ "We aim to cater to different syllabi and curricula across the globe. It is a community effort by educators and contributors around the world, built in the open on GitHub.",
+ "Free means the whole book for everyone: read online without paywalls or premium unlocks. Open means anyone can fix a typo, add a chapter, or fork a book for a classroom."
+ ]
+ },
+ {
+ "id": "how-different",
+ "type": "comparison",
+ "question": "How is Open Free Books different from GeoGebra, Math is Fun, and Wikipedia?",
+ "intro": "We respect these projects — they solve different problems. Open Free Books is a free, open-source textbook library on the web.",
+ "peers": [
+ {
+ "name": "GeoGebra",
+ "url": "https://www.geogebra.org/",
+ "role": "Interactive geometry and algebra tools, activities, and classroom apps.",
+ "contrast": "Open Free Books publishes structured textbook chapters and learning paths, not a general-purpose construction sandbox."
+ },
+ {
+ "name": "Math is Fun",
+ "url": "https://www.mathsisfun.com/",
+ "role": "Friendly reference pages, games, and explanations across many topics.",
+ "contrast": "Open Free Books is curriculum-shaped: ordered chapters, prerequisites in the catalog map, and authored books — not a grab-bag of articles."
+ },
+ {
+ "name": "Wikipedia",
+ "url": "https://www.wikipedia.org/",
+ "role": "Encyclopedic, community-edited reference on almost everything.",
+ "contrast": "Not a wiki-style fact pile. We publish opinionated paths from prerequisite to mastery — structure on purpose, not everything about X."
+ }
+ ],
+ "ofb": "Free forever, no ads, no commercial upsell, and open on GitHub for anyone to fix or extend."
+ },
+ {
+ "id": "free-forever",
+ "type": "standard",
+ "question": "Is it really free? Will there be ads or paid tiers?",
+ "answer": [
+ "Yes — free is the product, not a trial. Every book and chapter is readable without a subscription or premium unlock.",
+ "Open Free Books offers no commercial services and no ads, now or in the future. Sustainability comes from community contribution and shared open infrastructure, not from turning learners into the product."
+ ]
+ },
+ {
+ "id": "open-github",
+ "type": "standard",
+ "question": "What does \"open\" mean? How do I contribute?",
+ "answer": [
+ "Open means the textbooks are built in public on GitHub. You can read the source, open a pull request, fork for your syllabus, and see every change in version control.",
+ "To add or improve material, start with the contributing guide on this site or open a discussion or pull request on GitHub. You do not need permission from a publisher."
+ ]
+ },
+ {
+ "id": "vs-commercial",
+ "type": "standard",
+ "question": "How is this different from commercial textbook platforms?",
+ "answer": [
+ "Many commercial platforms sell access, host ads, or bundle paid services around \"digital\" textbooks. Content may be locked behind logins, and terms can change when a vendor updates its product.",
+ "Open Free Books is non-commercial: no paywalls, no ad network, no investor-driven upsell. Books are static HTML in an open repository — readable in a browser today, forkable for your classroom tomorrow."
+ ]
+ },
+ {
+ "id": "not-wiki",
+ "type": "standard",
+ "question": "Is this a wiki?",
+ "answer": [
+ "No. Wikis excel at scattered facts across many topics; they do not usually carry a learner from prerequisite to mastery in one opinionated path.",
+ "We publish ordered chapters, assessed skills, and a catalog map of what to study next — curated structure, not an everything-about-X article farm."
+ ]
+ },
+ {
+ "id": "not-replace-teachers",
+ "type": "standard",
+ "question": "Does Open Free Books replace teachers?",
+ "answer": [
+ "No. A great teacher still sets pace, asks hard questions, and notices when a student is stuck. That relationship is not what we are trying to automate.",
+ "Textbooks are reference and structure: explanations, exercises, diagrams. Those can be free, forkable, and kept current on the web so teachers spend less time hunting permissions and more time teaching."
+ ]
+ },
+ {
+ "id": "why-html",
+ "type": "standard",
+ "question": "Why web pages instead of PDFs?",
+ "answer": [
+ "Paper and most \"digital\" textbooks are fixed layouts — hard to search, hard to update, and poor homes for live diagrams or interactives.",
+ "HTML on the open web can link to sources, resize for any screen, embed simulations, and improve over time. The book becomes something you use, not a file you download once."
+ ]
+ },
+ {
+ "id": "ai-content",
+ "type": "standard",
+ "question": "Is the content AI-generated?",
+ "answer": [
+ "No. Chapters are authored HTML with care: figures, interactives, and maps where they help understanding — not bulk-generated slop or print PDFs pasted online.",
+ "Core curriculum changes slowly; one carefully reviewed book, shared openly, beats regenerating the same chapter thousands of times in a data center."
+ ]
+ },
+ {
+ "id": "subjects",
+ "type": "standard",
+ "question": "What subjects are available?",
+ "answer": [
+ "Mathematics is live today — browse strands and chapters in the catalog. Science, humanities, and computing are on the roadmap.",
+ "New subjects and chapters appear as contributors publish them on GitHub. Planned chapters show in the catalog before a full page exists."
+ ]
+ },
+ {
+ "id": "catalog-curriculum",
+ "type": "standard",
+ "question": "What are curriculum labels on the catalog?",
+ "answer": [
+ "Each subject in the catalog has a set of curricula you can filter by — exam boards, school programs, and similar labels — so you can see which chapters fit your syllabus.",
+ "We try to keep chapter structure and prerequisite maps compatible with as many curricula as possible. We do our best, but coverage is not perfect; the catalog will grow and improve as more people contribute.",
+ "Those labels appear only in the catalog (badges and filters), not inside chapter pages. You choose what applies to you on the catalog; inside a chapter, the focus stays on the topic itself."
+ ]
+ },
+ {
+ "id": "find-chapters",
+ "type": "standard",
+ "question": "How do I find a chapter?",
+ "answer": [
+ "Open the catalog for a subject — list view by default, or map view to follow prerequisites. Use curriculum filters if you need to narrow the list.",
+ "Press ⌘K or Ctrl+K (or open Search in the header) to search live chapter pages. The dedicated search page indexes chapter content, not every site page."
+ ]
+ },
+ {
+ "id": "about-manifesto",
+ "type": "standard",
+ "question": "Where are the project principles?",
+ "answer": [
+ "The About page (Manifesto) lists the values we will not trade away as the catalog grows: free, open, curriculum-shaped, independent, durable, crafted, inspectable, and web-native.",
+ "Read the manifesto for the full principles behind every book on this site."
+ ]
+ }
+ ]
+}
diff --git a/data/math-curriculum.json b/data/math-curriculum.json
index 57ac57c..4bb1eaf 100644
--- a/data/math-curriculum.json
+++ b/data/math-curriculum.json
@@ -5,7 +5,9 @@
"graph": {
"edges": [
{ "from": "counting-and-numbers", "to": "basic-math-notation" },
- { "from": "basic-math-notation", "to": "directed-numbers-rates" },
+ { "from": "basic-math-notation", "to": "properties-of-operations" },
+ { "from": "properties-of-operations", "to": "written-calculation" },
+ { "from": "written-calculation", "to": "directed-numbers-rates" },
{ "from": "directed-numbers-rates", "to": "polynomials-expansion-factorisation" },
{ "from": "polynomials-expansion-factorisation", "to": "algebraic-fractions-formulas" },
{ "from": "polynomials-expansion-factorisation", "to": "linear-equations-two-unknowns" },
@@ -65,6 +67,20 @@
"status": "live",
"curriculums": ["Foundation"]
},
+ {
+ "slug": "properties-of-operations",
+ "title": "Properties of operations",
+ "description": "See why order, grouping, and distribution can change how you write a sum or product without changing the result — with interactive area and dot models.",
+ "status": "live",
+ "curriculums": ["Foundation"]
+ },
+ {
+ "slug": "written-calculation",
+ "title": "Written calculation",
+ "description": "Add, subtract, multiply, and divide whole numbers using vertical written form: align columns, regroup, and read the result.",
+ "status": "live",
+ "curriculums": ["Foundation"]
+ },
{
"slug": "directed-numbers-rates",
"title": "Directed numbers, rates & ratios",
diff --git a/frontend/src/components/catalog-app.tsx b/frontend/src/components/catalog-app.tsx
index 8a5ff09..b70d4cb 100644
--- a/frontend/src/components/catalog-app.tsx
+++ b/frontend/src/components/catalog-app.tsx
@@ -153,7 +153,21 @@ export function CatalogApp() {
-