Deux courbes partagées dans lib/motion.ts, pour que le CSS et Framer aient le même feeling, et quatre primitives que Tailwind ne sait pas exprimer : reflet de bouton, halo qui suit le curseur, soulignement qui se dessine, secousse d'erreur. Nouveaux composants : Stagger (les collections se distribuent une par une), Spotlight (trouve son parent tout seul, donc les cartes restent des composants serveur), ScrollProgress, BackToTop, Parallax, Spinner, CheckMark. Reveal accepte maintenant une direction. Côté rendu : le trait de nav glisse d'un onglet à l'autre, les cartes se soulèvent, le hero réagit au scroll, les piliers entrent en zig-zag, la lightbox glisse dans le sens demandé et les formulaires répondent. prefers-reduced-motion est respecté partout : Framer coupe via useReducedMotion, le CSS via la media query déjà en place. Le commit embarque aussi la réorganisation des composants par domaine qui était en cours dans l'arbre de travail — les deux étaient trop imbriquées pour être séparées proprement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+181
@@ -0,0 +1,181 @@
|
||||
// Email templates for the contact form.
|
||||
// Brand-forward but email-client-safe: black header/footer bands (logo + gold,
|
||||
// the site's identity) wrapping a LIGHT body with dark text. Dark text on a
|
||||
// light body is the one combination Gmail dark mode never inverts, so it stays
|
||||
// readable everywhere — unlike a fully dark email, whose light text Gmail flips
|
||||
// to dark and hides.
|
||||
// Table layout + inline styles + bgcolor attributes for client compatibility.
|
||||
|
||||
import { SITE_URL } from "@/lib/site";
|
||||
|
||||
const COLORS = {
|
||||
band: "#0a0a0a", // black brand bands (header/footer) — the site's black
|
||||
page: "#e9e4d8", // warm parchment page background
|
||||
card: "#fbfaf6", // near-white body card
|
||||
border: "#ddd6c6", // warm hairline border
|
||||
rule: "#c4b896", // gold accent rule
|
||||
ink: "#1a1714", // near-black body text
|
||||
inkSoft: "#5a5346", // secondary text
|
||||
accent: "#8a7338", // gold, darkened so it reads on the light body
|
||||
accentOnDark: "#c4b896", // site gold, used on the black bands
|
||||
box: "#f1ece0", // quoted-message background
|
||||
};
|
||||
|
||||
const DISPLAY_FONT = '"Arial Narrow", "Helvetica Neue Condensed", "Helvetica Neue", Arial, sans-serif';
|
||||
const SANS_FONT = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif';
|
||||
|
||||
const STUDIO_NAME = "Highland Games Studio";
|
||||
const TAGLINE = "Where summits become worlds.";
|
||||
// Loaded from the live site — the asset must be deployed there to render.
|
||||
const LOGO_EMAIL_URL = `${SITE_URL}/image/Studio/Full%20Logo.png`;
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function nl2br(value: string): string {
|
||||
return escapeHtml(value).replace(/\n/g, "<br />");
|
||||
}
|
||||
|
||||
type ContactInput = {
|
||||
name: string;
|
||||
email: string;
|
||||
subject: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
// Small gold, wide-tracked eyebrow label — the site's signature accent.
|
||||
function eyebrow(text: string): string {
|
||||
return `<div style="font-family:${DISPLAY_FONT};font-size:12px;letter-spacing:5px;color:${COLORS.accent};text-transform:uppercase;">${text}</div>`;
|
||||
}
|
||||
|
||||
// Big condensed uppercase heading, in near-black ink.
|
||||
function heading(text: string): string {
|
||||
return `<div style="font-family:${DISPLAY_FONT};font-size:36px;line-height:1.05;letter-spacing:1px;color:${COLORS.ink};text-transform:uppercase;margin:10px 0 0;">${text}</div>`;
|
||||
}
|
||||
|
||||
function shell(innerHtml: string): string {
|
||||
return `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="color-scheme" content="light" />
|
||||
<meta name="supported-color-schemes" content="light" />
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:${COLORS.page};">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" bgcolor="${COLORS.page}" style="background:${COLORS.page};padding:32px 12px;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" bgcolor="${COLORS.card}" style="max-width:560px;background:${COLORS.card};border:1px solid ${COLORS.border};">
|
||||
<!-- black logo band -->
|
||||
<tr>
|
||||
<td align="center" bgcolor="${COLORS.band}" style="background:${COLORS.band};padding:36px 32px 30px;">
|
||||
<img src="${LOGO_EMAIL_URL}" alt="${STUDIO_NAME}" width="240" style="display:block;width:240px;max-width:72%;height:auto;border:0;" />
|
||||
</td>
|
||||
</tr>
|
||||
<!-- gold accent rule -->
|
||||
<tr>
|
||||
<td bgcolor="${COLORS.rule}" style="background:${COLORS.rule};height:3px;line-height:3px;font-size:0;"> </td>
|
||||
</tr>
|
||||
<!-- light body -->
|
||||
${innerHtml}
|
||||
<!-- black footer band -->
|
||||
<tr>
|
||||
<td bgcolor="${COLORS.band}" style="background:${COLORS.band};padding:24px 32px;">
|
||||
<div style="font-family:${DISPLAY_FONT};font-size:14px;letter-spacing:3px;color:${COLORS.accentOnDark};text-transform:uppercase;">${STUDIO_NAME}</div>
|
||||
<div style="font-family:${SANS_FONT};font-size:12px;color:#8f867a;margin-top:4px;">${TAGLINE}</div>
|
||||
<div style="font-family:${SANS_FONT};font-size:11px;color:#6f6a5f;margin-top:12px;line-height:1.6;">Sent automatically from the studio website.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
// Sent to the studio staff with the submitted message.
|
||||
export function staffNotificationEmail(input: ContactInput): {
|
||||
subject: string;
|
||||
html: string;
|
||||
text: string;
|
||||
} {
|
||||
const { name, email, subject, message } = input;
|
||||
|
||||
const row = (label: string, value: string) => `
|
||||
<tr>
|
||||
<td style="padding:0 0 14px;">
|
||||
<div style="font-family:${DISPLAY_FONT};font-size:11px;letter-spacing:3px;color:${COLORS.accent};text-transform:uppercase;">${label}</div>
|
||||
<div style="font-family:${SANS_FONT};font-size:15px;color:${COLORS.ink};margin-top:4px;">${value}</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
const inner = `
|
||||
<tr>
|
||||
<td bgcolor="${COLORS.card}" style="background:${COLORS.card};padding:36px 32px;">
|
||||
${eyebrow("New message")}
|
||||
${heading("Someone reached out")}
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin-top:28px;">
|
||||
${row("From", `${escapeHtml(name)} <${escapeHtml(email)}>`)}
|
||||
${row("Subject", escapeHtml(subject))}
|
||||
</table>
|
||||
<div style="font-family:${DISPLAY_FONT};font-size:11px;letter-spacing:3px;color:${COLORS.accent};text-transform:uppercase;margin:8px 0 10px;">Message</div>
|
||||
<div style="font-family:${SANS_FONT};font-size:15px;color:${COLORS.ink};line-height:1.7;background:${COLORS.box};border:1px solid ${COLORS.border};padding:18px;">${nl2br(message)}</div>
|
||||
<!-- reply button (filled black, gold text) -->
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" style="margin-top:28px;">
|
||||
<tr>
|
||||
<td bgcolor="${COLORS.band}" style="background:${COLORS.band};">
|
||||
<a href="mailto:${encodeURIComponent(email)}?subject=${encodeURIComponent("Re: " + subject)}"
|
||||
style="display:inline-block;font-family:${DISPLAY_FONT};font-size:14px;letter-spacing:3px;text-transform:uppercase;color:${COLORS.accentOnDark};text-decoration:none;padding:14px 30px;">
|
||||
Reply to ${escapeHtml(name)} →
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
return {
|
||||
subject: `[Contact] ${subject}`,
|
||||
html: shell(inner),
|
||||
text: `New message for ${STUDIO_NAME}\n\nFrom: ${name} <${email}>\nSubject: ${subject}\n\n${message}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Auto-reply sent to the person who submitted the form.
|
||||
export function autoReplyEmail(input: ContactInput): {
|
||||
subject: string;
|
||||
html: string;
|
||||
text: string;
|
||||
} {
|
||||
const { name, message } = input;
|
||||
const firstName = name.split(/\s+/)[0] || name;
|
||||
|
||||
const inner = `
|
||||
<tr>
|
||||
<td bgcolor="${COLORS.card}" style="background:${COLORS.card};padding:36px 32px;">
|
||||
${eyebrow("Message received")}
|
||||
${heading("Thanks for<br />reaching out")}
|
||||
<div style="font-family:${SANS_FONT};font-size:15px;color:${COLORS.ink};line-height:1.8;margin-top:26px;">
|
||||
<p style="margin:0 0 16px;">Hi ${escapeHtml(firstName)},</p>
|
||||
<p style="margin:0 0 16px;">Thanks for getting in touch with ${STUDIO_NAME}. We've received your message and a real human will get back to you soon.</p>
|
||||
<p style="margin:0 0 10px;color:${COLORS.inkSoft};font-size:13px;">For reference, here's what you sent us:</p>
|
||||
</div>
|
||||
<div style="font-family:${SANS_FONT};font-size:14px;color:${COLORS.inkSoft};line-height:1.7;background:${COLORS.box};border:1px solid ${COLORS.border};padding:18px;">${nl2br(message)}</div>
|
||||
<div style="font-family:${DISPLAY_FONT};font-size:18px;letter-spacing:2px;color:${COLORS.accent};text-transform:uppercase;margin-top:30px;">The ${STUDIO_NAME} team</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
return {
|
||||
subject: `We got your message`,
|
||||
html: shell(inner),
|
||||
text: `Hi ${firstName},\n\nThanks for getting in touch with ${STUDIO_NAME}. We've received your message and a real human will get back to you soon.\n\nFor reference, here's what you sent us:\n\n${message}\n\nThe ${STUDIO_NAME} team`,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Formats an ISO date (`YYYY-MM-DD`) for display.
|
||||
* Forced to UTC so a post dated 2026-05-09 never renders as May 8 for
|
||||
* visitors west of Greenwich.
|
||||
*/
|
||||
export function formatDate(date: string): string {
|
||||
return new Date(date).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
timeZone: "UTC",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Rough reading time in minutes, from raw MDX.
|
||||
*
|
||||
* Frontmatter is already stripped by `lib/devblog`, but markdown syntax is
|
||||
* not: fences, link targets and heading markers are dropped so a post is not
|
||||
* credited for characters nobody reads. 200 wpm, floored at one minute.
|
||||
*/
|
||||
export function readingTime(content: string): number {
|
||||
const words = content
|
||||
.replace(/```[\s\S]*?```/g, "")
|
||||
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
|
||||
.replace(/[#>*_`~-]/g, " ")
|
||||
.split(/\s+/)
|
||||
.filter(Boolean).length;
|
||||
|
||||
return Math.max(1, Math.round(words / 200));
|
||||
}
|
||||
+108
-9
@@ -1,16 +1,71 @@
|
||||
export type GameStatus = "in-development" | "released" | "coming-soon";
|
||||
|
||||
export type Screenshot = {
|
||||
src: string;
|
||||
/** Short caption. Also used as the image alt text. */
|
||||
caption: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A gameplay pillar: what the player actually *does*.
|
||||
*
|
||||
* Copy rules, borrowed from the EmberWild site so both properties describe the
|
||||
* game the same way:
|
||||
* 1. No story. No lore, no factions, no twists. A visitor should arrive
|
||||
* knowing what they will do, not what they will find out.
|
||||
* 2. The title is one word. The body is one sentence.
|
||||
* 3. If the screenshot already says it, do not write it.
|
||||
*/
|
||||
export type Pillar = {
|
||||
title: string;
|
||||
body: string;
|
||||
/** The capture that illustrates it. */
|
||||
shot: string;
|
||||
};
|
||||
|
||||
export type Game = {
|
||||
slug: string;
|
||||
title: string;
|
||||
status: "in-development" | "released" | "coming-soon";
|
||||
status: GameStatus;
|
||||
/** One readable line. Shown under the title on cards and on the game page. */
|
||||
tagline: string;
|
||||
/** Full pitch. Shown in the "About" block of the game page. */
|
||||
description: string;
|
||||
genres: string[];
|
||||
releaseWindow?: string;
|
||||
steamUrl?: string;
|
||||
// Image de couverture affichée sur les cards (home, liste, page projet).
|
||||
// Dépose l'image dans /public/image/Emberwild/ puis renseigne le chemin,
|
||||
// ex: "/image/Emberwild/cover.jpg". Laisse vide pour un placeholder.
|
||||
cover?: string;
|
||||
/** Player count, e.g. "1-6". Drives the co-op block on the game page. */
|
||||
players?: string;
|
||||
platform?: string;
|
||||
|
||||
/** What the player does, one band each on the game page. */
|
||||
pillars: Pillar[];
|
||||
|
||||
/**
|
||||
* Illustrated key art / cover (portrait or square-ish artwork).
|
||||
* Rendered on its own panel — never used as a background behind text,
|
||||
* because the artwork is light and text would become unreadable.
|
||||
*/
|
||||
keyArt?: string;
|
||||
|
||||
/**
|
||||
* Wide in-game capture used as the page banner. Prefer a 16:9 screenshot.
|
||||
* Text is never laid over it either; the title block sits underneath.
|
||||
*/
|
||||
banner?: string;
|
||||
|
||||
/**
|
||||
* Gallery. This is the ONLY place screenshots are declared: the game page
|
||||
* shows them all, the home page shows the first few of the featured game.
|
||||
* Drop new files in /public/image/Games/<Game>/ and add them here.
|
||||
*/
|
||||
screenshots: Screenshot[];
|
||||
};
|
||||
|
||||
export const STATUS_LABEL: Record<GameStatus, string> = {
|
||||
"in-development": "IN DEVELOPMENT",
|
||||
released: "RELEASED",
|
||||
"coming-soon": "COMING SOON",
|
||||
};
|
||||
|
||||
export const games: Game[] = [
|
||||
@@ -18,15 +73,59 @@ export const games: Game[] = [
|
||||
slug: "emberwild",
|
||||
title: "Emberwild",
|
||||
status: "in-development",
|
||||
tagline: "A survival adventure across shifting horizons.",
|
||||
tagline: "Co-op survival in a world gone wild.",
|
||||
description:
|
||||
"Inspired by Raft and Aloft. Build, explore, and survive across drifting worlds. A new survival adventure currently in early development. The first world from Highland Games Studio.",
|
||||
genres: ["Survival", "Crafting", "Exploration"],
|
||||
"Emberwild is a 1-6 player co-op survival craft game. Drive the blight out of a handcrafted world, farm and build on the ground you take back, and watch the colour return with you. Five regions, nothing generated, nothing repeated.",
|
||||
genres: ["Survival", "Crafting", "Co-op", "Exploration"],
|
||||
releaseWindow: "TBA",
|
||||
cover: "/image/Games/Emberwild/Cover.png",
|
||||
players: "1-6",
|
||||
platform: "PC",
|
||||
keyArt: "/image/Games/Emberwild/Cover.png",
|
||||
banner: "/image/Games/Emberwild/Capture_1.png",
|
||||
// Only three captures exist so far, so "Build" borrows the key art. Swap
|
||||
// it for a real in-game shot of a base as soon as there is one.
|
||||
pillars: [
|
||||
{
|
||||
title: "Restore",
|
||||
body: "Drive the blight out and the colour comes back with it. What you clear stays clear.",
|
||||
shot: "/image/Games/Emberwild/Capture_1.png",
|
||||
},
|
||||
{
|
||||
title: "Survive",
|
||||
body: "Farm, cook, fish, hunt. Clean ground grows more and sleeps quieter.",
|
||||
shot: "/image/Games/Emberwild/Capture_2.png",
|
||||
},
|
||||
{
|
||||
title: "Explore",
|
||||
body: "One handcrafted world, five regions. Nothing generated, nothing repeated.",
|
||||
shot: "/image/Games/Emberwild/Capture_3.png",
|
||||
},
|
||||
{
|
||||
title: "Build",
|
||||
body: "Raise a home. Push outposts into everything you take back.",
|
||||
shot: "/image/Games/Emberwild/Cover.png",
|
||||
},
|
||||
],
|
||||
screenshots: [
|
||||
{
|
||||
src: "/image/Games/Emberwild/Capture_1.png",
|
||||
caption: "First light over the clearing",
|
||||
},
|
||||
{
|
||||
src: "/image/Games/Emberwild/Capture_2.png",
|
||||
caption: "Autumn treeline",
|
||||
},
|
||||
{
|
||||
src: "/image/Games/Emberwild/Capture_3.png",
|
||||
caption: "The path through the meadow",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/** The game showcased on the home page. */
|
||||
export const featuredGame: Game | undefined = games[0];
|
||||
|
||||
export function getGame(slug: string): Game | undefined {
|
||||
return games.find((g) => g.slug === slug);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Shared motion tokens.
|
||||
*
|
||||
* The same two curves as `--ease-out-expo` / `--ease-out-soft` in
|
||||
* `globals.css`, in the tuple form Framer Motion expects. Keeping them here
|
||||
* means a CSS hover and a Framer entrance land on the same feel instead of
|
||||
* drifting apart file by file.
|
||||
*/
|
||||
export const EASE_OUT_EXPO = [0.22, 1, 0.36, 1] as const satisfies [
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
];
|
||||
|
||||
export const EASE_OUT_SOFT = [0.33, 1, 0.68, 1] as const satisfies [
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
];
|
||||
|
||||
/** How far a scroll-triggered element has to be inside the viewport to fire. */
|
||||
export const VIEWPORT = { once: true, margin: "-80px" } as const;
|
||||
+83
-3
@@ -1,6 +1,86 @@
|
||||
// Single source of truth for the public site URL.
|
||||
// Override per environment with NEXT_PUBLIC_SITE_URL; falls back to production.
|
||||
// Single source of truth for everything "studio-level":
|
||||
// URL, identity, navigation, contact channels.
|
||||
// Pages must never hard-code these values — import them from here.
|
||||
|
||||
export const SITE_URL =
|
||||
process.env.NEXT_PUBLIC_SITE_URL ?? "https://highlandgamesstudio.com";
|
||||
|
||||
export const LOGO_URL = `${SITE_URL}/image/Studio/Logo Flat.png`;
|
||||
/** Studio identity. Reused in metadata, JSON-LD, footer, hero. */
|
||||
export const STUDIO = {
|
||||
name: "Highland Games Studio",
|
||||
shortName: "Highland Games",
|
||||
tagline: "Where summits become worlds.",
|
||||
description:
|
||||
"Indie game studio crafting survival adventures. Where summits become worlds.",
|
||||
foundingDate: "2026",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Brand assets, with their intrinsic sizes so every `<Image>` keeps the
|
||||
* correct aspect ratio (see `components/ui/Logo.tsx`).
|
||||
*
|
||||
* `Full Logo.png` is not referenced directly: it ships with an opaque black
|
||||
* plate that would show as a rectangle over the #0a0a0a background.
|
||||
* `Wordmark.png` is the transparent version, rebuilt by
|
||||
* `node scripts/build-wordmark.mjs`.
|
||||
*/
|
||||
export const BRAND = {
|
||||
/** Mountain mark alone, transparent. */
|
||||
mark: { src: "/image/Studio/Logo Flat.png", width: 1344, height: 768 },
|
||||
/** Mountain mark + "HIGHLAND GAMES STUDIO" lockup, transparent. */
|
||||
wordmark: { src: "/image/Studio/Wordmark.png", width: 986, height: 407 },
|
||||
} as const;
|
||||
|
||||
export const LOGO_URL = `${SITE_URL}${BRAND.mark.src}`;
|
||||
|
||||
/** Hero background video. `poster` stays null until a real still is exported. */
|
||||
export const HERO_VIDEO = {
|
||||
src: "/Video/Header_Video.mp4",
|
||||
poster: null as string | null,
|
||||
};
|
||||
|
||||
/** Primary navigation — shared by the navbar and the footer. */
|
||||
export const NAV_LINKS = [
|
||||
{ href: "/studio", label: "Studio" },
|
||||
{ href: "/games", label: "Games" },
|
||||
{ href: "/devblog", label: "Devblog" },
|
||||
{ href: "/contact", label: "Contact" },
|
||||
] as const;
|
||||
|
||||
export const LEGAL_LINKS = [
|
||||
{ href: "/legal/notice", label: "Legal Notice" },
|
||||
{ href: "/legal/privacy", label: "Privacy" },
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Social accounts. Set `href` to null while an account does not exist yet —
|
||||
* the footer hides those entries instead of rendering a dead `#` link.
|
||||
*/
|
||||
export const SOCIAL_LINKS: {
|
||||
key: "x" | "discord" | "youtube" | "steam";
|
||||
label: string;
|
||||
href: string | null;
|
||||
}[] = [
|
||||
{ key: "x", label: "Twitter / X", href: null },
|
||||
{ key: "discord", label: "Discord", href: null },
|
||||
{ key: "youtube", label: "YouTube", href: null },
|
||||
{ key: "steam", label: "Steam", href: null },
|
||||
];
|
||||
|
||||
export const CONTACT_CHANNELS = [
|
||||
{
|
||||
label: "GENERAL",
|
||||
description: "Questions, feedback, or just to say hello.",
|
||||
email: "hello@highlandgamesstudio.com",
|
||||
},
|
||||
{
|
||||
label: "PRESS",
|
||||
description: "Review keys, interviews and press assets.",
|
||||
email: "press@highlandgamesstudio.com",
|
||||
},
|
||||
{
|
||||
label: "BUSINESS",
|
||||
description: "Publishing, partnerships and collaborations.",
|
||||
email: "business@highlandgamesstudio.com",
|
||||
},
|
||||
] as const;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// The people behind the studio.
|
||||
//
|
||||
// Identity (name, tagline, links, brand assets) lives in `lib/site.ts` —
|
||||
// this file is only the roster. It sits here rather than inside
|
||||
// /studio/page.tsx because the home page counts it too: the "3 people" figure
|
||||
// on the home page and the team grid on /studio can never disagree.
|
||||
|
||||
export type Member = {
|
||||
name: string;
|
||||
role: string;
|
||||
bio: string;
|
||||
/** null until a real photo is shot — the UI falls back to the logo mark. */
|
||||
photo: string | null;
|
||||
};
|
||||
|
||||
export const TEAM: Member[] = [
|
||||
{
|
||||
name: "Mathew Simon",
|
||||
role: "CEO · Developer",
|
||||
bio: "Builds the systems, gameplay loops and tools that bring our worlds to life.",
|
||||
photo: "/image/Studio/Teams/CEO_Dev.jpg",
|
||||
},
|
||||
{
|
||||
name: "Kevin Tostivin",
|
||||
role: "3D Artist · Art Director",
|
||||
bio: "Shapes the visual identity, environments and overall look of every Highland Games world.",
|
||||
photo: "/image/Studio/Teams/Kevin.jpeg",
|
||||
},
|
||||
{
|
||||
name: "Axel",
|
||||
role: "3D Animator",
|
||||
bio: "Brings characters and creatures to life, giving motion and feel to everything on screen.",
|
||||
photo: "/image/Studio/Teams/Axel.jpg",
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user