Step 29.2: UI kit in the Deep Field look

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
fireshaper 2026-11-04 17:58:12 -05:00
parent 9ce2ae36b7
commit 47ea4987b5
34 changed files with 192 additions and 255 deletions

View File

@ -64,11 +64,7 @@ export function App() {
const retry = () => void state.refetch(); const retry = () => void state.refetch();
if (state.isPending) { if (state.isPending) {
return ( return <p className="grid min-h-dvh place-items-center font-display text-ink-400">Loading…</p>;
<p className="grid min-h-dvh place-items-center font-display tracking-[0.3em] text-ink-400 uppercase">
Loading…
</p>
);
} }
if (state.data === undefined) { if (state.data === undefined) {
// The first load failed: nothing to show yet. // The first load failed: nothing to show yet.

View File

@ -84,11 +84,7 @@ export function GameRoot({ game, children }: { game: Game; children: ReactNode }
}; };
if (status === "starting") { if (status === "starting") {
return ( return <p className="grid min-h-dvh place-items-center font-display text-ink-400">Loading…</p>;
<p className="grid min-h-dvh place-items-center font-display tracking-[0.3em] text-ink-400 uppercase">
Loading…
</p>
);
} }
if (status === "elsewhere") { if (status === "elsewhere") {
return ( return (

View File

@ -356,9 +356,7 @@ function Totals({ summary }: { summary: Summary }) {
> >
{figures.map(([label, value, tone]) => ( {figures.map(([label, value, tone]) => (
<div key={label}> <div key={label}>
<dt className="font-display text-[10px] tracking-wider text-ink-400 uppercase"> <dt className="font-display text-xs text-ink-400">{label}</dt>
{label}
</dt>
<dd className={`font-display text-base font-bold tabular-nums ${tone}`}>{value}</dd> <dd className={`font-display text-base font-bold tabular-nums ${tone}`}>{value}</dd>
</div> </div>
))} ))}
@ -382,7 +380,7 @@ function PilotLines({ summary }: { summary: Summary }) {
})} })}
</span> </span>
{line.leveledUp && ( {line.leveledUp && (
<span className="shrink-0 rounded-full bg-amber-500/20 px-1.5 text-[10px] font-semibold text-amber-200 uppercase"> <span className="shrink-0 rounded-full bg-amber-500/20 px-1.5 text-xs font-semibold text-amber-200">
{words.leveledUp} {words.leveledUp}
</span> </span>
)} )}

View File

@ -57,26 +57,22 @@ export function DealModal() {
</p> </p>
</div> </div>
<dl className="mt-4 grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 rounded-xl bg-space-950/60 p-3 text-sm ring-1 ring-amber-400/25"> <dl className="mt-4 grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 rounded-xl bg-space-950/60 p-3 text-sm ring-1 ring-amber-400/25">
<dt className="font-display text-[11px] tracking-[0.14em] text-ink-400 uppercase"> <dt className="font-display text-xs text-ink-400">You get</dt>
You get
</dt>
<dd aria-label="Ships and bay"> <dd aria-label="Ships and bay">
A {courier.name} for you, a {clipper.name} for {veteran.name}, and a bay at {bay.name} A {courier.name} for you, a {clipper.name} for {veteran.name}, and a bay at {bay.name}
</dd> </dd>
<dt className="font-display text-[11px] tracking-[0.14em] text-ink-400 uppercase">Pilot</dt> <dt className="font-display text-xs text-ink-400">Pilot</dt>
<dd aria-label="Veteran pilot"> <dd aria-label="Veteran pilot">
…and {veteran.name}. {veteran.bio} …and {veteran.name}. {veteran.bio}
</dd> </dd>
<dt className="font-display text-[11px] tracking-[0.14em] text-ink-400 uppercase">Loan</dt> <dt className="font-display text-xs text-ink-400">Loan</dt>
<dd <dd
aria-label="Loan amount" aria-label="Loan amount"
className="font-display font-semibold text-amber-200 tabular-nums" className="font-display font-semibold text-amber-200 tabular-nums"
> >
{formatCredits(dealLoanAmount)} {formatCredits(dealLoanAmount)}
</dd> </dd>
<dt className="font-display text-[11px] tracking-[0.14em] text-ink-400 uppercase"> <dt className="font-display text-xs text-ink-400">Repayment</dt>
Repayment
</dt>
<dd aria-label="Repayment share"> <dd aria-label="Repayment share">
{Math.round(loanRepaymentShare * 100)}% of every payout until it&apos;s paid off {Math.round(loanRepaymentShare * 100)}% of every payout until it&apos;s paid off
</dd> </dd>

View File

@ -63,7 +63,7 @@ export function FleetAsleep() {
if (!fleetQuiet(state)) return null; if (!fleetQuiet(state)) return null;
return ( return (
<Panel tone="amber" className="text-sm" aria-label="Fleet asleep"> <Panel tone="amber" className="text-sm" aria-label="Fleet asleep">
<p className="flex items-center gap-2 font-display font-semibold tracking-wide text-amber-100"> <p className="flex items-center gap-2 font-display font-semibold text-amber-100">
<Icon name="rest" className="size-4 shrink-0" /> <Icon name="rest" className="size-4 shrink-0" />
Fleet asleep — waiting for your orders Fleet asleep — waiting for your orders
</p> </p>

View File

@ -14,7 +14,7 @@ export function GoalsCard() {
if (next.length === 0) return null; if (next.length === 0) return null;
return ( return (
<Panel as="section" feature id="goals" aria-labelledby="goals-title"> <Panel as="section" id="goals" aria-labelledby="goals-title">
<h2 id="goals-title" className="flex items-center gap-2 font-display text-lg font-bold"> <h2 id="goals-title" className="flex items-center gap-2 font-display text-lg font-bold">
<Icon name="target" className="size-5 text-amber-300" /> <Icon name="target" className="size-5 text-amber-300" />
Goals Goals

View File

@ -22,7 +22,7 @@ export function DispatchOfficePanel() {
if (state.dispatchOffice.owned) { if (state.dispatchOffice.owned) {
return ( return (
<Panel tone="cyan" className="text-sm" aria-label="Dispatch Office"> <Panel tone="cyan" className="text-sm" aria-label="Dispatch Office">
<p className="flex items-center gap-2 font-display font-semibold tracking-wide text-cyan-100"> <p className="flex items-center gap-2 font-display font-semibold text-cyan-100">
<Icon name="send" className="size-4" /> <Icon name="send" className="size-4" />
Dispatch Office Dispatch Office
</p> </p>
@ -37,8 +37,8 @@ export function DispatchOfficePanel() {
); );
} }
return ( return (
<Panel tone="amber" feature className="text-sm" aria-label="Dispatch Office"> <Panel tone="amber" className="text-sm" aria-label="Dispatch Office">
<p className="font-display font-semibold tracking-wide text-amber-100">Dispatch Office</p> <p className="font-display font-semibold text-amber-100">Dispatch Office</p>
<p className="mt-1 text-ink-300"> <p className="mt-1 text-ink-300">
Lets your hired pilots run jobs on their own, so the company keeps earning while you&apos;re Lets your hired pilots run jobs on their own, so the company keeps earning while you&apos;re
away. away.
@ -100,9 +100,7 @@ export function PilotIndependence({ pilot }: { pilot: Pilot }) {
return ( return (
<div className="mt-3 rounded-lg bg-space-950/50 p-2.5 ring-1 ring-space-700"> <div className="mt-3 rounded-lg bg-space-950/50 p-2.5 ring-1 ring-space-700">
<fieldset> <fieldset>
<legend className="font-display text-[11px] tracking-[0.14em] text-ink-400 uppercase"> <legend className="font-display text-xs text-ink-400">Flies</legend>
Flies
</legend>
<div className="mt-1 flex overflow-hidden rounded-lg border border-space-600"> <div className="mt-1 flex overflow-hidden rounded-lg border border-space-600">
{(["manual", "independent"] as const).map((mode) => ( {(["manual", "independent"] as const).map((mode) => (
<label <label
@ -155,9 +153,7 @@ function OrdersEditor({ pilot }: { pilot: Pilot }) {
return ( return (
<details className="mt-2"> <details className="mt-2">
<summary className="cursor-pointer font-display text-xs tracking-wider text-amber-200 uppercase"> <summary className="cursor-pointer font-display text-xs text-amber-200">Orders</summary>
Orders
</summary>
<form <form
aria-label={`Orders for ${pilot.name}`} aria-label={`Orders for ${pilot.name}`}
className="mt-2 space-y-3 text-sm" className="mt-2 space-y-3 text-sm"

View File

@ -203,7 +203,7 @@ function PilotSection({ log, state, open }: { log: PilotLog; state: PlayerState;
return ( return (
<details open={open} className="rounded-xl border border-space-600 bg-space-950/50 p-3"> <details open={open} className="rounded-xl border border-space-600 bg-space-950/50 p-3">
<summary className="flex cursor-pointer items-baseline justify-between gap-2"> <summary className="flex cursor-pointer items-baseline justify-between gap-2">
<span className="font-display font-bold tracking-wide">{log.name}</span> <span className="font-display font-bold">{log.name}</span>
{log.xp > 0 && <span className="text-xs text-violet-300">+{log.xp} XP</span>} {log.xp > 0 && <span className="text-xs text-violet-300">+{log.xp} XP</span>}
</summary> </summary>
{log.now && ( {log.now && (

View File

@ -86,7 +86,7 @@ function CallBody({ call, state }: { call: RadioCall; state: PlayerState }) {
disabled={!available || answer.isPending} disabled={!available || answer.isPending}
onClick={() => answer.mutate({ dispatchId: call.dispatchId, optionId: option.id })} onClick={() => answer.mutate({ dispatchId: call.dispatchId, optionId: option.id })}
> >
<span className="font-display tracking-wide"> <span className="font-display">
{option.label} {option.label}
{option.id === call.defaultOptionId && " (if you don't answer)"} {option.id === call.defaultOptionId && " (if you don't answer)"}
</span> </span>
@ -151,7 +151,7 @@ export function RadioCallsPanel() {
if (state.radioCalls.length === 0) return null; if (state.radioCalls.length === 0) return null;
return ( return (
<Panel tone="amber" className="text-sm" aria-label="Radio calls"> <Panel tone="amber" className="text-sm" aria-label="Radio calls">
<p className="flex items-center gap-2 font-display font-semibold tracking-wide text-amber-100"> <p className="flex items-center gap-2 font-display font-semibold text-amber-100">
<Icon name="alert" className="size-4" /> <Icon name="alert" className="size-4" />
Radio calls ({state.radioCalls.length}) Radio calls ({state.radioCalls.length})
</p> </p>

View File

@ -77,7 +77,7 @@ function Stop({ id, size }: { id: string; size: "sm" | "md" }) {
{/* CSS-drawn label: the route is already in text nearby, so keep it out of the DOM. */} {/* CSS-drawn label: the route is already in text nearby, so keep it out of the DOM. */}
<span <span
data-label={locationName(id)} data-label={locationName(id)}
className="mt-0.5 max-w-full truncate font-display text-[10px] font-semibold tracking-wider text-ink-300 uppercase after:content-[attr(data-label)]" className="mt-0.5 max-w-full truncate font-display text-xs font-semibold text-ink-300 after:content-[attr(data-label)]"
/> />
</div> </div>
); );

View File

@ -275,7 +275,7 @@ export function ServiceSection({ shipId }: { shipId: string }) {
className="mt-4 rounded-xl border border-space-600 bg-space-900/60 p-3 text-sm" className="mt-4 rounded-xl border border-space-600 bg-space-900/60 p-3 text-sm"
> >
<div className="flex items-baseline justify-between gap-2"> <div className="flex items-baseline justify-between gap-2">
<span className="font-display font-bold tracking-wide">Service</span> <span className="font-display font-bold">Service</span>
{booking && ( {booking && (
<span className="text-ink-400">{booking.by === "auto" ? "Auto" : "Booked"}</span> <span className="text-ink-400">{booking.by === "auto" ? "Auto" : "Booked"}</span>
)} )}

View File

@ -136,7 +136,7 @@ export function Toasts() {
dropOverflow(); dropOverflow();
void navigate("/logbook"); void navigate("/logbook");
}} }}
className="pointer-events-auto self-end rounded-full border border-space-600 bg-space-900/95 px-3 py-1 font-display text-xs tracking-wide text-ink-200 shadow-panel" className="pointer-events-auto self-end rounded-full border border-space-600 bg-space-900/95 px-3 py-1 font-display text-xs text-ink-200 shadow-panel"
> >
+{more} more +{more} more
</button> </button>

View File

@ -87,17 +87,11 @@ export function WelcomeCards({
}} }}
className="w-full max-w-md overflow-hidden rounded-t-2xl border border-amber-400/50 bg-space-900/95 p-6 pb-[max(1.5rem,env(safe-area-inset-bottom))] shadow-panel sm:rounded-2xl" className="w-full max-w-md overflow-hidden rounded-t-2xl border border-amber-400/50 bg-space-900/95 p-6 pb-[max(1.5rem,env(safe-area-inset-bottom))] shadow-panel sm:rounded-2xl"
> >
<p <p aria-live="polite" className="font-display text-xs font-semibold text-cyan-300/80">
aria-live="polite"
className="font-display text-[11px] font-semibold tracking-[0.2em] text-cyan-300/80 uppercase"
>
{index + 1} of {cards.length} {index + 1} of {cards.length}
</p> </p>
<div key={index} data-testid="welcome-card" className={reduced ? "" : "animate-slide"}> <div key={index} data-testid="welcome-card" className={reduced ? "" : "animate-slide"}>
<h2 <h2 id="welcome-title" className="mt-2 font-display text-2xl font-bold text-amber-300">
id="welcome-title"
className="mt-2 font-display text-2xl font-bold tracking-wide text-amber-300"
>
{card.title} {card.title}
</h2> </h2>
<p className="mt-3 text-ink-200">{welcomeBody(card.body)}</p> <p className="mt-3 text-ink-200">{welcomeBody(card.body)}</p>

View File

@ -40,10 +40,68 @@
--color-problem: #ff8d7a; /* Something needs attention. */ --color-problem: #ff8d7a; /* Something needs attention. */
--color-on-problem: #2a0a05; --color-on-problem: #2a0a05;
--shadow-glow-amber: 0 0 0 1px rgb(251 191 36 / 0.45), 0 0 22px -4px rgb(251 191 36 / 0.6); /* Accent scales in the role hues: amber action, blue flight, mint earned, coral problem. */
--shadow-glow-cyan: 0 0 0 1px rgb(34 211 238 / 0.4), 0 0 22px -4px rgb(34 211 238 / 0.55); --color-amber-50: #fff8e8;
--shadow-glow-emerald: 0 0 0 1px rgb(52 211 153 / 0.4), 0 0 22px -4px rgb(52 211 153 / 0.55); --color-amber-100: #fff0d1;
--shadow-panel: inset 0 1px 0 0 rgb(255 255 255 / 0.05), 0 12px 32px -12px rgb(0 0 0 / 0.8); --color-amber-200: #ffe2a8;
--color-amber-300: #ffd27a;
--color-amber-400: #ffc04d;
--color-amber-500: #f5a623;
--color-amber-600: #c97f10;
--color-amber-700: #9c620f;
--color-amber-800: #7a4d10;
--color-amber-900: #5c3b10;
--color-amber-950: #2e1c04;
--color-cyan-50: #eef6ff;
--color-cyan-100: #dcedff;
--color-cyan-200: #bddcff;
--color-cyan-300: #93c9ff;
--color-cyan-400: #68aef5;
--color-cyan-500: #4690e0;
--color-cyan-600: #2f74c2;
--color-cyan-700: #2a5c98;
--color-cyan-800: #244a78;
--color-cyan-900: #1f3d61;
--color-cyan-950: #122540;
--color-sky-50: #eef6ff;
--color-sky-100: #dcedff;
--color-sky-200: #bddcff;
--color-sky-300: #93c9ff;
--color-sky-400: #68aef5;
--color-sky-500: #4690e0;
--color-sky-600: #2f74c2;
--color-sky-700: #2a5c98;
--color-sky-800: #244a78;
--color-sky-900: #1f3d61;
--color-sky-950: #122540;
--color-emerald-50: #eafbf3;
--color-emerald-100: #d0f6e4;
--color-emerald-200: #aaf0cf;
--color-emerald-300: #84e3b5;
--color-emerald-400: #5ed09a;
--color-emerald-500: #3db680;
--color-emerald-600: #2c9367;
--color-emerald-700: #267553;
--color-emerald-800: #215d44;
--color-emerald-900: #1c4c39;
--color-emerald-950: #0e2c21;
--color-rose-50: #fff2ef;
--color-rose-100: #ffe2db;
--color-rose-200: #ffc8bb;
--color-rose-300: #ffa896;
--color-rose-400: #ff8d7a;
--color-rose-500: #f56f5a;
--color-rose-600: #dc5340;
--color-rose-700: #b54232;
--color-rose-800: #8f372b;
--color-rose-900: #732f26;
--color-rose-950: #3f1610;
/* Selection rings, without the old neon blur. */
--shadow-glow-amber: 0 0 0 1px rgb(255 192 77 / 0.55);
--shadow-glow-cyan: 0 0 0 1px rgb(147 201 255 / 0.5);
--shadow-glow-emerald: 0 0 0 1px rgb(132 227 181 / 0.5);
--shadow-panel: 0 10px 30px -14px rgb(0 0 0 / 0.7);
--animate-rise: rise 0.35s cubic-bezier(0.2, 0.8, 0.2, 1) both; --animate-rise: rise 0.35s cubic-bezier(0.2, 0.8, 0.2, 1) both;
--animate-glow: glow 2.4s ease-in-out infinite; --animate-glow: glow 2.4s ease-in-out infinite;

View File

@ -33,10 +33,8 @@ export function Layout({
{/* The wordmark only fits beside the chips from 640 px; on phones the mark alone. */} {/* The wordmark only fits beside the chips from 640 px; on phones the mark alone. */}
<div className="flex min-w-0 items-center gap-2 overflow-hidden"> <div className="flex min-w-0 items-center gap-2 overflow-hidden">
<LogoMark /> <LogoMark />
<span className="hidden font-display text-lg font-bold tracking-[0.28em] sm:inline"> <span className="hidden font-display text-lg font-bold sm:inline">DEADHEAD</span>
DEADHEAD <span className="hidden font-display text-xs text-cyan-300/70 lg:inline">
</span>
<span className="hidden font-display text-[10px] tracking-[0.3em] text-cyan-300/70 uppercase lg:inline">
Space Freight Space Freight
</span> </span>
</div> </div>
@ -131,15 +129,13 @@ function NavTab({
<span <span
aria-hidden="true" aria-hidden="true"
data-testid={`badge-${label}`} data-testid={`badge-${label}`}
className="absolute -top-1 -right-1 grid h-4.5 min-w-4.5 animate-glow place-items-center rounded-full bg-rose-500 px-1 font-display text-[10px] font-bold text-white ring-2 ring-space-950" className="absolute -top-1 -right-1 grid h-4.5 min-w-4.5 animate-glow place-items-center rounded-full bg-rose-500 px-1 font-display text-xs font-bold text-white ring-2 ring-space-950"
> >
{badge > 9 ? "9+" : badge} {badge > 9 ? "9+" : badge}
</span> </span>
)} )}
</span> </span>
<span className="font-display text-[10px] font-semibold tracking-wide uppercase sm:text-[11px] sm:tracking-wider"> <span className="font-display text-xs font-semibold text-xs">{label}</span>
{label}
</span>
</> </>
)} )}
</NavLink> </NavLink>

View File

@ -166,7 +166,7 @@ export function MapCanvas({
{loading && ( {loading && (
<p <p
role="status" role="status"
className="absolute inset-x-0 top-4 text-center font-display text-sm tracking-widest text-ink-400 uppercase" className="absolute inset-x-0 top-4 text-center font-display text-sm text-ink-400"
> >
Loading map… Loading map…
</p> </p>

View File

@ -113,7 +113,7 @@ export function Dashboard() {
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<div className="space-y-4"> <div className="space-y-4">
{state.tutorial && ( {state.tutorial && (
<Panel feature tone="amber"> <Panel tone="amber">
<Eyebrow className="text-amber-300/80">Contract pilot</Eyebrow> <Eyebrow className="text-amber-300/80">Contract pilot</Eyebrow>
<h2 className="mt-0.5 font-display text-lg font-bold text-amber-200"> <h2 className="mt-0.5 font-display text-lg font-bold text-amber-200">
Flying for {content.tutorialCompanyName} Flying for {content.tutorialCompanyName}
@ -140,10 +140,7 @@ export function Dashboard() {
aria-labelledby="on-jobs-title" aria-labelledby="on-jobs-title"
tone={onJobs.length ? "cyan" : "default"} tone={onJobs.length ? "cyan" : "default"}
> >
<h2 <h2 id="on-jobs-title" className="font-display text-xs font-semibold text-cyan-300/80">
id="on-jobs-title"
className="font-display text-[11px] font-semibold tracking-[0.18em] text-cyan-300/80 uppercase"
>
Ships on jobs Ships on jobs
</h2> </h2>
{onJobs.length === 0 ? ( {onJobs.length === 0 ? (
@ -186,7 +183,7 @@ export function Dashboard() {
<Icon name="rest" className="size-5" /> <Icon name="rest" className="size-5" />
</span> </span>
<div className="min-w-0"> <div className="min-w-0">
<h2 className="font-display text-sm font-semibold tracking-wide text-ink-50"> <h2 className="font-display text-sm font-semibold text-ink-50">
Pilots resting: {resting.length} Pilots resting: {resting.length}
</h2> </h2>
{resting[0] ? ( {resting[0] ? (
@ -204,7 +201,7 @@ export function Dashboard() {
{inShop.length > 0 && ( {inShop.length > 0 && (
<Panel aria-label="In the shop" tone="amber"> <Panel aria-label="In the shop" tone="amber">
<h2 className="font-display text-sm font-semibold tracking-wide text-ink-50"> <h2 className="font-display text-sm font-semibold text-ink-50">
In the shop: {inShop.length} In the shop: {inShop.length}
</h2> </h2>
<ul className="mt-1 space-y-0.5 text-sm text-amber-100"> <ul className="mt-1 space-y-0.5 text-sm text-amber-100">
@ -227,7 +224,7 @@ export function Dashboard() {
<DispatchOfficePanel /> <DispatchOfficePanel />
{!state.tutorial && ( {!state.tutorial && (
<Panel className="text-sm" aria-label="Logbook"> <Panel className="text-sm" aria-label="Logbook">
<p className="flex items-center gap-2 font-display font-semibold tracking-wide text-ink-100"> <p className="flex items-center gap-2 font-display font-semibold text-ink-100">
<Icon name="route" className="size-4 text-cyan-300" /> <Icon name="route" className="size-4 text-cyan-300" />
Logbook Logbook
</p> </p>
@ -258,7 +255,7 @@ function YourShip({ className = "" }: { className?: string }) {
if (mine.kind === "none") { if (mine.kind === "none") {
return ( return (
<Panel as="section" aria-label="Your ship" feature className={className}> <Panel as="section" aria-label="Your ship" className={className}>
<Eyebrow>Your ship</Eyebrow> <Eyebrow>Your ship</Eyebrow>
<p className="mt-2 text-sm text-ink-200">You&apos;re not flying a ship right now.</p> <p className="mt-2 text-sm text-ink-200">You&apos;re not flying a ship right now.</p>
<Link to="/pilots" className={`mt-3 ${buttonClass("secondary", "sm")}`}> <Link to="/pilots" className={`mt-3 ${buttonClass("secondary", "sm")}`}>
@ -285,7 +282,6 @@ function YourShip({ className = "" }: { className?: string }) {
<Panel <Panel
as="section" as="section"
aria-label="Your ship" aria-label="Your ship"
feature
tone={dispatch ? "cyan" : "default"} tone={dispatch ? "cyan" : "default"}
className={`flex flex-col gap-3 ${className}`} className={`flex flex-col gap-3 ${className}`}
> >
@ -301,7 +297,7 @@ function YourShip({ className = "" }: { className?: string }) {
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<Eyebrow>Your ship</Eyebrow> <Eyebrow>Your ship</Eyebrow>
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<h2 className="truncate font-display text-xl font-bold tracking-wide">{name}</h2> <h2 className="truncate font-display text-xl font-bold">{name}</h2>
{dispatch ? ( {dispatch ? (
<Pill tone="cyan" icon="send"> <Pill tone="cyan" icon="send">
In flight In flight
@ -365,10 +361,8 @@ function YourShip({ className = "" }: { className?: string }) {
aria-label="Your ship low fuel" aria-label="Your ship low fuel"
className="mt-2 rounded-lg border border-rose-400/40 bg-rose-950/30 px-3 py-2 text-sm text-rose-200" className="mt-2 rounded-lg border border-rose-400/40 bg-rose-950/30 px-3 py-2 text-sm text-rose-200"
> >
<span className="font-display font-semibold tracking-wide uppercase"> <span className="font-display font-semibold">Low fuel</span> ·{" "}
Low fuel {locationName(low.destinationId)} needs {formatFu(low.fuelFu)}
</span>{" "}
· {locationName(low.destinationId)} needs {formatFu(low.fuelFu)}
</p> </p>
)} )}
</div> </div>
@ -440,7 +434,7 @@ function Alerts() {
<Panel as="section" aria-labelledby="alerts-title" tone="muted"> <Panel as="section" aria-labelledby="alerts-title" tone="muted">
<h2 <h2
id="alerts-title" id="alerts-title"
className="flex items-center gap-2 font-display text-sm font-semibold tracking-wide text-ink-50" className="flex items-center gap-2 font-display text-sm font-semibold text-ink-50"
> >
<Icon name="alert" className="size-4 text-amber-300" /> <Icon name="alert" className="size-4 text-amber-300" />
Alerts Alerts

View File

@ -113,7 +113,7 @@ export function DevTools() {
{state.pilots.length > 0 && ( {state.pilots.length > 0 && (
<fieldset className="space-y-2"> <fieldset className="space-y-2">
<legend className="text-xs tracking-wider text-ink-400 uppercase">Pilot</legend> <legend className="text-xs text-ink-400">Pilot</legend>
<select <select
aria-label="Pilot" aria-label="Pilot"
value={pilotId} value={pilotId}
@ -149,7 +149,7 @@ export function DevTools() {
{state.ships.length > 0 && ( {state.ships.length > 0 && (
<fieldset className="space-y-2"> <fieldset className="space-y-2">
<legend className="text-xs tracking-wider text-ink-400 uppercase">Ship</legend> <legend className="text-xs text-ink-400">Ship</legend>
<select <select
aria-label="Ship" aria-label="Ship"
value={shipId} value={shipId}

View File

@ -427,9 +427,7 @@ export function DispatchSheet({ contract, onClose }: { contract: Contract; onClo
/> />
<Art sprite={option.modelSprite} className="size-10 shrink-0" /> <Art sprite={option.modelSprite} className="size-10 shrink-0" />
<span className="min-w-0"> <span className="min-w-0">
<span className="block font-display font-bold tracking-wide"> <span className="block font-display font-bold">{option.shipName}</span>
{option.shipName}
</span>
<span className="block text-xs text-ink-400"> <span className="block text-xs text-ink-400">
{option.modelName} · {option.pilotName ?? "no pilot"} {option.modelName} · {option.pilotName ?? "no pilot"}
</span> </span>
@ -537,7 +535,7 @@ export function DispatchSheet({ contract, onClose }: { contract: Contract; onClo
function Cell({ label, value, good }: { label: string; value: string; good?: boolean }) { function Cell({ label, value, good }: { label: string; value: string; good?: boolean }) {
return ( return (
<div className="rounded-lg bg-space-950/60 px-2 py-1.5 ring-1 ring-space-700"> <div className="rounded-lg bg-space-950/60 px-2 py-1.5 ring-1 ring-space-700">
<dt className="font-display text-[10px] tracking-[0.14em] text-ink-400 uppercase">{label}</dt> <dt className="font-display text-xs text-ink-400">{label}</dt>
<dd <dd
className={`font-display text-sm font-semibold tabular-nums ${good ? "text-emerald-300" : "text-ink-50"}`} className={`font-display text-sm font-semibold tabular-nums ${good ? "text-emerald-300" : "text-ink-50"}`}
> >

View File

@ -118,9 +118,7 @@ function ShipRow(props: {
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<span className="truncate font-display text-lg font-bold tracking-wide"> <span className="truncate font-display text-lg font-bold">{ship.name}</span>
{ship.name}
</span>
{dispatch ? ( {dispatch ? (
<Pill tone="cyan" icon="send"> <Pill tone="cyan" icon="send">
In flight In flight
@ -170,7 +168,7 @@ function ShipRow(props: {
aria-label={`${ship.name} low fuel`} aria-label={`${ship.name} low fuel`}
className="flex items-center gap-2 rounded-lg border border-rose-400/40 bg-rose-950/30 px-3 py-2 text-sm text-rose-200" className="flex items-center gap-2 rounded-lg border border-rose-400/40 bg-rose-950/30 px-3 py-2 text-sm text-rose-200"
> >
<span className="font-display font-semibold tracking-wide uppercase">Low fuel</span> <span className="font-display font-semibold">Low fuel</span>
<span className="text-rose-200/80"> <span className="text-rose-200/80">
{locationName(low.destinationId)} needs {formatFu(low.fuelFu)} {locationName(low.destinationId)} needs {formatFu(low.fuelFu)}
</span> </span>
@ -383,9 +381,7 @@ export function DeadheadPicker({ ship, onClose }: { ship: Ship; onClose: () => v
{sprite && <Art sprite={sprite} className="size-10 shrink-0" />} {sprite && <Art sprite={sprite} className="size-10 shrink-0" />}
<span className="min-w-0 flex-1"> <span className="min-w-0 flex-1">
<span className="flex items-baseline justify-between gap-2"> <span className="flex items-baseline justify-between gap-2">
<span className="font-display font-bold tracking-wide"> <span className="font-display font-bold">{locationName(option.id)}</span>
{locationName(option.id)}
</span>
<span className="font-display text-sm text-ink-300 tabular-nums"> <span className="font-display text-sm text-ink-300 tabular-nums">
{formatCredits(option.fuelCost)} fuel used · {formatDuration(option.tripMs)} {formatCredits(option.fuelCost)} fuel used · {formatDuration(option.tripMs)}
</span> </span>
@ -425,7 +421,7 @@ export function HullGauge({ ship }: { ship: Ship }) {
const tone = pct < needsRepairBelowPct ? "rose" : pct < noDangerousBelowPct ? "amber" : "emerald"; const tone = pct < needsRepairBelowPct ? "rose" : pct < noDangerousBelowPct ? "amber" : "emerald";
return ( return (
<div> <div>
<div className="flex items-baseline justify-between font-display text-[11px] tracking-[0.14em] text-ink-400 uppercase"> <div className="flex items-baseline justify-between font-display text-xs text-ink-400">
<span>Hull</span> <span>Hull</span>
<span className="tracking-normal normal-case">{Math.round(pct)}%</span> <span className="tracking-normal normal-case">{Math.round(pct)}%</span>
</div> </div>
@ -436,7 +432,7 @@ export function HullGauge({ ship }: { ship: Ship }) {
aria-label={`${ship.name} needs repair`} aria-label={`${ship.name} needs repair`}
className="mt-2 flex items-center gap-2 rounded-lg border border-rose-400/40 bg-rose-950/30 px-3 py-2 text-sm text-rose-200" className="mt-2 flex items-center gap-2 rounded-lg border border-rose-400/40 bg-rose-950/30 px-3 py-2 text-sm text-rose-200"
> >
<span className="font-display font-semibold tracking-wide uppercase">Needs repair</span> <span className="font-display font-semibold">Needs repair</span>
<span className="text-rose-200/80">Repair it at an HQ</span> <span className="text-rose-200/80">Repair it at an HQ</span>
</p> </p>
)} )}

View File

@ -58,7 +58,7 @@ function HqCard({ bay }: { bay: Bay }) {
); );
return ( return (
<Panel as="li" feature tone="emerald" aria-label={`HQ at ${name}`} className="text-sm"> <Panel as="li" tone="emerald" aria-label={`HQ at ${name}`} className="text-sm">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{location && ( {location && (
<Art <Art
@ -67,7 +67,7 @@ function HqCard({ bay }: { bay: Bay }) {
/> />
)} )}
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<h2 className="font-display text-xl font-bold tracking-wide">{name}</h2> <h2 className="font-display text-xl font-bold">{name}</h2>
<p className="text-ink-300"> <p className="text-ink-300">
Fuel: <FuelPrice locationId={bay.locationId} /> Fuel: <FuelPrice locationId={bay.locationId} />
</p> </p>

View File

@ -116,7 +116,7 @@ export function LocationBoards({ onSelect }: { onSelect?: (contract: Contract) =
type="button" type="button"
aria-pressed={id === locationId} aria-pressed={id === locationId}
onClick={() => setParams({ location: id }, { replace: true })} onClick={() => setParams({ location: id }, { replace: true })}
className="flex shrink-0 items-center gap-2 rounded-full border border-space-600 bg-space-900/70 py-1 pr-3 pl-1 font-display text-sm font-semibold tracking-wide text-ink-300 backdrop-blur transition hover:border-space-500 hover:text-ink-50 aria-pressed:border-amber-400 aria-pressed:bg-amber-500/15 aria-pressed:text-amber-100 aria-pressed:shadow-glow-amber" className="flex shrink-0 items-center gap-2 rounded-full border border-space-600 bg-space-900/70 py-1 pr-3 pl-1 font-display text-sm font-semibold text-ink-300 backdrop-blur transition hover:border-space-500 hover:text-ink-50 aria-pressed:border-amber-400 aria-pressed:bg-amber-500/15 aria-pressed:text-amber-100 aria-pressed:shadow-glow-amber"
> >
{sprite ? <Art sprite={sprite} className="size-7" /> : null} {sprite ? <Art sprite={sprite} className="size-7" /> : null}
{locationName(id)} {locationName(id)}
@ -176,7 +176,7 @@ function LocationBoard({
const refresh = ( const refresh = (
<p <p
aria-label="Board refresh" aria-label="Board refresh"
className="flex items-center gap-1.5 font-display text-xs tracking-wider text-ink-400 uppercase" className="flex items-center gap-1.5 font-display text-xs text-ink-400"
> >
<Icon name="clock" className="size-3.5 text-cyan-300" /> <Icon name="clock" className="size-3.5 text-cyan-300" />
New contracts in{" "} New contracts in{" "}
@ -186,7 +186,7 @@ function LocationBoard({
{fresh.size > 0 && ( {fresh.size > 0 && (
<span <span
role="status" role="status"
className="ml-2 animate-rise rounded-full border border-cyan-400/50 bg-cyan-500/15 px-2 py-0.5 tracking-wider text-cyan-100 normal-case" className="ml-2 animate-rise rounded-full border border-cyan-400/50 bg-cyan-500/15 px-2 py-0.5 text-cyan-100 normal-case"
> >
New contracts posted New contracts posted
</span> </span>
@ -222,11 +222,10 @@ function LocationBoard({
as="li" as="li"
key={contract.id} key={contract.id}
tone={contract.firstContact ? "amber" : "default"} tone={contract.firstContact ? "amber" : "default"}
feature={contract.firstContact}
className={`flex flex-col gap-2 ${fresh.has(contract.id) ? "animate-rise" : ""}`} className={`flex flex-col gap-2 ${fresh.has(contract.id) ? "animate-rise" : ""}`}
> >
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<span className="font-display text-sm font-semibold tracking-wide text-ink-300"> <span className="font-display text-sm font-semibold text-ink-300">
{locationName(contract.originId)} → {locationName(contract.destinationId)} {locationName(contract.originId)} → {locationName(contract.destinationId)}
</span> </span>
<span className="font-display text-lg font-bold whitespace-nowrap text-emerald-300 tabular-nums"> <span className="font-display text-lg font-bold whitespace-nowrap text-emerald-300 tabular-nums">
@ -234,7 +233,7 @@ function LocationBoard({
</span> </span>
</div> </div>
<div> <div>
<h3 className="font-display text-lg leading-tight font-bold tracking-wide"> <h3 className="font-display text-lg leading-tight font-bold">
{goodName(contract)} {goodName(contract)}
</h3> </h3>
<div className="mt-1 flex flex-wrap items-center gap-1.5"> <div className="mt-1 flex flex-wrap items-center gap-1.5">

View File

@ -52,9 +52,7 @@ export function Locations() {
/> />
</div> </div>
<div> <div>
<div className="font-display text-lg font-bold tracking-wide text-ink-300"> <div className="font-display text-lg font-bold text-ink-300">{location.name}</div>
{location.name}
</div>
<div className="text-ink-400">Undiscovered — deliver here to unlock.</div> <div className="text-ink-400">Undiscovered — deliver here to unlock.</div>
</div> </div>
</Panel> </Panel>
@ -76,16 +74,14 @@ export function Locations() {
/> />
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<span className="font-display text-xl font-bold tracking-wide"> <span className="font-display text-xl font-bold">{location.name}</span>
{location.name}
</span>
{hasBay && ( {hasBay && (
<Pill tone="emerald" icon="hq"> <Pill tone="emerald" icon="hq">
HQ HQ
</Pill> </Pill>
)} )}
</div> </div>
<span className="font-display text-xs tracking-wider text-cyan-300/80"> <span className="font-display text-xs text-cyan-300/80">
{location.id === "earth" {location.id === "earth"
? "Home" ? "Home"
: `${formatMillionKm(distanceAt("earth", location.id))} from Earth`} : `${formatMillionKm(distanceAt("earth", location.id))} from Earth`}
@ -93,25 +89,15 @@ export function Locations() {
</div> </div>
</div> </div>
<dl className="mt-3 grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 rounded-lg bg-space-950/50 p-2.5 text-ink-200 ring-1 ring-space-700"> <dl className="mt-3 grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 rounded-lg bg-space-950/50 p-2.5 text-ink-200 ring-1 ring-space-700">
<dt className="font-display text-[11px] tracking-[0.14em] text-ink-400 uppercase"> <dt className="font-display text-xs text-ink-400">Type</dt>
Type
</dt>
<dd>{TYPE_LABELS[location.type]}</dd> <dd>{TYPE_LABELS[location.type]}</dd>
<dt className="font-display text-[11px] tracking-[0.14em] text-ink-400 uppercase"> <dt className="font-display text-xs text-ink-400">Landing</dt>
Landing
</dt>
<dd>{LANDING_LABELS[location.landingRequirement]}</dd> <dd>{LANDING_LABELS[location.landingRequirement]}</dd>
<dt className="font-display text-[11px] tracking-[0.14em] text-ink-400 uppercase"> <dt className="font-display text-xs text-ink-400">Produces</dt>
Produces
</dt>
<dd>{location.cargoClassMix.produces.map(cargoName).join(", ") || "—"}</dd> <dd>{location.cargoClassMix.produces.map(cargoName).join(", ") || "—"}</dd>
<dt className="font-display text-[11px] tracking-[0.14em] text-ink-400 uppercase"> <dt className="font-display text-xs text-ink-400">Demands</dt>
Demands
</dt>
<dd>{location.cargoClassMix.demands.map(cargoName).join(", ") || "—"}</dd> <dd>{location.cargoClassMix.demands.map(cargoName).join(", ") || "—"}</dd>
<dt className="font-display text-[11px] tracking-[0.14em] text-ink-400 uppercase"> <dt className="font-display text-xs text-ink-400">Fuel</dt>
Fuel
</dt>
<dd aria-label={`Fuel at ${location.name}`}> <dd aria-label={`Fuel at ${location.name}`}>
<FuelPrice locationId={location.id} /> <FuelPrice locationId={location.id} />
</dd> </dd>

View File

@ -74,7 +74,7 @@ function Panel({
{art} {art}
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-baseline justify-between gap-2"> <div className="flex items-baseline justify-between gap-2">
<h2 className="font-display text-lg font-bold tracking-wide">{title}</h2> <h2 className="font-display text-lg font-bold">{title}</h2>
<button <button
type="button" type="button"
onClick={onClose} onClick={onClose}

View File

@ -52,7 +52,7 @@ export function Market() {
{MODELS.map((model) => { {MODELS.map((model) => {
const affordable = state.company.credits >= model.price; const affordable = state.company.credits >= model.price;
return ( return (
<Panel as="li" key={model.id} feature={affordable} className="flex flex-col text-sm"> <Panel as="li" key={model.id} className="flex flex-col text-sm">
<div className="relative -mx-4 -mt-4 grid h-40 place-items-center overflow-hidden rounded-t-xl bg-radial from-cyan-400/20 via-space-850/40 to-transparent to-75%"> <div className="relative -mx-4 -mt-4 grid h-40 place-items-center overflow-hidden rounded-t-xl bg-radial from-cyan-400/20 via-space-850/40 to-transparent to-75%">
<div className="absolute inset-x-8 bottom-4 h-3 rounded-[50%] bg-cyan-300/15 blur-md" /> <div className="absolute inset-x-8 bottom-4 h-3 rounded-[50%] bg-cyan-300/15 blur-md" />
<Art <Art
@ -61,7 +61,7 @@ export function Market() {
/> />
</div> </div>
<div className="mt-2 flex items-baseline justify-between gap-2"> <div className="mt-2 flex items-baseline justify-between gap-2">
<span className="font-display text-xl font-bold tracking-wide">{model.name}</span> <span className="font-display text-xl font-bold">{model.name}</span>
<span <span
className={`font-display text-base font-bold tabular-nums ${affordable ? "text-amber-300" : "text-ink-400"}`} className={`font-display text-base font-bold tabular-nums ${affordable ? "text-amber-300" : "text-ink-400"}`}
> >
@ -124,7 +124,7 @@ function Spec(props: { icon: IconName; label: string; value: string; share?: num
return ( return (
<div> <div>
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<dt className="flex items-center gap-1.5 font-display text-[11px] tracking-[0.14em] text-ink-400 uppercase"> <dt className="flex items-center gap-1.5 font-display text-xs text-ink-400">
<Icon name={props.icon} className="size-3.5" /> <Icon name={props.icon} className="size-3.5" />
{props.label} {props.label}
</dt> </dt>
@ -162,7 +162,7 @@ function Reach({ model }: { model: ShipModel }) {
if (parts.length === 0) return null; if (parts.length === 0) return null;
return ( return (
<p aria-label="Reach" className="mt-3 text-xs text-ink-300"> <p aria-label="Reach" className="mt-3 text-xs text-ink-300">
<span className="font-display tracking-[0.14em] text-ink-400 uppercase">Reach </span> <span className="font-display text-ink-400">Reach </span>
{parts.join(" · ")} {parts.join(" · ")}
</p> </p>
); );

View File

@ -53,7 +53,7 @@ function HirePilot() {
const hasBay = state.bays.some((b) => b.locationId === bay); const hasBay = state.bays.some((b) => b.locationId === bay);
return ( return (
<Panel feature tone="amber" className="text-sm"> <Panel tone="amber" className="text-sm">
{state.bays.length === 0 ? ( {state.bays.length === 0 ? (
<p className="text-ink-300">Lease a bay to hire pilots there.</p> <p className="text-ink-300">Lease a bay to hire pilots there.</p>
) : ( ) : (
@ -62,9 +62,7 @@ function HirePilot() {
<Icon name="hire" className="size-5" /> <Icon name="hire" className="size-5" />
</span> </span>
<label className="flex items-center gap-2"> <label className="flex items-center gap-2">
<span className="font-display text-xs tracking-wider text-ink-300 uppercase"> <span className="font-display text-xs text-ink-300">Hire at</span>
Hire at
</span>
<select <select
aria-label="Hire at" aria-label="Hire at"
value={bay} value={bay}
@ -121,17 +119,17 @@ function PilotCard({ pilot }: { pilot: Pilot }) {
}`} }`}
> >
{initials} {initials}
<span className="absolute -right-1 -bottom-1 grid size-6 place-items-center rounded-full bg-space-950 font-display text-[11px] text-ink-50 ring-2 ring-space-600"> <span className="absolute -right-1 -bottom-1 grid size-6 place-items-center rounded-full bg-space-950 font-display text-xs text-ink-50 ring-2 ring-space-600">
{pilot.level} {pilot.level}
</span> </span>
</span> </span>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-baseline justify-between gap-2"> <div className="flex items-baseline justify-between gap-2">
<span className="truncate font-display text-lg font-bold tracking-wide"> <span className="truncate font-display text-lg font-bold">
{pilot.name} {pilot.name}
{pilot.isAvatar && <span className="ml-1 text-xs text-amber-300">(you)</span>} {pilot.isAvatar && <span className="ml-1 text-xs text-amber-300">(you)</span>}
</span> </span>
<span className="font-display text-xs tracking-wider whitespace-nowrap text-ink-300 uppercase"> <span className="font-display text-xs whitespace-nowrap text-ink-300">
Level {pilot.level} Level {pilot.level}
</span> </span>
</div> </div>
@ -172,7 +170,7 @@ function PilotCard({ pilot }: { pilot: Pilot }) {
<div className="mt-3 rounded-lg bg-space-950/50 p-2.5 ring-1 ring-space-700"> <div className="mt-3 rounded-lg bg-space-950/50 p-2.5 ring-1 ring-space-700">
<div <div
className={`font-display text-[11px] tracking-[0.14em] uppercase ${pilot.skillPointsAvailable > 0 ? "text-amber-300" : "text-ink-400"}`} className={`font-display text-xs ${pilot.skillPointsAvailable > 0 ? "text-amber-300" : "text-ink-400"}`}
> >
Skill points:{" "} Skill points:{" "}
<span aria-label={`${pilot.name} skill points`}>{pilot.skillPointsAvailable}</span> <span aria-label={`${pilot.name} skill points`}>{pilot.skillPointsAvailable}</span>
@ -268,7 +266,7 @@ function Duty({ pilot, canRest }: { pilot: Pilot; canRest: boolean }) {
const used = Math.min(pilot.dutyHours, pilot.dutyLimit); const used = Math.min(pilot.dutyHours, pilot.dutyLimit);
return ( return (
<div className="mt-3"> <div className="mt-3">
<div className="flex items-baseline justify-between font-display text-[11px] tracking-[0.14em] text-ink-400 uppercase"> <div className="flex items-baseline justify-between font-display text-xs text-ink-400">
<span>Duty</span> <span>Duty</span>
<span className="tracking-normal normal-case"> <span className="tracking-normal normal-case">
{formatGameHours(pilot.dutyHours)} of {formatGameHours(pilot.dutyLimit)} {formatGameHours(pilot.dutyHours)} of {formatGameHours(pilot.dutyLimit)}

View File

@ -118,7 +118,7 @@ function Section({ title, children }: { title: string; children: ReactNode }) {
const id = `settings-${title.toLowerCase()}`; const id = `settings-${title.toLowerCase()}`;
return ( return (
<Panel as="section" aria-labelledby={id} className="space-y-3"> <Panel as="section" aria-labelledby={id} className="space-y-3">
<h2 id={id} className="font-display text-sm font-bold tracking-wider text-ink-300 uppercase"> <h2 id={id} className="font-display text-sm font-bold text-ink-300">
{title} {title}
</h2> </h2>
{children} {children}

View File

@ -86,7 +86,7 @@ function HullRepair({ shipId, atHq }: { shipId: string; atHq: boolean }) {
className="mt-4 rounded-xl border border-space-600 bg-space-900/60 p-3 text-sm" className="mt-4 rounded-xl border border-space-600 bg-space-900/60 p-3 text-sm"
> >
<div className="flex items-baseline justify-between gap-2"> <div className="flex items-baseline justify-between gap-2">
<span className="font-display font-bold tracking-wide">Hull</span> <span className="font-display font-bold">Hull</span>
<span className="tabular-nums">{Math.round(pct)}%</span> <span className="tabular-nums">{Math.round(pct)}%</span>
</div> </div>
{pct >= 100 ? ( {pct >= 100 ? (
@ -143,9 +143,7 @@ export function ShipUpgrades({ shipId, onClose }: { shipId: string; onClose: ()
<HullRepair shipId={ship.id} atHq={atHq} /> <HullRepair shipId={ship.id} atHq={atHq} />
{/* Booked from anywhere; the work starts at the HQ (CR-027). */} {/* Booked from anywhere; the work starts at the HQ (CR-027). */}
{state.tutorial === null && <ServiceSection shipId={ship.id} />} {state.tutorial === null && <ServiceSection shipId={ship.id} />}
<h3 className="mt-4 font-display text-sm font-bold tracking-wider text-ink-300 uppercase"> <h3 className="mt-4 font-display text-sm font-bold text-ink-300">Upgrades</h3>
Upgrades
</h3>
<ul className="mt-2 space-y-2"> <ul className="mt-2 space-y-2">
{content.upgrades.map((upgrade) => { {content.upgrades.map((upgrade) => {
const level = ship.upgrades[upgrade.id] ?? 0; const level = ship.upgrades[upgrade.id] ?? 0;
@ -176,7 +174,7 @@ export function ShipUpgrades({ shipId, onClose }: { shipId: string; onClose: ()
className="rounded-xl border border-space-600 bg-space-900/60 p-3 text-sm" className="rounded-xl border border-space-600 bg-space-900/60 p-3 text-sm"
> >
<div className="flex items-baseline justify-between gap-2"> <div className="flex items-baseline justify-between gap-2">
<span className="font-display font-bold tracking-wide">{upgrade.name}</span> <span className="font-display font-bold">{upgrade.name}</span>
<span className="text-ink-400"> <span className="text-ink-400">
Level {level}/{max} Level {level}/{max}
</span> </span>

View File

@ -39,12 +39,10 @@ export function StartScreen() {
</div> </div>
<div className="relative animate-rise"> <div className="relative animate-rise">
<h1 className="bg-linear-to-b from-amber-100 via-amber-300 to-amber-600 bg-clip-text font-display text-5xl font-bold tracking-[0.2em] text-transparent drop-shadow-[0_0_24px_rgb(251_191_36/0.35)] sm:text-7xl"> <h1 className="bg-linear-to-b from-amber-100 via-amber-300 to-amber-600 bg-clip-text font-title text-3xl sm:text-5xl text-transparent drop-shadow-[0_0_24px_rgb(251_191_36/0.35)] sm:text-7xl">
DEADHEAD DEADHEAD
</h1> </h1>
<p className="mt-2 font-display text-sm tracking-[0.45em] text-cyan-300/80 uppercase"> <p className="mt-2 font-display text-sm text-cyan-300/80">Space Freight Tycoon</p>
Space Freight Tycoon
</p>
</div> </div>
<p className="relative max-w-sm animate-rise text-base text-ink-200"> <p className="relative max-w-sm animate-rise text-base text-ink-200">
Fly company runs between Earth and the Moon, save up for your own ship, and build a freight Fly company runs between Earth and the Moon, save up for your own ship, and build a freight

View File

@ -46,7 +46,7 @@ export function TutorialBoard() {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{firstRun && !introDismissed && ( {firstRun && !introDismissed && (
<Panel aria-label="Intro" feature tone="amber" className="flex items-center gap-3 text-sm"> <Panel aria-label="Intro" tone="amber" className="flex items-center gap-3 text-sm">
<Art sprite={loaner.sprite} className="size-16 shrink-0 -rotate-12" /> <Art sprite={loaner.sprite} className="size-16 shrink-0 -rotate-12" />
<div> <div>
<p className="font-display text-base font-bold text-amber-200"> <p className="font-display text-base font-bold text-amber-200">
@ -106,11 +106,10 @@ export function TutorialBoard() {
data-suggested={suggested || undefined} data-suggested={suggested || undefined}
data-scripted={scripted ? true : undefined} data-scripted={scripted ? true : undefined}
tone={scripted ? "fuchsia" : suggested ? "amber" : "default"} tone={scripted ? "fuchsia" : suggested ? "amber" : "default"}
feature={Boolean(scripted) || suggested}
className={`flex flex-col gap-2 ${suggested ? "shadow-glow-amber" : ""}`} className={`flex flex-col gap-2 ${suggested ? "shadow-glow-amber" : ""}`}
> >
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<span className="font-display font-bold tracking-wide"> <span className="font-display font-bold">
{scripted ? ( {scripted ? (
<span className="text-fuchsia-200">{scripted.name}</span> <span className="text-fuchsia-200">{scripted.name}</span>
) : ( ) : (
@ -143,9 +142,7 @@ export function TutorialBoard() {
<div className="mt-auto flex items-end justify-between gap-2 border-t border-space-700/70 pt-2"> <div className="mt-auto flex items-end justify-between gap-2 border-t border-space-700/70 pt-2">
<dl className="grid flex-1 grid-cols-3 gap-2 text-sm"> <dl className="grid flex-1 grid-cols-3 gap-2 text-sm">
<div> <div>
<dt className="font-display text-[10px] tracking-[0.16em] text-ink-400 uppercase"> <dt className="font-display text-xs text-ink-400">Pay</dt>
Pay
</dt>
<dd <dd
aria-label="Pay" aria-label="Pay"
className="font-display text-base font-bold text-emerald-300 tabular-nums" className="font-display text-base font-bold text-emerald-300 tabular-nums"
@ -154,17 +151,13 @@ export function TutorialBoard() {
</dd> </dd>
</div> </div>
<div> <div>
<dt className="font-display text-[10px] tracking-[0.16em] text-ink-400 uppercase"> <dt className="font-display text-xs text-ink-400">Time</dt>
Time
</dt>
<dd aria-label="Trip time" className="font-display text-base tabular-nums"> <dd aria-label="Trip time" className="font-display text-base tabular-nums">
{formatDuration(tripMs)} {formatDuration(tripMs)}
</dd> </dd>
</div> </div>
<div> <div>
<dt className="font-display text-[10px] tracking-[0.16em] text-ink-400 uppercase"> <dt className="font-display text-xs text-ink-400">XP</dt>
XP
</dt>
<dd <dd
aria-label="XP" aria-label="XP"
className="font-display text-base text-violet-300 tabular-nums" className="font-display text-base text-violet-300 tabular-nums"

View File

@ -4,19 +4,16 @@ export type ButtonVariant = "primary" | "secondary" | "ghost" | "success" | "dan
export type ButtonSize = "sm" | "md" | "lg"; export type ButtonSize = "sm" | "md" | "lg";
const VARIANTS: Record<ButtonVariant, string> = { const VARIANTS: Record<ButtonVariant, string> = {
primary: primary: "bg-action text-on-action hover:brightness-110",
"bg-linear-to-b from-amber-300 to-amber-500 text-space-950 shadow-glow-amber hover:from-amber-200 hover:to-amber-400", secondary: "border border-glass-line bg-white/6 text-ink-50 hover:bg-white/12",
secondary: ghost: "text-ink-300 hover:bg-white/6 hover:text-ink-50",
"border border-space-500 bg-space-800/80 text-ink-50 hover:border-amber-400/60 hover:bg-space-700", success: "bg-earned text-space-950 hover:brightness-110",
ghost: "text-ink-300 hover:bg-white/5 hover:text-ink-50", danger: "border border-problem/60 bg-problem/12 text-problem hover:bg-problem/20",
success:
"bg-linear-to-b from-emerald-300 to-emerald-500 text-space-950 shadow-glow-emerald hover:from-emerald-200 hover:to-emerald-400",
danger: "border border-rose-500/60 bg-rose-500/15 text-rose-200 hover:bg-rose-500/25",
}; };
const SIZES: Record<ButtonSize, string> = { const SIZES: Record<ButtonSize, string> = {
sm: "min-h-9 px-3 text-xs", sm: "min-h-11 px-3 text-sm",
md: "min-h-11 px-4 text-sm", md: "min-h-12 px-4 text-[15px]",
lg: "min-h-13 px-6 text-base", lg: "min-h-13 px-6 text-base",
}; };
@ -27,7 +24,7 @@ export function buttonClass(
block = false, block = false,
): string { ): string {
return [ return [
"inline-flex items-center justify-center gap-2 rounded-lg font-display font-semibold uppercase tracking-wider", "inline-flex items-center justify-center gap-2 rounded-xl text-center leading-tight font-semibold",
"transition duration-150 select-none active:translate-y-px active:scale-[0.98]", "transition duration-150 select-none active:translate-y-px active:scale-[0.98]",
"disabled:pointer-events-none disabled:opacity-40 disabled:shadow-none disabled:saturate-50", "disabled:pointer-events-none disabled:opacity-40 disabled:shadow-none disabled:saturate-50",
VARIANTS[variant], VARIANTS[variant],

View File

@ -3,72 +3,37 @@ import type { ElementType, HTMLAttributes, ReactNode } from "react";
export type Tone = "default" | "amber" | "cyan" | "emerald" | "rose" | "fuchsia" | "muted"; export type Tone = "default" | "amber" | "cyan" | "emerald" | "rose" | "fuchsia" | "muted";
const TONES: Record<Tone, string> = { const TONES: Record<Tone, string> = {
default: "border-space-600/70 bg-space-900/75", default: "border-glass-line bg-white/5",
muted: "border-space-700/70 bg-space-900/45", muted: "border-glass-line bg-transparent",
amber: "border-amber-400/45 bg-amber-950/40", amber: "border-action/45 bg-action/8",
cyan: "border-cyan-400/40 bg-cyan-950/40", cyan: "border-flight/35 bg-flight/7",
emerald: "border-emerald-400/40 bg-emerald-950/40", emerald: "border-earned/35 bg-earned/7",
rose: "border-rose-500/45 bg-rose-950/40", rose: "border-problem/45 bg-problem/9",
fuchsia: "border-fuchsia-400/50 bg-fuchsia-950/40", fuchsia: "border-fuchsia-400/45 bg-fuchsia-400/8",
};
const BRACKETS: Record<Tone, string> = {
default: "border-cyan-300/60",
muted: "border-ink-500",
amber: "border-amber-300",
cyan: "border-cyan-300",
emerald: "border-emerald-300",
rose: "border-rose-300",
fuchsia: "border-fuchsia-300",
}; };
export interface PanelProps extends HTMLAttributes<HTMLElement> { export interface PanelProps extends HTMLAttributes<HTMLElement> {
as?: ElementType; as?: ElementType;
tone?: Tone; tone?: Tone;
/** Corner brackets, for panels that should stand out. */
feature?: boolean;
children?: ReactNode; children?: ReactNode;
} }
/** A translucent HUD panel (decision 0.5.47). */ /** A card inside a glass panel (decision 0.5.165). */
export function Panel({ export function Panel({
as: Tag = "div", as: Tag = "div",
tone = "default", tone = "default",
feature = false,
className = "", className = "",
children, children,
...rest ...rest
}: PanelProps) { }: PanelProps) {
return ( return (
<Tag <Tag className={`relative rounded-2xl border p-4 ${TONES[tone]} ${className}`} {...rest}>
className={`relative rounded-xl border p-4 shadow-panel backdrop-blur-md ${TONES[tone]} ${className}`}
{...rest}
>
{feature && <Brackets tone={tone} />}
{children} {children}
</Tag> </Tag>
); );
} }
function Brackets({ tone }: { tone: Tone }) { /** A small label above a value or section. */
const c = `pointer-events-none absolute size-3 ${BRACKETS[tone]}`;
return (
<span aria-hidden="true">
<span className={`${c} -top-px -left-px rounded-tl-xl border-t-2 border-l-2`} />
<span className={`${c} -top-px -right-px rounded-tr-xl border-t-2 border-r-2`} />
<span className={`${c} -bottom-px -left-px rounded-bl-xl border-b-2 border-l-2`} />
<span className={`${c} -right-px -bottom-px rounded-br-xl border-r-2 border-b-2`} />
</span>
);
}
/** A small uppercase label above a value or section. */
export function Eyebrow({ children, className = "" }: { children: ReactNode; className?: string }) { export function Eyebrow({ children, className = "" }: { children: ReactNode; className?: string }) {
return ( return <div className={`text-xs font-semibold text-ink-400 ${className}`}>{children}</div>;
<div
className={`font-display text-[11px] font-semibold tracking-[0.18em] text-ink-400 uppercase ${className}`}
>
{children}
</div>
);
} }

View File

@ -40,26 +40,22 @@ export function Sheet({
}) { }) {
return createPortal( return createPortal(
<div <div
className={`fixed inset-x-0 top-14 bottom-0 ${layer} flex items-end justify-center bg-space-950/75 backdrop-blur-sm sm:items-center sm:p-4`} className={`fixed inset-x-0 top-14 bottom-0 ${layer} flex items-end justify-center bg-space-950/70 sm:items-center sm:p-4`}
> >
<div <div
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-labelledby={titleId} aria-labelledby={titleId}
className={`relative max-h-full w-full max-w-md animate-sheet overflow-y-auto rounded-t-2xl border bg-space-900/95 p-5 pb-[max(1.25rem,env(safe-area-inset-bottom))] shadow-panel sm:rounded-2xl ${BORDER_TONES[tone] ?? "border-space-600"}`} className={`relative max-h-full w-full max-w-md animate-sheet overflow-y-auto rounded-t-3xl border bg-space-850 p-5 pb-[max(1.25rem,env(safe-area-inset-bottom))] shadow-panel sm:rounded-3xl ${BORDER_TONES[tone] ?? "border-glass-line"}`}
> >
<div <div
aria-hidden="true" aria-hidden="true"
className="mx-auto -mt-2 mb-3 h-1 w-10 rounded-full bg-space-600 sm:hidden" className="mx-auto -mt-2 mb-3 h-1 w-10 rounded-full bg-white/25 sm:hidden"
/> />
{eyebrow && ( {eyebrow && <div className="text-xs font-semibold text-ink-400">{eyebrow}</div>}
<div className="font-display text-[11px] font-semibold tracking-[0.2em] text-cyan-300/80 uppercase">
{eyebrow}
</div>
)}
<h2 <h2
id={titleId} id={titleId}
className={`font-display text-xl font-bold tracking-wide ${TITLE_TONES[tone] ?? "text-ink-50"}`} className={`font-title text-lg leading-snug ${TITLE_TONES[tone] ?? "text-ink-50"}`}
> >
{title} {title}
</h2> </h2>

View File

@ -4,12 +4,12 @@ import { Icon, type IconName } from "./icons";
import type { Tone } from "./Panel"; import type { Tone } from "./Panel";
const PILL_TONES: Record<Tone, string> = { const PILL_TONES: Record<Tone, string> = {
default: "border-space-500 bg-space-800 text-ink-200", default: "border-glass-line bg-white/6 text-ink-200",
muted: "border-space-600 bg-space-900 text-ink-400", muted: "border-glass-line bg-transparent text-ink-400",
amber: "border-amber-400/50 bg-amber-500/15 text-amber-200", amber: "border-action/50 bg-action/12 text-action",
cyan: "border-cyan-400/50 bg-cyan-500/15 text-cyan-200", cyan: "border-flight/45 bg-flight/12 text-flight",
emerald: "border-emerald-400/50 bg-emerald-500/15 text-emerald-200", emerald: "border-earned/45 bg-earned/12 text-earned",
rose: "border-rose-400/50 bg-rose-500/15 text-rose-200", rose: "border-problem/50 bg-problem/12 text-problem",
fuchsia: "border-fuchsia-400/50 bg-fuchsia-500/15 text-fuchsia-200", fuchsia: "border-fuchsia-400/50 bg-fuchsia-500/15 text-fuchsia-200",
}; };
@ -27,7 +27,7 @@ export function Pill({
}) { }) {
return ( return (
<span <span
className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 font-display text-[11px] font-semibold tracking-wider whitespace-nowrap uppercase ${PILL_TONES[tone]} ${className}`} className={`inline-flex items-center gap-1 rounded-lg border px-2 py-0.5 text-xs font-semibold whitespace-nowrap ${PILL_TONES[tone]} ${className}`}
> >
{icon && <Icon name={icon} className="size-3" />} {icon && <Icon name={icon} className="size-3" />}
{children} {children}
@ -36,11 +36,12 @@ export function Pill({
} }
const METER_FILLS: Partial<Record<Tone, string>> = { const METER_FILLS: Partial<Record<Tone, string>> = {
amber: "from-amber-500 to-amber-300 shadow-[0_0_10px] shadow-amber-400/60", amber: "bg-action",
cyan: "from-cyan-500 to-sky-300 shadow-[0_0_10px] shadow-cyan-400/60", cyan: "bg-flight",
emerald: "from-emerald-500 to-emerald-300 shadow-[0_0_10px] shadow-emerald-400/60", emerald: "bg-earned",
rose: "from-rose-600 to-rose-400 shadow-[0_0_10px] shadow-rose-500/60", rose: "bg-problem",
fuchsia: "from-fuchsia-500 to-fuchsia-300", fuchsia: "bg-fuchsia-300",
default: "bg-ink-300",
}; };
/** A labelled `progressbar`. `label` is its accessible name. */ /** A labelled `progressbar`. `label` is its accessible name. */
@ -69,10 +70,10 @@ export function Meter({
aria-valuemin={min} aria-valuemin={min}
aria-valuemax={max} aria-valuemax={max}
aria-valuenow={value} aria-valuenow={value}
className={`overflow-hidden rounded-full bg-space-950/80 ring-1 ring-space-600/60 ${size === "sm" ? "h-1.5" : "h-2.5"} ${className}`} className={`overflow-hidden rounded-full bg-white/12 ${size === "sm" ? "h-1" : "h-1.5"} ${className}`}
> >
<div <div
className={`h-full rounded-full bg-linear-to-r transition-[width] duration-700 ${METER_FILLS[tone] ?? METER_FILLS.cyan}`} className={`h-full rounded-full transition-[width] duration-700 ${METER_FILLS[tone] ?? METER_FILLS.cyan}`}
style={{ width: `${percent}%` }} style={{ width: `${percent}%` }}
/> />
</div> </div>
@ -104,16 +105,14 @@ export function StatTile({
className?: string; className?: string;
}) { }) {
return ( return (
<div <div className={`rounded-2xl border border-glass-line bg-white/5 px-3 py-2.5 ${className}`}>
className={`rounded-xl border border-space-600/70 bg-space-900/70 px-3 py-2.5 shadow-panel backdrop-blur-md ${className}`} <div className="flex items-center gap-1.5 text-xs font-semibold text-ink-400">
>
<div className="flex items-center gap-1.5 font-display text-[11px] font-semibold tracking-[0.16em] text-ink-400 uppercase">
{icon && <Icon name={icon} className="size-3.5" />} {icon && <Icon name={icon} className="size-3.5" />}
{label} {label}
</div> </div>
<div <div
aria-label={valueLabel} aria-label={valueLabel}
className={`mt-1 font-display text-lg font-bold tabular-nums ${STAT_VALUE_TONES[tone] ?? "text-ink-50"}`} className={`mt-1 text-lg font-bold tabular-nums ${STAT_VALUE_TONES[tone] ?? "text-ink-50"}`}
> >
{value} {value}
</div> </div>
@ -138,20 +137,10 @@ export function ScreenHeader({
return ( return (
<div className="flex items-end justify-between gap-3"> <div className="flex items-end justify-between gap-3">
<div className="flex min-w-0 items-center gap-3"> <div className="flex min-w-0 items-center gap-3">
{icon && ( {icon && <Icon name={icon} className="size-6 shrink-0 text-ink-300" />}
<span className="grid size-11 shrink-0 place-items-center rounded-xl border border-amber-400/40 bg-amber-500/10 text-amber-300 shadow-glow-amber">
<Icon name={icon} className="size-6" />
</span>
)}
<div className="min-w-0"> <div className="min-w-0">
{eyebrow && ( {eyebrow && <div className="text-xs font-semibold text-ink-400">{eyebrow}</div>}
<div className="font-display text-[11px] font-semibold tracking-[0.22em] text-cyan-300/80 uppercase"> <h1 className="font-title text-xl leading-tight">{title}</h1>
{eyebrow}
</div>
)}
<h1 className="font-display text-2xl leading-tight font-bold tracking-wide uppercase">
{title}
</h1>
{subtitle && <p className="text-sm text-ink-300">{subtitle}</p>} {subtitle && <p className="text-sm text-ink-300">{subtitle}</p>}
</div> </div>
</div> </div>