---
name: triumph-design-system-v3
description: Screen system for Greenville Triumph member-facing portals, account pages, and public web. Extends v2, does not replace it.
---

# Triumph Design System v3 — screens

v3 is the **screen** layer. It loads v2's `tokens.css` and `pattern.css`
unchanged and adds components for things a member or a guest actually uses:
the member portal, account pages, public-facing web.

**It does not replace v2.** Print, flyers, PDFs, and anything handed to a
client stay on v2. Data dashboards stay on design.greenvilletriumph.club.

```
Print / promo / handout   →  v2
Member or public screen   →  v3   (this)
Internal data dashboard   →  design.greenvilletriumph.club
```

Link order, always:

```html
<link rel="stylesheet" href="tokens.css">
<link rel="stylesheet" href="pattern.css">
<link rel="stylesheet" href="components.css">
<script src="field.js"></script>
```

---

## RULE 0 — Two grounds. Never a third.

Navy and fog. That is the whole palette of section backgrounds.

A panel on fog is `#fff`. A panel on navy is `var(--navy)`. `--mist` is a chip
and an avatar colour; the moment it fills a band the width of the content
column it reads as a third ground and the page looks broken.

Sections alternate in **content blocks**, not one-by-one. Group sections that
tell one story onto one ground and flip at the story break:

```
hero            navy
tickets  season fog     ← "your stuff"
benefits zone    navy   ← "what your membership gives you"
news             fog
```

## RULE 1 — One continuous field.

The weave runs the length of the page as a single field. It is not a texture
restarted per section.

`components.css` handles this with `--field-y` and `field.js` writes it. If you
paint the weave yourself you will get a hard reset at every boundary and the
tiles will not meet, which reads as an accidental header bar mid-page.

The ink flips at a seam. The grid never does.

## RULE 2 — No box in a box in a box.

A filled or bordered panel may hold text and controls. It may not hold another
filled panel that holds another filled panel.

- A long list is ONE surface with dividing rules, not one card per row.
- A grid of cells shares one hairline: `gap: 1px` over a hairline ground, and
  the cells carry **no borders of their own**. Two rules in one place is a
  smudge.
- If a row needs a status, give it a bar or a mark. Not a tinted inner box.

## RULE 3 — Colour lives in marks, not in type.

Triumph Green (`--green`) and Lime (`--lime`) **never set type on a light
surface.** They fail contrast, Lime badly.

The status word stays navy. The colour goes in the mark under it and the
legend swatch above the list. That is what `.status-word`, `.status-marks`,
and `.legend` are for.

On navy, Lime is correct for eyebrows, accents, and the avatar chip.

## RULE 4 — Less on the page, more in the sheet.

A surface shows a heading and the ONE value someone came for, then `Details`.
Every other fact, the paragraph, and the links live in the shared detail sheet.

There is **one** sheet per page. Tiles, ledger rows, and cards all open it.
That is what makes the rule affordable.

## RULE 5 — Collections collapse. They never enumerate.

Anything that renders per-item must degrade before it runs off the edge. The
member portal holds this for seat counts; the same shape applies to any list.

- Names in prose: list up to three, then switch to a count.
  `Seat 10 and 11` · `Seat 10, 11, and 12` · `10 seats`
- Repeated marks: one per item up to six, then one per distinct outcome.
- Any wrapper that renders N children gets `flex-wrap` and a width cap.

Write the test. `scripts/test_seat_scaling.js` in the Member Portal is the
worked example: it asserts 1, 2, 3, 6, and 10 and it caught a missing serial
comma in its own fix.

## RULE 6 — Copy.

- Oxford comma, always. `tenure rewards, attendance rewards, and referral perks`
- Zero em-dashes. Commas, periods, semicolons, parentheses.
- Literal labels. `No scan`, not `Didn't make it`. `Sent to Gcanas`, not
  `Shared with a friend`.
- Say which of two things happened. A seat leaves a pass when a transfer is
  **sent**, not when it is **claimed**, so the row says which.

---

## Components

| Class | What it is |
|---|---|
| `.field` `.field-navy` `.field-fog` | Section ground + continuous weave |
| `.on-solid` | Fill under running text on a navy field |
| `.appbar` | Sticky bar: mark, current section, initials, lime rule |
| `.grid-divided` `.on-navy` | Cells sharing one hairline |
| `.tile` | Heading + one value + Details, whole surface clickable |
| `.ledger` | Long dated list on one surface, month rows inside it |
| `.status-word` `.status-marks` `.legend` | Rule 3 in practice |
| `.sheet-scrim` `.sheet` `.facts` | The one detail sheet |

## Accessibility floors that are also design floors

- Tap targets 44px. A `<button>` tile gets this for free; a bare link does not.
- A sticky bar that hides on scroll must come back on scroll up.
- Scroll-lock a modal with `position: fixed` on `body`, not `overflow: hidden`.
  iOS ignores the latter. Restore the scroll offset on close.
- Anything that opens the sheet from an in-page anchor must release the lock,
  or the jump lands behind a frozen body.
