diff --git a/keystatic.config.ts b/keystatic.config.ts
index 6e916f5..34c61d6 100644
--- a/keystatic.config.ts
+++ b/keystatic.config.ts
@@ -1,12 +1,21 @@
import { config } from '@keystatic/core'
import { contentCollections } from '@/keystatic/collections/general'
+import { siteCollections } from '@/keystatic/collections/site'
+import { ARTICLES, PAGES, RELEASES, TAGS } from '@/lib/constants/collections'
export default config({
storage: {
kind: 'local',
},
+ ui: {
+ navigation: {
+ Content: [ARTICLES, PAGES, TAGS],
+ Site: [RELEASES],
+ },
+ },
collections: {
...contentCollections,
+ ...siteCollections,
},
})
diff --git a/src/components/post/ArticleCard/ArticleCardItem.astro b/src/components/post/ArticleCard/ArticleCardItem.astro
index d7219e5..b583838 100644
--- a/src/components/post/ArticleCard/ArticleCardItem.astro
+++ b/src/components/post/ArticleCard/ArticleCardItem.astro
@@ -10,7 +10,7 @@ type Props = {
const { article } = Astro.props
const { cover, excerpt } = article.summary
-const date = article.published
+const date = article.publishedAt
---
diff --git a/src/components/post/Footer/ArticleFooter.astro b/src/components/post/Footer/ArticleFooter.astro
index ab100f3..09623d3 100644
--- a/src/components/post/Footer/ArticleFooter.astro
+++ b/src/components/post/Footer/ArticleFooter.astro
@@ -1,17 +1,22 @@
---
import type { Sequence } from '@/lib/site/navigation'
+import type { Article } from '@/lib/site/types'
+import RevisionCard from './RevisionCard.astro'
import SequenceNav from './SequenceNav.astro'
type Props = {
+ article: Article
sequence: Sequence | null
}
-const { sequence } = Astro.props
+const { article, sequence } = Astro.props
+const { title, changes } = article
---
-{sequence && (
+{(changes.length > 0 || sequence) && (
)}
diff --git a/src/components/post/Footer/RevisionCard.astro b/src/components/post/Footer/RevisionCard.astro
new file mode 100644
index 0000000..7e7cb9f
--- /dev/null
+++ b/src/components/post/Footer/RevisionCard.astro
@@ -0,0 +1,38 @@
+---
+import { CHANGE_TYPES } from '@/lib/constants/changelog'
+import { toMilitaryDTG } from '@/lib/content/dates'
+import type { Change } from '@/lib/site/types'
+
+type Props = {
+ title: string
+ changes: Change[]
+}
+
+const LIMIT = 5
+
+const { title, changes } = Astro.props
+
+const labelOf = (kind: Change['kind']) =>
+ CHANGE_TYPES.find((option) => option.value === kind)?.label ?? kind
+---
+
+
+
+
+ Revisions
+
+
{title}
+
+
+ {changes.slice(0, LIMIT).map(({ kind, text, release }) => (
+ -
+ v{release.version}
+
+ {labelOf(kind)}
+
{text}
+
+ ))}
+
+
diff --git a/src/components/post/Header/ArticleHeader.astro b/src/components/post/Header/ArticleHeader.astro
index e093fd6..b25baa6 100644
--- a/src/components/post/Header/ArticleHeader.astro
+++ b/src/components/post/Header/ArticleHeader.astro
@@ -15,7 +15,7 @@ type Props = {
const { article } = Astro.props
const crumbs = trailOf(article)
-const published = article.published
+const published = article.publishedAt
---
diff --git a/src/components/post/Header/Meta.astro b/src/components/post/Header/Meta.astro
index 20583ba..eb9d8d8 100644
--- a/src/components/post/Header/Meta.astro
+++ b/src/components/post/Header/Meta.astro
@@ -11,8 +11,10 @@ type Props = {
const { article } = Astro.props
const site = await getSite()
const tags = article.tags.map((id) => site.tags.get(id)).filter((tag) => tag !== undefined)
-const date = article.updated ?? article.published
-const dateLabel = article.updated ? 'Updated' : 'Published'
+const latest = article.changes[0]?.release.date
+const updated = latest && article.publishedAt && latest > article.publishedAt ? latest : null
+const date = updated ?? article.publishedAt
+const dateLabel = updated ? 'Updated' : 'Published'
---
diff --git a/src/content/general/articles/calcination.mdoc b/src/content/general/articles/calcination.mdoc
index 6742272..9d91ced 100644
--- a/src/content/general/articles/calcination.mdoc
+++ b/src/content/general/articles/calcination.mdoc
@@ -12,8 +12,7 @@ summary:
cover:
effect: none
ratio: original
-dates:
- published: 2026-09-30T12:27:00.000Z
+published: 2026-09-30T12:27:00.000Z
tags:
- alchemical-materialism
- genesis
diff --git a/src/content/general/articles/coagulation.mdoc b/src/content/general/articles/coagulation.mdoc
index d407872..ccf4937 100644
--- a/src/content/general/articles/coagulation.mdoc
+++ b/src/content/general/articles/coagulation.mdoc
@@ -12,8 +12,7 @@ summary:
cover:
effect: none
ratio: original
-dates:
- published: 2026-09-30T12:27:00.000Z
+published: 2026-09-30T12:27:00.000Z
tags:
- alchemical-materialism
- genesis
diff --git a/src/content/general/articles/conjunction.mdoc b/src/content/general/articles/conjunction.mdoc
index 1d8c014..79c2dec 100644
--- a/src/content/general/articles/conjunction.mdoc
+++ b/src/content/general/articles/conjunction.mdoc
@@ -12,8 +12,7 @@ summary:
cover:
effect: none
ratio: original
-dates:
- published: 2026-09-30T12:27:00.000Z
+published: 2026-09-30T12:27:00.000Z
tags:
- alchemical-materialism
- genesis
diff --git a/src/content/general/articles/dissolution.mdoc b/src/content/general/articles/dissolution.mdoc
index f14b3b1..d81310a 100644
--- a/src/content/general/articles/dissolution.mdoc
+++ b/src/content/general/articles/dissolution.mdoc
@@ -12,8 +12,7 @@ summary:
cover:
effect: none
ratio: original
-dates:
- published: 2026-09-30T12:27:00.000Z
+published: 2026-09-30T12:27:00.000Z
tags:
- alchemical-materialism
- genesis
diff --git a/src/content/general/articles/fermentation.mdoc b/src/content/general/articles/fermentation.mdoc
index ea87029..023b660 100644
--- a/src/content/general/articles/fermentation.mdoc
+++ b/src/content/general/articles/fermentation.mdoc
@@ -12,8 +12,7 @@ summary:
cover:
effect: none
ratio: original
-dates:
- published: 2026-09-30T12:27:00.000Z
+published: 2026-09-30T12:27:00.000Z
tags:
- alchemical-materialism
- genesis
diff --git a/src/content/general/articles/prima-materia.mdoc b/src/content/general/articles/prima-materia.mdoc
index ce3d054..9990edb 100644
--- a/src/content/general/articles/prima-materia.mdoc
+++ b/src/content/general/articles/prima-materia.mdoc
@@ -15,8 +15,7 @@ summary:
credit: John Martin – The Great Day of His Wrath
effect: xeroxed
ratio: 21-9
-dates:
- published: 2026-09-21T12:35:00.000Z
+published: 2026-09-21T12:35:00.000Z
tags:
- the-crucible
- alchemical-materialism
diff --git a/src/content/general/articles/stages.mdoc b/src/content/general/articles/stages.mdoc
index 1abca15..2e0a118 100644
--- a/src/content/general/articles/stages.mdoc
+++ b/src/content/general/articles/stages.mdoc
@@ -10,7 +10,6 @@ summary:
cover:
effect: none
ratio: original
-dates:
- published: 2026-09-21T12:34:00.000Z
+published: 2026-09-21T12:34:00.000Z
tags: []
---
diff --git a/src/content/general/articles/sublimation.mdoc b/src/content/general/articles/sublimation.mdoc
index db7bc6c..686d91a 100644
--- a/src/content/general/articles/sublimation.mdoc
+++ b/src/content/general/articles/sublimation.mdoc
@@ -12,8 +12,7 @@ summary:
cover:
effect: none
ratio: original
-dates:
- published: 2026-09-30T12:27:00.000Z
+published: 2026-09-30T12:27:00.000Z
tags:
- alchemical-materialism
- genesis
diff --git a/src/content/general/articles/the-crucible.mdoc b/src/content/general/articles/the-crucible.mdoc
index 930f79c..213d6e5 100644
--- a/src/content/general/articles/the-crucible.mdoc
+++ b/src/content/general/articles/the-crucible.mdoc
@@ -8,8 +8,7 @@ summary:
Where we explore the crucible – a materialistic alchemist Worldbuilding
Framework
cover: {}
-dates:
- published: 2026-09-21T12:33:00.000Z
+published: 2026-09-21T12:33:00.000Z
tags:
- the-crucible
---
diff --git a/src/content/site/releases/v0-12.yaml b/src/content/site/releases/v0-12.yaml
new file mode 100644
index 0000000..5d67b2c
--- /dev/null
+++ b/src/content/site/releases/v0-12.yaml
@@ -0,0 +1,6 @@
+version: '0.12'
+date: 2026-09-30T13:00:00.000Z
+changes:
+ - kind: added
+ article: prima-materia
+ text: Section on the ground state; first table of correspondences
diff --git a/src/content/site/releases/v0-13.yaml b/src/content/site/releases/v0-13.yaml
new file mode 100644
index 0000000..45fbe48
--- /dev/null
+++ b/src/content/site/releases/v0-13.yaml
@@ -0,0 +1,12 @@
+version: '0.13'
+date: 2026-09-30T13:00:00.000Z
+changes:
+ - kind: added
+ article: calcination
+ text: Test
+ - kind: added
+ article: fermentation
+ text: Big test with a longer sentence
+ - kind: changed
+ article: coagulation
+ text: Lorem ipsum, blablabla ipbsum whorum
diff --git a/src/content/site/releases/v0-14.yaml b/src/content/site/releases/v0-14.yaml
new file mode 100644
index 0000000..c3c5176
--- /dev/null
+++ b/src/content/site/releases/v0-14.yaml
@@ -0,0 +1,19 @@
+version: '0.14'
+date: 2026-09-30T13:00:00.000Z
+changes:
+ - kind: added
+ article: coagulation
+ text: >-
+ s the raw release exactly as Keystatic reads it, inferred from the schema
+ like the other three
+ - kind: changed
+ article: prima-materia
+ text: >-
+ releases are loaded, resolved and attached to articles. Six files; nothing
+ renders yet.
+ - kind: fixed
+ article: fermentation
+ text: We don't burn the priests anymore, we burn the gods
+ - kind: removed
+ article: the-crucible
+ text: AND EVERYTHING GOES BOOOOOOOM
diff --git a/src/keystatic/collections/general/articles.ts b/src/keystatic/collections/general/articles.ts
index 4e01807..5670ec6 100644
--- a/src/keystatic/collections/general/articles.ts
+++ b/src/keystatic/collections/general/articles.ts
@@ -1,8 +1,7 @@
import { collection, fields } from '@keystatic/core'
-import { base, stage } from '@/keystatic/fields/content/base'
+import { base, published, stage } from '@/keystatic/fields/content/base'
import { content } from '@/keystatic/fields/content/content'
-import { dates } from '@/keystatic/fields/content/dates'
import { articleRoute } from '@/keystatic/fields/content/route'
import { articleSummary } from '@/keystatic/fields/content/summary'
import { ARTICLES, TAGS } from '@/lib/constants/collections'
@@ -21,7 +20,7 @@ export const articles = collection({
stage,
route: articleRoute,
summary: articleSummary(ARTICLES),
- dates,
+ published,
tags: fields.multiRelationship({
label: 'Tags',
collection: TAGS,
diff --git a/src/keystatic/collections/site/index.ts b/src/keystatic/collections/site/index.ts
new file mode 100644
index 0000000..49a9119
--- /dev/null
+++ b/src/keystatic/collections/site/index.ts
@@ -0,0 +1,7 @@
+import { RELEASES } from '@/lib/constants/collections'
+
+import { releases } from './releases'
+
+export const siteCollections = {
+ [RELEASES]: releases,
+}
diff --git a/src/keystatic/collections/site/releases.ts b/src/keystatic/collections/site/releases.ts
new file mode 100644
index 0000000..5a86f54
--- /dev/null
+++ b/src/keystatic/collections/site/releases.ts
@@ -0,0 +1,66 @@
+import { collection, fields } from '@keystatic/core'
+
+import { CHANGE_TYPES } from '@/lib/constants/changelog'
+import { ARTICLES, RELEASES } from '@/lib/constants/collections'
+
+const VERSION = /^\d+\.\d+(\.\d+)?$/
+
+export const releases = collection({
+ label: 'Releases',
+ slugField: 'version',
+ path: `src/content/site/${RELEASES}/*`,
+ columns: ['version', 'date'],
+ schema: {
+ version: fields.slug({
+ name: {
+ label: 'Version',
+ description: 'Without the v: 0.12, 1.0, 1.0.3',
+ validation: {
+ isRequired: true,
+ pattern: {
+ regex: VERSION,
+ message: 'Numbers separated by dots, like 0.12 or 1.0.3',
+ },
+ },
+ },
+ slug: {
+ label: 'File ID',
+ generate: (version) => `v${version.replaceAll('.', '-')}`,
+ },
+ }),
+ date: fields.datetime({
+ label: 'Date',
+ defaultValue: {
+ kind: 'now',
+ },
+ validation: {
+ isRequired: true,
+ },
+ }),
+ changes: fields.array(
+ fields.object({
+ kind: fields.select({
+ label: 'Kind',
+ options: CHANGE_TYPES,
+ defaultValue: 'added',
+ }),
+ article: fields.relationship({
+ label: 'Article',
+ description: 'Empty for site-wide changes',
+ collection: ARTICLES,
+ }),
+ text: fields.text({
+ label: 'Text',
+ validation: {
+ isRequired: true,
+ },
+ }),
+ }),
+ {
+ label: 'Changes',
+ itemLabel: (props) =>
+ `${props.fields.kind.value} · ${props.fields.article.value ?? 'site'} · ${props.fields.text.value}`,
+ },
+ ),
+ },
+})
diff --git a/src/keystatic/fields/content/base.ts b/src/keystatic/fields/content/base.ts
index f4cd809..995c3d1 100644
--- a/src/keystatic/fields/content/base.ts
+++ b/src/keystatic/fields/content/base.ts
@@ -45,6 +45,13 @@ export const noindex = fields.checkbox({
label: 'Hide from search engines',
})
+export const published = fields.datetime({
+ label: 'Published',
+ defaultValue: {
+ kind: 'now',
+ },
+})
+
export const base = {
title,
subtitle,
diff --git a/src/keystatic/fields/content/dates.ts b/src/keystatic/fields/content/dates.ts
deleted file mode 100644
index 9f08760..0000000
--- a/src/keystatic/fields/content/dates.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { fields } from '@keystatic/core'
-
-export const dates = fields.object(
- {
- published: fields.datetime({
- label: 'Published',
- defaultValue: {
- kind: 'now',
- },
- }),
- updated: fields.datetime({
- label: 'Updated',
- }),
- },
- {
- label: 'Dates',
- },
-)
diff --git a/src/layouts/general/ArticleLayout.astro b/src/layouts/general/ArticleLayout.astro
index c78b62b..6fa3823 100644
--- a/src/layouts/general/ArticleLayout.astro
+++ b/src/layouts/general/ArticleLayout.astro
@@ -38,7 +38,7 @@ const sequence = sequenceOf(article)
-
+
{
list(): Promise
@@ -36,10 +36,11 @@ async function readAll(
export async function loadContent(): Promise {
const { collections } = createContentReader()
const problems = createDiagnostics()
- const [articles, pages, tags] = await Promise.all([
+ const [articles, pages, tags, releases] = await Promise.all([
readAll(collections.articles, ARTICLES, problems),
readAll(collections.pages, PAGES, problems),
readAll(collections.tags, TAGS, problems),
+ readAll(collections.releases, RELEASES, problems),
])
- return { articles, pages, tags, diagnostics: problems.list }
+ return { articles, pages, tags, releases, diagnostics: problems.list }
}
diff --git a/src/lib/content/types.ts b/src/lib/content/types.ts
index 6c409df..b1d8e0c 100644
--- a/src/lib/content/types.ts
+++ b/src/lib/content/types.ts
@@ -9,10 +9,12 @@ type Collections = typeof config.collections
export type ArticleEntry = Entry
export type PageEntry = Entry
export type TagEntry = Entry
+export type ReleaseEntry = Entry
export type Content = {
articles: Map
pages: Map
tags: Map
+ releases: Map
diagnostics: Diagnostic[]
}
diff --git a/src/lib/site/index.ts b/src/lib/site/index.ts
index 193642c..6b87fe5 100644
--- a/src/lib/site/index.ts
+++ b/src/lib/site/index.ts
@@ -3,6 +3,7 @@ import { loadContent } from '../content/load'
import type { Content } from '../content/types'
import { createDiagnostics } from '../diagnostics'
import type { Diagnostics } from '../diagnostics/types'
+import { collectReleases } from './releases'
import {
isBuilt,
newestFirst,
@@ -58,6 +59,7 @@ export function buildSite(content: Content, visibility: Visibility): Site {
const articles = placeArticles(content, visibility, problems)
const pages = placePages(content, visibility, problems)
const tags = collectTags(content, articles)
+ const releases = collectReleases(content, articles, problems)
validate(articles.values(), content, problems)
const byPath = indexUrls([...articles.values(), ...pages.values()], problems)
const roots = [...articles.values()]
@@ -68,6 +70,7 @@ export function buildSite(content: Content, visibility: Visibility): Site {
articles,
pages,
tags,
+ releases,
byPath,
known: new Set(content.articles.keys()),
roots,
diff --git a/src/lib/site/releases.ts b/src/lib/site/releases.ts
new file mode 100644
index 0000000..cf048fb
--- /dev/null
+++ b/src/lib/site/releases.ts
@@ -0,0 +1,59 @@
+import { RELEASES } from '../constants/collections'
+import { parseDate } from '../content/dates'
+import type { Content } from '../content/types'
+import type { Diagnostics } from '../diagnostics/types'
+import type { Article, Release } from './types'
+
+const partsOf = (version: string) => version.split('.').map(Number)
+
+function compareVersions(a: string, b: string): number {
+ const [left, right] = [partsOf(a), partsOf(b)]
+ for (let i = 0; i < Math.max(left.length, right.length); i++) {
+ const difference = (left[i] ?? 0) - (right[i] ?? 0)
+ if (difference) return difference
+ }
+ return 0
+}
+
+const releaseOrder = (a: Release, b: Release) => compareVersions(b.version, a.version)
+
+export function collectReleases(
+ content: Content,
+ articles: Map,
+ problems: Diagnostics,
+): Release[] {
+ const releases: Release[] = []
+
+ for (const [id, entry] of content.releases) {
+ const date = parseDate(entry.date)
+ if (!date) {
+ problems.error(RELEASES, id, 'Needs a date')
+ continue
+ }
+
+ const release: Release = { id, version: entry.version, date, changes: [] }
+ for (const { kind, article: target, text } of entry.changes) {
+ const article = target ? articles.get(target) : null
+ if (target && !article) {
+ if (!content.articles.has(target))
+ problems.error(RELEASES, id, `Article "${target}" does not exist`)
+ continue
+ }
+ release.changes.push({ kind, article: article ?? null, text, release })
+ }
+
+ if (release.changes.length) releases.push(release)
+ }
+
+ releases.sort(releaseOrder)
+ releases.forEach((release, i) => {
+ const older = releases[i + 1]
+ if (older && release.date < older.date)
+ problems.warning(RELEASES, release.id, `Dated before v${older.version}`)
+ })
+
+ for (const release of releases)
+ for (const change of release.changes) change.article?.changes.push(change)
+
+ return releases
+}
diff --git a/src/lib/site/rules.ts b/src/lib/site/rules.ts
index b9b2597..4f33a53 100644
--- a/src/lib/site/rules.ts
+++ b/src/lib/site/rules.ts
@@ -27,7 +27,7 @@ export function segmentProblem(segment: string, atRoot: boolean): string | null
const time = (date: Date | null) => date?.getTime() ?? 0
const UNINDEXED = Number.MAX_SAFE_INTEGER
-export const newestFirst = (a: Article, b: Article) => time(b.published) - time(a.published)
+export const newestFirst = (a: Article, b: Article) => time(b.publishedAt) - time(a.publishedAt)
export const siblingOrder = (a: Article, b: Article) =>
(a.route.index ?? UNINDEXED) - (b.route.index ?? UNINDEXED) ||
diff --git a/src/lib/site/tree.ts b/src/lib/site/tree.ts
index ded37ec..2d13a41 100644
--- a/src/lib/site/tree.ts
+++ b/src/lib/site/tree.ts
@@ -54,10 +54,10 @@ export function placeArticles(
id,
path: `${parent?.path ?? ''}/${segment}`,
listed: isListed(entry.status, visibility),
- published: parseDate(entry.dates.published),
- updated: parseDate(entry.dates.updated),
+ publishedAt: parseDate(entry.published),
parent,
children: [],
+ changes: [],
}
placed.set(id, article)
return article
diff --git a/src/lib/site/types.ts b/src/lib/site/types.ts
index 5a852ad..0e781f2 100644
--- a/src/lib/site/types.ts
+++ b/src/lib/site/types.ts
@@ -1,3 +1,4 @@
+import type { ChangeType } from '../constants/changelog'
import { ARTICLES, PAGES, TAGS } from '../constants/collections'
import type { ArticleEntry, PageEntry, TagEntry } from '../content/types'
import type { Diagnostic } from '../diagnostics/types'
@@ -7,10 +8,10 @@ export type Article = ArticleEntry & {
id: string
path: string
listed: boolean
- published: Date | null
- updated: Date | null
+ publishedAt: Date | null
parent: Article | null
children: Article[]
+ changes: Change[]
}
export type Page = PageEntry & {
@@ -26,12 +27,27 @@ export type Tag = TagEntry & {
articles: Article[]
}
+export type Release = {
+ id: string
+ version: string
+ date: Date
+ changes: Change[]
+}
+
+export type Change = {
+ kind: ChangeType
+ article: Article | null
+ text: string
+ release: Release
+}
+
export type Routed = Article | Page
export type Site = {
articles: Map
pages: Map
tags: Map
+ releases: Release[]
byPath: Map
known: Set
roots: Article[]
diff --git a/src/lib/site/validate.ts b/src/lib/site/validate.ts
index 6e4e181..3c5c9f8 100644
--- a/src/lib/site/validate.ts
+++ b/src/lib/site/validate.ts
@@ -4,17 +4,16 @@ import type { Diagnostics } from '../diagnostics/types'
import type { Article } from './types'
export function validate(articles: Iterable, content: Content, problems: Diagnostics) {
- for (const { id, status, summary, published, updated, tags } of articles) {
+ for (const { id, status, summary, publishedAt, tags } of articles) {
const { excerpt, cover } = summary
const error = (message: string) => problems.error(ARTICLES, id, message)
const warn = (message: string) => problems.warning(ARTICLES, id, message)
if (status !== 'draft' && !excerpt) error('Needs an excerpt')
- if (status !== 'draft' && !published) error('Needs a published date')
+ if (status !== 'draft' && !publishedAt) error('Needs a published date')
if (cover.image && !cover.alt) error('Cover image needs alt text')
if (!cover.image && (cover.alt || cover.caption || cover.credit))
warn('The cover has text but not image!')
- if (published && updated && updated < published) warn('Updated before it was published')
for (const tag of tags) if (!content.tags.has(tag)) error(`Tag "${tag}" does not exist`)
}
}
diff --git a/src/styles/components/post/article-footer.css b/src/styles/components/post/article-footer.css
index cdf9097..2dbc2b2 100644
--- a/src/styles/components/post/article-footer.css
+++ b/src/styles/components/post/article-footer.css
@@ -5,5 +5,17 @@
display: grid;
gap: var(--size-10);
+
+ @media (--bp-desktop) {
+ @mixin layout-columns;
+
+ & > .revision-card {
+ grid-column: body-start / body-end;
+ }
+
+ & > .sequence-nav {
+ grid-column: rail-start / margin-end;
+ }
+ }
}
}
diff --git a/src/styles/components/post/article-header.css b/src/styles/components/post/article-header.css
index 92d6f2d..eda91fb 100644
--- a/src/styles/components/post/article-header.css
+++ b/src/styles/components/post/article-header.css
@@ -109,17 +109,10 @@
.article-header__tag {
@mixin py var(--size-1);
@mixin px var(--size-3);
-
+ @mixin typo_tag;
display: block;
-
- font-size: var(--type--2);
- font-weight: var(--font-weight-emphasis);
- line-height: var(--leading--2);
color: var(--color-text-inverse);
- letter-spacing: var(--tracking-tag);
-
background-color: var(--color-surface-inverse);
-
transition: background-color var(--motion-fast) var(--ease-out);
&:hover {
diff --git a/src/styles/components/post/index.css b/src/styles/components/post/index.css
index 6320e09..c827ac9 100644
--- a/src/styles/components/post/index.css
+++ b/src/styles/components/post/index.css
@@ -2,3 +2,4 @@
@import url("stage-badge.css");
@import url("article-footer.css");
@import url("sequence-nav.css");
+@import url("revision-card.css");
diff --git a/src/styles/components/post/revision-card.css b/src/styles/components/post/revision-card.css
new file mode 100644
index 0000000..65a1b1e
--- /dev/null
+++ b/src/styles/components/post/revision-card.css
@@ -0,0 +1,92 @@
+@layer components.blocks {
+ .revision-card {
+ @mixin border-t var(--border-boundary), solid, var(--color-border-strong);
+ @mixin pt var(--size-8);
+ }
+
+ .revision-card__head {
+ @mixin mb var(--size-6);
+
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--size-2) var(--size-6);
+ align-items: baseline;
+ justify-content: space-between;
+ }
+
+ .revision-card__heading {
+ @mixin typo_heading-2;
+ @mixin ma var(--size-0);
+ }
+
+ .revision-card__subject {
+ @mixin typo_eyebrow;
+ @mixin ma var(--size-0);
+
+ color: var(--color-text-secondary);
+ }
+
+ .revision-card__list {
+ @mixin ma var(--size-0);
+ @mixin pa var(--size-0);
+
+ list-style: none;
+
+ @media (--bp-tablet-up) {
+ display: grid;
+ grid-template-columns: auto auto auto minmax(0, 1fr);
+ column-gap: var(--size-10);
+ }
+ }
+
+ .revision-card__row {
+ @mixin border-b var(--border-hairline), solid, var(--color-border-subtle);
+ @mixin py var(--size-6);
+
+ display: grid;
+ grid-template-columns: auto auto minmax(0, 1fr);
+ gap: var(--size-2) var(--size-8);
+ align-items: baseline;
+
+ font-family: var(--font-mono);
+ font-size: var(--type-0);
+ line-height: var(--leading-0);
+
+ @media (--bp-tablet-up) {
+ grid-column: 1 / -1;
+ grid-template-columns: subgrid;
+ }
+ }
+
+ .revision-card__version {
+ font-weight: var(--font-weight-emphasis);
+ color: var(--color-text-primary);
+ }
+
+ .revision-card__date {
+ color: var(--color-text-quiet);
+ }
+
+ .revision-card__kind {
+ @mixin typo_tag;
+ @mixin py var(--size-1);
+ @mixin px var(--size-3);
+
+ justify-self: start;
+ color: var(--color-text-inverse);
+ background-color: var(--color-surface-inverse);
+ }
+
+ .revision-card__text {
+ @mixin ma var(--size-0);
+
+ grid-column: 1 / -1;
+ font-family: var(--font-body);
+ line-height: var(--leading-prose-tight);
+ color: var(--color-text-primary);
+
+ @media (--bp-tablet-up) {
+ grid-column: auto;
+ }
+ }
+}
diff --git a/src/styles/mixins/helpers.css b/src/styles/mixins/helpers.css
index f7dd7d1..5b7e84e 100644
--- a/src/styles/mixins/helpers.css
+++ b/src/styles/mixins/helpers.css
@@ -15,3 +15,11 @@
width: min(100% - 2 * var(--layout-page-margin), var(--layout-wrapper-width));
}
+
+@define-mixin layout-columns {
+ grid-template-columns:
+ [rail-start] minmax(var(--layout-rail-width), 1fr)
+ [rail-end body-start] var(--layout-content-width)
+ [body-end margin-start] var(--layout-margin-width) [margin-end];
+ column-gap: var(--layout-gutter);
+}
diff --git a/src/styles/mixins/typography.css b/src/styles/mixins/typography.css
index bf11b7d..51123bb 100644
--- a/src/styles/mixins/typography.css
+++ b/src/styles/mixins/typography.css
@@ -108,3 +108,12 @@
text-transform: uppercase;
letter-spacing: var(--tracking-eyebrow);
}
+
+@define-mixin typo_tag {
+ font-family: var(--font-mono);
+ font-size: var(--type--2);
+ font-weight: var(--font-weight-emphasis);
+ line-height: var(--leading--2);
+ text-transform: uppercase;
+ letter-spacing: var(--tracking-tag);
+}