Parva Machina
// STANDARDS

Parva Machina standards

Every game has its own site, its own code and its own palette. The publisher is the only thing they share, and this page holds the rules for that.

The previews on this page run on the very file you would copy. If a preview looks wrong, the standard is wrong.

This standard is a layer on top of the Centvix standards. Wherever it is silent, the Centvix standard applies.

// §1 — SCOPE

Scope

This standard applies to the websites and repositories of every Parva Machina sub-project: the game sites and their code. It does not apply to parvamachina.com itself, which is a direct Centvix product and follows the Centvix standard as written.

Rule levels are the same as in the Centvix standard. must is a hard requirement; a site that breaks it does not launch. should is a strong recommendation; if a site deviates, the reason is written down in the repository README.

// §2 — OVERRIDES

Overrides

Four rules differ from the Centvix standard. Everything else, including the founder copy, the five required pages and the legal link order, stays as Centvix defines it.

#Centvix ruleParva Machina rule
1About page ends with "About Centvix", then "About the founder"About page ends with "About Parva Machina" (section 1), then "About the founder"; the founder copy is unchanged
2Footer's third column carries the "About Centvix" copy and a centvix.com linkFooter's third column carries the "About Parva Machina" copy and a parvamachina.com link
3Copyright bar reads "Copyright © YEAR Centvix", Centvix linking to https://centvix.comCopyright bar reads "Copyright © YEAR Parva Machina", Parva Machina linking to https://parvamachina.com; the year is computed, never hard-coded
4Footer ratio 6-2-4Footer ratio 5·3·4 on game sites (a registered exception)
// §3 — ADDITIONAL RULES

Additional rules

  1. 01must

    Copy all six component files (pmf.css, pmf-types.ts, pmf-data.ts, pmf-icons.ts, pmf-render.ts, and PmFooter.tsx for React sites) and import pmf.css once, in the root layout.

  2. 02must

    Map the three required tokens (--pmf-bg, --pmf-fg, --pmf-accent) by reference, never by value, so the site's theme variants reach the footer.

  3. 03should

    If the site's border token is an opaque colour rather than an alpha, set all four hairline steps explicitly: --pmf-line-soft, --pmf-line, --pmf-line-strong, --pmf-line-tick.

  4. 04must

    Pass the footer resolved strings and ready-made hrefs. The standard never constructs a URL; the site's own path builder stays the single source of truth.

  5. 05must

    Run node scripts/sync-network.mjs before the build. It fetches https://parvamachina.com/network.json into a gitignored copy; the committed network.fallback.json is the offline path. A failed fetch is a warning, not a failed release.

  6. 06must

    Call assertKnownGame(registry, ownId). A misspelt id makes self-exclusion match nothing and the game links to itself in its own footer.

  7. 07must

    If the site shows store badges, commit Apple's and Google's official artwork per locale to the site's own public/ directory as appstore-<locale>.svg and googleplay-<locale>.svg, and render them untouched: no radius, no filter, no opacity, no recolour.

  8. 08must

    Leave --pmf-brass alone. The Parva Machina plate keeps the brand's own dark ground on every host, at zero radius.

  9. 09should

    Anything the tokens cannot express goes upstream into pmf.css, not into a local patch.

// §4 — CHECKLIST

Pre-launch checklist

Before a game site launches, every item below is checked off, on top of the six items in the Centvix checklist (read with the overrides above).

  • The About page ends with the "About Parva Machina" and "About the founder" sections; both links work.

  • The footer uses the 5·3·4 layout, with the fixed Parva Machina copy in the third column.

  • The copyright bar reads "Copyright © YEAR Parva Machina", with Parva Machina linked to https://parvamachina.com.

  • All six pmf component files are copied (PmFooter.tsx on React sites), pmf.css is imported once, and the three tokens are mapped by reference.

  • sync-network.mjs runs before the build, network.fallback.json is committed, and assertKnownGame is called with the site's own id.

  • Store badges, if shown, are the official artwork per locale, untouched.

// COMPONENT REFERENCE

Component reference

The rest of this page is the component reference: the token contract and the four components that make Parva Machina visible on a game's own site, each with a live preview that runs on the very file you would copy.

// 00 — THE CONTRACT

Token contract

The component knows nothing about the host's tokens. Three variables are required; everything else derives from them. A host writes the mapping once and gets all four components.

Required
  • --pmf-bgthe footer’s ground
  • --pmf-fgprimary ink; the hairlines derive from it
  • --pmf-accentthe game’s signature — not brass
Derived
  • --pmf-fg-mutedcolor-mix(in oklab, fg 72%, bg)
  • --pmf-fg-subtlecolor-mix(in oklab, fg 56%, bg)
  • --pmf-surfacecolor-mix(in oklab, fg 4%, bg)
  • --pmf-line-softfg @ 9%
  • --pmf-linefg @ 13%
  • --pmf-line-strongfg @ 18%
  • --pmf-line-tickfg @ 32%
  • --pmf-radius0
  • --pmf-hover-shadownone
  • --pmf-focus-shadownone
  • --pmf-font-sansinherit
  • --pmf-font-monoui-monospace, …
  • --pmf-scale1
  • --pmf-maxw1180px
Locked
  • --pmf-brass#c8892f
  • .pmf__pm-plate#131211 / #e8e3d8 / radius 0
Host mappingglobals.css
/* Once, in the game's own stylesheet. */
.pmf,
.pmf-topbar,
.pmf-badge,
.pmf-legalbar {
	--pmf-bg: var(--sac-bg);
	--pmf-fg: var(--sac-fg);
	--pmf-accent: var(--sac-success);
	--pmf-radius: var(--sac-radius);
	--pmf-font-sans: var(--sac-display);
	--pmf-font-mono: var(--sac-mono);

	/* Opaque mode: --sac-border is a colour, not an alpha, so all steps are set. */
	--pmf-line-soft: #16211c;
	--pmf-line: var(--sac-border);
	--pmf-line-strong: #2a3b33;
	--pmf-line-tick: #3d5449;

	/* Shadow marks interaction, never presence. */
	--pmf-hover-shadow: var(--sac-glow-green);
	--pmf-focus-shadow: var(--sac-glow-green);
}

Map by reference, never by value. Custom properties resolve at use time, so a reference makes the host's theme variants flow into the footer on their own. A literal freezes it in the default theme — and it looks perfectly fine until someone flips the switch.

// 01 — FOOTER

Footer — 5·3·4

The full statement. The game's identity across five columns, the two menus across three, the publisher across four; copyright and legal links beneath.

Anatomy
≥ 1001px5 · 3 · 4
game
discover · network
Parva Machina
copyright
legal
701 – 1000px6 · 3 · 3 + 12
game
discover
network
Parva Machina
copyright
legal
≤ 700px1
game
discover
network
Parva Machina
copyright
legal
// 02 — TOP BAND

Top band

A hairline strip above the game's own header. Deliberately quiet: it names the publisher and gets out of the way.

Theme
// 03 — BRAND BADGE

Brand badge

One line, for under a hero or beside a title. At this size the mark carries it, not the wordmark.

Theme
// 04 — LEGAL STRIP

Legal strip

The copyright line on its own, for pages the full footer would overwhelm. Identical treatment to the bar inside it.

Theme
// 05 — INSTALLING

Wiring it into a game

Copy the files, map the tokens, build the data from your own i18n and path builders. The standard never constructs a URL.

Data schemapmf-types.ts
/*
 * pmf — the data contract, v2.
 *
 * v2 (2026-10-08): the publisher column and the copyright holder are fixed by
 * the standard and read by the renderer itself — `studio` carries only the
 * Parva Machina locale, `copyright` only the year. v1's `studio.blurb`,
 * `studio.aboutLabel` and string `copyright` are gone.
 *
 * ONE object. The hardest constraint on it, and the one everything else falls
 * out of: `PmFooterData` is 100% JSON-serializable. Anything that is not — an
 * SVG node — travels through a named slot instead, never through the object.
 *
 * That constraint is what lets the same shape be produced by a react-i18next
 * call, by MDX frontmatter, or by a build script reading remote JSON. It is
 * also what lets the reference DOM be generated from a fixture file, which is
 * how three renderers are held to one output.
 *
 * ── Resolved strings only ───────────────────────────────────────────────────
 * Every human-readable field is a resolved string. No translation keys, no
 * `t()` inside the component, no i18n dependency of any kind.
 *
 * This is the only shape that works across the network: SilkWard and SysAdmin
 * Crisis resolve at runtime against a loaded react-i18next namespace, while
 * Regnarium resolves at build time against a content tree. A key-based schema
 * would force one of them to invent a dictionary it does not have.
 *
 * ── The standard never builds a URL ─────────────────────────────────────────
 * Every `href` arrives ready. This is `path.*` from src/i18n/index.ts applied
 * across repositories: each site's own path builder stays the single source of
 * truth and the footer is purely a consumer.
 *
 * It is also what makes the route divergence a non-problem. The footer never
 * learns that SilkWard and Regnarium use `[locale]` while SysAdmin Crisis uses
 * `[lang]`, or that two sites put legal pages at the root while the third
 * nests them under `legal/`.
 */

export interface PmFooterData {
	version: 2;
	/** BCP-47. Used for `hreflang` on cross-origin links and for `dir` — never for copy. */
	locale: string;
	dir?: 'ltr' | 'rtl';

	game: GameIdentity;
	socials: SocialLink[];
	stores: StoreLinks;
	discover: MenuColumn;
	network: NetworkColumn;
	studio: StudioBlock;
	legal: FooterLink[];
	/**
	 * The copyright year. Build it with `pmCopyright(BUILD_YEAR)` from `pmf-data.ts`:
	 * the holder is always Parva Machina (ADR-005) and the renderer writes it.
	 */
	copyright: Copyright;
	a11y: A11yStrings;
}

export interface GameIdentity {
	/**
	 * Must match an id in the network registry.
	 *
	 * This is checked as a BUILD ERROR, not a warning — see `checkNetwork()` in
	 * README.md. A misspelt id makes the self-exclusion filter match nothing, so
	 * the game links to itself in its own footer, with a link that works
	 * perfectly and therefore never gets reported.
	 */
	id: string;
	name: string;
	/** The game's own home — where the wordmark links. */
	href: string;
	/** Two or three sentences. Resolved. */
	description: string;
	wordmark: Wordmark;
}

/**
 * Three variants, and the choice between them is not stylistic.
 *
 * - `text`   — SilkWard and SysAdmin Crisis have no logo asset. This IS the logo.
 * - `img`    — a same-origin path, legal under `img-src 'self'`. The right form
 *              for Regnarium's `brand/lockup-yatay.svg`: cacheable, no markup
 *              weight. `width`/`height` are mandatory, against layout shift.
 * - `slot`   — metadata only; the `<svg>` arrives through the `game-logo` slot.
 *              Use this and only this when the mark must inherit `currentColor`.
 *              The data object stays serializable because the node never enters it.
 */
export type Wordmark =
	| { kind: 'text' }
	| { kind: 'img'; src: string; width: number; height: number; alt: string }
	| { kind: 'slot'; width: number; height: number; title: string };

/**
 * Icons are hand-drawn on the 26px grid at 1.2px stroke — the same convention as
 * `src/components/schematic/PrincipleIcon.astro`. They ship as an inline sprite
 * emitted by the component itself.
 *
 * Not an icon font, not a CDN package, and deliberately NOT an external
 * `<use href="/sprite.svg#id">`: external `use` is subject to fetch rules, and
 * every site in this network runs `default-src 'self'`.
 */
export type SocialPlatform =
	| 'x'
	| 'bluesky'
	| 'mastodon'
	| 'discord'
	| 'github'
	| 'youtube'
	| 'itch'
	| 'reddit'
	| 'steam'
	| 'linkedin'
	| 'email'
	| 'rss';

export interface SocialLink {
	platform: SocialPlatform;
	href: string;
	/** Resolved, becomes the accessible name — "SilkWard on Bluesky", not "Bluesky". */
	label: string;
	/** Defaults to `me noopener`. */
	rel?: string;
}

/**
 * Either store may be absent and the row collapses; both absent and the block is
 * omitted with no reserved gap.
 *
 * Badges resolve to `${basePath}appstore-${badgeLocale ?? locale}.svg` and
 * `${basePath}googleplay-${badgeLocale ?? locale}.svg`. Both vendors publish
 * localised artwork, and under `img-src 'self'` every locale you use must be
 * committed locally.
 *
 * `badgeLocale` is separate from `locale` because Apple and Google use different
 * locale code sets, and neither matches your route params.
 */
export interface StoreLinks {
	ios?: { href: string; badgeLocale?: string };
	android?: { href: string; badgeLocale?: string };
	/** Defaults to `/standard/`. */
	basePath?: string;
}

export interface MenuColumn {
	/** Column heading. Resolved. */
	label: string;
	links: FooterLink[];
}

export interface FooterLink {
	href: string;
	label: string;
	external?: boolean;
	rel?: string;
	/** Renders `aria-current="page"`. */
	current?: boolean;
}

export interface NetworkColumn {
	label: string;
	items: NetworkItem[];
	/** Resolved marker for an unreleased sibling. Required if any item is `soon`. */
	soonLabel?: string;
}

export interface NetworkItem {
	id: string;
	name: string;
	href: string;
	/**
	 * The sibling game's own signature colour, rendered as a 6px dot — the one
	 * place a foreign hue is allowed inside the footer.
	 */
	signature?: string;
	/** `soon` renders a muted mono marker instead of a live link treatment. */
	status?: 'live' | 'soon';
}

/**
 * The publisher column. Its copy is FIXED — the approved "About Parva Machina"
 * block of the Parva Machina standard (`/en/standards/`), word for word — so
 * there is no field for it: the renderer reads `PM_STUDIO.aboutCopy` in the
 * resolved Parva Machina locale, and the copy and the links can never disagree.
 * Build this with `pmStudio(locale)` from `pmf-data.ts`.
 */
export interface StudioBlock {
	/**
	 * Which Parva Machina locale to link to.
	 *
	 * Parva Machina ships `tr` and `en`; a game may carry locales it does not.
	 * The renderer passes it (or, when absent, the footer's `locale`) through
	 * `pmLocaleFor()`, which maps by primary subtag and falls back to `en` —
	 * never link a visitor to a 404 to preserve a locale prefix.
	 */
	pmLocale?: string;
}

/**
 * One locale of the "About Parva Machina" block.
 *
 * The link line reads `linkLabel →`. The arrow is not part of the string: the
 * footer draws it as `PMF_ARROW`, and a Markdown About page writes U+2192.
 */
export interface PmAboutCopy {
	title: string;
	body: string;
	linkLabel: string;
}

/**
 * The host's part of `Copyright © {year} Parva Machina`. The holder and its link
 * (`PM_STUDIO.name`, `PM_STUDIO.origin`) are fixed and written by the renderer.
 */
export interface Copyright {
	/** Computed at build, never hard-coded. */
	year: number;
}

export interface A11yStrings {
	footerLabel: string;
	discoverLabel: string;
	networkLabel: string;
	legalLabel: string;
	socialsLabel: string;
	pmSocialsLabel: string;
}
SourceSiteFooter.tsx
import { PmFooter } from '@/standard/PmFooter';
import { assertKnownGame, pmCopyright, pmStudio, selectNetwork } from '@/standard/pmf-data';
import registry from '@/standard/network.json';

assertKnownGame(registry, 'sysadmin');

export function SiteFooter({ lang }: { lang: string }) {
	const { t } = useTranslation();

	return (
		<PmFooter
			data={{
				version: 2,
				locale: lang,
				game: {
					id: 'sysadmin',
					name: 'SysAdmin Crisis',
					href: localePath(lang, ''),
					description: t('site.footer.pitch'),
					wordmark: { kind: 'text' },
				},
				socials: [{ platform: 'github', href: '…', label: t('site.social.github') }],
				stores: {},
				discover: {
					label: t('site.footer.discover'),
					links: [
						{ href: localePath(lang, 'about'), label: t('site.nav.about') },
						{ href: localePath(lang, 'contact'), label: t('site.nav.contact') },
					],
				},
				network: {
					label: t('site.footer.network'),
					soonLabel: t('site.footer.soon'),
					items: selectNetwork(registry, 'sysadmin'),
				},
				studio: pmStudio(lang),
				legal: LEGAL_LINKS.map((l) => ({
					href: localePath(lang, l.path),
					label: t(l.key),
				})),
				copyright: pmCopyright(BUILD_YEAR),
				a11y: { /* … */ },
			}}
		/>
	);
}
pmf.csssrc/standard/pmf.css
/*
 * pmf — Parva Machina brand footer standard
 * v1.0.0
 *
 * ONE file. Plain CSS, zero dependencies, zero build step. Copy it verbatim
 * into a game site and map the token contract; nothing else is required.
 *
 * This file is the standard. The `/components` page on parvamachina.com does
 * not re-express it in Tailwind — it imports THIS file and prints THIS file's
 * source. If the preview looks wrong, the standard is wrong. Drift is
 * structurally impossible, which is the only reason a standard survives four
 * repositories and one maintainer.
 *
 * ── The contract ────────────────────────────────────────────────────────────
 * Three variables are required. Everything else derives from them:
 *
 *     .pmf { --pmf-bg: …; --pmf-fg: …; --pmf-accent: …; }
 *
 * Map them BY REFERENCE, never by value:
 *
 *     .pmf { --pmf-accent: var(--sac-success); }   ← correct
 *     .pmf { --pmf-accent: #5ef08a; }              ← wrong
 *
 * Custom properties resolve at use time, so a reference makes every one of the
 * host's theme variants (`data-theme`, `data-contrast`, `data-font`) flow into
 * the footer for free. A literal freezes the footer in the default theme — and
 * it looks perfectly fine until someone flips the switch, which is why this is
 * the first thing written down.
 *
 * ── Namespace ───────────────────────────────────────────────────────────────
 * `pmf-`, not `pm-`: parvamachina.com already owns `.pm-nisan`, `.pm-cetvel`
 * and `.pm-izgara`, and SysAdmin Crisis owns `sac-`. Every selector here is a
 * single class, unlayered, and nothing outside `.pmf` is ever targeted.
 */

:where(.pmf, .pmf-topbar, .pmf-badge, .pmf-legalbar) {
	/* ── Tier 1: required ───────────────────────────────────────────────── */
	--pmf-bg: #131211;
	--pmf-fg: #e8e3d8;
	--pmf-accent: #c8892f;

	/* ── Tier 2: derived, all overridable ───────────────────────────────── */
	--pmf-fg-muted: color-mix(in oklab, var(--pmf-fg) 72%, var(--pmf-bg));
	--pmf-fg-subtle: color-mix(in oklab, var(--pmf-fg) 56%, var(--pmf-bg));
	--pmf-surface: color-mix(in oklab, var(--pmf-fg) 4%, var(--pmf-bg));
	--pmf-accent-contrast: var(--pmf-bg);

	/*
	 * The hairline ramp. parvamachina.com carries eight steps because that page
	 * has eight kinds of surface; a footer has three — inner dividers, block
	 * separators, cell edges.
	 *
	 * ALPHA MODE (default): the ramp is one ink plus three scalars. With the
	 * default ink and alphas, `--pmf-line` resolves to exactly
	 * rgb(232 227 216 / 0.13) — the STANDARD hairline in global.css, byte for
	 * byte. The default state of this file IS the Parva Machina design.
	 *
	 * OPAQUE MODE (opt-in): a host whose border token is an opaque colour —
	 * SysAdmin Crisis's `--sac-border: #1e2b25` — must set all THREE steps
	 * explicitly. Do NOT derive them by mixing toward the ground: mixing a
	 * translucent colour with an opaque one RAISES alpha, so `line-soft` comes
	 * out heavier than `line`. That reads as "almost right", which is worse
	 * than broken. Setting fewer than three steps is a spec violation.
	 */
	--pmf-line-ink: var(--pmf-fg);
	--pmf-line-a-soft: 0.09;
	--pmf-line-a: 0.13;
	--pmf-line-a-strong: 0.18;

	--pmf-line-soft: color-mix(in srgb, var(--pmf-line-ink) calc(var(--pmf-line-a-soft) * 100%), transparent);
	--pmf-line: color-mix(in srgb, var(--pmf-line-ink) calc(var(--pmf-line-a) * 100%), transparent);
	--pmf-line-strong: color-mix(in srgb, var(--pmf-line-ink) calc(var(--pmf-line-a-strong) * 100%), transparent);
	/* The registration-mark / hover-edge step. Opaque mode must set this too. */
	--pmf-line-tick: color-mix(in srgb, var(--pmf-line-ink) 32%, transparent);

	/*
	 * Brass. LOCKED — not part of the host mapping, not overridable.
	 *
	 * It governs exactly four things, all inside the Parva Machina column: the
	 * mark's frame and centre dot, the mono eyebrow, the about-link underline,
	 * and the network heading tick. A publisher's mark that recolours per host
	 * is not a mark, it is decoration.
	 *
	 * It is also QUARANTINED to that column, so it reads as a colophon stamped
	 * onto the page rather than as a second accent leaking through the footer.
	 * Contrast is fine where it matters: #c8892f on #07090b is 6.72:1.
	 */
	--pmf-brass: #c8892f;

	/* ── Shape ──────────────────────────────────────────────────────────── */
	/*
	 * Radius is INHERITED — a host with rounded corners keeps them, on the four
	 * elements listed at the bottom of this file. It never reaches hairline
	 * rules, grid gaps, or store badges (see `.pmf__store img`).
	 */
	--pmf-radius: 0;

	/*
	 * Shadow is REFUSED at rest, on every host. Depth here is line weight and
	 * ground tone. A glowing footer competes with the game above it, and every
	 * glow a host owns is tuned for a different surface. The two hooks below
	 * are interaction-only: shadow marks interaction, never presence.
	 */
	--pmf-hover-shadow: none;
	--pmf-focus-shadow: none;

	/* ── Type island ────────────────────────────────────────────────────── */
	/*
	 * `.pmf` sets its own font sizes. It cannot use the `.t-*` ramp, which
	 * exists only on parvamachina.com. Every step routes through `--pmf-scale`
	 * so a host's font-size preference reaches all six at once.
	 */
	--pmf-font-sans: inherit;
	--pmf-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
	--pmf-scale: 1;

	--pmf-t-wordmark: calc(14px * var(--pmf-scale));
	--pmf-t-body: calc(14.5px * var(--pmf-scale));
	--pmf-t-link: calc(14px * var(--pmf-scale));
	--pmf-t-label: calc(11px * var(--pmf-scale));

… + 887

PmFooter.tsxsrc/standard/PmFooter.tsx
/*
 * pmf — the React reference implementation.
 *
 * For the three Next.js game sites. Server-component safe: no hooks, no state,
 * no effects, no `'use client'`. It renders in the RSC pass and ships zero
 * JavaScript, which matters on sites whose whole claim is that they make no
 * requests after the first load.
 *
 * ── Installing ──────────────────────────────────────────────────────────────
 *   1. Copy this file, `pmf.css`, `pmf-types.ts`, `pmf-data.ts`, `pmf-icons.ts`
 *      into the game's own tree.
 *   2. `import './pmf.css'` once, in the root layout.
 *   3. Write the token mapping — see README.md §2.
 *   4. Build `PmFooterData` from your own i18n and path builders; take
 *      `studio` from `pmStudio(locale)` and `copyright` from
 *      `pmCopyright(BUILD_YEAR)` — both are fixed by the standard.
 *
 * ── The one rule that will bite ─────────────────────────────────────────────
 * This file is a MIRROR of `pmf-render.ts`, which is normative. If you change
 * the markup here, change it there, and run `npm run standard:check` on
 * parvamachina.com — it diffs the two and fails on any difference. Fixing a bug
 * in one renderer and not the other is how a standard quietly stops being one.
 */
import type { JSX } from 'react';
import type { Copyright, FooterLink, PmFooterData, SocialLink } from './pmf-types';
import { PMF_ARROW, PMF_MARK, PM_STUDIO, pmAboutCopy, pmLocaleFor } from './pmf-data';
import { PMF_ICONS } from './pmf-icons';

export interface PmFooterProps {
	data: PmFooterData;
	/**
	 * The inline `<svg>` for a `wordmark.kind === 'slot'` logo — the only case
	 * where the mark must inherit `currentColor`. Everything else goes through
	 * the data object.
	 */
	gameLogo?: JSX.Element;
}

function Sprite({ data }: { data: PmFooterData }) {
	const ids = [
		...new Set([
			...data.socials.map((social) => social.platform),
			...PM_STUDIO.socials.map((social) => social.platform),
		]),
	];
	return (
		<svg className="pmf__sprite" aria-hidden="true">
			{ids.map((id) => (
				<symbol
					key={id}
					id={`pmf-i-${id}`}
					viewBox="0 0 26 26"
					fill="none"
					stroke="currentColor"
					strokeWidth={1.2}
					dangerouslySetInnerHTML={{ __html: PMF_ICONS[id] }}
				/>
			))}
		</svg>
	);
}

function Socials({ socials, label }: { socials: readonly SocialLink[]; label: string }) {
	if (socials.length === 0) return null;
	return (
		<ul className="pmf__socials" role="list" aria-label={label}>
			{socials.map((social) => (
				<li key={social.href}>
					<a
						className="pmf__social"
						href={social.href}
						rel={social.rel ?? 'me noopener'}
						aria-label={social.label}
					>
						<svg aria-hidden="true">
							<use href={`#pmf-i-${social.platform}`} />
						</svg>
					</a>
				</li>
			))}
		</ul>
	);
}

function LinkItems({ links }: { links: FooterLink[] }) {
	return (
		<>
			{links.map((link) => (
				<li key={link.href}>
					<a
						href={link.href}
						rel={link.rel ?? (link.external ? 'noopener' : undefined)}
						aria-current={link.current ? 'page' : undefined}
					>
						{link.label}
					</a>
				</li>
			))}
		</>
	);
}

function Wordmark({ data, gameLogo }: { data: PmFooterData; gameLogo?: JSX.Element }) {
	const mark = data.game.wordmark;
	if (mark.kind === 'img') {
		return (
			<img
				className="pmf__logo"
				src={mark.src}
				alt={mark.alt}
				width={mark.width}
				height={mark.height}
			/>
		);
	}
	if (mark.kind === 'slot' && gameLogo) return gameLogo;
	return <span className="pmf__wordmark">{data.game.name}</span>;
}

function Stores({ data }: { data: PmFooterData }) {

… + 186

READMEsrc/standard/README.md
# pmf — Parva Machina brand components

Four components that make the publisher visible on a game's own site, in a form
that survives four repositories, three stacks and three unrelated palettes.

| Class | What | Where |
| --- | --- | --- |
| `.pmf` | the 5·3·4 footer and its copyright bar | game landing page, bottom |
| `.pmf-topbar` | a hairline publisher strip | above the game's own header |
| `.pmf-badge` | one-line signature | under a hero, beside a title |
| `.pmf-legalbar` | the copyright line alone | where the full footer is too heavy |

This is the **Parva Machina layer** of the Centvix standard (ADR-005): a game
site follows the shared Centvix rules, and this kit is the thin studio layer on
top. The 5·3·4 footer is the one recorded exception to Centvix's 6·2·4 split
(ADR-006). The rules, live previews and full sources are on the standards page,
`https://parvamachina.com/en/standards/` (`/tr/standartlar/`).

## 1. Copy

```
pmf.css          the standard — every visual decision is here
pmf-types.ts     the data contract
pmf-data.ts      constants and the three helpers
pmf-icons.ts     the hand-drawn social set
pmf-render.ts    the normative renderer (framework-free)
PmFooter.tsx     the React mirror, for the Next.js sites
```

Copy all six. Then `import './pmf.css'` once, in the root layout.

## 2. Map the tokens

Three variables are required; everything else derives. Write the mapping once:

```css
.pmf,
.pmf-topbar,
.pmf-badge,
.pmf-legalbar {
	--pmf-bg: var(--your-ground);
	--pmf-fg: var(--your-ink);
	--pmf-accent: var(--your-signature);
}
```

**By reference, never by value.** Custom properties resolve at use time, so a
reference makes your theme variants — `data-theme`, `data-contrast`,
`data-font` — reach the footer with no extra rules. A literal freezes it in the
default theme, and it looks perfectly fine until someone flips the switch.

If your border token is an **opaque colour** rather than an alpha, set all four
hairline steps explicitly: `--pmf-line-soft`, `--pmf-line`, `--pmf-line-strong`,
`--pmf-line-tick`. Do not derive them by mixing toward the ground — mixing a
translucent colour with an opaque one raises alpha, so `line-soft` comes out
heavier than `line`.

## 3. Build the data

`PmFooterData` takes **resolved strings**, never translation keys, and every
`href` arrives ready. The standard never constructs a URL: your own path builder
stays the single source of truth, which is why route shapes can differ across
the network without the footer ever learning about it.

Two parts of the footer are **not** yours to write, and the contract (v2) has
no field for them: the renderer reads the "About Parva Machina" copy and the
copyright holder from `PM_STUDIO` itself. The host only says which locale and
which year:

```ts
import { pmCopyright, pmStudio } from './pmf-data';

version: 2,
studio: pmStudio(locale),           // { pmLocale } — the copy follows it
copyright: pmCopyright(BUILD_YEAR), // { year } — Copyright © YEAR <a>Parva Machina</a>
```

The third column is fixed copy: the heading "About Parva Machina" (TR "Parva
Machina hakkında"), one paragraph, and a `parvamachina.com →` link. It is the
same text that closes each game's About page, and it is not reworded per site.
Compute `BUILD_YEAR` at build time (`new Date().getFullYear()` in a build-time
module), never in the browser.

The legal links go in this order: **Terms, Privacy, Cookies**.

## 4. Wire the network

```bash
node scripts/sync-network.mjs   # prebuild
```

… + 38