Parva Machina standartları
Her oyunun kendi sitesi var, kendi kodu ve kendi paleti. Ortak olan tek şey yapımcı ve bu sayfa, bu ortaklığın kurallarını tutuyor.
Bu sayfadaki önizlemeler, kopyalanacak dosyanın ta kendisini kullanıyor. Önizleme bozuksa standart bozuktur.
Bu standart, Centvix standartlarının üzerine gelen bir katmandır; burada yazmayan her konuda Centvix standardı geçerlidir.
Kapsam
Bu standart, Parva Machina'nın her alt projesinin sitesine ve reposuna uygulanır: oyun siteleri ve kodları. parvamachina.com'a uygulanmaz; site Centvix'in doğrudan ürünüdür ve Centvix standardına olduğu gibi uyar.
Kural seviyeleri Centvix standardındakiyle aynıdır. must katı bir zorunluluktur; onu çiğneyen site yayına çıkmaz. should güçlü bir öneridir; bir site ayrılırsa gerekçe repo README'sine yazılır.
Ezmeler
Dört kural Centvix standardından ayrılır. Geri kalan her şey, kurucu metni, beş zorunlu sayfa ve yasal bağlantı sırası dahil, Centvix'in tanımladığı gibi kalır.
| # | Centvix kuralı | Parva Machina kuralı |
|---|---|---|
| 1 | About sayfası "Centvix hakkında" ile, ardından "Kurucu hakkında" ile biter | About sayfası "Parva Machina hakkında" (bölüm 1) ile, ardından "Kurucu hakkında" ile biter; kurucu metni değişmez |
| 2 | Footer'ın 3. sütununda "Centvix hakkında" metni ve centvix.com linki | Footer'ın 3. sütununda "Parva Machina hakkında" metni ve parvamachina.com linki |
| 3 | Copyright "Copyright © YIL Centvix"; Centvix, https://centvix.com'a linkli | Copyright "Copyright © YIL Parva Machina"; Parva Machina, https://parvamachina.com'a linkli; yıl hesaplanır, sabit yazılmaz |
| 4 | Footer oranı 6-2-4 | Oyun sitelerinde footer oranı 5·3·4 (kayıtlı istisna) |
Ek kurallar
- 01must
Altı bileşen dosyasının hepsini kopyala (
pmf.css,pmf-types.ts,pmf-data.ts,pmf-icons.ts,pmf-render.tsve React siteleri içinPmFooter.tsx) vepmf.css'i kök layout'ta bir kez içe aktar. - 02must
Üç zorunlu token'ı (
--pmf-bg,--pmf-fg,--pmf-accent) değere değil referansa bağla; böylece sitenin tema varyantları footer'a ulaşır. - 03should
Sitenin kenarlık token'ı alfa değil opak bir renkse, dört
hairline(ince çizgi) tonunu da açıkça yaz:--pmf-line-soft,--pmf-line,--pmf-line-strong,--pmf-line-tick. - 04must
Footer'a çözülmüş metinleri ve hazır
href'leri ver. Standart hiçbir zaman URL kurmaz; sitenin kendi yol kurucusu tek doğruluk kaynağı kalır. - 05must
Build'den önce
node scripts/sync-network.mjsçalıştır. Betikhttps://parvamachina.com/network.jsondosyasını git'e girmeyen bir kopyaya çeker; commit'lenmişnetwork.fallback.jsonçevrimdışı yoldur. Başarısız çekim uyarıdır, başarısız sürüm değil. - 06must
assertKnownGame(registry, ownId)çağır. Yanlış yazılmış bir id, oyunun kendisini listeden çıkarmasını engeller; oyun kendi footer'ında kendine link verir. - 07must
Site mağaza rozeti gösteriyorsa Apple'ın ve Google'ın resmî görsellerini dil başına sitenin kendi
public/dizinineappstore-<locale>.svgvegoogleplay-<locale>.svgadıyla commit'le ve dokunmadan göster: köşe yarıçapı, filtre, saydamlık ve yeniden renklendirme yok. - 08must
--pmf-brass'a dokunma. Parva Machina plakası her host'ta markanın kendi koyu zeminini, sıfır yarıçapla taşır. - 09should
Token'ların ifade edemediği her şey yerel yama olarak değil,
pmf.css'e, yani üst akışa girer.
Yayın öncesi kontrol listesi
Bir oyun sitesi yayına çıkmadan önce, Centvix kontrol listesindeki altı maddeye (yukarıdaki ezmelerle birlikte okunur) ek olarak aşağıdaki maddelerin hepsi işaretlenir.
About sayfası "Parva Machina hakkında" ve "Kurucu hakkında" bölümleriyle bitiyor; iki link de çalışıyor.
Footer 5·3·4 düzenini kullanıyor; üçüncü sütunda sabit Parva Machina metni var.
Copyright "Copyright © YIL Parva Machina" yazıyor; Parva Machina, https://parvamachina.com'a linkli.
Altı pmf bileşen dosyasının hepsi kopyalanmış (React sitelerinde
PmFooter.tsx),pmf.cssbir kez içe aktarılmış, üç token referansla eşlenmiş.sync-network.mjsbuild'den önce çalışıyor,network.fallback.jsoncommit'li,assertKnownGamesitenin kendi id'siyle çağrılıyor.Mağaza rozetleri, varsa, dil başına resmî görsel ve dokunulmamış.
Bileşen referansı
Sayfanın geri kalanı bileşen referansıdır: token sözleşmesi ve Parva Machina'yı bir oyunun kendi sitesinde görünür kılan dört bileşen. Her birinin önizlemesi, kopyalanacak dosyanın ta kendisiyle çalışır.
Token sözleşmesi
Bileşen host'un token'larını bilmez. Üç değişken zorunlu; kalan her biri onlardan türetilir. Host eşlemeyi bir kez yazar, dört bileşeni birden alır.
--pmf-bgthe footer’s ground--pmf-fgprimary ink; the hairlines derive from it--pmf-accentthe game’s signature — not brass
--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
--pmf-brass#c8892f.pmf__pm-plate#131211 / #e8e3d8 / radius 0
/* 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);
}Değere değil, referansa bağla. Özel değişkenler kullanım anında çözülür — referans yazınca host'un tema varyantları footer'a kendiliğinden akar. Sabit yazınca footer varsayılan temada donar ve biri anahtarı çevirene kadar kusursuz görünür.
Alt bilgi — 5·3·4
Tam beyan. Oyunun kimliği beş şeritte, iki menü üçte, yapımcı dörtte; altında telif ve yasal bağlantılar.
Üst bant
Oyunun kendi başlığının üstünde saç teli bir şerit. Bilinçli olarak sessiz: yapımcıyı söyler ve çekilir.
Marka rozeti
Tek satır. Hero altına ya da başlığın yanına. Bu ölçekte imi taşıyan şey kelime imi değil, nişanın kendisi.
Yasal şerit
Tam alt bilginin ağır kaldığı sayfalar için telif satırı tek başına. Alt bilgideki bandın birebir aynısı.
Bir oyuna takmak
Dosyaları kopyala, tokenları eşle, veriyi kendi i18n ve rota kurucularından üret. Standart hiçbir zaman URL kurmaz.
/*
* 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;
}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 — 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
/*
* 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
# 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