Theming

Read the producer's Studio colour palette so your game takes their brand with no code change.

A producer sets your game's colours in Studio, and they reach the page as CSS custom properties named --gs-<role>. A game that reads them takes the producer's brand with no code change and no redeploy. The reference scaffolds are wired exactly this way, so read one alongside this chapter for a worked example; this is how to do the same in a game of your own.

Start the palette in Studio

Put the colours your game ships with in gamestage.settings.json, beside gamestage.yaml. A deploy writes each one into your game's Colour Palette in Studio where that field is empty, so a producer opens Studio to your real colours rather than a column of blank boxes, and changes them from there.

{
  "primary_colour": "#1D428A",
  "css_variables_backgroundMainColour": "#0B1620",
  "css_variables_onBackgroundColour": "#F4F2F2",
  "css_variables_highlightMainColour": "#F2C94C"
}

A deploy never overwrites a colour a producer has set. gamestage create writes this file for you from the scaffold's own CSS, and gamestage verify fails a game whose CSS reads a colour the file leaves out, naming it.

How the palette reaches the page

The producer's colour palette is a project setting. The client reads it and sets each colour on the document root as a --gs-* custom property, so your CSS can read var(--gs-backgroundMainColour) and the rest. Nothing per-colour is wired by hand: a colour a producer has not set is simply absent, and your CSS falls back to the value you shipped.

The roles, and where each one goes

The palette is organised as Material 3 roles. Each role is a set of paired colours: a surface and the on colour that sits legibly on it, plus a container variant for quieter surfaces. Map your game's parts to the roles like this.

Your game's surfaceRoleBackgroundText on it
Page / app backgroundBackground (container)--gs-backgroundContainerColour--gs-onBackgroundContainerColour
Cards, panels, inputsBackground (element)--gs-backgroundMainColour--gs-onBackgroundColour
Option / choice buttonsTertiary--gs-tertiaryContainerColour--gs-onTertiaryContainerColour
Primary action, selected stateHighlight--gs-highlightMainColour--gs-onHighlightColour
Secondary actionSecondary--gs-secondaryContainerColour--gs-onSecondaryContainerColour
Header / footer chromePrimary--gs-primaryMainColour--gs-onPrimaryColour
A correct or successful statePositive--gs-positiveContainerColour--gs-onPositiveContainerColour
A wrong state or an errorNegative--gs-negativeContainerColour--gs-onNegativeContainerColour

Each role also carries a MainColour for a solid fill or a border, and a VariantColour for a border or a subtle overlay, e.g. --gs-positiveMainColour for the border of a correct answer whose fill is --gs-positiveContainerColour.

The two rules

Pair a surface with its own on colour

The role colours are designed in pairs. A surface from one role must take its text from the same role, or a producer whose palette is legible everywhere can still end up with white text on a pale tile. Read a background and its text together:

button.option {
  background: var(--gs-tertiaryContainerColour);
  color: var(--gs-onTertiaryContainerColour);
}

Never mix a surface from one role with the on colour of another.

Keep your colours in Studio, not in the page

Read every variable with no fallback value. Put each colour in gamestage.settings.json instead; a deploy writes it into Studio, and that is the only place it lives.

:root {
  --bg: var(--gs-backgroundContainerColour);
  --text: var(--gs-onBackgroundContainerColour);
}
button.primary {
  background: var(--gs-highlightMainColour);
  color: var(--gs-onHighlightColour);
}
SituationWhat the fan sees
Studio answersThe producer's colours.
Returning fan, Studio slow or downThe colours their device remembered from the last visit.
First visit, Studio downThe library's "Can't load the game right now" screen with Try again.
gamestage dev --serveThe colours in gamestage.settings.json.

A fallback colour in the page is a second copy that drifts: a producer changes a colour in Studio and fans still see the old one first. gamestage verify fails a page that reads a colour with a literal fallback like this, and gamestage deploy refuses it before uploading.

The title's own typeface

A producer can set the game's name in its own typeface from Studio's Brand settings, rather than the page's default font. Mark the element that shows the name:

<h1 data-gamestage-title>Wage Bill</h1>

No element marked this way, and the client styles the page's first heading instead. Either way, nothing changes until a producer fills in a setting: an empty field leaves your page's own title face exactly as you built it, and gamestage verify does not ask you to fill any of them in.

Studio settingWhat it changesLeft empty
Title typefaceThe Google Font the name is set in, e.g. "Bebas Neue"The page's own font
Title weightA number from 100 to 900 (400 regular, 700 bold)The typeface's heaviest weight
Title letter spacingA fraction of the type size, e.g. 0.08 to spread it out, -0.02 to tighten itThe typeface's own spacing
Title caseType "upper" to show the name in capitals whatever case it is typed inAs typed
Title colourThe colour of the nameThe page's own text colour

A typeface not on Studio's list is ignored and your page's own font stands, so a typo can never break a live game. The client loads only the weight actually used, after the page has drawn, so the title's own font never delays first paint.

Logo text and its accent colour are separate settings, for a name styled as a logo rather than read as a word. "Logo text" replaces the game's name on the page's title and on its intro screen only: the browser tab, the share text and a screen reader still read the game's real name. Put the characters to pick out in square brackets, WAGE[$], and "Logo accent colour" colours them; leave the accent colour empty and it uses the title colour instead.

What not to theme

Leave a colour fixed when it means something the brand does not decide. A football pitch is green and a target marker is gold whatever the brand, so those stay as hardcoded accents rather than reading a role. Muted text and hairline borders have no role of their own; derive them from the ink colour so they track the palette, e.g. color-mix(in srgb, var(--gs-onBackgroundColour, #232020) 66%, var(--gs-backgroundContainerColour, #FCFBFB)).