Greenville Triumph · 2026

Build dashboards that look on brand the first time.

The Triumph design system. Two tracks, one brand: dark data-dense dashboards for staff, and the brand guide for everything a fan or sponsor sees. Link two files, use the tokens, and it comes out right.

tokens.css components.css Two files. Zero hardcoded hex.
match-day-revenue.html live
Revenue MTD
$284,610
12.4% vs prior
Tickets sold
8,142
6.1% vs prior
STH base
1,210
93% renewed

Revenue by channel

On track
Stone Stadium94%
GE Vernova Park71%
00 · Start here

Two links and you inherit the whole brand.

Fonts (Fabiola + Inter), the full palette, the type scale, spacing, and every component arrive through these two stylesheets. Then clone starter.html and edit content, not layout.

First, pick the right system
You are here
Triumph Dashboard System

Internal staff dashboards and ops tools. Data-dense, dark, screen-tuned. Everything on this page.

  • AudienceStaff, internal only
  • CanvasGraphite ladder, locked dark
  • TypeInter + JetBrains Mono, one Fabiola hero
  • Shipstokens.css + components.css

Use for: report.greenvilletriumph.club, scan, numbers, projections, the intern boards.

The other half
Triumph Design System v2

The actual Brandpad brand. Flyers, digital ads, printed pieces, and any page a customer or sponsor will see.

  • AudienceFans, sponsors, the public
  • CanvasUpstate Fog light or Greenville Blue
  • TypeFabiola display + Inter 900 headings
  • Shipstokens / pattern / logos css

Use for: matchday flyers, IG posts, sponsor one-sheeters, landing pages.

The split is audience, not medium. A web page does not automatically follow the dashboard system. If a fan or a sponsor will see it, it is built on the brand, on a light canvas, even though it is a web page. Sections 08 Pattern, 09 Craft, 07 Logos, and 10 Voice are shared law and govern both.

<!-- dark-mode lock (meta + first style block) -->
<meta name="color-scheme" content="only dark">
<meta name="supported-color-schemes" content="dark">
<meta name="darkreader-lock">
<meta name="theme-color" content="#0b1017">
<style>:root,html,body{background:#0b1017!important;color:#f2fafa!important;color-scheme:only dark;}</style>

<!-- tokens first, then components; all three fonts ride along -->
<link rel="stylesheet" href="https://design.greenvilletriumph.club/tokens.css">
<link rel="stylesheet" href="https://design.greenvilletriumph.club/components.css">

The four rules that keep it on brand

  • Use var(--token-name) in CSS. Never hardcode a hex, rgba, or oklch value.
  • Fabiola is the hero only (.h-display, once per page). Every other heading is Inter.
  • Pick one accent per layout: Electric Lime or Light Green, not both.
  • Labels are --text-label, nothing renders below --text-xs, and font-size only ever takes a --text-* token.
01 · Color

Seven brand colors. Never an eighth.

The whole system is built from these. Every surface, border, and accent is one of them or a derived tint. Click a swatch to copy its hex.

Brand palette
Greenville Blue#002855PMS 295 C · deep band
Triumph Green#61a631PMS 369 C · primary accent
Light Green#97d700PMS 375 C · lime accent
Electric Lime#69b3e7PMS 292 C · sky accent
Morning Mist#d2e6e5PMS 5455 C · soft text
Upstate Fog#f2fafaPMS 7541 C · primary text
Deep Night#020022PMS Black 6 C · canvas
Surface elevation

Depth without leaving the palette. Deep Night lifted toward Upstate Fog gives a calm slate ladder, so cards read as layers instead of a flat wall of saturated navy. Greenville Blue is reserved for one deep emphasis band.

bg--color-bg
surface-1band
surface-2card
surface-3raised
surface-4overlay
Contrast (engineering note, not Brandpad)
PairingPreviewRatioGrade
Upstate Fog on graphite canvasBody text16.8AAA
Morning Mist on graphite canvasSoft text13.7AAA
Electric Lime on graphite canvasAccent text7.8AAA
Triumph Green on graphite canvasAccent text5.9AA
Greenville Blue on Upstate FogText on light13.8AAA
Triumph Green on Upstate FogAvoid this2.83FAIL

On light surfaces, body text is Greenville Blue or Deep Night, never Triumph Green.

Brand track: promo accents

On flyers, ads, and customer-facing pages the hero accent is the green gradient, not a flat fill. It runs light to dark, and it never flips. Accents are for highlights only, never a large background, and never both accents in one layout.

Hero gradient#97d700 → #72b431Promo only · never flip
Liberty Yellow#ffe900Greenville Liberty only
Liberty Sky#70b2e4Greenville Liberty only

Greenville Liberty runs Greenville Blue plus Liberty Yellow and Liberty Sky. Those two are the sister club's colours and do not enter a Triumph layout. On dashboards, yellow of any kind means one thing only: a genuine warning state.

02 · Typography

One Fabiola moment. Inter does the work.

Brandpad assigns Fabiola Capitals to its headings. On a data-dense screen that fights the data, and any weight above 400 synthetic-bolds the single-weight file into mush. So Fabiola is restricted to one hero (.h-display, rendered at the real Brandpad spec) and every working heading is Inter at a genuine heavy cut weight.

.h-displayFabiola Capitals · 400 · the hero only · --text-display
Together we triumph
h1Inter · 800 · page title
Match-day snapshot
h2Inter · 800 · section title
Revenue by channel
h3Inter · 700 · card / subsection
Season ticket holders
h4Inter · 700 · label heading
Per-section availability
subheaderJetBrains Mono · 700 · --text-lg · heads a content group
Per-section availability
ledeInter · 300 · large intro only
A light weight reads elegant at large sizes and weak at small ones, so 300 stays big.
bodyInter · 400 · default
Body copy is Inter Regular at 400 for legibility on the dark canvas. This is a deliberate dashboard choice; Brandpad ships marketing body at 300.
labelJetBrains Mono · 600 · --text-label · also h6, .section-number, .kpi-label
01 · Tickets sold
kickerJetBrains Mono · 700 · accent · ONE per page
Greenville Triumph · 2026
statJetBrains Mono · 800 · tabular · --text-4xl
18,472
The label law
Do
  • Every uppercase mono label is --text-label · 600 · --track-label · muted. Eyebrows, section numbers, KPI and stat labels, table headers, nav links: one spec, no exceptions.
  • --text-xs is the absolute floor. Footnotes, deltas, badges, dense cells sit there; nothing sits below it.
  • Subheaders are headings: .subsection-label at --text-lg minimum.
  • One featured kicker per page may take .eyebrow-accent.
Don't
  • Type a raw rem or px font-size. Size only via --text-* tokens.
  • Re-shrink a registry label in page CSS. A local 0.65rem clone of .kpi-label is the canonical sin.
  • Type a literal letter-spacing on a label; the --track-* tokens are the only values.
  • Put the accent on wayfinding. Green means interactive or live.
Handling Fabiola
Do
  • Display and hero only, weight 400, once per page.
  • Leave the vertical seating alone. The metric-override @font-face (ascent, descent, line gap) is baked into tokens.css, so Fabiola already sits correctly.
  • Correct the horizontal edge by hand when two Fabiola lines stack, or when a Fabiola hero shares a left edge with an Inter eyebrow. Fabiola's left side bearing varies per glyph, so a flush-left stack goes ragged.
  • Nudge with margin-left: -(lsb)em keyed to that line's first glyph. The measured per-glyph table is in tokens.css.
  • Keep font-synthesis: none.
Don't
  • Hand-nudge Fabiola vertically with margins, line-height, or translateY. If it looks wrong, fix it in tokens.css, not on the page.
  • Route h2 or h3 through Fabiola. It synthetic-bolds to mush.
  • Use any Fabiola weight above 400.
  • Bake a side-bearing nudge into a class or a template. It is per-content, so it applies per piece.
  • Nudge a single centred hero. It needs nothing.
03 · Layout & spacing

Fill the screen, keep the rhythm.

Three container widths and drop-in grids so a dashboard uses the monitor instead of stranding a skinny column in the middle. A single fluid spacing scale keeps the vertical rhythm consistent everywhere.

The layout law
Do
  • Default to horizontal. On a desktop dashboard, side by side is the starting point. A single column is the fallback under 1000px, not the first move.
  • Ask of every block: does this fit beside its neighbour? If yes, put it there.
  • Use .split, .split-wide, .prose-cols, or a grid. They collapse on their own when the columns would get too narrow to read.
  • Let a long paragraph run .prose-cols across the canvas instead of a 64ch ribbon with an empty half beside it.
  • Reserve the single column for genuinely sequential reading.
Never
  • Drop everything into one narrow left-aligned stack. This is the most repeated failure in the system. It is not a design decision, it is the lazy default.
  • Leave 60% of a 1440px canvas empty while the copy breaks five words in.
  • Shrink type so a too-narrow column "fits". Widen the column instead.
  • Stack two things that would sit happily side by side.
  • Insert a <br> to fix a bad wrap. Fix the width.

This is enforced, not just written down. Above 1000px a .section-header that carries a description stops being a stack and becomes two columns automatically: the number and title on the left, the supporting copy on the right, sharing a baseline. Every section header on this page and on every dashboard that links components.css reclaims that dead half with no page edits. Headings also carry text-wrap: balance and body copy carries text-wrap: pretty by default, so a heading that has to break splits into even lines rather than dumping one orphan word per line. If a phrase must never break at all, give it .phrase. If balance still leaves you a one-word column, the container is too narrow: widen it.

Containers
.container
1200px

Reading width. Prose, single-column docs.

.container-wide
1440px

Dashboard width. KPI rows, charts, multi-column data.

.container-full
100%

Full bleed with gutters. Dense tables, seat maps.

Grids

Responsive by default, collapse to one column on narrow screens.

grid-4
grid-4
grid-4
grid-4
col-8 (chart)
col-4 (rail)
Spacing scale
--space-xs
--space-sm
--space-md
--space-lg
--space-xl
--space-section
04 · Components

The controls, brand-correct out of the box.

Sharp edges, lift on hover (never scale), one accent per layout. Everything below is a class in components.css.

Buttons
Inputs
Toggle & segmented
Tabs
Chips
Badges, pills & deltas
Sold out In Review Live On track 12.4% 3.1%
States & feedback
No matches found
Try widening the date range.
Snapshot failed
The API returned 502. Retry in a minute.
updated 2m ago updated 26m ago updated 3h ago
Saved
Board updated. Shown in place here; live, toasts stack bottom-right via .toast-stack.
05 · Data display

Numbers, the way Triumph reads them.

Tabular mono figures, accent-keyed KPIs, dense tables with totals, and Chart.js wired to the palette through the tok() helper.

KPI tiles
Revenue
$2.41M
12% vs last season
Tickets sold
18,472
8% vs last season
Avg order
$128
2% vs last season
Sell-through
94.2%
flat vs last season

Tickets by channel

Cumulative sales

live
Executive scoreboard
Greenville Triumph

2026 Season Ticketing

As of Jun 4, 2026 · 5 played · 14 to go
Total revenue
$499,086
tickets + parking, season to date
Tickets distributed
26,354
admissions, season to date
Season ticket members
874
$344,349 in season plans
Home sell-through
94.2%
20,217 of 21,460 seats
Title-card hero

For pages that ARE the event (matchday guides, one-pagers): the Main lockup, a quiet Inter kicker, the fixture in poster Fabiola, and an info rail where Light Green marks the one live fact. The scale boundary is hard: poster Fabiola belongs to title-card pages only; data dashboards keep .h-display at the calibrated size.

Greenville Triumph

Match 9 of 17 · 2026 USL League One

Triumph vs Knoxville
Kickoff7:00 PM
VenueGE Vernova Park
Gates5:30 PM
Capacity3,842
Stat band
Matches
17
2026 home slate
Capacity
3,842
GE Vernova Park
STH base
1,210
renewals + new
Sell-through
94.2%
season to date
Data table
MatchOpponentSoldHeldOpenStatus
5/9Lexington SC3,84200Sold out
6/3Charlotte Independence3,104220518On sale
6/21One Knoxville2,4881801,174Slow
7/11Chattanooga Red Wolves3,610120112Near sellout
Total13,0445201,804
06 · Dashboard demo

A real page, assembled only from this system.

No bespoke CSS. Every block above, composed the way a live Triumph dashboard composes them. This is the bar a page hits on the first try.

Tickets · Season 2026 updated 2m ago
Revenue
$2.41M
Sold
18,472
STH
1,210
Sell-through
94.2%

Weekly sales

Top reps

Sam$612K
Brandon$548K
Owen$410K

Brand-correct, screen-tuned, every time.

07 · Logos

Three lockups, three backgrounds.

Pick the lockup for the space and the variant for the background. Never recolor, stretch, rotate, or add effects.

Main
Main color
Main · ColorPNG
Main on light
Main · On lightPNG
Main on dark
Main · On darkPNG
Simplified
Simplified color
Simplified · ColorPNG
Simplified on light
Simplified · On lightPNG
Simplified on dark
Simplified · On darkPNG
Wordmark
Wordmark color
Wordmark · ColorPNG
Wordmark on light
Wordmark · On lightPNG
Wordmark on dark
Wordmark · On darkPNG
Elements
Brand pattern
PatternPNG
Brand shape
ShapePNG

The Pattern element above is the weave. How to place it is 08 Pattern, and getting it wrong is the most common brand error in the whole system.

Lockup law
Do
  • Full-colour crest on light surfaces only.
  • On dark or navy, use the white crest (triumph-main-on-dark.png).
  • Always the full lockup, at a size where it reads.
  • Give GE Vernova Park real presence. It is a venue partner, not a footnote.
Never
  • Full-colour crest on a dark background.
  • Recolour, outline, rotate, stretch, flip, or crop a logo.
  • Add a shadow, glow, or any effect.
  • Size a raw GE Vernova PNG to its file box. Those files carry 38 to 61 percent transparent padding, so they render about 150 percent too small. Re-measure to the alpha bounding box and crop first.
08 · Pattern

One large woven field. Never small tiles.

The interlocking-T weave is the brand's one texture, and it is the element most often used wrong. It is a graphic element, not a texture, not wallpaper, not a watermark. It reads as woven fabric only when the motif is big, the field runs edge to edge, and it carries the house −8° tilt. The tilt is wanted; square reads wrong. What goes wrong is scale and inconsistency, not angle. This section is law for both systems.

The one method
<!-- ONE field: cover, no-repeat, tilted -8deg -->
<section class="weave weave--navy">
  <!-- content is a z-index:1 sibling, handled by the class -->
</section>

/* the whole class, for reference */
.weave::before {
  position: absolute;
  /* vw, never % - a % inset starves on short bands */
  inset: calc(-1 * 12vw);
  background-position: center; background-size: cover; background-repeat: no-repeat;
  transform: rotate(-8deg);  /* the house tilt */
}
.weave--flat::before  { inset: 0; transform: none; }  /* square, opt-in */
.weave--light::before { background-image: url("triumph-pattern-field.png");       opacity: .07; }
.weave--navy::before  { background-image: url("triumph-pattern-field-white.png"); opacity: .09; }

Why cover, and not a tile

  • The asset is a finished panel (1600×1853), not a repeat unit. Tiling a panel leaves visible blocks.
  • It is cut from the interior of the 11812px master, inset one full repeat period on every side. Cropping to the artwork's alpha bounding box puts the panel's finished border on the edge, and that border is isolated poles with blank gaps between them, not weave.
  • cover holds the motif scale identical across every band, as long as the band is shorter than about 1.19× the viewport width. Every normal section is.
  • The −8° tilt is the default and it is wanted. Square reads wrong. The class handles the overscan that keeps the tilt from baring a corner, so you never do that math by hand.
  • Two colourways from one crop: navy artwork for light surfaces, the same file forced to white for dark ones.
Navy field PNG White field PNG 11812px master
Right, right, wrong
Correct
Dark surface · white artwork · opacity .09
Correct
Light surface · navy artwork · opacity .07
Banned
Same tilt, but tiled small and dense. Reads as pasted chunks.
One treatment per page

Every band on a page gets the same asset, same method, same scale, same rotation. The only two things allowed to differ between surfaces are the colourway and the opacity. Keep a page to two surfaces, one light and one dark. A third near-white or Morning Mist band reads as drift. Separate two adjacent same-colour sections with a hairline rule, not with a new background colour. Float dense content, tables especially, on a solid panel above the field so the weave never fights the text.

Do
  • One continuous field, edge to edge, motif big.
  • Keep the −8° tilt. It is the house look.
  • cover and no-repeat, every time.
  • White artwork on dark, navy artwork on light.
  • Hold one opacity per surface: .07 light, .09 dark.
  • Same treatment on every band of the page.
  • Put tables and long prose on a panel above the field.
Never
  • Repeat-tile it. Small tile equals pasted chunks.
  • Shrink background-size to make a fine texture. Size is not a knob.
  • Step the size down again at mobile breakpoints.
  • Use it as a faint watermark on a light surface.
  • Tilt one band and leave the next one square.
  • Hand-roll the tilt with a percent inset. It starves on short bands and bares the corners.
  • Leave a band bare while its neighbours carry the weave.
  • Put it on body or any global background.
  • Run it behind white text without a panel.

Legacy note: .brand-field and .brand-zone repeat-tile the weave. That, not their tilt, is why they are deprecated. They stay only so already-shipped dashboards keep rendering. Do not use them in new work, and migrate a page to .weave whenever you touch it. The hero of this page runs on .weave.

09 · Craft

The rules that separate designed from assembled.

Tokens keep a page on brand. They do not make it good. These are the composition rules, learned the hard way on real pieces, and they apply to a flyer and a dashboard alike.

01
On brand pieces, no hero photography.

On a flyer, ad, or customer-facing page: no stadium shot, no crowd, no action photo behind a headline, unless that piece specifically called for one. Default to graphic and typography only: the weave, the logos, colour, and type. If a layout feels empty, that is a composition problem, and a photo is not the fix.

Fill it with weave, scale, or colour blocking instead. Dashboards may use a photo break as a section divider.

02
Use the whole canvas.

The single most common failure is content huddled in the middle with a lake of dead space around it. Composition has to reach into every region. Anchor things to top and bottom, push type to real margins, let a hero word or the weave run full bleed.

Test: if it survives being cropped to its middle third, you did not use the space.

03
Never stack single words vertically.

It is BEER GARDEN on one line, not BEER over GARDEN. A column of single stacked words looks broken every time. If a phrase will not fit, scale the type down or widen the block first. Only then break it, and break into balanced multi-word lines at a natural boundary.

text-wrap: balance on headings. Give hero text room to be wide, not tall.

04
Vary the alignment.

A symmetric stack of centred text on centred blocks reads amateur. Pick an axis and commit to it, then break it deliberately once. Left-aligned type with one full-bleed element carries more authority than everything centred.

Applies to section headers and KPI rows too, not just flyers.

05
A flyer is not a web page.

On a printed or social piece: no buttons, no pulsing dots, no live countdowns, no boxed dashboard chrome. Those are interface, and they read as clip art on a graphic. The inverse holds too, a dashboard does not need a hero headline the size of a poster.

Know which artefact you are making before you start.

06
Adversarially read every number.

Before a figure goes on anything a client sees, read it the way a sceptic would. A stat that flatters on first read and embarrasses on second is worse than no stat. Check that the number, the label, and the timeframe actually agree.

Cross-check totals against the source. Never invent a figure.

Rule 03, seen
Banned
Beer
Garden
Tonight

Broken and cramped. One word per line is never the answer.

Correct
Beer Garden

Phrase intact on one line, scaled to fit its container.

10 · Voice

Clear, inclusive, never corporate.

Together we triumph

Soccer connects people across cultures, languages, and backgrounds, creating community, opportunity, and a sense of home.

Do
  • Lead with clarity. Do not overcomplicate.
  • Stay inclusive. Speak to belonging.
  • Say "Greenville Triumph" in everyday copy.
Avoid
  • Corporate or detached tone.
  • Aggressive or exclusionary language.
  • "GVL Triumph", or any em dash.
11 · Anti-patterns

The drift that makes a page look off.

Do
  • Reference tokens by name: var(--color-accent).
  • Lift on hover with translateY and a border tint.
  • Flat surfaces from the elevation ladder.
  • Fabiola weight 400, hero only.
  • One accent per layout.
  • Tabular-nums on every figures column.
  • The weave as one big field, cover, no repeat.
  • Default to horizontal. Side by side before stacked.
  • Reach into every region of the canvas.
  • Multi-word phrases on one line.
  • Say "Greenville Triumph" and "Greenville Liberty" in full.
Don't
  • Hardcode hex, rgba, or oklch.
  • Scale on hover, or pulse scroll arrows.
  • Gradient page or section backgrounds.
  • Fabiola on h2/h3, or any weight above 400.
  • Pair Electric Lime with Light Green.
  • Use yellow as a decorative accent.
  • Tile or shrink the weave.
  • Put a hero photo behind a headline.
  • Stack single words one per line.
  • Drop everything into one narrow vertical column.
  • Leave half the canvas empty while copy breaks 5 words in.
  • Huddle content in the middle of the canvas.
  • Write an em dash, or any restricted-access wording.
12 · Ship checklist

Before it goes out.

Dashboard track Brand track (flyers, ads, anything a customer sees)
14 · Token reference

The names, copy-paste ready.

The most-used tokens as raw values. The full set lives in tokens.css.

/* canvas + elevation - neutral navy-graphite ladder */
--color-bg: #0b1017;
--color-surface-1: #121821;  --color-surface-2: #1a212c;
--color-surface-3: #232c39;  --color-surface-4: #2e3846;
/* text */
--color-text: #f2fafa;  --color-text-soft: #d2e6e5;
/* accents (one per layout) */
--color-accent: #61a631;  --color-blue: #69b3e7;  --color-pop: #97d700;
/* type + layout */
--font-display: Fabiola Capitals;
--font-sans: Inter;  --font-mono: JetBrains Mono;
--text-label: clamp(0.905rem, 0.86rem + 0.22vw, 1.00rem);
--track-label: 0.08em;  --track-kicker: 0.12em;  --track-subhead: 0.05em;
--content-wide: 1440px;  --space-section: clamp(2.25rem, 1.8rem + 1.9vw, 3.75rem);
/* left nav rail */
--rail-w: 60px;  --rail-w-open: 248px;  --rail-hover-delay: 0.12s;
/* Chart.js: canvas cannot read vars, use the helper. Read from document.body,
   not documentElement - the CVD remap lives on body[data-cvd], and body
   inherits every :root token anyway */
const tok = n => getComputedStyle(document.body).getPropertyValue(n).trim();