# SurveyStream — Brand System

**[`/docs/brand-guidelines`](BRAND-GUIDELINES.md) is the master specification** — strategy,
logo, colour, typography, assets and print in one document, generated from the token files so it
cannot drift from the code. It renders as a page at `/docs/brand-guidelines`; the file itself is
`docs/BRAND-GUIDELINES.md`. Start there for the *what*; the pages are the *why*.

**`/portal` is the interactive quick reference** — click-to-copy swatches with live contrast
badges, a typography playground, and copy-the-raw-SVG asset cards. One self-contained document with
the three webfonts embedded, generated from `tokens/design-tokens.json` and the real SVGs.

An Astro site. `npm install && npm run build` emits `./dist` — copy that to any web server.

```bash
npm install
npm run dev      # http://localhost:4330
npm run build    # -> dist/
npm run preview  # serve the build locally
npm run portal   # regenerate the portal document on its own
npm run hub      # regenerate the hub
npm run links    # crawl the running dev server for broken links
```

Serving from a sub-path? Set `base: '/brand'` in `astro.config.mjs` and every asset URL follows.
For Netlify / Vercel / Cloudflare Pages: build command `npm run build`, output directory `dist`.

**The dev port is pinned to 4330 with `strictPort: true`**, not Astro's default 4321. Another
project on this machine sits on 4321, and Astro silently increments to the next free port when its
default is taken — so this site would come up on 4322 while every note said 4321, and you would be
reading a different project's 404s as bugs in this one. Strict means it now refuses to start rather
than moving, which tells you the truth immediately. `npx astro dev stop` frees a stuck instance.

## Routes

| Route | Page |
| --- | --- |
| `/` | Hub |
| `/brand-reference` | Strategy document |
| `/logo` · `/logo-svg` | Lockup + the 600×140 SVG |
| `/colour` · `/typography` | The two systems |
| `/pattern` | Seamless background tile |
| `/og-card` · `/og-card-canvas` | Social card + the raw canvas |
| `/print` | Stationery sheets |
| `/flow-and-lattice` | Exploration |
| `/tokens` | Engineering handoff — every machine-readable file |
| `/docs` · `/docs/*` | The 11 Markdown specifications, rendered |
| `/portal` | Interactive style guide |
| `/files/*` | Raw file access, one endpoint per deliverable |

## Everything is a route

This is the rule the site is built on, and it exists because breaking it is
invisible until someone clicks.

**Astro’s dev server answers a navigation request from its route table.** When
nothing matches it returns its own 404 — it does *not* fall through to
`public/`. A file sitting in `public/` therefore returns 200 to `curl`, to
`fetch()`, and to an `<img>`, and 404s for every human who clicks a link to it.
Worse, it only misbehaves in dev, so a production smoke test says everything is
fine.

So:

- **Markdown** renders at `/docs/*` via a content collection whose glob `base`
  points outside `src/`. The files stay at their canonical repo paths —
  `tokens/README.md` belongs beside the tokens — and are rendered, not copied.
- **Raw files** are served by `src/pages/files/[...path].ts`, a real route, at
  `/files/<repo path>`. `curl {host}/files/tokens/design-tokens.json` returns
  the same bytes the repo holds.
- **`public/` holds subresources only** — stylesheets, SVGs, images, fonts. Those
  *are* fetched as subresources, so falling through is correct for them.
- Never put a clickable file in `public/`. Beyond the 404, a `public/` file with
  the same name as a route **shadows the route entirely** — Astro skips it with a
  build warning that is easy to miss.

`npm run links` crawls the running dev server and checks every link **with
browser navigation headers**. A checker that uses default `curl` headers reports
a clean bill of health on a site whose links are all broken; that is exactly what
happened here.

**Every file the system ships is reachable from the header nav.** A page gets a
route and a `NAV` entry in `src/components/SiteHeader.astro`; a machine-readable
file gets a row in `FILE_GROUPS` in `src/lib/files.ts`, which `/tokens` renders
and `/files` serves from the same array. Both stat the real file, so a path that
stops existing fails the build instead of shipping a dead link.

## Header

Every page carries a sticky site header: the mark and wordmark link home, then nav to each
deliverable with the current page marked via `aria-current="page"`, and the theme toggle on the
right. It is defined once in `src/components/SiteHeader.astro` and mounted by the layout, so adding
a route means adding one line to the `NAV` array there.

Details worth knowing if you edit it:

- Header tokens are namespaced `--hd-*` and toggle tokens `--tg-*`. Every page brings its own
  palette, so the chrome must not inherit from or collide with it.
- `html { scroll-padding-top }` clears the header for in-page anchors.
- The hub has its own sticky section nav. A style block placed **after** the `<slot />` offsets it
  to `top: var(--hd-h)` — page styles are injected inside `<body>`, so anything that needs to beat
  them has to come later in document order.
- `@media print` hides the header, which matters for `/print`.

## Theme

Every page carries a **Light / Dark / Auto** toggle, in the header. It writes `ss-theme` to
`localStorage` and stamps `data-theme` on `<html>`; "Auto" clears the stamp so the page follows
`prefers-color-scheme` again. A tiny inline script in `<head>` applies the stored choice before
first paint, so a dark-mode visitor never gets a white flash.

Two pages are deliberately single-theme and hide the toggle, because they are artwork rather than
documents: **`/print`** (paper is paper) and **`/og-card-canvas`** (a fixed 1200×630 image). They
set `fixedTheme` on the layout.

The portal is a route (`/portal`) but bypasses `Base.astro` — it is a complete document with its
own header, toggle and embedded fonts, so the layout would give it two of each. Its toggle writes a
separate `ss-portal-theme` key, so it does not fight the site's `ss-theme`.
The dark-mode specimens inside `/pattern` and `/flow-and-lattice` stay dark in both themes — the
page chrome flips around them. Showing a dark-mode asset on white would misrepresent it.

## Deliverables

| # | Deliverable | Route | Source |
| --- | --- | --- | --- |
| 01 | **Brand Reference** — strategy, audiences, archetypes, dials, logo territories | `/brand-reference` | `src/content/pages/surveystream-brand-guidelines.html` |
| 02 | **Logo Lockup** — geometric S mark + outlined Archivo Bold wordmark | `/logo` | `src/content/pages/logo/` · `logo/` |
| 03 | **Colour System** — tokens, live WCAG validation, print conversions | `/colour` | `src/content/pages/color-system/` · `color-system/` |
| 04 | **Typography** — font evidence, 1.200 scale, numeral rules | `/typography` | `src/content/pages/typography/` · `typography/` |
| 05 | **Background Pattern** — seamless 400×400 tile, seam-tested | `/pattern` | `src/content/pages/pattern/` · `pattern/` |
| 06 | **Open Graph Card** — 1200×630 | `/og-card` | `src/content/pages/og/` · `og/` |
| 07 | **Print Stationery** — card, letterhead, invoice, millimetre-exact | `/print` | `src/content/pages/print/` · `print/` |
| 08 | **Flow & Lattice** — first-pass mark studies | `/flow-and-lattice` | `src/content/pages/flow-and-lattice.html` |
| 09 | **Design Tokens** — DTCG, CSS, Tailwind v3 + v4 | `/tokens` | `tokens/` |
| 10 | **Brand Portal** — interactive style guide | `/portal` | `src/content/brand-portal.html` (generated) |

## Layout

```
src/
  pages/
    index.astro          the hub
    [page].astro         ONE route for all ten authored HTML pages
    tokens.astro         the machine-readable file index
    portal.astro         serves the generated portal document whole
    docs/index.astro     documentation index
    docs/[...slug].astro renders the 11 Markdown files
    files/[...path].ts   raw file access, as a route
  layouts/
    Base.astro           head, header, theme toggle, no-flash script
    Doc.astro            prose layout + on-this-page rail
  components/            SiteHeader.astro, ThemeToggle.astro
  lib/
    pages.ts             the ten HTML pages: route, source, title
    files.ts             the file manifest /tokens renders and /files serves
    docs.ts              titles and blurbs for the Markdown docs
  content/
    hub.html             GENERATED by scripts/build-hub.mjs
    brand-portal.html    GENERATED by scripts/build-portal.mjs
    pages/*.html         the ten authored page sources
  assets/fonts/          the webfonts (source; public/ gets a copy)
  content.config.ts      the docs collection, globbed from outside src/
  styles/theme.css       three-state theme scaffolding

scripts/
  build-hub.mjs          regenerates src/content/hub.html
  build-portal.mjs       regenerates src/content/brand-portal.html
  sync-public.mjs        assembles public/ (subresources only)
  check-links.mjs        crawls with browser navigation headers
  rehype-repo-links.mjs  rewrites repo-path links in Markdown to routes

BRAND-GUIDELINES.md     the master specification (generated), renders at /docs/brand-guidelines
tokens/                 design-tokens.json, theme.css, tailwind configs, README
logo/                   16 SVGs + interlock/ variant + metrics + README
color-system/           tokens.json, tokens.css, COLOR-SYSTEM.md
typography/             type-tokens.json, type.css, TYPOGRAPHY.md
pattern/                5 tiles, pattern.css, README
og/                     1× and 2× PNGs, README
print/                  15 stationery SVGs, PRINT-SPECS.md
public/                 DERIVED — gitignored, subresources only
dist/                   build output. This is what you deploy.
```

**Page content lives in `src/`; deliverables live at their repo paths.** The ten authored HTML
pages moved into `src/content/pages/` because nothing but the routes consumes them. The tokens,
SVGs and Markdown stayed put: `tokens/design-tokens.json` is the address the handoff quotes, and
burying it in `src/` would make it unservable. They are reached through `/files` and `/docs`
instead.

The HTML pages are injected rather than converted to `.astro` for a concrete reason: `.astro`
parses `{` as the start of a JS expression, and every one of these files carries large `<style>`
and `<script>` blocks full of braces.

## Canonical choices

- **Palette:** the teal-navy system — Deepwater `#08222E`, Stream `#10BBD1`, Signal `#5FE3EC`,
  Payout `#F2A23C`. The slate/mint variants (`logo/lockup-600x140*.svg`, `pattern/tile-slate-*`)
  are alternates, not canonical.
- **Primary lockup:** `logo/lockup-horizontal.svg`. The interlock variant in `logo/interlock/` is
  a live option, not a replacement.
- **Faces:** Archivo (display/wordmark), Inter (UI/body), JetBrains Mono (telemetry).

## Engineering handoff

```
brand-portal.html             interactive reference — copy hexes and asset SVGs straight from it
tokens/design-tokens.json     W3C DTCG — the consolidated export. Consume this.
tokens/theme.css              CSS custom properties, all three theme mechanisms
tokens/tailwind.config.js     Tailwind v3
tokens/tailwind-v4.css        Tailwind v4 @theme
color-system/tokens.json      Domain source — carries the contrast validation
color-system/tokens.css       Custom properties, all three theme states
typography/type-tokens.json   DTCG — off-scale steps flagged $offScale
typography/type.css           Scale, numerals, metric-matched fallbacks
pattern/pattern.css           Tile data URIs + ready classes
logo/metrics.json             Lockup measurements
og/README.md                  Export commands and meta tags
```

Both CSS token files handle the three theme states correctly (bare `:root`,
`prefers-color-scheme`, and an explicit `[data-theme]` stamp). Never declare a colour only inside a
media or `[data-theme]` block.

## Open decisions

1. **Pulse mint** `#2FE3A2` enters the palette, or the system runs on Stream alone.
2. **Primary or interlock mark.** The interlock is stronger at display size; it needs its
   small-size fallback rule (accent dropped below ~40 px) enforced wherever it is used.
3. **Seven neutrals, not six** — light borders cannot clear 3:1 on a tinted canvas with six steps.
4. **The taker side's action colour** — share Stream, or give it Pulse outright.

## Known gaps

- Pantone matches are computed against published screen approximations. Confirm on a physical
  Bridge guide under D50 before releasing artwork.
- The premium font recommendation (Klim Söhne suite, GT Walsheim alternate) is unverified — those
  binaries are paywalled.
- Archivo's GPOS fires exactly one kern pair on "SurveyStream"; `yS` and `St` want manual review.
- `backdrop-filter` in the OG card does not survive server-side rendering (Satori / `@vercel/og`).
  Ship the exported PNG.
- The print SVGs are RGB layout masters — SVG has no CMYK. Re-apply colour as CMYK or spot
  swatches and export PDF/X-4 before anything reaches a press. See `print/PRINT-SPECS.md` §1.
- The card front colour needs a decision: the specified `#0F172A` has no usable Pantone match
  (nearest 289 C, ΔE 5.8); brand Deepwater `#08222E` matches 5395 C at ΔE 1.7.
- No motion system, iconography, or component library yet.

## Licence

Archivo, Inter, JetBrains Mono and Gabarito are SIL OFL 1.1 — commercial use, web embedding, PDF
embedding and outlining are all permitted. The wordmark SVGs ship as outlines, so no font is
required at render time. Add the four `OFL.txt` files alongside `src/assets/fonts/` before sending
this bundle outside the company.
