# Greenville Triumph Dashboard System - Build Contract

If you read only this file, you can still build a brand-correct Triumph
dashboard. Live reference and examples: https://design.greenvilletriumph.club/

This is the **dashboard** system: staff-facing tools, ops surfaces, ticketing
and revenue dashboards. On brand, tuned for dense data on a screen.

---

## The system in one idea: an instrument panel, not a quiet dashboard

This builds Triumph staff dashboards: dense, data-heavy operations surfaces read
at desk distance. The goal is a broadcast-grade instrument panel. It should look
engineered, read like a scoreboard, and hold a lot of data without ever looking
noisy. Density is the point, not a problem to solve. A Triumph dashboard is
expected to carry many numbers, many charts, and many small readouts at once.

"Clean" and "dense" are not in tension here, and you never resolve that tension
by removing data. Clean comes entirely from structural discipline: one grid
rhythm, one spacing scale, one label size, one type hierarchy, and color used as
signal instead of decoration. When a page feels busy, the fault is inconsistency
(mismatched spacing, a stray font size, a second accent, boxes inside boxes),
not the amount of information. Fix the structure, keep the data. A page with
forty numbers on a strict grid reads calmer than a page with ten numbers on a
sloppy one.

Color is signal, not surface. The canvas and the card surfaces are a restrained
navy-graphite field so that green, sky, lime, and status colors register as
intentional accents against it rather than as one more shade of the same blue.
Green means interactive or live, nothing else. Brand color is concentrated in
three sanctioned spots per page, and everywhere else stays quiet. This is a
rebalance, not a rebrand: still navy, still Triumph, just no longer
navy-on-navy-on-navy at every layer. See the elevation ladder in section 3 and
the sanctioned-green-spots rule directly below.

Structure carries the identity: the blueprint line grid, sharp corners (radius
0), the surface elevation ladder, the signature feature card, the scoreboard
hero, and the left navigation rail. Motion is subtle, choreographed,
reduced-motion-guarded, and never a scale transform. Every decorative element
must be load-bearing. Restraint is the technique and every value is tuned to
the minimum that achieves the effect; restraint is not the same as absence.

### The three sanctioned green spots per page

Triumph Green appears loud in exactly three places, and stays quiet everywhere else:

1. The signature feature-card top edge (`.card-feature`, the 1px green-to-sky-to-transparent edge; a structural edge, never a fill).
2. The one hero scoreboard moment (`.exec-hero-card` `.is-lead` numeral at `--text-4xl`).
3. Live and interactive state (active nav rail link, focus rings, pills, links, CTAs, live counters).

These are structural and stateful, not decorative, so they do not count against
the one-accent-per-layout rule.

---

## 1. Paste this into `<head>`

```html
<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>

<link rel="stylesheet" href="https://design.greenvilletriumph.club/tokens.css">
<link rel="stylesheet" href="https://design.greenvilletriumph.club/components.css">
```

All three faces (Fabiola Capitals, Inter, JetBrains Mono) load automatically
through `@font-face` in tokens.css; no Google Fonts link needed. Best starting
point: clone one of the three starters and edit content only:

- `https://design.greenvilletriumph.club/starter.html` - the quiet dashboard
  (KPI row, chart, table). Default for routine pages.
- `https://design.greenvilletriumph.club/starter-dataluxe.html` - the
  instrument panel (exec scoreboard opener, brand-zone field, bento mosaic,
  split rail, rings, vision control). For flagship ops boards.
- `https://design.greenvilletriumph.club/starter-titlecard.html` - the
  title-card one-pager (poster Fabiola `.title-hero`). For pages that ARE the
  event (matchday guides, one-pagers), never data dashboards.

---

## 2. Hard rules (non-negotiable)

- Use `var(--token-name)` in CSS. Never hardcode a hex, rgba, or oklch value. (Chart.js datasets are the only exception, see section 6.)
- Fabiola is the hero only: one `.h-display` per page, weight 400. Every other heading is Inter. Never put Fabiola on h2/h3, never weight above 400 on it.
- Seven brand colors, never an eighth. Surfaces are the derived slate ladder, not new colors.
- One accent per layout: Electric Lime (`--color-blue`) OR Light Green (`--color-pop`), never both.
- SIZE LAW: `font-size` only ever takes a `--text-*` token, never a raw rem/px value. `--text-xs` is the absolute floor; NOTHING renders smaller, including captions, footnotes, chart annotations, status pills, and count bubbles. Never shrink a registry component's type in page CSS (`.kpi-label { font-size: 0.65rem }` is the canonical sin). Labels too small, everywhere, was the system's longest-running failure; these tokens are the fix.
- Every uppercase mono label is `--text-label` / `--weight-semibold` / `--track-label` / muted. That covers eyebrows, section numbers, KPI and stat labels, table headers, nav links, and any key/dt-style label you invent. Do not re-implement the role with a local class at a smaller size; use the registry class or match its spec exactly.
- Labels never wear the accent. Triumph Green means interactive or live (links, active nav, CTAs, live counters, NOW states), nothing else. Exactly ONE featured kicker per page (the hero's) may opt in via `.eyebrow-accent` (`--weight-bold` / `--track-kicker` / accent).
- Tracking only via the tokens: `--track-label` (working labels), `--track-kicker` (the one featured kicker), `--track-subhead` (`.subsection-label`). Never type a literal letter-spacing value on a label. Wide tracking on tiny text is the recurring disease; the tokens are sized to the label tier.
- Subheaders are HEADINGS, not labels. Any header of a content group (`.subsection-label`, timeline group headers, map group titles) renders at `--text-lg` minimum with tracking at or under `--track-subhead`. `h6` and `.eyebrow` are labels; they never head a content group. Tiny mono caps as subheaders is the #1 recurring failure; do not reintroduce it.
- Triumph Green is never body text on a light surface (fails contrast). Use Greenville Blue or Deep Night.
- Yellow (`--color-warning`) is for genuine warning state only, never decorative.
- Sharp corners by default (`--radius` is 0). `--radius-full` only on pills, badges, toggle thumbs.
- Lift on hover (`translateY`), never scale. No gradient page or section backgrounds.
- Tabular-nums on every numeric column.
- Divided cell rows (stat bands, scoreboards) and left-bar accents use symmetric horizontal padding so text NEVER touches a divider or accent bar. Flush only the first cell of each row to the container edge.
- No em-dashes in copy. No "GVL Triumph" (use "Greenville Triumph"). No restricted-access framing.

---

## 3. Tokens (most-used; full set in tokens.css)

```
/* canvas + elevation ladder (neutral navy-graphite, not saturated blue) */
--color-bg          page canvas (#0b1017)
--color-surface-1   faint band (alternate sections: <section class="bg-surface">)
--color-surface-2   card / panel
--color-surface-3   raised / hover
--color-surface-4   overlay / drawer
--color-band-navy   the ONE deliberate saturated Greenville-Blue band (use sparingly;
                    everywhere else on the ladder is neutral graphite by design)

/* text */
--color-text        primary (#f2fafa)   --color-text-soft  secondary (#d2e6e5)
--color-text-muted  labels/meta         --color-text-faint disabled/placeholder
--text-alpha-90/70/50/30/15   Upstate Fog at opacity; for text on a colored/variable bg

/* borders */
--color-border  --color-border-emphasis  --color-border-strong

/* accents (one per layout) */
--color-accent  #61a631 Triumph Green   --color-blue #69b3e7 sky   --color-pop #97d700 lime
--accent-subtle (row tint)   --accent-muted (active/focus)

/* status (signals only) */
--color-success  --color-warning  --color-danger  --color-info  (+ each -subtle)

/* chart series (hex; stays in the seven colors) */
--series-green #61a631   --series-sky #69b3e7   --series-lime #97d700   --series-mist #d2e6e5

/* type */
--font-display Fabiola Capitals (.h-display only)   --font-sans Inter   --font-mono JetBrains Mono
--text-xs (THE FLOOR) ... --text-4xl (fluid)   --text-display (Fabiola hero only)
--text-label   the one size for every uppercase mono label (~15px at 1440)
--track-label / --track-kicker / --track-subhead   the only letter-spacing values labels may use
--weight-light 300 ... --weight-black 900   (body default 400; 300 for large lede only)

/* spacing + layout */
--space-xs/sm/md/lg/xl + --space-section   (fluid)   --space-1..16 (fixed 4px base)
--content-max 1200px   --content-wide 1440px   --gutter   --nav-h 60px
--radius 0   --radius-full 999px
--shadow-sm/md/lg   --shadow-float/-float-lg (overlays: dropdowns, toasts, popovers)
--duration/-instant/-fast/-slow   --ease-out/-in/-in-out/-spring (spring overshoots via linear() where supported)

/* left nav rail (see section 4b) */
--rail-w 60px (collapsed)   --rail-w-open 248px (expanded)
--rail-hover-delay 0.12s (hover-intent debounce, read by rail.js)   --rail-transition   --rail-z 85

/* frosted chrome (nav + rail + glass overlays; never a raw rgba literal) */
--surface-frost       .nav / .nav-rail background
--surface-frost-deep  .frosted / drawer-scrim background
```

---

## 4. Layout pattern (the canonical dashboard shape)

Slim top bar (brand + actions only, no section links) plus a left nav rail that
owns section wayfinding with scroll-spy, then banded numbered sections,
continuous scroll. The rail is default; see 4b for its full contract.

```html
<header class="nav"><div class="nav-inner container-wide">
  <a class="nav-brand" href="#top"><img src="https://design.greenvilletriumph.club/images/brand/triumph-wordmark-on-dark.png" alt="">Name</a>
  <div class="nav-actions">
    <button class="nav-rail-toggle" aria-expanded="false" aria-controls="nav-rail">…</button>
    <!-- Export button, .vision CVD control, etc. -->
  </div>
</div></header>

<aside class="nav-rail" aria-label="Section navigation">
  <div class="nav-rail-inner">
    <nav class="nav-rail-links" id="nav-rail">
      <a class="nav-rail-link" href="#overview"><span class="nav-rail-mark">01</span><span class="nav-rail-label">Overview</span></a>
      <!-- one .nav-rail-link per section, in document order -->
    </nav>
  </div>
</aside>
<div class="nav-rail-scrim" hidden></div>

<body class="has-rail">
  <section id="overview" class="bg-surface"><div class="container-wide">
    <div class="section-header">
      <span class="section-number">01 · Overview</span>
      <h2 class="section-title">Headline.</h2>
      <p class="section-desc">One supporting sentence.</p>
    </div>
    <!-- content -->
  </div></section>
```

Containers: `.container` (1200, reading), `.container-wide` (1440, dashboards),
`.container-full` (full bleed). Grids: `.grid-kpi`, `.grid-cards`, `.grid-2/3/4`,
and `.grid` (12-col) with `.col-4 .col-8` spans. Alternate `class="bg-surface"`
on sections for banded rhythm.

---

## 4b. Left nav rail (canonical section wayfinding)

The rail replaced the old horizontal `.nav-links` row for in-page section
jumps. It is fixed to the left edge, collapsed to icon/mark width by default,
and expands into the full labelled panel via hover-intent, keyboard focus, or a
deliberate click-to-pin. `.nav-links` still exists in components.css for a page
that wants a horizontal link row instead, but the rail is the default for every
new dashboard.

**Note on an older constraint:** an earlier revision of this doc (and a matching
CSS comment) said hover must NEVER widen the rail - that was written for a
prior design iteration and is not a permanent rule. The project owner confirmed
the actual requirement is the opposite: hovering the collapsed rail should
expand it into a fully usable panel (links clickable, labels visible). The
three-tier model below is the current, correct behavior.

**Hard rule (still non-negotiable):** no rail state may ever obscure, hide, or
clip real page content (numbers, table columns, headline text) *persistently*.
Two early versions broke this by making an *expanded, reflow-free* panel sit
directly over content with only the 60px collapsed width reserved (one
translucent, one opaque) - the fix isn't "never widen on hover," it's "hover's
widened panel floats as a self-shadowed overlay and auto-dismisses the instant
the pointer/focus leaves," so it never *permanently* hides anything. Only a
deliberate, persistent pin earns a permanent page reflow.

**When to use it:** every dashboard with more than 2-3 scroll sections. Skip it
only for a single-section page with nothing to jump to.

**Classes:**

```
nav-rail                the fixed left rail container
nav-rail-inner           vertical flex column inside it
nav-rail-brand           reserved for a small icon-only Triumph mark at the rail top;
                         currently unused (display:none) - no square icon exists in the
                         asset library yet, so the rail does not duplicate the top bar's
                         full lockup. Reintroduce only if a real icon mark ships.
nav-rail-links           <nav id="nav-rail"> holding the section anchors
nav-rail-link (.active)  one section anchor; .active = scroll-spy current
nav-rail-mark            two-digit index shown always ("01", "02"...)
nav-rail-label           section name; invisible at rest, opacity:1 (inline
                         beside its mark) whenever the panel is open, in any
                         of the three tiers below
nav-rail-toggle          click-to-pin button (desktop) / drawer trigger (mobile);
                         lives as first child of .nav-actions in the top bar
nav-rail-scrim           tap-to-close scrim behind the drawer on mobile ONLY
                         (no desktop scrim - see below)
```

State hooks: `.nav-rail.hover-open` (debounced hover-intent, added/removed by
rail.js - see below), `body.has-rail` (offsets content by the collapsed rail
width on desktop, and reflows to the full panel width via
`body:has(.nav-rail.pinned)` ONLY when pinned; removes the offset entirely
under 900px), `.nav-rail.pinned` (click-pinned open, persists to `localStorage`
key `railPinned`), `.nav-rail.open` (mobile drawer transform hook).

**Behavior contract - the actual three-tier interaction model** (rev
2026-07-01): collapsed by default (marks only, `--rail-w` 60px wide). Three
tiers, ordered least -> most deliberate:

1. **Hover (transient, debounced ~150ms):** raw CSS `:hover` fires the instant
   the pointer enters and can't be debounced, which is exactly what caused the
   bug report this rev fixes (an incidental cursor pass near the left edge
   triggered the full panel; jitter near the panel's growing edge caused
   open/close flicker; a link click snapped shut the instant the mouse
   released). So this tier is driven by JS (`rail.js`), not a `:hover`
   selector: on `mouseenter` it starts a timer (`--rail-hover-delay`, ~150ms)
   before adding `.nav-rail.hover-open`; on `mouseleave` it clears any pending
   open timer and removes `.hover-open` after a short ~100ms grace period (so
   crossing the gap between the rail and its own children, or the instant
   between mouse-up on a link and the next paint, doesn't snap it shut before
   the click registers). `.hover-open` expands the full 248px labelled panel
   as a FLOATING OVERLAY - opaque surface + `--shadow-float`, no page reflow.
   Reflowing on every hover would make the whole page jump around on every
   mouse pass, which is worse than the original bug; a debounced
   hover-expand-as-floating-overlay is the standard pattern (VS Code's
   activity bar, Notion's sidebar, GitHub's collapsed nav all work this way).
2. **Focus (immediate, keyboard Tab):** `:focus-within` opens the identical
   floating-overlay panel, but instantly, no debounce - deliberate keyboard
   focus landing in the rail is already real intent, not incidental motion.
   Dismisses on blur.
3. **Pin (deliberate click-to-lock):** `.nav-rail.pinned` (toggled by
   `.nav-rail-toggle`) opens the panel AND reflows the page - `body`'s left
   padding animates from `--rail-w` to `--rail-w-open` (via
   `body:has(.nav-rail.pinned)`) so the panel occupies its own reserved column
   and never overlaps content, persisting across reloads via `localStorage`.
   Escape (or the scrim on mobile, or a second toggle click) closes it and the
   page reflows back.

Under 900px the rail collapses to a single toggle button in `.nav-actions` that
opens a full slide-in drawer over a `.nav-rail-scrim` (an explicit modal over
full-width content - no reflow on mobile); hover-intent doesn't apply on touch
(there's no `mouseenter`/`mouseleave`). Keyboard: rail links are real anchors
in DOM order; `:focus-within` opens the full labelled panel so Tab users see
labels, not bare marks; Escape closes a pinned/open rail and returns focus to
the toggle. All motion (width, the body-padding reflow, opacity, the mobile
transform) is gated by the global reduced-motion rule.

There is no desktop scrim: hover/focus panels float above content on their own
shadow (never a dimmed backdrop) and the pinned panel reflows the page into
reserved space, so `.nav-rail-scrim` is used only for the mobile drawer.

**Canonical script** lives in one shared file, `rail.js`, loaded identically by
every page that uses the rail via `<script src="rail.js"></script>` (placed
after the rail markup, e.g. at the end of `<body>`) - do not fork per-page
copies. It owns scroll-spy, the debounced hover-intent open/close, and the
click-to-pin / mobile-drawer / Escape toggle logic:

```html
<script src="rail.js"></script>
```

```js
// rail.js — see the file itself for the full, current implementation.
// Three responsibilities: (1) IntersectionObserver scroll-spy marking the
// current section's .nav-rail-link .active; (2) debounced hover-intent -
// mouseenter starts a --rail-hover-delay (~150ms) timer before adding
// .hover-open, mouseleave clears it and removes .hover-open after a ~100ms
// grace period; (3) click-to-pin / mobile drawer open-close / Escape, with
// .pinned persisted to localStorage key `railPinned`.
```

Key the link map by the target section's real `id` (`map[el.id] = a`), never by
`href.slice(1)`. `id` is the one keying convention across every file. Wrap
`localStorage` calls in try/catch (sandboxed previews can throw). The
focus/pin panel opening, the pin's page reflow, and the label opacity swap are
all pure CSS keyed off `:focus-within` / `.pinned` - only scroll-spy,
hover-intent debouncing, click-to-pin persistence, the mobile drawer, and
Escape need script.

---

## 4c. Brand pattern (tessellation) - two templates, one asset

The interlocking-T tessellation (`--brand-pattern`, `images/brand/triumph-pattern-fog.png`)
is a single genuinely-seamless 328x328 tile, white ink on transparent. It is tiled with CSS
`background: var(--brand-pattern) repeat` and rotated -8deg as one already-continuous layer.
Rotating a repeat-tiled background is NOT the anti-pattern - it only breaks if the tile itself
isn't a true seamless period. That happened once (the file was a corrupt 229x305 crop); the
CSS was never the problem and was restored unchanged.

Two ready-made wrappers apply it. Pick by scope, don't invent a third:

```
brand-zone    full-bleed, page-height field. Spans the hero AND the section right after
              it, fades out over a long mask (~1.9 screens down). Use ONCE per page,
              wrapping the hero + the section right after it. Live example: this page's
              #top through #board.
brand-field   contained card/section wash. -30% inset, opacity var(--brand-pattern-opacity,
              0.08), fades out around 68% down its own box. Use on any card or section
              that wants the texture without owning the whole hero.
```

```html
<!-- brand-zone: page-height hero field -->
<div class="brand-zone">
  <header class="nav">...</header>
  <section class="hero">...</section>
  <section id="board">...</section> <!-- the fade completes somewhere in here -->
</div>

<!-- brand-field: contained section wash -->
<section class="hero brand-field">
  <div class="container-wide">...</div>
</section>
```

**Permanent rule (do not relitigate):** `background-size` stays at the 165px token - never
shrink the tile to a "micro" scale, and never swap the rotate+repeat+mask CSS for a single
pre-baked cover image. Both were tried and rejected. If the pattern ever looks broken (gaps,
seams, misaligned lines), the bug is the underlying PNG's repeat period, not this CSS. Fix it
by cropping a new genuinely-seamless square tile from the vector-exported source in
`Triumph Rebrand Assets\04. Elements\Greenville Triumph - Pattern - ORIGINAL.png` - verify
seamlessness empirically (tile it flat 3x3, then rotate the composite -8deg and check again
before shipping, since a rotated view can reveal a seam a flat test missed) - and drop it in
at the SAME filename. Never touch the CSS treatment to "fix" a pattern problem.

---

## 5. Component classes (registry)

```
nav, nav-inner, nav-brand, nav-actions                  slim sticky top bar (brand
                                                          + actions; no section links)
nav-links                                               optional horizontal section-link
                                                          row; .nav-rail is the default,
                                                          use this only if a page wants
                                                          links in the top bar instead
nav-rail, nav-rail-inner, nav-rail-links,                left section-nav rail: collapsed
  nav-rail-link (.active), nav-rail-mark,                 by default, scroll-spy, expands
  nav-rail-label, nav-rail-toggle, nav-rail-scrim          on hover or click, mobile drawer
                                                          under 900px (full contract: 4b)
brand-zone, brand-field                                  interlocking-T tessellation wrap;
                                                          zone = page-height, field = contained
                                                          section wash (full contract: 4c)
section-header, section-number, section-title,          section chrome
  section-desc, section-lede, subsection-label
card (+ card-raised, card-flush), card-head/title/desc/body/foot
kpi (marker) / kpi-tile (boxed) + kpi-label/value/foot  KPIs, in .grid-kpi
stat-band > stat > stat-eyebrow/value/meta              headline number row
exec-hero-card > exec-hero-eyebrow/title/sub +          executive scoreboard
  exec-scoreboard > exec-stat (.is-lead) >                (page lead, use once;
  exec-stat-label/value/sub                                value is --text-4xl)
title-hero > title-hero-mark/kicker/title +             title-card hero (matchday
  title-hero-rail > thr-item (.is-live) >                 guides, one-pagers; poster
  thr-label/value                                          Fabiola - NOT dashboards)
  -> .exec-hero-card = dashboard that REPORTS on the event (default).
     .title-hero = a page that IS the event (matchday guide / one-pager). Never both.
data-table (+ .dense, tr.total, td.num/.pos/.neg)       wrap in .table-scroll
btn (+ btn-solid/outline/ghost, btn-sm)                 buttons
input-wrap > input-underline                            underline input
toggle-input + toggle-track > toggle-thumb              switch
tabs > tab.active   |   segmented > button.active       tabs / segmented
toolbar > toolbar-group / toolbar-spacer                filter bar
chip.active + chip-x                                     filter chips
badge (+ -pill, -success/-warning/-danger/-info/-accent)
pill (+ .live pulse, .success/.warning/.danger/.info)
delta.up / .down / .flat                                change chip
progress > span[style=width] (+ .warning/.danger; .progress-block)
sparkline                                               Chart.js micro slot
chart-card > chart-head + chart-canvas[height]          chart panel
chart-legend                                            manual legend row
frosted                                                 glass overlay (nav/overlays ONLY);
                                                          bg is --surface-frost-deep, a token,
                                                          never a raw rgba literal
drawer-scrim + drawer (.open) > drawer-head/body        slide-in panel
toast-stack > toast (+ -success/-danger/-info/-warning) ephemeral feedback (JS-driven)
empty-state / error-state                               states
stale.fresh/.aging/.dead                                "updated Xm ago"
skeleton / skeleton-row                                 loading
guideline.do / .dont > guideline-label + ul             do/don't docs
hover-lift / hover-border / hover-glow                  hover affordances
reveal (+ .in)                                          scroll fade-up; REQUIRES JS observer + noscript
```

---

## 6. Chart.js

Self-hosted from the design domain, pinned 4.4.7 (the file's sha384 was
verified against the pinned jsDelivr build before shipping). Do NOT use the
CDN + SRI form: TLS-inspecting office proxies rewrite the bytes, the
integrity check fails, and every canvas on the page silently renders blank.

```html
<script defer src="https://design.greenvilletriumph.club/vendor/chart.umd.min.js"></script>
```

Canvas cannot read CSS variables, so pull palette values with the helper.
Read from `document.body`, NOT `documentElement`: the colorblind-safe remap
lives on `body[data-cvd]`, so a documentElement read silently ignores it
(body inherits every :root token anyway, so body is always correct):

```js
const tok = n => getComputedStyle(document.body).getPropertyValue(n).trim();
Chart.defaults.font.family = "'JetBrains Mono', monospace";
Chart.defaults.font.size = 12.5;   // size law applies to charts too: never below 12
Chart.defaults.color = tok('--color-text-muted');
// dataset colors: tok('--series-green'), tok('--series-sky'), tok('--series-lime'), tok('--series-mist')
// grid + ticks: tok('--color-border'), tok('--color-text-muted')
```

---

## 7. Before you ship

Both stylesheets linked (tokens before components). Dark-mode lock present.
Fabiola only on the hero at weight 400, every other heading Inter. No hex/rgba/oklch
literals except Chart.js. One accent per layout. Tabular-nums on numbers.
No font-size below `--text-xs`, no raw rem/px font-sizes, every uppercase label at
`--text-label` with a `--track-*` token, no accent on labels (one `.eyebrow-accent`
kicker max), subheaders at `--text-lg` minimum, chart type at 12px minimum.
"Greenville Triumph", never "GVL Triumph". No em-dashes, no restricted-access framing.
Left rail present with scroll-spy, collapsed by default, expands on hover or click,
mobile drawer under 900px, Escape closes it. Canvas and card surfaces are the neutral
graphite ladder; the only saturated navy is the one `--color-band-navy` band. No
hardcoded chrome colors; nav and glass surfaces use `--surface-frost` / `--surface-frost-deep`.

The brand book of record is https://brandpad.io/greenville-triumph/ (humans, login).
This system implements it for dashboards.

---

**Rev 2026-06-30 consolidation:** added the left nav rail (section 4b) as the
canonical section-wayfinding component, retuned the surface elevation ladder to
neutral navy-graphite so accents read as accents (`--color-bg` moved
`#14181f` → `#0b1017`; full ladder in tokens.css), and tokenized the frosted-chrome
colors (`--surface-frost`, `--surface-frost-deep`) so no dashboard hardcodes nav/glass
backgrounds. These are resolved rules, not a changelog: build to the values and
components stated above, not to any prior version of this file.

**Rev 2026-07-01 v2 (deep review):** `.brand-zone` now actually ships from
components.css (it was documented here but only existed as the showcase's page
CSS; intensity dial is `--brand-zone-opacity` in tokens.css). Chart.js is
self-hosted at `/vendor/chart.umd.min.js` (section 6; the CDN+SRI form blanks
every canvas behind TLS-inspecting proxies). The `tok()` helper reads
`document.body` so charts follow the CVD remap. Two new clonable starters:
`starter-dataluxe.html` (instrument panel) and `starter-titlecard.html`
(title-card one-pager). Full findings: `HANDOFFS/03-fable-deep-review.md`.
