# Theming

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.

```json
{
  "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 surface | Role | Background | Text on it |
| --- | --- | --- | --- |
| Page / app background | Background (container) | `--gs-backgroundContainerColour` | `--gs-onBackgroundContainerColour` |
| Cards, panels, inputs | Background (element) | `--gs-backgroundMainColour` | `--gs-onBackgroundColour` |
| Option / choice buttons | Tertiary | `--gs-tertiaryContainerColour` | `--gs-onTertiaryContainerColour` |
| Primary action, selected state | Highlight | `--gs-highlightMainColour` | `--gs-onHighlightColour` |
| Secondary action | Secondary | `--gs-secondaryContainerColour` | `--gs-onSecondaryContainerColour` |
| Header / footer chrome | Primary | `--gs-primaryMainColour` | `--gs-onPrimaryColour` |
| A correct or successful state | Positive | `--gs-positiveContainerColour` | `--gs-onPositiveContainerColour` |
| A wrong state or an error | Negative | `--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:

```css
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.

```css
:root {
  --bg: var(--gs-backgroundContainerColour);
  --text: var(--gs-onBackgroundContainerColour);
}
button.primary {
  background: var(--gs-highlightMainColour);
  color: var(--gs-onHighlightColour);
}
```

| Situation | What the fan sees |
| --- | --- |
| Studio answers | The producer's colours. |
| Returning fan, Studio slow or down | The colours their device remembered from the last visit. |
| First visit, Studio down | The library's "Can't load the game right now" screen with Try again. |
| `gamestage dev --serve` | The 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:

```html
<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 setting | What it changes | Left empty |
| --- | --- | --- |
| Title typeface | The Google Font the name is set in, e.g. "Bebas Neue" | The page's own font |
| Title weight | A number from 100 to 900 (400 regular, 700 bold) | The typeface's heaviest weight |
| Title letter spacing | A fraction of the type size, e.g. `0.08` to spread it out, `-0.02` to tighten it | The typeface's own spacing |
| Title case | Type "upper" to show the name in capitals whatever case it is typed in | As typed |
| Title colour | The colour of the name | The 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](/docs/layout-and-loading#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))`.
