Skip to main content
Skip to content

Last updated on 4 June 2026

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…EditNotes
The ember-orange accentglobals.css --p-accent-400/500/600/700 in :root4 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 + surfacesglobals.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-* tokensDrives every badge, alert, chart severity colour.
Border radiusglobals.css --radius-* tokensSingle source for card / button / input corners.
Motion timing + easingglobals.css --duration-* / --ease-*Used by Tailwind utilities + inline styles.
Fontssrc/app/layout.tsx (import) + globals.css @theme inline (binding)See "Typography" below.
Brand name / emails / social / legalsrc/lib/site-config.tsSITE_CONFIG.name, .email.*, .legalEntity, .api.*, etc.
Brand mark / logo glyphsrc/components/layout/site-header.tsx, src/components/layout/site-footer.tsx, src/components/layout/dashboard-sidebar.tsxReplace 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:

FileWhy it's a raw literal
src/lib/tokens.tsJS 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.tsxRuns 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.tsxDynamic 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.tsxWebGL shader uniforms — GPU paints happen outside the CSS tree. Defaults come from BRAND.ember.* in src/lib/tokens.ts.
src/components/ui/radar-bg.tsxSame — WebGL canvas uniforms need hex fallbacks from src/lib/tokens.ts when CSS variables are unavailable.
src/components/ui/ambient-glow.tsxSame — 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.

  1. Site-wide stringssrc/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.
  2. Accent coloursrc/app/globals.css --p-accent-400/500/600/700 in :root (4 OKLCH primitive values). Light-mode semantic tokens already map to those primitives. See "Theme structure" above for the cascade.
  3. Display font — swap the JetBrains_Mono import in src/app/layout.tsx (see "Typography" below).
  4. Integration logossrc/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 in src/components/sections/hero-section.tsx immediately above the <IntegrationLogos /> call neutral - do not switch to wording that implies endorsement.
  5. Dashboard demo personaalex@example.com / "Alex Chen" appear in dashboard-header-actions.tsx, dashboard/settings/page.tsx, dashboard/team/page.tsx, dashboard/audit/page.tsx. Rename centrally.
  6. Demo-marked sections — grep the repo for DEMO_CONTENT and [CUSTOMISE:. Every match is a file you should sanitise before launch (copy, metrics, bios, testimonials, notification bodies, code samples).
  7. Brand-coupled CLI / SDK stringsSITE_CONFIG.api.sdkPackage, cliCommand, pythonPackage, goPackage, and rustCrate drive the hero CLI, docs snippets, SDK examples, and dashboard CLI page. Keep cliCommand identifier-safe for code samples, then run rg "@forge/|\\bforge\\b|forge-cli" src CUSTOMIZATION.md DEMO_CONTENT.md to catch any buyer-specific demo copy you still need to rewrite.
  8. Legal pagessrc/app/terms/page.tsx, src/app/privacy/page.tsx, src/app/cookie-policy/page.tsx reference SITE_CONFIG.name / .legalEntity. Review the sample copy for your jurisdiction. The cookie policy names third-party vendors via SITE_CONFIG.thirdPartyServices.payment and .monitoring — replace these with the actual vendors you use before publishing (shipping another company's name in a legal template is a liability risk).
  9. Exception files — see the "Documented exceptions" table above. src/lib/tokens.ts carries the raw hex mirror consumed by global-error.tsx, WebGL effects, and the three dynamic-OG files; sync it with your new --p-accent-* values.
  10. Licence validation URLscripts/validate-licence.mjs points at https://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.
  11. Compliance config (Gate 17)src/lib/site-config.ts: fill SITE_CONFIG.legalEntity.* (Companies Act §82 — companyName, companyNumber, placeOfRegistration, registeredAddress, jurisdiction, vatNumber, regulator + regulatorNumber if regulated). Set SITE_CONFIG.regions.{uk,eu,us} to the regions you actually serve. Toggle SITE_CONFIG.compliance.ccpa.doNotSell = true only if you sell or share PII for cross-context behavioural ads. Toggle SITE_CONFIG.compliance.ccpa.sensitivePII = true only if you collect sensitive PI categories. Set SITE_CONFIG.compliance.dmca.{agentName,agentEmail,agentAddress} (and register the agent with the US Copyright Office) only if you publish user-uploaded content. Set SITE_CONFIG.compliance.modernSlaveryStatementUrl if 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:

css
/* 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:

css
--p-accent-500: oklch(0.62 0.20 280);   /* violet */

Use oklch.com to pick values visually.

Background shades

css
--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:

css
--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

RoleFontVariable (layout)@theme inline binding
Body text / sansGeist--font-geist-sans--font-sans
Code / monoJetBrains Mono--font-jetbrains-mono--font-mono
Display headingsJetBrains 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

  1. Place your .woff2 files in public/fonts/.
  2. Update src/app/layout.tsx while keeping the CSS variable names stable:
tsx
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:

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:

tsx
<h1 style={{ fontSize: "var(--text-step-4)" }}>Heading</h1>

Animation Settings

Adjust scroll animation timing

Motion tokens are in globals.css:

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:

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:

tsx
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:

  1. Wrap any transient success / error / loading indicator in a region with role="status" + aria-live="polite" (or role="alert" + aria-live="assertive" for errors) so screen-reader users hear the outcome.
  2. Keep the visible indicator mounted — don't toggle it in and out of the DOM — otherwise AT users miss the announcement.
  3. 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/.