Add web-rendered portfolio dossier

This commit is contained in:
Meta Vens 2026-05-31 20:51:24 +01:00
commit 131921f97a
4 changed files with 1616 additions and 0 deletions

File diff suppressed because it is too large Load diff

View file

@ -24,6 +24,8 @@ sidebarGroups:
slug: Docs/Bifrost
- label: Portfolio Pitch
slug: Pitch
- label: Portfolio Dossier
slug: Portfolio-Dossier
- title: Projects
links:
- label: Aetheria

View file

@ -0,0 +1,252 @@
import fs from "node:fs";
import path from "node:path";
const root = process.cwd();
const dossierRoot = path.join(root, "docs", "portfolio-dossier");
const outputPath = path.join(root, "GameCult", "Portfolio-Dossier.md");
const figureMap = {
"figures/moonshot-trajectory.pdf": "/static/interactive/portfolio-pitch/figures/moonshot-trajectory.svg",
"figures/value-orbit-map.pdf": "/static/interactive/portfolio-pitch/figures/value-orbit-map.svg",
"figures/patronage-furnace.pdf": "/static/interactive/portfolio-pitch/figures/patronage-furnace.svg",
"figures/surface-web-stack.pdf": "/static/interactive/portfolio-pitch/figures/surface-web-stack.svg",
};
function readTex(relativePath) {
return fs.readFileSync(path.join(dossierRoot, `${relativePath}.tex`), "utf8");
}
function expandInputs(tex, currentDir = "") {
return tex.replace(/\\input\{([^}]+)\}/g, (_, inputPath) => {
if (inputPath === "preamble") return "";
if (inputPath === "figures/surface-web-stack") return "\n\\SurfaceWebStack\n";
if (inputPath === "figures/value-furnace") return "\n\\ValueFurnace\n";
const relative = path.normalize(path.join(currentDir, inputPath)).replace(/\\/g, "/");
const file = path.join(dossierRoot, `${relative}.tex`);
if (!fs.existsSync(file)) return "";
return `\n${expandInputs(fs.readFileSync(file, "utf8"), path.dirname(relative))}\n`;
});
}
function readBraceArg(text, start) {
while (text[start] && /\s/.test(text[start])) start += 1;
if (text[start] !== "{") return null;
let depth = 0;
for (let index = start; index < text.length; index += 1) {
const char = text[index];
if (char === "\\" && text[index + 1]) {
index += 1;
continue;
}
if (char === "{") depth += 1;
if (char === "}") depth -= 1;
if (depth === 0) {
return {
value: text.slice(start + 1, index),
end: index + 1,
};
}
}
return null;
}
function replaceCommand(text, command, replacer) {
let output = "";
let cursor = 0;
const token = `\\${command}`;
while (cursor < text.length) {
const index = text.indexOf(token, cursor);
if (index === -1) {
output += text.slice(cursor);
break;
}
output += text.slice(cursor, index);
let next = index + token.length;
const args = [];
while (true) {
const arg = readBraceArg(text, next);
if (!arg) break;
args.push(arg.value);
next = arg.end;
}
output += replacer(args);
cursor = next;
}
return output;
}
function stripInline(tex) {
return tex
.replace(/\\textbf\{([^{}]+)\}/g, "**$1**")
.replace(/\\texttt\{([^{}]+)\}/g, "`$1`")
.replace(/\\color\{[^}]+\}/g, "")
.replace(/\\Large/g, "")
.replace(/\\\\\[[^\]]*\]/g, "\n")
.replace(/\\\\/g, "\n")
.replace(/\\_/g, "_")
.replace(/\\%/g, "%")
.replace(/[ \t]+\n/g, "\n")
.trim();
}
function callout(label, body, kind = "orange") {
return `<aside class="portfolio-dossier-callout ${kind}">\n<strong>${label}</strong>\n\n${stripInline(body)}\n</aside>`;
}
function projectionTable(rows) {
const body = rows
.map(([label, value]) => `<tr><th>${label}</th><td>${stripInline(value)}</td></tr>`)
.join("\n");
return `<table class="portfolio-dossier-table">\n<tbody>\n${body}\n</tbody>\n</table>`;
}
function figure(src, caption) {
const image = figureMap[src] || src;
return `<figure class="portfolio-dossier-figure">\n<img src="${image}" alt="${stripInline(caption)}" />\n<figcaption>${stripInline(caption)}</figcaption>\n</figure>`;
}
function convertSpecialBlocks(tex) {
return tex
.replace(
/\\begin\{center\}\s*\\fcolorbox\{voidorange\}\{voidpanel\}\{\\parbox\{[^}]+\}\{\\color\{voidtext\}\\Large\s*([\s\S]*?)\}\}\s*\\end\{center\}/g,
(_, body) => callout("Public spell", body, "orange"),
)
.replace(
/\\begin\{center\}\s*\\fcolorbox\{voidorange\}\{voidpanel\}\{\\parbox\{[^}]+\}\{\\color\{voidtext\}\s*([\s\S]*?)\}\}\s*\\end\{center\}/g,
(_, body) => callout("Pipeline", body, "orange"),
)
.replace(
/\\begin\{center\}\s*\\fcolorbox\{voidblue\}\{voidpanel\}\{\\parbox\{[^}]+\}\{\\color\{voidtext\}\s*([\s\S]*?)\}\}\s*\\end\{center\}/g,
(_, body) => callout("Surface contract", body, "blue"),
);
}
function convertFigureTables(tex) {
return tex
.replace(
/\\SurfaceWebStack/g,
`<section class="portfolio-dossier-card">\n<h4>Eve Surface-Web Stack</h4>\n${projectionTable([
["Provider", "Owns state, permissions, commands, and acceptance."],
["CultUI Tree", "Declares semantic controls, bindings, layout intent, and provenance."],
["Eve", "Projects one surface contract into web, desktop, mobile, game UI, and overlays."],
["Renderer", "Chooses native affordances without stealing authority from the provider."],
])}\n</section>`,
)
.replace(
/\\ValueFurnace/g,
`<section class="portfolio-dossier-card">\n<h4>Value Furnace</h4>\n${projectionTable([
["Input", "Patronage, game spend, verified work, support, and risk."],
["Normalization", "Project-local rules turn events into effective points."],
["Governance", "Log-power voting keeps power legible without pretending all support is equal."],
["Output", "Priority, reward pressure, credit, receipts, and scoped voting weight."],
])}\n</section>`,
);
}
function convertTexToMarkdown(tex) {
let text = tex
.replace(/\\documentclass[\s\S]*?\\begin\{document\}/, "")
.replace(/\\PortfolioTitlePage/g, "")
.replace(/\\tableofcontents/g, "")
.replace(/\\clearpage/g, "")
.replace(/\\appendix/g, "\n## Appendix\n")
.replace(/\\end\{document\}/g, "");
text = convertFigureTables(convertSpecialBlocks(text));
text = replaceCommand(text, "ProjectHeader", ([name, domain, thesis, room]) => {
return `\n## ${stripInline(name)}\n\n<section class="portfolio-dossier-project-meta">\n${projectionTable([
["Domain", domain],
["Thesis", thesis],
["Homepage Room", room],
])}\n</section>\n`;
});
text = replaceCommand(text, "ProjectionBand", ([base, sharp, mythic, failure]) => {
return `\n<section class="portfolio-dossier-card projection-band">\n<h4>Projection Band</h4>\n${projectionTable([
["Base Case", base],
["Sharp Case", sharp],
["Mythic Case", mythic],
["Failure Mode", failure],
])}\n</section>\n`;
});
text = replaceCommand(text, "ValueLadder", ([survival, product, platform, worldWeb]) => {
return `\n<section class="portfolio-dossier-card value-ladder">\n<h4>Value Projection Ladder</h4>\n${projectionTable([
["Survival", survival],
["Product", product],
["Platform", platform],
["World-Web", worldWeb],
])}\n</section>\n`;
});
text = replaceCommand(text, "MoonGraphic", ([, src, caption]) => `\n${figure(src, caption)}\n`);
text = replaceCommand(text, "Verdict", ([body]) => `\n${callout("Void says", body, "orange")}\n`);
text = replaceCommand(text, "Term", ([body]) => `<span class="portfolio-term">${stripInline(body)}</span>`);
text = replaceCommand(text, "Signal", ([body]) => `<span class="portfolio-signal">${stripInline(body)}</span>`);
text = text
.replace(/\\chapter\{([^}]+)\}/g, "\n## $1\n")
.replace(/\\section\{([^}]+)\}/g, "\n### $1\n")
.replace(/\\subsection\{([^}]+)\}/g, "\n#### $1\n")
.replace(/\\begin\{itemize\}/g, "")
.replace(/\\end\{itemize\}/g, "")
.replace(/^\s*\\item\s+/gm, "- ")
.replace(/\\begin\{center\}|\\end\{center\}/g, "")
.replace(/\\begin\{tabularx\}[\s\S]*?\\end\{tabularx\}/g, "")
.replace(/\\begin\{figure\}[\s\S]*?\\end\{figure\}/g, "")
.replace(/\\noindent/g, "")
.replace(/\\vspace\{[^}]+\}/g, "")
.replace(/\\href\{([^}]+)\}\{([^}]+)\}/g, "[$2]($1)")
.replace(/\\\[/g, () => "$$")
.replace(/\\\]/g, () => "$$")
.replace(/\\rightarrow/g, "\\to")
.replace(/\\max/g, "\\max")
.replace(/\\_/g, "_")
.replace(/\\%/g, "%")
.replace(/\n{3,}/g, "\n\n")
.trim();
return text;
}
const expanded = expandInputs(readTex("main"));
const body = convertTexToMarkdown(expanded);
const generatedAt = new Date().toISOString().slice(0, 10);
const page = `---
title: Portfolio Dossier
description: "GameCult's over-the-top TeX portfolio dossier rendered as a dark-mode web page with Montserrat and Ubuntu branding."
enableToc: true
cssclasses:
- portfolio-dossier-page
sidebarGroups:
- title: Studio
links:
- label: Visual Novel Home
slug: index
- label: Portfolio Pitch
slug: Pitch
- label: Portfolio Dossier
slug: Portfolio-Dossier
- label: Project Atlas
slug: Projects
- label: Graph
slug: Graph
---
<section class="portfolio-dossier-hero">
<p class="portfolio-dossier-kicker">GameCult / Void Dossier</p>
<h1>Portfolio Dossier</h1>
<p>Eve, Bifrost, CultMesh, Sai, and the surface-web portfolio. Same TeX source, rendered for humans who would rather not receive a white PDF brick to the face.</p>
<div class="portfolio-dossier-actions">
<a href="/static/interactive/portfolio-pitch/figures/moonshot-trajectory.svg">Moonshot graphic</a>
<a href="/Pitch">Play the VN pitch</a>
<a href="https://github.com/GameCult">GameCult GitHub</a>
</div>
<p class="portfolio-dossier-source">Generated from <code>docs/portfolio-dossier/main.tex</code> on ${generatedAt}.</p>
</section>
${body}
`;
fs.writeFileSync(outputPath, page, "utf8");
console.log(`Wrote ${path.relative(root, outputPath)}`);

View file

@ -1845,3 +1845,215 @@ body[data-slug="Pitch"] .gamecult-pitch-vn .sai-speaker-stage.has-dom-cards {
min-height: 520px;
}
}
.portfolio-dossier-page {
--dossier-panel: rgba(8, 13, 21, 0.9);
--dossier-panel-strong: rgba(11, 17, 26, 0.96);
--dossier-border: rgba(89, 183, 255, 0.18);
--dossier-orange: #ff8a2a;
--dossier-blue: #59b7ff;
article {
font-family: "Ubuntu", var(--bodyFont), sans-serif;
font-weight: 300;
}
article h1,
article h2,
article h3,
article h4 {
font-family: "Montserrat", var(--headerFont), sans-serif;
font-weight: 100;
letter-spacing: 0;
}
article h2 {
margin-top: 3.2rem;
padding-bottom: 0.45rem;
border-bottom: 1px solid rgba(89, 183, 255, 0.14);
color: var(--dossier-blue);
}
article h3 {
color: var(--dossier-orange);
}
article h4 {
color: var(--dossier-blue);
font-weight: 300;
}
article p,
article li,
article td {
line-height: 1.68;
}
}
.portfolio-dossier-hero {
position: relative;
margin: 0 0 2.4rem;
padding: clamp(1.4rem, 4vw, 3.6rem);
border: 1px solid rgba(89, 183, 255, 0.2);
border-radius: 8px;
background:
linear-gradient(rgba(5, 7, 11, 0.84), rgba(5, 7, 11, 0.92)),
url("/static/interactive/portfolio-pitch/figures/moonshot-trajectory.svg") center / cover;
box-shadow:
0 28px 70px rgba(0, 0, 0, 0.34),
inset 0 1px 0 rgba(255, 255, 255, 0.04);
overflow: hidden;
}
.portfolio-dossier-kicker,
.portfolio-dossier-source {
margin: 0;
color: rgba(183, 199, 217, 0.78);
font-size: 0.78rem;
font-weight: 500;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.portfolio-dossier-hero h1 {
margin: 0.25rem 0 0.85rem;
color: #edf6ff;
font-size: clamp(2.4rem, 8vw, 5.8rem);
font-weight: 100 !important;
line-height: 0.95;
}
.portfolio-dossier-hero p:not(.portfolio-dossier-kicker, .portfolio-dossier-source) {
max-width: 48rem;
color: #edf6ff;
font-size: clamp(1rem, 1.6vw, 1.28rem);
}
.portfolio-dossier-actions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin: 1.4rem 0 0.9rem;
}
.portfolio-dossier-actions a {
display: inline-flex;
align-items: center;
min-height: 2.35rem;
padding: 0.48rem 0.78rem;
border: 1px solid rgba(255, 138, 42, 0.36);
border-radius: 6px;
background: rgba(5, 7, 11, 0.72);
color: #ffd6ad;
font-family: "Ubuntu", var(--bodyFont), sans-serif;
font-weight: 500;
text-decoration: none;
}
.portfolio-dossier-callout,
.portfolio-dossier-card,
.portfolio-dossier-project-meta,
.portfolio-dossier-figure {
margin: 1.25rem 0;
border: 1px solid var(--dossier-border);
border-radius: 8px;
background:
linear-gradient(135deg, var(--dossier-panel-strong), var(--dossier-panel)),
radial-gradient(circle at 100% 0%, rgba(255, 138, 42, 0.12), transparent 32%);
box-shadow: 0 18px 44px rgba(0, 0, 0, 0.2);
}
.portfolio-dossier-callout {
padding: 1rem 1.15rem;
}
.portfolio-dossier-callout strong {
display: block;
margin-bottom: 0.35rem;
color: var(--dossier-orange);
font-family: "Montserrat", var(--headerFont), sans-serif;
font-weight: 300;
text-transform: uppercase;
}
.portfolio-dossier-callout.blue strong {
color: var(--dossier-blue);
}
.portfolio-dossier-card,
.portfolio-dossier-project-meta {
padding: 1rem;
}
.portfolio-dossier-table {
width: 100%;
margin: 0;
border-collapse: collapse;
}
.portfolio-dossier-table th,
.portfolio-dossier-table td {
padding: 0.72rem 0.4rem;
border-bottom: 1px solid rgba(89, 183, 255, 0.1);
vertical-align: top;
}
.portfolio-dossier-table th {
width: 10.5rem;
color: var(--dossier-blue);
font-family: "Montserrat", var(--headerFont), sans-serif;
font-weight: 300;
text-align: left;
}
.portfolio-dossier-table tr:last-child th,
.portfolio-dossier-table tr:last-child td {
border-bottom: 0;
}
.portfolio-dossier-figure {
padding: 0.75rem;
}
.portfolio-dossier-figure img {
display: block;
width: 100%;
border-radius: 6px;
background: #05070b;
}
.portfolio-dossier-figure figcaption {
margin-top: 0.75rem;
color: #ffd6ad;
font-size: 0.95rem;
}
.portfolio-term {
color: var(--dossier-orange);
font-weight: 500;
}
.portfolio-signal {
color: var(--dossier-blue);
font-weight: 500;
}
@media (max-width: 720px) {
.portfolio-dossier-table,
.portfolio-dossier-table tbody,
.portfolio-dossier-table tr,
.portfolio-dossier-table th,
.portfolio-dossier-table td {
display: block;
width: 100%;
}
.portfolio-dossier-table th {
padding-bottom: 0.2rem;
border-bottom: 0;
}
.portfolio-dossier-table td {
padding-top: 0;
}
}