diff --git a/package.json b/package.json index a40681b..2698ca2 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@types/node": "^22.20.4", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", + "autoprefixer": "^10.6.1", "eslint": "^10.11.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-astro": "^3.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b5f4f1..297b312 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,6 +66,9 @@ importers: '@types/react-dom': specifier: ^19.2.4 version: 19.3.0(@types/react@19.3.0) + autoprefixer: + specifier: ^10.6.1 + version: 10.6.1(postcss@8.5.28) eslint: specifier: ^10.11.0 version: 10.11.0 @@ -1470,6 +1473,13 @@ packages: '@astrojs/markdown-remark': optional: true + autoprefixer@10.6.1: + resolution: {integrity: sha512-cL1Qz6ADZhcEbny/8HPfe99J6HhNoYtpX2LFLIbhgGE7Q1hlQVkYFdetDN7Id3KiQxhDrHwzlHr/YQCnZ8+xSA==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -1949,6 +1959,9 @@ packages: resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==} engines: {node: '>=20'} + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + fresh@2.0.0: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} @@ -4994,6 +5007,15 @@ snapshots: - uploadthing - yaml + autoprefixer@10.6.1(postcss@8.5.28): + dependencies: + browserslist: 4.29.0 + caniuse-lite: 1.0.30001810 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.28 + postcss-value-parser: 4.2.0 + axobject-query@4.1.0: {} bail@2.0.2: {} @@ -5448,6 +5470,8 @@ snapshots: dependencies: tiny-inflate: 1.0.3 + fraction.js@5.3.4: {} + fresh@2.0.0: {} fsevents@2.3.3: diff --git a/postcss.config.mjs b/postcss.config.mjs index 5184ed3..773a6cc 100644 --- a/postcss.config.mjs +++ b/postcss.config.mjs @@ -7,5 +7,6 @@ export default { files: ['./src/styles/base/custom-media.css'], }, 'postcss-custom-media': {}, + autoprefixer: {}, }, } diff --git a/src/assets/content/articles/alchemical-materialism/summary/cover/image.jpg b/src/assets/content/articles/alchemical-materialism/summary/cover/image.jpg new file mode 100644 index 0000000..e50205b Binary files /dev/null and b/src/assets/content/articles/alchemical-materialism/summary/cover/image.jpg differ diff --git a/src/assets/content/articles/pm-world/summary/cover/image.jpg b/src/assets/content/articles/pm-world/summary/cover/image.jpg new file mode 100644 index 0000000..e1245b2 Binary files /dev/null and b/src/assets/content/articles/pm-world/summary/cover/image.jpg differ diff --git a/src/assets/content/articles/prima-materia/john_martin_-_the_great_day_of_his_wrath_-_google_art_project.jpg b/src/assets/content/articles/prima-materia/john_martin_-_the_great_day_of_his_wrath_-_google_art_project.jpg deleted file mode 100644 index 03d0afc..0000000 Binary files a/src/assets/content/articles/prima-materia/john_martin_-_the_great_day_of_his_wrath_-_google_art_project.jpg and /dev/null differ diff --git a/src/assets/content/articles/stages/summary/cover/image.jpg b/src/assets/content/articles/stages/summary/cover/image.jpg new file mode 100644 index 0000000..312960c Binary files /dev/null and b/src/assets/content/articles/stages/summary/cover/image.jpg differ diff --git a/src/assets/content/articles/the-crucible/summary/cover/image.jpg b/src/assets/content/articles/the-crucible/summary/cover/image.jpg new file mode 100644 index 0000000..c781ac8 Binary files /dev/null and b/src/assets/content/articles/the-crucible/summary/cover/image.jpg differ diff --git a/src/assets/content/articles/tincture/summary/cover/image.jpg b/src/assets/content/articles/tincture/summary/cover/image.jpg new file mode 100644 index 0000000..6e5af3b Binary files /dev/null and b/src/assets/content/articles/tincture/summary/cover/image.jpg differ diff --git a/src/components/markdoc/blocks/general/Definitions/Definition.astro b/src/components/markdoc/blocks/general/Definitions/Definition.astro index 9e24d54..c46213c 100644 --- a/src/components/markdoc/blocks/general/Definitions/Definition.astro +++ b/src/components/markdoc/blocks/general/Definitions/Definition.astro @@ -7,9 +7,10 @@ type Props = { term: string kind?: TermKind see?: string + ink?: boolean } -const { term, kind = 'entry', see = '' } = Astro.props +const { term, kind = 'entry', see = '', ink = false } = Astro.props const links: SeeLink[] = see .split(',') @@ -18,6 +19,6 @@ const links: SeeLink[] = see .map((name) => ({ title: name, href: `#${slugify(name)}` })) --- - + diff --git a/src/components/markdoc/blocks/general/Definitions/DefinitionList.astro b/src/components/markdoc/blocks/general/Definitions/DefinitionList.astro index aad9b8f..2058d56 100644 --- a/src/components/markdoc/blocks/general/Definitions/DefinitionList.astro +++ b/src/components/markdoc/blocks/general/Definitions/DefinitionList.astro @@ -1,3 +1,13 @@ -
+--- +import type { DefinitionVariant } from '@/lib/constants/blocks/definitions' + +type Props = { + variant?: DefinitionVariant +} + +const { variant = 'cross' } = Astro.props +--- + +
diff --git a/src/components/markdoc/blocks/general/TableWrapper.astro b/src/components/markdoc/blocks/general/TableWrapper.astro index 0727ee6..52e03a9 100644 --- a/src/components/markdoc/blocks/general/TableWrapper.astro +++ b/src/components/markdoc/blocks/general/TableWrapper.astro @@ -10,6 +10,8 @@ const { variant = 'default', caption } = Astro.props ---
- +
+ +
{caption &&
{caption}
}
diff --git a/src/components/terms/TermEntry.astro b/src/components/terms/TermEntry.astro index 826c903..550f105 100644 --- a/src/components/terms/TermEntry.astro +++ b/src/components/terms/TermEntry.astro @@ -25,9 +25,7 @@ const label = TERM_KINDS.find((option) => option.value === kind)?.label ?? kind
-

- -

+ {see.length > 0 && (

diff --git a/src/content/general/articles/alchemical-materialism.mdoc b/src/content/general/articles/alchemical-materialism.mdoc new file mode 100644 index 0000000..6450f50 --- /dev/null +++ b/src/content/general/articles/alchemical-materialism.mdoc @@ -0,0 +1,140 @@ +--- +title: Alchemical Materialism +subtitle: The grammar of the Crucible +status: published +noindex: false +stage: stub +route: + parent: the-crucible + index: 1 + sequence: false +summary: + excerpt: Three Primes, five Essences, one engine. + cover: + image: >- + /src/assets/content/articles/alchemical-materialism/summary/cover/image.jpg + alt: >- + A night valley lit by a roaring furnace; orange flames and smoke fill the + sky above dark buildings, and small figures and horses in the foreground + are dwarfed by the blaze. + caption: Coalbrookdale by Night + credit: Philip James de Loutherbourg + effect: none + ratio: 21-9 +published: 2026-09-30T20:41:00.000Z +tags: + - alchemical-materialism + - the-crucible +--- +## Overview + +- shared grammar of all seven stages +- **Primes =>** *Soul, Body, Spirit;* forces in tension +- **Essences =>** *Aether, Air, Earth, Fire, Water;* surplus regimes +- **Engine =>** baseline, degrees, signatures → *Materia* + +## The Engine + +- **Token =>** one *Prime* or one *Essence;* the unit of *Materia* +- **Baseline =>** stage default; no deviations, no tokens +- **Degree =>** structured; steps along fixed axes +- **Signature =>** freeform; declared limitations; taboos, weaknesses, obligations, anomalies; yields tokens +- **Materia =>** combined tokens from *degrees & signatures;* the pool is the entity's identity +- **Spending =>** tokens buy the stage's traits, capabilities, relationships +- **Leftovers =>** unspent tokens flow downstream + +## The Elements + +### The Three Primes + +- Three alchemical principles: something **burns** *(Sulphur),* something **remains** *(Salt),* something **transforms** *(Mercury)* +- Every entity holds all three; dominance sets character +- **Interdependence:** + - No **Soul =>** nothing starts + - No **Body =>** nothing hols + - No **Spirit =>** nothing changes + +{% tableWrapper + variant="default" + caption="The three Primes - Overview" + columns=["rowheader", "default", "default", "default", "default", "default"] %} +{% table %} +- Prime +- Alchemical +- Core +- Keyword +- Organization +- Failure +--- +- Soul +- Sulfur +- Agency; the spark +- »I burn« +- Individual excellence +- Consumed everything +--- +- Body +- Salt +- Structure; the form +- »It endures« +- Institutional strength +- Calcifies everything +--- +- Spirit +- Mercury +- Transformation; the flux +- »Nothing stays« +- Adaptive innovation +- Dissolves everything +{% /table %} +{% /tableWrapper %} + +### The Five Essences + +- **Regime =>** what a surplus enables; mechanical reading +- **Symbolic =>** resonance; myth, faith, flavour +- **Lenses =>** each stage reads essences through its own lens; core fixed, expression varies +- **Assignment =>** ask what a surplus enables; never assign by physical association + +{% tableWrapper + variant="default" + caption="The Five Essences" + columns=["rowheader", "default", "default", "default", "default"] %} +{% table %} +- Essence +- Regime +- Qualities +- Properties +- Image +--- +- Aether +- Given away +- Quintessence +- Transcendent, ordered, numinous +- The Stars +--- +- Air +- Governs & administers +- Hot & Wet +- Pervasive, overarching, commanding +- The Sky +--- +- Earth +- Accumulates +- Cold & Dry +- Heavy, material, foundational +- The Mountain +--- +- Fire +- Spent in violence +- Hot & Dry +- Bright, consuming, refining +- The Volcano +--- +- Water +- Travels & trades +- Cold & Wet +- Flowing, deep, dissolving +- The River +{% /table %} +{% /tableWrapper %} diff --git a/src/content/general/articles/meta.mdoc b/src/content/general/articles/meta.mdoc new file mode 100644 index 0000000..3e1f8c5 --- /dev/null +++ b/src/content/general/articles/meta.mdoc @@ -0,0 +1,14 @@ +--- +title: The Metatron +status: draft +noindex: false +stage: stub +route: + sequence: false +summary: + cover: + effect: none + ratio: original +published: 2026-10-12T14:30:00.000Z +tags: [] +--- diff --git a/src/content/general/articles/pm-area.mdoc b/src/content/general/articles/pm-area.mdoc new file mode 100644 index 0000000..c95c889 --- /dev/null +++ b/src/content/general/articles/pm-area.mdoc @@ -0,0 +1,341 @@ +--- +title: Area +status: draft +noindex: false +stage: stub +route: + segment: area + parent: prima-materia + index: 2 + sequence: false +summary: + cover: + effect: none + ratio: original +published: 2026-10-07T13:38:00.000Z +tags: + - the-crucible + - genesis + - prima-materia +--- +## Overview + +An area is one hex of the World map; it reads from it and rolls its own moisture, terrain, and anomaly; every Region inside starts from what the area sets. + +{% definitions variant="compact" %} +{% definition term="Elevation" kind="entry" %} +How high the Area stands: Lowland, Upland, or Montane. It is determined by the distance to the ranges +{% /definition %} + +{% definition term="Band" kind="entry" %} +The Area's temperature, from Arctic to Tropical +{% /definition %} + +{% definition term="Current" kind="entry" %} +Warm or cold water along a coastal area +{% /definition %} + +{% definition term="Moisture" kind="entry" %} +How much rain the area gets: Arid, Moderate, or Wet +{% /definition %} + +{% definition term="Anomaly" kind="entry" %} +A local strangeness confined to one area +{% /definition %} + +{% definition term="Terrain" kind="entry" %} +What shaped the Area's ground: Glacial, Plateau, Alluvial, Dissection, Karst or none +{% /definition %} + +{% definition term="Lake" kind="entry" %} +Standing water on the ground +{% /definition %} + +{% definition term="Salt Basin" kind="entry" %} +Salt water or salt crust where a lake has no outflow in dry country: a salt lake like the Dead Sea, or a dry salt pan. +{% /definition %} +{% /definitions %} + +### Procedure + +1. Roll the anomaly +1. Read the elevation +1. Read the band and current +1. Roll the moisture +1. Read or roll the terrain +1. Read the lakes + +## Anomalies + +Area Anomalies are locale strangeness, rarer the less warped the world is. Only land areas roll them. Roll `2d6` against the {% xref reference="pm-world" anchor="warp" %}Anomaly Target{% /xref %}. On a hit, roll `1d36` (for non-costal areas: `1d26`) or simply choose – re-roll any Anomalies with effects that contradict or simply have no effect. + +**Reach =>** an anomaly covers one Area and changes every read of the values it names inside it + +**Timing =>** Each effect applies at the step that sets its value. A set replaces what its step would roll. + +**Map Facts =>** Drawn rivers and coasts stay; the anomaly only changes what passed through them + +{% tableWrapper + variant="default" + caption="Random Table – World Anomalies" + columns=["num", "rowheader", "default", "default"] %} +{% table %} +- d36 +- Anomaly +- Description +- Effect +--- +- **11** +- Upthrust +- Raised ground far from any range +- `elevation +1` +--- +- **12** +- Sunken Basin +- Ground dropped below its surroundings +- `elevation -1` +--- +- **13** +- Verdant +- Unnaturally lush growth +- `cover = Forest` +--- +- **14** +- Glassed +- Ground fused to Glass +- `cover = Barren` +--- +- **15** +- Sinkland +- Sinkholes and caves +- `terrain = Karst` +--- +- **16** +- Wellspring +- Springs rising from the ground +- `1d3` lakes +--- +- **21** +- Thunderhead +- Violent wet season +- `moisture = Wet` +--- +- **22** +- Searing Wind +- A hot dry wind that never stops +- `moisture = Arid` +--- +- **23** +- Wind-scoured +- Wind-stripped country +- `Wind level = 4` +--- +- **24** +- Dead Calm +- Air that never moves +- `Wind level = 0` +--- +- **25** +- Pall +- Permanent overcast +- `band -1` +--- +- **26** +- Kiln +- Ground warm from below +- `band +1` +--- +- **31** +- Drowned Coast +- Sunken coast; marsh and shallow +- `coastal drainage -2` +--- +- **32** +- Skerries +- A coast broken into rocky islets +- `coastal drainage +2` +--- +- **33** +- Upwelling +- Cold, rich water rising at the coast +- `current = cold` +--- +- **34** +- Warm Drift +- Warm water running along the coast +- `current = warm` +--- +- **35** +- Banks +- Wide, shallow shelf offshore +- Shelf reaches 3 areas from land +--- +- **36** +- Abyssal Shore +- Deep water at the shoreline +- no Shelf; Deep starts at the coast +{% /table %} +{% /tableWrapper %} + +## Elevation + +1. Count the distance in Areas to each range within 3 Areas +1. Read each range's column, and take the highest result + +{% tableWrapper + variant="default" + columns=["rowheader", "key", "key", "key", "key"] %} +{% table %} +- Range +- 0 +- 1 +- 2 +- 3+ +--- +- High +- Montane +- Montane +- Upland +- Lowland +--- +- Medium +- Montane +- Upland +- Lowland +- Lowland +--- +- Low +- Upland +- Lowland +- Lowland +- Lowland +--- +- Rift +- Lowland +- Upland +- Lowland +- Lowland +--- +- Hotspot +- Montane +- Lowland +- Lowland +- Lowland +{% /table %} +{% /tableWrapper %} + +## Band and Current + +The Area takes its band from the World map. A coastal Area also takes a current from its band's prevailing wind. + +1. Take the band from the World map +1. A coastal Area facing open sea takes a current, unless its Wind level is 0. A windward coast has a warm current: `band +1`. A leeward coast has a cold current: `band −1`. An Area with both takes the warm current. + +**Open Sea =>** Ocean or Deep within 2 Areas of the coast + +## Moisture + +1. Roll `2d6`, and add the {% xref reference="pm-world" anchor="water" %}Water{% /xref %} moisture and the belt shift: a wet belt adds, a dry belt subtracts, and Wind level sets how much. A neutral belt adds nothing. + +{% tableWrapper + variant="default" + caption="Belt Shift" + columns=["rowheader", "num", "num", "num"] %} +{% table %} +- Wind level +- 0 +- 1–2 +- 3–4 +--- +- Wet belt +- 0 +- `+1` +- `+1` +--- +- Dry belt +- 0 +- `-1` +- `-2` +{% /table %} +{% /tableWrapper %} + +{% tableWrapper + variant="default" + caption="Moisture" + columns=["rowheader", "key", "key", "key"] %} +{% table %} +- Total +- ≤5 +- 6–9 +- 10+ +--- +- Moisture +- Arid +- Moderate +- Wet +{% /table %} +{% /tableWrapper %} + +**Fogbound =>** a coastal Area with a cold current that reads Arid is Fogbound: `cover +1` + +**Salt-Flat =>** A lake in an Arid Area reads Salt-flat + +## Terrain + +Read the first terrain whose condition the Area meets, top to bottom. If none applies, the Area has no terrain. + +{% tableWrapper variant="default" columns=["rowheader", "default", "key"] %} +{% table %} +- Terrain +- Description +- Condition +--- +- Glacial +- ice-scoured ground +- `band = Arctic | Subarctic || elevation = Montane` +--- +- Plateau +- high, flat ground +- `elevation = Upland && origin = Uplift | Rift` +--- +- Alluvial +- river-laid soil +- `elevation = Lowland && has river` +--- +- Dissection +- dry ground cut by gullies +- `moisture = Arid` +--- +- Karst +- limestone dissolved into caves +- `elevation = Lowland && origin = Uplift` +{% /table %} +{% /tableWrapper %} + +**Scoured =>** On a {% xref reference="pm-world" anchor="world-anomalies" %}Scoured{% /xref %} world, Temperate Areas also read Glacial. + +## Lakes + +1. Read the Area's lakes: where a river cannot reach the sea, on a Rift floor, and from anomalies +1. A river crossing the Area rolls `1d6`. On a `6`, or `5–6` where `terrain = Glacial`, it widens into a lake and flows on +1. A lake without an outflow in an Area with `moisture = Arid` reads Salt Basin, unless its spring-fed. +1. Read each lake's size from the river that feeds or flows through it + +{% tableWrapper + variant="default" + caption="Lake Size" + columns=["rowheader", "key", "key", "key", "key"] %} +{% table %} +- River Tier +- River +- Major River +- Grand River +- no River +--- +- Lake Size +- Tract +- Region +- Area +- Tract +{% /table %} +{% /tableWrapper %} diff --git a/src/content/general/articles/pm-locality.mdoc b/src/content/general/articles/pm-locality.mdoc new file mode 100644 index 0000000..5599b7c --- /dev/null +++ b/src/content/general/articles/pm-locality.mdoc @@ -0,0 +1,549 @@ +--- +title: Locality +status: draft +noindex: false +stage: stub +route: + segment: locality + parent: prima-materia + index: 5 + sequence: false +summary: + cover: + effect: none + ratio: original +published: 2026-10-07T15:27:00.000Z +tags: + - the-crucible + - genesis + - prima-materia +--- +## Overview + +Every locality starts from its tract, rolls its own cover, and may hold a landform and a resource. + +{% definitions variant="compact" %} +{% definition term="Position" kind="entry" %} +Where the Locality lies against water: Floor, Shore or Flank +{% /definition %} + +{% definition term="Landform" kind="entry" %} +A small physical feature of the Locality's ground, such as a meander, a headland, or a spur +{% /definition %} + +{% definition term="Resource" kind="entry" %} +Something the Locality is exceptional for: a deposit, a wild population, or ground ideally suited to it. +{% /definition %} +{% /definitions %} + +### Procedure + +1. Place the Tract's features +1. Read the position +1. Roll the cover +1. Roll the landform +1. Roll the resource + +## Placing + +1. Draw each river and headwaters through the Tract, and mark the localities they cross +1. Mark the Localities on a coast or a lake +1. Put a volcano in the centre Locality. +1. Put a landmark on a fitting Locality: a river landmark on a river, a coast landmark on the coast, any other anywhere + +## Position + +Read the position from what you marked; from left to right – a Locality takes the first position that fits + +{% tableWrapper + variant="default" + columns=["rowheader", "key", "key", "key"] %} +{% table %} +- Position +- Shore +- Floor +- Flank +--- +- Clause +- on a coast or a lake +- a river or headwaters crosses it +- anywhere else +{% /table %} +{% /tableWrapper %} + +## Cover + +1. Take the Tract's cover +1. Roll `1d6` for the Locality's deviation + +{% tableWrapper + variant="default" + caption="Random Table – Cover Deviation" + columns=["rowheader", "key", "key", "key"] %} +{% table %} +- 1d6 +- 1 +- 2–5 +- 6 +--- +- Cover +- one step toward Barren +- – +- one step toward Forest +{% /table %} +{% /tableWrapper %} + +## Landform + +Roll `1d6` on the position's column + +{% tableWrapper + variant="default" + caption="Random Table - Landform" + columns=["num", "key", "key", "key"] %} +{% table %} +- 1d6 +- Floor +- Shore +- Flank +--- +- 1–2 +- – +- – +- – +--- +- 3 +- Terrace +- Beach +- Bench +--- +- 4 +- Meander +- Bay +- Hollow +--- +- 5 +- Confluence +- Headland +- Spur +--- +- 6 +- River Island +- Islet +- Knoll +{% /table %} +{% /tableWrapper %} + +## Resources + +1. Roll `2d6`, plus `1d6` for each that applies: `fertility = Rich`; volcanic; a landform +1. Count the sixes: one gives a Common resource, two an Uncommon resource, three or more a Rare +1. Pick a random among the resources of that tier whose gate the locality passes. + +{% tableWrapper + variant="default" + caption="Random Table – Cover Deviation" + columns=["rowheader", "key", "key", "key", "key"] %} +{% table %} +- Sixes +- 0 +- 1 +- 2 +- 3+ +--- +- Resource +- – +- Common +- Uncommon +- Rare +{% /table %} +{% /tableWrapper %} + +**Step down =>** With no resource of that tier eligible, take the next tier down. + +{% tableWrapper + variant="default" + caption="Resource List" + columns=["rowheader", "default", "default", "key"] %} +{% table %} +- Resource +- Gate +- Bar +- Tier +--- +- Alchemical Earths +- `terrain = Karst | Dissection || volcanic || lake = Salt Basin` +- `cover = Forest` +- Common +--- +- Base Metals +- `drainage = Broken` +- +- Common +--- +- Cattle +- `cover = Grassland | Woodland && elevation ≠ Montane` +- `band = Arctic` +- Common +--- +- Clay +- `position = Floor | Shore || drainage = Sodden` +- `elevation = Montane` +- Common +--- +- Dates +- `moisture = Arid && band = Subtropical | Tropical` +- +- Common +--- +- Dye Plants +- `cover = Grassland | Woodland && fertility ≠ Poor` +- `band = Arctic` +- Common +--- +- Fish +- `coastal || riverine || lake = fresh` +- +- Common +--- +- Flax +- `elevation = Lowland && drainage = Shed` +- `band = Arctic` +- Common +--- +- Fruits +- `cover = Woodland && drainage = Shed` +- `band = Arctic` +- Common +--- +- Grapes +- `drainage = Shed | Broken && band = Temperate | Subtropical` +- +- Common +--- +- Hardwoods +- `cover = Woodland | Forest && band = Temperate | Subtropical | Tropical` +- +- Common +--- +- Hardy Grains +- `cover = Scrub | Grassland && band = Subarctic | Temperate` +- `elevation = Montane` +- Common +--- +- Hemp +- `cover = Grassland | Woodland && drainage = Shed | Sodden` +- `band = Arctic` +- Common +--- +- Herbs +- `cover = Scrub | Woodland` +- +- Common +--- +- Honey +- `cover ≠ Barren && band = Subarctic | Temperate | Subtropical` +- +- Common +--- +- Horses +- `cover = Grassland && drainage = Shed` +- +- Common +--- +- Kelp +- `coastal && band = Arctic | Subarctic | Temperate` +- `world_anomaly = Dead Seas` +- Common +--- +- Ochre +- `drainage = Broken && moisture ≠ Wet` +- `cover = Forest` +- Common +--- +- Oilseeds +- `cover = Grassland | Woodland && drainage = Shed` +- `band = Arctic` +- Common +--- +- Olives +- `climate = Mediterranean | Semiarid && drainage = Shed | Broken` +- `elevation = Montane` +- Common +--- +- Peat +- `drainage = Sodden && band = Arctic | Subarctic | Temperate` +- `cover = Barren` +- Common +--- +- Psychoactives +- `cover = Woodland | Forest` +- +- Common +--- +- Reeds +- `drainage = Sodden || position = Floor` +- `cover = Barren` +- Common +--- +- Rice +- `drainage = Sodden && band = Temperate | Subtropical | Tropical` +- +- Common +--- +- Salt +- `lake = Salt Basin || coastal || terrain = Karst` +- `moisture = Wet` +- Common +--- +- Seals +- `coastal && band = Arctic | Subarctic` +- `world_anomaly = Dead Seas` +- Common +--- +- Semiprecious Stone +- `drainage = Broken || terrain = Karst` +- +- Common +--- +- Sheep/Goats +- `cover = Scrub | Grassland` +- `drainage = Sodden` +- Common +--- +- Shellfish +- `position = Shore` +- `band = Arctic || (world_anomaly = Dead Seas && coastal)` +- Common +--- +- Smallstock +- `cover = Woodland | Forest && elevation ≠ Montane` +- +- Common +--- +- Softwoods +- `cover = Woodland | Forest && band = Subarctic | Temperate` +- +- Common +--- +- Special Lifestock +- `moisture = Arid || elevation = Montane` +- `cover = Forest` +- Common +--- +- Staple Grains +- `cover = Grassland && fertility ≠ Poor` +- `band = Arctic` +- Common +--- +- Stone +- `drainage = Broken || elevation = Montane` +- +- Common +--- +- Wild Game +- `cover = Grassland | Woodland | Forest` +- +- Common +--- +- Alum +- `volcanic || terrain = Karst` +- `drainage = Sodden` +- Uncommon +--- +- Amber +- `coastal && band = Subarctic | Temperate` +- +- Uncommon +--- +- Bitumen +- `terrain = Dissection || lake = Salt Basin` +- `cover = Forest` +- Uncommon +--- +- Cedar +- `cover = Woodland | Forest && climate = Mediterranean | Semiarid` +- +- Uncommon +--- +- Citrus +- `climate = Mediterranean | Sultry && drainage = Shed` +- +- Uncommon +--- +- Cochineal +- `cover = Scrub && band = Subtropical | Tropical` +- +- Uncommon +--- +- Cotton +- `drainage = Shed && band = Subtropical | Tropical` +- +- Uncommon +--- +- Iron +- `drainage = Broken | Sodden` +- +- Uncommon +--- +- Jade +- `origin = Suture && riverine` +- +- Uncommon +--- +- Lacquer +- `cover = Woodland | Forest && moisture = Wet && band = Temperate | Subtropical` +- +- Uncommon +--- +- Marble +- `origin = Suture && drainage = Broken` +- +- Uncommon +--- +- Obsidian +- `volcanic && drainage = Broken` +- +- Uncommon +--- +- Pelts +- `cover = Woodland | Forest && band = Arctic | Subarctic` +- +- Uncommon +--- +- Poppy +- `cover = Grassland && band = Subtropical` +- +- Uncommon +--- +- Saffron +- `climate = Semiarid | Continental && drainage = Shed` +- +- Uncommon +--- +- Silver +- `drainage = Broken && elevation = Upland | Montane` +- +- Uncommon +--- +- Spices +- `cover = Woodland | Forest && band = Tropical` +- +- Uncommon +--- +- Stibnite +- `origin = Arc | Transform && drainage = Broken` +- +- Uncommon +--- +- Sugar +- `riverine && elevation = Lowland && band = Subtropical | Tropical` +- +- Uncommon +--- +- Sulfur +- `volcanic || lake = Salt Basin` +- `drainage = Sodden` +- Uncommon +--- +- Tea +- `elevation = Upland && moisture = Wet` +- +- Uncommon +--- +- Turtles +- `position = Shore && band = Subtropical | Tropical` +- +- Uncommon +--- +- Whales +- `open sea && current = cold` +- `world_anomaly = Dead Seas` +- Uncommon +--- +- White Sand +- `landform = Beach || cover = Barren` +- `drainage = Sodden` +- Uncommon +--- +- Yew +- `cover = Woodland | Forest && elevation = Upland | Montane && band = Subarctic | Temperate` +- +- Uncommon +--- +- Aromatics +- `climate = Desert | Hot Steppe && cover = Scrub` +- +- Rare +--- +- Asbestos +- `origin = Suture && elevation = Upland | Montane` +- +- Rare +--- +- Behemoth +- `climate = Savanna | Monsoon && fertility = Rich` +- +- Rare +--- +- Cinnabar +- `volcanic && elevation = Upland | Montane` +- +- Rare +--- +- Coal +- `origin = Uplift && drainage = Broken` +- +- Rare +--- +- Gold +- `origin = Arc | Island Arc && riverine` +- +- Rare +--- +- Kaolin +- `elevation = Upland && landform = Hollow` +- +- Rare +--- +- Lapis +- `elevation = Montane && landform = Spur` +- +- Rare +--- +- Lotus +- `drainage = Sodden && band = Subtropical | Tropical` +- +- Rare +--- +- Murex +- `position = Shore && band = Subtropical` +- `world_anomaly = Dead Seas` +- Rare +--- +- Nitre +- `moisture = Arid && terrain = Karst | Dissection` +- +- Rare +--- +- Pozzolana +- `volcano = Cone | Shield | Fissure` +- +- Rare +--- +- Precious Stone +- `volcanic && riverine` +- +- Rare +--- +- Silk +- `cover = Woodland && fertility = Rich && band = Temperate | Subtropical` +- +- Rare +--- +- Silphium +- `cover = Scrub && climate = Mediterranean` +- +- Rare +{% /table %} +{% /tableWrapper %} diff --git a/src/content/general/articles/pm-region.mdoc b/src/content/general/articles/pm-region.mdoc new file mode 100644 index 0000000..85a75ec --- /dev/null +++ b/src/content/general/articles/pm-region.mdoc @@ -0,0 +1,304 @@ +--- +title: Region +status: draft +noindex: false +stage: stub +route: + segment: region + parent: prima-materia + index: 3 + sequence: false +summary: + cover: + effect: none + ratio: original +published: 2026-10-07T15:27:00.000Z +tags: + - the-crucible + - genesis + - prima-materia +--- +## Overview + +Every Region starts from its Area and rolls its own gradient, moisture and cover. + +{% definitions variant="compact" %} +{% definition term="Gradient" kind="entry" %} +How steep the Region's ground is, on the scale Gentle → Moderate → Steep. +{% /definition %} + +{% definition term="Moisture" kind="entry" %} +How much rain the Region gets, on the scale Arid → Moderate → Wet. +{% /definition %} + +{% definition term="Climate" kind="entry" %} +The Region's climate, one of 15, from Icecap to Monsoon +{% /definition %} + +{% definition term="Drainage" kind="entry" %} +What happens to water on the Region's ground, on the scale Sodden → Shed → Broken: it stands, runs off, or cuts the ground apart. +{% /definition %} + +{% definition term="Cover" kind="entry" %} +What grows on the Region's ground, on the scale Barren → Scrub → Grassland → Woodland → Forest. +{% /definition %} + +{% definition term="Mouth" kind="entry" %} +The landform where a river meets the sea: Delta, Estuary, Ria, or Fjord. +{% /definition %} +{% /definitions %} + +**Lakes =>** Place the Area's lakes as {% xref reference="pm-area" anchor="lakes" %}Area: Lakes{% /xref %} says. A Region filled by a lake has no ground; skip its steps. + +### Procedure + +1. Roll the gradient +1. Roll the moisture +1. Read the climate +1. Read the drainage +1. Roll the Cover + +## Gradient + +1. Read the first gradient whose condition the Area meets, top to bottom. +1. Roll `1d6` for the Region's deviation. + +{% tableWrapper variant="default" columns=["rowheader", "text"] %} +{% table %} +- Gradient +- Condition +--- +- Steep +- `elevation = Montane || terrain = Glacial` +--- +- Gentle +- `terrain = Plateau` +--- +- Moderate +- `terrain = Dissection | Karst || elevation = Upland` +{% /table %} +{% /tableWrapper %} + +If none of these Conditions apply, the Gradient is Gentle. + +{% tableWrapper + variant="default" + caption="Gradient Deviation" + columns=["rowheader", "key", "key", "key"] %} +{% table %} +- 1d6 +- 1 +- 2–5 +- 6 +--- +- Gradient +- one step gentler +- – +- one step steeper +{% /table %} +{% /tableWrapper %} + +## Moisture + +1. Take the Area's Moisture +1. Roll `1d6` for the Region's deviation +1. Shift for the Region's position: a Region in the lee of a range is drier, a Region on a windward coast is wetter. + +{% tableWrapper + variant="default" + caption="Moisture Deviation" + columns=["rowheader", "key", "key", "key"] %} +{% table %} +- 1d6 +- 1 +- 2–5 +- 6 +--- +- Moisture +- one step drier +- – +- one step wetter +{% /table %} +{% /tableWrapper %} + +{% tableWrapper + variant="default" + caption="Position" + columns=["rowheader", "key", "key", "key"] %} +{% table %} +- Wind Level +- 0 +- 1–2 +- 3–4 +--- +- Lee +- – +- `moisture -1` +- `moisture -2` +--- +- Windward coast +- – +- `moisture +1` +- `moisture +2` +{% /table %} +{% /tableWrapper %} + +**Lee =>** A region is in the lee when Montane ground lies upwind of it within 2 Regions + +## Climate + +Read the Region's band against its moisture. A Montane Region reads its band one step colder. + +{% tableWrapper + variant="default" + columns=["rowheader", "key", "key", "key"] %} +{% table %} +- Band +- Arid +- Moderate +- Wet +--- +- Arctic +- Icecap +- Permafrost +- Polar +--- +- Subarctic +- Cold Steppe +- Boreal +- Subpolar +--- +- Temperate +- Semiarid +- Continental +- Oceanic +--- +- Subtropical +- Desert +- Mediterranean +- Sultry +--- +- Tropical +- Hot Steppe +- Savanna +- Monsoon +{% /table %} +{% /tableWrapper %} + +## Drainage + +1. Read the gradient against the moisture +1. For a coastal region, apply `coastal drainage`. + +{% tableWrapper + variant="default" + columns=["rowheader", "key", "key", "key"] %} +{% table %} +- Gradient +- Arid +- Moderate +- Wet +--- +- Steep +- Broken +- Broken +- Broken +--- +- Moderate +- Broken +- Shed +- Shed +--- +- Gentle +- Shed +- Shed +- Sodden +{% /table %} +{% /tableWrapper %} + +**Mouths =>** Where a river meets the sea, its mouth is named by the Region's drainage: Sodden a Delta, Shed an Estuary, Broken a Ria. Broken ground with `terrain = Glacial` makes Fjord instead. + +## Cover + +1. Roll `2d6`, and add the modifiers for the Region's moisture, band, and drainage. A Montane Region uses the colder band it read for climate. +1. Read the total + +{% tableWrapper + variant="default" + caption="Cover Modifiers - Moisture" + columns=["rowheader", "num", "num", "num"] %} +{% table %} +- Moisture +- Arid +- Moderate +- Wet +--- +- Modifier +- `-2` +- – +- `+2` +{% /table %} +{% /tableWrapper %} + +{% tableWrapper + variant="default" + caption="Cover Modifiers – Band" + columns=["rowheader", "num", "num", "num", "num", "num"] %} +{% table %} +- Band +- Arctic +- Subarctic +- Temperate +- Subtropical +- Tropical +--- +- Modifier +- `-3` +- – +- – +- `-1` +- `+1` +{% /table %} +{% /tableWrapper %} + +{% tableWrapper + variant="default" + caption="Cover Modifiers – Drainage" + columns=["rowheader", "num", "num", "num"] %} +{% table %} +- Drainage +- Sodden +- Shed +- Broken +--- +- Modifier +- `+1` +- – +- `-1` +{% /table %} +{% /tableWrapper %} + +{% tableWrapper + variant="default" + caption="Cover" + columns=["rowheader", "key", "key", "key", "key", "key"] %} +{% table %} +- Total +- ≤3 +- 4–5 +- 6–7 +- 8–9 +- 10+ +--- +- Cover +- Barren +- Scrub +- Grassland +- Woodland +- Forest +{% /table %} +{% /tableWrapper %} + +**Arctic =>** An Arctic Region reads Grassland at most + +**Anomalies =>** An anomaly that sets cover, replaces the roll; {% xref reference="pm-area" anchor="anomalies" %}Fogbound{% /xref %} adds `+1` to it diff --git a/src/content/general/articles/pm-tract.mdoc b/src/content/general/articles/pm-tract.mdoc new file mode 100644 index 0000000..c3b6ca2 --- /dev/null +++ b/src/content/general/articles/pm-tract.mdoc @@ -0,0 +1,348 @@ +--- +title: Tract +status: draft +noindex: false +stage: stub +route: + segment: tract + parent: prima-materia + index: 4 + sequence: false +summary: + cover: + effect: none + ratio: original +published: 2026-10-07T15:27:00.000Z +tags: + - the-crucible + - genesis + - prima-materia +--- +## Overview + +Every Tract starts from its Region and rolls its own relief, fertility, headwaters and landmark. + +{% definitions variant="compact" %} +{% definition term="Fertility" kind="entry" %} +How rich the Tract's soil is, on the scale Poor → Fair → Rich +{% /definition %} + +{% definition term="Headwaters" kind="entry" see="Riverine" %} +Small water that rises in a Tract and runs downhill, below the size of a River +{% /definition %} + +{% definition term="Riverine" kind="entry" see="Headwaters" %} +Crossed by a river or headwaters +{% /definition %} + +{% definition term="Volcanic" kind="entry" see="Volcano" %} +Holding volcanoes or volcanic ground +{% /definition %} + +{% definition term="Volcano" kind="entry" see="volcanic" %} +A single volcanic feature: a Cone, Shield, or Fissure +{% /definition %} + +{% definition term="Landmark" kind="entry" %} +One notable feature of the Tract's ground, water or coast +{% /definition %} + +{% definition term="Crossable" kind="entry" %} +Open to travel: on foot across ground and rivers, by landing from a sea on a coast +{% /definition %} +{% /definitions %} + +### Procedure + +1. Roll for landmark +1. Roll for volcano +1. Roll the relief +1. Roll the headwaters +1. Roll the fertility +1. Read the cover + +## Landmark + +Roll `1d6`. On a `6`, the Tract has a landmark: roll `1d36`; a non-coastal Tract rolls `1d26`; re-roll any Landmarks that fail the gate. + +**Timing =>** Each effect applies at the step that sets its value; lakes, crossings, and coast effects apply at once. A set replaces what its step would read or roll. + +{% tableWrapper + variant="default" + caption="Random Table – World Anomalies" + columns=["num", "rowheader", "default", "key", "default"] %} +{% table %} +- d36 +- Landmark +- Description +- Gate +- Effect +--- +- **11** +- Fen +- Waterlogged Ground +- – +- `drainage = Sodden` +--- +- **12** +- Gullies +- Ground cut into channels +- – +- `drainage = Broken` +--- +- **13** +- Loam +- Deep, dark soil +- – +- `fertility = Rich` +--- +- **14** +- Scree +- Loose stone over thin soil +- – +- `fertility = Poor` +--- +- **15** +- Caverns +- A cave passage beneath the land +- `terrain = Karst` or volcanic +- every barrier in the Tract crossable underground +--- +- **16** +- Scarp +- A long cliff across the land +- – +- Uncrossable +--- +- **21** +- Spring +- Water rising from the ground +- – +- headwaters rise here +--- +- **22** +- Swallow-hole +- A shaft that takes the water +- – +- no headwaters rise here +--- +- **23** +- Mere +- Standing water +- – +- a lake +--- +- **24** +- Dry Lake +- A lakebed without water +- – +- a Salt Basin +--- +- **25** +- Ford +- Shallows across the river +- a river +- river crossable +--- +- **26** +- Gorge +- The river cut deep into rock +- a river +- river uncrossable +--- +- **31** +- Harbour +- A sheltered landing +- Coastal +- coast crossable +--- +- **32** +- Sea Cliffs +- Rock walls over the sea +- Coastal +- coast uncrossable +--- +- **33** +- Salt Marsh +- Tidal mud and grasses +- Costal +- `drainage = Sodden` +--- +- **34** +- Sea Stacks +- Rock pillars off a broken shoore +- Coastal +- `drainage = Broken` +--- +- **35** +- Spit +- A tongue of land into the sea +- Coastal +- the coast reaches one Locality out to sea +--- +- **36** +- Cove +- A bay cut into the shore +- Coastal +- the sea reaches one Locality inland +{% /table %} +{% /tableWrapper %} + +## Volcano + +1. If the Tract is volcanic, roll `1d6`. On a `6`, it has a volcano +1. Read the volcano's type from the origin of the margin or hotspot + +**Volcanic =>** A Tract is volcanic when its Area lies on a volcanic margin or hotspot + +{% tableWrapper + variant="default" + columns=["rowheader", "default", "default", "key"] %} +{% table %} +- Type +- Origin +- Description +- Effect +--- +- Cone +- Arc | Island Arc +- Steep, explosive peak +- `gradient = Steep` +--- +- Shield +- Hotspot +- Broad, gentle dome +- `fertility = Rich` +--- +- Fissure +- Rift | Transform +- Cracks and lava fields +- `cover = Barren` +{% /table %} +{% /tableWrapper %} + +## Relief + +1. Roll `1d6` +1. A shifted Tract moves its elevation and its gradient one step the same way, and re-reads its drainage from the {% xref reference="pm-region" anchor="#drainage" %}Region's table{% /xref %}. +1. A Tract shifted into Montane reads its band one step colder and re-reads its climate. + +{% tableWrapper + variant="default" + columns=["rowheader", "key", "key", "key"] %} +{% table %} +- 1d6 +- 1 +- 2–5 +- 6 +--- +- Relief +- one step down +- – +- one step up +{% /table %} +{% /tableWrapper %} + +**Barriers =>** Montane ground, Major and Grand Rivers, and Broken coasts are barriers. Landmarks and saddles open or close them. + +**Saddle =>** A Tract that Relief shifts down out of Montane is a saddle across the range. + +## Headwaters + +1. Roll `1d6` and add the modifiers for the Tract's gradient and moisture +1. On `6+`, headwaters rise in the Tract and join the nearest rivers, lake or coast. If a range lies between, they end in a lake. + +{% tableWrapper + variant="default" + caption="Fertility Modifiers" + columns=["default", "num"] %} +{% table %} +- Clause +- Modifier +--- +- `gradient = Gentle` +- `-1` +--- +- `gradient = Steep` +- `+1` +--- +- `moisture = Arid` +- `-1` +--- +- `moisture = Wet` +- `+1` +{% /table %} +{% /tableWrapper %} + +## Fertility + +1. Roll `1d6` and add the modifiers +1. Read the total + +{% tableWrapper + variant="default" + caption="Fertility Modifiers" + columns=["default", "num"] %} +{% table %} +- Clause +- Modifier +--- +- `terrain = Alluvial` +- `+1` +--- +- volcanic +- `+1` +--- +- `terrain = Glacial | Karst` +- `-1` +--- +- `band = Arctic | Subarctic` +- `-1` +--- +- `moisture = Arid` +- `-1` +--- +- riverine +- `+1` +{% /table %} +{% /tableWrapper %} + +{% tableWrapper + variant="default" + caption="Random Tables – Fertility" + columns=["rowheader", "key", "key", "key"] %} +{% table %} +- Total +- ≤1 +- 2–5 +- 6+ +--- +- Fertility +- Poor +- Fair +- Rich +{% /table %} +{% /tableWrapper %} + +## Cover + +Take the Region's cover, and shift it by every clause that applies. + +{% tableWrapper + variant="default" + caption="Fertility Modifiers" + columns=["default", "num"] %} +{% table %} +- Clause +- Modifier +--- +- `fertility = Poor` +- `-1` +--- +- `fertility = Rich` +- `+1` +--- +- `drainage = Broken` +- `-1` +{% /table %} +{% /tableWrapper %} diff --git a/src/content/general/articles/pm-world.mdoc b/src/content/general/articles/pm-world.mdoc new file mode 100644 index 0000000..e61e71f --- /dev/null +++ b/src/content/general/articles/pm-world.mdoc @@ -0,0 +1,1193 @@ +--- +title: The World +status: draft +noindex: false +stage: stub +route: + segment: the-world + parent: prima-materia + index: 1 + sequence: false +summary: + excerpt: Where we burn the Worlds that make up the setting + cover: + image: /src/assets/content/articles/pm-world/summary/cover/image.jpg + alt: >- + Light bursts through storm cloud and darkness above a primordial sea; a + small figure hovers at the centre of the blaze. + caption: The Creation of Light + credit: John Martin + effect: none + ratio: original +published: 2026-10-01T13:38:00.000Z +tags: + - the-crucible + - genesis + - prima-materia +--- +## Overview + +{% lead %} +World step covers the whole planet and runs once. It has two halves: the **parameters** set the world's physical character in numbers, and the **map** turns those numbers into coasts, plates, margins, and rivers. Every part below reads both. +{% /lead %} + +## Effects + +Every level writes its effects in the same few words. An entry is any row a rule can roll, choose or work: an anomaly, terrain, landmark, crossing, good or mode. + +{% definitions variant="compact" %} +{% definition term="reads" kind="entry" %} +Counts as the named value for every rule that follows. A tract that reads Montane is Montane +{% /definition %} + +{% definition term="shift" kind="entry" %} +Move a value along its scale by the number given, such as `band -1` or `cover +2`. A shift stops at either end of the scale. +{% /definition %} + +{% definition term="requires" kind="entry" %} +The conditions an entry needs to be eligible +{% /definition %} + +{% definition term="blocks" kind="entry" %} +A condition that makes an entry ineligible, regardless of what it requires +{% /definition %} + +{% definition term="eligible" kind="entry" %} +The entry meets its requirements and none of its blocks. It can be rolled, chosen, or worked in that place. +{% /definition %} + +{% definition term="ineligible" kind="entry" %} +The entry cannot appear or be worked in that place. A roll that lands on it simply re-rolls. +{% /definition %} + +{% definition term="favoured" kind="entry" %} +The entry counts one Favour in that place +{% /definition %} + +{% definition term="hinders" kind="entry" %} +The entry counts one Hindrance in that place. +{% /definition %} +{% /definitions %} + +## Parameters + +The parameters describe the world as a whole, before anything is drawn. + +{% definitions variant="compact" %} +{% definition term="Axis" kind="entry" see="degree" %} +One of seven ways a world can differ from baseline +{% /definition %} + +{% definition term="Degree" kind="entry" see="axis" %} +An axis' setting, from -2 to +2. A degree of 0 is baseline +{% /definition %} + +{% definition term="World Anomaly" kind="entry" %} +A strangeness that axes cannot express, rolled or chosen from its own table +{% /definition %} + +{% definition term="Warp" kind="entry" %} +The total of every degree and anomaly. It sets how often Areas hold anomalies of their own. +{% /definition %} +{% /definitions %} + +### Procedure + +1. Set the axes +1. Add World Anomalies +1. Tally the Warp + +### The Axes + +Set each of the seven axes to a degree. Choose the degree, read them of a pre-generated map or roll `2d6` for random generation. + +{% tableWrapper + variant="default" + caption="Random Table – Degree" + columns=["rowheader", "num", "num", "num", "num", "num"] %} +{% table %} +- 2d6 +- 2 +- 3–5 +- 6–8 +- 9–11 +- 12 +--- +- Degree +- -2 +- -1 +- 0 +- +1 +- +2 +{% /table %} +{% /tableWrapper %} + +**Effects =>** Each axis' table lists its effect at every degree. Later rules apply it by referencing the axis: shift the bands by Heat, multiply the plates by tectonics. + +**Causes =>** A cause explains a degree and has no effect on its own. Causes are optional; each axis names some examples. + +{% tableWrapper + variant="default" + caption="Axes Overview" + columns=["rowheader", "default", "text"] %} +{% table %} +- Axis +- Measure +- Target +--- +- Heat +- temperature +- bands +--- +- Size +- planetary mass +- grid; margin height +--- +- Wind +- circulation +- Wind level; strength +--- +- Tilt +- axial tilt +- band widths +--- +- Water +- extent of sea +- land share; moisture +--- +- Tides +- tidal range +- coastline; coastal drainage +--- +- Tectonics +- crustal activity +- plate count +{% /table %} +{% /tableWrapper %} + +#### Heat + +Heat is the world's overall temperature. It moves every band on the map warmer or colder. + +**Colder:** an ash-veiled sky · a cold core · a faint or distant sun + +**Warmer:** two suns · a buried star · thick, slow-burning air + +{% tableWrapper + variant="deviation" + columns=["num", "rowheader", "text", "num"] %} +{% table %} +- Degree +- Position +- Description +- Effect +--- +- -2 +- Frigid +- Ice sheets reach the middle latitudes; winters at the equator +- `bands -2` +--- +- -1 +- Cool +- Long winters; no tropics +- `bands -1` +--- +- 0 +- Baseline +- Earthlike +- – +--- +- +1 +- Sweltering +- Short winters; little polar ice +- `bands +1` +--- +- +2 +- Torrid +- Equator too hot for water; mild poles +- `bands +2` +{% /table %} +{% /tableWrapper %} + +#### Size + +Size is the world's mass. It sets the map grid, and through gravity it raises or lowers every range's height. + +**Smaller:** moon of a giant · stripped to its core · born close to the sun + +**Larger:** born in the cold · two worlds merged · swallowed moons + +{% tableWrapper + variant="deviation" + columns=["num", "rowheader", "text", "num"] %} +{% table %} +- Degree +- Position +- Description +- Effect +--- +- -2 +- Small +- Low gravity; thin air; short distances +- `margin height`\ + `+2` +--- +- -1 +- Lesser +- Light gravity; thinner air +- `margin height`\ + `+1` +--- +- 0 +- Baseline +- Earthlike +- – +--- +- +1 +- Greater +- Heavy gravity; thicker air +- `margin height`\ + `-1` +--- +- +2 +- Vast +- Crushing gravity; dense air; long distances +- `margin height`\ + `-2` +{% /table %} +{% /tableWrapper %} + +#### Wind + +Wind is the strength of the air's circulation. Its Wind level sets how much the moisture belts and the rain shadows matter. + +**Calmer:** a spin slowed by its moon · thick, heavy air · ground warmed evenly from below + +**Stronger:** a fast spin · thin, light air · a fierce sun over icy poles + +{% tableWrapper + variant="deviation" + columns=["num", "rowheader", "text", "num"] %} +{% table %} +- Degree +- Position +- Description +- Effect +--- +- -2 +- Slack +- Still air; no prevailing winds; weathers lingers +- Wind level 0 +--- +- -1 +- Faint +- Light winds; slow-moving weather +- Wind level 1 +--- +- 0 +- Baseline +- Earthlike +- Wind level 2 +--- +- +1 +- Brisk +- Strong winds; frequent storms +- Wind level 3 +--- +- +2 +- Roaring +- Constant gales; violent storms +- Wind level 4 +{% /table %} +{% /tableWrapper %} + +**Still air.** At Wind level 0 there is not prevailing wind and no current, and every belt reads neutral. + +#### Tilt + +Tilt is how far the world's axis leans. It sets the width of each band, and with it the strength of the seasons. + +**Straighter:** a steadying moon · a close sun that wore the tilt away · a heavy equatorial bulge + +**Steeper:** a great impact · a heavy ice cap off the pole · a lost moon + +{% tableWrapper + variant="deviation" + columns=["num", "rowheader", "text", "num"] %} +{% table %} +- Degree +- Position +- Description +- Effect +--- +- -2 +- Upright +- no seasons, the same weather all year +- band widths\ + 2:1:9:1:2 +--- +- -1 +- Slight +- Mild seasons +- band widths\ + 4:1:5:1:4 +--- +- 0 +- Baseline +- Earthlike +- band widths\ + 4:2:2:3:2 +--- +- +1 +- Steep +- Hot summers, bitter winters +- band widths\ + 4:3:1:3:4 +--- +- +2 +- Toppled +- Extreme seasons; polar day and night far from the poles +- band widths\ + 6:1:1:1:6 +{% /table %} +{% /tableWrapper %} + +**Band widths.** The ratio runs from the pole to the equator: Arctic, Subarctic, Temperate, Subtropical, Tropical + +#### Water + +Water is how much of the world is sea. It sets the land share and shifts moisture. + +**Drier:** a sea beneath the crust · thin air leaking water to space · seas boiled off by a young sun + +**Wetter:** water brought by comets · water rising from the deep · a shattered ice moon + +{% tableWrapper + variant="deviation" + columns=["num", "rowheader", "text", "num"] %} +{% table %} +- Degree +- Position +- Description +- Effect +--- +- -2 +- Patched +- Landlocked seas; vast dry interiors +- land 70%;\ + `moisture -2` +--- +- -1 +- Dry +- Large continents; dry interiors +- land 55%;\ + `moisture -1` +--- +- 0 +- Baseline +- Earthlike +- land 30% +--- +- +1 +- Brimming +- Small continents; little land far from the sea +- land 20%;\ + `moisture +1` +--- +- +2 +- Drowned +- Islands only; no land far from the sea +- land 10%;\ + `moisture +2` +{% /table %} +{% /tableWrapper %} + +#### Tides + +Tides is the reach of the tides. it shapes the coastline and shifts the drainage of coastal Regions. + +**Weaker:** no moon · two moons pulling against each other · a sun too distant to pull + +**Stronger:** a close moon · a companions world · a moon on a swinging orbit + +{% tableWrapper + variant="deviation" + columns=["num", "rowheader", "text", "num"] %} +{% table %} +- Degree +- Position +- Description +- Effect +--- +- -2 +- Still +- No tides; stagnant shore +- smooth coast;\ + `coast drainage`\ + `-2` +--- +- -1 +- Weak +- small tides +- smooth coast;\ + `coast drainage`\ + `-1` +--- +- 0 +- Baseline +- Earthlike +- – +--- +- +1 +- Strong +- High tides; wide tidal flats +- ragged coast;\ + `coast drainage`\ + `+1` +--- +- +2 +- Violent +- Extreme tides; coasts flooded and drained daily +- ragged coast;\ + `coast drainage`\ + `+2` +{% /table %} +{% /tableWrapper %} + +#### Tectonics + +Tectonics is how active the crust s. It multiplies the number of plates. + +**Quieter:** a cooled core · a crust to thick to crack · a dry mantle + +**More active:** a young hot core · a close giant's pull · a thin crust + +{% tableWrapper + variant="deviation" + columns=["num", "rowheader", "text", "num"] %} +{% table %} +- Degree +- Position +- Description +- Effect +--- +- -2 +- Dead +- No earthquakes; old worn land +- `plates ÷2` +--- +- -1 +- Sluggish +- Rare earthquakes +- `plates ×¾` +--- +- 0 +- Baseline +- Earthlike +- – +--- +- +1 +- Active +- Frequent earthquakes +- `plates ×1½` +--- +- +2 +- Seething +- Constant earthquakes; frequent eruptions +- `plates ×2` +{% /table %} +{% /tableWrapper %} + +### World Anomalies + +World Anomalies are the world's signatures; strangeness outside the axes, counted in warp. Any world can hold any anomaly. + +1. Author anomalies, skip them, or roll `1d3-1` of them +1. Roll a `1d36` to randomly determine which anomalies the world got. Re-roll any repeats and second Forms. + +{% tableWrapper + variant="default" + caption="Random Table – World Anomalies" + columns=["num", "rowheader", "default", "num", "text"] %} +{% table %} +- d36 +- Anomaly +- Description +- Warp +- Effect +--- +- **11** +- Hearth +- Heat source at one pole +- 2 +- bands run outward from one pole; Tropical there, Arctic opposite +--- +- **12** +- Inverted +- Poles warm, equator cold +- 2 +- band order reversed +--- +- **13** +- Skewed +- Bands at an angle to the poles +- 2 +- bands run diagonally from a chosen corner +--- +- **14** +- Sundered +- One hemisphere hot, the other cold +- 2 +- one hemisphere `bands -2`, the other `bands +2` +--- +- **15** +- Girdled +- One band's air turned against it +- 1 +- one chosen band's belt reads the opposite of its roll; a neutral belt rolls `1d2`: `1` dry, `2` wet +--- +- **16** +- Open Poles +- Warm currents keep the polar seas open +- 1 +- `polar bands +1` +--- +- **21** +- Firebelt +- Volcanism world-wide +- 2 +- every margin is volcanic +--- +- **22** +- Scoured +- Recent ice age; glaciated land +- 1 +- Temperate areas read Glacial at any elevation; Tarn eligible at any elevation +--- +- **23** +- Cratered +- Old impacts; ringed basins +- 1 +- every Area rolls `1d6`; on a `6`, it holds a crater lake +--- +- **24** +- Pack Ice +- Arctic and Subarctic seas frozen over +- 1 +- sea in those bands read as land of the band +--- +- **25** +- Brackish +- No lasting standing water +- 1 +- no lakes; every lake reads Salt-Flat +--- +- **26** +- Dead Seas +- No life in open sea +- 1 +- Fishing eligible only on rivers and lakes{% sidenote type="default" marker="❖" note="This includes resources:\n- Fish and Turtles only in rivers and lakes\n- Shellfish, Seals, Kelp, Whales and Murex ineligible" /%};\ + no Whale Grounds +--- +- **31** +- Hollow +- Inner surface of a hollow sphere +- 3 +- [see below](#hollow) +--- +- **32** +- Flat +- Flat disc; rimmed +- 3 +- [see below](#flat) +--- +- **33** +- Torus +- Ring-shaped; inner sea +- 3 +- [see below](#torus) +--- +- **34** +- Shard +- Fragment of a world; open edges +- 3 +- [see below](shard) +--- +- **35** +- Ringed +- Planetary ring; shadow band +- 1 +- any chosen Area row `bands -2` +--- +- **36** +- World Rift +- Planet-spanning rift +- 2 +- one Rift margin crosses the world, pole to pole +{% /table %} +{% /tableWrapper %} + +#### Forms + +The Form is the shape of the world itself. Every world has one, the default is **Spheroid,** or Hollow, Flat, Torus, or Shard from a World Anomaly. + +{% definitions variant="compact" %} +{% definition term="Spheroid" kind="form" %} +An ordinary globe. Bands run by latitude from each pole to the equator, and the map wraps east to west{% sidenote type="optional" marker="✱" note="You can place your poles in the east and west and your bands would run by longitude and the map wraps then north to south" /%}. +{% /definition %} + +{% definition term="Hollow" kind="form" %} +The inside of a hollow sphere: the land covers the inner surface, and there is no sky. There are no bands; every Area rolls its band on Rolled Bands instead. Wind is locked at Slack. The map wraps in both directions. +{% /definition %} + +{% definition term="Flat" kind="form" %} +A disc with a rim around its edge. The band forms rings: Tropical at the centre, then Subtropical, Temperate, Subarctic, and Arctic along the rim. Measure the band widths from the rim inward. Measure the band widths from the rim inward. The map has no wrap, and margins stop at the rim. +{% /definition %} + +{% definition term="Torus" kind="form" %} +A ring-shaped world around an inner sea. The bands run Temperate, Subtropical, Tropical, Subtropical, Temperate from the top edge of the map to the bottom; there are no Subarctic or Arctic bands (unless you have a Heat degree or World anomaly). For widths use the Tilt band widths without their Arctic and Subarctic parts. The map wraps in both directions, so the two Temperate edges meet. +{% /definition %} + +{% definition term="Shard" kind="form" %} +A broken fragment of a world with open edges. The bands run by latitude, as on a Spheroid. The map has no wrap, and margins stop at the edge. There is no Deep: every sea hex reads Shelf or Ocean. Land in the Areas along the edge breaks up into islands. +{% /definition %} +{% /definitions %} + +### Warp + +Warp is the total of all degrees (each counted regardless of -/+), plus every anomaly's warp. It sets the target for Area Anomalies. + +{% tableWrapper + variant="default" + columns=["rowheader", "num", "num", "num", "num"] %} +{% table %} +- Warp +- 0–3 +- 4–7 +- 8–12 +- 13+ +--- +- Anomaly Target +- `12+` +- `11+` +- `10+` +- `9+` +{% /table %} +{% /tableWrapper %} + +## Map + +The map turns parameters into land. The grid and bands lay out the world's shape and climate, the coast divides land from sea, the plates and their margins raise the mountains, and the rivers run from these mountains to the sea. + +1. Layout the grid +1. Set the bands +1. Draw the coasts +1. Place the plates +1. Generate the margins +1. Draw the rivers + +**Drawn Map.** Take the coasts, mountains and rivers from your map, sketch plates to match, and roll only what the map lacks. + +### Grid + +The grid sets how many Areas the world has. Larger worlds have more; the table gives an example grid for each Size. + +{% tableWrapper + variant="default" + caption="Example Grid Sizes" + columns=["rowheader", "default", "num", "num"] %} +{% table %} +- Size +- Radius +- Grid +- Areas +--- +- Small +- 2500km +- 58 × 34 +- 1054 +--- +- Lesser +- 4500km +- 105 × 60 +- 6332 +--- +- Baseline +- 6400km +- 149 × 86 +- 12807 +--- +- Greater +- 8500km +- 198 × 114 +- 22590 +--- +- Vast +- 11000km +- 256 × 148 +- 37833 +{% /table %} +{% /tableWrapper %} + +### Bands + +Bands divide the map by temperature. Each band also carries a prevailing wind, which sets coastal currents, and a belt which sets whether its air brings rain. + +{% definitions variant="compact" %} +{% definition term="Band" kind="entry" %} +A strip of the map with one temperature: Arctic, Subarctic, Temperate, Subtropical, or Tropical, from the poles to the equator. +{% /definition %} + +{% definition term="End Band" kind="entry" %} +Frozen or Scorched: a band beyond the ordinary five, where water is never liquid +{% /definition %} + +{% definition term="Prevailing Wind" kind="entry" %} +The direction a band's wind usually comes from. +{% /definition %} + +{% definition term="Belt" kind="entry" %} +Whether the band's air is wet, neutral or dry. +{% /definition %} + +{% definition term="Windward Coast" kind="entry" %} +A coast facing the prevailing wind's direction, or either direction beside it. Every other coast is leeward. +{% /definition %} +{% /definitions %} + +1. Count the rows from the top edge down the equator. +1. Split those rows by the [Tilt](#tilt) band width, from the pole to the equator. Round each band, and let Temperate take up any difference. Mirror the bands for the south. +1. Roll each band's prevailing wind and belt, once per band and hemisphere +1. Shift the bands with the Band Ladder + +#### Prevailing Winds and Belts + +Roll `1d6`for the wind and `1d6`for the belt, per band and hemisphere. The wind result is the direction the wind comes from. Hex orientation decides which six directions exist. + +{% tableWrapper + variant="default" + caption="Random Table - Prevailing Wind" + columns=["num", "key", "key"] %} +{% table %} +- 1d6 +- Pointy-top +- Flat-top +--- +- **1** +- Northeast +- North +--- +- **2** +- East +- Northeast +--- +- **3** +- Southeast +- Southeast +--- +- **4** +- Southwest +- South +--- +- **5** +- West +- Southwest +--- +- **6** +- Northwest +- Northwest +{% /table %} +{% /tableWrapper %} + +{% tableWrapper + variant="default" + caption="Random Table – Belt" + columns=["rowheader", "key", "key", "key"] %} +{% table %} +- 1d6 +- 1–2 +- 3–4 +- 5–6 +--- +- Belt +- Dry +- Neutral +- Wet +{% /table %} +{% /tableWrapper %} + +For an Earthlike Wind and Belt configuration, use the following table: + +{% tableWrapper + variant="default" + caption="Earthlike Winds & Belts" + columns=["rowheader", "key", "key", "key"] %} +{% table %} +- Band +- North +- South +- Belt +--- +- Arctic +- NE +- SE +- dry +--- +- Subarctic +- SW +- NW +- neutral +--- +- Temperate +- SW +- NW +- wet +--- +- Subtropical +- NE +- SE +- dry +--- +- Tropical +- NE +- SE +- wet +{% /table %} +{% /tableWrapper %} + +**Rows =>** A band's wind and belt stay with the rows. The band ladder changes a band's temperature, never its wind or belt. + +#### Band Ladder + +1. Net [Heat](#heat) and every [World Anomaly](#world-anomalies) that shifts bands into one shift +1. Apply it to each band. A band shifted one step past Arctic or Tropical stays there; two or more steps make it Frozen or Scorched. + +**End Bands.** No shift, feature, or anomaly changes an end band. On the map, it sea reads land of the band: sea floor under Scorched, an ice sheet under Frozen, and coasts end at the band's edge. + +#### Rolled Bands + +On a hollow world, roll each Area's band on `2d6` when the Area is generated. Heat and anomalies then shift the result like any band. + +{% tableWrapper + variant="default" + caption="Random Table - Bands" + columns=["rowheader", "key", "key", "key", "key", "key"] %} +{% table %} +- 2d6 +- 2 +- 3–4 +- 5–9 +- 10–11 +- 12 +--- +- Band +- Arctic +- Subarctic +- Temperate +- Subtropical +- Tropical +{% /table %} +{% /tableWrapper %} + +### Coast + +The coast divides land from sea. Draw it or roll it in [Land Cells](#land-cells). With a drawn coast, count the land, read the [Water](#water) degree off it, and go to [Water Zones](#water-zones). + +{% definitions variant="compact" %} +{% definition term="Cell" kind="entry" %} +A block of the map, one band tall and one column wide. A cell holds many Areas, and the coastline is drawn inside it. +{% /definition %} + +{% definition term="Coastal" kind="entry" %} +A hex or cell that touches the sea +{% /definition %} + +{% definition term="Continent" kind="entry" %} +A contiguous body of land, however large or small. Continents and islands are both landmasses +{% /definition %} + +{% definition term="Continent" kind="entry" %} +A landmass large enough to carry at least one continental crust. Plates read it for crust, and every continent gets at least one river. +{% /definition %} + +{% definition term="Island" kind="entry" %} +Any landmass that is not a continent. An island takes its character from the plate beneath it. +{% /definition %} +{% /definitions %} + +#### Land Cells + +1. Divide the map into x rows, where x is the number of your bands (usually ten), and as many columns as you like. E.g. eight columns give a few large continents; sixteen give more and smaller ones. +1. Roll `1d6` per cell, row by row from top to bottom. Add `+1` if the cell above is land. +1. A cell whose die meets the land threshold is land. +1. Joined lands form a landmass, three or more joined cells are a continent, anything smaller is an island. Cells touch across edges that wrap. +1. Draw the coastline inside the land cells. Still or Weak [Tides](#tides) make smooth coasts with lagoons and deltas. Strong or Violent Tides cut inlets and rias. + +{% tableWrapper + variant="default" + caption="Land Thresholds" + columns=["rowheader", "num", "num", "num", "num", "num"] %} +{% table %} +- Water +- Parched +- Dry +- Baseline +- Brimming +- Drowned +--- +- Threshold +- `3+` +- `4+` +- `5+` +- `6` +- `6`{% sidenote type="default" marker="⟗" note="no `+1` from adjacency; every land cell is an island" /%} +{% /table %} +{% /tableWrapper %} + +#### Water Zones + +Water zones divide the sea by its distance from land + +{% tableWrapper + variant="default" + columns=["rowheader", "num", "num", "num"] %} +{% table %} +- Zone +- Shelf +- Ocean +- Deep +--- +- Areas from land +- `1` +- `2–3` +- `4+` +{% /table %} +{% /tableWrapper %} + +### Plates + +Plates are the slabs of crust that cover the whole world, land and sea alike. Where two plates meet is a margin, and margins are where mountains rise. + +{% definitions variant="compact" %} +{% definition term="Plate" kind="entry" %} +A slab of crust. Together the plates cover the whole map +{% /definition %} + +{% definition term="Crust" kind="entry" %} +Continental where part of a continent lies on the plate, oceanic everywhere else. +{% /definition %} + +{% definition term="Hotspot" kind="entry" %} +A volcanic point inside a plate, away from its margins +{% /definition %} +{% /definitions %} + +1. Roll the number of plates for the world's [Size](#size), and multiply it by [Tectonics](#tectonics) +1. Scatter one seed per plate, e.g. roll a column and a row, and re-roll a seed within 3 Areas of another. A seed can land on land or sea; its plate covers both either way. +1. Roll each plate's speed on `1d3`. Each turn, every plate claims the free areas along its edge, as many rings deep as its speed. Stop when the map is full +1. Mark each plate's crust. +1. Roll `1d6` per plate for a hotspot, on a `6` or `5+` on a Small world. + +{% tableWrapper + variant="default" + caption="Plate Counts" + columns=["rowheader", "num", "num", "num", "num", "num"] %} +{% table %} +- Size +- Small +- Lesser +- Baseline +- Greater +- Vast +--- +- Plates +- `2d3` +- `1d6+4` +- `2d6+8` +- `3d6+18` +- `4d6+32` +{% /table %} +{% /tableWrapper %} + +**Wrap =>** Plates follow the Form's wrap. An edge that does not wrap stops a plate and makes no margin + +**Drawn plates =>** Sketch the plates instead, and skip steps 2 and 3. + +### Margins + +A margin is where two plates meet. How the plates move and what they carry, decides what rises there. Margins and hotspots are the ranges, and every Area reads its elevation from the nearest one. + +{% definitions variant="compact" %} +{% definition term="Margin" kind="entry" %} +The line where two plates meet +{% /definition %} + +{% definition term="Height" kind="entry" %} +How high a margin stands: none, low, medium, or high, or a rift +{% /definition %} + +{% definition term="Origin" kind="entry" %} +The kind of margin, read from its movement, rolled height and crust: Uplift, Rift, Transform, Suture, Arc, Island Arc, or Hotspot. +{% /definition %} + +{% definition term="Volcanic" kind="entry" %} +Holding volcanoes. A margin or hotspot is volcanic by its origin. +{% /definition %} + +{% definition term="Range" kind="entry" %} +A margin of height low or higher, a rift, or a hotspot. Mountains and rivers start from ranges. +{% /definition %} +{% /definitions %} + +1. For each pair of neighbouring plates, roll `1d6` for movement, then `1d6` for height on that movement's column. +1. Read the margin's origin from the rolled height: what it is, whether it is volcanic, and the resources it favours. +1. Shift the height by [Size](#size), along none → low → medium → high. A rift holds against Size. + +{% tableWrapper + variant="default" + caption="Margin Movement" + columns=["rowheader", "key", "key", "key"] %} +{% table %} +- 1d3 +- 1 +- 2 +- 3 +--- +- Movement +- **Away;** the plates pull apart +- **Alongside;** the plates slide past each other +- **Towards;** the plates push together +{% /table %} +{% /tableWrapper %} + +{% tableWrapper + variant="default" + caption="Margin Height" + columns=["num", "key", "key", "key"] %} +{% table %} +- 1d6 +- Away +- Alongside +- Towards +--- +- 1 +- none +- none +- low +--- +- 2 +- low +- low +- low +--- +- 3 +- low +- low +- medium +--- +- 4 +- low +- low +- medium +--- +- 5 +- rift +- medium +- medium +--- +- 6 +- rift +- medium +- high +{% /table %} +{% /tableWrapper %} + +{% tableWrapper + variant="default" + caption="Margin Origins" + columns=["rowheader", "key", "default", "default", "default"] %} +{% table %} +- Origin +- Margin +- Description +- Volcanic +- Favours +--- +- Uplift +- Away, low +- An old, worn rise +- no +- Coal, Stone +--- +- Rift +- Away, rift +- A sunken valley between raised shoulders +- yes +- Alum, Alchemical Earths +--- +- Transform +- Alongside +- A fractured ridge +- Height: medium+ +- Stone, Iron +--- +- Suture +- Towards, continental against continental +- Mountains pushed up inland +- no +- Jade, Asbestos +--- +- Arc +- Towards, continental against oceanic +- A range along the continents edge +- Height: medium+ +- Base Metals, Gold, Sulfur, Pozzolana +--- +- Island Arc +- Towards, oceanic against oceanic +- A line of volcanic islands +- Height: medium+ +- Base Metals, Gold, Sulfur, Pozzolana +--- +- Hotspot +- Hotspot +- A single volcanic peak +- yes +- Obsidian, Sulfur +{% /table %} +{% /tableWrapper %} + +**None =>** A margin of height »none« after [Size](#size] raises nothing and is not a range. + +**Long margin =>** Split a margin longer than 15 Areas at its midpoint, and roll each half. + +#### Islands + +Once the margins exist, give each island an origin. An island on a margin takes that margin's origin. An island on a continental plate is a shelf island and has none. Any other island is its plate's hotspot. The origin decides whether the island is volcanic and which goods it favours. + +### Rivers + +Rivers rise on the ranges and run to the sea. They carry water into dry country and mark the floors of valleys. + +1. Roll `1d6` for each range: `1–2` one river, `3–5` two, `6` three. A Rift's rivers rise on its shoulders. A continent with no range gets one river. +1. Split each range's rivers between its flanks by eye, and draw them downhill to the nearest coast. A river never crosses a range +1. A river with no way to the sea ends in a lake. A Rift holds a lake on its floor. +1. Read each river's tier from its length, in Areas from its source. + +{% tableWrapper variant="default" columns=[] %} +{% table %} +- Tier +- River +- Major River +- Grand River +--- +- Areas from source +- 1–3 +- 4–6 +- 7+ +{% /table %} +{% /tableWrapper %} + +**Confluence =>** Two rivers of equal tier meeting make the rover below one tier higher. A lower tier joining a higher one is absorbed + +**End Bands =>** No water rises, runs or pools in an end band. A range inside one sends its river from the band's edge, and tier counts from there. A river that reaches one stops at its edge: as a Salt-flat against Scorched, as a lake against Frozen. diff --git a/src/content/general/articles/prima-materia.mdoc b/src/content/general/articles/prima-materia.mdoc index 9990edb..80acf50 100644 --- a/src/content/general/articles/prima-materia.mdoc +++ b/src/content/general/articles/prima-materia.mdoc @@ -7,13 +7,13 @@ stage: stub route: parent: stages index: 1 - sequence: false + sequence: true summary: cover: image: /src/assets/content/articles/prima-materia/summary/cover/image.jpg alt: The Great Day of His Wrath credit: John Martin – The Great Day of His Wrath - effect: xeroxed + effect: none ratio: 21-9 published: 2026-09-21T12:35:00.000Z tags: @@ -21,192 +21,48 @@ tags: - alchemical-materialism - genesis --- -{% lead %} -Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. This first paragraph is the lead, so it should be larger and carry the drop cap. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. A sentence with *emphasis*, **strong text**, `inline code`, ~~struck text~~ and an [external link](https://example.com). -{% /lead %} +- **Scope =>** complete generation procedure; world map to locality -## Heading two, long enough to wrap onto a second line - -Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur{% footnote note="A footnote with *emphasis* and a [link](https://example.com)." /%}. Excepteur sint occaecat cupidatat non proident{% sidenote type="default" marker="†" note="A sidenote in the margin, beside the line it belongs to." /%}. - -### Heading three - -Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam. - -#### Heading four - -At vero eos et accusamus et iusto odio dignissimos ducimus. - -## Lists - -- First item -- Second item, with enough text to run past the end of a line and wrap underneath the marker -- Third item - - Nested item - - Another nested item - - Third level - -1. First step -1. Second step - 1. Sub-step - 1. Another sub-step -1. Third step - -## Quotes and rules - -> A plain blockquote, no attribution. -> -> With a second paragraph. - -{% quote - cite="Paracelsus" - source="Die dritte Defension" - href="https://example.com" %} -All things are poison, and nothing is without poison; the dosage alone makes it so a thing is not a poison. -{% /quote %} - ---- - -## Callouts - -{% callout type="default" title="TITA TITLE" %} -I AM DONE WITH THIS SHIT! SO COMPLETELY AND TOTALLY DONE! -{% /callout %} - -{% callout type="optional" title="Note" %} -A default callout with a [link](https://example.com) and `code` inside it. -{% /callout %} - -{% callout type="example" title="Worked example" %} -An example callout containing a list: - -- one -- two -{% /callout %} - -{% reveal summary="Spoiler" %} -Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. This first paragraph is the lead, so it should be larger and carry the drop cap. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. A sentence with *emphasis*, **strong text**, `inline code`, ~~struck text~~ and an [external link](https://example.com). -{% /reveal %} - -## Code - -```js -const crucible = { stage: 'prima-materia', heat: 3 } - -for (const [key, value] of Object.entries(crucible)) { - console.log(`${key}: ${value}`) -} -``` - -## Tables +## Hierarchy {% tableWrapper variant="default" - caption="Stages of the Work" - columns=["rowheader", "key", "num", "num", "text"] %} + caption="Hierarchy of the Zoom Levels" + columns=["rowheader", "num", "num", "default", "default"] %} {% table %} -- Stage -- No -- Heat -- Hours -- Notes +- Level +- Across flats (km) +- Area (km²) +- Children +- Scope --- -- Prima Materia -- I -- 3 -- 0.25 -- The undifferentiated ground; nothing is named yet. +- Area +- 270 +- 63133 +- 9 Regions +- plate, seam, band, base moisture --- -- Calcination -- II -- 7 -- 1.50 -- Burned down to what will not burn. +- Region +- 90 +- 7015 +- 9 Countries +- gradient, climate, drainage, base density --- -- Solutio -- III -- 5 -- 12 -- Dissolved back into flow; forms loosen. +- Country +- 30 +- 779 +- 9 localities +- soil, features --- -- Tincture -- VII +- Locality - 10 -- 120.75 -- The finished colour, carried into everything it touches. +- 87 +- – +- position, density, capacity, surplus {% /table %} {% /tableWrapper %} -## Images +## Sequence -{% figure - image="/src/assets/content/articles/prima-materia/john_martin_-_the_great_day_of_his_wrath_-_google_art_project.jpg" - alt="A placeholder image" - caption="An inline figure with a caption" - credit="Dave Damage" - effect="xeroxed" - ratio="16-9" - size="inline" /%} - -Aliquam erat volutpat. Fusce sed velit at arcu tincidunt vulputate{% sidenote type="example" marker="‡" note="A second sidenote, typed as an example." /%}. Nam vitae lacus a nisl ultrices dictum. - -Finally, a link to another article: {% xref reference="the-crucible" %}The Crucible{% /xref %}. - -## Code - -```ts -const crucible = { stage: 'prima-materia', heat: 3 } - -function temper(work: typeof crucible): 'calcined' | 'raw' { - return work.heat > 5 ? 'calcined' : 'raw' -} -``` - -A fence with no language, which should render unhighlighted and with no label: - -``` -Prima materia — the undifferentiated ground. -Nothing is named yet. -``` - -A long line, which should scroll inside the block rather than widen the article: - -```css -.prose :is(h2, h3, h4) + .table-wrapper thead th { border-block-end: var(--border-card) solid var(--color-border-strong); color: inherit; background-color: transparent; } -``` - -An unknown language, which should render plain and raise a warning in the dev banner: - -```brainfuck -+[----->+++<]>+. -``` - -{% callout type="example" title="Code inside a block" %} -The frame should sit inside the callout body, not fight it: - -```js -export const isTag = (node) => Markdoc.Tag.isTag(node) -``` -{% /callout %} - -## Spoiler - -Duis aute irure dolor in reprehenderit, and then {% spoiler %}the prima materia was the vessel all along{% /spoiler %}, which changes the reading of everything above. - -A spoiler long enough to wrap across a line break: {% spoiler %}Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua{% /spoiler %}. And text after it. - -## Definitions - -{% definitions %} -{% definition term="Prima Materia" kind="prime" see="Calcination, Solutio" %} -The undifferentiated ground. Nothing is named yet, which is precisely what makes it workable. -{% /definition %} - -{% definition term="Calcination" kind="stage" see="Prima Materia" %} -Burned down to what will not burn. -{% /definition %} - -{% definition term="Solutio" kind="stage" %} -Dissolved back into flow, so that forms loosen. -{% /definition %} -{% /definitions %} +1. {% xref reference="pm-world" %}**The World**{% /xref %} +1. {% xref reference="pm-area" %}Area{% /xref %} diff --git a/src/content/general/articles/stages.mdoc b/src/content/general/articles/stages.mdoc index 2e0a118..24945f1 100644 --- a/src/content/general/articles/stages.mdoc +++ b/src/content/general/articles/stages.mdoc @@ -5,11 +5,59 @@ noindex: false stage: stub route: parent: the-crucible + index: 2 sequence: true summary: cover: + image: /src/assets/content/articles/stages/summary/cover/image.jpg + alt: >- + An enormous tiered tower of stone arches rising above a harbour city; its + lower levels are complete while the upper tiers stand unfinished, swarming + with workers, cranes, and scaffolding. + caption: The Tower of Babel + credit: Pieter Bruegel the Elder effect: none - ratio: original + ratio: 21-9 published: 2026-09-21T12:34:00.000Z tags: [] --- +## Overview + +{% tableWrapper + variant="default" + caption="The Seven Stages" + columns=["rowheader", "default", "default"] %} +{% table %} +- Stage +- Generates +- Reads from +--- +- {% xref reference="prima-materia" %}Prima Materia{% /xref %} +- Terrain, climate, rivers, resources +- – +--- +- Calcination +- Stocks, lineages, cultures +- Land +--- +- Fermentation +- Religions, gods +- Land, peoples +--- +- Sublimation +- Magical traditions, lores +- Peoples, faith +--- +- Coagulation +- Vessels +- Peoples, faith, traditions +--- +- Conjunction +- Realms, tribes, settlements +- Vessels, land, peoples +--- +- Dissolution +- War, tribute, treaty, collapse +- Realms +{% /table %} +{% /tableWrapper %} diff --git a/src/content/general/articles/the-crucible.mdoc b/src/content/general/articles/the-crucible.mdoc index 8652269..8b623eb 100644 --- a/src/content/general/articles/the-crucible.mdoc +++ b/src/content/general/articles/the-crucible.mdoc @@ -1,20 +1,71 @@ --- title: The Crucible -status: draft +status: published noindex: false stage: stub route: index: 1 - sequence: false + sequence: true summary: - excerpt: >- - Where we explore the crucible – a materialistic alchemist Worldbuilding - Framework + excerpt: Fantasy setting generator based on the 7 alchemical stages. cover: + image: /src/assets/content/articles/the-crucible/summary/cover/image.jpg + alt: >- + A vast palace hall of colossal columns and towers under a storm-lit sky; + crowds of feasters recoil in terror from glowing writing on the wall. + caption: Belshazzar's Feast + credit: John Martin effect: none - ratio: original + ratio: 21-9 published: 2026-09-21T12:33:00.000Z tags: - the-crucible --- -hmm, do you even sharp? +## Overview + +- Procedural worldbuilding tool for fantasy settings +- Seven stages: same procedure every stage +- Upstream sets what's possible; each stage chooses within it +- Tokens from *degrees* (fixed axes) and *signatures* (declared limitations) +- Procedure: see {% xref reference="alchemical-materialism" %}Alchemical Materialism{% /xref %} + +## Reading Order + +- **Generate =>** start at {% xref reference="prima-materia" %}Prima Materia{% /xref %} +- **Understand/Modify =>** start at {% xref reference="alchemical-materialism" %}Alchemical Materialism{% /xref %} + +## The Seven Stages + +{% table %} +- Stage +- Generates +- Reads from +--- +- {% xref reference="prima-materia" %}*Prima Materia*{% /xref %} +- Terrain, climate, rivers, resources +- – +--- +- *Calcination* +- Stocks, lineages, cultures +- Land +--- +- *Fermentation* +- Religions, gods +- Land, peoples +--- +- *Sublimation* +- Magical traditions, lores +- Peoples, faith +--- +- *Coagulation* +- Vessels +- Peoples, faith, traditions +--- +- *Conjunction* +- Realms, tribes, settlements +- Vessels, land, peoples +--- +- *Dissolution* +- War, tribute, treaty, collapse +- Realms +{% /table %} diff --git a/src/content/general/articles/tincture.mdoc b/src/content/general/articles/tincture.mdoc new file mode 100644 index 0000000..10bc282 --- /dev/null +++ b/src/content/general/articles/tincture.mdoc @@ -0,0 +1,30 @@ +--- +title: Tincture +subtitle: Customs and Colour +status: draft +noindex: false +stage: stub +route: + parent: the-crucible + index: 3 + sequence: false +summary: + excerpt: >- + Flavour tables for cultural customs; the detail that makes a culture + recognisable at the table. + cover: + image: /src/assets/content/articles/tincture/summary/cover/image.jpg + alt: >- + A crowded town square split between tavern and church; on one side a fat + reveller rides a barrel with a roasting spit, on the other a gaunt figure + on a cart offers fish, while dancers, beggars, and merchants fill every + corner. + caption: The Fight Between Carnival and Lent + credit: Pieter Bruegel the Elder + effect: none + ratio: original +published: 2026-10-01T08:03:00.000Z +tags: + - the-crucible + - genesis +--- diff --git a/src/content/general/tags/prima-materia.json b/src/content/general/tags/prima-materia.json new file mode 100644 index 0000000..0167365 --- /dev/null +++ b/src/content/general/tags/prima-materia.json @@ -0,0 +1,4 @@ +{ + "title": "Prima Materia", + "description": "The first stage of the crucible" +} diff --git a/src/content/site/releases/v0-12.yaml b/src/content/site/releases/v0-12.yaml deleted file mode 100644 index 5d67b2c..0000000 --- a/src/content/site/releases/v0-12.yaml +++ /dev/null @@ -1,6 +0,0 @@ -version: '0.12' -date: 2026-09-30T13:00:00.000Z -changes: - - kind: added - article: prima-materia - text: Section on the ground state; first table of correspondences diff --git a/src/content/site/releases/v0-13.yaml b/src/content/site/releases/v0-13.yaml deleted file mode 100644 index 45fbe48..0000000 --- a/src/content/site/releases/v0-13.yaml +++ /dev/null @@ -1,12 +0,0 @@ -version: '0.13' -date: 2026-09-30T13:00:00.000Z -changes: - - kind: added - article: calcination - text: Test - - kind: added - article: fermentation - text: Big test with a longer sentence - - kind: changed - article: coagulation - text: Lorem ipsum, blablabla ipbsum whorum diff --git a/src/content/site/releases/v0-14.yaml b/src/content/site/releases/v0-14.yaml deleted file mode 100644 index c3c5176..0000000 --- a/src/content/site/releases/v0-14.yaml +++ /dev/null @@ -1,19 +0,0 @@ -version: '0.14' -date: 2026-09-30T13:00:00.000Z -changes: - - kind: added - article: coagulation - text: >- - s the raw release exactly as Keystatic reads it, inferred from the schema - like the other three - - kind: changed - article: prima-materia - text: >- - releases are loaded, resolved and attached to articles. Six files; nothing - renders yet. - - kind: fixed - article: fermentation - text: We don't burn the priests anymore, we burn the gods - - kind: removed - article: the-crucible - text: AND EVERYTHING GOES BOOOOOOOM diff --git a/src/keystatic/blocks/general/color.ts b/src/keystatic/blocks/general/color.ts new file mode 100644 index 0000000..ac1a8e3 --- /dev/null +++ b/src/keystatic/blocks/general/color.ts @@ -0,0 +1,19 @@ +import { fields } from '@keystatic/core' +import { mark } from '@keystatic/core/content-components' + +import { paletteIcon } from '@keystar/ui/icon/icons/paletteIcon' + +import { COLOR_VARIANTS } from '@/lib/constants/blocks/color' + +export const color = mark({ + label: 'Colour', + icon: paletteIcon, + tag: 'span', + schema: { + variant: fields.select({ + label: 'Colour', + options: COLOR_VARIANTS, + defaultValue: 'lava-text', + }), + }, +}) diff --git a/src/keystatic/blocks/general/definition-list/definitions.ts b/src/keystatic/blocks/general/definition-list/definitions.ts index 5207e3c..c97b116 100644 --- a/src/keystatic/blocks/general/definition-list/definitions.ts +++ b/src/keystatic/blocks/general/definition-list/definitions.ts @@ -1,5 +1,8 @@ +import { fields } from '@keystatic/core' import { repeating } from '@keystatic/core/content-components' +import { DEFINITION_VARIANTS } from '@/lib/constants/blocks/definitions' + export const definitions = repeating({ label: 'Definitions', description: 'A list of terms defined for this article only', @@ -9,5 +12,11 @@ export const definitions = repeating({ min: 1, }, }, - schema: {}, + schema: { + variant: fields.select({ + label: 'Style', + options: DEFINITION_VARIANTS, + defaultValue: 'cross', + }), + }, }) diff --git a/src/keystatic/blocks/general/element.ts b/src/keystatic/blocks/general/element.ts new file mode 100644 index 0000000..c08c9ab --- /dev/null +++ b/src/keystatic/blocks/general/element.ts @@ -0,0 +1,24 @@ +import { fields } from '@keystatic/core' +import { inline } from '@keystatic/core/content-components' + +import { flameIcon } from '@keystar/ui/icon/icons/flameIcon' + +import { ELEMENT_STYLES, ELEMENTS } from '@/lib/constants/elements' + +export const element = inline({ + label: 'Element', + description: 'An elemental glyph, set in Unigrim Dee', + icon: flameIcon, + schema: { + name: fields.select({ + label: 'Element', + options: ELEMENTS, + defaultValue: 'earth', + }), + style: fields.select({ + label: 'Style', + options: ELEMENT_STYLES, + defaultValue: 'filled', + }), + }, +}) diff --git a/src/keystatic/blocks/general/index.ts b/src/keystatic/blocks/general/index.ts index c439b15..34665f4 100644 --- a/src/keystatic/blocks/general/index.ts +++ b/src/keystatic/blocks/general/index.ts @@ -1,6 +1,8 @@ import { callout } from './callout' +import { color } from './color' import { definition } from './definition-list/definition' import { definitions } from './definition-list/definitions' +import { element } from './element' import { figure } from './figure' import { footnote } from './footnote' import { lead } from './lead' @@ -9,19 +11,23 @@ import { reveal } from './reveal' import { sidenote } from './sidenote' import { spoiler } from './spoiler' import { tableWrapper } from './table-wrapper' +import { underline } from './underline' import { xref } from './xref' export const generalBlocks = (collection: string) => ({ figure: figure(collection), callout, + color, definition, definitions, lead, footnote, + element, sidenote, tableWrapper, quote, reveal, spoiler, + underline, xref: xref(), }) diff --git a/src/keystatic/blocks/general/underline.ts b/src/keystatic/blocks/general/underline.ts new file mode 100644 index 0000000..0e2365e --- /dev/null +++ b/src/keystatic/blocks/general/underline.ts @@ -0,0 +1,10 @@ +import { mark } from '@keystatic/core/content-components' + +import { underlineIcon } from '@keystar/ui/icon/icons/underlineIcon' + +export const underline = mark({ + label: 'Underline', + icon: underlineIcon, + tag: 'u', + schema: {}, +}) diff --git a/src/keystatic/collections/site/releases.ts b/src/keystatic/collections/site/releases.ts index 5a86f54..984c9cc 100644 --- a/src/keystatic/collections/site/releases.ts +++ b/src/keystatic/collections/site/releases.ts @@ -9,6 +9,7 @@ export const releases = collection({ label: 'Releases', slugField: 'version', path: `src/content/site/${RELEASES}/*`, + format: 'json', columns: ['version', 'date'], schema: { version: fields.slug({ diff --git a/src/keystatic/fields/content/content.ts b/src/keystatic/fields/content/content.ts index c4a843f..1010524 100644 --- a/src/keystatic/fields/content/content.ts +++ b/src/keystatic/fields/content/content.ts @@ -7,7 +7,7 @@ export const content = (collection: string) => fields.markdoc({ label: 'Content', options: { - heading: [2, 3, 4], + heading: [2, 3, 4, 5], image: assets(collection), }, components: generalBlocks(collection), diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 9d96066..62004b3 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -46,7 +46,7 @@ const previewUrl = preview ? new URL(preview.src, Astro.site) : undefined - + diff --git a/src/lib/constants/blocks/color.ts b/src/lib/constants/blocks/color.ts new file mode 100644 index 0000000..bec1009 --- /dev/null +++ b/src/lib/constants/blocks/color.ts @@ -0,0 +1,10 @@ +export const COLOR_VARIANTS = [ + { label: 'Lava text', value: 'lava-text' }, + { label: 'Lime text', value: 'lime-text' }, + { label: 'Canary text', value: 'canary-text' }, + { label: 'Lava fill', value: 'lava-fill' }, + { label: 'Lime fill', value: 'lime-fill' }, + { label: 'Canary fill', value: 'canary-fill' }, +] as const + +export type ColorVariant = (typeof COLOR_VARIANTS)[number]['value'] diff --git a/src/lib/constants/blocks/definitions.ts b/src/lib/constants/blocks/definitions.ts new file mode 100644 index 0000000..e08196d --- /dev/null +++ b/src/lib/constants/blocks/definitions.ts @@ -0,0 +1,12 @@ +export const DEFINITION_VARIANTS = [ + { + label: 'Boxed', + value: 'cross', + }, + { + label: 'Compact', + value: 'compact', + }, +] as const + +export type DefinitionVariant = (typeof DEFINITION_VARIANTS)[number]['value'] diff --git a/src/lib/constants/blocks/table.ts b/src/lib/constants/blocks/table.ts index 1cf6168..43c84cf 100644 --- a/src/lib/constants/blocks/table.ts +++ b/src/lib/constants/blocks/table.ts @@ -3,10 +3,18 @@ export const TABLE_VARIANTS = [ label: 'Default', value: 'default', }, + { + label: 'Wide', + value: 'wide', + }, { label: 'Matrix', value: 'matrix', }, + { + label: 'Deviation', + value: 'deviation', + }, ] as const export type TableVariant = (typeof TABLE_VARIANTS)[number]['value'] diff --git a/src/lib/constants/elements.ts b/src/lib/constants/elements.ts new file mode 100644 index 0000000..55e187d --- /dev/null +++ b/src/lib/constants/elements.ts @@ -0,0 +1,20 @@ +export const ELEMENTS = [ + { label: 'Earth', value: 'earth', glyph: 'N' }, + { label: 'Fire', value: 'fire', glyph: 'B' }, + { label: 'Water', value: 'water', glyph: 'H' }, + { label: 'Air', value: 'air', glyph: 'E' }, + { label: 'Aether', value: 'aether', glyph: 'Q' }, + { label: 'Soul', value: 'soul', glyph: 'A' }, + { label: 'Body', value: 'body', glyph: 'C' }, + { label: 'Spirit', value: 'spirit', glyph: 'S' }, +] as const + +export type ElementName = (typeof ELEMENTS)[number]['value'] + +export const ELEMENT_STYLES = [ + { label: 'Filled', value: 'filled' }, + { label: 'Outline', value: 'outline' }, + { label: 'Bare', value: 'bare' }, +] as const + +export type ElementStyle = (typeof ELEMENT_STYLES)[number]['value'] diff --git a/src/lib/constants/terms.ts b/src/lib/constants/terms.ts index 61c951f..eb13385 100644 --- a/src/lib/constants/terms.ts +++ b/src/lib/constants/terms.ts @@ -1,11 +1,6 @@ export const TERM_KINDS = [ { label: 'Entry', value: 'entry' }, - { label: 'Prime', value: 'prime' }, - { label: 'Essence', value: 'essence' }, - { label: 'Element', value: 'element' }, - { label: 'Stage', value: 'stage' }, - { label: 'Tier', value: 'tier' }, - { label: 'Premise', value: 'premise' }, + { label: 'Form', value: 'form' }, ] as const export type TermKind = (typeof TERM_KINDS)[number]['value'] diff --git a/src/lib/markdoc/tags.ts b/src/lib/markdoc/tags.ts index 90e260e..4a6fba9 100644 --- a/src/lib/markdoc/tags.ts +++ b/src/lib/markdoc/tags.ts @@ -4,7 +4,9 @@ import Markdoc, { type Node, type RenderableTreeNode, type Schema } from '@markd import { generalBlocks } from '@/keystatic/blocks/general' +import type { DefinitionVariant } from '../constants/blocks/definitions' import { ARTICLES } from '../constants/collections' +import { ELEMENTS } from '../constants/elements' import { inlineOf } from './notes' import { eachTag, firstTag, isTag } from './tree' import type { NodeState, ResolveLink } from './types' @@ -67,6 +69,7 @@ const generated = tableWrapper: 'TableWrapper', quote: 'Quote', reveal: 'Reveal', + underline: 'u', }, }, }).tags ?? {} @@ -86,6 +89,27 @@ export function createTags(state: NodeState, resolveLink: ResolveLink): Record option.value === name) + + if (!entry) { + state.problems.push({ level: 'error', message: `Unknown element "${name}".` }) + return [] + } + + return new Tag('span', { class: `element-glyph element-glyph--${style}` }, [ + new Tag('span', { class: 'element-glyph__symbol', 'aria-hidden': 'true' }, [ + entry.glyph, + ]), + new Tag('span', { class: 'element-glyph__name' }, [entry.label]), + ]) + }, + }, spoiler: { ...generated.spoiler, transform(node, config) { @@ -96,6 +120,17 @@ export function createTags(state: NodeState, resolveLink: ResolveLink): Record { + definition.attributes.ink = variant === 'cross' + }) + return new Tag('DefinitionList', { variant }, children) + }, + }, lead: { ...generated.lead, transform(node, config) { diff --git a/src/styles/base/colors.css b/src/styles/base/colors.css index c6fa4fb..daeb97f 100644 --- a/src/styles/base/colors.css +++ b/src/styles/base/colors.css @@ -47,6 +47,9 @@ --color-text-disabled: var(--color-palette-stone); --color-text-inverse: var(--color-palette-paper); --color-text-on-accent: var(--color-palette-black-ink); + --color-text-lava: oklch(from var(--color-primary) 55% c h); + --color-text-lime: oklch(from var(--color-secondary) 50% c h); + --color-text-canary: oklch(from var(--color-tertiary) 52% c h); /* == Surfaces == */ --color-surface-base: var(--color-palette-stock); @@ -124,6 +127,9 @@ --color-text-disabled: var(--color-palette-stone); --color-text-inverse: var(--color-palette-black-ink); --color-text-on-accent: var(--color-palette-black-ink); + --color-text-lava: var(--color-primary); + --color-text-lime: var(--color-secondary); + --color-text-canary: var(--color-tertiary); /* == Surfaces: elevation still means lighter == */ --color-surface-base: var(--color-palette-black-ink); diff --git a/src/styles/components/blocks/accent.css b/src/styles/components/blocks/accent.css new file mode 100644 index 0000000..aeee741 --- /dev/null +++ b/src/styles/components/blocks/accent.css @@ -0,0 +1,34 @@ +@layer components.blocks { + .accent--lava-text { + color: var(--color-text-lava); + } + + .accent--lime-text { + color: var(--color-text-lime); + } + + .accent--canary-text { + color: var(--color-text-canary); + } + + .accent--lava-fill, + .accent--lime-fill, + .accent--canary-fill { + @mixin px var(--space-1); + + color: var(--color-text-on-accent); + box-decoration-break: clone; + } + + .accent--lava-fill { + background-color: var(--color-primary); + } + + .accent--lime-fill { + background-color: var(--color-secondary); + } + + .accent--canary-fill { + background-color: var(--color-tertiary); + } +} diff --git a/src/styles/components/blocks/element-glyph.css b/src/styles/components/blocks/element-glyph.css new file mode 100644 index 0000000..2c1f538 --- /dev/null +++ b/src/styles/components/blocks/element-glyph.css @@ -0,0 +1,34 @@ +@layer components.blocks { + .element-glyph { + display: inline-grid; + place-items: center; + + aspect-ratio: 1; + width: 1.1em; + + font-family: var(--font-symbols); + font-size: 1.3em; + font-weight: var(--font-weight-body); + font-style: normal; + line-height: 1; + vertical-align: -0.2em; + } + + .element-glyph--filled { + color: var(--color-text-inverse); + background-color: var(--color-surface-inverse); + } + + .element-glyph--outline { + box-shadow: inset 0 0 0 var(--border-hairline) currentcolor; + } + + .element-glyph--bare { + aspect-ratio: auto; + width: auto; + } + + .element-glyph__name { + @mixin visually-hidden; + } +} diff --git a/src/styles/components/blocks/index.css b/src/styles/components/blocks/index.css index 9aef59c..0cb557c 100644 --- a/src/styles/components/blocks/index.css +++ b/src/styles/components/blocks/index.css @@ -9,3 +9,5 @@ @import url("code.css"); @import url("terms.css"); @import url("spoiler.css"); +@import url("element-glyph.css"); +@import url("accent.css"); diff --git a/src/styles/components/blocks/sidenote.css b/src/styles/components/blocks/sidenote.css index e5957e2..6bc803c 100644 --- a/src/styles/components/blocks/sidenote.css +++ b/src/styles/components/blocks/sidenote.css @@ -1,19 +1,23 @@ @layer components.blocks { .sidenote-ref { - @mixin px var(--size-1); - - font-family: var(--font-mono); + font-family: var(--font-body); font-size: 0.75em; font-weight: var(--font-weight-emphasis); - color: var(--color-text-primary); - text-decoration: none; + line-height: 1; - &:hover { - color: var(--color-primary); - } + & a { + @mixin px var(--size-1); - &:focus-visible { - @mixin focus-ring; + color: var(--color-text-primary); + text-decoration: none; + + &:hover { + color: var(--color-primary); + } + + &:focus-visible { + @mixin focus-ring; + } } } @@ -23,6 +27,8 @@ @mixin border-t var(--border-card), solid, var(--color-border-strong); color: var(--color-text-secondary); + text-align: start; + white-space: normal; /* Hangs, so the first line starts against the marker. */ & .sidenote__marker { diff --git a/src/styles/components/blocks/table-wrapper.css b/src/styles/components/blocks/table-wrapper.css index 532ade1..64e7332 100644 --- a/src/styles/components/blocks/table-wrapper.css +++ b/src/styles/components/blocks/table-wrapper.css @@ -24,6 +24,15 @@ } } + & .table-scroll { + overflow-x: auto; + overscroll-behavior-x: contain; + + &:focus-visible { + @mixin focus-ring; + } + } + & table { @mixin my var(--size-0); @@ -38,22 +47,174 @@ & td { &.text { font-family: var(--font-body); - font-size: 1.077em; - line-height: 1.55; + line-height: 1.7; color: var(--color-text-secondary); text-wrap: pretty; } } + + & code { + @mixin px var(--size-2); + @mixin py var(--size-1); + font-size: inherit; + color: var(--color-text-inverse); + background-color: var(--color-surface-inverse); + box-decoration-break: clone; + } } & .num { font-variant-numeric: tabular-nums; - text-align: right; + text-align: center; white-space: nowrap; } & .key { - font-weight: var(--font-weight-emphasis); + text-align: center; + } + } + + .table-wrapper--wide { + @media (--bp-desktop) { + @mixin me calc(-1 * (var(--layout-gutter) + var(--layout-margin-width))); + } + } + + .table-wrapper--matrix { + & table { + table-layout: fixed; + border-collapse: collapse; + border: var(--border-card) solid var(--color-border-strong); + + & :is(th, td) { + border: var(--border-hairline) solid var(--color-border-subtle); + text-align: center; + } + + & thead th { + border-color: var(--color-border-normal); + color: var(--color-text-inverse); + background-color: var(--color-surface-inverse); + } + + & tr > :first-child { + width: 8rem; + text-align: start; + } + + & tbody tr > :first-child { + @mixin typo_table-header; + border-color: var(--color-border-normal); + color: var(--color-text-inverse); + background-color: var(--color-surface-inverse); + } + + & tbody tr:hover > :first-child { + color: var(--color-primary); + } + + & tbody td { + font-variant-numeric: tabular-nums; + + &:hover { + box-shadow: inset 0 0 0 var(--border-card) var(--color-border-strong); + } + } + } + } + + /* Crosshair: highlight the hovered cell's column, one rule per column. */ + .table-wrapper--matrix table:has(tbody td:nth-child(2):hover) { + & tbody td:nth-child(2) { + background-color: var(--color-surface-hover); + } + + & thead th:nth-child(2) { + color: var(--color-primary); + } + } + + .table-wrapper--matrix table:has(tbody td:nth-child(3):hover) { + & tbody td:nth-child(3) { + background-color: var(--color-surface-hover); + } + + & thead th:nth-child(3) { + color: var(--color-primary); + } + } + + .table-wrapper--matrix table:has(tbody td:nth-child(4):hover) { + & tbody td:nth-child(4) { + background-color: var(--color-surface-hover); + } + + & thead th:nth-child(4) { + color: var(--color-primary); + } + } + + .table-wrapper--matrix table:has(tbody td:nth-child(5):hover) { + & tbody td:nth-child(5) { + background-color: var(--color-surface-hover); + } + + & thead th:nth-child(5) { + color: var(--color-primary); + } + } + + .table-wrapper--matrix table:has(tbody td:nth-child(6):hover) { + & tbody td:nth-child(6) { + background-color: var(--color-surface-hover); + } + + & thead th:nth-child(6) { + color: var(--color-primary); + } + } + + .table-wrapper--matrix table:has(tbody td:nth-child(7):hover) { + & tbody td:nth-child(7) { + background-color: var(--color-surface-hover); + } + + & thead th:nth-child(7) { + color: var(--color-primary); + } + } + + .table-wrapper--matrix table:has(tbody td:nth-child(8):hover) { + & tbody td:nth-child(8) { + background-color: var(--color-surface-hover); + } + + & thead th:nth-child(8) { + color: var(--color-primary); + } + } + + .table-wrapper--deviation { + & table { + table-layout: fixed; + + & thead th { + &:nth-child(1) { + width: 10%; + } + + &:nth-child(2) { + width: 25%; + } + + &:nth-child(3) { + width: 45%; + } + + &:nth-child(4) { + width: 20%; + } + } } } diff --git a/src/styles/components/blocks/terms.css b/src/styles/components/blocks/terms.css index 92d162e..42fc320 100644 --- a/src/styles/components/blocks/terms.css +++ b/src/styles/components/blocks/terms.css @@ -26,12 +26,12 @@ } .term__body { + @mixin typo_body; @mixin ma var(--size-0); } - .term__definition { - @mixin typo_body; - @mixin ma var(--size-0); + :where(.term__body p) { + @mixin my var(--size-0); } .term__see { @@ -100,4 +100,53 @@ background-color: var(--color-surface-elevated-1); } + + .terms--compact { + @mixin my var(--space-9); + @mixin border-t var(--border-hairline), solid, var(--color-border-strong); + + grid-template-columns: minmax(0, 9rem) minmax(0, 1fr); + column-gap: var(--size-9); + } + + .terms--compact .term, + .terms--compact .term__body { + @mixin py var(--size-5); + @mixin border-b var(--border-hairline), solid, var(--color-border-subtle); + } + + .terms--compact .term { + gap: var(--size-2); + justify-content: start; + } + + .terms--compact .term__name { + @mixin typo_heading-5; + } + + .terms--compact .term__body { + @mixin typo_definition-data; + } + + .terms--compact .term__see { + @mixin mt var(--size-3); + + padding-block-start: var(--size-0); + border-block-start: none; + } + + .terms--compact .term:target .term__name { + color: var(--color-primary); + } + + @media (--bp-tablet-down) { + .terms--compact { + grid-template-columns: minmax(0, 1fr); + } + + .terms--compact .term { + padding-block-end: var(--size-0); + border-block-end: none; + } + } } diff --git a/src/styles/components/prose/prose.css b/src/styles/components/prose/prose.css index 6631ba4..120e3ad 100644 --- a/src/styles/components/prose/prose.css +++ b/src/styles/components/prose/prose.css @@ -93,6 +93,34 @@ border-block: var(--border-shell) solid var(--color-text-primary); } + & h5 { + @mixin typo_heading-5; + @mixin mt var(--space-12); + @mixin mb var(--space-4); + + display: flex; + gap: var(--size-4); + align-items: center; + color: var(--color-text-secondary); + + &::before { + content: ""; + + flex: none; + + width: var(--size-3); + height: var(--size-3); + + background-color: var(--color-primary); + } + + &::after { + content: ""; + flex: 1; + border-block-start: var(--border-hairline) solid var(--color-border-strong); + } + } + /* === LISTS === */ & ul, @@ -107,12 +135,13 @@ & li { --prose-line: 1lh; - display: grid; - grid-template-columns: var(--slot-prose-marker) minmax(0, 1fr); - column-gap: var(--slot-prose-gap); - & > * { - grid-column: 2; + position: relative; + padding-inline-start: calc(var(--slot-prose-marker) + var(--slot-prose-gap)); + + &::before { + position: absolute; + inset-inline-start: 0; } & > ul, @@ -121,6 +150,16 @@ @mixin mb var(--size-0); gap: var(--space-3); } + + & > p { + &:first-child { + margin-block-start: var(--size-0); + } + + &:last-child { + margin-block-end: var(--size-0); + } + } } } @@ -129,11 +168,11 @@ &::before { content: ""; - justify-self: center; + inset-block-start: calc((var(--prose-line) - 0.625rem) / 2); + inset-inline-start: calc((var(--slot-prose-marker) - 0.625rem) / 2); width: 0.625rem; height: 0.625rem; - margin-block-start: calc((var(--prose-line) - 0.625rem) / 2); background-color: var(--color-text-primary); } @@ -149,8 +188,8 @@ & ul { & > li { &::before { + inset-block-start: calc((var(--prose-line) - var(--size-2)) / 2); height: var(--size-2); - margin-block-start: calc((var(--prose-line) - var(--size-2)) / 2); background-color: var(--color-text-primary); box-shadow: none; } @@ -169,11 +208,13 @@ &::before { content: counter(step, decimal-leading-zero); + inset-block-start: calc((var(--prose-line) - 1.375rem) / 2); + display: grid; place-items: center; + width: var(--slot-prose-marker); height: 1.375rem; - margin-block-start: calc((var(--prose-line) - 1.375rem) / 2); font-family: var(--font-mono); font-size: 0.75rem; @@ -239,7 +280,7 @@ background-color: var(--color-surface-elevated-1); & code { - @mixin pa var (--size-0); + @mixin pa var(--size-0); font: inherit; background-color: transparent; } @@ -287,6 +328,12 @@ text-decoration: line-through; } + & u { + text-decoration: underline; + text-decoration-thickness: var(--border-hairline); + text-underline-offset: var(--size-2); + } + /* === TABLES === */ table { @mixin typo_table-data; @@ -297,9 +344,10 @@ & th, & td { - @mixin py var(--space-4); - @mixin px var(--space-6); + @mixin py var(--size-3); + @mixin px var(--size-5); @mixin border-b var(--border-hairline), solid, var(--color-border-subtle); + text-align: start; } & thead { diff --git a/src/styles/mixins/typography.css b/src/styles/mixins/typography.css index c54e4bc..dffea7b 100644 --- a/src/styles/mixins/typography.css +++ b/src/styles/mixins/typography.css @@ -33,6 +33,15 @@ letter-spacing: var(--tracking-label); } +@define-mixin typo_heading-5 { + font-family: var(--font-header); + font-size: var(--type--1); + font-weight: var(--font-weight-label); + line-height: var(--leading--1); + text-transform: uppercase; + letter-spacing: var(--tracking-label); +} + @define-mixin typo_code { font-family: var(--font-mono); font-size: var(--type--1);