Brand system + product / 2026
Forge City Circuit
One city. Every season.
A civic sports identity carried from a single forged shield into a 3D crest and a working fan portal for eight neighborhood franchises across three sports.
Explore live project ↗- Project
- Self-initiated concept
- Role
- Creative Direction + Design + Development
- Built with
- JavaScript ES modules · Three.js · HTML · CSS
- Code
- View on GitHub ↗

01 / The brief
A brand that survives a real product.
Forge City Circuit is a fictional amateur league: eight neighborhood franchises playing baseball, basketball and flag football.
The challenge was not to draw a crest. It was to build an identity flexible enough to organize three sports and eight teams across standings, schedules, rosters and playoffs without losing its character.
02 / Identity system
One shield, worn three ways.
- A chamfered steel shield, six rivets and one central field form the fixed grammar.
- Sport icons and accent colors do the variable work: red for baseball, orange for basketball, steel blue for flag football.
- A simplified mark takes over at small sizes, where rivets and fine lines would turn to noise.
- The same silhouette and rivet positions become a 3D crest in Three.js, with drag and arrow-key rotation and a static SVG fallback.
03 / The key decision
No framework for the fan portal.
The portal has five views: home, standings, schedule, bracket and team pages. Each reads from one JSON season snapshot. That is small enough that a framework would add more than it saves.
So the portal is plain JavaScript ES modules and a hash router under 50 lines. There is no build step, the code is easy to follow, and it deploys as static files.
The trade-off: every view manages its own DOM updates. If the data became live and the views grew more interactive, I would move to a component framework.
export function route(pattern, render) {
const segments = pattern.split('/').filter(Boolean);
routes.push({ segments, render });
}
async function dispatch() {
const hash = location.hash.replace(/^#\/?/, '');
const found = match(hash.split('/').filter(Boolean));
window.scrollTo(0, 0);
if (found) await found.render(found.params);
else onNotFound();
}

04 / Data
Every view tells the same story.
Standings, schedules, rosters and brackets all come from one deterministic season snapshot covering eight teams across three sports. A score shown on the home page matches the standings, the schedule and the bracket, because they are all calculated from the same source.


05 / Scope & next steps
Built deliberately. Scoped honestly.
The league, teams, venues, scores and schedules are fictional portfolio content, shown as a fixed snapshot.
There are no automated tests yet.
Next: smoke tests that load every portal route, plus a check that standings, schedules and brackets agree with the source data.
EXPLORE THE WORK