diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..cf0e3e5 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,42 @@ +name: Build and Deploy DAVE | DMGs Site + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 8 + + - name: Cache pnpm store + uses: actions/cache@v4 + with: + path: ~/.pnpm-store + key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm- + + - name: Install dependencies + run: pnpm install --no-frozen-lockfile + + - name: Build + run: pnpm run build + + - name: Deploy + run: | + rm -rf /var/www/dave-dmg/blog/* + cp -r dist/* /var/www/dave-dmg/blog/ diff --git a/markdoc.config.mjs b/markdoc.config.mjs index d4b2233..77b301d 100644 --- a/markdoc.config.mjs +++ b/markdoc.config.mjs @@ -1,4 +1,9 @@ -import { defineMarkdocConfig, component, nodes } from '@astrojs/markdoc/config'; +import { + defineMarkdocConfig, + component, + nodes, + Markdoc, +} from '@astrojs/markdoc/config'; export default defineMarkdocConfig({ nodes: { @@ -16,5 +21,70 @@ export default defineMarkdocConfig({ color: { type: String }, }, }, + Callout: { + render: component('./src/components/content/Callout.astro'), + attributes: { + type: { + type: String, + default: 'default', + }, + title: { type: String }, + }, + }, + Figure: { + render: component('./src/components/content/Figure.astro'), + selfClosing: true, + attributes: { + src: { + type: String, + required: true, + }, + alt: { + type: String, + required: true, + }, + caption: { type: String }, + credit: { type: String }, + }, + }, + Sidenote: { + selfClosing: true, + attributes: { + id: { + type: String, + required: true, + }, + title: { + type: String, + }, + marker: { + type: String, + default: '⋄', + }, + content: { + type: String, + required: true, + }, + type: { + type: String, + default: 'default', + }, + }, + transform(node, config) { + const attrs = node.transformAttributes(config); + return new Markdoc.Tag('sup', {}, [ + new Markdoc.Tag( + 'a', + { + href: `#${attrs.id}`, + id: `ref-${attrs.id}`, + class: 'sidenote-ref', + style: `anchor-name: --note-${attrs.id}`, + }, + [`[${attrs.marker}]`], + ), + ]); + }, + }, }, }); diff --git a/src/components/content/Callout.astro b/src/components/content/Callout.astro new file mode 100644 index 0000000..cd7d071 --- /dev/null +++ b/src/components/content/Callout.astro @@ -0,0 +1,103 @@ +--- +interface Props { + type: | "default" | "optional" | "example" | "spoiler"; + title?: string; +} + +const { type, title} = Astro.props; +const isSpoiler = type === 'spoiler' +--- + +{isSpoiler ? ( +
+ + {title || 'Show spoiler'} + {type.toUpperCase()} + +
+ +
+
+) : ( +
+ {title &&
{title}
} + {type.toUpperCase()} +
+ +
+
+)} + + diff --git a/src/components/content/Figure.astro b/src/components/content/Figure.astro new file mode 100644 index 0000000..63f125c --- /dev/null +++ b/src/components/content/Figure.astro @@ -0,0 +1,63 @@ +--- +import { Image } from 'astro:assets'; + +interface Props { + src: string; + alt: string; + caption?: string; + credit?: string; +} + +const { src, alt, caption, credit } = Astro.props; + +const images = import.meta.glob<{ default: ImageMetadata }>( + '/src/content/**/*.{png,jpg,jpeg,webp,avif}', + { eager: true }, +); + +const imagePath = `/src${src}`; +console.log(imagePath); + +const image = images[imagePath]?.default; +--- + +
+ {image ? ( + {alt} + ) : ( + {alt} + )} + {(caption || credit) && ( +
+ {caption && {caption}} + {credit && {credit}} +
+ )} +
+ + diff --git a/src/components/content/Sidenote.astro b/src/components/content/Sidenote.astro new file mode 100644 index 0000000..ae9ba67 --- /dev/null +++ b/src/components/content/Sidenote.astro @@ -0,0 +1,114 @@ +--- +interface Props { + id: string; + marker: string; + type: string; + content: string; + title: string; +} + +const {id, marker, type, content, title} = Astro.props +--- + +
+
+ {title &&
{title}
} + {type.toUpperCase()} +

{content}

+ +
+
+ + diff --git a/src/components/layout/PostHeader/Cover.astro b/src/components/layout/PostHeader/Cover.astro index 5cb5a19..8e02885 100644 --- a/src/components/layout/PostHeader/Cover.astro +++ b/src/components/layout/PostHeader/Cover.astro @@ -57,8 +57,7 @@ const image = images[imagePath]?.default; } .caption { - padding: var(--spacing-snug); - font-family: var(--font-mono); + padding: var(--space-4); color: var(--color-text-inverse); background-color: var(--color-surface-inverse); } @@ -70,6 +69,6 @@ const image = images[imagePath]?.default; .caption-text, .caption-meta { - font-size: var(--typo-size-sm); + @mixin typo_code; } diff --git a/src/content/articles/alchemical-materialism/index.mdoc b/src/content/articles/alchemical-materialism/index.mdoc index e19d7ac..2d07ce9 100644 --- a/src/content/articles/alchemical-materialism/index.mdoc +++ b/src/content/articles/alchemical-materialism/index.mdoc @@ -21,7 +21,7 @@ seo: - **Foundational system** of the crucible - Generates **entities** – cultures, religions, states, vessels, magical traditions – through **systematic elemental combination,** not arbitrary assignment - Two interlocking element sets - - **Three Primes =>** Soul, Body, Spirit; dynamic forces in tension + - **Three Primes** **=>** Soul, Body, Spirit; dynamic forces in tension - **Five Essences =>** Earth, Fire, Water, Air, Aether; multivalent elemental character - **Elemental Pool Mechanic =>** add tokens from material conditions and history, tally, spend on capabilities, traits, and relationships - **One Engine =>** elemental pool drives everything; capabilities, identity, and cultural character all flow from the same source @@ -210,8 +210,6 @@ seo: - No – the product is neither ingredient {% /table %} ---- - ## The Seven Aspects - Provide additional specificity @@ -243,6 +241,12 @@ seo: - Mercury - Thought, consciousness; intellect, emotion, skills --- +- *Mysteries* +- {% ElementSymbol element="mysteries" size="var(--type-3)" color="inherit" /%} +- Gold +- Sun +- Hidden, occult; magic, destiny, secrets, *residua*{% Sidenote #residua title="What are Residua?" marker="⋄" content="Residua are our fancy name for otherworldly realms – the energies that were left over when the world was created. Think the underworld, inferno, or the elemental planes." type="default" /%} +--- - *Society* - {% ElementSymbol element="society" size="var(--type-3)" color="inherit" /%} - Tin @@ -255,12 +259,6 @@ seo: - Venus - Creation, expression; artistry, rituals, craft --- -- *Mysteries* -- {% ElementSymbol element="mysteries" size="var(--type-3)" color="inherit" /%} -- Gold -- Sun -- Hidden, occult; magic, destiny, secrets, *residua* ---- - *Forces* - {% ElementSymbol element="forces" size="var(--type-3)" color="inherit" /%} - Iron diff --git a/src/content/articles/domain/index.mdoc b/src/content/articles/domain/index.mdoc new file mode 100644 index 0000000..416c5e6 --- /dev/null +++ b/src/content/articles/domain/index.mdoc @@ -0,0 +1,321 @@ +--- +title: Domain +subtitle: What the earth provides +summary: >- + Climate, topography, biome, features, resources. The ground beneath; what + grows, what weathers, what the soil yields. The material conditions that force + civilizations into being. +cover: + showInHeader: false +publishDate: 2026-03-05T08:47:00.000Z +status: published +isFeatured: false +parent: prima-materia +tags: + - Prima Materia + - Generation + - Domain +relatedArticles: [] +seo: + noIndex: false +--- +## Overview + +- **Map is the Starting Point;** napkin sketch or Azgaar export +- Domain = Climate + Topography + Biome + Features + Resources +- **Scale** + - Locality → Territory → Province → Region → Area → Landmass + - *Features* and *resources* are **locality-level** – a river is *here*, iron is in *this* valley + - *Resources* = **abundance,** not exclusive access; most common material is available to anyone; the list marks what a locality has in surplus +- **Climate =>** 10 types; simplified Köppen-Geiger; temperature, precipitation, characteristics +- **Topography =>** 6 types; elevation and terrain character +- **Biome =>** 10 types; dominant vegetation; +- **Features**{% Sidenote #features-resources title="Relevance of features & resources" marker="⋄" content="Features and resources are only relevant on the locality level" type="optional" /%} **=>** 10 types; rivers, coasts, volcanoes, passes, settlements; affect availability tables downstream +- **Resources =>** 75 entries; what the land yields in abundance +- Tables follow below; elemental association appear later when they become mechanically relevant + +## Procedure + +1. **Pick your area or region** – the broad stroke; a continent's western coast, a mountain range, a river basin +1. **Generate localities within it** – each locality gets its own Climate, Topography, Biome + - For those wanting a more random approach, the following [table](https://docs.google.com/spreadsheets/d/16d9MUMFMqbUXkL6bOHN3sbFSvQiR0K3yta--RiW4Ldw/edit?usp=sharing) can be used to generate biomes based on Climate, Topography, and Features +1. **Assign features** – rivers, coasts, passes, settlements +1. **Roll for resources** – each locality has a **1-in-10 chance** to generate a resource +1. **Read the dominant pattern** – the Climate, Topography, and Biome that appear most across localities become the domain signature for the culture living there +{% Callout type="example" title="Gwayth Teyd" %} +Looking at the map for my »Chainbreaker« setting, I decide to start with the area on the western coast of Khimbria – my northern continent which is dominated by temperate rainforest. Knowing that I want to the dominant culture to be inspired by the britannic tribes of the antiquity, I decide to call it: the Gwayth Ted. +{% /Callout %} + +## Climate + +- Borrowed and simplified from Köppen-Geiger climate classification +- Ten types covering the range of conditions your world might produce + +{% table %} +- Climate +- Temp. +- Precipiation +- K-G +- Characteristics +--- +- *Artic* +- Extremely cold winters (< -30°C); cool summers (<10°C) +- Very low; < 300 mm/year +- ET, EF +- Cold year-round; mostly snow; permafrost +--- +- *Arid* +- Hot summers (>30°C) or cold winters (<0°C) +- Very low; < 250mm/year +- BWh, BWk +- True desert; hot (BWh) or cold (BWk) +--- +- *Continental* +- Cold winters (<0°C); hot summers (>25°C) +- Moderate; 500 – 1000 mm/year +- Dfa, Dfb, Dwa, Dwb +- Sharp seasonal swing; summer rainfall +--- +- *Mediterranean* +- Mild winters (0 – 10°C); hot summers (>25°C) +- Moderate; 500–900mm/year; dry summers +- Csa, Csb +- Hot dry summers; wet winters +--- +- *Monsoon* +- Warm year-round (>20°C); mild seasonal variation +- Very high seasonal; 1500–3000+mm/year in wet seasonal +- Am +- Extreme wet/dry cycle; flooding defines calendar +--- +- *Oceanic* +- Mild winters (0–10°C); cool summers (<25°C) +- High; > 800mm/year +- Cfb, Cfc +- Mild year-round; contant rainfall; grey skies +--- +- *Semiarid* +- Cold winters (often <0°C); hot summers (>25°C) +- Low to moderate; 250mm/year +- BSh, BSk +- Steppe; drier than grassland, wetter than desert +--- +- *Subarctic* +- Very cold winters (< -20°C); short summers (<15°C) +- Low to moderate; 300–600mm/year +- Dfc, Dfd, Dwc, Dwd +- Brutal winters; brief growing season +--- +- *Subtropical* +- Mild winters (5–10°C); hot summers (>22°C) +- Moderate to high; 600-1200mm/year +- Cfa, Cwa +- Humid heat; mild winters; even rainfall +--- +- *Tropical* +- Warm year-round; avg > 18°C +- High; > 1500mm/year +- Af, Aw/As +- Constant warmth; heavy rain; Aw/As have dry season +{% /table %} + +## Topography + +- Lay of the land +- Elevation shapes what grows, who lives here, and how hard it is to move + +{% table %} +- Topography +- Elevation +- Characteristics +--- +- *Lowlands* +- Below sea level to 50 m +- Deltas, polders, fens, tidal flats; often waterlogged or reclaimed; fertile but floodprone +--- +- *Plains* +- 50–300 m +- Open steppe, prairie, farmland; wide horizons; wind-exposed; good for agriculture and cavalry +--- +- *Hills* +- 300–800 m +- Rolling terrain; mixed land use; defensible ridgelines; broken sightlines +--- +- *Highlands* +- 800 – 1500 m (flat) +- Elevated plateaus; wind-scoured; isolated; natural fortress country +--- +- *Mountains* +- 1500+ m +- Peaks, passes, and valleys; vertical climate zones; barriers to movement; mineral wealth +--- +- *Badlands* +- Variable +- Eroded, broken terrain; gullies, mesas, exposed rock; poor soil; difficult to settle, easy to hide in +{% /table %} + +## Biome + +- Dominant Vegetation +- What grows tells you what lives + +{% table %} +- Biome +- Description +--- +- *Barren* +- Salt flats, volacanic waste, eroded badlands; no significant vegetation; exposed rock and mineral deposits +--- +- *Tundra* +- Permafrost beneath low scrub, mosses, lichens; treeless; wind-exposed; brief growing season +--- +- *Shrublands* +- Dry-adapted woody scrub; charparral, maquis, fynbos; fire-prone, wind-scoured, quick to regenerate +--- +- *Grasslands* +- Open steppe, prairie, pampas; deep fertile soil beneath tall grasses; few trees, wide horizons +--- +- *Savanna* +- Tropical or subtropical grassland with scattered trees; distinct wet and dry seasons; periodic burns +--- +- *Wetlands* +- Bogs, marshes, fens, swamps; waterlogged ground; dense reed bets, peat deposists; high biodiversity +--- +- *Woods* +- Open canopy with clearings; light penetrates to ground level; mixed undergrowth; edge habitat +--- +- *Forest* +- Dense temperate woodland; heavy canopy; deep leaf litter, large hardoods; oak, beech, maple +--- +- *Rainforest* +- Multi-layered tropical canopy; constant moisture; maximum biodiversity; epiphytes, vines, dense undergrowth +--- +- *Taiga* +- Boreal connifer forest; spruce, pine, larch over muskeg and frozen ground; +{% /table %} + +## Features +- Geographical features at the locality level +{% table %} +- Feature +- Description +--- +- *River* +- Flowing freshwater; trade artery, irrigation source, natural boundary; floods deposit fertile silt +--- +- *Coast* +- Land meets sea; harbors, tidal flats, salt marshes; access to maritime trade and naval power +--- +- *Lake* +- Enclosed body of water; fishery, freshwater reserve, natural landmark; still water hides depth +--- +- *Floodplain* +- Low ground beside rivers; seasonally inundated; rich alluvial soik; high yield, high risk +--- +- *Oasis* +- Isolated water source in arid terrain; date palms, wells, caravan stops; life surrounded by nothing +--- +- *Volcano* +- Active or dormant; volcanic soil is fertile, eruptions devastating; obsidian, sulfur, hot spring nearby +--- +- *Island* +- Land surrounded by water; isolation breeds distinctiveness; defensible, resource-limited, reade-dependent +--- +- *Mountain Pass* +- Navigable route through high terrain; chokepoint for trade and invasion; whoever holds it controls movement +--- +- *Town* +- Permanent settlement; market, craft production, local administration; polulation 1000–5000 +--- +- *City* +- Major settlement; walls, institutions, dense population, surplus extraction, specialization, political gravity; population 5000+ +{% /table %} + +## Resources +- What the land provides +- Each locality has a **1-in-10 chance** to generate a resource +- Mark **abundance** – a defining surplus worth trading or fighting over +- Resources are used in the *Conjuction* state +- Use the following [table](https://docs.google.com/spreadsheets/d/1bUUr5F93SrQ4fZm7ONf2YTudJ-1lnbyKiFHbtLgMgrY/edit?usp=sharing) to roll for the resource of a locality + +| Resources | Description | +|--------------------|-------------------------------------------------------------------------------------------------------| +| Stone | Limestone, granite, sandstone, slate; ubiquitous building and toolmaking material | +| Base Metals | Copper, tin, lead, zinc; smelted from ore into everyday tools, vessels, fittings | +| Vegetables | Root crops, legumes, leafy greens; garden staples grown in moist soil | +| Fruits | Orchard and vine crops; apples, figs, pomegranates, pears; wind-pollinated, sun-ripened | +| Staple Grains | Wheat, barley, millet, sorghum; the caloric foundation of settled civilization | +| Clay | Shaped wet, hardened by kiln; pottery, brick, tile, ceramic | +| Alchemical Earths | Calcium oxide, potash, natron, vitriol; reactive mineral substances used in processing and craft | +| Fish | River and coastal catch; preserved by salt, smoke, or drying | +| Hunting Companions | Hawks, falcons, hounds; trained animals used in pursuit and killing of game | +| Resins | Hardened tree sap; pine tar, frankincense, myrrh; sealant, adhesive, incense | +| Salt | Mineral preservative and seasoning; harvested from sea, spring, or mine | +| Oilseeds | Sesame, flax, rapeseed; pressed and heated into oil for lamps, cooking, and craft | +| Furs | Beaver, rabbit, fox pelts; insulation, clothing, and high-volume trade commodity | +| Livestock | Cattle, sheep, goats, pigs; kept for meat, milk, wool, leather, traction | +| Reeds | Marsh grasses; used for weaving, thatching, basket-making, matting | +| Hemp | Coarse fiber plant; rope, sailcloth, sacking, rough textile | +| Softwoods | Pine, fir, spruce; fast-growing construction timber and primary fuel wood | +| Dye Plants | Indigo, madder, weld; plants processed into colorfast textile dyes | +| Flax | Fine fiber plant; retted and spun into linen; light, breathable fabric and sailcloth | +| Aromatic Herbs | Lavender, rosemary, sage, thyme; culinary and ritual aromatics | +| Woad | Leaf-derived blue pigment; body paint, textile dye; labor-intensive extraction | +| Psychoactives | Mushrooms, ergot, datura, cannabis; mind-altering substances used in ritual and medicine | +| Honey | Bee product; sweetener, preservative, fermented into mead; harvested from wild or kept hives | +| Wild Game | Deer, boar, hare, game birds; hunted in open country and woodland | +| Earth Pigments | Ochre, umber, sienna; mineral pigments ground for paint, body decoration, and burial rites | +| Seals | Marine mammals hunted for blubber, oil, meat, and heavy pelts; cold-water resource | +| Iron | Smelted from bog or mountain ore; tools, weapons, structural hardware; the essential metal | +| Olives | Long-lived orchard tree; fruit pressed into oil for cooking, lighting, preservation, and anointing | +| Hardwoods | Oak, ash, elm, walnut; dense timber for shipbuilding, furniture, tools, construction | +| Jade | Nephrite or jadeite; dense, hard stone worked into blades, ornaments, and ritual objects | +| Dates | Desert palm fruit; high-calorie staple of arid regions; dried for storage and transport | +| Artisan Stone | Alabaster, marble, pumice, soapstone; decorative and sculptural stone shaped by cutting and polishing | +| Dyes | Processed colorants; cochineal, kermes, woad extract; requiring boiling, mordanting, vat fermentation | +| Bamboo | Fast-growing giant grass; hollow, light, strong; construction, scaffolding, weapons, tools | +| Obsidian | Volcanic glass; fractures to razor edges; blades, arrowheads, mirrors | +| Rice | Grain cultivated in flooded paddies; requires irrigation infrastructure and water management | +| Citrus | Lemons, oranges, limes; juicy, acidic fruit; prevents scurvy, flavors food, preserves | +| Papyrus | Aquatic reed processed into writing material; stripped, layered, pressed, dried | +| Grapes | Vine fruit; eaten fresh, dried as raisins, crushed and fermented into wine and vinegar | +| Medicinal Herbs | Healing plants; prepared as poultice, tincture, tea, or salve; specialized knowledge required | +| Special Livestock | Camels, llamas, yaks, reindeer; animals adapted to extreme terrain; transport, milk, fiber, meat | +| Cotton | Tropical fiber plant; ginned, carded, spun into soft, dyeable textile | +| Pelts | Luxury furs; ermine, sable, marten; high-value status goods moving through long-distance trade | +| Horses | Riding, draft, and pack animal; cavalry, courier, transport; requires pasture and fodder | +| Silk | Caterpillar cocoon fiber; reeled, twisted, woven into extremely fine, strong fabric | +| Maize | Corn; high-yield grain requiring processing (nixtamalization); flour, porridge, flatbread | +| Yew | Slow-growing toxic hardwood; exceptional bow-stave timber; durable, flexible, poisonous | +| Tea | Dried leaf steeped in hot water; stimulant beverage; requires specific highland growing conditions | +| Ivory (marine) | Walrus and narwhal tusk; carved into fine objects, jewelry, inlay; rare and highly portable | +| Amber | Fossilized tree resin; translucent, sometimes containing preserved insects; ornamental and trade good | +| Porphyry | Dense purple-red igneous stone; extremely hard to quarry and work; imperial building material | +| Bitumen | Natural tar and petroleum seeps; waterproofing, adhesive, fuel, caulking | +| Murex | Sea snail harvested for purple dye; thousands of shells yield grams of pigment | +| Saffron | Crocus flower stigma; harvested by hand, three threads per flower; dye, spice, medicine | +| Kaolin | Crocus flower stigma; harvested by hand, three threads per flower; dye, spice, medicine | +| War Beasts | Elephants, rhinoceri, or similar; large animals trained for combat; living siege weapons | +| Precious Stones | Diamonds, rubies, sapphires, emeralds; formed under extreme pressure and heat; cut and polished | +| Arsenic | Realgar, orpiment; toxic mineral compounds; pigment, poison, glass-making additive | +| Sulfur | Yellow mineral from volcanic deposits; combustible; used in fumigation, medicine, fire-weapons | +| Cinnabar | Mercury sulfide ore; bright vermillion pigment; toxic; source of liquid mercury | +| Sugar | Cane or beet juice boiled and crystallized; requires heavy irrigation and intensive labor | +| Spices | Pepper, cinnamon, clove, nutmeg; dried seeds, bark, and buds; flavor, preservation, medicine | +| Pearls | Formed inside oysters and mussels; harvested by diving; gems requiring no cutting or polishing | +| Precious Metals | Gold and silver; panned, mined, smelted; currency, jewelry, gilding, ritual objects | +| Silphium | Extinct or near-extinct plant; medicinal, contraceptive, culinary; once worth its weight in coin | +| Steppe Horses | Superior war and riding breeds from harsh grassland; exceptional endurance and hardiness | +| Coffee | Roasted bean brewed into stimulant drink; requires specific tropical highland growing conditions | +| Whales | Hunted for oil, bone, baleen, ambergris; deep-ocean pursuit requiring specialized vessels | +| Rare Woods | Ebony, lignum vitae, purpleheart, sandalwood; dense exotic timber from distant sources | +| Cedar | Aromatic rot-resistant softwood; temple and ship timber; naturally insect-repellent | +| Cocoa | Tree pod containing bitter seeds; roasted and ground into paste or drink; requires tropical lowlands | +| Incense | Frankincense, myrrh, and similar gum resins; burned to produce aromatic smoke | +| Lapis Lazuli | Deep blue metamorphic stone; ground into ultramarine pigment; jewelry, inlay, decoration | +| Coca | Shrub leaf chewed or brewed as stimulant; suppresses hunger and altitude sickness; highland crop | +| Poppy | Flower producing opium latex; powerful painkiller and sedative; highly addictive | + +{% Callout type="example" title="Gwayth Teyd Part II" %} +After having generated the localities of the Gwayth Teyd, I discover that most of its localities have a *Oceanic climate* while the terrain is mostly *Lowlands* with a *Forest* vegetation. The Gwayth Teyd also features a surpsingly high number of *Silk* and *Dye* resources. +{% /Callout %} diff --git a/src/content/articles/kindred/index.mdoc b/src/content/articles/kindred/index.mdoc new file mode 100644 index 0000000..60e109a --- /dev/null +++ b/src/content/articles/kindred/index.mdoc @@ -0,0 +1,19 @@ +--- +title: Kindred +subtitle: What the Body is +summary: >- + Species-level biology. The body you're born in, not the culture you're born + into. Humans are the baseline; everything else carries elemental weight. +cover: + showInHeader: false +publishDate: 2026-03-05T08:51:00.000Z +status: draft +isFeatured: false +parent: prima-materia +tags: + - Prima Materia + - Generator +relatedArticles: [] +seo: + noIndex: false +--- diff --git a/src/content/articles/meta/index.mdoc b/src/content/articles/meta/index.mdoc new file mode 100644 index 0000000..b07a7a6 --- /dev/null +++ b/src/content/articles/meta/index.mdoc @@ -0,0 +1,13 @@ +--- +title: The Metatron +summary: Where meta stuff is presented +cover: + showInHeader: false +publishDate: 2026-03-04T23:45:00.000Z +status: published +isFeatured: false +tags: [] +relatedArticles: [] +seo: + noIndex: false +--- diff --git a/src/content/articles/prima-materia/index.mdoc b/src/content/articles/prima-materia/index.mdoc index f20dc93..83610e5 100644 --- a/src/content/articles/prima-materia/index.mdoc +++ b/src/content/articles/prima-materia/index.mdoc @@ -11,7 +11,22 @@ parent: the-crucible tags: - Crucible Stage - Generation + - Material Conditions + - Domain + - Kindred relatedArticles: [] seo: noIndex: false --- +## Overview + +- **Stage 1** of the Crucible; tangible foundation +- Everything built later – kin, faith, magic, states – rests on what exists here +- The physical world before culture touches it +- Two halves: + - [**Domain:**](/the-crucible/prima-materia/domain) the land; climate, topography, biome, features, resources + - [**Kindred:**](/the-crucible/prima-materia/kindred) the biology; species-level bodies, physical traits, innate abilities +- Neither half generates *Culture* – that's **Calcination** +- *Prima Materia* generates **Conditions;** the material constraints culture must respond to +- Different land, different problems; different problems, different solutions; different solutions, different civilisations +- The land does not care what you believe; the body does not care what you build diff --git a/src/fonts/JuliaMono/JuliaMono-Black.woff2 b/src/fonts/JuliaMono/JuliaMono-Black.woff2 new file mode 100644 index 0000000..0b68f01 Binary files /dev/null and b/src/fonts/JuliaMono/JuliaMono-Black.woff2 differ diff --git a/src/fonts/JuliaMono/JuliaMono-BlackItalic.woff2 b/src/fonts/JuliaMono/JuliaMono-BlackItalic.woff2 new file mode 100644 index 0000000..f04e075 Binary files /dev/null and b/src/fonts/JuliaMono/JuliaMono-BlackItalic.woff2 differ diff --git a/src/fonts/JuliaMono/JuliaMono-Bold.woff2 b/src/fonts/JuliaMono/JuliaMono-Bold.woff2 new file mode 100644 index 0000000..0cadc8e Binary files /dev/null and b/src/fonts/JuliaMono/JuliaMono-Bold.woff2 differ diff --git a/src/fonts/JuliaMono/JuliaMono-BoldItalic.woff2 b/src/fonts/JuliaMono/JuliaMono-BoldItalic.woff2 new file mode 100644 index 0000000..50dea7e Binary files /dev/null and b/src/fonts/JuliaMono/JuliaMono-BoldItalic.woff2 differ diff --git a/src/fonts/JuliaMono/JuliaMono-BoldLatin.woff2 b/src/fonts/JuliaMono/JuliaMono-BoldLatin.woff2 new file mode 100644 index 0000000..4aaee2c Binary files /dev/null and b/src/fonts/JuliaMono/JuliaMono-BoldLatin.woff2 differ diff --git a/src/fonts/JuliaMono/JuliaMono-ExtraBold.woff2 b/src/fonts/JuliaMono/JuliaMono-ExtraBold.woff2 new file mode 100644 index 0000000..39b7eb4 Binary files /dev/null and b/src/fonts/JuliaMono/JuliaMono-ExtraBold.woff2 differ diff --git a/src/fonts/JuliaMono/JuliaMono-ExtraBoldItalic.woff2 b/src/fonts/JuliaMono/JuliaMono-ExtraBoldItalic.woff2 new file mode 100644 index 0000000..0f373de Binary files /dev/null and b/src/fonts/JuliaMono/JuliaMono-ExtraBoldItalic.woff2 differ diff --git a/src/fonts/JuliaMono/JuliaMono-Light.woff2 b/src/fonts/JuliaMono/JuliaMono-Light.woff2 new file mode 100644 index 0000000..6391227 Binary files /dev/null and b/src/fonts/JuliaMono/JuliaMono-Light.woff2 differ diff --git a/src/fonts/JuliaMono/JuliaMono-LightItalic.woff2 b/src/fonts/JuliaMono/JuliaMono-LightItalic.woff2 new file mode 100644 index 0000000..cd952e4 Binary files /dev/null and b/src/fonts/JuliaMono/JuliaMono-LightItalic.woff2 differ diff --git a/src/fonts/JuliaMono/JuliaMono-Medium.woff2 b/src/fonts/JuliaMono/JuliaMono-Medium.woff2 new file mode 100644 index 0000000..e22ed87 Binary files /dev/null and b/src/fonts/JuliaMono/JuliaMono-Medium.woff2 differ diff --git a/src/fonts/JuliaMono/JuliaMono-MediumItalic.woff2 b/src/fonts/JuliaMono/JuliaMono-MediumItalic.woff2 new file mode 100644 index 0000000..c8a750d Binary files /dev/null and b/src/fonts/JuliaMono/JuliaMono-MediumItalic.woff2 differ diff --git a/src/fonts/JuliaMono/JuliaMono-Regular.woff2 b/src/fonts/JuliaMono/JuliaMono-Regular.woff2 new file mode 100644 index 0000000..2d5fc06 Binary files /dev/null and b/src/fonts/JuliaMono/JuliaMono-Regular.woff2 differ diff --git a/src/fonts/JuliaMono/JuliaMono-RegularItalic.woff2 b/src/fonts/JuliaMono/JuliaMono-RegularItalic.woff2 new file mode 100644 index 0000000..498e94a Binary files /dev/null and b/src/fonts/JuliaMono/JuliaMono-RegularItalic.woff2 differ diff --git a/src/fonts/JuliaMono/JuliaMono-RegularLatin.woff2 b/src/fonts/JuliaMono/JuliaMono-RegularLatin.woff2 new file mode 100644 index 0000000..18479c2 Binary files /dev/null and b/src/fonts/JuliaMono/JuliaMono-RegularLatin.woff2 differ diff --git a/src/fonts/JuliaMono/JuliaMono-SemiBold.woff2 b/src/fonts/JuliaMono/JuliaMono-SemiBold.woff2 new file mode 100644 index 0000000..8582d49 Binary files /dev/null and b/src/fonts/JuliaMono/JuliaMono-SemiBold.woff2 differ diff --git a/src/fonts/JuliaMono/JuliaMono-SemiBoldItalic.woff2 b/src/fonts/JuliaMono/JuliaMono-SemiBoldItalic.woff2 new file mode 100644 index 0000000..174b388 Binary files /dev/null and b/src/fonts/JuliaMono/JuliaMono-SemiBoldItalic.woff2 differ diff --git a/src/keystatic/components/callout.ts b/src/keystatic/components/callout.ts new file mode 100644 index 0000000..f26ab3b --- /dev/null +++ b/src/keystatic/components/callout.ts @@ -0,0 +1,39 @@ +import { wrapper } from '@keystatic/core/content-components'; +import { fields } from '@keystatic/core'; +import { megaphoneIcon } from '@keystar/ui/icon/icons/megaphoneIcon'; + +const calloutComponent = { + Callout: wrapper({ + label: 'Callout', + icon: megaphoneIcon, + schema: { + type: fields.select({ + label: 'Type', + defaultValue: 'default', + options: [ + { + label: 'Default', + value: 'default', + }, + { + label: 'Optional', + value: 'optional', + }, + { + label: 'Example', + value: 'example', + }, + { + label: 'Spoiler', + value: 'spoiler', + }, + ], + }), + title: fields.text({ + label: 'Title', + }), + }, + }), +}; + +export default calloutComponent; diff --git a/src/keystatic/components/figure.ts b/src/keystatic/components/figure.ts new file mode 100644 index 0000000..6a3b8ef --- /dev/null +++ b/src/keystatic/components/figure.ts @@ -0,0 +1,35 @@ +import { imageIcon } from '@keystar/ui/icon/icons/imageIcon'; +import { block } from '@keystatic/core/content-components'; +import { fields } from '@keystatic/core'; + +const figureComponent = { + Figure: block({ + label: 'Figure', + icon: imageIcon, + schema: { + src: fields.image({ + label: 'Image', + directory: 'src/content/articles', + publicPath: '/content/articles', + }), + alt: fields.text({ + label: 'Alt Text', + validation: { + length: { + min: 1, + }, + }, + }), + caption: fields.text({ + label: 'Caption', + multiline: true, + }), + credit: fields.text({ + label: 'Credit/Attribution', + description: 'Photographer, artist, or source', + }), + }, + }), +}; + +export default figureComponent; diff --git a/src/keystatic/components/index.ts b/src/keystatic/components/index.ts index 63a02fc..f7b2f0e 100644 --- a/src/keystatic/components/index.ts +++ b/src/keystatic/components/index.ts @@ -1,5 +1,11 @@ import elementComponent from './element.ts'; +import calloutComponent from './callout.ts'; +import sidenoteComponent from './sidenote.ts'; +import figureComponent from './figure.ts'; export const generalComponents = { ...elementComponent, + ...calloutComponent, + ...sidenoteComponent, + ...figureComponent, }; diff --git a/src/keystatic/components/sidenote.ts b/src/keystatic/components/sidenote.ts new file mode 100644 index 0000000..1f65ca1 --- /dev/null +++ b/src/keystatic/components/sidenote.ts @@ -0,0 +1,50 @@ +import { inline } from '@keystatic/core/content-components'; +import { fields } from '@keystatic/core'; +import { panelRightDashedIcon } from '@keystar/ui/icon/icons/panelRightDashedIcon'; + +const sidenoteComponents = { + Sidenote: inline({ + label: 'Sidenote', + icon: panelRightDashedIcon, + schema: { + id: fields.text({ + label: 'ID', + validation: { + isRequired: true, + }, + }), + title: fields.text({ + label: 'Title', + }), + marker: fields.text({ + label: 'Marker', + defaultValue: '⋄', + }), + content: fields.text({ + label: 'Body', + multiline: true, + validation: { isRequired: true }, + }), + type: fields.select({ + label: 'Type', + options: [ + { + value: 'default', + label: 'Default', + }, + { + value: 'example', + label: 'Example', + }, + { + value: 'optional', + label: 'Optional', + }, + ], + defaultValue: 'default', + }), + }, + }), +}; + +export default sidenoteComponents; diff --git a/src/layouts/ContentLayout.astro b/src/layouts/ContentLayout.astro index 7b77dc5..912c265 100644 --- a/src/layouts/ContentLayout.astro +++ b/src/layouts/ContentLayout.astro @@ -5,6 +5,8 @@ import Base from '@layouts/Base.astro'; import Header from '@compontents/layout/PostHeader/index.astro'; import { getBreadcrumbs } from '@lib/utils/paths'; import { toMilitaryDTG } from '@lib/utils/date'; +import { extractSidenotes } from '@lib/utils/extractors'; +import Sidenote from '@compontents/content/Sidenote.astro'; interface Props { entry: CollectionEntry<'articles'> | CollectionEntry<'elements'>; @@ -15,7 +17,8 @@ const { entry, collectionName } = Astro.props; const { Content } = await render(entry); const { title, summary, subtitle, updateDate, publishDate, tags, seo } = entry.data; -const hasMargin = Astro.slots.has('margin') +const sidenotes = entry.body ? extractSidenotes(entry.body) : []; +const hasMargin = Astro.slots.has('margin') || !!sidenotes; const breadcrumbs = await getBreadcrumbs( entry.data.parent ?? null, @@ -57,6 +60,7 @@ const headerCover = {hasMargin &&( )} @@ -69,8 +73,9 @@ const headerCover = .frame { @mixin layout-wrapper; + position: relative; + @media (--bp-desktop) { - position: relative; display: grid; grid-template-columns: var(--layout-content-width) var(--layout-margin-width); gap: var(--layout-gutter); @@ -82,7 +87,6 @@ const headerCover = } .margin { - position: relative; min-width: 0; } diff --git a/src/lib/paths_legacy.ts b/src/lib/paths_legacy.ts deleted file mode 100644 index 8e5c9b4..0000000 --- a/src/lib/paths_legacy.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { getCollection, type CollectionEntry } from 'astro:content'; - -export type ContentType = 'article' | 'page' | 'element'; - -export interface ResolvedEntry { - type: ContentType; - path: string; - entry: - | CollectionEntry<'pages'> - | CollectionEntry<'articles'> - | CollectionEntry<'elements'>; -} - -function buildPath( - slug: string, - parentSlug: string | null, - articles: CollectionEntry<'articles'>[], - visited: Set = new Set(), -): string { - if (!parentSlug) return slug; - if (visited.has(parentSlug)) { - console.warn(`Circular reference detected at ${parentSlug}`); - return slug; - } - visited.add(parentSlug); - - const parent = articles.find((a) => a.slug === parentSlug); - if (!parent) return slug; - - const parentPath = buildPath( - parent.slug, - parent.data.parent ?? null, - articles, - visited, - ); - - return `${parentPath}/${slug}`; -} - -function resolveArticles( - articles: CollectionEntry<'articles'>[], -): ResolvedEntry[] { - return articles - .filter((entry) => entry.data.status === 'published') - .map((entry) => ({ - type: 'article' as const, - path: buildPath(entry.slug, entry.data.parent ?? null, articles), - entry, - })); -} - -interface GlobEntry { - id: string; - data: { status: string; parent?: string | null }; -} - -function resolveGlobCollection( - collection: GlobEntry[], - type: ContentType, - articles: CollectionEntry<'articles'>[], -): { type: ContentType; path: string; entry: GlobEntry }[] { - return collection - .filter((entry) => entry.data.status === 'published') - .map((entry) => ({ - type, - path: buildPath(entry.id, entry.data.parent ?? null, articles), - entry, - })); -} - -export async function resolveAllPaths(): Promise { - const articles = await getCollection('articles'); - const elements = await getCollection('elements'); - const pages = await getCollection('pages'); - - const resolvedPages: ResolvedEntry[] = pages.map((page) => ({ - type: 'page' as const, - path: `static/${page.slug}`, - entry: page, - })); - - return [ - ...resolveArticles(articles), - ...resolveGlobCollection(elements, 'element', articles), - ...resolvedPages, - ] as ResolvedEntry[]; -} diff --git a/src/lib/types/content.ts b/src/lib/types/content.ts index f151ec6..749df63 100644 --- a/src/lib/types/content.ts +++ b/src/lib/types/content.ts @@ -41,3 +41,13 @@ export interface PaletteEntry { parent: string | null; depth: number; } + +/** Sidenote */ + +export interface Sidenote { + id: string; + title: string; + marker: string; + type: string; + content: string; +} diff --git a/src/lib/utils/extractors.ts b/src/lib/utils/extractors.ts new file mode 100644 index 0000000..b7c6214 --- /dev/null +++ b/src/lib/utils/extractors.ts @@ -0,0 +1,21 @@ +import Markdoc from '@markdoc/markdoc'; +import type { Sidenote } from '@lib/types/content'; + +export function extractSidenotes(raw: string): Sidenote[] { + const ast = Markdoc.parse(raw); + const sidenotes: Sidenote[] = []; + + for (const node of ast.walk()) { + if (node.type === 'tag' && node.tag === 'Sidenote') { + const attrs = node.attributes; + sidenotes.push({ + id: attrs.id, + marker: attrs.marker ?? '⋄', + title: attrs.title ?? '', + type: attrs.type ?? 'default', + content: attrs.content, + }); + } + } + return sidenotes; +} diff --git a/src/styles/base/colors.css b/src/styles/base/colors.css index b962b1d..cc9e07d 100644 --- a/src/styles/base/colors.css +++ b/src/styles/base/colors.css @@ -83,7 +83,8 @@ --color-overlay-heavy: oklch(0% 0 0deg / 60%); /* == Shadows == */ - --color-shadow: oklch(0% 0 0deg / 10%); + --color-shadow: var(--space-2) var(--space-2) 0 oklch(from var(--color-surface-inverse) calc(l - 0.075) c h), var(--space-4) var(--space-4) 0 oklch(from var(--color-surface-inverse) calc(l - 0.2) c h); +; --color-shadow-strong: oklch(0% 0 0deg / 25%); /* == Utility == */ diff --git a/src/styles/base/content.css b/src/styles/base/content.css index d73a6c8..3c1046c 100644 --- a/src/styles/base/content.css +++ b/src/styles/base/content.css @@ -233,7 +233,7 @@ contain: style; list-style: none; - & li { + & > li { @mixin ml var(--space-6); @mixin pl var(--space-4); @@ -302,7 +302,7 @@ border: var(--size-1) solid var(--color-border-strong); color: var(--color-text-inverse); - text-align: left; + text-align: center; background: var(--color-surface-inverse); } @@ -311,6 +311,14 @@ @mixin pa var(--space-4); border: var(--size-1) solid var(--color-border-subtle); + text-align: center; + } + + & th, + & td { + &:first-child { + text-align: left; + } } } @@ -336,7 +344,8 @@ } /* === DETAILS / SUMMARY === */ - & details { + + /* & details { @mixin mr var(--space-8); @mixin border-l var(--size-4), solod, var(--color-border-normal); @@ -377,7 +386,7 @@ & details > *:not(summary) { @mixin px var(--space-6); - } + } */ & hr { @mixin my var(--space-10); @@ -411,7 +420,8 @@ } /* === FIGURES === */ - & figure { + + /* & figure { @mixin my var(--space-10); & img { @@ -426,7 +436,7 @@ color: var(--color-text-tertiary); } - } + } */ /* === INLINE ELEMENTS === */ @@ -459,6 +469,11 @@ text-decoration: none; background: var(--color-primary); } + + &.sidenote-ref { + scroll-margin-top: calc(var(--layout-header-height) + var(--size-8)); + text-decoration: none; + } } &:visited { @@ -517,11 +532,11 @@ font-style: normal; } - & cite { + /* & cite { font-weight: 600; font-style: normal; color: var(--color-text-secondary); - } + } */ & q { font-style: normal; diff --git a/src/styles/base/fonts.css b/src/styles/base/fonts.css index 36f919e..6ada313 100644 --- a/src/styles/base/fonts.css +++ b/src/styles/base/fonts.css @@ -11,6 +11,8 @@ src: url('/src/fonts/UnigrimDee-Regular.woff2') format('woff2'); } +/** + @font-face { font-family: 'Iosevka Slab'; font-weight: 300; @@ -147,4 +149,125 @@ font-display: swap; src: url('/src/fonts/IosevkaSansMono/IosevkaSansMono-ExtraBoldItalic.woff2') format('woff2'); + } +**/ + +@font-face { + font-family: 'Julia Mono'; + font-weight: 300; + font-style: normal; + + font-display: swap; + src: url('/src/fonts/JuliaMono/JuliaMono-Light.woff2') + format('woff2'); +} + +@font-face { + font-family: 'Julia Mono'; + font-weight: 400; + font-style: normal; + + font-display: swap; + src: url('/src/fonts/JuliaMono/JuliaMono-Regular.woff2') + format('woff2'); +} + +@font-face { + font-family: 'Julia Mono'; + font-weight: 500; + font-style: normal; + + font-display: swap; + src: url('/src/fonts/JuliaMono/JuliaMono-Medium.woff2') + format('woff2'); +} + +@font-face { + font-family: 'Julia Mono'; + font-weight: 600; + font-style: normal; + + font-display: swap; + src: url('/src/fonts/JuliaMono/JuliaMono-SemiBold.woff2') + format('woff2'); +} + +@font-face { + font-family: 'Julia Mono'; + font-weight: 700; + font-style: normal; + + font-display: swap; + src: url('/src/fonts/JuliaMono/JuliaMono-Bold.woff2') + format('woff2'); +} + +@font-face { + font-family: 'Julia Mono'; + font-weight: 900; + font-style: normal; + + font-display: swap; + src: url('/src/fonts/JuliaMono/JuliaMono-Black.woff2') + format('woff2'); +} + +@font-face { + font-family: 'Julia Mono'; + font-weight: 300; + font-style: italic; + + font-display: swap; + src: url('/src/fonts/JuliaMono/JuliaMono-LightItalic.woff2') + format('woff2'); +} + +@font-face { + font-family: 'Julia Mono'; + font-weight: 400; + font-style: italic; + + font-display: swap; + src: url('/src/fonts/JuliaMono/JuliaMono-RegularItalic.woff2') + format('woff2'); +} + +@font-face { + font-family: 'Julia Mono'; + font-weight: 500; + font-style: italic; + + font-display: swap; + src: url('/src/fonts/JuliaMono/JuliaMono-MediumItalic.woff2') + format('woff2'); +} + +@font-face { + font-family: 'Julia Mono'; + font-weight: 600; + font-style: italic; + + font-display: swap; + src: url('/src/fonts/JuliaMono/JuliaMono-SemiBoldItalic.woff2') + format('woff2'); +} + +@font-face { + font-family: 'Julia Mono'; + font-weight: 700; + font-style: italic; + + font-display: swap; + src: url('/src/fonts/JuliaMono/JuliaMono-BoldItalic.woff2') + format('woff2'); +} + +@font-face { + font-family: 'Julia Mono'; + font-weight: 900; + font-style: italic; + + font-display: swap; + src: url('/src/fonts/JuliaMono/JuliaMono-BlackItalic.woff2') + format('woff2'); } diff --git a/src/styles/base/primitives.css b/src/styles/base/primitives.css index cce17ea..fcea5b5 100644 --- a/src/styles/base/primitives.css +++ b/src/styles/base/primitives.css @@ -146,8 +146,8 @@ /* === FONT FAMILIES === */ --font-header: 'Geist Variable', sans; --font-display: 'Blaka',serif; - --font-body: 'Iosevka Slab', sans; - --font-mono: 'Iosevka Mono', monospace; + --font-body: 'Julia Mono', sans; + --font-mono: 'Julia Mono', monospace; --font-symbols: 'Unigrim Dee', fantasy; /* === SIZE (REM-based) === */ @@ -205,9 +205,10 @@ --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* === LAYOUT === */ - --layout-content-width: minmax(0, 72ch); - --layout-margin-width: minmax(18ch, 1fr); - --layout-gutter: var(--space-6); + --layout-content-width: minmax(0, 80ch); + --layout-margin-width: minmax(10ch, 1fr); + --layout-gutter: var(--space-12); --layout-page-margin: var(--space-8); --layout-max-width: 90rem; + --layout-header-height: 3.125rem; } diff --git a/src/styles/mixins/patterns.css b/src/styles/mixins/patterns.css new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/styles/mixins/patterns.css @@ -0,0 +1 @@ +