@@ -265,19 +265,19 @@ description: "A full-screen visual novel tour through GameCult's public wiki, pr
@@ -294,13 +294,13 @@ description: "A full-screen visual novel tour through GameCult's public wiki, pr
Moonshot Trajectory

-
Bifrost, Eve, CultMesh, and Sai form the launch path from worker-owned production into the cooperative surface web.
+
Bifrost, Thing, CultMesh, and Sai form the launch path from worker-owned production into the cooperative surface web.
-
Eve Surface Web Stack
-

-
Eve streams CultUI semantics across web, desktop, mobile, game UI, and creator overlays without splitting truth into dashboard islands.
+
Thing Surface Web Stack
+

+
Thing streams Thing Script semantics across web, desktop, mobile, game UI, and creator overlays without splitting truth into dashboard islands.
diff --git a/README.md b/README.md
index deadd46..ed54d81 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,14 @@
`gamecult-site` is the public `gamecult.org` root site: studio pitch, projects, docs, blog, and the usual pile of ambitions trying to look organized in public.
+**Valuable here:** the GameCult brand, defined in code: `site/quartz.config.ts` (`configuration.theme`: typefaces, weights, palette tokens) and `site/quartz/styles/custom.scss` (ground wash, layout, page-type variants). `docs/brand-design-language.md` explains which decisions there were deliberate. Anything with a visual surface in the swarm follows this rather than inventing a look. Second, the published content: `GameCult/index.md` (manifesto), `GameCult/Projects/` (one public page per project), `GameCult/Blog/`, `GameCult/Docs/`.
+
+**Problem it solves:** the studio needs one public face and one visual identity that every repo and agent can read from source, instead of prose descriptions that drift. The Quartz engine itself is shared and lives in `GameCult/GameCult-Quartz`; this repo owns only the content and the GameCult overlay.
+
+**Entry points:** `site/quartz.config.ts`, `site/quartz.layout.ts`, `docs/brand-design-language.md`, `GameCult/`; build with `scripts/quartz/quartz.ps1 build` (see Local Development).
+
+**Live versus planned:** live is the site at gamecult.org, deployed to GitHub Pages from `main` by `.github/workflows/deploy-quartz.yml`, built on the shared engine in `GameCult/GameCult-Quartz` (extraction rationale: `docs/gamecult-quartz-extraction-plan.md`). In progress is the masthead and one-column redesign campaign (`docs/masthead-campaign.md` and the `docs/masthead-cut-*.spec.json` cut specs); treat that document as the plan, not as a description of the site today.
+
The site builds against the shared `GameCult-Quartz` engine. This repo owns published content and the GameCult overlay.
## Repo Shape
diff --git a/docs/brand-design-language.md b/docs/brand-design-language.md
index c0a45b6..50e27c9 100644
--- a/docs/brand-design-language.md
+++ b/docs/brand-design-language.md
@@ -116,8 +116,13 @@ of filling the row.
## Layout
-The site is one centred column. Its width is `--gamecult-column` (72rem),
-declared on `body` and applied once, as `.page`'s `max-width`. The masthead, the
+The site is one column that fills the screen. Its width is `--gamecult-column`
+(120rem, so the page runs edge to edge up to 1920px and monitors wider than that
+get side margins), declared on `body` and applied once, as `.page`'s
+`max-width`. Running prose (paragraphs, captions and definitions anywhere in the
+article, and the article's own lists and quotes) keeps a line of at most
+`--gamecult-measure` (76ch); tables, media, code, math and each page's own
+blocks take the column's width. The masthead, the
article and the site footer fill that column and share its left and right edges
on every page; nothing is centred inside it, and no page places anything beside
it. On `/tour` and `/Pitch` the visual novel is the whole page and the masthead
diff --git a/docs/research/stale-claims-project-page.md b/docs/research/stale-claims-project-page.md
new file mode 100644
index 0000000..4c43851
--- /dev/null
+++ b/docs/research/stale-claims-project-page.md
@@ -0,0 +1,16 @@
+# Stale claims: GameCult/Projects/VoidBot.md
+
+Serves follow-up `void-body:follow_up:chat-s335-340`. Page read at gamecult-site 84cf7d6. Ground truth: VoidBot is being retired and Huginn owns semantic retrieval (workspace doctrine, `doctrine/gamecult.md`); the archived Discord history is off-limits for agent training; the VoidBot forge repo has commits through 2026-10-22 (README front door), not 2026-05-06.
+
+1. `GameCult/Projects/VoidBot.md:3` description: "Discord-native assistant ... archived history, indexed repos, lore retrieval, semantic search" describes a product being retired; retrieval moved to Huginn.
+2. `GameCult/Projects/VoidBot.md:4` socialDeck: "Discord memory, repo retrieval, Codex handoff." same retired framing.
+3. `GameCult/Projects/VoidBot.md:9` tagline: "A Discord-native assistant, ... one more opinionated little archivist." presents it as a live server resident.
+4. `GameCult/Projects/VoidBot.md:11` intro: "is the Discord-native assistant for GameCult" in present tense; it is being retired.
+5. `GameCult/Projects/VoidBot.md:11` "answers out of archived chat history ... runs semantic search" is the capability Huginn now owns.
+6. `GameCult/Projects/VoidBot.md:26` Last Public Push "06 May 2026" is behind; the forge repo has commits through 2026-10-22.
+7. `GameCult/Projects/VoidBot.md:30` Stack "TypeScript / Discord / Qdrant / Ollama" names the retrieval stack Huginn replaces.
+8. `GameCult/Projects/VoidBot.md:34` Status "Active Discord assistant" contradicts the retirement.
+9. `GameCult/Projects/VoidBot.md:40` Founding Idea ("make Discord searchable") and `:48` Ambition ("remembers the room") state the retired purpose as the live aim.
+10. `GameCult/Projects/VoidBot.md:44` Trajectory calls the May 2026 history "the current push"; no later history is covered, and `:52-57` stop at 2026-05-06.
+
+Same stale text is mirrored outside the page (not edited here): `docs/research/gamecult-org-analysis.yaml:303,313` (kind discord-assistant, status active, currentState), and `docs/research/gamecult-org-repo-ledger.json`.
diff --git a/scripts/checks/rule-token.mjs b/scripts/checks/rule-token.mjs
new file mode 100644
index 0000000..46a4595
--- /dev/null
+++ b/scripts/checks/rule-token.mjs
@@ -0,0 +1,18 @@
+// Target invariant site-masthead:one-rule-token: no literal hairline colour in custom.scss;
+// every grouping rule (1px single-side border, column-rule) reads --gamecult-rule.
+// The VN speaker card is game UI, not grouping, and keeps its own edge colours.
+import { readFileSync } from "node:fs"
+
+const path = process.argv[2] ?? "site/quartz/styles/custom.scss"
+const hairline = /^\s*(border-(top|bottom|left|right)|column-rule):\s*1px solid\s+(rgba?\(|#)/
+let selector = ""
+const bad = []
+readFileSync(path, "utf8").split("\n").forEach((line, i) => {
+ if (line.trimEnd().endsWith("{")) selector = line
+ if (hairline.test(line) && !selector.includes("sai-speaker")) bad.push(`${path}:${i + 1}: ${line.trim()}`)
+})
+if (bad.length) {
+ console.error(`literal hairlines (use var(--gamecult-rule)):\n${bad.join("\n")}`)
+ process.exit(1)
+}
+console.log("one-rule-token: ok")
diff --git a/site/quartz.config.ts b/site/quartz.config.ts
index 79d969e..9d014d0 100644
--- a/site/quartz.config.ts
+++ b/site/quartz.config.ts
@@ -81,7 +81,8 @@ const config: QuartzConfig = {
Plugin.GitHubFlavoredMarkdown(),
Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }),
Plugin.Description(),
- Plugin.Latex({ renderEngine: "katex" }),
+ // MathML beside the visible KaTeX: screen readers read it, KaTeX hides it visually.
+ Plugin.Latex({ renderEngine: "katex", katexOptions: { output: "htmlAndMathml" } }),
],
filters: [Plugin.RemoveDrafts()],
emitters: [
diff --git a/site/quartz.layout.ts b/site/quartz.layout.ts
index 1aa294b..bdcc86e 100644
--- a/site/quartz.layout.ts
+++ b/site/quartz.layout.ts
@@ -3,6 +3,7 @@ import * as Component from "./quartz/components"
import GameCultMasthead from "./quartz/components/GameCultMasthead"
import GameCultArticleMeta from "./quartz/components/GameCultArticleMeta"
import GameCultThemeLock from "./quartz/components/GameCultThemeLock"
+import GameCultEstateDoors from "./quartz/components/GameCultEstateDoors"
const isGraphPage = (page: any) => page.fileData.slug === "Graph"
const isThingPage = (page: any) => page.fileData.slug === "Thing"
@@ -15,7 +16,7 @@ const isBlogArticle = (page: any) =>
export const sharedPageComponents: SharedLayout = {
head: Component.Head(),
header: [GameCultThemeLock(), GameCultMasthead()],
- afterBody: [],
+ afterBody: [GameCultEstateDoors()],
footer: Component.Footer({
links: {
GitHub: "https://github.com/GameCult",
diff --git a/site/quartz/components/GameCultEstateDoors.tsx b/site/quartz/components/GameCultEstateDoors.tsx
new file mode 100644
index 0000000..8c46e9b
--- /dev/null
+++ b/site/quartz/components/GameCultEstateDoors.tsx
@@ -0,0 +1,55 @@
+import { QuartzComponent, QuartzComponentConstructor } from "./types"
+
+// Without scripts all three doors stay visible. With scripts the page grows a
+// tab row and shows one door at a time.
+const estateDoorsScript = `
+document.addEventListener("nav", () => {
+ const root = document.querySelector("[data-estate-doors]")
+ if (!root || root.dataset.ready) return
+ root.dataset.ready = "1"
+ const doors = Array.from(root.querySelectorAll(".estate-door"))
+ const bar = document.createElement("div")
+ bar.className = "estate-switch"
+ bar.setAttribute("role", "tablist")
+ const tabs = doors.map((door) => {
+ const tab = document.createElement("button")
+ tab.type = "button"
+ tab.className = "estate-tab"
+ tab.id = "tab-" + door.id
+ tab.setAttribute("role", "tab")
+ tab.setAttribute("aria-controls", door.id)
+ tab.textContent = door.dataset.estate
+ door.setAttribute("role", "tabpanel")
+ door.setAttribute("aria-labelledby", tab.id)
+ bar.appendChild(tab)
+ return tab
+ })
+ const show = (i, focus) => {
+ doors.forEach((d, j) => {
+ d.hidden = j !== i
+ tabs[j].setAttribute("aria-selected", String(j === i))
+ tabs[j].tabIndex = j === i ? 0 : -1
+ })
+ if (focus) tabs[i].focus()
+ }
+ tabs.forEach((tab, i) => {
+ tab.addEventListener("click", () => show(i, false))
+ tab.addEventListener("keydown", (e) => {
+ const step = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0
+ if (step) {
+ e.preventDefault()
+ show((i + step + tabs.length) % tabs.length, true)
+ }
+ })
+ })
+ root.insertBefore(bar, root.firstChild)
+ const hashed = doors.findIndex((d) => "#" + d.id === location.hash)
+ show(Math.max(hashed, 0), false)
+})
+`
+
+export default (() => {
+ const GameCultEstateDoors: QuartzComponent = () => null
+ GameCultEstateDoors.afterDOMLoaded = estateDoorsScript
+ return GameCultEstateDoors
+}) satisfies QuartzComponentConstructor
diff --git a/site/quartz/styles/custom.scss b/site/quartz/styles/custom.scss
index 7fd5702..cb44a75 100644
--- a/site/quartz/styles/custom.scss
+++ b/site/quartz/styles/custom.scss
@@ -30,8 +30,11 @@ body {
--gamecult-rule: rgba(148, 163, 184, 0.22);
/* The ground's radial intensity; 1 on the home page. */
--gamecult-wash: 0.5;
- /* The site's one column; .page's max-width; the masthead, article and footer share its edges. */
- --gamecult-column: 72rem;
+ /* The site's one column; .page's max-width; the masthead, article and footer share its edges.
+ The page fills the screen up to 1920px; only wider monitors get side margins. */
+ --gamecult-column: 120rem;
+ /* The reading measure: the longest line of running prose, wherever the column ends. */
+ --gamecult-measure: 76ch;
}
body[data-slug="index"] {
@@ -313,7 +316,7 @@ body[data-slug="Blog/witness-authoritative-networking"] {
.ritual-paper-results-strip span {
padding: 0.85rem 0.65rem;
color: rgba(232, 237, 244, 0.88);
- border-right: 1px solid rgba(255, 178, 30, 0.2);
+ border-right: 1px solid var(--gamecult-rule);
font-family: "IBM Plex Mono", Consolas, monospace;
font-size: 0.76rem;
line-height: 1.45;
@@ -355,7 +358,6 @@ body[data-slug="Blog/witness-authoritative-networking"] {
.katex-display {
margin: 1.15rem 0;
padding: 0.8rem 1rem;
- overflow-x: auto;
border: 1px solid rgba(96, 119, 255, 0.38);
border-radius: 6px;
background:
@@ -369,7 +371,7 @@ body[data-slug="Blog/witness-authoritative-networking"] {
gap: 1rem;
margin: 2rem 0 0;
padding-top: 0.95rem;
- border-top: 1px solid rgba(174, 189, 210, 0.2);
+ border-top: 1px solid var(--gamecult-rule);
column-span: all;
}
@@ -405,7 +407,7 @@ body[data-slug="Blog/witness-authoritative-networking"] {
.ritual-paper-page {
column-count: 2;
column-gap: 2.6rem;
- column-rule: 1px solid rgba(49, 88, 255, 0.2);
+ column-rule: 1px solid var(--gamecult-rule);
}
.ritual-paper-page::before,
@@ -849,10 +851,17 @@ article > h6 {
margin-top: 0;
}
+/* Running prose keeps a readable line wherever it sits: tables, media, code, math and the page's own blocks take the
+ column's width, but the paragraphs, captions and definitions inside them do not. */
+article :is(p, figcaption, dd, .integrated-dossier-note, .integrated-dossier-lead),
+article > :is(ul, ol, blockquote, dl):not(.gamecult-entries) {
+ max-width: var(--gamecult-measure);
+}
+
/* KaTeX does not break inside a formula, so math wider than the column scrolls
- in its own paragraph or display block instead of widening the page; math that
- fits shows no scrollbar. */
-article p:has(.katex),
+ in its own block (paragraph, list item, heading, table cell, display block)
+ instead of widening the page; math that fits shows no scrollbar. */
+article :is(p, li, h1, h2, h3, h4, h5, h6, th, td):has(.katex),
article .katex-display {
overflow-x: auto;
overflow-y: hidden;
@@ -1537,7 +1546,7 @@ body[data-slug="Pitch"] .gamecult-compound-vn .sai-speaker-stage.has-dom-cards {
}
.gamecult-studio-page > p {
- max-width: 76ch;
+ max-width: var(--gamecult-measure);
margin: 1rem 0;
color: var(--gamecult-studio-copy);
font-size: 1.06rem;
@@ -2022,3 +2031,181 @@ article.integrated-dossier-page {
@media (max-width: 800px) {
.swarm-directory { grid-template-columns: minmax(0, 1fr); }
}
+
+/* Projects: one section per steward Mind, portrait beside its pitch. */
+.steward-index {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem 1.6rem;
+ margin: 1.5rem 0 1rem;
+}
+
+.steward-index a {
+ font-family: var(--titleFont);
+ font-size: 1.05rem;
+}
+
+.steward-index span {
+ margin-left: 0.3rem;
+ color: var(--gray);
+ font-family: var(--bodyFont);
+ font-size: 0.8rem;
+}
+
+.steward {
+ display: grid;
+ grid-template-columns: 11rem minmax(0, 1fr);
+ gap: 2rem;
+ align-items: start;
+ padding-top: 2rem;
+ margin-top: 2rem;
+ border-top: 1px solid var(--gamecult-rule);
+ scroll-margin-top: 1rem;
+}
+
+.steward-portrait {
+ width: 100%;
+ height: auto;
+ margin: 0;
+ border-radius: 0;
+}
+
+.steward-body h2 {
+ margin: 0.3rem 0 0.8rem;
+}
+
+.steward-voice {
+ color: var(--gamecult-studio-copy);
+ font-size: 1.08rem;
+ line-height: 1.7;
+}
+
+.steward-projects {
+ max-width: 72ch;
+ list-style: none;
+ padding: 0;
+ margin: 1.2rem 0 0;
+}
+
+.steward-projects li {
+ margin: 0 0 0.95rem;
+ line-height: 1.5;
+ overflow-wrap: anywhere;
+}
+
+.steward-projects li > a:first-of-type {
+ font-weight: 600;
+}
+
+.steward-projects li em {
+ display: block;
+ margin-top: 0.15rem;
+ color: var(--gamecult-studio-copy);
+ font-size: 0.95rem;
+ font-style: normal;
+}
+
+@media (max-width: 800px) {
+ .steward {
+ grid-template-columns: minmax(0, 1fr);
+ gap: 0.75rem;
+ }
+
+ .steward-portrait {
+ width: 8rem;
+ }
+}
+
+/* Stacked entries: name and status on one line, description full width beneath.
+ Ruled rows, no fill, border box or pill, per the Surfaces law. */
+.gamecult-entries {
+ margin: 1rem 0 0;
+ padding: 0;
+ list-style: none;
+}
+
+.gamecult-entry {
+ padding: 0.85rem 0;
+ border-top: 1px solid var(--gamecult-rule);
+}
+
+.gamecult-entry p {
+ margin: 0;
+}
+
+.gamecult-entry-head {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 0.2rem 1rem;
+}
+
+.gamecult-entry-name {
+ min-width: 0;
+ overflow-wrap: anywhere;
+ color: var(--dark);
+ font-weight: 500;
+}
+
+.gamecult-entry-status {
+ flex: none;
+ color: rgba(255, 138, 42, 0.96);
+ font-family: var(--titleFont);
+ font-size: 0.76rem;
+ font-weight: 300;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+}
+
+.gamecult-entry-body {
+ margin-top: 0.25rem;
+ color: rgba(183, 199, 217, 0.88);
+ font-size: 0.94rem;
+ line-height: 1.55;
+}
+
+.gamecult-entry-lead::first-letter {
+ text-transform: uppercase;
+}
+
+/* Estate doors: one pitch at a time when scripts run; all three stacked when not. */
+.estate-switch {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ margin: 1rem 0 0.5rem;
+}
+
+.estate-tab {
+ flex: 1 1 8rem;
+ min-height: 44px;
+ padding: 0.5rem 1rem;
+ border: 1px solid var(--gamecult-rule);
+ border-radius: 0;
+ background: transparent;
+ color: var(--dark);
+ font-family: var(--titleFont);
+ font-size: 0.82rem;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ cursor: pointer;
+}
+
+.estate-tab[aria-selected="true"] {
+ border-color: rgba(255, 138, 42, 0.96);
+ color: rgba(255, 138, 42, 0.96);
+}
+
+.estate-tab:focus-visible {
+ outline: 2px solid rgba(255, 138, 42, 0.96);
+ outline-offset: 2px;
+}
+
+.estate-door[hidden] {
+ display: none;
+}
+
+.estate-door h3 {
+ margin-top: 1.4rem;
+}