Pre-PMTools

This commit is contained in:
2026-10-16 19:15:33 +02:00
parent 1bddc5597e
commit 496606653d
51 changed files with 3659 additions and 268 deletions

View File

@@ -32,6 +32,7 @@
"@types/node": "^22.20.4", "@types/node": "^22.20.4",
"@types/react": "^19.2.18", "@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4", "@types/react-dom": "^19.2.4",
"autoprefixer": "^10.6.1",
"eslint": "^10.11.0", "eslint": "^10.11.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-astro": "^3.2.1", "eslint-plugin-astro": "^3.2.1",

24
pnpm-lock.yaml generated
View File

@@ -66,6 +66,9 @@ importers:
'@types/react-dom': '@types/react-dom':
specifier: ^19.2.4 specifier: ^19.2.4
version: 19.3.0(@types/react@19.3.0) version: 19.3.0(@types/react@19.3.0)
autoprefixer:
specifier: ^10.6.1
version: 10.6.1(postcss@8.5.28)
eslint: eslint:
specifier: ^10.11.0 specifier: ^10.11.0
version: 10.11.0 version: 10.11.0
@@ -1470,6 +1473,13 @@ packages:
'@astrojs/markdown-remark': '@astrojs/markdown-remark':
optional: true 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: axobject-query@4.1.0:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1949,6 +1959,9 @@ packages:
resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==} resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==}
engines: {node: '>=20'} engines: {node: '>=20'}
fraction.js@5.3.4:
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
fresh@2.0.0: fresh@2.0.0:
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@@ -4994,6 +5007,15 @@ snapshots:
- uploadthing - uploadthing
- yaml - 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: {} axobject-query@4.1.0: {}
bail@2.0.2: {} bail@2.0.2: {}
@@ -5448,6 +5470,8 @@ snapshots:
dependencies: dependencies:
tiny-inflate: 1.0.3 tiny-inflate: 1.0.3
fraction.js@5.3.4: {}
fresh@2.0.0: {} fresh@2.0.0: {}
fsevents@2.3.3: fsevents@2.3.3:

View File

@@ -7,5 +7,6 @@ export default {
files: ['./src/styles/base/custom-media.css'], files: ['./src/styles/base/custom-media.css'],
}, },
'postcss-custom-media': {}, 'postcss-custom-media': {},
autoprefixer: {},
}, },
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 MiB

View File

@@ -7,9 +7,10 @@ type Props = {
term: string term: string
kind?: TermKind kind?: TermKind
see?: string see?: string
ink?: boolean
} }
const { term, kind = 'entry', see = '' } = Astro.props const { term, kind = 'entry', see = '', ink = false } = Astro.props
const links: SeeLink[] = see const links: SeeLink[] = see
.split(',') .split(',')
@@ -18,6 +19,6 @@ const links: SeeLink[] = see
.map((name) => ({ title: name, href: `#${slugify(name)}` })) .map((name) => ({ title: name, href: `#${slugify(name)}` }))
--- ---
<TermEntry id={slugify(term)} title={term} kind={kind} see={links} ink> <TermEntry id={slugify(term)} title={term} kind={kind} see={links} ink={ink}>
<slot /> <slot />
</TermEntry> </TermEntry>

View File

@@ -1,3 +1,13 @@
<dl class="terms terms--cross"> ---
import type { DefinitionVariant } from '@/lib/constants/blocks/definitions'
type Props = {
variant?: DefinitionVariant
}
const { variant = 'cross' } = Astro.props
---
<dl class:list={['terms', `terms--${variant}`]}>
<slot /> <slot />
</dl> </dl>

View File

@@ -10,6 +10,8 @@ const { variant = 'default', caption } = Astro.props
--- ---
<figure class:list={['table-wrapper', `table-wrapper--${variant}`]}> <figure class:list={['table-wrapper', `table-wrapper--${variant}`]}>
<div class="table-scroll" tabindex="0" role="region" aria-label={caption ?? 'Table'}>
<slot /> <slot />
</div>
{caption && <figcaption>{caption}</figcaption>} {caption && <figcaption>{caption}</figcaption>}
</figure> </figure>

View File

@@ -25,9 +25,7 @@ const label = TERM_KINDS.find((option) => option.value === kind)?.label ?? kind
</dt> </dt>
<dd class="term__body"> <dd class="term__body">
<p class="term__definition">
<slot /> <slot />
</p>
{see.length > 0 && ( {see.length > 0 && (
<p class="term__see"> <p class="term__see">

View File

@@ -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 %}

View File

@@ -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: []
---

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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

View File

@@ -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 %}

File diff suppressed because it is too large Load Diff

View File

@@ -7,13 +7,13 @@ stage: stub
route: route:
parent: stages parent: stages
index: 1 index: 1
sequence: false sequence: true
summary: summary:
cover: cover:
image: /src/assets/content/articles/prima-materia/summary/cover/image.jpg image: /src/assets/content/articles/prima-materia/summary/cover/image.jpg
alt: The Great Day of His Wrath alt: The Great Day of His Wrath
credit: John Martin – The Great Day of His Wrath credit: John Martin – The Great Day of His Wrath
effect: xeroxed effect: none
ratio: 21-9 ratio: 21-9
published: 2026-09-21T12:35:00.000Z published: 2026-09-21T12:35:00.000Z
tags: tags:
@@ -21,192 +21,48 @@ tags:
- alchemical-materialism - alchemical-materialism
- genesis - genesis
--- ---
{% lead %} - **Scope =>** complete generation procedure; world map to locality
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 %}
## Heading two, long enough to wrap onto a second line ## Hierarchy
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
{% tableWrapper {% tableWrapper
variant="default" variant="default"
caption="Stages of the Work" caption="Hierarchy of the Zoom Levels"
columns=["rowheader", "key", "num", "num", "text"] %} columns=["rowheader", "num", "num", "default", "default"] %}
{% table %} {% table %}
- Stage - Level
- No - Across flats (km)
- Heat - Area (km²)
- Hours - Children
- Notes - Scope
--- ---
- Prima Materia - Area
- I - 270
- 3 - 63133
- 0.25 - 9 Regions
- The undifferentiated ground; nothing is named yet. - plate, seam, band, base moisture
--- ---
- Calcination - Region
- II - 90
- 7 - 7015
- 1.50 - 9 Countries
- Burned down to what will not burn. - gradient, climate, drainage, base density
--- ---
- Solutio - Country
- III - 30
- 5 - 779
- 12 - 9 localities
- Dissolved back into flow; forms loosen. - soil, features
--- ---
- Tincture - Locality
- VII
- 10 - 10
- 120.75 - 87
- The finished colour, carried into everything it touches. - –
- position, density, capacity, surplus
{% /table %} {% /table %}
{% /tableWrapper %} {% /tableWrapper %}
## Images ## Sequence
{% figure 1. {% xref reference="pm-world" %}**The World**{% /xref %}
image="/src/assets/content/articles/prima-materia/john_martin_-_the_great_day_of_his_wrath_-_google_art_project.jpg" 1. {% xref reference="pm-area" %}Area{% /xref %}
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 %}

View File

@@ -5,11 +5,59 @@ noindex: false
stage: stub stage: stub
route: route:
parent: the-crucible parent: the-crucible
index: 2
sequence: true sequence: true
summary: summary:
cover: 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 effect: none
ratio: original ratio: 21-9
published: 2026-09-21T12:34:00.000Z published: 2026-09-21T12:34:00.000Z
tags: [] 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 %}

View File

@@ -1,20 +1,71 @@
--- ---
title: The Crucible title: The Crucible
status: draft status: published
noindex: false noindex: false
stage: stub stage: stub
route: route:
index: 1 index: 1
sequence: false sequence: true
summary: summary:
excerpt: >- excerpt: Fantasy setting generator based on the 7 alchemical stages.
Where we explore the crucible – a materialistic alchemist Worldbuilding
Framework
cover: 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 effect: none
ratio: original ratio: 21-9
published: 2026-09-21T12:33:00.000Z published: 2026-09-21T12:33:00.000Z
tags: tags:
- the-crucible - 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 %}

View File

@@ -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
---

View File

@@ -0,0 +1,4 @@
{
"title": "Prima Materia",
"description": "The first stage of the crucible"
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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',
}),
},
})

View File

@@ -1,5 +1,8 @@
import { fields } from '@keystatic/core'
import { repeating } from '@keystatic/core/content-components' import { repeating } from '@keystatic/core/content-components'
import { DEFINITION_VARIANTS } from '@/lib/constants/blocks/definitions'
export const definitions = repeating({ export const definitions = repeating({
label: 'Definitions', label: 'Definitions',
description: 'A list of terms defined for this article only', description: 'A list of terms defined for this article only',
@@ -9,5 +12,11 @@ export const definitions = repeating({
min: 1, min: 1,
}, },
}, },
schema: {}, schema: {
variant: fields.select({
label: 'Style',
options: DEFINITION_VARIANTS,
defaultValue: 'cross',
}),
},
}) })

View File

@@ -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',
}),
},
})

View File

@@ -1,6 +1,8 @@
import { callout } from './callout' import { callout } from './callout'
import { color } from './color'
import { definition } from './definition-list/definition' import { definition } from './definition-list/definition'
import { definitions } from './definition-list/definitions' import { definitions } from './definition-list/definitions'
import { element } from './element'
import { figure } from './figure' import { figure } from './figure'
import { footnote } from './footnote' import { footnote } from './footnote'
import { lead } from './lead' import { lead } from './lead'
@@ -9,19 +11,23 @@ import { reveal } from './reveal'
import { sidenote } from './sidenote' import { sidenote } from './sidenote'
import { spoiler } from './spoiler' import { spoiler } from './spoiler'
import { tableWrapper } from './table-wrapper' import { tableWrapper } from './table-wrapper'
import { underline } from './underline'
import { xref } from './xref' import { xref } from './xref'
export const generalBlocks = (collection: string) => ({ export const generalBlocks = (collection: string) => ({
figure: figure(collection), figure: figure(collection),
callout, callout,
color,
definition, definition,
definitions, definitions,
lead, lead,
footnote, footnote,
element,
sidenote, sidenote,
tableWrapper, tableWrapper,
quote, quote,
reveal, reveal,
spoiler, spoiler,
underline,
xref: xref(), xref: xref(),
}) })

View File

@@ -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: {},
})

View File

@@ -9,6 +9,7 @@ export const releases = collection({
label: 'Releases', label: 'Releases',
slugField: 'version', slugField: 'version',
path: `src/content/site/${RELEASES}/*`, path: `src/content/site/${RELEASES}/*`,
format: 'json',
columns: ['version', 'date'], columns: ['version', 'date'],
schema: { schema: {
version: fields.slug({ version: fields.slug({

View File

@@ -7,7 +7,7 @@ export const content = (collection: string) =>
fields.markdoc({ fields.markdoc({
label: 'Content', label: 'Content',
options: { options: {
heading: [2, 3, 4], heading: [2, 3, 4, 5],
image: assets(collection), image: assets(collection),
}, },
components: generalBlocks(collection), components: generalBlocks(collection),

View File

@@ -46,7 +46,7 @@ const previewUrl = preview ? new URL(preview.src, Astro.site) : undefined
<Font cssVariable="--font-geist-mono" preload /> <Font cssVariable="--font-geist-mono" preload />
<Font cssVariable="--font-blaka" preload /> <Font cssVariable="--font-blaka" preload />
<Font cssVariable="--font-julia-mono" preload /> <Font cssVariable="--font-julia-mono" preload />
<Font cssVariable="--font-unigrim" /> <Font cssVariable="--font-unigrim" preload />
<style is:inline> <style is:inline>
@layer reset, tokens, base, components.prose, components.blocks, utilities; @layer reset, tokens, base, components.prose, components.blocks, utilities;
</style> </style>

View File

@@ -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']

View File

@@ -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']

View File

@@ -3,10 +3,18 @@ export const TABLE_VARIANTS = [
label: 'Default', label: 'Default',
value: 'default', value: 'default',
}, },
{
label: 'Wide',
value: 'wide',
},
{ {
label: 'Matrix', label: 'Matrix',
value: 'matrix', value: 'matrix',
}, },
{
label: 'Deviation',
value: 'deviation',
},
] as const ] as const
export type TableVariant = (typeof TABLE_VARIANTS)[number]['value'] export type TableVariant = (typeof TABLE_VARIANTS)[number]['value']

View File

@@ -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']

View File

@@ -1,11 +1,6 @@
export const TERM_KINDS = [ export const TERM_KINDS = [
{ label: 'Entry', value: 'entry' }, { label: 'Entry', value: 'entry' },
{ label: 'Prime', value: 'prime' }, { label: 'Form', value: 'form' },
{ label: 'Essence', value: 'essence' },
{ label: 'Element', value: 'element' },
{ label: 'Stage', value: 'stage' },
{ label: 'Tier', value: 'tier' },
{ label: 'Premise', value: 'premise' },
] as const ] as const
export type TermKind = (typeof TERM_KINDS)[number]['value'] export type TermKind = (typeof TERM_KINDS)[number]['value']

View File

@@ -4,7 +4,9 @@ import Markdoc, { type Node, type RenderableTreeNode, type Schema } from '@markd
import { generalBlocks } from '@/keystatic/blocks/general' import { generalBlocks } from '@/keystatic/blocks/general'
import type { DefinitionVariant } from '../constants/blocks/definitions'
import { ARTICLES } from '../constants/collections' import { ARTICLES } from '../constants/collections'
import { ELEMENTS } from '../constants/elements'
import { inlineOf } from './notes' import { inlineOf } from './notes'
import { eachTag, firstTag, isTag } from './tree' import { eachTag, firstTag, isTag } from './tree'
import type { NodeState, ResolveLink } from './types' import type { NodeState, ResolveLink } from './types'
@@ -67,6 +69,7 @@ const generated =
tableWrapper: 'TableWrapper', tableWrapper: 'TableWrapper',
quote: 'Quote', quote: 'Quote',
reveal: 'Reveal', reveal: 'Reveal',
underline: 'u',
}, },
}, },
}).tags ?? {} }).tags ?? {}
@@ -86,6 +89,27 @@ export function createTags(state: NodeState, resolveLink: ResolveLink): Record<s
]) ])
}, },
}, },
element: {
...generated.element,
inline: undefined,
transform(node) {
const name = text(node.attributes.name)
const style = text(node.attributes.style) || 'filled'
const entry = ELEMENTS.find((option) => 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: { spoiler: {
...generated.spoiler, ...generated.spoiler,
transform(node, config) { transform(node, config) {
@@ -96,6 +120,17 @@ export function createTags(state: NodeState, resolveLink: ResolveLink): Record<s
]) ])
}, },
}, },
color: {
...generated.color,
transform(node, config) {
const variant = text(node.attributes.variant) || 'lava-text'
return new Tag(
'span',
{ class: `accent accent--${variant}` },
node.transformChildren(config),
)
},
},
sidenote: { sidenote: {
...generated.sidenote, ...generated.sidenote,
transform(node, config) { transform(node, config) {
@@ -110,7 +145,9 @@ export function createTags(state: NodeState, resolveLink: ResolveLink): Record<s
const n = (state.sidenoteCount += 1) const n = (state.sidenoteCount += 1)
const children = inlineOf(text(node.attributes.note)) const children = inlineOf(text(node.attributes.note))
return [ return [
new Tag('a', { class: 'sidenote-ref', id: `snref-${n}`, href: `#sn-${n}` }, [marker]), new Tag('sup', { class: 'sidenote-ref' }, [
new Tag('a', { id: `snref-${n}`, href: `#sn-${n}` }, [marker]),
]),
new Tag( new Tag(
'span', 'span',
{ class: `sidenote sidenote--${noteType}`, id: `sn-${n}`, role: 'note' }, { class: `sidenote sidenote--${noteType}`, id: `sn-${n}`, role: 'note' },
@@ -178,6 +215,19 @@ export function createTags(state: NodeState, resolveLink: ResolveLink): Record<s
] ]
}, },
}, },
definitions: {
...generated.definitions,
transform(node, config) {
const { variant = 'cross' } = node.transformAttributes(config) as {
variant?: DefinitionVariant
}
const children = node.transformChildren(config)
eachTag(children, 'Definition', (definition) => {
definition.attributes.ink = variant === 'cross'
})
return new Tag('DefinitionList', { variant }, children)
},
},
lead: { lead: {
...generated.lead, ...generated.lead,
transform(node, config) { transform(node, config) {

View File

@@ -47,6 +47,9 @@
--color-text-disabled: var(--color-palette-stone); --color-text-disabled: var(--color-palette-stone);
--color-text-inverse: var(--color-palette-paper); --color-text-inverse: var(--color-palette-paper);
--color-text-on-accent: var(--color-palette-black-ink); --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 == */ /* == Surfaces == */
--color-surface-base: var(--color-palette-stock); --color-surface-base: var(--color-palette-stock);
@@ -124,6 +127,9 @@
--color-text-disabled: var(--color-palette-stone); --color-text-disabled: var(--color-palette-stone);
--color-text-inverse: var(--color-palette-black-ink); --color-text-inverse: var(--color-palette-black-ink);
--color-text-on-accent: 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 == */ /* == Surfaces: elevation still means lighter == */
--color-surface-base: var(--color-palette-black-ink); --color-surface-base: var(--color-palette-black-ink);

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -9,3 +9,5 @@
@import url("code.css"); @import url("code.css");
@import url("terms.css"); @import url("terms.css");
@import url("spoiler.css"); @import url("spoiler.css");
@import url("element-glyph.css");
@import url("accent.css");

View File

@@ -1,10 +1,13 @@
@layer components.blocks { @layer components.blocks {
.sidenote-ref { .sidenote-ref {
@mixin px var(--size-1); font-family: var(--font-body);
font-family: var(--font-mono);
font-size: 0.75em; font-size: 0.75em;
font-weight: var(--font-weight-emphasis); font-weight: var(--font-weight-emphasis);
line-height: 1;
& a {
@mixin px var(--size-1);
color: var(--color-text-primary); color: var(--color-text-primary);
text-decoration: none; text-decoration: none;
@@ -16,6 +19,7 @@
@mixin focus-ring; @mixin focus-ring;
} }
} }
}
.sidenote { .sidenote {
@mixin typo_body-small; @mixin typo_body-small;
@@ -23,6 +27,8 @@
@mixin border-t var(--border-card), solid, var(--color-border-strong); @mixin border-t var(--border-card), solid, var(--color-border-strong);
color: var(--color-text-secondary); color: var(--color-text-secondary);
text-align: start;
white-space: normal;
/* Hangs, so the first line starts against the marker. */ /* Hangs, so the first line starts against the marker. */
& .sidenote__marker { & .sidenote__marker {

View File

@@ -24,6 +24,15 @@
} }
} }
& .table-scroll {
overflow-x: auto;
overscroll-behavior-x: contain;
&:focus-visible {
@mixin focus-ring;
}
}
& table { & table {
@mixin my var(--size-0); @mixin my var(--size-0);
@@ -38,22 +47,174 @@
& td { & td {
&.text { &.text {
font-family: var(--font-body); font-family: var(--font-body);
font-size: 1.077em; line-height: 1.7;
line-height: 1.55;
color: var(--color-text-secondary); color: var(--color-text-secondary);
text-wrap: pretty; 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 { & .num {
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
text-align: right; text-align: center;
white-space: nowrap; white-space: nowrap;
} }
& .key { & .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%;
}
}
} }
} }

View File

@@ -26,12 +26,12 @@
} }
.term__body { .term__body {
@mixin typo_body;
@mixin ma var(--size-0); @mixin ma var(--size-0);
} }
.term__definition { :where(.term__body p) {
@mixin typo_body; @mixin my var(--size-0);
@mixin ma var(--size-0);
} }
.term__see { .term__see {
@@ -100,4 +100,53 @@
background-color: var(--color-surface-elevated-1); 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;
}
}
} }

View File

@@ -93,6 +93,34 @@
border-block: var(--border-shell) solid var(--color-text-primary); 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 === */ /* === LISTS === */
& ul, & ul,
@@ -107,12 +135,13 @@
& li { & li {
--prose-line: 1lh; --prose-line: 1lh;
display: grid;
grid-template-columns: var(--slot-prose-marker) minmax(0, 1fr);
column-gap: var(--slot-prose-gap);
& > * { position: relative;
grid-column: 2; padding-inline-start: calc(var(--slot-prose-marker) + var(--slot-prose-gap));
&::before {
position: absolute;
inset-inline-start: 0;
} }
& > ul, & > ul,
@@ -121,6 +150,16 @@
@mixin mb var(--size-0); @mixin mb var(--size-0);
gap: var(--space-3); 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 { &::before {
content: ""; 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; width: 0.625rem;
height: 0.625rem; height: 0.625rem;
margin-block-start: calc((var(--prose-line) - 0.625rem) / 2);
background-color: var(--color-text-primary); background-color: var(--color-text-primary);
} }
@@ -149,8 +188,8 @@
& ul { & ul {
& > li { & > li {
&::before { &::before {
inset-block-start: calc((var(--prose-line) - var(--size-2)) / 2);
height: var(--size-2); height: var(--size-2);
margin-block-start: calc((var(--prose-line) - var(--size-2)) / 2);
background-color: var(--color-text-primary); background-color: var(--color-text-primary);
box-shadow: none; box-shadow: none;
} }
@@ -169,11 +208,13 @@
&::before { &::before {
content: counter(step, decimal-leading-zero); content: counter(step, decimal-leading-zero);
inset-block-start: calc((var(--prose-line) - 1.375rem) / 2);
display: grid; display: grid;
place-items: center; place-items: center;
width: var(--slot-prose-marker);
height: 1.375rem; height: 1.375rem;
margin-block-start: calc((var(--prose-line) - 1.375rem) / 2);
font-family: var(--font-mono); font-family: var(--font-mono);
font-size: 0.75rem; font-size: 0.75rem;
@@ -287,6 +328,12 @@
text-decoration: line-through; text-decoration: line-through;
} }
& u {
text-decoration: underline;
text-decoration-thickness: var(--border-hairline);
text-underline-offset: var(--size-2);
}
/* === TABLES === */ /* === TABLES === */
table { table {
@mixin typo_table-data; @mixin typo_table-data;
@@ -297,9 +344,10 @@
& th, & th,
& td { & td {
@mixin py var(--space-4); @mixin py var(--size-3);
@mixin px var(--space-6); @mixin px var(--size-5);
@mixin border-b var(--border-hairline), solid, var(--color-border-subtle); @mixin border-b var(--border-hairline), solid, var(--color-border-subtle);
text-align: start;
} }
& thead { & thead {

View File

@@ -33,6 +33,15 @@
letter-spacing: var(--tracking-label); 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 { @define-mixin typo_code {
font-family: var(--font-mono); font-family: var(--font-mono);
font-size: var(--type--1); font-size: var(--type--1);