Customisation Guide — UI02 Forge Developer Platform
Demo content is tracked separately. See DEMO_CONTENT.md for the canonical inventory of files containing buyer-replaceable copy, and the grep commands to enumerate every demo marker before publishing.
Theme structure — the one-file promise
99% of visual customisation happens in one file: src/app/globals.css. The
three-tier OKLCH token system below is the single source of truth for every
colour, spacing, motion, and font binding in the kit.
Tier 1 — Primitives Tier 2 — Semantic aliases Tier 3 — Component tokens
( --p-*, raw OKLCH ) → ( --brand-accent, --brand- → ( --bg-deep, --bg-surface,
glow, --status-success, --text-primary, --border-
--status-error, … ) primary, … )
Primitives are the source of truth for brand hue and surface families. Most
semantic aliases derive from primitives via color-mix(in oklch, var(--p-accent-500) X%, transparent) or direct var() reference. A few
non-brand tokens intentionally keep raw OKLCH alpha values for neutral borders,
overlays, syntax colours, and light-mode contrast overrides; keep those in this
file and treat src/lib/tokens.ts as the JavaScript mirror for non-CSS contexts.
Where to edit
| You want to change… | Edit | Notes |
|---|---|---|
| The ember-orange accent | globals.css --p-accent-400/500/600/700 in :root | 4 OKLCH primitive values. Light mode reuses those primitives through semantic aliases such as --brand-accent, --brand-accent-hover, and --brand-accent-text; override primitives in html.light only if you need a separate light-mode hue. |
| Light/dark backgrounds + surfaces | globals.css --p-ink-* (:root) / matching html.light overrides | --p-ink-1000 is the page background, 950/900 are surfaces / elevated panels. |
| Status colours (success, warning, error, info) | globals.css --status-* tokens | Drives every badge, alert, chart severity colour. |
| Border radius | globals.css --radius-* tokens | Single source for card / button / input corners. |
| Motion timing + easing | globals.css --duration-* / --ease-* | Used by Tailwind utilities + inline styles. |
| Fonts | src/app/layout.tsx (import) + globals.css @theme inline (binding) | See "Typography" below. |
| Brand name / emails / social / legal | src/lib/site-config.ts | SITE_CONFIG.name, .email.*, .legalEntity, .api.*, etc. |
| Brand mark / logo glyph | src/components/layout/site-header.tsx, src/components/layout/site-footer.tsx, src/components/layout/dashboard-sidebar.tsx | Replace the visible Terminal icon with your own mark in the marketing header/footer and dashboard shell. |
Documented exceptions (raw hex allowed)
CSS custom properties can't be resolved in a handful of execution contexts. These files intentionally hold literal hex values and must be updated by hand if you retheme the accent:
| File | Why it's a raw literal |
|---|---|
src/lib/tokens.ts | JS mirror of the CSS palette - consumed by Three.js, WebGL shaders, and build-time image/icon generators that cannot read CSS variables. Edit globals.css first, then sync the matching hex values here. Drift between the two is the most common rebrand footgun. |
src/app/global-error.tsx | Runs when the root layout crashes — globals.css has not loaded, so it consumes BRAND, SURFACE, and GLOBAL_ERROR from src/lib/tokens.ts. |
src/app/icon.tsx, src/app/apple-icon.tsx, src/app/opengraph-image.tsx | Dynamic build-time icon / OG-image generation via next/og. These files consume BRAND and SURFACE values from src/lib/tokens.ts; those token values are baked into generated image assets at build time because there is no CSS context. |
src/components/ui/magic-rings.tsx | WebGL shader uniforms — GPU paints happen outside the CSS tree. Defaults come from BRAND.ember.* in src/lib/tokens.ts. |
src/components/ui/radar-bg.tsx | Same — WebGL canvas uniforms need hex fallbacks from src/lib/tokens.ts when CSS variables are unavailable. |
src/components/ui/ambient-glow.tsx | Same — consumes WEBGL_RGB from src/lib/tokens.ts for linear-RGB shader fallbacks; CSS variables are not resolvable in shader uniforms. |
Assets generated at build time
The three icon.tsx / apple-icon.tsx / opengraph-image.tsx files use
next/og + export const dynamic = "force-static" so they're baked into
out/icon, out/apple-icon, and out/opengraph-image during pnpm build.
If you want hand-designed PNGs instead: delete the TSX file and drop a PNG
into public/ under the equivalent path (public/favicon.ico takes
precedence over app/icon.tsx; public/apple-icon.png over
app/apple-icon.tsx; for OG, set metadata.openGraph.images explicitly in
layout.tsx).
Rebrand Checklist
Realistic time: 45–90 minutes for a thorough pass. The items below
are ordered by "biggest visual change per minute of effort" — you can ship
a demo-grade rebrand in ~30 minutes if you stop at step 5. Shipping to real
buyers means every item, plus a grep pass for the literals called out in
DEMO_CONTENT.md.
- Site-wide strings —
src/lib/site-config.ts:name,legalEntity,tagline,footerBlurb,url,metadata.*,api.*,contact.*,social.*. Every header/footer string, legal page, and most docs copy flows from here. - Accent colour —
src/app/globals.css--p-accent-400/500/600/700in:root(4 OKLCH primitive values). Light-mode semantic tokens already map to those primitives. See "Theme structure" above for the cascade. - Display font — swap the
JetBrains_Monoimport insrc/app/layout.tsx(see "Typography" below). - Integration logos —
src/components/sections/integration-logos.tsx. Kit ships neutral "Partner 01...10" placeholder glyphs. Replace only with logos you have rights to display, and keep the heading insrc/components/sections/hero-section.tsximmediately above the<IntegrationLogos />call neutral - do not switch to wording that implies endorsement. - Dashboard demo persona —
alex@example.com/ "Alex Chen" appear indashboard-header-actions.tsx,dashboard/settings/page.tsx,dashboard/team/page.tsx,dashboard/audit/page.tsx. Rename centrally. - Demo-marked sections — grep the repo for
DEMO_CONTENTand[CUSTOMISE:. Every match is a file you should sanitise before launch (copy, metrics, bios, testimonials, notification bodies, code samples). - Brand-coupled CLI / SDK strings —
SITE_CONFIG.api.sdkPackage,cliCommand,pythonPackage,goPackage, andrustCratedrive the hero CLI, docs snippets, SDK examples, and dashboard CLI page. KeepcliCommandidentifier-safe for code samples, then runrg "@forge/|\\bforge\\b|forge-cli" src CUSTOMIZATION.md DEMO_CONTENT.mdto catch any buyer-specific demo copy you still need to rewrite. - Legal pages —
src/app/terms/page.tsx,src/app/privacy/page.tsx,src/app/cookie-policy/page.tsxreferenceSITE_CONFIG.name/.legalEntity. Review the sample copy for your jurisdiction. The cookie policy names third-party vendors viaSITE_CONFIG.thirdPartyServices.paymentand.monitoring— replace these with the actual vendors you use before publishing (shipping another company's name in a legal template is a liability risk). - Exception files — see the "Documented exceptions" table above.
src/lib/tokens.tscarries the raw hex mirror consumed byglobal-error.tsx, WebGL effects, and the three dynamic-OG files; sync it with your new--p-accent-*values. - Licence validation URL —
scripts/validate-licence.mjspoints athttps://templateempire.io/api/validate-licence. Under the standard licence, do not remove licence key verification where present. White-label/reseller agreements may authorise a different validation domain; align that with your contract before changing the script. - Compliance config (Gate 17) —
src/lib/site-config.ts: fillSITE_CONFIG.legalEntity.*(Companies Act §82 — companyName, companyNumber, placeOfRegistration, registeredAddress, jurisdiction, vatNumber, regulator + regulatorNumber if regulated). SetSITE_CONFIG.regions.{uk,eu,us}to the regions you actually serve. ToggleSITE_CONFIG.compliance.ccpa.doNotSell = trueonly if you sell or share PII for cross-context behavioural ads. ToggleSITE_CONFIG.compliance.ccpa.sensitivePII = trueonly if you collect sensitive PI categories. SetSITE_CONFIG.compliance.dmca.{agentName,agentEmail,agentAddress}(and register the agent with the US Copyright Office) only if you publish user-uploaded content. SetSITE_CONFIG.compliance.modernSlaveryStatementUrlif turnover ≥ £36M. See COMPLIANCE.md for the full breakdown.
Brand Colours
Changing accent tokens updates most branded UI. Some effect components use hardcoded values — see the Documented exceptions section above.
Most colours use the OKLCH colour space (three-tier token system). Edit
src/app/globals.css.
Changing the accent colour
The ember orange brand is defined at the primitive tier. Update these four values to rebrand most surfaces:
/* src/app/globals.css — :root */
--p-accent-400: oklch(0.72 0.17 30); /* hover / light accent */
--p-accent-500: oklch(0.62 0.17 30); /* BASE — buttons, links, glow */
--p-accent-600: oklch(0.52 0.15 30); /* active / strong */
--p-accent-700: oklch(0.42 0.13 30); /* dark variant */
For a violet brand, replace 30 (hue) with 280 and adjust chroma:
--p-accent-500: oklch(0.62 0.20 280); /* violet */
Use oklch.com to pick values visually.
Background shades
--p-ink-1000: oklch(0.065 0.003 15); /* page background — warm near-black */
--p-ink-950: oklch(0.085 0.008 15); /* card / surface */
--p-ink-900: oklch(0.110 0.010 18); /* elevated panels */
To add a subtle colour tint (e.g. slight blue-black), increase chroma:
--p-ink-1000: oklch(0.065 0.006 260); /* very slightly cool-tinted black */
Typography
Fonts are self-hosted by default. src/app/layout.tsx uses next/font/local and loads the bundled files in public/fonts/, so pnpm build does not need Google Fonts or any other external font endpoint.
Role -> font -> CSS variable
| Role | Font | Variable (layout) | @theme inline binding |
|---|---|---|---|
| Body text / sans | Geist | --font-geist-sans | --font-sans |
| Code / mono | JetBrains Mono | --font-jetbrains-mono | --font-mono |
| Display headings | JetBrains Mono (same font) | --font-jetbrains-mono | --font-display |
JetBrains Mono fills both the mono and display slots in this kit - the monospaced display lends the DevTools aesthetic. The bundled font files are Latin variable .woff2 files from Fontsource and keep the default build clean-room friendly.
Option A: Replace the bundled self-hosted fonts
- Place your
.woff2files inpublic/fonts/. - Update
src/app/layout.tsxwhile keeping the CSS variable names stable:
import localFont from "next/font/local";
const geistSans = localFont({
src: "../../public/fonts/geist-latin-variable.woff2",
variable: "--font-geist-sans",
display: "swap",
weight: "100 900",
});
const jetbrainsMono = localFont({
src: "../../public/fonts/jetbrains-mono-latin-variable.woff2",
variable: "--font-jetbrains-mono",
display: "swap",
weight: "100 800",
});
Use different self-hosted fonts
Keep release builds on next/font/local so clean-room installs do not depend on build-time font metadata downloads. Swap the files in public/fonts/, then keep the variable: names the same or update the matching @theme inline binding in globals.css. Every .font-display, font-mono, and the default font-sans already reads from the CSS variables.
Make the display font the sans instead of the mono
If you want headlines to use the sans face rather than the monospaced one,
update globals.css:
/* src/app/globals.css — @theme inline block */
--font-display: var(--font-geist-sans); /* was var(--font-jetbrains-mono) */
Fluid type scale
The type scale uses clamp() in globals.css. Apply via:
<h1 style={{ fontSize: "var(--text-step-4)" }}>Heading</h1>
Animation Settings
Adjust scroll animation timing
Motion tokens are in globals.css:
--duration-normal: 300ms; /* default transition speed */
--duration-slow: 400ms; /* slower UI transitions */
--ease-default: cubic-bezier(0.16, 1, 0.3, 1);
Lenis smooth scroll speed
Edit src/components/providers/lenis-provider.tsx:
const lenis = new Lenis({
duration: 1.2, // lower = faster scroll (0.8 is snappier)
smoothWheel: true,
});
Disable animations entirely
Remove <LenisProvider> from src/components/providers.tsx and remove
motion.* props from components. The prefers-reduced-motion: reduce media
query in globals.css will suppress CSS animations automatically.
Dark / Light Mode
The template ships dark-first. The light class on <html> activates
the light palette.
globals.css contains @custom-variant dark (&:where(html:not(.light) *))
so Tailwind v4's dark: prefix tracks the explicit .light toggle rather
than only the OS prefers-color-scheme media query. Use dark: utilities
normally (e.g. dark:bg-card applies in dark mode and drops in light mode).
To toggle programmatically:
function toggleTheme() {
const isLight = document.documentElement.classList.toggle("light");
localStorage.setItem("te-theme", isLight ? "light" : "dark");
}
The inline theme script in src/app/layout.tsx applies the persisted preference
before React hydrates so there's no FOUC flash.
metadataBase
layout.tsx sets metadataBase: new URL(SITE_CONFIG.url). Keep
SITE_CONFIG.url pointed at your production origin — Next.js resolves
openGraph.images + twitter.images against this, and without a correct
value the exported HTML hardcodes http://localhost:3000/... into every
page's <meta> tags. Social previews would render localhost links in
production.
Wiring Dashboard Forms
The dashboard forms (src/app/dashboard/settings/page.tsx,
…/webhooks/page.tsx, …/api-keys/page.tsx) ship as demo-state forms
— they mutate local React state on submit but don't POST anywhere. The
contact form on /contact is the one live example of a wired success state
(it uses role="status" + aria-live="polite" on the confirmation
message).
When you replace the demo handlers with real network submissions, follow the contact-form pattern:
- Wrap any transient success / error / loading indicator in a region with
role="status"+aria-live="polite"(orrole="alert"+aria-live="assertive"for errors) so screen-reader users hear the outcome. - Keep the visible indicator mounted — don't toggle it in and out of the DOM — otherwise AT users miss the announcement.
- Clear the indicator after a timeout (the contact form uses 3 s) so it doesn't re-announce on the next submit.
Grep for aria-live in src/components/sections/contact-form.tsx to see
the exact wiring.
Adding Pages
All pages use the App Router under src/app/. Create a new folder with a
page.tsx:
src/app/changelog/page.tsx
The static export will generate out/changelog/index.html automatically.
Removing Unused Pages
Delete the folder from src/app/. The build will exclude it automatically.
If you remove the dashboard entirely, also remove the dashboard nav links
from src/components/layout/.